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
5use std::collections::BTreeMap;
6use std::path::{Path, PathBuf};
7
8#[cfg(feature = "registry")]
9use octofhir_canonical_manager::{CanonicalManager, FcmConfig};
10use serde_json::Value;
11
12/// A StructureDefinition as found in a package.
13#[derive(Debug, Clone)]
14pub struct Definition {
15    /// File name inside the package (`StructureDefinition-us-core-patient.json`).
16    pub file: String,
17    pub json: Value,
18}
19
20/// The StructureDefinitions of a package and what the package says about itself.
21#[derive(Debug, Clone, Default)]
22pub struct Package {
23    pub name: String,
24    pub version: String,
25    /// `fhirVersions` of the manifest (`4.0.1`).
26    pub fhir_versions: Vec<String>,
27    /// `dependencies` of the manifest: name → version.
28    pub dependencies: BTreeMap<String, String>,
29    pub definitions: Vec<Definition>,
30}
31
32impl Package {
33    /// `name@version`.
34    pub fn id(&self) -> String {
35        format!("{}@{}", self.name, self.version)
36    }
37}
38
39/// A package and the packages it depends on (core packages excepted: their definitions are
40/// built in), whose definitions profiles may be based on.
41#[derive(Debug, Clone)]
42pub struct Loaded {
43    pub package: Package,
44    pub dependencies: Vec<Package>,
45}
46
47/// Core packages: their StructureDefinitions come from the built-in FHIR Schemas.
48pub fn is_core(name: &str) -> bool {
49    name.starts_with("hl7.fhir.r") && name.ends_with(".core")
50}
51
52/// Load `spec`: a folder, or `name@version` (installed with its dependencies when missing; needs
53/// the `registry` feature).
54pub fn load(spec: &str) -> Result<Loaded, String> {
55    let path = Path::new(spec);
56    if path.is_dir() {
57        let package = read_folder(&package_folder(path))?;
58        return Ok(Loaded {
59            package,
60            dependencies: Vec::new(),
61        });
62    }
63    let Some((name, version)) = spec.split_once('@') else {
64        return Err(format!("`{spec}` is neither a folder nor name@version"));
65    };
66    from_registry(name, version)
67}
68
69#[cfg(not(feature = "registry"))]
70fn from_registry(name: &str, version: &str) -> Result<Loaded, String> {
71    Err(format!(
72        "{name}@{version}: loading from the package registry needs the `registry` feature"
73    ))
74}
75
76/// `name@version` from the local package store, installed from the registry with its
77/// dependencies when missing.
78#[cfg(feature = "registry")]
79fn from_registry(name: &str, version: &str) -> Result<Loaded, String> {
80    let runtime = tokio::runtime::Builder::new_current_thread()
81        .enable_all()
82        .build()
83        .map_err(|e| format!("cannot start the runtime: {e}"))?;
84    runtime.block_on(async {
85        let config = FcmConfig::load().await.map_err(|e| e.to_string())?;
86        let manager = CanonicalManager::new(config)
87            .await
88            .map_err(|e| e.to_string())?;
89        let folder = installed_folder(&manager, name, version).await?;
90        let package = read_folder(&folder)?;
91        let mut dependencies = Vec::new();
92        for (dep_name, dep_version) in &package.dependencies {
93            if is_core(dep_name) {
94                continue;
95            }
96            // A dependency that cannot be installed only costs the profiles based on it.
97            match installed_folder(&manager, dep_name, dep_version).await {
98                Ok(folder) => dependencies.push(read_folder(&folder)?),
99                Err(error) => eprintln!("warning: {dep_name}@{dep_version}: {error}"),
100            }
101        }
102        Ok(Loaded {
103            package,
104            dependencies,
105        })
106    })
107}
108
109#[cfg(feature = "registry")]
110async fn installed_folder(
111    manager: &CanonicalManager,
112    name: &str,
113    version: &str,
114) -> Result<PathBuf, String> {
115    let spec = format!("{name}@{version}");
116    let installed = manager.list_packages().await.map_err(|e| e.to_string())?;
117    if !installed.iter().any(|p| p == &spec) {
118        manager
119            .install_package(name, version)
120            .await
121            .map_err(|e| format!("cannot install {spec}: {e}"))?;
122    }
123    let dir = manager
124        .package_dir(name, version)
125        .await
126        .map_err(|e| format!("{spec}: {e}"))?;
127    Ok(package_folder(&dir))
128}
129
130/// The folder holding the package's resources: `package/` when there is one.
131fn package_folder(path: &Path) -> PathBuf {
132    let inner = path.join("package");
133    if inner.is_dir() {
134        inner
135    } else {
136        path.to_path_buf()
137    }
138}
139
140/// The manifest and StructureDefinitions of an unpacked package.
141pub fn read_folder(folder: &Path) -> Result<Package, String> {
142    let manifest: Value = std::fs::read_to_string(folder.join("package.json"))
143        .ok()
144        .and_then(|text| serde_json::from_str(&text).ok())
145        .unwrap_or(Value::Null);
146    let text = |value: &Value| value.as_str().unwrap_or_default().to_owned();
147    let mut package = Package {
148        name: text(&manifest["name"]),
149        version: text(&manifest["version"]),
150        fhir_versions: manifest["fhirVersions"]
151            .as_array()
152            .into_iter()
153            .flatten()
154            .filter_map(|v| v.as_str().map(str::to_owned))
155            .collect(),
156        dependencies: manifest["dependencies"]
157            .as_object()
158            .into_iter()
159            .flatten()
160            .filter_map(|(name, version)| Some((name.clone(), version.as_str()?.to_owned())))
161            .collect(),
162        definitions: Vec::new(),
163    };
164    if package.name.is_empty() {
165        package.name = folder
166            .file_name()
167            .map(|name| name.to_string_lossy().into_owned())
168            .unwrap_or_default();
169    }
170    let mut entries: Vec<PathBuf> = std::fs::read_dir(folder)
171        .map_err(|e| format!("{}: {e}", folder.display()))?
172        .filter_map(|entry| entry.ok().map(|entry| entry.path()))
173        .filter(|path| path.extension().is_some_and(|e| e == "json"))
174        .collect();
175    entries.sort();
176    for path in entries {
177        let Ok(text) = std::fs::read_to_string(&path) else {
178            continue;
179        };
180        let Ok(json) = serde_json::from_str::<Value>(&text) else {
181            continue;
182        };
183        if json["resourceType"] == "StructureDefinition" {
184            package.definitions.push(Definition {
185                file: path
186                    .file_name()
187                    .map(|name| name.to_string_lossy().into_owned())
188                    .unwrap_or_default(),
189                json,
190            });
191        }
192    }
193    Ok(package)
194}