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}