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}