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 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 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 unimplemented!()
362 }
363}