1use std::{collections::HashSet, sync::Arc};
2
3use crate::{ServerEnvironmentVariables, fhir_client::ServerCTX, services::create_services};
4use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
5use haste_artifacts::ARTIFACT_RESOURCES;
6use haste_config::Config;
7use haste_fhir_client::{
8 FHIRClient,
9 request::{FHIRSearchTypeRequest, SearchRequest},
10 url::{Parameter, ParsedParameter, ParsedParameters},
11};
12use haste_fhir_model::r4::generated::{
13 resources::{Resource, ResourceType, SearchParameter, StructureDefinition},
14 terminology::IssueType,
15 types::{Coding, FHIRCode, FHIRUri, Meta},
16};
17use haste_fhir_operation_error::OperationOutcomeError;
18use haste_fhir_search::{SearchEngine, SearchOptions};
19use haste_jwt::{ProjectId, TenantId};
20
21use haste_repository::{Repository, fhir::CachePolicy, types::SupportedFHIRVersions};
22use sha1::{Digest, Sha1};
23
24fn generate_sha256_hash(value: &Resource) -> String {
25 let json = haste_fhir_serialization_json::to_string(value).expect("failed to serialize value.");
26 let mut sha_hasher = Sha1::new();
27 sha_hasher.update(json.as_bytes());
28 let sha1 = sha_hasher.finalize();
29
30 let sha_string = URL_SAFE_NO_PAD.encode(&sha1);
31
32 sha_string
33}
34
35static HASH_TAG_SYSTEM: &str = "https://haste.health/fhir/CodeSystem/hash";
36
37fn _add_hash_tag(meta: &mut Option<Box<Meta>>, sha_hash: String) {
38 let hash_tag = Box::new(Coding {
39 system: Some(Box::new(FHIRUri {
40 value: Some(HASH_TAG_SYSTEM.to_string()),
41 ..Default::default()
42 })),
43 code: Some(Box::new(FHIRCode {
44 value: Some(sha_hash),
45 ..Default::default()
46 })),
47 ..Default::default()
48 });
49
50 let meta = if let Some(meta) = meta {
51 meta
52 } else {
53 *meta = Some(Box::new(Meta::default()));
54 meta.as_mut().unwrap()
55 };
56
57 match &mut meta.tag {
58 Some(tags) => tags.push(hash_tag),
59 None => meta.tag = Some(vec![hash_tag]),
60 }
61}
62
63fn add_hash_tag(resource: &mut Resource, sha_hash: String) {
64 match resource {
65 Resource::StructureDefinition(structure_definition) => {
66 _add_hash_tag(&mut structure_definition.meta, sha_hash)
67 }
68 Resource::CodeSystem(code_system) => _add_hash_tag(&mut code_system.meta, sha_hash),
69 Resource::ValueSet(value_set) => _add_hash_tag(&mut value_set.meta, sha_hash),
70 Resource::SearchParameter(search_parameter) => {
71 _add_hash_tag(&mut search_parameter.meta, sha_hash)
72 }
73 _ => {}
74 }
75}
76
77fn get_id(resource: &Resource) -> String {
78 match resource {
79 Resource::StructureDefinition(structure_definition) => {
80 structure_definition.id.clone().unwrap_or_default()
81 }
82 Resource::CodeSystem(code_system) => code_system.id.clone().unwrap_or_default(),
83 Resource::ValueSet(value_set) => value_set.id.clone().unwrap_or_default(),
84 Resource::SearchParameter(search_parameter) => {
85 search_parameter.id.clone().unwrap_or_default()
86 }
87 _ => todo!("Unsupported resource type"),
88 }
89}
90
91pub fn get_resource_type(resource: &Resource) -> ResourceType {
92 match resource {
93 Resource::StructureDefinition(_) => ResourceType::StructureDefinition,
94 Resource::CodeSystem(_) => ResourceType::CodeSystem,
95 Resource::ValueSet(_) => ResourceType::ValueSet,
96 Resource::SearchParameter(_) => ResourceType::SearchParameter,
97 _ => todo!("Unsupported resource type"),
98 }
99}
100
101pub async fn load_artifacts(
102 config: Arc<dyn Config<ServerEnvironmentVariables>>,
103) -> Result<(), OperationOutcomeError> {
104 let services = create_services(config.clone()).await?;
105
106 let ctx = Arc::new(ServerCTX::system(
107 TenantId::System,
108 ProjectId::System,
109 services.fhir_client.clone(),
110 services.rate_limit.clone(),
111 ));
112
113 let mut hashes = HashSet::new();
114
115 for resource in ARTIFACT_RESOURCES.iter() {
116 let sha_hash = generate_sha256_hash(*&resource);
117 hashes.insert(sha_hash);
118
119 match &**resource {
120 Resource::SearchParameter(_)
121 | Resource::CodeSystem(_)
122 | Resource::ValueSet(_)
123 | Resource::StructureDefinition(_) => {
124 let mut resource = (**resource).clone();
125 let resource_type = get_resource_type(&resource);
126 let id = get_id(&resource);
127 let sha_hash = generate_sha256_hash(&resource);
128
129 add_hash_tag(&mut resource, sha_hash.clone());
130
131 let res = services
132 .fhir_client
133 .conditional_update(
134 ctx.clone(),
135 resource_type.clone(),
136 ParsedParameters::new(vec![
137 ParsedParameter::Resource(Parameter {
138 name: "_id".to_string(),
139 value: vec![id.clone()],
140 modifier: None,
141 chains: None,
142 }),
143 ParsedParameter::Resource(Parameter {
144 name: "_tag".to_string(),
145 value: vec![HASH_TAG_SYSTEM.to_string() + "|" + &sha_hash],
146 modifier: Some("not".to_string()),
147 chains: None,
148 }),
149 ]),
150 resource.clone(),
151 )
152 .await;
153
154 if let Ok(_res) = res {
155 println!("Updated {}", resource_type.as_ref());
156 } else if let Err(err) = res {
157 if let IssueType::Invalid(_) = err.outcome().issue[0].code.as_ref() {
158 println!("BACKTRACE: {}", err.backtrace().unwrap());
159 panic!("INVALID");
160 }
161 }
162 }
163 _ => {
164 }
166 }
167 }
168
169 println!(
170 "Loaded a total of '{}' artifacts with unique hashes '{}'",
171 ARTIFACT_RESOURCES.len(),
172 hashes.len(),
173 );
174
175 Ok(())
176}
177
178pub async fn get_all_sds<Repo: Repository, Search: SearchEngine>(
179 kinds: &[&str],
180 repo: &Repo,
181 search_engine: &Search,
182) -> Result<Vec<StructureDefinition>, OperationOutcomeError> {
183 let sd_search = FHIRSearchTypeRequest {
184 resource_type: ResourceType::StructureDefinition,
185 parameters: ParsedParameters::new(vec![
186 ParsedParameter::Resource(Parameter {
187 name: "kind".to_string(),
188 value: kinds.iter().map(|s| s.to_string()).collect(),
189 modifier: None,
190 chains: None,
191 }),
192 ParsedParameter::Resource(Parameter {
193 name: "abstract".to_string(),
194 value: vec!["false".to_string()],
195 modifier: None,
196 chains: None,
197 }),
198 ParsedParameter::Resource(Parameter {
199 name: "derivation".to_string(),
200 value: vec!["specialization".to_string()],
201 modifier: None,
202 chains: None,
203 }),
204 ]),
211 };
212 let sd_results = search_engine
213 .search(
214 &SupportedFHIRVersions::R4,
215 &TenantId::System,
216 &ProjectId::System,
217 &SearchRequest::Type(sd_search),
218 Some(SearchOptions { count_limit: false }),
219 )
220 .await?;
221
222 let version_ids = sd_results
223 .entries
224 .iter()
225 .map(|v| &v.version_id)
226 .collect::<Vec<_>>();
227
228 let sds = repo
229 .read_by_version_ids(
230 &TenantId::System,
231 &ProjectId::System,
232 version_ids.as_slice(),
233 CachePolicy::NoCache,
234 )
235 .await?
236 .into_iter()
237 .filter_map(|r| match r {
238 Resource::StructureDefinition(sd) => Some(sd),
239 _ => None,
240 });
241
242 Ok(sds.collect())
243}
244
245pub async fn get_all_sps<Repo: Repository, Search: SearchEngine>(
246 repo: &Repo,
247 search_engine: &Search,
248) -> Result<Vec<SearchParameter>, OperationOutcomeError> {
249 let sp_search = FHIRSearchTypeRequest {
250 resource_type: ResourceType::SearchParameter,
251 parameters: ParsedParameters::new(vec![]),
252 };
253 let sp_results = search_engine
254 .search(
255 &SupportedFHIRVersions::R4,
256 &TenantId::System,
257 &ProjectId::System,
258 &SearchRequest::Type(sp_search),
259 Some(SearchOptions { count_limit: false }),
260 )
261 .await?;
262
263 let version_ids = sp_results
264 .entries
265 .iter()
266 .map(|v| &v.version_id)
267 .collect::<Vec<_>>();
268
269 let sps = repo
270 .read_by_version_ids(
271 &TenantId::System,
272 &ProjectId::System,
273 version_ids.as_slice(),
274 CachePolicy::NoCache,
275 )
276 .await?
277 .into_iter()
278 .filter_map(|r| match r {
279 Resource::SearchParameter(sp) => Some(sp),
280 _ => None,
281 });
282
283 Ok(sps.collect())
284}