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
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(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
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 = 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 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}