Skip to main content

haste_sd_to_json_schema/
lib.rs

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: (usize, 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.String"
66        | "http://hl7.org/fhirpath/System.Time"
67        | "http://hl7.org/fhirpath/System.Date"
68        | "http://hl7.org/fhirpath/System.DateTime"
69        | "http://hl7.org/fhirpath/System.Instant"
70        | "markdown"
71        | "url"
72        | "canonical"
73        | "uuid"
74        | "string"
75        | "uri"
76        | "code"
77        | "id"
78        | "oid"
79        | "base64Binary"
80        | "xhtml"
81        | "instant"
82        | "time"
83        | "date"
84        | "dateTime" => JSONSchemaType::String,
85        "http://hl7.org/fhirpath/System.Boolean" | "boolean" => JSONSchemaType::Boolean,
86        "http://hl7.org/fhirpath/System.Integer"
87        | "http://hl7.org/fhirpath/System.Decimal"
88        | "decimal"
89        | "integer"
90        | "unsignedInt"
91        | "positiveInt" => JSONSchemaType::Number,
92        _ => JSONSchemaType::String,
93    }
94}
95
96fn is_fhir_primitive_type(fhir_type: &str) -> bool {
97    PRIMITIVE_TYPES.contains(&fhir_type)
98}
99
100fn wrap_if_array(
101    sd: &StructureDefinition,
102    element: &ElementDefinition,
103    base: Processed,
104) -> Processed {
105    match base.cardinality.1 {
106        Max::Unlimited if !utilities::conditionals::is_root(sd, element) => Processed {
107            cardinality: base.cardinality,
108            field: base.field,
109            schema: json!({
110                "type": "array",
111                "items": base.schema,
112            }),
113        },
114        Max::Fixed(n) if n > 1 && !utilities::conditionals::is_root(sd, element) => Processed {
115            cardinality: base.cardinality,
116            field: base.field,
117            schema: json!({
118                "type": "array",
119                "items": base.schema,
120            }),
121        },
122        _ => base,
123    }
124}
125
126// Generate a JSON Schema reference for a FHIR type
127// If it's a Resource or DomainResource, we return a generic object schema.
128fn datatype_reference_schema(schema_loc: &str, fhir_type: &str) -> serde_json::Value {
129    match fhir_type {
130        "DomainResource" | "Resource" => json!({
131            "type": "object",
132             "additionalProperties": true,
133        }),
134        _ => json!({
135            "$ref": format!("{}/{}", schema_loc, fhir_type)
136        }),
137    }
138}
139
140fn process_leaf(
141    schema_loc: &str,
142    sd: &StructureDefinition,
143    element: &ElementDefinition,
144) -> Vec<Processed> {
145    let cardinality = utilities::extract::cardinality(element);
146    let base_schema = if is_typechoice(element) {
147        element
148            .type_
149            .as_ref()
150            .unwrap_or(&vec![])
151            .iter()
152            .map(|fhir_type| {
153                let type_code = fhir_type
154                    .code
155                    .value
156                    .as_ref()
157                    .map(|s| s.as_str())
158                    .unwrap_or_default();
159
160                let field_name = utilities::generate::type_choice_variant_name(element, type_code);
161
162                if is_fhir_primitive_type(type_code) {
163                    vec![
164                        Processed {
165                            cardinality: (0, cardinality.1.clone()),
166                            field: format!("_{}", field_name),
167                            schema: datatype_reference_schema(schema_loc, "Element"),
168                        },
169                        Processed {
170                            cardinality: (0, cardinality.1.clone()),
171                            field: field_name,
172                            schema: json!({
173                                "type": fhir_primitive_type_to_json_schema_type(type_code)
174                            }),
175                        },
176                    ]
177                } else {
178                    vec![Processed {
179                        cardinality: (0, cardinality.1.clone()),
180                        field: field_name,
181                        schema: datatype_reference_schema(schema_loc, type_code),
182                    }]
183                }
184            })
185            .flatten()
186            .collect()
187    } else {
188        let type_code = element
189            .type_
190            .as_ref()
191            .and_then(|t| t.first())
192            .map(|t| t.code.as_ref())
193            .and_then(|c| c.value.as_ref())
194            .map(|s| s.as_str())
195            .unwrap_or_default();
196        let field_name = utilities::extract::field_name(
197            element
198                .path
199                .value
200                .as_ref()
201                .map(|s| s.as_str())
202                .unwrap_or(""),
203        );
204
205        if is_fhir_primitive_type(type_code) {
206            vec![
207                Processed {
208                    cardinality: (0, cardinality.1.clone()),
209                    field: format!("_{}", field_name),
210                    schema: datatype_reference_schema(schema_loc, "Element"),
211                },
212                Processed {
213                    cardinality,
214                    field: field_name,
215                    schema: json!({
216                        "type": fhir_primitive_type_to_json_schema_type(type_code)
217                    }),
218                },
219            ]
220        } else {
221            vec![Processed {
222                cardinality,
223                field: field_name,
224                schema: datatype_reference_schema(schema_loc, type_code),
225            }]
226        }
227    };
228
229    base_schema
230        .into_iter()
231        .map(|schema| wrap_if_array(sd, element, schema))
232        .collect()
233}
234
235fn process_complex(
236    sd: &StructureDefinition,
237    element: &ElementDefinition,
238    children: Vec<Processed>,
239    // nested_types: &mut Vec<StructureDefinition>,
240) -> Processed {
241    let mut required_properties = vec![];
242    let mut properties: HashMap<String, serde_json::Value> = HashMap::new();
243    if utilities::conditionals::is_root(sd, element) && utilities::conditionals::is_resource_sd(sd)
244    {
245        properties.insert(
246            "resourceType".to_string(),
247            json!({
248                "type": "string",
249                "const": sd.type_.value.as_ref().unwrap_or(&"Unknown".to_string()),
250            }),
251        );
252        required_properties.push("resourceType".to_string());
253    };
254
255    for child in children.into_iter() {
256        if child.cardinality.0 > 0 {
257            required_properties.push(child.field.clone());
258        }
259        properties.insert(child.field, child.schema);
260    }
261
262    wrap_if_array(
263        sd,
264        element,
265        Processed {
266            cardinality: utilities::extract::cardinality(element),
267            field: utilities::extract::field_name(
268                element
269                    .path
270                    .value
271                    .as_ref()
272                    .map(|s| s.as_str())
273                    .unwrap_or(""),
274            ),
275            schema: json!({
276                "type": "object",
277                "properties": properties,
278                "required": required_properties,
279                "additionalProperties": false,
280            }),
281        },
282    )
283}
284
285pub fn isolated_schema(
286    schema_loc: &str,
287    sd: &StructureDefinition,
288) -> Result<serde_json::Value, OperationOutcomeError> {
289    let mut visitor = |element: &ElementDefinition,
290                       children: Vec<Vec<Processed>>,
291                       _index: usize|
292     -> Vec<Processed> {
293        if children.len() == 0 {
294            process_leaf(schema_loc, &sd, element)
295        } else {
296            vec![process_complex(
297                &sd,
298                element,
299                children.into_iter().flatten().collect(),
300            )]
301        }
302    };
303
304    let mut result = traversal::traversal(sd, &mut visitor).map_err(|e| {
305        OperationOutcomeError::error(
306            IssueType::Exception(None),
307            format!("Error traversing StructureDefinition: {}", e),
308        )
309    })?;
310
311    if let Some(result) = result.pop() {
312        Ok(result.schema)
313    } else {
314        Err(OperationOutcomeError::error(
315            IssueType::Exception(None),
316            "No schema generated from StructureDefinition".to_string(),
317        ))
318    }
319}
320
321// Creates a type schema for a bundle of resources
322pub fn bundle_of_resource(resource_schema: serde_json::Value) -> serde_json::Value {
323    json!({
324        "type": "object",
325        "properties": {
326            "resourceType": {
327                "type": "string",
328                "const": "Bundle"
329            },
330            "type": {
331                "enum": ["collection", "searchset", "history"]
332            },
333            "entry": {
334                "type": "array",
335                "items": {
336                    "type": "object",
337                    "properties": {
338                        "resource": resource_schema
339                    },
340                    "required": ["resource"],
341                    "additionalProperties": true
342                }
343            }
344        },
345        "required": ["resourceType", "type", "entry"],
346        "additionalProperties": false
347    })
348}
349
350pub fn self_contained_schema(
351    defs: &HashMap<String, serde_json::Value>,
352    sd: &StructureDefinition,
353) -> Result<serde_json::Value, OperationOutcomeError> {
354    let mut schema = isolated_schema("#/$defs", sd)?;
355    schema["$defs"] = json!(defs);
356
357    Ok(schema)
358}
359
360#[cfg(test)]
361mod test {
362    use std::sync::LazyLock;
363
364    use haste_fhir_model::r4::generated::{
365        resources::{Bundle, Patient},
366        terminology::StructureDefinitionKind,
367        types::{FHIRString, HumanName},
368    };
369
370    use super::*;
371
372    static RESOURCE_SDS: LazyLock<Vec<StructureDefinition>> = LazyLock::new(|| {
373        let sd_str =
374            include_str!("../../artifacts/artifacts/r4/hl7/minified/profiles-resources.min.json");
375
376        let bundle: Bundle = haste_fhir_serialization_json::from_str(sd_str)
377            .expect("Failed to parse StructureDefinitions");
378
379        bundle
380            .entry
381            .unwrap_or_default()
382            .into_iter()
383            .filter_map(|entry| entry.resource)
384            .filter_map(|resource| {
385                if let haste_fhir_model::r4::generated::resources::Resource::StructureDefinition(
386                    sd,
387                ) = *resource
388                {
389                    Some(sd)
390                } else {
391                    None
392                }
393            })
394            .collect()
395    });
396
397    pub static FHIR_COMPLEX_TYPE_DEFINITIONS: LazyLock<HashMap<String, serde_json::Value>> =
398        LazyLock::new(|| {
399            let sd_str =
400                include_str!("../../artifacts/artifacts/r4/hl7/minified/profiles-types.min.json");
401
402            let bundle: Bundle = haste_fhir_serialization_json::from_str(sd_str)
403                .expect("Failed to parse StructureDefinitions");
404
405            bundle
406            .entry
407            .unwrap_or_default()
408            .into_iter()
409            .filter_map(|entry| entry.resource)
410            .filter_map(|resource| {
411                if let haste_fhir_model::r4::generated::resources::Resource::StructureDefinition(
412                    sd,
413                ) = *resource
414                {
415                    Some(sd)
416                } else {
417                    None
418                }
419            })
420            .filter(|sd| match sd.kind.as_ref() {
421                StructureDefinitionKind::ComplexType(None) => true,
422                _ => false,
423            })
424            .map(|sd| {
425                (
426                    sd.type_.value.clone().unwrap(),
427                    isolated_schema("#/$defs", &sd).unwrap(),
428                )
429            })
430            .collect::<HashMap<String, _>>()
431        });
432
433    #[test]
434    fn test_sd_to_json_schema() {
435        let patient_sd = RESOURCE_SDS
436            .iter()
437            .find(|v| v.type_.value.as_ref().map(|s| s.as_str()) == Some("Patient"))
438            .unwrap();
439
440        let schema = self_contained_schema(&*FHIR_COMPLEX_TYPE_DEFINITIONS, patient_sd).unwrap();
441
442        println!("{}", serde_json::to_string_pretty(&schema).unwrap());
443
444        assert_eq!(true, !serde_json::to_string(&schema).unwrap().is_empty());
445    }
446
447    #[test]
448    fn patient_sd_test() {
449        let patient_sd = RESOURCE_SDS
450            .iter()
451            .find(|v| v.type_.value.as_ref().map(|s| s.as_str()) == Some("Patient"))
452            .unwrap();
453
454        let schema = self_contained_schema(&*FHIR_COMPLEX_TYPE_DEFINITIONS, patient_sd).unwrap();
455
456        // println!("{}", serde_json::to_string_pretty(&schema).unwrap());
457
458        let patient_data = haste_fhir_serialization_json::to_string(&Patient {
459            name: Some(vec![Box::new(HumanName {
460                family: Some(Box::new(FHIRString {
461                    value: Some("Doe".to_string()),
462                    ..Default::default()
463                })),
464                given: Some(vec![Box::new(FHIRString {
465                    value: Some("John".to_string()),
466                    ..Default::default()
467                })]),
468                ..Default::default()
469            })]),
470            ..Default::default()
471        })
472        .unwrap();
473
474        let mut patient_json = serde_json::from_str(&patient_data).unwrap();
475        let result = jsonschema::validate(&schema, &patient_json);
476        assert_eq!(result.is_ok(), true);
477
478        patient_json["name"][0]["_given"] = json!("This is not a valid value");
479        let result = jsonschema::validate(&schema, &patient_json);
480        assert_eq!(result.is_err(), true);
481
482        patient_json["name"][0]["_given"] = json!([{"id": "1"}]);
483        let result = jsonschema::validate(&schema, &patient_json);
484        println!("{:?}", result);
485        assert_eq!(result.is_ok(), true);
486
487        patient_json["name"] = json!("This is not a valid value");
488        let result = jsonschema::validate(&schema, &patient_json);
489
490        assert_eq!(result.is_err(), true);
491    }
492}