Skip to main content

haste_fhir_client/
axum.rs

1use crate::request::{
2    DeleteResponse, FHIRAcceptedResponse, FHIRResponse, HistoryResponse, InvokeResponse,
3    SearchResponse,
4};
5use axum::response::IntoResponse;
6use haste_fhir_model::r4::generated::{
7    resources::Resource,
8    terminology::IssueType,
9    types::{FHIRId, FHIRInstant},
10};
11use haste_fhir_operation_error::OperationOutcomeError;
12use haste_reflect::MetaValue;
13use http::{HeaderMap, StatusCode};
14
15fn add_resource_headers(headers: &mut HeaderMap, resource: &Resource) {
16    let _id = resource
17        .get_field("id")
18        .and_then(|id| id.as_any().downcast_ref::<String>());
19
20    let meta = resource.get_field("meta");
21
22    let last_modified = meta
23        .and_then(|meta| meta.get_field("lastUpdated"))
24        .and_then(|lu| lu.as_any().downcast_ref::<FHIRInstant>())
25        .and_then(|lu| lu.value.as_ref());
26
27    let version_id = meta
28        .and_then(|meta| meta.get_field("versionId"))
29        .and_then(|vid| vid.as_any().downcast_ref::<FHIRId>())
30        .and_then(|vid| vid.value.as_ref());
31
32    if let Some(last_modified) = last_modified {
33        headers.insert(
34            axum::http::header::LAST_MODIFIED,
35            last_modified
36                .format("%a, %d %b %Y %H:%M:%S GMT")
37                .parse()
38                .unwrap(),
39        );
40    }
41    if let Some(version_id) = version_id {
42        headers.insert(
43            axum::http::header::ETAG,
44            format!("W/\"{version_id}\"").parse().unwrap(),
45        );
46    }
47}
48
49fn add_headers(response: &FHIRResponse) -> HeaderMap {
50    let mut header = HeaderMap::new();
51    header.insert(
52        axum::http::header::CONTENT_TYPE,
53        "application/fhir+json; charset=utf-8".parse().unwrap(),
54    );
55    // Without this, browsers use Last-Modified to guess a freshness lifetime
56    // and serve reads from their cache for hours after the resource changes.
57    // no-store also keeps responses, which can hold patient data, off disk.
58    header.insert(
59        axum::http::header::CACHE_CONTROL,
60        "no-store".parse().unwrap(),
61    );
62
63    match response {
64        FHIRResponse::Create(resp) => {
65            add_resource_headers(&mut header, &resp.resource);
66        }
67        FHIRResponse::Read(resp) => {
68            if let Some(resource) = &resp.resource {
69                add_resource_headers(&mut header, resource);
70            }
71        }
72        FHIRResponse::VersionRead(resp) => {
73            add_resource_headers(&mut header, &resp.resource);
74        }
75        FHIRResponse::Update(resp) => {
76            add_resource_headers(&mut header, &resp.resource);
77        }
78        FHIRResponse::Patch(fhirpatch_response) => {
79            add_resource_headers(&mut header, &fhirpatch_response.resource);
80        }
81        _ => {}
82    }
83
84    header
85}
86
87impl IntoResponse for FHIRResponse {
88    fn into_response(self) -> axum::response::Response {
89        let header = add_headers(&self);
90
91        match self {
92            FHIRResponse::Create(response) => {
93                FHIRResponse::created_json(header, &response.resource)
94            }
95
96            FHIRResponse::Read(response) => match response.resource {
97                Some(resource) => FHIRResponse::ok_json(header, &resource),
98                None => FHIRResponse::not_found(),
99            },
100
101            FHIRResponse::VersionRead(response) => {
102                FHIRResponse::ok_json(header, &response.resource)
103            }
104
105            FHIRResponse::Update(response) => FHIRResponse::ok_json(header, &response.resource),
106
107            FHIRResponse::Capabilities(response) => {
108                FHIRResponse::ok_json(header, &response.capabilities)
109            }
110
111            FHIRResponse::History(history_response) => {
112                FHIRResponse::history_response(header, history_response)
113            }
114
115            FHIRResponse::Search(search_response) => {
116                FHIRResponse::search_response(header, search_response)
117            }
118
119            FHIRResponse::Batch(response) => FHIRResponse::ok_json(header, &response.resource),
120
121            FHIRResponse::Invoke(invoke_response) => {
122                FHIRResponse::invoke_response(header, invoke_response)
123            }
124
125            FHIRResponse::Delete(delete_response) => {
126                FHIRResponse::delete_response(header, delete_response)
127            }
128
129            FHIRResponse::Patch(response) => FHIRResponse::ok_json(header, &response.resource),
130
131            FHIRResponse::Transaction(response) => {
132                FHIRResponse::ok_json(header, &response.resource)
133            }
134
135            FHIRResponse::Accepted(response) => FHIRResponse::accepted(header, &response),
136        }
137    }
138}
139
140impl FHIRResponse {
141    fn json_response<T: serde::Serialize>(
142        status: StatusCode,
143        header: HeaderMap,
144        value: &T,
145    ) -> axum::response::Response {
146        (
147            status,
148            header,
149            // Unwrap should be safe here.
150            serde_json::to_string(value).unwrap(),
151        )
152            .into_response()
153    }
154
155    fn ok_json<T: serde::Serialize>(header: HeaderMap, value: &T) -> axum::response::Response {
156        Self::json_response(StatusCode::OK, header, value)
157    }
158
159    fn created_json<T: serde::Serialize>(header: HeaderMap, value: &T) -> axum::response::Response {
160        Self::json_response(StatusCode::CREATED, header, value)
161    }
162
163    fn no_content(header: HeaderMap) -> axum::response::Response {
164        (StatusCode::NO_CONTENT, header, "").into_response()
165    }
166
167    /// No body, so no content type either: only the status URL to poll.
168    fn accepted(
169        mut header: HeaderMap,
170        response: &FHIRAcceptedResponse,
171    ) -> axum::response::Response {
172        header.remove(axum::http::header::CONTENT_TYPE);
173        match response.content_location.parse() {
174            Ok(location) => {
175                header.insert(axum::http::header::CONTENT_LOCATION, location);
176                (StatusCode::ACCEPTED, header).into_response()
177            }
178            Err(_) => OperationOutcomeError::fatal(
179                IssueType::exception(),
180                "The job's status URL is not a valid header value.".to_string(),
181            )
182            .into_response(),
183        }
184    }
185
186    fn not_found() -> axum::response::Response {
187        OperationOutcomeError::error(IssueType::not_found(), "Resource not found.".to_string())
188            .into_response()
189    }
190
191    fn history_response(header: HeaderMap, response: HistoryResponse) -> axum::response::Response {
192        match response {
193            HistoryResponse::Instance(response) => Self::ok_json(header, &response.bundle),
194            HistoryResponse::Type(response) => Self::ok_json(header, &response.bundle),
195            HistoryResponse::System(response) => Self::ok_json(header, &response.bundle),
196        }
197    }
198
199    fn search_response(header: HeaderMap, response: SearchResponse) -> axum::response::Response {
200        match response {
201            SearchResponse::Type(response) => Self::ok_json(header, &response.bundle),
202            SearchResponse::System(response) => Self::ok_json(header, &response.bundle),
203        }
204    }
205
206    fn invoke_response(header: HeaderMap, response: InvokeResponse) -> axum::response::Response {
207        match response {
208            InvokeResponse::Instance(response) => Self::ok_json(header, &response.resource),
209            InvokeResponse::Type(response) => Self::ok_json(header, &response.resource),
210            InvokeResponse::System(response) => Self::ok_json(header, &response.resource),
211        }
212    }
213
214    fn delete_response(header: HeaderMap, response: DeleteResponse) -> axum::response::Response {
215        match response {
216            DeleteResponse::Instance(response) => Self::ok_json(header, &response.resource),
217            DeleteResponse::Type(_) | DeleteResponse::System(_) => Self::no_content(header),
218        }
219    }
220}