1use std::collections::HashMap;
2
3use haste_codegen::{
4 traversal,
5 utilities::{self, conditionals::is_typechoice, extract::Max},
6};
7use haste_fhir_model::r4::generated::{
8 resources::StructureDefinition, terminology::IssueType, types::ElementDefinition,
9};
10use haste_fhir_operation_error::OperationOutcomeError;
11use serde_json::json;
12
13#[derive(serde::Serialize, serde::Deserialize, Debug)]
14#[serde(rename_all = "lowercase")]
15enum JSONSchemaType {
16 Object,
17 Boolean,
18 String,
19 Number,
20 Array,
21}
22
23#[allow(dead_code)]
24struct JSONSchema {}
25
26struct Processed {
27 cardinality: (u64, Max),
28 field: String,
29 schema: serde_json::Value,
30}
31
32static PRIMITIVE_TYPES: &[&str] = &[
33 "http://hl7.org/fhirpath/System.String",
34 "http://hl7.org/fhirpath/System.Time",
35 "http://hl7.org/fhirpath/System.Date",
36 "http://hl7.org/fhirpath/System.DateTime",
37 "http://hl7.org/fhirpath/System.Instant",
38 "xhtml",
39 "markdown",
40 "url",
41 "canonical",
42 "uuid",
43 "string",
44 "uri",
45 "code",
46 "id",
47 "oid",
48 "base64Binary",
49 "time",
50 "date",
51 "dateTime",
52 "instant",
53 "http://hl7.org/fhirpath/System.Boolean",
54 "boolean",
55 "http://hl7.org/fhirpath/System.Integer",
56 "http://hl7.org/fhirpath/System.Decimal",
57 "decimal",
58 "integer",
59 "unsignedInt",
60 "positiveInt",
61];
62
63fn fhir_primitive_type_to_json_schema_type(fhir_type: &str) -> JSONSchemaType {
64 match fhir_type {
65 "http://hl7.org/fhirpath/System.Boolean" | "boolean" => JSONSchemaType::Boolean,
66 "http://hl7.org/fhirpath/System.Integer"
67 | "http://hl7.org/fhirpath/System.Decimal"
68 | "decimal"
69 | "integer"
70 | "unsignedInt"
71 | "positiveInt" => JSONSchemaType::Number,
72 _ => JSONSchemaType::String,
73 }
74}
75
76fn is_fhir_primitive_type(fhir_type: &str) -> bool {
77 PRIMITIVE_TYPES.contains(&fhir_type)
78}
79
80fn wrap_if_array(
81 sd: &StructureDefinition,
82 element: &ElementDefinition,
83 base: Processed,
84) -> Processed {
85 match base.cardinality.1 {
86 Max::Unlimited if !utilities::conditionals::is_root(sd, element) => Processed {
87 cardinality: base.cardinality,
88 field: base.field,
89 schema: json!({
90 "type": "array",
91 "items": base.schema,
92 }),
93 },
94 Max::Fixed(n) if n > 1 && !utilities::conditionals::is_root(sd, element) => Processed {
95 cardinality: base.cardinality,
96 field: base.field,
97 schema: json!({
98 "type": "array",
99 "items": base.schema,
100 }),
101 },
102 _ => base,
103 }
104}
105
106fn datatype_reference_schema(schema_loc: &str, fhir_type: &str) -> serde_json::Value {
109 match fhir_type {
110 "DomainResource" | "Resource" => json!({
111 "type": "object",
112 "additionalProperties": true,
113 }),
114 _ => json!({
115 "$ref": format!("{}/{}", schema_loc, fhir_type)
116 }),
117 }
118}
119
120fn process_leaf(
121 schema_loc: &str,
122 sd: &StructureDefinition,
123 element: &ElementDefinition,
124) -> Vec<Processed> {
125 let cardinality = utilities::extract::cardinality(element);
126 let base_schema = if is_typechoice(element) {
127 element
128 .type_
129 .as_ref()
130 .unwrap_or(&vec![])
131 .iter()
132 .flat_map(|fhir_type| {
133 let type_code = fhir_type.code.value.as_deref().unwrap_or_default();
134
135 let field_name = utilities::generate::type_choice_variant_name(element, type_code);
136
137 if is_fhir_primitive_type(type_code) {
138 vec![
139 Processed {
140 cardinality: (0, cardinality.1),
141 field: format!("_{field_name}"),
142 schema: datatype_reference_schema(schema_loc, "Element"),
143 },
144 Processed {
145 cardinality: (0, cardinality.1),
146 field: field_name,
147 schema: json!({
148 "type": fhir_primitive_type_to_json_schema_type(type_code)
149 }),
150 },
151 ]
152 } else {
153 vec![Processed {
154 cardinality: (0, cardinality.1),
155 field: field_name,
156 schema: datatype_reference_schema(schema_loc, type_code),
157 }]
158 }
159 })
160 .collect()
161 } else {
162 let type_code = element
163 .type_
164 .as_ref()
165 .and_then(|t| t.first())
166 .map(|t| t.code.as_ref())
167 .and_then(|c| c.value.as_ref())
168 .map(std::string::String::as_str)
169 .unwrap_or_default();
170 let field_name =
171 utilities::extract::field_name(element.path.value.as_deref().map_or("", |s| s));
172
173 if is_fhir_primitive_type(type_code) {
174 vec![
175 Processed {
176 cardinality: (0, cardinality.1),
177 field: format!("_{field_name}"),
178 schema: datatype_reference_schema(schema_loc, "Element"),
179 },
180 Processed {
181 cardinality,
182 field: field_name,
183 schema: json!({
184 "type": fhir_primitive_type_to_json_schema_type(type_code)
185 }),
186 },
187 ]
188 } else {
189 vec![Processed {
190 cardinality,
191 field: field_name,
192 schema: datatype_reference_schema(schema_loc, type_code),
193 }]
194 }
195 };
196
197 base_schema
198 .into_iter()
199 .map(|schema| wrap_if_array(sd, element, schema))
200 .collect()
201}
202
203fn process_complex(
204 sd: &StructureDefinition,
205 element: &ElementDefinition,
206 children: Vec<Processed>,
207) -> Processed {
208 let mut required_properties = vec![];
209 let mut properties: HashMap<String, serde_json::Value> = HashMap::new();
210 if utilities::conditionals::is_root(sd, element) && utilities::conditionals::is_resource_sd(sd)
211 {
212 properties.insert(
213 "resourceType".to_string(),
214 json!({
215 "type": "string",
216 "const": sd.type_.value.as_ref().unwrap_or(&"Unknown".to_string()),
217 }),
218 );
219 required_properties.push("resourceType".to_string());
220 }
221
222 for child in children {
223 if child.cardinality.0 > 0 {
224 required_properties.push(child.field.clone());
225 }
226 properties.insert(child.field, child.schema);
227 }
228
229 wrap_if_array(
230 sd,
231 element,
232 Processed {
233 cardinality: utilities::extract::cardinality(element),
234 field: utilities::extract::field_name(element.path.value.as_deref().map_or("", |s| s)),
235 schema: json!({
236 "type": "object",
237 "properties": properties,
238 "required": required_properties,
239 "additionalProperties": false,
240 }),
241 },
242 )
243}
244
245pub fn isolated_schema(
261 schema_loc: &str,
262 sd: &StructureDefinition,
263) -> Result<serde_json::Value, OperationOutcomeError> {
264 let mut visitor = |element: &ElementDefinition,
265 children: Vec<Vec<Processed>>,
266 _index: usize|
267 -> Vec<Processed> {
268 if children.is_empty() {
269 process_leaf(schema_loc, sd, element)
270 } else {
271 vec![process_complex(
272 sd,
273 element,
274 children.into_iter().flatten().collect(),
275 )]
276 }
277 };
278
279 let mut result = traversal::traversal(sd, &mut visitor).map_err(|e| {
280 OperationOutcomeError::error(
281 IssueType::exception(),
282 format!("Error traversing StructureDefinition: {e}"),
283 )
284 })?;
285
286 if let Some(result) = result.pop() {
287 Ok(result.schema)
288 } else {
289 Err(OperationOutcomeError::error(
290 IssueType::exception(),
291 "No schema generated from StructureDefinition".to_string(),
292 ))
293 }
294}
295
296#[must_use]
298pub fn bundle_of_resource(resource_schema: &serde_json::Value) -> serde_json::Value {
299 json!({
300 "type": "object",
301 "properties": {
302 "resourceType": {
303 "type": "string",
304 "const": "Bundle"
305 },
306 "type": {
307 "enum": ["collection", "searchset", "history"]
308 },
309 "entry": {
310 "type": "array",
311 "items": {
312 "type": "object",
313 "properties": {
314 "resource": resource_schema
315 },
316 "required": ["resource"],
317 "additionalProperties": true
318 }
319 }
320 },
321 "required": ["resourceType", "type", "entry"],
322 "additionalProperties": false
323 })
324}
325
326#[cfg(test)]
327mod test {
328 use std::sync::LazyLock;
329
330 use haste_fhir_model::r4::generated::{
331 resources::{Bundle, Patient},
332 terminology::StructureDefinitionKind,
333 types::{FHIRString, HumanName},
334 };
335
336 use super::*;
337
338 static RESOURCE_SDS: LazyLock<Vec<StructureDefinition>> = LazyLock::new(|| {
339 let sd_str = include_str!(
340 "../../../../artifacts/r4/hl7-core/definitions/hl7/profiles-resources.min.json"
341 );
342
343 let bundle: Bundle =
344 serde_json::from_str(sd_str).expect("Failed to parse StructureDefinitions");
345
346 bundle
347 .entry
348 .unwrap_or_default()
349 .into_iter()
350 .filter_map(|entry| entry.resource)
351 .filter_map(|resource| {
352 if let haste_fhir_model::r4::generated::resources::Resource::StructureDefinition(
353 sd,
354 ) = *resource
355 {
356 Some(sd)
357 } else {
358 None
359 }
360 })
361 .collect()
362 });
363
364 const TEST_SCHEMA_BASE_URL: &str = "https://example.com/schemas/fhir";
367
368 pub static FHIR_COMPLEX_TYPE_DEFINITIONS: LazyLock<HashMap<String, serde_json::Value>> =
372 LazyLock::new(|| {
373 let sd_str = include_str!(
374 "../../../../artifacts/r4/hl7-core/definitions/hl7/profiles-types.min.json"
375 );
376
377 let bundle: Bundle =
378 serde_json::from_str(sd_str).expect("Failed to parse StructureDefinitions");
379
380 bundle
381 .entry
382 .unwrap_or_default()
383 .into_iter()
384 .filter_map(|entry| entry.resource)
385 .filter_map(|resource| {
386 if let haste_fhir_model::r4::generated::resources::Resource::StructureDefinition(
387 sd,
388 ) = *resource
389 {
390 Some(sd)
391 } else {
392 None
393 }
394 })
395 .filter(|sd|
396 sd.kind == StructureDefinitionKind::complex_type()
397 )
398 .map(|sd| {
399 let type_name = sd.type_.value.clone().unwrap();
400 (
401 format!("{TEST_SCHEMA_BASE_URL}/{type_name}"),
402 isolated_schema(TEST_SCHEMA_BASE_URL, &sd).unwrap(),
403 )
404 })
405 .collect::<HashMap<String, _>>()
406 });
407
408 struct TestSchemaRetriever;
411
412 impl jsonschema::Retrieve for TestSchemaRetriever {
413 fn retrieve(
414 &self,
415 uri: &jsonschema::Uri<String>,
416 ) -> Result<serde_json::Value, Box<dyn std::error::Error + Send + Sync>> {
417 FHIR_COMPLEX_TYPE_DEFINITIONS
418 .get(uri.as_str())
419 .cloned()
420 .ok_or_else(|| format!("Unknown schema: {uri}").into())
421 }
422 }
423
424 #[test]
425 fn test_sd_to_json_schema() {
426 let patient_sd = RESOURCE_SDS
427 .iter()
428 .find(|v| v.type_.value.as_deref() == Some("Patient"))
429 .unwrap();
430
431 let schema = isolated_schema(TEST_SCHEMA_BASE_URL, patient_sd).unwrap();
432
433 println!("{}", serde_json::to_string_pretty(&schema).unwrap());
434
435 assert!(!serde_json::to_string(&schema).unwrap().is_empty());
436 }
437
438 #[test]
439 fn patient_sd_test() {
440 let patient_sd = RESOURCE_SDS
441 .iter()
442 .find(|v| v.type_.value.as_deref() == Some("Patient"))
443 .unwrap();
444
445 let schema = isolated_schema(TEST_SCHEMA_BASE_URL, patient_sd).unwrap();
446
447 let validator = jsonschema::options()
448 .with_retriever(TestSchemaRetriever)
449 .build(&schema)
450 .unwrap();
451
452 let patient_data = serde_json::to_string(&Patient {
453 name: Some(vec![HumanName {
454 family: Some(Box::new(FHIRString {
455 value: Some("Doe".to_string()),
456 ..Default::default()
457 })),
458 given: Some(vec![FHIRString {
459 value: Some("John".to_string()),
460 ..Default::default()
461 }]),
462 ..Default::default()
463 }]),
464 ..Default::default()
465 })
466 .unwrap();
467
468 let mut patient_json = serde_json::from_str(&patient_data).unwrap();
469 let result = validator.validate(&patient_json);
470 assert!(result.is_ok());
471
472 patient_json["name"][0]["_given"] = json!("This is not a valid value");
473 let result = validator.validate(&patient_json);
474 assert!(result.is_err());
475
476 patient_json["name"][0]["_given"] = json!([{"id": "1"}]);
477 let result = validator.validate(&patient_json);
478 println!("{result:?}");
479 assert!(result.is_ok());
480
481 patient_json["name"] = json!("This is not a valid value");
482 let result = validator.validate(&patient_json);
483
484 assert!(result.is_err());
485 }
486}