1use std::collections::{HashMap, HashSet};
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 #[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 resource_schema(resource_name: &str) -> serde_json::Value {
79 json!({
80 "$ref": format!("#/components/schemas/{resource_name}")
81 })
82}
83
84fn operation_outcome_schema() -> serde_json::Value {
85 json!({
86 "$ref": "#/components/schemas/OperationOutcome"
87 })
88}
89
90fn json_content(schema: serde_json::Value) -> HashMap<String, serde_json::Value> {
91 HashMap::from([("application/json".to_string(), schema)])
92}
93
94fn response(
95 description: impl Into<String>,
96 content: Option<HashMap<String, serde_json::Value>>,
97) -> OpenAPIOperationContent {
98 OpenAPIOperationContent {
99 description: description.into(),
100 content,
101 }
102}
103
104fn resource_response(
105 resource_name: &str,
106 description: impl Into<String>,
107) -> OpenAPIOperationContent {
108 response(
109 description,
110 Some(json_content(json!({
111 "schema": resource_schema(resource_name)
112 }))),
113 )
114}
115
116fn operation_outcome_response(description: impl Into<String>) -> OpenAPIOperationContent {
117 response(
118 description,
119 Some(json_content(json!({
120 "schema": operation_outcome_schema()
121 }))),
122 )
123}
124
125fn id_parameter(resource_name: &str) -> serde_json::Value {
126 json!({
127 "name": "id",
128 "in": "path",
129 "required": true,
130 "schema": {
131 "type": "string"
132 },
133 "description": format!("The ID of the {resource_name} resource")
134 })
135}
136
137fn read_resource_operation(resource_name: &str) -> OpenAPIOperation {
138 OpenAPIOperation {
139 request_body: None,
140 responses: HashMap::from([
141 (
142 "200".to_string(),
143 resource_response(
144 resource_name,
145 format!("Successful read of {resource_name} resource"),
146 ),
147 ),
148 (
149 "400".to_string(),
150 operation_outcome_response("Client error"),
151 ),
152 (
153 "500".to_string(),
154 operation_outcome_response("Server error"),
155 ),
156 ]),
157 parameters: vec![id_parameter(resource_name)],
158 }
159}
160
161fn put_resource_operation(resource_name: &str) -> OpenAPIOperation {
162 OpenAPIOperation {
163 request_body: Some(resource_response(
164 resource_name,
165 format!("The {resource_name} resource to create or update"),
166 )),
167 responses: HashMap::from([
168 (
169 "200".to_string(),
170 resource_response(
171 resource_name,
172 format!("Successful put/creation of {resource_name} resource"),
173 ),
174 ),
175 (
176 "400".to_string(),
177 operation_outcome_response("Client error"),
178 ),
179 (
180 "500".to_string(),
181 operation_outcome_response("Server error"),
182 ),
183 ]),
184 parameters: vec![id_parameter(resource_name)],
185 }
186}
187
188fn delete_instance_operation(resource_name: &str) -> OpenAPIOperation {
189 OpenAPIOperation {
190 request_body: None,
191 responses: HashMap::from([
192 (
193 "200".to_string(),
194 response(
195 format!("Successful deletion of {resource_name} resource"),
196 None,
197 ),
198 ),
199 (
200 "400".to_string(),
201 operation_outcome_response("Client error"),
202 ),
203 ]),
204 parameters: vec![id_parameter(resource_name)],
205 }
206}
207
208fn patch_resource_operation(resource_name: &str) -> OpenAPIOperation {
209 OpenAPIOperation {
210 request_body: Some(OpenAPIOperationContent {
211 description: format!("JSON Patch operation for {resource_name} resource."),
212 content: Some(json_content(json!({
213 "schema": {
214 "type": "array"
215 }
216 }))),
217 }),
218 responses: HashMap::from([
219 (
220 "200".to_string(),
221 resource_response(
222 resource_name,
223 format!("Successful patch of {resource_name} resource"),
224 ),
225 ),
226 (
227 "400".to_string(),
228 operation_outcome_response("Client error"),
229 ),
230 ]),
231 parameters: vec![id_parameter(resource_name)],
232 }
233}
234
235fn create_resource_operation(resource_name: &str) -> OpenAPIOperation {
236 OpenAPIOperation {
237 request_body: Some(resource_response(
238 resource_name,
239 format!("The {resource_name} resource to create"),
240 )),
241 responses: HashMap::from([
242 (
243 "200".to_string(),
244 resource_response(
245 resource_name,
246 format!("Successful creation of {resource_name} resource"),
247 ),
248 ),
249 (
250 "400".to_string(),
251 operation_outcome_response("Client error"),
252 ),
253 ]),
254 parameters: vec![],
255 }
256}
257
258fn search_resource_operation(
259 resource_name: &str,
260 parameters: Vec<serde_json::Value>,
261) -> OpenAPIOperation {
262 OpenAPIOperation {
263 request_body: None,
264 responses: HashMap::from([
265 (
266 "200".to_string(),
267 response(
268 "Successful search operation",
269 Some(json_content(json!({
270 "schema": haste_sd_to_json_schema::bundle_of_resource(&json!({
271 "$ref": format!("#/components/schemas/{resource_name}")
272 }))
273 }))),
274 ),
275 ),
276 (
277 "400".to_string(),
278 operation_outcome_response("Client error"),
279 ),
280 ]),
281 parameters,
282 }
283}
284
285fn delete_resource_operation(parameters: Vec<serde_json::Value>) -> OpenAPIOperation {
286 OpenAPIOperation {
287 request_body: None,
288 responses: HashMap::from([
289 (
290 "200".to_string(),
291 response("Successful delete operation", None),
292 ),
293 (
294 "400".to_string(),
295 operation_outcome_response("Client error"),
296 ),
297 ]),
298 parameters,
299 }
300}
301
302fn resource_search_parameters_schema(
303 resource_name: &str,
304 search_parameters: &[SearchParameter],
305) -> Vec<serde_json::Value> {
306 search_parameters
307 .iter()
308 .filter(|sp| {
309 sp.base.iter().any(|b| {
310 let base = b.as_str();
311
312 base == Some(resource_name)
313 || base == Some("Resource")
314 || base == Some("DomainResource")
315 }) && sp.type_ != SearchParamType::composite()
316 })
317 .map(|sp| {
318 let search_type = if sp.type_ == SearchParamType::number() {
319 "number"
320 } else {
321 "string"
322 };
323
324 json!({
325 "name": sp.code.value,
326 "in": "query",
327 "required": false,
328 "schema": {
329 "type": search_type
330 },
331 "description": sp.description.value.as_deref().unwrap_or_default()
332 })
333 })
334 .collect()
335}
336
337pub fn open_api_schema_generator<S: std::hash::BuildHasher>(
370 server_root: &str,
371 api_version: &str,
372 schema_base_url: &str,
373 sds: &[StructureDefinition],
374 search_parameters: &[SearchParameter],
375 supported_resource_names: &HashSet<String, S>,
376) -> Result<OpenAPI, OperationOutcomeError> {
377 let mut openapi_schema = create_openapi_schema(server_root, api_version);
378
379 add_resource_schemas(
380 &mut openapi_schema,
381 sds,
382 search_parameters,
383 supported_resource_names,
384 schema_base_url,
385 )?;
386
387 add_complex_type_schemas(&mut openapi_schema, sds, schema_base_url);
388
389 Ok(openapi_schema)
390}
391
392fn create_openapi_schema(server_root: &str, api_version: &str) -> OpenAPI {
393 let mut fhir_server_variables = HashMap::new();
394
395 fhir_server_variables.insert(
396 "tenant".to_string(),
397 OpenAPIServerVariable {
398 default: "my-tenant".to_string(),
399 description: Some("Tenant identifier".to_string()),
400 },
401 );
402
403 fhir_server_variables.insert(
404 "project".to_string(),
405 OpenAPIServerVariable {
406 default: "my-project".to_string(),
407 description: Some("Project identifier".to_string()),
408 },
409 );
410
411 fhir_server_variables.insert(
412 "fhir_version".to_string(),
413 OpenAPIServerVariable {
414 default: "r4".to_string(),
415 description: Some("FHIR version".to_string()),
416 },
417 );
418
419 OpenAPI {
420 openapi: "3.1.1".to_string(),
421 servers: vec![OpenAPIServer {
422 url: format!(
423 "{}/w/{}/{}/api/v1/fhir/{}",
424 server_root, "{tenant}", "{project}", "{fhir_version}"
425 ),
426 description: Some("Haste Health FHIR Server".to_string()),
427 variables: fhir_server_variables,
428 }],
429 info: OpenAPIInfo {
430 title: "Haste Health API Documentation".to_string(),
431 version: api_version.to_string(),
432 },
433 components: OpenAPIComponents {
434 schemas: HashMap::new(),
435 },
436 paths: HashMap::new(),
437 }
438}
439
440fn add_resource_schemas<S: std::hash::BuildHasher>(
441 openapi_schema: &mut OpenAPI,
442 sds: &[StructureDefinition],
443 search_parameters: &[SearchParameter],
444 supported_resource_names: &HashSet<String, S>,
445 schema_base_url: &str,
446) -> Result<(), OperationOutcomeError> {
447 let resource_sds = sds
448 .iter()
449 .filter(|sd| sd.kind == StructureDefinitionKind::resource())
450 .filter(|sd| {
451 sd.type_
452 .value
453 .as_ref()
454 .is_some_and(|name| supported_resource_names.contains(name))
455 });
456
457 for sd in resource_sds {
458 let resource_name = sd.type_.value.as_ref().ok_or_else(|| {
459 OperationOutcomeError::error(
460 IssueType::structure(),
461 format!(
462 "StructureDefinition missing type for id {}",
463 sd.id.as_ref().unwrap_or(&"unknown".to_string())
464 ),
465 )
466 })?;
467
468 add_resource_schema(
469 openapi_schema,
470 resource_name,
471 search_parameters,
472 schema_base_url,
473 );
474 }
475
476 Ok(())
477}
478
479fn add_resource_schema(
480 openapi_schema: &mut OpenAPI,
481 resource_name: &str,
482 search_parameters: &[SearchParameter],
483 schema_base_url: &str,
484) {
485 openapi_schema.components.schemas.insert(
488 resource_name.to_string(),
489 json!({
490 "$ref": format!("{}/{}", schema_base_url, resource_name)
491 }),
492 );
493
494 openapi_schema.paths.insert(
496 format!("/{resource_name}/{{id}}"),
497 OpenAPIPathItem {
498 get: Some(read_resource_operation(resource_name)),
499 post: None,
500 patch: Some(patch_resource_operation(resource_name)),
501 put: Some(put_resource_operation(resource_name)),
502 delete: Some(delete_instance_operation(resource_name)),
503 },
504 );
505
506 let resource_search_parameters =
508 resource_search_parameters_schema(resource_name, search_parameters);
509
510 openapi_schema.paths.insert(
511 format!("/{resource_name}"),
512 OpenAPIPathItem {
513 get: Some(search_resource_operation(
514 resource_name,
515 resource_search_parameters.clone(),
516 )),
517 patch: None,
518 put: None,
519 post: Some(create_resource_operation(resource_name)),
520 delete: Some(delete_resource_operation(resource_search_parameters)),
521 },
522 );
523}
524
525fn add_complex_type_schemas(
526 openapi_schema: &mut OpenAPI,
527 sds: &[StructureDefinition],
528 schema_base_url: &str,
529) {
530 for sd in sds.iter().filter(|sd| {
537 sd.kind == StructureDefinitionKind::complex_type()
538 || sd.name.value.as_deref() == Some("Element")
539 }) {
540 let Some(type_name) = sd.type_.value.as_ref() else {
541 continue;
542 };
543
544 openapi_schema.components.schemas.insert(
545 type_name.clone(),
546 json!({
547 "$ref": format!("{}/{}", schema_base_url, type_name)
548 }),
549 );
550 }
551}
552
553pub fn all_resource_names(sds: &[StructureDefinition]) -> HashSet<String> {
558 sds.iter()
559 .filter(|sd| sd.kind == StructureDefinitionKind::resource())
560 .filter_map(|sd| sd.type_.value.clone())
561 .collect()
562}