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.as_ref().is_some_and(|f| !f.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 let clean = display.replace('\t', " ").replace(['\n', '\r'], " ");
312 variant.doc_comment = Some(format!(" {clean}"));
313 }
314
315 variant.serde_rename = Some(concept.code.clone());
317
318 rust_enum.add_variant(variant);
319 }
320
321 rust_enum
322 }
323
324 fn create_enum_from_concepts(
326 &self,
327 enum_name: &str,
328 concepts: &[ValueSetComposeConcept],
329 value_set_url: &str,
330 ) -> RustEnum {
331 let mut rust_enum = RustEnum::new(enum_name.to_string());
332 rust_enum.doc_comment = Some(format!(" Generated enum for ValueSet: {value_set_url}"));
333
334 for concept in concepts {
335 let variant_name = ValueSetConcept::new(concept.code.clone()).to_variant_name();
336 let mut variant = RustEnumVariant::new(variant_name);
337
338 if let Some(display) = &concept.display {
339 let clean = display.replace('\t', " ").replace(['\n', '\r'], " ");
340 variant.doc_comment = Some(format!(" {clean}"));
341 }
342
343 variant.serde_rename = Some(concept.code.clone());
345
346 rust_enum.add_variant(variant);
347 }
348
349 rust_enum
350 }
351
352 pub fn generate_placeholder_enum(&mut self, value_set_url: &str) -> String {
354 let enum_name = self.generate_enum_name(value_set_url);
355
356 if !self.is_cached(value_set_url) {
357 let mut rust_enum = RustEnum::new(enum_name.clone());
358 rust_enum.doc_comment = Some(format!(" Generated enum for ValueSet: {value_set_url}"));
359
360 rust_enum.add_variant(RustEnumVariant::new("Unknown".to_string()));
362
363 self.cache_value_set(value_set_url.to_string(), enum_name.clone(), rust_enum);
364 }
365
366 enum_name
367 }
368}
369
370impl Default for ValueSetManager {
371 fn default() -> Self {
372 Self::new()
373 }
374}
375
376#[derive(Debug, Clone)]
378pub struct ValueSetConcept {
379 pub code: String,
380 pub display: Option<String>,
381 pub definition: Option<String>,
382 pub system: Option<String>,
383}
384
385impl ValueSetConcept {
386 pub fn new(code: String) -> Self {
387 Self {
388 code,
389 display: None,
390 definition: None,
391 system: None,
392 }
393 }
394
395 pub fn to_variant_name(&self) -> String {
397 let sanitized_code = match self.code.as_str() {
399 "=" => "Equal".to_string(),
400 "!=" => "NotEqual".to_string(),
401 "<" => "LessThan".to_string(),
402 "<=" => "LessThanOrEqual".to_string(),
403 ">" => "GreaterThan".to_string(),
404 ">=" => "GreaterThanOrEqual".to_string(),
405 "+" => "Plus".to_string(),
406 "-" => "Minus".to_string(),
407 "*" => "Star".to_string(),
408 "/" => "Slash".to_string(),
409 "&" => "Ampersand".to_string(),
410 "|" => "Pipe".to_string(),
411 "%" => "Percent".to_string(),
412 "#" => "Hash".to_string(),
413 "@" => "At".to_string(),
414 "!" => "Exclamation".to_string(),
415 "?" => "Question".to_string(),
416 "^" => "Caret".to_string(),
417 "~" => "Tilde".to_string(),
418 "(" => "LeftParen".to_string(),
419 ")" => "RightParen".to_string(),
420 "[" => "LeftBracket".to_string(),
421 "]" => "RightBracket".to_string(),
422 "{" => "LeftBrace".to_string(),
423 "}" => "RightBrace".to_string(),
424 "'" => "SingleQuote".to_string(),
425 "\"" => "DoubleQuote".to_string(),
426 "`" => "Backtick".to_string(),
427 "$" => "Dollar".to_string(),
428 ";" => "Semicolon".to_string(),
429 ":" => "Colon".to_string(),
430 "," => "Comma".to_string(),
431 _ => {
432 self.code
434 .chars()
435 .map(|c| match c {
436 'a'..='z' | 'A'..='Z' | '0'..='9' => c.to_string(),
437 '-' | '_' | '.' | ' ' => "-".to_string(), _ => format!("_{:02x}", c as u32), })
440 .collect::<String>()
441 }
442 };
443
444 let name = sanitized_code
446 .split(&['-', '_', '.', ' '][..])
447 .filter(|part| !part.is_empty()) .map(|part| {
449 let mut chars = part.chars();
450 match chars.next() {
451 None => String::new(),
452 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
453 }
454 })
455 .collect::<String>();
456
457 if name.is_empty() {
459 "Unknown".to_string()
460 } else if name.chars().next().unwrap_or('0').is_ascii_digit() {
461 format!("Code{name}")
462 } else {
463 match name.as_str() {
465 "Self" | "SelfType" => format!("{name}Value"),
466 "Type" | "Match" | "Use" | "Mod" | "Ref" | "Mut" | "Let" | "Fn" | "Impl"
467 | "Trait" | "Struct" | "Enum" | "Pub" | "Crate" | "Super" | "Return" | "If"
468 | "Else" | "While" | "For" | "In" | "Loop" | "Break" | "Continue" | "As"
469 | "Move" | "Static" | "Const" | "Unsafe" | "Extern" | "Where" | "Async"
470 | "Await" | "Dyn" | "Abstract" | "Become" | "Box" | "Do" | "Final" | "Override"
471 | "Priv" | "Typeof" | "Unsized" | "Virtual" | "Yield" => {
472 format!("{name}Value")
473 }
474 _ => name,
475 }
476 }
477 }
478}
479
480#[cfg(test)]
481mod tests {
482 use super::*;
483
484 #[test]
485 fn test_generate_enum_name() {
486 let manager = ValueSetManager::new();
487
488 assert_eq!(
489 manager.generate_enum_name("http://hl7.org/fhir/ValueSet/administrative-gender"),
490 "AdministrativeGender"
491 );
492
493 assert_eq!(
494 manager.generate_enum_name("http://hl7.org/fhir/ValueSet/123-test"),
495 "ValueSet123Test"
496 );
497 }
498
499 #[test]
500 fn test_concept_variant_name() {
501 let concept = ValueSetConcept::new("male".to_string());
502 assert_eq!(concept.to_variant_name(), "Male");
503
504 let concept = ValueSetConcept::new("unknown-gender".to_string());
505 assert_eq!(concept.to_variant_name(), "UnknownGender");
506
507 let concept = ValueSetConcept::new("123-code".to_string());
508 assert_eq!(concept.to_variant_name(), "Code123Code");
509 }
510}