1use crate::{FHIRTerminology, resolvers::CanonicalResolver};
2use haste_fhir_generated_ops::generated::{CodeSystemLookup, ValueSetExpand, ValueSetValidateCode};
3use haste_fhir_model::r4::{
4 datetime::DateTime,
5 generated::{
6 resources::{
7 CodeSystem, CodeSystemConcept, Resource, ResourceType, ValueSet,
8 ValueSetComposeInclude, ValueSetComposeIncludeConceptDesignation, ValueSetExpansion,
9 ValueSetExpansionContains,
10 },
11 terminology::{CodesystemContentMode, IssueType},
12 types::{FHIRBoolean, FHIRDateTime, FHIRString, FHIRUri},
13 },
14};
15use haste_fhir_operation_error::OperationOutcomeError;
16use std::{borrow::Cow, pin::Pin, sync::Arc};
17
18pub struct FHIRCanonicalTerminology<Resolver: CanonicalResolver> {
19 resolver: Arc<Resolver>,
20}
21
22impl<Resolver: CanonicalResolver> FHIRCanonicalTerminology<Resolver> {
23 pub fn new(resolver: Resolver) -> Self {
24 FHIRCanonicalTerminology {
25 resolver: Arc::new(resolver),
26 }
27 }
28}
29
30async fn resolve_valueset<Resolver: CanonicalResolver>(
31 canonical_resolution: Arc<Resolver>,
32 input: ValueSetExpand::Input,
33) -> Result<Option<ValueSet>, OperationOutcomeError> {
34 if let Some(valueset) = input.valueSet.as_ref() {
35 return Ok(Some(valueset.clone()));
36 } else if let Some(url) = &input.url.as_ref().and_then(|u| u.value.as_ref()) {
37 let Resource::ValueSet(value_set) = canonical_resolution
38 .resolve(ResourceType::ValueSet, url.to_string())
39 .await?
40 else {
41 return Ok(None);
42 };
43
44 return Ok(Some(value_set));
45 }
46 Ok(None)
47}
48
49fn are_codes_inline(include: &ValueSetComposeInclude) -> bool {
50 include.concept.is_some()
51}
52
53fn codes_inline_to_expansion(include: &ValueSetComposeInclude) -> Vec<ValueSetExpansionContains> {
54 include
55 .concept
56 .as_ref()
57 .map(|v| Cow::Borrowed(v))
58 .unwrap_or(Cow::Owned(vec![]))
59 .iter()
60 .map(|c| ValueSetExpansionContains {
61 system: include.system.clone(),
62 code: Some(c.code.clone()),
63 display: c.display.clone(),
64 ..Default::default()
65 })
66 .collect()
67}
68
69async fn resolve_codesystem<Resolver: CanonicalResolver>(
70 canonical_resolution: Arc<Resolver>,
71 url: &str,
72) -> Result<Option<CodeSystem>, OperationOutcomeError> {
73 let Resource::CodeSystem(code_system) = canonical_resolution
74 .resolve(ResourceType::CodeSystem, url.to_string())
75 .await?
76 else {
77 return Ok(None);
78 };
79
80 Ok(Some(code_system))
81}
82
83async fn get_concepts(
84 codesystem: CodeSystem,
85) -> Result<Vec<CodeSystemConcept>, OperationOutcomeError> {
86 match codesystem.content.as_ref() {
87 CodesystemContentMode::NotPresent(_) => Err(OperationOutcomeError::error(
88 IssueType::NotSupported(None),
89 "CodeSystem content is 'not-present'".to_string(),
90 )),
91 CodesystemContentMode::Fragment(_)
92 | CodesystemContentMode::Complete(_)
93 | CodesystemContentMode::Supplement(_) => {
94 Ok(codesystem.concept.clone().unwrap_or_default())
95 }
96 _ => Err(OperationOutcomeError::error(
97 IssueType::Invalid(None),
98 "CodeSystem content has invalid value".to_string(),
99 )),
100 }
101}
102
103fn code_system_concept_to_valueset_expansion(
104 url: Option<&str>,
105 version: Option<&str>,
106 codesystem_concept: Vec<CodeSystemConcept>,
107) -> Vec<ValueSetExpansionContains> {
108 codesystem_concept
109 .into_iter()
110 .map(|c| ValueSetExpansionContains {
111 system: url.map(|url| {
112 Box::new(FHIRUri {
113 value: Some(url.to_string()),
114 ..Default::default()
115 })
116 }),
117 version: version.map(|v| {
118 Box::new(FHIRString {
119 value: Some(v.to_string()),
120 ..Default::default()
121 })
122 }),
123 code: Some(c.code),
124 display: c.display,
125 designation: c.designation.map(|designations| {
126 designations
127 .into_iter()
128 .map(|d| ValueSetComposeIncludeConceptDesignation {
129 id: d.id,
130 extension: d.extension,
131 modifierExtension: d.modifierExtension,
132 language: d.language,
133 use_: d.use_,
134 value: d.value,
135 })
136 .collect::<Vec<_>>()
137 }),
138 contains: if let Some(concepts) = c.concept {
139 Some(code_system_concept_to_valueset_expansion(
140 url, version, concepts,
141 ))
142 } else {
143 None
144 },
145 ..Default::default()
146 })
147 .collect()
148}
149
150async fn get_valueset_expansion_contains<Resolver: CanonicalResolver + Send + Sync + 'static>(
151 canonical_resolution: Arc<Resolver>,
152 include: &ValueSetComposeInclude,
153) -> Result<Vec<ValueSetExpansionContains>, OperationOutcomeError> {
154 if are_codes_inline(include) {
155 Ok(codes_inline_to_expansion(include))
156 } else if let Some(valueset_uris) = include.valueSet.as_ref() {
157 let mut contains = vec![];
158 for valueset_uri in valueset_uris {
159 if let Some(valueset_uri) = valueset_uri.value.as_ref() {
160 let output = expand_valueset(
161 canonical_resolution.clone(),
162 ValueSetExpand::Input {
163 url: Some(FHIRUri {
164 value: Some(valueset_uri.to_string()),
165 ..Default::default()
166 }),
167 valueSet: None,
168 valueSetVersion: None,
169 context: None,
170 contextDirection: None,
171 filter: None,
172 date: None,
173 offset: None,
174 count: None,
175 includeDesignations: None,
176 designation: None,
177 includeDefinition: None,
178 activeOnly: None,
179 excludeNested: None,
180 excludeNotForUI: None,
181 excludePostCoordinated: None,
182 displayLanguage: None,
183 exclude_system: None,
184 system_version: None,
185 check_system_version: None,
186 force_system_version: None,
187 },
188 )
189 .await?;
190
191 contains.extend(
192 output
193 .return_
194 .expansion
195 .unwrap_or_default()
196 .contains
197 .unwrap_or_default(),
198 )
199 }
200 }
201 Ok(contains)
202 } else if let Some(system) = include.system.as_ref()
203 && let Some(uri) = system.value.as_ref()
204 && let Some(code_system) =
205 resolve_codesystem(canonical_resolution.clone(), uri.as_str()).await?
206 {
207 let url = code_system.url.clone();
208 let version = code_system.version.clone();
209
210 return Ok(code_system_concept_to_valueset_expansion(
211 url.and_then(|v| v.value).as_ref().map(|url| url.as_str()),
212 version.and_then(|v| v.value).as_ref().map(|v| v.as_str()),
213 get_concepts(code_system).await?,
214 ));
215 } else {
216 Ok(vec![])
217 }
218}
219
220async fn get_valueset_expansion<Resolver: CanonicalResolver + Sync + Send + 'static>(
221 canonical_resolution: Arc<Resolver>,
222 value_set: &ValueSet,
223) -> Result<Vec<ValueSetExpansionContains>, OperationOutcomeError> {
224 let mut result = Vec::new();
225 if let Some(compose) = value_set.compose.as_ref() {
226 for include in compose.include.iter() {
227 result.extend(
228 get_valueset_expansion_contains(canonical_resolution.clone(), include).await?,
229 );
230 }
231 }
232 Ok(result)
233}
234
235fn expand_valueset<Resolver: CanonicalResolver + Sync + Send + 'static>(
236 canonical_resolution: Arc<Resolver>,
237 input: ValueSetExpand::Input,
238) -> Pin<Box<dyn Future<Output = Result<ValueSetExpand::Output, OperationOutcomeError>> + Send>> {
239 Box::pin(async move {
241 let value_set = resolve_valueset(canonical_resolution.clone(), input).await?;
242
243 if let Some(mut value_set) = value_set {
244 let contains = get_valueset_expansion(canonical_resolution.clone(), &value_set).await?;
245 value_set.expansion = Some(ValueSetExpansion {
246 contains: Some(contains),
247 timestamp: Box::new(FHIRDateTime {
248 value: Some(DateTime::Iso8601(chrono::Utc::now())),
249 ..Default::default()
250 }),
251 ..Default::default()
252 });
253
254 Ok(ValueSetExpand::Output { return_: value_set })
255 } else {
256 return Err(OperationOutcomeError::error(
257 IssueType::NotFound(None),
258 "ValueSet could not be resolved".to_string(),
259 ));
260 }
261 })
262}
263
264impl<Resolver: CanonicalResolver + Send + Sync + 'static> FHIRTerminology
265 for FHIRCanonicalTerminology<Resolver>
266{
267 async fn expand(
268 &self,
269 input: ValueSetExpand::Input,
270 ) -> Result<ValueSetExpand::Output, OperationOutcomeError> {
271 expand_valueset(self.resolver.clone(), input).await
272 }
273 async fn validate(
274 &self,
275 input: ValueSetValidateCode::Input,
276 ) -> Result<ValueSetValidateCode::Output, OperationOutcomeError> {
277 let Some(code) = input.code else {
278 return Err(OperationOutcomeError::error(
279 IssueType::Invalid(None),
280 "No code provided for validation only support 'code' field validation".to_string(),
281 ));
282 };
283
284 let expansion = self
286 .expand(ValueSetExpand::Input {
287 url: input.url,
288 valueSet: input.valueSet,
289 valueSetVersion: input.valueSetVersion,
290 context: input.context,
291 contextDirection: None,
292 filter: None,
293 date: None,
294 offset: None,
295 count: None,
296 includeDesignations: None,
297 designation: None,
298 includeDefinition: None,
299 activeOnly: None,
300 excludeNested: None,
301 excludeNotForUI: None,
302 excludePostCoordinated: None,
303 displayLanguage: None,
304 exclude_system: None,
305 system_version: None,
306 check_system_version: None,
307 force_system_version: None,
308 })
309 .await?;
310
311 let valueset = expansion.return_;
312
313 if let Some(expansion) = valueset.expansion
314 && let Some(contains) = expansion.contains
315 {
316 for contain in contains {
317 if contain
318 .code
319 .as_ref()
320 .map(|c| &c.value == &code.value)
321 .unwrap_or(false)
322 {
323 return Ok(ValueSetValidateCode::Output {
324 result: FHIRBoolean {
325 value: Some(true),
326 ..Default::default()
327 },
328 display: None,
329 message: Some(FHIRString {
330 value: Some("Code is valid in the ValueSet".to_string()),
331 ..Default::default()
332 }),
333 });
334 }
335 }
336 }
337
338 Ok(ValueSetValidateCode::Output {
339 result: FHIRBoolean {
340 value: Some(false),
341 ..Default::default()
342 },
343 display: None,
344 message: Some(FHIRString {
345 value: Some("Code is valid in the ValueSet".to_string()),
346 ..Default::default()
347 }),
348 })
349 }
350 async fn lookup(
351 &self,
352 _input: CodeSystemLookup::Input,
353 ) -> Result<CodeSystemLookup::Output, OperationOutcomeError> {
354 unimplemented!()
356 }
357}