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 "additionalProperties": true
317 }
318 }
319 },
320 "required": ["resourceType", "type"],
321 "additionalProperties": true
322 })
323}
324
325#[cfg(test)]
326mod test {
327 use std::sync::LazyLock;
328
329 use haste_fhir_model::r4::generated::{
330 resources::{Bundle, Patient},
331 terminology::StructureDefinitionKind,
332 types::{FHIRString, HumanName},
333 };
334
335 use super::*;
336
337 static RESOURCE_SDS: LazyLock<Vec<StructureDefinition>> = LazyLock::new(|| {
338 let sd_str = include_str!(
339 "../../../../artifacts/r4/hl7-core/definitions/hl7/profiles-resources.min.json"
340 );
341
342 let bundle: Bundle =
343 serde_json::from_str(sd_str).expect("Failed to parse StructureDefinitions");
344
345 bundle
346 .entry
347 .unwrap_or_default()
348 .into_iter()
349 .filter_map(|entry| entry.resource)
350 .filter_map(|resource| {
351 if let haste_fhir_model::r4::generated::resources::Resource::StructureDefinition(
352 sd,
353 ) = *resource
354 {
355 Some(sd)
356 } else {
357 None
358 }
359 })
360 .collect()
361 });
362
363 const TEST_SCHEMA_BASE_URL: &str = "https://example.com/schemas/fhir";
366
367 pub static FHIR_COMPLEX_TYPE_DEFINITIONS: LazyLock<HashMap<String, serde_json::Value>> =
371 LazyLock::new(|| {
372 let sd_str = include_str!(
373 "../../../../artifacts/r4/hl7-core/definitions/hl7/profiles-types.min.json"
374 );
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 .filter(|sd|
395 sd.kind == StructureDefinitionKind::complex_type()
396 )
397 .map(|sd| {
398 let type_name = sd.type_.value.clone().unwrap();
399 (
400 format!("{TEST_SCHEMA_BASE_URL}/{type_name}"),
401 isolated_schema(TEST_SCHEMA_BASE_URL, &sd).unwrap(),
402 )
403 })
404 .collect::<HashMap<String, _>>()
405 });
406
407 struct TestSchemaRetriever;
410
411 impl jsonschema::Retrieve for TestSchemaRetriever {
412 fn retrieve(
413 &self,
414 uri: &jsonschema::Uri<String>,
415 ) -> Result<serde_json::Value, Box<dyn std::error::Error + Send + Sync>> {
416 FHIR_COMPLEX_TYPE_DEFINITIONS
417 .get(uri.as_str())
418 .cloned()
419 .ok_or_else(|| format!("Unknown schema: {uri}").into())
420 }
421 }
422
423 #[test]
424 fn test_sd_to_json_schema() {
425 let patient_sd = RESOURCE_SDS
426 .iter()
427 .find(|v| v.type_.value.as_deref() == Some("Patient"))
428 .unwrap();
429
430 let schema = isolated_schema(TEST_SCHEMA_BASE_URL, patient_sd).unwrap();
431
432 println!("{}", serde_json::to_string_pretty(&schema).unwrap());
433
434 assert!(!serde_json::to_string(&schema).unwrap().is_empty());
435 }
436
437 #[test]
438 fn patient_sd_test() {
439 let patient_sd = RESOURCE_SDS
440 .iter()
441 .find(|v| v.type_.value.as_deref() == Some("Patient"))
442 .unwrap();
443
444 let schema = isolated_schema(TEST_SCHEMA_BASE_URL, patient_sd).unwrap();
445
446 let validator = jsonschema::options()
447 .with_retriever(TestSchemaRetriever)
448 .build(&schema)
449 .unwrap();
450
451 let patient_data = serde_json::to_string(&Patient {
452 name: Some(vec![HumanName {
453 family: Some(Box::new(FHIRString {
454 value: Some("Doe".to_string()),
455 ..Default::default()
456 })),
457 given: Some(vec![FHIRString {
458 value: Some("John".to_string()),
459 ..Default::default()
460 }]),
461 ..Default::default()
462 }]),
463 ..Default::default()
464 })
465 .unwrap();
466
467 let mut patient_json = serde_json::from_str(&patient_data).unwrap();
468 let result = validator.validate(&patient_json);
469 assert!(result.is_ok());
470
471 patient_json["name"][0]["_given"] = json!("This is not a valid value");
472 let result = validator.validate(&patient_json);
473 assert!(result.is_err());
474
475 patient_json["name"][0]["_given"] = json!([{"id": "1"}]);
476 let result = validator.validate(&patient_json);
477 println!("{result:?}");
478 assert!(result.is_ok());
479
480 patient_json["name"] = json!("This is not a valid value");
481 let result = validator.validate(&patient_json);
482
483 assert!(result.is_err());
484 }
485}