Skip to main content

haste_openapi_schema_generator/
lib.rs

1use std::collections::HashMap;
2
3use haste_fhir_model::r4::generated::{
4    resources::{SearchParameter, StructureDefinition},
5    terminology::{IssueType, SearchParamType, StructureDefinitionKind},
6};
7use haste_fhir_operation_error::OperationOutcomeError;
8use serde::{Deserialize, Serialize};
9use serde_json::json;
10
11#[derive(Deserialize, Serialize)]
12pub struct OpenAPIComponents {
13    schemas: std::collections::HashMap<String, serde_json::Value>,
14}
15
16#[derive(Deserialize, Serialize)]
17pub struct OpenAPIOperationContent {
18    description: String,
19    // Content Type to Schema mapping
20    #[serde(skip_serializing_if = "Option::is_none")]
21    content: Option<HashMap<String, serde_json::Value>>,
22}
23
24#[derive(Deserialize, Serialize)]
25pub struct OpenAPIOperation {
26    #[serde(rename = "requestBody", skip_serializing_if = "Option::is_none")]
27    request_body: Option<OpenAPIOperationContent>,
28    responses: HashMap<String, OpenAPIOperationContent>,
29    parameters: Vec<serde_json::Value>,
30}
31
32#[derive(Deserialize, Serialize)]
33pub struct OpenAPIPathItem {
34    #[serde(skip_serializing_if = "Option::is_none")]
35    get: Option<OpenAPIOperation>,
36    #[serde(skip_serializing_if = "Option::is_none")]
37    post: Option<OpenAPIOperation>,
38    #[serde(skip_serializing_if = "Option::is_none")]
39    put: Option<OpenAPIOperation>,
40    #[serde(skip_serializing_if = "Option::is_none")]
41    delete: Option<OpenAPIOperation>,
42    #[serde(skip_serializing_if = "Option::is_none")]
43    patch: Option<OpenAPIOperation>,
44}
45
46pub type OpenAPIPaths = HashMap<String, OpenAPIPathItem>;
47
48#[derive(Deserialize, Serialize)]
49pub struct OpenAPIInfo {
50    title: String,
51    version: String,
52}
53
54#[derive(Deserialize, Serialize)]
55pub struct OpenAPIServerVariable {
56    default: String,
57    #[serde(skip_serializing_if = "Option::is_none")]
58    description: Option<String>,
59}
60
61#[derive(Deserialize, Serialize)]
62pub struct OpenAPIServer {
63    url: String,
64    #[serde(skip_serializing_if = "Option::is_none")]
65    description: Option<String>,
66    variables: HashMap<String, OpenAPIServerVariable>,
67}
68
69#[derive(Deserialize, Serialize)]
70pub struct OpenAPI {
71    servers: Vec<OpenAPIServer>,
72    openapi: String,
73    info: OpenAPIInfo,
74    components: OpenAPIComponents,
75    paths: OpenAPIPaths,
76}
77
78fn read_resource_operation(resource_name: &str) -> OpenAPIOperation {
79    OpenAPIOperation {
80        request_body: None,
81        responses: HashMap::from([
82            (
83                "200".to_string(),
84                OpenAPIOperationContent {
85                    description: format!("Successful read of {} resource", resource_name),
86                    content: Some(HashMap::from([(
87                        "application/json".to_string(),
88                        json!({ "schema": {"$ref": format!("#/components/schemas/{}", resource_name) }}),
89                    )])),
90                },
91            ),
92            (
93                "400".to_string(),
94                OpenAPIOperationContent {
95                    description: "Client error".to_string(),
96                    content: Some(HashMap::from([(
97                        "application/json".to_string(),
98                        json!({ "schema": {"$ref": "#/components/schemas/OperationOutcome" }}),
99                    )])),
100                },
101            ),
102            (
103                "500".to_string(),
104                OpenAPIOperationContent {
105                    description: "Server error".to_string(),
106                    content: Some(HashMap::from([(
107                        "application/json".to_string(),
108                        json!({ "schema": {"$ref": "#/components/schemas/OperationOutcome" }}),
109                    )])),
110                },
111            ),
112        ]),
113        parameters: vec![json!({
114            "name": "id",
115            "in": "path",
116            "required": true,
117            "schema": {
118                "type": "string"
119            },
120            "description": format!("The ID of the {} resource", resource_name)
121        })],
122    }
123}
124
125fn put_resource_operation(resource_name: &str) -> OpenAPIOperation {
126    OpenAPIOperation {
127        request_body: Some(OpenAPIOperationContent {
128            description: format!("The {} resource to create or update", resource_name),
129            content: Some(HashMap::from([(
130                "application/json".to_string(),
131                json!({ "schema": {"$ref": format!("#/components/schemas/{}", resource_name) }}),
132            )])),
133        }),
134        responses: HashMap::from([
135            (
136                "200".to_string(),
137                OpenAPIOperationContent {
138                    description: format!("Successful put/creation of {} resource", resource_name),
139                    content: Some(HashMap::from([(
140                        "application/json".to_string(),
141                        json!({ "schema": {"$ref": format!("#/components/schemas/{}", resource_name) }}),
142                    )])),
143                },
144            ),
145            (
146                "400".to_string(),
147                OpenAPIOperationContent {
148                    description: "Client error".to_string(),
149                    content: Some(HashMap::from([(
150                        "application/json".to_string(),
151                        json!({ "schema": {"$ref": "#/components/schemas/OperationOutcome" }}),
152                    )])),
153                },
154            ),
155            (
156                "500".to_string(),
157                OpenAPIOperationContent {
158                    description: "Server error".to_string(),
159                    content: Some(HashMap::from([(
160                        "application/json".to_string(),
161                        json!({ "schema": {"$ref": "#/components/schemas/OperationOutcome" }}),
162                    )])),
163                },
164            ),
165        ]),
166        parameters: vec![json!({
167            "name": "id",
168            "in": "path",
169            "required": true,
170            "schema": {
171                "type": "string"
172            },
173            "description": format!("The ID of the {} resource", resource_name)
174        })],
175    }
176}
177
178fn delete_instance_operation(resource_name: &str) -> OpenAPIOperation {
179    OpenAPIOperation {
180        request_body: None,
181        responses: HashMap::from([
182            (
183                "200".to_string(),
184                OpenAPIOperationContent {
185                    description: format!("Successful deletion of {} resource", resource_name),
186                    content: None,
187                },
188            ),
189            (
190                "400".to_string(),
191                OpenAPIOperationContent {
192                    description: "Client error".to_string(),
193                    content: Some(HashMap::from([(
194                        "application/json".to_string(),
195                        json!({ "schema": {"$ref": "#/components/schemas/OperationOutcome" }}),
196                    )])),
197                },
198            ),
199        ]),
200        parameters: vec![json!({
201            "name": "id",
202            "in": "path",
203            "required": true,
204            "schema": {
205                "type": "string"
206            },
207            "description": format!("The ID of the {} resource", resource_name)
208        })],
209    }
210}
211
212fn patch_resource_operation(resource_name: &str) -> OpenAPIOperation {
213    OpenAPIOperation {
214        request_body: Some(OpenAPIOperationContent {
215            description: format!("JSON Patch operation for {} resource.", resource_name),
216            content: Some(HashMap::from([(
217                "application/json".to_string(),
218                json!({ "schema": {"type": "array" }}),
219            )])),
220        }),
221        responses: HashMap::from([
222            (
223                "200".to_string(),
224                OpenAPIOperationContent {
225                    description: format!("Successful patch of {} resource", resource_name),
226                    content: Some(HashMap::from([(
227                        "application/json".to_string(),
228                        json!({ "schema": {"$ref": format!("#/components/schemas/{}", resource_name) }}),
229                    )])),
230                },
231            ),
232            (
233                "400".to_string(),
234                OpenAPIOperationContent {
235                    description: "Client error".to_string(),
236                    content: Some(HashMap::from([(
237                        "application/json".to_string(),
238                        json!({ "schema": {"$ref": "#/components/schemas/OperationOutcome" }}),
239                    )])),
240                },
241            ),
242        ]),
243        parameters: vec![json!({
244            "name": "id",
245            "in": "path",
246            "required": true,
247            "schema": {
248                "type": "string"
249            },
250            "description": format!("The ID of the {} resource", resource_name)
251        })],
252    }
253}
254
255fn resource_search_parameters_schema(
256    resource_name: &str,
257    search_parameters: &Vec<SearchParameter>,
258) -> Vec<serde_json::Value> {
259    let mut params = vec![];
260
261    for sp in search_parameters.iter().filter(|sp| {
262        sp.base.iter().any(|b| {
263            let base = b.as_str();
264            base == Some(resource_name)
265                || base == Some("Resource")
266                || base == Some("DomainResource")
267        }) && sp.type_ != SearchParamType::composite()
268    }) {
269        let search_type = if sp.type_ == SearchParamType::number() {
270            "number"
271        } else {
272            "string"
273        };
274
275        params.push(json!({
276            "name": sp.code.value,
277            "in": "query",
278            "required": false,
279            "schema": {
280                "type": search_type
281            },
282            "description": sp.description.value.as_ref().map(|s| s.as_str()).unwrap_or("")
283        }));
284    }
285
286    params
287}
288
289fn create_resource_operation(resource_name: &str) -> OpenAPIOperation {
290    OpenAPIOperation {
291        request_body: Some(OpenAPIOperationContent {
292            description: format!("The {} resource to create", resource_name),
293            content: Some(HashMap::from([(
294                "application/json".to_string(),
295                json!({ "schema": {"$ref": format!("#/components/schemas/{}", resource_name) }}),
296            )])),
297        }),
298        responses: HashMap::from([
299            (
300                "200".to_string(),
301                OpenAPIOperationContent {
302                    description: format!("Successful creation of {} resource", resource_name),
303                    content: Some(HashMap::from([(
304                        "application/json".to_string(),
305                        json!({ "schema": {"$ref": format!("#/components/schemas/{}", resource_name) }}),
306                    )])),
307                },
308            ),
309            (
310                "400".to_string(),
311                OpenAPIOperationContent {
312                    description: "Client error".to_string(),
313                    content: Some(HashMap::from([(
314                        "application/json".to_string(),
315                        json!({ "schema": {"$ref": "#/components/schemas/OperationOutcome" }}),
316                    )])),
317                },
318            ),
319        ]),
320        parameters: vec![],
321    }
322}
323
324fn search_resource_operation(
325    resource_name: &str,
326    parameters: Vec<serde_json::Value>,
327) -> OpenAPIOperation {
328    OpenAPIOperation {
329        request_body: None,
330        responses: HashMap::from([
331            (
332                "200".to_string(),
333                OpenAPIOperationContent {
334                    description: "Successful search operation".to_string(),
335                    content: Some(HashMap::from([(
336                        "application/json".to_string(),
337                        json!({ "schema": haste_sd_to_json_schema::bundle_of_resource(json!({
338                            "$ref": format!("#/components/schemas/{}", resource_name)
339                        })) }),
340                    )])),
341                },
342            ),
343            (
344                "400".to_string(),
345                OpenAPIOperationContent {
346                    description: "Client error".to_string(),
347                    content: Some(HashMap::from([(
348                        "application/json".to_string(),
349                        json!({ "schema": {"$ref": "#/components/schemas/OperationOutcome" }}),
350                    )])),
351                },
352            ),
353        ]),
354        parameters,
355    }
356}
357
358fn delete_resource_operation(parameters: Vec<serde_json::Value>) -> OpenAPIOperation {
359    OpenAPIOperation {
360        request_body: None,
361        responses: HashMap::from([
362            (
363                "200".to_string(),
364                OpenAPIOperationContent {
365                    description: "Successful delete operation".to_string(),
366                    content: None,
367                },
368            ),
369            (
370                "400".to_string(),
371                OpenAPIOperationContent {
372                    description: "Client error".to_string(),
373                    content: Some(HashMap::from([(
374                        "application/json".to_string(),
375                        json!({ "schema": {"$ref": "#/components/schemas/OperationOutcome" }}),
376                    )])),
377                },
378            ),
379        ]),
380        parameters,
381    }
382}
383
384pub fn open_api_schema_generator(
385    server_root: &str,
386    api_version: &str,
387    sds: &Vec<StructureDefinition>,
388    search_parameters: &Vec<SearchParameter>,
389) -> Result<OpenAPI, OperationOutcomeError> {
390    let mut fhir_server_variables = HashMap::new();
391    fhir_server_variables.insert(
392        "tenant".to_string(),
393        OpenAPIServerVariable {
394            default: "my-tenant".to_string(),
395            description: Some("Tenant identifier".to_string()),
396        },
397    );
398    fhir_server_variables.insert(
399        "project".to_string(),
400        OpenAPIServerVariable {
401            default: "my-project".to_string(),
402            description: Some("Project identifier".to_string()),
403        },
404    );
405    fhir_server_variables.insert(
406        "fhir_version".to_string(),
407        OpenAPIServerVariable {
408            default: "r4".to_string(),
409            description: Some("FHIR version".to_string()),
410        },
411    );
412    let mut openapi_schema = OpenAPI {
413        openapi: "3.1.1".to_string(),
414        servers: vec![OpenAPIServer {
415            url: format!(
416                "{}/w/{}/{}/api/v1/fhir/{}",
417                server_root, "{tenant}", "{project}", "{fhir_version}"
418            ),
419            description: Some("Haste Health FHIR Server".to_string()),
420            variables: fhir_server_variables,
421        }],
422        info: OpenAPIInfo {
423            title: "Haste Health API Documentation".to_string(),
424            version: api_version.to_string(),
425        },
426        components: OpenAPIComponents {
427            schemas: HashMap::new(),
428        },
429
430        paths: HashMap::new(),
431    };
432
433    let complex_sds = sds
434        .iter()
435        .filter(|sd| sd.kind == StructureDefinitionKind::complex_type());
436
437    for sd in complex_sds {
438        let json_schema = haste_sd_to_json_schema::isolated_schema("#/components/schemas", sd)?;
439        let type_name = sd.type_.value.as_ref().ok_or_else(|| {
440            OperationOutcomeError::error(
441                IssueType::structure(),
442                format!(
443                    "StructureDefinition missing type for id {}",
444                    sd.id.as_ref().unwrap_or(&"unknown".to_string())
445                ),
446            )
447        })?;
448        openapi_schema
449            .components
450            .schemas
451            .insert(type_name.clone(), json_schema);
452    }
453
454    let resource_sds = sds
455        .iter()
456        .filter(|sd| sd.kind == StructureDefinitionKind::resource());
457
458    for sd in resource_sds {
459        let json_schema = haste_sd_to_json_schema::isolated_schema("#/components/schemas", sd)?;
460        let resource_name = sd.type_.value.as_ref().ok_or_else(|| {
461            OperationOutcomeError::error(
462                IssueType::structure(),
463                format!(
464                    "StructureDefinition missing type for id {}",
465                    sd.id.as_ref().unwrap_or(&"unknown".to_string())
466                ),
467            )
468        })?;
469
470        // Read Operation
471        openapi_schema.paths.insert(
472            format!("/{}/{{id}}", resource_name),
473            OpenAPIPathItem {
474                get: Some(read_resource_operation(&resource_name)),
475                post: None,
476                patch: Some(patch_resource_operation(&resource_name)),
477                put: Some(put_resource_operation(&resource_name)),
478                delete: Some(delete_instance_operation(&resource_name)),
479            },
480        );
481
482        let resource_search_parameters =
483            resource_search_parameters_schema(&resource_name, search_parameters);
484
485        openapi_schema.paths.insert(
486            format!("/{}", resource_name),
487            OpenAPIPathItem {
488                get: Some(search_resource_operation(
489                    &resource_name,
490                    resource_search_parameters.clone(),
491                )),
492                patch: None,
493                put: None,
494                post: Some(create_resource_operation(&resource_name)),
495                delete: Some(delete_resource_operation(resource_search_parameters)),
496            },
497        );
498
499        openapi_schema
500            .components
501            .schemas
502            .insert(resource_name.clone(), json_schema);
503    }
504
505    openapi_schema.components.schemas.insert(
506        "Element".to_string(),
507        json!({
508            "additionalProperties": false,
509            "properties": {
510                "extension": {
511                    "items": {
512                        "$ref": "#/components/schemas/Extension"
513                    },
514                    "type": "array"
515                },
516                "id": {
517                    "type": "string"
518                }
519            },
520            "required": [],
521            "type": "object"
522        }),
523    );
524
525    Ok(openapi_schema)
526}