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 = 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 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}