Skip to main content

haste_fhir_terminology/
client.rs

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    // Implementation would go here
237    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        // Implementation would go here
288        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        // Implementation would go here
362        unimplemented!()
363    }
364}