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 #[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: Option<String> = b.as_ref().into();
264 let base = base.as_ref().map(|s| s.as_str());
265 base == Some(resource_name)
266 || base == Some("Resource")
267 || base == Some("DomainResource")
268 }) && !matches!(sp.type_.as_ref(), &SearchParamType::Composite(_))
269 }) {
270 let search_type = match sp.type_.as_ref() {
271 SearchParamType::Quantity(_)
272 | SearchParamType::Special(_)
273 | SearchParamType::Token(_)
274 | SearchParamType::Uri(_)
275 | SearchParamType::Null(_)
276 | SearchParamType::Reference(_)
277 | SearchParamType::Composite(_)
278 | SearchParamType::Date(_)
279 | SearchParamType::String(_) => "string",
280
281 SearchParamType::Number(_) => "number",
282 };
283
284 params.push(json!({
285 "name": sp.code.value,
286 "in": "query",
287 "required": false,
288 "schema": {
289 "type": search_type
290 },
291 "description": sp.description.value.as_ref().map(|s| s.as_str()).unwrap_or("")
292 }));
293 }
294
295 params
296}
297
298fn create_resource_operation(resource_name: &str) -> OpenAPIOperation {
299 OpenAPIOperation {
300 request_body: Some(OpenAPIOperationContent {
301 description: format!("The {} resource to create", resource_name),
302 content: Some(HashMap::from([(
303 "application/json".to_string(),
304 json!({ "schema": {"$ref": format!("#/components/schemas/{}", resource_name) }}),
305 )])),
306 }),
307 responses: HashMap::from([
308 (
309 "200".to_string(),
310 OpenAPIOperationContent {
311 description: format!("Successful creation of {} resource", resource_name),
312 content: Some(HashMap::from([(
313 "application/json".to_string(),
314 json!({ "schema": {"$ref": format!("#/components/schemas/{}", resource_name) }}),
315 )])),
316 },
317 ),
318 (
319 "400".to_string(),
320 OpenAPIOperationContent {
321 description: "Client error".to_string(),
322 content: Some(HashMap::from([(
323 "application/json".to_string(),
324 json!({ "schema": {"$ref": "#/components/schemas/OperationOutcome" }}),
325 )])),
326 },
327 ),
328 ]),
329 parameters: vec![],
330 }
331}
332
333fn search_resource_operation(
334 resource_name: &str,
335 parameters: Vec<serde_json::Value>,
336) -> OpenAPIOperation {
337 OpenAPIOperation {
338 request_body: None,
339 responses: HashMap::from([
340 (
341 "200".to_string(),
342 OpenAPIOperationContent {
343 description: "Successful search operation".to_string(),
344 content: Some(HashMap::from([(
345 "application/json".to_string(),
346 json!({ "schema": haste_sd_to_json_schema::bundle_of_resource(json!({
347 "$ref": format!("#/components/schemas/{}", resource_name)
348 })) }),
349 )])),
350 },
351 ),
352 (
353 "400".to_string(),
354 OpenAPIOperationContent {
355 description: "Client error".to_string(),
356 content: Some(HashMap::from([(
357 "application/json".to_string(),
358 json!({ "schema": {"$ref": "#/components/schemas/OperationOutcome" }}),
359 )])),
360 },
361 ),
362 ]),
363 parameters,
364 }
365}
366
367fn delete_resource_operation(parameters: Vec<serde_json::Value>) -> OpenAPIOperation {
368 OpenAPIOperation {
369 request_body: None,
370 responses: HashMap::from([
371 (
372 "200".to_string(),
373 OpenAPIOperationContent {
374 description: "Successful delete operation".to_string(),
375 content: None,
376 },
377 ),
378 (
379 "400".to_string(),
380 OpenAPIOperationContent {
381 description: "Client error".to_string(),
382 content: Some(HashMap::from([(
383 "application/json".to_string(),
384 json!({ "schema": {"$ref": "#/components/schemas/OperationOutcome" }}),
385 )])),
386 },
387 ),
388 ]),
389 parameters,
390 }
391}
392
393pub fn open_api_schema_generator(
394 server_root: &str,
395 api_version: &str,
396 sds: &Vec<StructureDefinition>,
397 search_parameters: &Vec<SearchParameter>,
398) -> Result<OpenAPI, OperationOutcomeError> {
399 let mut fhir_server_variables = HashMap::new();
400 fhir_server_variables.insert(
401 "tenant".to_string(),
402 OpenAPIServerVariable {
403 default: "my-tenant".to_string(),
404 description: Some("Tenant identifier".to_string()),
405 },
406 );
407 fhir_server_variables.insert(
408 "project".to_string(),
409 OpenAPIServerVariable {
410 default: "my-project".to_string(),
411 description: Some("Project identifier".to_string()),
412 },
413 );
414 fhir_server_variables.insert(
415 "fhir_version".to_string(),
416 OpenAPIServerVariable {
417 default: "r4".to_string(),
418 description: Some("FHIR version".to_string()),
419 },
420 );
421 let mut openapi_schema = OpenAPI {
422 openapi: "3.1.1".to_string(),
423 servers: vec![OpenAPIServer {
424 url: format!(
425 "{}/w/{}/{}/api/v1/fhir/{}",
426 server_root, "{tenant}", "{project}", "{fhir_version}"
427 ),
428 description: Some("Haste Health FHIR Server".to_string()),
429 variables: fhir_server_variables,
430 }],
431 info: OpenAPIInfo {
432 title: "Haste Health API Documentation".to_string(),
433 version: api_version.to_string(),
434 },
435 components: OpenAPIComponents {
436 schemas: HashMap::new(),
437 },
438
439 paths: HashMap::new(),
440 };
441
442 let complex_sds = sds.iter().filter(|sd| match sd.kind.as_ref() {
443 StructureDefinitionKind::ComplexType(_) => true,
444 _ => false,
445 });
446
447 for sd in complex_sds {
448 let json_schema = haste_sd_to_json_schema::isolated_schema("#/components/schemas", sd)?;
449 let type_name = sd.type_.value.as_ref().ok_or_else(|| {
450 OperationOutcomeError::error(
451 IssueType::Structure(None),
452 format!(
453 "StructureDefinition missing type for id {}",
454 sd.id.as_ref().unwrap_or(&"unknown".to_string())
455 ),
456 )
457 })?;
458 openapi_schema
459 .components
460 .schemas
461 .insert(type_name.clone(), json_schema);
462 }
463
464 let resource_sds = sds.iter().filter(|sd| match sd.kind.as_ref() {
465 StructureDefinitionKind::Resource(_) => true,
466 _ => false,
467 });
468
469 for sd in resource_sds {
470 let json_schema = haste_sd_to_json_schema::isolated_schema("#/components/schemas", sd)?;
471 let resource_name = sd.type_.value.as_ref().ok_or_else(|| {
472 OperationOutcomeError::error(
473 IssueType::Structure(None),
474 format!(
475 "StructureDefinition missing type for id {}",
476 sd.id.as_ref().unwrap_or(&"unknown".to_string())
477 ),
478 )
479 })?;
480
481 openapi_schema.paths.insert(
483 format!("/{}/{{id}}", resource_name),
484 OpenAPIPathItem {
485 get: Some(read_resource_operation(&resource_name)),
486 post: None,
487 patch: Some(patch_resource_operation(&resource_name)),
488 put: Some(put_resource_operation(&resource_name)),
489 delete: Some(delete_instance_operation(&resource_name)),
490 },
491 );
492
493 let resource_search_parameters =
494 resource_search_parameters_schema(&resource_name, search_parameters);
495
496 openapi_schema.paths.insert(
497 format!("/{}", resource_name),
498 OpenAPIPathItem {
499 get: Some(search_resource_operation(
500 &resource_name,
501 resource_search_parameters.clone(),
502 )),
503 patch: None,
504 put: None,
505 post: Some(create_resource_operation(&resource_name)),
506 delete: Some(delete_resource_operation(resource_search_parameters)),
507 },
508 );
509
510 openapi_schema
511 .components
512 .schemas
513 .insert(resource_name.clone(), json_schema);
514 }
515
516 openapi_schema.components.schemas.insert(
517 "Element".to_string(),
518 json!({
519 "additionalProperties": false,
520 "properties": {
521 "extension": {
522 "items": {
523 "$ref": "#/components/schemas/Extension"
524 },
525 "type": "array"
526 },
527 "id": {
528 "type": "string"
529 }
530 },
531 "required": [],
532 "type": "object"
533 }),
534 );
535
536 Ok(openapi_schema)
537}