hl7_2_from_xsd_into_json_dictionary/
lib.rs1#![warn(missing_docs, clippy::pedantic)]
26
27pub mod dictionary;
28pub mod schema;
29
30pub use hl7_2_xml_lite_helper as xml;
33
34pub use dictionary::{Document, Field, Item};
35
36use std::collections::BTreeMap;
37use std::fmt;
38use std::path::{Path, PathBuf};
39
40#[derive(Debug, Clone, Default)]
42pub struct Options {
43 pub name: Option<String>,
47 pub version: Option<String>,
49 pub inherits: Option<String>,
53 pub aliases: BTreeMap<String, String>,
57 pub structures: Vec<String>,
60}
61
62#[derive(Debug)]
64pub enum Error {
65 Io(PathBuf, std::io::Error),
67 Xml(PathBuf, xml::Error),
69 NoPrefix(PathBuf),
72 NoStructure(PathBuf, String),
74 UnknownStructure(String),
76}
77
78impl fmt::Display for Error {
79 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
80 match self {
81 Error::Io(path, error) => write!(f, "{}: {error}", path.display()),
82 Error::Xml(path, error) => write!(f, "{}: {error}", path.display()),
83 Error::NoPrefix(path) => write!(
84 f,
85 "{}: no structure schema includes a '<prefix>_segments.xsd', \
86 so the base schemas cannot be found",
87 path.display()
88 ),
89 Error::NoStructure(path, id) => write!(
90 f,
91 "{}: declares no '{id}{}' complexType, so it does not describe \
92 the structure its filename names",
93 path.display(),
94 schema::CONTENT
95 ),
96 Error::UnknownStructure(id) => write!(f, "no schema for structure {id}"),
97 }
98 }
99}
100
101impl std::error::Error for Error {}
102
103pub fn convert_directory(directory: &Path, options: &Options) -> Result<Document, Error> {
115 let structure_files = structure_files(directory)?;
116
117 let mut prefix = None;
118 for path in &structure_files {
119 let root = read_xml(path)?;
120 if let Some(found) = schema::included_prefix(&root) {
121 prefix = Some(found);
122 break;
123 }
124 }
125 let Some(prefix) = prefix else {
126 return Err(Error::NoPrefix(directory.to_path_buf()));
127 };
128
129 let base = directory.join(&prefix);
130 let mut types = schema::Types::default();
131 types.absorb(&read_xml(&with_suffix(&base, "_types.xsd"))?);
132 types.absorb(&read_xml(&with_suffix(&base, "_fields.xsd"))?);
133 let segments = schema::segments(&read_xml(&with_suffix(&base, "_segments.xsd"))?, &types);
134
135 let mut structures = BTreeMap::new();
136 for path in &structure_files {
137 let id = structure_id(path);
138 if !options.structures.is_empty() && !options.structures.contains(&id) {
139 continue;
140 }
141 let root = read_xml(path)?;
142 let items = schema::structure(&root, &id)
143 .ok_or_else(|| Error::NoStructure(path.clone(), id.clone()))?;
144 structures.insert(id, items);
145 }
146 for wanted in &options.structures {
147 if !structures.contains_key(wanted) {
148 return Err(Error::UnknownStructure(wanted.clone()));
149 }
150 }
151
152 let source = directory.file_name().map_or_else(
153 || directory.display().to_string(),
154 |name| name.to_string_lossy().into_owned(),
155 );
156 let mut description = format!(
157 "Generated by hl7-2-from-xsd-into-json-dictionary from the HL7 v2.xml \
158 schemas in {source}/ (base prefix {prefix}). \
159 Edit the schemas and regenerate; do not hand-edit."
160 );
161 if let Some(name) = &options.name {
162 description = format!("{name}: {description}");
163 }
164
165 Ok(Document {
166 version: Some(
167 options
168 .version
169 .clone()
170 .unwrap_or_else(|| version_from_prefix(&prefix)),
171 ),
172 description: Some(description),
173 inherits: options.inherits.clone(),
174 types: types.composites(),
175 segments,
176 aliases: options.aliases.clone(),
177 structures,
178 })
179}
180
181#[must_use]
183pub fn version_from_prefix(prefix: &str) -> String {
184 prefix.replace('_', ".")
185}
186
187fn structure_files(directory: &Path) -> Result<Vec<PathBuf>, Error> {
192 let entries =
193 std::fs::read_dir(directory).map_err(|error| Error::Io(directory.to_path_buf(), error))?;
194 let mut paths: Vec<PathBuf> = entries
195 .filter_map(Result::ok)
196 .map(|entry| entry.path())
197 .filter(|path| {
198 let Some(name) = path.file_name().and_then(|n| n.to_str()) else {
199 return false;
200 };
201 let name = name.to_ascii_lowercase();
205 path.extension()
206 .is_some_and(|extension| extension.eq_ignore_ascii_case("xsd"))
207 && !["_types.xsd", "_fields.xsd", "_segments.xsd"]
208 .iter()
209 .any(|base| name.ends_with(base))
210 })
211 .collect();
212 paths.sort();
213 Ok(paths)
214}
215
216fn structure_id(path: &Path) -> String {
217 path.file_stem()
218 .map(|stem| stem.to_string_lossy().into_owned())
219 .unwrap_or_default()
220}
221
222fn with_suffix(base: &Path, suffix: &str) -> PathBuf {
223 let mut name = base.as_os_str().to_os_string();
224 name.push(suffix);
225 PathBuf::from(name)
226}
227
228fn read_xml(path: &Path) -> Result<xml::Element, Error> {
229 let text =
230 std::fs::read_to_string(path).map_err(|error| Error::Io(path.to_path_buf(), error))?;
231 xml::parse(&text).map_err(|error| Error::Xml(path.to_path_buf(), error))
232}