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 OpenAPI {
412 openapi: "3.1.1".to_string(),
413 servers: vec![OpenAPIServer {
414 url: format!(
417 "{}/w/{}/{}/api/v1/fhir",
418 server_root, "{tenant}", "{project}"
419 ),
420 description: Some("Haste Health FHIR Server".to_string()),
421 variables: fhir_server_variables,
422 }],
423 info: OpenAPIInfo {
424 title: "Haste Health API Documentation".to_string(),
425 version: api_version.to_string(),
426 },
427 components: OpenAPIComponents {
428 schemas: HashMap::new(),
429 },
430 paths: HashMap::new(),
431 }
432}
433
434fn add_resource_schemas<S: std::hash::BuildHasher>(
435 openapi_schema: &mut OpenAPI,
436 sds: &[StructureDefinition],
437 search_parameters: &[SearchParameter],
438 supported_resource_names: &HashSet<String, S>,
439 schema_base_url: &str,
440) -> Result<(), OperationOutcomeError> {
441 let resource_sds = sds
442 .iter()
443 .filter(|sd| sd.kind == StructureDefinitionKind::resource())
444 .filter(|sd| {
445 sd.type_
446 .value
447 .as_ref()
448 .is_some_and(|name| supported_resource_names.contains(name))
449 });
450
451 for sd in resource_sds {
452 let resource_name = sd.type_.value.as_ref().ok_or_else(|| {
453 OperationOutcomeError::error(
454 IssueType::structure(),
455 format!(
456 "StructureDefinition missing type for id {}",
457 sd.id.as_ref().unwrap_or(&"unknown".to_string())
458 ),
459 )
460 })?;
461
462 add_resource_schema(
463 openapi_schema,
464 resource_name,
465 search_parameters,
466 schema_base_url,
467 );
468 }
469
470 Ok(())
471}
472
473fn add_resource_schema(
474 openapi_schema: &mut OpenAPI,
475 resource_name: &str,
476 search_parameters: &[SearchParameter],
477 schema_base_url: &str,
478) {
479 openapi_schema.components.schemas.insert(
482 resource_name.to_string(),
483 json!({
484 "$ref": format!("{}/{}", schema_base_url, resource_name)
485 }),
486 );
487
488 openapi_schema.paths.insert(
490 format!("/{resource_name}/{{id}}"),
491 OpenAPIPathItem {
492 get: Some(read_resource_operation(resource_name)),
493 post: None,
494 patch: Some(patch_resource_operation(resource_name)),
495 put: Some(put_resource_operation(resource_name)),
496 delete: Some(delete_instance_operation(resource_name)),
497 },
498 );
499
500 let resource_search_parameters =
502 resource_search_parameters_schema(resource_name, search_parameters);
503
504 openapi_schema.paths.insert(
505 format!("/{resource_name}"),
506 OpenAPIPathItem {
507 get: Some(search_resource_operation(
508 resource_name,
509 resource_search_parameters.clone(),
510 )),
511 patch: None,
512 put: None,
513 post: Some(create_resource_operation(resource_name)),
514 delete: Some(delete_resource_operation(resource_search_parameters)),
515 },
516 );
517}
518
519fn add_complex_type_schemas(
520 openapi_schema: &mut OpenAPI,
521 sds: &[StructureDefinition],
522 schema_base_url: &str,
523) {
524 for sd in sds.iter().filter(|sd| {
531 sd.kind == StructureDefinitionKind::complex_type()
532 || sd.name.value.as_deref() == Some("Element")
533 }) {
534 let Some(type_name) = sd.type_.value.as_ref() else {
535 continue;
536 };
537
538 openapi_schema.components.schemas.insert(
539 type_name.clone(),
540 json!({
541 "$ref": format!("{}/{}", schema_base_url, type_name)
542 }),
543 );
544 }
545}
546
547pub fn all_resource_names(sds: &[StructureDefinition]) -> HashSet<String> {
552 sds.iter()
553 .filter(|sd| sd.kind == StructureDefinitionKind::resource())
554 .filter_map(|sd| sd.type_.value.clone())
555 .collect()
556}