haste_fhir_terminology/
client.rs

1use crate::{FHIRTerminology, resolvers::CanonicalResolver};
2use haste_fhir_generated_ops::generated::{CodeSystemLookup, ValueSetExpand, ValueSetValidateCode};
3use haste_fhir_model::r4::generated::{
4    resources::{
5        CodeSystem, CodeSystemConcept, Resource, ResourceType, ValueSet, ValueSetComposeInclude,
6        ValueSetComposeIncludeConceptDesignation, ValueSetExpansion, ValueSetExpansionContains,
7    },
8    terminology::{CodesystemContentMode, IssueType},
9    types::{FHIRString, FHIRUri},
10};
11use haste_fhir_operation_error::OperationOutcomeError;
12use std::{borrow::Cow, pin::Pin, sync::Arc};
13
14pub struct FHIRCanonicalTerminology<Resolver: CanonicalResolver> {
15    resolver: Arc<Resolver>,
16}
17
18impl<Resolver: CanonicalResolver> FHIRCanonicalTerminology<Resolver> {
19    pub fn new(resolver: Resolver) -> Self {
20        FHIRCanonicalTerminology {
21            resolver: Arc::new(resolver),
22        }
23    }
24}
25
26async fn resolve_valueset<Resolver: CanonicalResolver>(
27    canonical_resolution: Arc<Resolver>,
28    input: ValueSetExpand::Input,
29) -> Result<Option<ValueSet>, OperationOutcomeError> {
30    if let Some(valueset) = input.valueSet.as_ref() {
31        return Ok(Some(valueset.clone()));
32    } else if let Some(url) = &input.url.as_ref().and_then(|u| u.value.as_ref()) {
33        let Resource::ValueSet(value_set) = canonical_resolution
34            .resolve(ResourceType::ValueSet, url.to_string())
35            .await?
36        else {
37            return Ok(None);
38        };
39
40        return Ok(Some(value_set));
41    }
42    Ok(None)
43}
44
45fn are_codes_inline(include: &ValueSetComposeInclude) -> bool {
46    include.concept.is_some()
47}
48
49fn codes_inline_to_expansion(include: &ValueSetComposeInclude) -> Vec<ValueSetExpansionContains> {
50    include
51        .concept
52        .as_ref()
53        .map(|v| Cow::Borrowed(v))
54        .unwrap_or(Cow::Owned(vec![]))
55        .iter()
56        .map(|c| ValueSetExpansionContains {
57            system: include.system.clone(),
58            code: Some(c.code.clone()),
59            display: c.display.clone(),
60            ..Default::default()
61        })
62        .collect()
63}
64
65async fn resolve_codesystem<Resolver: CanonicalResolver>(
66    canonical_resolution: Arc<Resolver>,
67    url: &str,
68) -> Result<Option<CodeSystem>, OperationOutcomeError> {
69    let Resource::CodeSystem(code_system) = canonical_resolution
70        .resolve(ResourceType::CodeSystem, url.to_string())
71        .await?
72    else {
73        return Ok(None);
74    };
75
76    Ok(Some(code_system))
77}
78
79async fn get_concepts(
80    codesystem: CodeSystem,
81) -> Result<Vec<CodeSystemConcept>, OperationOutcomeError> {
82    match codesystem.content.as_ref() {
83        CodesystemContentMode::NotPresent(_) => Err(OperationOutcomeError::error(
84            IssueType::NotSupported(None),
85            "CodeSystem content is 'not-present'".to_string(),
86        )),
87        CodesystemContentMode::Fragment(_)
88        | CodesystemContentMode::Complete(_)
89        | CodesystemContentMode::Supplement(_) => {
90            Ok(codesystem.concept.clone().unwrap_or_default())
91        }
92        _ => Err(OperationOutcomeError::error(
93            IssueType::Invalid(None),
94            "CodeSystem content has invalid value".to_string(),
95        )),
96    }
97}
98
99fn code_system_concept_to_valueset_expansion(
100    url: Option<&str>,
101    version: Option<&str>,
102    codesystem_concept: Vec<CodeSystemConcept>,
103) -> Vec<ValueSetExpansionContains> {
104    codesystem_concept
105        .into_iter()
106        .map(|c| ValueSetExpansionContains {
107            system: url.map(|url| {
108                Box::new(FHIRUri {
109                    value: Some(url.to_string()),
110                    ..Default::default()
111                })
112            }),
113            version: version.map(|v| {
114                Box::new(FHIRString {
115                    value: Some(v.to_string()),
116                    ..Default::default()
117                })
118            }),
119            code: Some(c.code),
120            display: c.display,
121            designation: c.designation.map(|designations| {
122                designations
123                    .into_iter()
124                    .map(|d| ValueSetComposeIncludeConceptDesignation {
125                        id: d.id,
126                        extension: d.extension,
127                        modifierExtension: d.modifierExtension,
128                        language: d.language,
129                        use_: d.use_,
130                        value: d.value,
131                    })
132                    .collect::<Vec<_>>()
133            }),
134            contains: if let Some(concepts) = c.concept {
135                Some(code_system_concept_to_valueset_expansion(
136                    url, version, concepts,
137                ))
138            } else {
139                None
140            },
141            ..Default::default()
142        })
143        .collect()
144}
145
146async fn get_valueset_expansion_contains<Resolver: CanonicalResolver + Send + Sync + 'static>(
147    canonical_resolution: Arc<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(code_system) =
201            resolve_codesystem(canonical_resolution.clone(), uri.as_str()).await?
202    {
203        let url = code_system.url.clone();
204        let version = code_system.version.clone();
205
206        return Ok(code_system_concept_to_valueset_expansion(
207            url.and_then(|v| v.value).as_ref().map(|url| url.as_str()),
208            version.and_then(|v| v.value).as_ref().map(|v| v.as_str()),
209            get_concepts(code_system).await?,
210        ));
211    } else {
212        Ok(vec![])
213    }
214}
215
216async fn get_valueset_expansion<Resolver: CanonicalResolver + Sync + Send + 'static>(
217    canonical_resolution: Arc<Resolver>,
218    value_set: &ValueSet,
219) -> Result<Vec<ValueSetExpansionContains>, OperationOutcomeError> {
220    let mut result = Vec::new();
221    if let Some(compose) = value_set.compose.as_ref() {
222        for include in compose.include.iter() {
223            result.extend(
224                get_valueset_expansion_contains(canonical_resolution.clone(), include).await?,
225            );
226        }
227    }
228    Ok(result)
229}
230
231fn expand_valueset<Resolver: CanonicalResolver + Sync + Send + 'static>(
232    canonical_resolution: Arc<Resolver>,
233    input: ValueSetExpand::Input,
234) -> Pin<Box<dyn Future<Output = Result<ValueSetExpand::Output, OperationOutcomeError>> + Send>> {
235    // Implementation would go here
236    Box::pin(async move {
237        let value_set = resolve_valueset(canonical_resolution.clone(), input).await?;
238
239        if let Some(mut value_set) = value_set {
240            let contains = get_valueset_expansion(canonical_resolution.clone(), &value_set).await?;
241            value_set.expansion = Some(ValueSetExpansion {
242                contains: Some(contains),
243                ..Default::default()
244            });
245
246            Ok(ValueSetExpand::Output { return_: value_set })
247        } else {
248            return Err(OperationOutcomeError::error(
249                IssueType::NotFound(None),
250                "ValueSet could not be resolved".to_string(),
251            ));
252        }
253    })
254}
255
256impl<Resolver: CanonicalResolver + Send + Sync + 'static> FHIRTerminology
257    for FHIRCanonicalTerminology<Resolver>
258{
259    async fn expand(
260        &self,
261        input: ValueSetExpand::Input,
262    ) -> Result<ValueSetExpand::Output, OperationOutcomeError> {
263        expand_valueset(self.resolver.clone(), input).await
264    }
265    async fn validate(
266        &self,
267        _input: ValueSetValidateCode::Input,
268    ) -> Result<ValueSetValidateCode::Output, OperationOutcomeError> {
269        // Implementation would go here
270        unimplemented!()
271    }
272    async fn lookup(
273        &self,
274        _input: CodeSystemLookup::Input,
275    ) -> Result<CodeSystemLookup::Output, OperationOutcomeError> {
276        // Implementation would go here
277        unimplemented!()
278    }
279}