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 Default for FHIRCanonicalTerminology {
22    fn default() -> Self {
23        Self::new()
24    }
25}
26
27impl FHIRCanonicalTerminology {
28    #[must_use]
29    pub fn new() -> Self {
30        FHIRCanonicalTerminology
31    }
32}
33
34async fn resolve_valueset<Resolver: CanonicalResolver>(
35    canonical_resolution: Resolver,
36    mut input: ValueSetExpand::Input,
37) -> Result<Option<Arc<Resource>>, OperationOutcomeError> {
38    if input.valueSet.is_some() {
39        let mut valueset: Option<ValueSet> = None;
40        std::mem::swap(&mut input.valueSet, &mut valueset);
41        Ok(valueset.map(|v| Arc::new(Resource::ValueSet(v))))
42    } else if let Some(url) = &input.url.as_ref().and_then(|u| u.value.as_ref()) {
43        let resolved_resource = canonical_resolution
44            .resolve(ResourceType::ValueSet, url)
45            .await?;
46
47        Ok(resolved_resource)
48    } else {
49        Ok(None)
50    }
51}
52
53fn are_codes_inline(include: &ValueSetComposeInclude) -> bool {
54    include.concept.is_some()
55}
56
57fn codes_inline_to_expansion(include: &ValueSetComposeInclude) -> Vec<ValueSetExpansionContains> {
58    include
59        .concept
60        .as_ref()
61        .map_or(Cow::Owned(vec![]), Cow::Borrowed)
62        .iter()
63        .map(|c| ValueSetExpansionContains {
64            system: include.system.clone(),
65            code: Some(c.code.clone()),
66            display: c.display.clone(),
67            ..Default::default()
68        })
69        .collect()
70}
71
72async fn resolve_codesystem<Resolver: CanonicalResolver>(
73    canonical_resolution: Resolver,
74    url: &str,
75) -> Result<Option<Arc<Resource>>, OperationOutcomeError> {
76    let code_system = canonical_resolution
77        .resolve(ResourceType::CodeSystem, url)
78        .await?;
79
80    Ok(code_system)
81}
82
83fn get_concepts(codesystem: &CodeSystem) -> Result<Vec<CodeSystemConcept>, OperationOutcomeError> {
84    match &codesystem.content {
85        content_type if content_type == &CodesystemContentMode::not_present() => {
86            Err(OperationOutcomeError::error(
87                IssueType::not_supported(),
88                "CodeSystem content is 'not-present'".to_string(),
89            ))
90        }
91        content_type
92            if content_type == &CodesystemContentMode::fragment()
93                || content_type == &CodesystemContentMode::complete()
94                || content_type == &CodesystemContentMode::supplement() =>
95        {
96            Ok(codesystem.concept.clone().unwrap_or_default())
97        }
98        _ => Err(OperationOutcomeError::error(
99            IssueType::invalid(),
100            "CodeSystem content has invalid value".to_string(),
101        )),
102    }
103}
104
105fn code_system_concept_to_valueset_expansion(
106    url: Option<&str>,
107    version: Option<&str>,
108    codesystem_concept: Vec<CodeSystemConcept>,
109) -> Vec<ValueSetExpansionContains> {
110    codesystem_concept
111        .into_iter()
112        .map(|c| ValueSetExpansionContains {
113            system: url.map(|url| {
114                Box::new(FHIRUri {
115                    value: Some(url.to_string()),
116                    ..Default::default()
117                })
118            }),
119            version: version.map(|v| {
120                Box::new(FHIRString {
121                    value: Some(v.to_string()),
122                    ..Default::default()
123                })
124            }),
125            code: Some(c.code),
126            display: c.display,
127            designation: c.designation.map(|designations| {
128                designations
129                    .into_iter()
130                    .map(|d| ValueSetComposeIncludeConceptDesignation {
131                        id: d.id,
132                        extension: d.extension,
133                        modifierExtension: d.modifierExtension,
134                        language: d.language,
135                        use_: d.use_,
136                        value: d.value,
137                    })
138                    .collect::<Vec<_>>()
139            }),
140            contains: c
141                .concept
142                .map(|concepts| code_system_concept_to_valueset_expansion(url, version, concepts)),
143            ..Default::default()
144        })
145        .collect()
146}
147
148async fn get_valueset_expansion_contains<
149    Resolver: CanonicalResolver + Send + Clone + Sync + 'static,
150>(
151    canonical_resolution: 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.clone()),
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(resource) =
205            resolve_codesystem(canonical_resolution.clone(), uri.as_str()).await?
206        && let Resource::CodeSystem(code_system) = &*resource
207    {
208        let url = code_system.url.clone();
209        let version = code_system.version.clone();
210
211        Ok(code_system_concept_to_valueset_expansion(
212            url.and_then(|v| v.value).as_deref(),
213            version.and_then(|v| v.value).as_deref(),
214            get_concepts(code_system)?,
215        ))
216    } else {
217        Ok(vec![])
218    }
219}
220
221async fn get_valueset_expansion<Resolver: CanonicalResolver + Sync + Send + Clone + 'static>(
222    canonical_resolution: Resolver,
223    value_set: &ValueSet,
224) -> Result<Vec<ValueSetExpansionContains>, OperationOutcomeError> {
225    let mut result = Vec::new();
226    if let Some(compose) = value_set.compose.as_ref() {
227        for include in &compose.include {
228            result.extend(
229                get_valueset_expansion_contains(canonical_resolution.clone(), include).await?,
230            );
231        }
232    }
233    Ok(result)
234}
235
236fn expand_valueset<Resolver: CanonicalResolver + Sync + Send + Clone + 'static>(
237    canonical_resolution: Resolver,
238    input: ValueSetExpand::Input,
239) -> Pin<Box<dyn Future<Output = Result<ValueSetExpand::Output, OperationOutcomeError>> + Send>> {
240    // Implementation would go here
241    Box::pin(async move {
242        let resolved = resolve_valueset(canonical_resolution.clone(), input).await?;
243
244        if let Some(resource) = resolved
245            && let Resource::ValueSet(value_set) = &*resource
246        {
247            let contains = get_valueset_expansion(canonical_resolution.clone(), value_set).await?;
248            let mut expanded_valueset = value_set.clone();
249
250            expanded_valueset.expansion = Some(ValueSetExpansion {
251                contains: Some(contains),
252                timestamp: Box::new(FHIRDateTime {
253                    value: Some(DateTime::Iso8601(chrono::Utc::now())),
254                    ..Default::default()
255                }),
256                ..Default::default()
257            });
258
259            Ok(ValueSetExpand::Output {
260                return_: expanded_valueset,
261            })
262        } else {
263            Err(OperationOutcomeError::error(
264                IssueType::not_found(),
265                "ValueSet could not be resolved".to_string(),
266            ))
267        }
268    })
269}
270
271impl FHIRTerminology for FHIRCanonicalTerminology {
272    async fn expand<Resolver: CanonicalResolver + Send + Clone + Sync + 'static>(
273        &self,
274        resolver: Resolver,
275        input: ValueSetExpand::Input,
276    ) -> Result<ValueSetExpand::Output, OperationOutcomeError> {
277        expand_valueset(resolver, input).await
278    }
279    async fn validate<Resolver: CanonicalResolver + Send + Clone + Sync + 'static>(
280        &self,
281        resolver: Resolver,
282        input: ValueSetValidateCode::Input,
283    ) -> Result<ValueSetValidateCode::Output, OperationOutcomeError> {
284        let Some(code) = input.code else {
285            return Err(OperationOutcomeError::error(
286                IssueType::invalid(),
287                "No code provided for validation only support 'code' field validation".to_string(),
288            ));
289        };
290
291        // Implementation would go here
292        let expansion = self
293            .expand(
294                resolver,
295                ValueSetExpand::Input {
296                    url: input.url,
297                    valueSet: input.valueSet,
298                    valueSetVersion: input.valueSetVersion,
299                    context: input.context,
300                    contextDirection: None,
301                    filter: None,
302                    date: None,
303                    offset: None,
304                    count: None,
305                    includeDesignations: None,
306                    designation: None,
307                    includeDefinition: None,
308                    activeOnly: None,
309                    excludeNested: None,
310                    excludeNotForUI: None,
311                    excludePostCoordinated: None,
312                    displayLanguage: None,
313                    exclude_system: None,
314                    system_version: None,
315                    check_system_version: None,
316                    force_system_version: None,
317                },
318            )
319            .await?;
320
321        let valueset = expansion.return_;
322
323        if let Some(expansion) = valueset.expansion
324            && let Some(contains) = expansion.contains
325        {
326            for contain in contains {
327                if contain.code.as_ref().is_some_and(|c| c.value == code.value) {
328                    return Ok(ValueSetValidateCode::Output {
329                        result: FHIRBoolean {
330                            value: Some(true),
331                            ..Default::default()
332                        },
333                        display: None,
334                        message: Some(FHIRString {
335                            value: Some("Code is valid in the ValueSet".to_string()),
336                            ..Default::default()
337                        }),
338                    });
339                }
340            }
341        }
342
343        Ok(ValueSetValidateCode::Output {
344            result: FHIRBoolean {
345                value: Some(false),
346                ..Default::default()
347            },
348            display: None,
349            message: Some(FHIRString {
350                value: Some("Code is valid in the ValueSet".to_string()),
351                ..Default::default()
352            }),
353        })
354    }
355    async fn lookup<Resolver: CanonicalResolver + Send + Clone + Sync + 'static>(
356        &self,
357        _resolver: Resolver,
358        _input: CodeSystemLookup::Input,
359    ) -> Result<CodeSystemLookup::Output, OperationOutcomeError> {
360        // Implementation would go here
361        unimplemented!()
362    }
363}