1use crate::fhir_types::{CodeSystem, ValueSet, ValueSetComposeConcept, ValueSetExpansionContains};
7use crate::rust_types::{RustEnum, RustEnumVariant};
8use std::collections::HashMap;
9use std::fs;
10use std::path::{Path, PathBuf};
11
12#[derive(Debug, Clone)]
14pub struct ValueSetManager {
15 value_set_dir: Option<PathBuf>,
17 value_set_cache: HashMap<String, String>,
19 enum_cache: HashMap<String, RustEnum>,
21}
22
23impl ValueSetManager {
24 pub fn new() -> Self {
25 Self {
26 value_set_dir: None,
27 value_set_cache: HashMap::new(),
28 enum_cache: HashMap::new(),
29 }
30 }
31
32 pub fn new_with_directory<P: AsRef<Path>>(value_set_dir: P) -> Self {
33 Self {
34 value_set_dir: Some(value_set_dir.as_ref().to_path_buf()),
35 value_set_cache: HashMap::new(),
36 enum_cache: HashMap::new(),
37 }
38 }
39
40 pub fn generate_enum_name(&self, value_set_url: &str) -> String {
42 let name = value_set_url
44 .split('/')
45 .next_back()
46 .unwrap_or("UnknownValueSet")
47 .split(&['-', '.'][..])
48 .filter(|part| !part.is_empty())
49 .map(|part| {
50 let mut chars = part.chars();
51 match chars.next() {
52 None => String::new(),
53 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
54 }
55 })
56 .collect::<String>();
57
58 if name.chars().next().unwrap_or('0').is_ascii_digit() {
60 format!("ValueSet{name}")
61 } else {
62 name
63 }
64 }
65
66 pub fn is_cached(&self, value_set_url: &str) -> bool {
68 self.value_set_cache.contains_key(value_set_url)
69 }
70
71 pub fn get_enum_name(&self, value_set_url: &str) -> Option<&String> {
73 self.value_set_cache.get(value_set_url)
74 }
75
76 pub fn cache_value_set(
78 &mut self,
79 value_set_url: String,
80 enum_name: String,
81 rust_enum: RustEnum,
82 ) {
83 self.value_set_cache
84 .insert(value_set_url, enum_name.clone());
85 self.enum_cache.insert(enum_name, rust_enum);
86 }
87
88 pub fn get_cached_enums(&self) -> &HashMap<String, RustEnum> {
90 &self.enum_cache
91 }
92
93 pub fn get_value_set_codes(
96 &self,
97 value_set_url: &str,
98 version: Option<&str>,
99 ) -> Result<Vec<(String, Option<String>)>, String> {
100 let value_set = match self.load_value_set(value_set_url, version) {
102 Ok(vs) => vs,
103 Err(err) => {
104 eprintln!("Warning: Could not load ValueSet '{value_set_url}': {err}");
105 return Err(format!("ValueSet not found: {value_set_url}"));
106 }
107 };
108
109 let mut codes = Vec::new();
110
111 if let Some(expansion) = &value_set.expansion {
113 if let Some(contains) = &expansion.contains {
114 for concept in contains {
115 codes.push((concept.code.clone(), concept.display.clone()));
116 }
117 if !codes.is_empty() {
118 return Ok(codes);
119 }
120 }
121 }
122
123 if let Some(compose) = &value_set.compose {
125 if let Some(includes) = &compose.include {
126 for include in includes {
127 if let Some(concepts) = &include.concept {
128 for concept in concepts {
129 codes.push((concept.code.clone(), concept.display.clone()));
130 }
131 }
132 }
133 }
134 }
135
136 if codes.is_empty() {
137 Err("No codes found in ValueSet".to_string())
138 } else {
139 Ok(codes)
140 }
141 }
142
143 pub fn generate_enum_from_value_set(
145 &mut self,
146 value_set_url: &str,
147 version: Option<&str>,
148 ) -> Result<String, String> {
149 let enum_name = self.generate_enum_name(value_set_url);
150
151 if self.is_cached(value_set_url) {
152 return Ok(enum_name);
153 }
154
155 let value_set = match self.load_value_set(value_set_url, version) {
157 Ok(vs) => vs,
158 Err(err) => {
159 eprintln!("Warning: Could not load ValueSet '{value_set_url}': {err}");
160 return Err(format!("ValueSet not found: {value_set_url}"));
161 }
162 };
163
164 if let Some(expansion) = &value_set.expansion {
166 if let Some(contains) = &expansion.contains {
167 if !contains.is_empty() {
168 let rust_enum =
169 self.create_enum_from_expansion(&enum_name, contains, value_set_url);
170 self.cache_value_set(value_set_url.to_string(), enum_name.clone(), rust_enum);
171 return Ok(enum_name);
172 }
173 }
174 }
175
176 if let Some(compose) = &value_set.compose {
178 if let Some(includes) = &compose.include {
179 for include in includes {
181 if include.filter.is_some() && !include.filter.as_ref().unwrap().is_empty() {
182 eprintln!("Warning: ValueSet '{value_set_url}' has filters, cannot generate enum. Falling back to String.");
183 return Err("ValueSet has filters".to_string());
184 }
185 }
186
187 let mut all_concepts = Vec::new();
189 for include in includes {
190 if let Some(concepts) = &include.concept {
191 all_concepts.extend(concepts.iter().cloned());
192 } else if let Some(system) = &include.system {
193 if let Ok(code_system) = self.load_code_system(system) {
195 if let Some(cs_concepts) = &code_system.concept {
196 for cs_concept in cs_concepts {
197 let compose_concept = ValueSetComposeConcept {
198 code: cs_concept.code.clone(),
199 display: cs_concept.display.clone(),
200 };
201 all_concepts.push(compose_concept);
202 }
203 }
204 }
205 }
206 }
207
208 if !all_concepts.is_empty() {
209 let rust_enum =
210 self.create_enum_from_concepts(&enum_name, &all_concepts, value_set_url);
211 self.cache_value_set(value_set_url.to_string(), enum_name.clone(), rust_enum);
212 return Ok(enum_name);
213 }
214 }
215 }
216
217 eprintln!("Warning: Could not generate enum for ValueSet '{value_set_url}', no expansion or compose concepts found. Falling back to String.");
219 Err("No concepts found in ValueSet".to_string())
220 }
221
222 fn load_value_set(
224 &self,
225 value_set_url: &str,
226 _version: Option<&str>,
227 ) -> Result<ValueSet, String> {
228 let value_set_dir = self
229 .value_set_dir
230 .as_ref()
231 .ok_or("No ValueSet directory configured")?;
232
233 let id = value_set_url
235 .split('/')
236 .next_back()
237 .ok_or("Invalid ValueSet URL")?;
238
239 let filenames = vec![
241 format!("ValueSet-{}.json", id),
242 format!("valueset-{}.json", id),
243 format!("{}.json", id),
244 ];
245
246 for filename in filenames {
247 let file_path = value_set_dir.join(&filename);
248 if file_path.exists() {
249 let content = fs::read_to_string(&file_path)
250 .map_err(|e| format!("Failed to read file '{}': {}", file_path.display(), e))?;
251
252 let value_set: ValueSet = serde_json::from_str(&content)
253 .map_err(|e| format!("Failed to parse ValueSet JSON: {e}"))?;
254 return Ok(value_set);
255 }
256 }
257
258 Err(format!("ValueSet file not found for ID: {id}"))
259 }
260
261 fn load_code_system(&self, system_url: &str) -> Result<CodeSystem, String> {
263 let value_set_dir = self
264 .value_set_dir
265 .as_ref()
266 .ok_or("No ValueSet directory configured")?;
267
268 let id = system_url
270 .split('/')
271 .next_back()
272 .ok_or("Invalid CodeSystem URL")?;
273
274 let filenames = vec![
276 format!("CodeSystem-{}.json", id),
277 format!("codesystem-{}.json", id),
278 format!("{}.json", id),
279 ];
280
281 for filename in filenames {
282 let file_path = value_set_dir.join(&filename);
283 if file_path.exists() {
284 let content = fs::read_to_string(&file_path)
285 .map_err(|e| format!("Failed to read file '{}': {}", file_path.display(), e))?;
286
287 let code_system: CodeSystem = serde_json::from_str(&content)
288 .map_err(|e| format!("Failed to parse CodeSystem JSON: {e}"))?;
289 return Ok(code_system);
290 }
291 }
292
293 Err(format!("CodeSystem file not found for ID: {id}"))
294 }
295
296 fn create_enum_from_expansion(
298 &self,
299 enum_name: &str,
300 contains: &[ValueSetExpansionContains],
301 value_set_url: &str,
302 ) -> RustEnum {
303 let mut rust_enum = RustEnum::new(enum_name.to_string());
304 rust_enum.doc_comment = Some(format!(" Generated enum for ValueSet: {value_set_url}"));
305
306 for concept in contains {
307 let variant_name = ValueSetConcept::new(concept.code.clone()).to_variant_name();
308 let mut variant = RustEnumVariant::new(variant_name);
309
310 if let Some(display) = &concept.display {
311 variant.doc_comment = Some(format!(" {}", display.clone()));
312 }
313
314 variant.serde_rename = Some(concept.code.clone());
316
317 rust_enum.add_variant(variant);
318 }
319
320 rust_enum
321 }
322
323 fn create_enum_from_concepts(
325 &self,
326 enum_name: &str,
327 concepts: &[ValueSetComposeConcept],
328 value_set_url: &str,
329 ) -> RustEnum {
330 let mut rust_enum = RustEnum::new(enum_name.to_string());
331 rust_enum.doc_comment = Some(format!(" Generated enum for ValueSet: {value_set_url}"));
332
333 for concept in concepts {
334 let variant_name = ValueSetConcept::new(concept.code.clone()).to_variant_name();
335 let mut variant = RustEnumVariant::new(variant_name);
336
337 if let Some(display) = &concept.display {
338 variant.doc_comment = Some(format!(" {}", display.clone()));
339 }
340
341 variant.serde_rename = Some(concept.code.clone());
343
344 rust_enum.add_variant(variant);
345 }
346
347 rust_enum
348 }
349
350 pub fn generate_placeholder_enum(&mut self, value_set_url: &str) -> String {
352 let enum_name = self.generate_enum_name(value_set_url);
353
354 if !self.is_cached(value_set_url) {
355 let mut rust_enum = RustEnum::new(enum_name.clone());
356 rust_enum.doc_comment = Some(format!(" Generated enum for ValueSet: {value_set_url}"));
357
358 rust_enum.add_variant(RustEnumVariant::new("Unknown".to_string()));
360
361 self.cache_value_set(value_set_url.to_string(), enum_name.clone(), rust_enum);
362 }
363
364 enum_name
365 }
366}
367
368impl Default for ValueSetManager {
369 fn default() -> Self {
370 Self::new()
371 }
372}
373
374#[derive(Debug, Clone)]
376pub struct ValueSetConcept {
377 pub code: String,
378 pub display: Option<String>,
379 pub definition: Option<String>,
380 pub system: Option<String>,
381}
382
383impl ValueSetConcept {
384 pub fn new(code: String) -> Self {
385 Self {
386 code,
387 display: None,
388 definition: None,
389 system: None,
390 }
391 }
392
393 pub fn to_variant_name(&self) -> String {
395 let sanitized_code = match self.code.as_str() {
397 "=" => "Equal".to_string(),
398 "!=" => "NotEqual".to_string(),
399 "<" => "LessThan".to_string(),
400 "<=" => "LessThanOrEqual".to_string(),
401 ">" => "GreaterThan".to_string(),
402 ">=" => "GreaterThanOrEqual".to_string(),
403 "+" => "Plus".to_string(),
404 "-" => "Minus".to_string(),
405 "*" => "Star".to_string(),
406 "/" => "Slash".to_string(),
407 "&" => "Ampersand".to_string(),
408 "|" => "Pipe".to_string(),
409 "%" => "Percent".to_string(),
410 "#" => "Hash".to_string(),
411 "@" => "At".to_string(),
412 "!" => "Exclamation".to_string(),
413 "?" => "Question".to_string(),
414 "^" => "Caret".to_string(),
415 "~" => "Tilde".to_string(),
416 "(" => "LeftParen".to_string(),
417 ")" => "RightParen".to_string(),
418 "[" => "LeftBracket".to_string(),
419 "]" => "RightBracket".to_string(),
420 "{" => "LeftBrace".to_string(),
421 "}" => "RightBrace".to_string(),
422 "'" => "SingleQuote".to_string(),
423 "\"" => "DoubleQuote".to_string(),
424 "`" => "Backtick".to_string(),
425 "$" => "Dollar".to_string(),
426 ";" => "Semicolon".to_string(),
427 ":" => "Colon".to_string(),
428 "," => "Comma".to_string(),
429 _ => {
430 self.code
432 .chars()
433 .map(|c| match c {
434 'a'..='z' | 'A'..='Z' | '0'..='9' => c.to_string(),
435 '-' | '_' | '.' | ' ' => "-".to_string(), _ => format!("_{:02x}", c as u32), })
438 .collect::<String>()
439 }
440 };
441
442 let name = sanitized_code
444 .split(&['-', '_', '.', ' '][..])
445 .filter(|part| !part.is_empty()) .map(|part| {
447 let mut chars = part.chars();
448 match chars.next() {
449 None => String::new(),
450 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
451 }
452 })
453 .collect::<String>();
454
455 if name.is_empty() {
457 "Unknown".to_string()
458 } else if name.chars().next().unwrap_or('0').is_ascii_digit() {
459 format!("Code{name}")
460 } else {
461 name
462 }
463 }
464}
465
466#[cfg(test)]
467mod tests {
468 use super::*;
469
470 #[test]
471 fn test_generate_enum_name() {
472 let manager = ValueSetManager::new();
473
474 assert_eq!(
475 manager.generate_enum_name("http://hl7.org/fhir/ValueSet/administrative-gender"),
476 "AdministrativeGender"
477 );
478
479 assert_eq!(
480 manager.generate_enum_name("http://hl7.org/fhir/ValueSet/123-test"),
481 "ValueSet123Test"
482 );
483 }
484
485 #[test]
486 fn test_concept_variant_name() {
487 let concept = ValueSetConcept::new("male".to_string());
488 assert_eq!(concept.to_variant_name(), "Male");
489
490 let concept = ValueSetConcept::new("unknown-gender".to_string());
491 assert_eq!(concept.to_variant_name(), "UnknownGender");
492
493 let concept = ValueSetConcept::new("123-code".to_string());
494 assert_eq!(concept.to_variant_name(), "Code123Code");
495 }
496}