Skip to main content

haste_fhir_terminology/
client.rs

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    // Implementation would go here
240    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        // Implementation would go here
285        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        // Implementation would go here
355        unimplemented!()
356    }
357}