Skip to main content

mlua_pkg/
config.rs

1//! Single entry value for the operations in [`crate::ops`].
2//!
3//! [`Config`] bundles *where things live* ([`Project`]) with *what the
4//! manifest says* ([`ManifestSource`]).  Every operation takes a `&Config`,
5//! so an embedding application has exactly one type to build, and every
6//! knob that `mlua-pkg.toml` / the CLI flags can express is reachable from
7//! Rust without touching the filesystem first:
8//!
9//! | knob | carried by |
10//! |------|-----------|
11//! | `--mlua-pkgs-dir` / `MLUA_PKG_DIR` | [`PkgDir`](crate::PkgDir) inside [`Project`] |
12//! | manifest / lockfile location | [`Project`] |
13//! | `[package]` + `[deps.<name>]` (the whole `mlua-pkg.toml`) | [`ManifestSource::Value`] holding a [`Manifest`] |
14//! | `add` arguments | [`AddSpec`](crate::ops::AddSpec) |
15//! | `update` flags | [`UpdateOpts`](crate::ops::UpdateOpts) |
16//!
17//! # File vs value
18//!
19//! [`ManifestSource::File`] is what the CLI uses: the manifest is read from
20//! `project.manifest_path()` and, for `add` / `update`, written back there.
21//!
22//! [`ManifestSource::Value`] is the SDK form: the caller hands over a
23//! [`Manifest`] it built (or parsed with [`Manifest::from_toml_str`]).
24//! Nothing is written to `manifest_path()`; operations that would change the
25//! manifest return the new one in their report instead
26//! ([`AddReport::manifest`](crate::ops::AddReport::manifest),
27//! [`UpdateReport::manifest`](crate::ops::UpdateReport::manifest)).
28//! The lockfile and the cache / vendored directories are still written to
29//! the paths in [`Project`] in both modes — they are outputs, not inputs.
30//!
31//! ```rust,no_run
32//! use mlua_pkg::{manifest::{Dep, Manifest, Package}, ops, Config, PkgDir, Project};
33//! use std::collections::HashMap;
34//!
35//! # fn main() -> Result<(), mlua_pkg::PkgError> {
36//! let root = "/srv/app";
37//! let project = Project::in_dir(root, PkgDir::default_in(root));
38//!
39//! // Build the manifest in memory instead of writing mlua-pkg.toml.
40//! let mut deps = HashMap::new();
41//! deps.insert("lshape".to_string(), Dep {
42//!     git: "https://github.com/ynishi/lshape".into(),
43//!     tag: Some("v0.1".into()),
44//!     rev: None, branch: None, entry: None, target_dir: None, patch_dir: None, patch_drift: None,
45//! });
46//! let manifest = Manifest {
47//!     package: Package { name: "app".into(), version: "0.1.0".into(), entry: None },
48//!     deps,
49//! };
50//!
51//! let cfg = Config::with_manifest(project, manifest);
52//! let report = ops::install(&cfg)?;
53//! assert_eq!(report.direct, 1);
54//! # Ok(())
55//! # }
56//! ```
57
58use crate::{manifest::Manifest, PkgError, Project};
59
60/// Where [`ops`](crate::ops) takes the manifest from.
61#[derive(Debug, Clone, PartialEq)]
62pub enum ManifestSource {
63    /// Read `project.manifest_path()`; `add` / `update` write back to it.
64    File,
65    /// Use this value; nothing is written to `manifest_path()`.
66    Value(Manifest),
67}
68
69/// Everything an operation needs: paths plus the manifest.
70#[derive(Debug, Clone, PartialEq)]
71pub struct Config {
72    project: Project,
73    manifest: ManifestSource,
74}
75
76impl Config {
77    /// File-backed manifest at `project.manifest_path()` (the CLI's mode).
78    pub fn new(project: Project) -> Self {
79        Self {
80            project,
81            manifest: ManifestSource::File,
82        }
83    }
84
85    /// In-memory manifest; `manifest_path()` is never read or written.
86    pub fn with_manifest(project: Project, manifest: Manifest) -> Self {
87        Self {
88            project,
89            manifest: ManifestSource::Value(manifest),
90        }
91    }
92
93    /// Paths (manifest / lockfile / [`PkgDir`](crate::PkgDir)).
94    pub fn project(&self) -> &Project {
95        &self.project
96    }
97
98    /// How the manifest is supplied.
99    pub fn manifest_source(&self) -> &ManifestSource {
100        &self.manifest
101    }
102
103    /// `true` for [`ManifestSource::Value`].
104    pub fn manifest_is_value(&self) -> bool {
105        matches!(self.manifest, ManifestSource::Value(_))
106    }
107
108    /// The manifest this config resolves to: a parse of `manifest_path()`
109    /// for [`ManifestSource::File`], a clone for [`ManifestSource::Value`].
110    ///
111    /// # Errors
112    ///
113    /// File mode only: whatever [`Manifest::from_path`] returns.
114    pub fn load_manifest(&self) -> Result<Manifest, PkgError> {
115        match &self.manifest {
116            ManifestSource::File => Manifest::from_path(self.project.manifest_path()),
117            ManifestSource::Value(m) => Ok(m.clone()),
118        }
119    }
120
121    /// Same paths, manifest replaced by `manifest` as a value.
122    ///
123    /// Used by `update` to re-run `install` against the manifest it just
124    /// rewrote without going through the filesystem.
125    pub fn replace_manifest(&self, manifest: Manifest) -> Self {
126        Self::with_manifest(self.project.clone(), manifest)
127    }
128}
129
130impl From<Project> for Config {
131    fn from(project: Project) -> Self {
132        Self::new(project)
133    }
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139    use crate::PkgDir;
140    use std::collections::HashMap;
141
142    fn manifest() -> Manifest {
143        Manifest {
144            package: crate::manifest::Package {
145                name: "x".into(),
146                version: "0.1.0".into(),
147                entry: None,
148            },
149            deps: HashMap::new(),
150        }
151    }
152
153    #[test]
154    fn file_mode_reads_manifest_path() {
155        let tmp = tempfile::tempdir().unwrap();
156        let project = Project::in_dir(tmp.path(), PkgDir::default_in(tmp.path()));
157        std::fs::write(
158            project.manifest_path(),
159            "[package]\nname = \"f\"\nversion = \"0.1.0\"\n",
160        )
161        .unwrap();
162        let cfg = Config::new(project);
163        assert!(!cfg.manifest_is_value());
164        assert_eq!(cfg.load_manifest().unwrap().package.name, "f");
165    }
166
167    #[test]
168    fn value_mode_never_touches_manifest_path() {
169        let tmp = tempfile::tempdir().unwrap();
170        let project = Project::in_dir(tmp.path(), PkgDir::default_in(tmp.path()));
171        let cfg = Config::with_manifest(project, manifest());
172        assert!(cfg.manifest_is_value());
173        assert_eq!(cfg.load_manifest().unwrap().package.name, "x");
174        assert!(!cfg.project().manifest_path().exists());
175    }
176
177    #[test]
178    fn replace_manifest_keeps_paths() {
179        let project = Project::new("m.toml", "m.lock", "pkgs");
180        let cfg = Config::new(project.clone());
181        let mut m = manifest();
182        m.package.name = "y".into();
183        let cfg2 = cfg.replace_manifest(m);
184        assert_eq!(cfg2.project(), &project);
185        assert!(cfg2.manifest_is_value());
186        assert_eq!(cfg2.load_manifest().unwrap().package.name, "y");
187    }
188}