Skip to main content

wdl_modules/
module.rs

1//! A [`Module`] pairs a parsed manifest with the directory it loaded
2//! from and the path through the lockfile that locates its dependency
3//! entries.
4//!
5//! Carrying these together fixes two ambiguities that a bare
6//! [`Manifest`] cannot resolve on its own:
7//!
8//! 1. A relative `LocalPath` in `module.json` is relative to that file, not to
9//!    the process's current working directory. [`Module::root`] is the
10//!    directory to rebase against.
11//! 2. Lockfile lookups must be scoped to the consumer's branch of the nested
12//!    `dependencies` tree, not searched globally. The
13//!    [`Module::lockfile_scope`] field records the chain of dependency names
14//!    from the top-level consumer down to this module.
15//!
16//! See [`crate::lockfile`] for how the scope is consumed by lookups.
17use std::path::Path;
18use std::path::PathBuf;
19use std::sync::Arc;
20
21use path_clean::PathClean;
22
23use crate::Manifest;
24use crate::dependency::DependencyName;
25use crate::manifest::ManifestError;
26
27/// Returns `true` when `dir` contains a `module.json` file at its root
28/// (i.e., `dir` is the on-disk location of a WDL module).
29///
30/// Directory walkers use this to recognize module boundaries: a
31/// directory that owns a `module.json` is the entrypoint of a separate
32/// module and should not be analyzed as part of an ancestor module.
33///
34/// # Examples
35///
36/// ```
37/// use wdl_modules::module::is_module_root;
38///
39/// let dir = tempfile::tempdir().unwrap();
40///
41/// // A directory without `module.json` is not a module root.
42/// assert!(!is_module_root(dir.path()));
43///
44/// // Creating `module.json` inside it makes it a module root.
45/// std::fs::write(dir.path().join("module.json"), b"{}").unwrap();
46/// assert!(is_module_root(dir.path()));
47/// ```
48pub fn is_module_root(dir: &Path) -> bool {
49    dir.join(crate::MANIFEST_FILENAME).is_file()
50}
51
52/// A cheap identity for a [`Module`] that combines its root directory with its
53/// lockfile scope.
54///
55/// Two modules with the same root and scope resolve dependencies identically,
56/// so this is a stable key for deduplicating and caching resolution work
57/// without cloning a module's manifest.
58#[derive(Clone, Debug, Eq, Hash, PartialEq)]
59pub struct ModuleId {
60    /// The directory containing the module's `module.json` file.
61    pub root: PathBuf,
62    /// The chain of dependency names from the top-level consumer to the module.
63    pub scope: Vec<DependencyName>,
64}
65
66/// A WDL module, pairing its parsed [`Manifest`], the directory on disk that
67/// holds the `module.json` file, and the lockfile scope that locates
68/// the module's entry within a top-level lockfile.
69#[derive(Clone, Debug)]
70pub struct Module {
71    /// The parsed manifest.
72    pub manifest: Arc<Manifest>,
73    /// The directory containing the `module.json` file.
74    pub root: PathBuf,
75    /// The chain of dependency names from the top-level consumer to
76    /// this module. Empty for the top-level consumer itself. A
77    /// dependency `coffeeshop` brought in by `cafe_menu` has scope
78    /// `[cafe_menu]`.
79    pub lockfile_scope: Vec<DependencyName>,
80}
81
82impl Module {
83    /// Builds a top-level [`Module`] from a manifest and its root
84    /// directory. The lockfile scope is empty.
85    pub fn new(manifest: Arc<Manifest>, root: PathBuf) -> Self {
86        Self {
87            manifest,
88            root,
89            lockfile_scope: Vec::new(),
90        }
91    }
92
93    /// Returns this module's identity, combining its root directory and
94    /// lockfile scope.
95    pub fn id(&self) -> ModuleId {
96        ModuleId {
97            root: self.root.clone(),
98            scope: self.lockfile_scope.clone(),
99        }
100    }
101
102    /// Reads `module.json` from `path` and constructs a top-level
103    /// [`Module`] with `path` as the root.
104    pub fn load_from_path(path: &Path) -> Result<Self, ManifestError> {
105        let manifest_path = path.join(crate::MANIFEST_FILENAME);
106        let bytes = std::fs::read(&manifest_path).map_err(|source| ManifestError::Io {
107            path: manifest_path,
108            source,
109        })?;
110        let manifest = Arc::new(Manifest::parse(&bytes)?);
111        Ok(Self::new(manifest, path.to_path_buf()))
112    }
113
114    /// Returns `path` joined to [`root`](Self::root) when `path` is
115    /// relative, or `path` itself when it is already absolute.
116    pub fn resolve_local_path(&self, path: &Path) -> PathBuf {
117        if path.is_absolute() {
118            path.clean()
119        } else {
120            self.root.join(path).clean()
121        }
122    }
123
124    /// Returns a child [`Module`] in the lockfile scope below this one,
125    /// extending the scope by `name`.
126    pub fn child(&self, name: DependencyName, manifest: Arc<Manifest>, root: PathBuf) -> Self {
127        let mut lockfile_scope = self.lockfile_scope.clone();
128        lockfile_scope.push(name);
129        Self {
130            manifest,
131            root,
132            lockfile_scope,
133        }
134    }
135}