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.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
126fn 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 ) -> 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(),
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(),
316 "No schema generated from StructureDefinition".to_string(),
317 ))
318 }
319}
320
321pub 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 =
377 serde_json::from_str(sd_str).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 =
403 serde_json::from_str(sd_str).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|
421 sd.kind == StructureDefinitionKind::complex_type()
422 )
423 .map(|sd| {
424 (
425 sd.type_.value.clone().unwrap(),
426 isolated_schema("#/$defs", &sd).unwrap(),
427 )
428 })
429 .collect::<HashMap<String, _>>()
430 });
431
432 #[test]
433 fn test_sd_to_json_schema() {
434 let patient_sd = RESOURCE_SDS
435 .iter()
436 .find(|v| v.type_.value.as_ref().map(|s| s.as_str()) == Some("Patient"))
437 .unwrap();
438
439 let schema = self_contained_schema(&*FHIR_COMPLEX_TYPE_DEFINITIONS, patient_sd).unwrap();
440
441 println!("{}", serde_json::to_string_pretty(&schema).unwrap());
442
443 assert_eq!(true, !serde_json::to_string(&schema).unwrap().is_empty());
444 }
445
446 #[test]
447 fn patient_sd_test() {
448 let patient_sd = RESOURCE_SDS
449 .iter()
450 .find(|v| v.type_.value.as_ref().map(|s| s.as_str()) == Some("Patient"))
451 .unwrap();
452
453 let schema = self_contained_schema(&*FHIR_COMPLEX_TYPE_DEFINITIONS, patient_sd).unwrap();
454
455 let patient_data = serde_json::to_string(&Patient {
458 name: Some(vec![HumanName {
459 family: Some(Box::new(FHIRString {
460 value: Some("Doe".to_string()),
461 ..Default::default()
462 })),
463 given: Some(vec![FHIRString {
464 value: Some("John".to_string()),
465 ..Default::default()
466 }]),
467 ..Default::default()
468 }]),
469 ..Default::default()
470 })
471 .unwrap();
472
473 let mut patient_json = serde_json::from_str(&patient_data).unwrap();
474 let result = jsonschema::validate(&schema, &patient_json);
475 assert_eq!(result.is_ok(), true);
476
477 patient_json["name"][0]["_given"] = json!("This is not a valid value");
478 let result = jsonschema::validate(&schema, &patient_json);
479 assert_eq!(result.is_err(), true);
480
481 patient_json["name"][0]["_given"] = json!([{"id": "1"}]);
482 let result = jsonschema::validate(&schema, &patient_json);
483 println!("{:?}", result);
484 assert_eq!(result.is_ok(), true);
485
486 patient_json["name"] = json!("This is not a valid value");
487 let result = jsonschema::validate(&schema, &patient_json);
488
489 assert_eq!(result.is_err(), true);
490 }
491}