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