Skip to main content

haste_fhir_package/
lib.rs

1//! FHIR NPM packages, the form implementation guides are published in:
2//! reading a package tarball ([`read`]) and fetching one from a package
3//! registry such as packages.fhir.org ([`resolve_version`], [`download`]).
4//!
5//! A package is a gzipped tarball with a `package/` folder holding
6//! `package.json`, the conformance resources as one JSON file each, and the
7//! examples in `package/example/`. See
8//! <https://hl7.org/fhir/packages.html>.
9
10mod error;
11mod registry;
12mod tarball;
13
14pub use error::PackageError;
15pub use registry::{download, resolve_version};
16pub use tarball::{Limits, pack, read};
17
18use serde::Deserialize;
19use std::{collections::BTreeMap, fmt::Display, str::FromStr};
20
21/// A package's `package.json`, as far as installing it needs.
22#[derive(Debug, Clone, Default, Deserialize)]
23#[serde(default)]
24pub struct Manifest {
25    pub name: String,
26    pub version: String,
27    /// The canonical URL its resources are published under.
28    pub canonical: Option<String>,
29    pub title: Option<String>,
30    pub description: Option<String>,
31    #[serde(rename = "fhirVersions")]
32    pub fhir_versions: Vec<String>,
33    /// Package name to the version this one was built against.
34    pub dependencies: BTreeMap<String, String>,
35    pub directories: Directories,
36}
37
38#[derive(Debug, Clone, Default, Deserialize)]
39#[serde(default)]
40pub struct Directories {
41    /// The examples' folder, relative to the package folder. `example` when
42    /// unset.
43    pub example: Option<String>,
44}
45
46/// A JSON file in a package.
47#[derive(Debug, Clone)]
48pub struct PackageFile {
49    /// Its path relative to the package folder.
50    pub path: String,
51    pub content: Vec<u8>,
52}
53
54/// A package read from its tarball.
55#[derive(Debug, Clone)]
56pub struct Package {
57    pub manifest: Manifest,
58    /// The JSON files directly in the package folder, other than
59    /// `package.json` and the index: its conformance resources.
60    pub resources: Vec<PackageFile>,
61    /// The JSON files in its example folder.
62    pub examples: Vec<PackageFile>,
63    /// What the tarball expanded to, headers included.
64    pub unpacked_bytes: u64,
65}
66
67/// A package to install, named the way registries and `package.json`
68/// dependencies name one: `name@version`, `name#version` or `name` alone for
69/// the latest version. A version may end in `.x` to take the latest match.
70#[derive(Debug, Clone, PartialEq, Eq)]
71pub struct PackageSpec {
72    pub name: String,
73    pub version: Option<String>,
74}
75
76impl FromStr for PackageSpec {
77    type Err = PackageError;
78
79    fn from_str(value: &str) -> Result<Self, Self::Err> {
80        let value = value.trim();
81        let (name, version) = match value.find(['@', '#']) {
82            Some(at) => (&value[..at], Some(&value[at + 1..])),
83            None => (value, None),
84        };
85        PackageSpec::new(name, version)
86    }
87}
88
89impl PackageSpec {
90    /// A spec for `name` at `version`, both checked.
91    ///
92    /// # Errors
93    ///
94    /// When the name or version has characters packages do not use.
95    pub fn new(name: &str, version: Option<&str>) -> Result<Self, PackageError> {
96        if !valid_name(name) {
97            return Err(PackageError::InvalidName(name.to_string()));
98        }
99        let version = match version.map(str::trim) {
100            None | Some("") => None,
101            Some(version) if valid_version(version) => Some(version.to_string()),
102            Some(version) => return Err(PackageError::InvalidVersion(version.to_string())),
103        };
104        Ok(PackageSpec {
105            name: name.to_string(),
106            version,
107        })
108    }
109}
110
111impl Display for PackageSpec {
112    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
113        match &self.version {
114            Some(version) => write!(f, "{}@{version}", self.name),
115            None => write!(f, "{}", self.name),
116        }
117    }
118}
119
120/// Package names are lowercase in practice (`hl7.fhir.us.core`); any case is
121/// accepted here. They become a URL path segment, so nothing else is.
122#[must_use]
123pub fn valid_name(name: &str) -> bool {
124    !name.is_empty()
125        && name.len() <= 214
126        && name.starts_with(|c: char| c.is_ascii_alphanumeric())
127        && name
128            .chars()
129            .all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '-' | '_'))
130}
131
132/// A semver-like version (`6.1.0`, `2.0.0-ballot`, `6.1.x`). It becomes a URL
133/// path segment, so nothing else is accepted.
134#[must_use]
135pub fn valid_version(version: &str) -> bool {
136    !version.is_empty()
137        && version.len() <= 64
138        && version.starts_with(|c: char| c.is_ascii_alphanumeric())
139        && version
140            .chars()
141            .all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '-' | '+'))
142}