Skip to main content

fhir_diff/
package.rs

1//! FHIR packages as the diff reads them: a folder (an unpacked package, or its `package`
2//! subfolder) or `name@version`, installed from the package registry into the local package
3//! store when it is not there yet.
4
5#[cfg(feature = "registry")]
6use std::collections::VecDeque;
7use std::collections::{BTreeMap, BTreeSet};
8use std::path::{Path, PathBuf};
9
10#[cfg(feature = "registry")]
11use octofhir_canonical_manager::{CanonicalManager, FcmConfig};
12use serde_json::Value;
13
14/// A StructureDefinition as found in a package.
15#[derive(Debug, Clone)]
16pub struct Definition {
17    /// File name inside the package (`StructureDefinition-us-core-patient.json`).
18    pub file: String,
19    pub json: Value,
20}
21
22/// The StructureDefinitions of a package and what the package says about itself.
23#[derive(Debug, Clone, Default)]
24pub struct Package {
25    pub name: String,
26    pub version: String,
27    /// `fhirVersions` of the manifest (`4.0.1`).
28    pub fhir_versions: Vec<String>,
29    /// `dependencies` of the manifest: name → version.
30    pub dependencies: BTreeMap<String, String>,
31    pub definitions: Vec<Definition>,
32}
33
34impl Package {
35    /// `name@version`.
36    pub fn id(&self) -> String {
37        format!("{}@{}", self.name, self.version)
38    }
39}
40
41/// A package and the packages it depends on (core packages excepted: their definitions are
42/// built in), whose definitions profiles may be based on.
43#[derive(Debug, Clone)]
44pub struct Loaded {
45    pub package: Package,
46    pub dependencies: Vec<Package>,
47}
48
49/// Core packages: their StructureDefinitions come from the built-in FHIR Schemas.
50pub fn is_core(name: &str) -> bool {
51    name.starts_with("hl7.fhir.r") && name.ends_with(".core")
52}
53
54/// Packages of value sets and code systems, which no profile is based on; some are very large
55/// (VSAC), so they are never read for bases.
56pub fn holds_no_profiles(name: &str) -> bool {
57    name.starts_with("hl7.terminology")
58        || matches!(
59            name,
60            "us.nlm.vsac" | "us.cdc.phinvads" | "ihe.formatcode.fhir"
61        )
62}
63
64/// The dependencies of `package` that may hold a base: neither core nor terminology packages.
65pub fn base_dependencies(package: &Package) -> impl Iterator<Item = (&str, &str)> {
66    package
67        .dependencies
68        .iter()
69        .filter(|(name, _)| !is_core(name) && !holds_no_profiles(name))
70        .map(|(name, version)| (name.as_str(), version.as_str()))
71}
72
73/// The base definitions `packages` use but none of them defines, core bases excepted.
74pub fn missing_bases<'a>(
75    packages: impl IntoIterator<Item = &'a Package> + Clone,
76) -> BTreeSet<String> {
77    let defined: BTreeSet<&str> = packages
78        .clone()
79        .into_iter()
80        .flat_map(|p| &p.definitions)
81        .filter_map(|d| d.json["url"].as_str())
82        .collect();
83    packages
84        .into_iter()
85        .flat_map(|p| &p.definitions)
86        .filter_map(|d| d.json["baseDefinition"].as_str())
87        .map(|url| url.split('|').next().unwrap_or(url))
88        .filter(|url| !url.starts_with(CORE_PREFIX) && !defined.contains(url))
89        .map(str::to_owned)
90        .collect()
91}
92
93const CORE_PREFIX: &str = "http://hl7.org/fhir/StructureDefinition/";
94
95/// Load `spec`: a folder, or `name@version` (installed with its dependencies when missing; needs
96/// the `registry` feature).
97pub fn load(spec: &str) -> Result<Loaded, String> {
98    let path = Path::new(spec);
99    if path.is_dir() {
100        let package = read_folder(&package_folder(path))?;
101        return Ok(Loaded {
102            package,
103            dependencies: Vec::new(),
104        });
105    }
106    let Some((name, version)) = spec.split_once('@') else {
107        return Err(format!("`{spec}` is neither a folder nor name@version"));
108    };
109    from_registry(name, version)
110}
111
112#[cfg(not(feature = "registry"))]
113fn from_registry(name: &str, version: &str) -> Result<Loaded, String> {
114    Err(format!(
115        "{name}@{version}: loading from the package registry needs the `registry` feature"
116    ))
117}
118
119/// `name@version` from the local package store, installed from the registry with its
120/// dependencies when missing.
121#[cfg(feature = "registry")]
122fn from_registry(name: &str, version: &str) -> Result<Loaded, String> {
123    let runtime = tokio::runtime::Builder::new_current_thread()
124        .enable_all()
125        .build()
126        .map_err(|e| format!("cannot start the runtime: {e}"))?;
127    runtime.block_on(async {
128        let config = FcmConfig::load().await.map_err(|e| e.to_string())?;
129        let manager = CanonicalManager::new(config)
130            .await
131            .map_err(|e| e.to_string())?;
132        let folder = installed_folder(&manager, name, version).await?;
133        let package = read_folder(&folder)?;
134        // Dependencies, transitively, only while a base is still missing.
135        let mut dependencies: Vec<Package> = Vec::new();
136        let mut seen = BTreeSet::from([package.id()]);
137        let mut queue: VecDeque<(String, String)> = base_dependencies(&package)
138            .map(|(n, v)| (n.to_owned(), v.to_owned()))
139            .collect();
140        while !missing_bases(std::iter::once(&package).chain(&dependencies)).is_empty() {
141            let Some((dep_name, dep_version)) = queue.pop_front() else {
142                break;
143            };
144            if !seen.insert(format!("{dep_name}@{dep_version}")) {
145                continue;
146            }
147            // A dependency that cannot be installed only costs the profiles based on it.
148            match installed_folder(&manager, &dep_name, &dep_version).await {
149                Ok(folder) => {
150                    let dependency = read_folder(&folder)?;
151                    queue.extend(
152                        base_dependencies(&dependency).map(|(n, v)| (n.to_owned(), v.to_owned())),
153                    );
154                    dependencies.push(dependency);
155                }
156                Err(error) => eprintln!("warning: {dep_name}@{dep_version}: {error}"),
157            }
158        }
159        Ok(Loaded {
160            package,
161            dependencies,
162        })
163    })
164}
165
166#[cfg(feature = "registry")]
167async fn installed_folder(
168    manager: &CanonicalManager,
169    name: &str,
170    version: &str,
171) -> Result<PathBuf, String> {
172    let spec = format!("{name}@{version}");
173    let installed = manager.list_packages().await.map_err(|e| e.to_string())?;
174    if !installed.iter().any(|p| p == &spec) {
175        manager
176            .install_package(name, version)
177            .await
178            .map_err(|e| format!("cannot install {spec}: {e}"))?;
179    }
180    let dir = manager
181        .package_dir(name, version)
182        .await
183        .map_err(|e| format!("{spec}: {e}"))?;
184    Ok(package_folder(&dir))
185}
186
187/// The folder holding the package's resources: `package/` when there is one.
188fn package_folder(path: &Path) -> PathBuf {
189    let inner = path.join("package");
190    if inner.is_dir() {
191        inner
192    } else {
193        path.to_path_buf()
194    }
195}
196
197/// The manifest and StructureDefinitions of an unpacked package.
198pub fn read_folder(folder: &Path) -> Result<Package, String> {
199    let manifest: Value = std::fs::read_to_string(folder.join("package.json"))
200        .ok()
201        .and_then(|text| serde_json::from_str(&text).ok())
202        .unwrap_or(Value::Null);
203    let text = |value: &Value| value.as_str().unwrap_or_default().to_owned();
204    let mut package = Package {
205        name: text(&manifest["name"]),
206        version: text(&manifest["version"]),
207        fhir_versions: manifest["fhirVersions"]
208            .as_array()
209            .into_iter()
210            .flatten()
211            .filter_map(|v| v.as_str().map(str::to_owned))
212            .collect(),
213        dependencies: manifest["dependencies"]
214            .as_object()
215            .into_iter()
216            .flatten()
217            .filter_map(|(name, version)| Some((name.clone(), version.as_str()?.to_owned())))
218            .collect(),
219        definitions: Vec::new(),
220    };
221    if package.name.is_empty() {
222        package.name = folder
223            .file_name()
224            .map(|name| name.to_string_lossy().into_owned())
225            .unwrap_or_default();
226    }
227    let mut entries: Vec<PathBuf> = std::fs::read_dir(folder)
228        .map_err(|e| format!("{}: {e}", folder.display()))?
229        .filter_map(|entry| entry.ok().map(|entry| entry.path()))
230        .filter(|path| path.extension().is_some_and(|e| e == "json"))
231        .collect();
232    entries.sort();
233    for path in entries {
234        let Ok(text) = std::fs::read_to_string(&path) else {
235            continue;
236        };
237        // Most files are examples, value sets and code systems: parse only what may be a
238        // definition.
239        if !text.contains("\"StructureDefinition\"") {
240            continue;
241        }
242        let Ok(json) = serde_json::from_str::<Value>(&text) else {
243            continue;
244        };
245        if json["resourceType"] == "StructureDefinition" {
246            package.definitions.push(Definition {
247                file: path
248                    .file_name()
249                    .map(|name| name.to_string_lossy().into_owned())
250                    .unwrap_or_default(),
251                json,
252            });
253        }
254    }
255    Ok(package)
256}