atom/manifest/deps.rs
1//! # Atom Dependency Handling
2//!
3//! This module provides the core types for working with an Atom manifest's dependencies.
4//! It defines the structure for specifying different types of dependencies in an atom's
5//! manifest file, including atom references, direct pins, and build-time sources.
6//!
7//! ## Dependency Types
8//!
9//! The manifest supports three main categories of dependencies:
10//!
11//! - **Atom dependencies** - References to other atoms by ID and version
12//! - **Pin dependencies** - Direct references to external sources (URLs, Git repos, tarballs)
13//! - **Source dependencies** - Build-time dependencies like source code or config files
14//!
15//! ## Key Types
16//!
17//! - [`Dependency`] - The main dependency structure containing all dependency types
18//! - [`AtomReq`] - Requirements for atom dependencies
19//! - [`PinReq`] - Requirements for pinned dependencies
20//! - [`SrcReq`] - Requirements for build-time sources
21//! - [`PinType`] - Enum distinguishing between direct and indirect pins
22//!
23//! ## Example Usage
24//!
25//! ```toml
26//! [deps.atoms]
27//! # Reference to another atom
28//! other-atom = { version = "^1.0.0", path = "../other-atom" }
29//!
30//! [deps.pins]
31//! # pin to external evaluation time source code
32//! external-lib = { url = "https://example.com/lib.tar.gz" }
33//!
34//! # Git pin
35//! git-dep = { url = "https://github.com/user/repo.git", ref = "main" }
36//!
37//! # Indirect pin (from another atom)
38//! shared-lib = { from = "other-atom", get = "lib" }
39//!
40//! [deps.srcs]
41//! # Build-time source
42//! src-code = { url = "https://registry.example.com/code.tar.gz" }
43//! ```
44//!
45//! ## Validation
46//!
47//! All dependency types use `#[serde(deny_unknown_fields)]` to ensure strict
48//! validation and prevent typos in manifest files. Optional fields are properly
49//! handled with `skip_serializing_if` to keep the TOML output clean.
50use std::marker::PhantomData;
51use std::path::PathBuf;
52
53use bstr::ByteSlice;
54use gix::url as gix_url;
55use semver::VersionReq;
56use serde::de::DeserializeOwned;
57use serde::{Deserialize, Serialize};
58use toml_edit::DocumentMut;
59use url::Url;
60
61use crate::id::AtomTag;
62use crate::{Lockfile, Manifest};
63
64/// A Writer struct to ensure modifications to the manifest and lock stay in sync
65/// # Example
66///
67/// ```rust,no_run
68/// use std::path::Path;
69///
70/// use atom::id::Name;
71/// use atom::manifest::deps::ManifestWriter;
72/// use atom::uri::Uri;
73///
74/// let mut writer = ManifestWriter::new(Path::new("/path/to/atom.toml")).unwrap();
75/// let uri = "my-atom@^1.0.0".parse::<Uri>().unwrap();
76/// let key = "my-atom".parse::<Name>().unwrap();
77/// writer.add_uri(uri, Some(key)).unwrap();
78/// writer.write_atomic().unwrap();
79/// ```
80pub struct ManifestWriter {
81 path: PathBuf,
82 doc: TypedDocument<Manifest>,
83 lock: Lockfile,
84}
85
86/// Newtype wrapper to tie DocumentMut to a specific serializable type T.
87struct TypedDocument<T> {
88 /// The actual document we want associated with our type
89 inner: DocumentMut,
90 _marker: PhantomData<T>,
91}
92
93/// A trait to implement writing to a mutable toml document representing an atom Manifest
94trait WriteDeps<T: Serialize> {
95 /// The error type returned by the methods.
96 type Error;
97
98 /// write the dep to the given toml doc
99 fn write_dep(&self, name: &str, doc: &mut TypedDocument<T>) -> Result<(), Self::Error>;
100}
101
102#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
103/// The dependencies specified in the manifest
104#[serde(untagged)]
105pub enum Dependency {
106 /// An atom dependency variant.
107 Atom(AtomReq),
108 /// A direct pin to an external source variant.
109 Pin(PinReq),
110 /// A dependency fetched at build-time as an FOD.
111 Src(SrcReq),
112}
113
114#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
115/// Represents a locked atom dependency, referencing a verifiable repository slice.
116#[serde(deny_unknown_fields)]
117pub struct AtomReq {
118 /// The tag of the atom, used if the dependency name in the manifest
119 /// differs from the atom's actual tag.
120 #[serde(skip_serializing_if = "Option::is_none")]
121 tag: Option<AtomTag>,
122 /// The semantic version requirement for the atom (e.g., "^1.0.0").
123 version: VersionReq,
124 /// The Git URL or local path where the atom's repository can be found.
125 #[serde(serialize_with = "serialize_url", deserialize_with = "deserialize_url")]
126 store: gix_url::Url,
127}
128
129#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
130#[serde(untagged)]
131/// Represents the different types of pins for dependencies.
132///
133/// This enum distinguishes between direct pins (pointing to external URLs)
134/// and indirect pins (referencing dependencies from other atoms).
135pub enum PinType {
136 /// A direct pin to an external source with a URL.
137 Direct(DirectPin),
138 /// An indirect pin referencing a dependency from another atom.
139 Indirect(IndirectPin),
140}
141
142#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
143#[serde(untagged)]
144/// Represents the two types of direct pins.
145pub enum DirectPin {
146 /// A simple pin, with an optional unpack field.
147 Straight(Pin),
148 /// A git pin, with a ref or version.
149 Git(GitPin),
150}
151
152#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
153#[serde(deny_unknown_fields)]
154/// Represents a simple pin, with an optional unpack field.
155pub struct Pin {
156 /// The URL of the pinned resource.
157 pub pin: Url,
158 /// If `true`, the resource will be unpacked after fetching.
159 #[serde(skip_serializing_if = "not")]
160 pub unpack: bool,
161}
162
163#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
164#[serde(deny_unknown_fields)]
165/// Represents a direct git pin to an external source.
166///
167/// This struct is used when a dependency is pinned directly to a Git repository.
168pub struct GitPin {
169 /// The URL of the Git repository.
170 pub repo: Url,
171 /// The fetching strategy, either by a specific ref (branch, tag, commit)
172 /// or by resolving a semantic version tag.
173 #[serde(flatten)]
174 pub fetch: GitStrat,
175}
176
177#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
178/// Represents the two types of git fetch strategies.
179pub enum GitStrat {
180 #[serde(rename = "ref")]
181 /// The refspec (e.g. branch or tag) of the source (for git-type pins).
182 Ref(String),
183 #[serde(rename = "version")]
184 /// The version requirement of the source (for git-type pins).
185 Version(VersionReq),
186}
187
188#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
189#[serde(deny_unknown_fields)]
190/// Represents an indirect pin referencing a dependency from another atom.
191///
192/// This struct is used when a dependency is sourced from another atom,
193/// enabling composition of complex systems from simpler atom components.
194pub struct IndirectPin {
195 /// The tag of the atom from which to source the dependency.
196 pub from: AtomTag,
197 /// The name of the dependency to acquire from the source atom. If `None`,
198 /// it defaults to the name of the current dependency.
199 ///
200 /// This field is omitted from serialization if None.
201 #[serde(skip_serializing_if = "Option::is_none")]
202 pub set: Option<String>,
203}
204
205#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
206/// Represents a direct pin to an external source, such as a URL or tarball.
207///
208/// This struct is used to specify pinned dependencies in the manifest,
209/// which can be either direct (pointing to URLs) or indirect (referencing
210/// dependencies from other atoms).
211#[serde(deny_unknown_fields)]
212pub struct PinReq {
213 /// An optional relative path within the fetched source, useful for Nix imports
214 /// or accessing a subdirectory within an archive.
215 ///
216 /// This field is omitted from serialization if None.
217 #[serde(skip_serializing_if = "Option::is_none")]
218 pub path: Option<PathBuf>,
219 /// The kind of pin, which can be a direct URL, a Git repository, or an
220 /// indirect reference to a dependency from another atom.
221 ///
222 /// This field is flattened in the TOML serialization.
223 #[serde(flatten)]
224 pub kind: PinType,
225}
226
227#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
228/// Represents a dependency which is fetched at build time as an FOD.
229#[serde(deny_unknown_fields)]
230pub struct SrcReq {
231 /// The URL from which to fetch the build-time source.
232 pub src: Url,
233}
234
235impl AtomReq {
236 /// Creates a new `AtomReq` with the specified version requirement and location.
237 ///
238 /// # Arguments
239 ///
240 /// * `version` - The semantic version requirement for the atom
241 /// * `locale` - The location of the atom, either as a URL or relative path
242 ///
243 /// # Returns
244 ///
245 /// A new `AtomReq` instance with the provided version and location.
246 pub fn new(version: VersionReq, store: gix_url::Url, tag: Option<AtomTag>) -> Self {
247 Self {
248 version,
249 store,
250 tag,
251 }
252 }
253
254 /// return a reference to the version
255 pub fn version(&self) -> &VersionReq {
256 &self.version
257 }
258
259 /// set the version to a new value
260 pub fn set_version(&mut self, version: VersionReq) {
261 self.version = version
262 }
263
264 /// return a reference to the store location
265 pub fn store(&self) -> &gix_url::Url {
266 &self.store
267 }
268
269 /// return a reference to the atom tag
270 pub fn tag(&self) -> Option<&AtomTag> {
271 self.tag.as_ref()
272 }
273}
274
275#[derive(thiserror::Error, Debug)]
276/// transparent errors for TypedDocument
277pub enum DocError {
278 /// The manifest path access.
279 #[error("the atom directory disappeared or is inaccessible: {0}")]
280 Missing(PathBuf),
281 /// Toml deserialization errors
282 #[error(transparent)]
283 De(#[from] toml_edit::de::Error),
284 /// Toml error
285 #[error(transparent)]
286 Ser(#[from] toml_edit::TomlError),
287 /// Filesystem error
288 #[error(transparent)]
289 Read(#[from] std::io::Error),
290 /// Serialization error
291 #[error(transparent)]
292 Manifest(#[from] toml_edit::ser::Error),
293 /// Serialization error
294 #[error(transparent)]
295 Write(#[from] tempfile::PersistError),
296 /// Git resolution error
297 #[error(transparent)]
298 Git(#[from] Box<crate::store::git::Error>),
299 /// Version resolution error
300 #[error(transparent)]
301 Semver(#[from] semver::Error),
302}
303
304impl<T: Serialize + DeserializeOwned> TypedDocument<T> {
305 /// Constructor: Create from a serializable instance of T.
306 /// This enforces that the document comes from serializing T.
307 pub fn new(doc: &str) -> Result<(Self, T), DocError> {
308 let validated: T = toml_edit::de::from_str(doc)?;
309
310 let inner = doc.parse::<DocumentMut>()?;
311 Ok((
312 Self {
313 inner,
314 _marker: PhantomData,
315 },
316 validated,
317 ))
318 }
319}
320
321impl<T: Serialize> AsMut<DocumentMut> for TypedDocument<T> {
322 fn as_mut(&mut self) -> &mut DocumentMut {
323 &mut self.inner
324 }
325}
326impl TypedDocument<Manifest> {
327 /// Write an atom dependency into the manifest document
328 pub fn write_atom_dep(
329 &mut self,
330 key: &str,
331 req: &AtomReq,
332 ) -> Result<(), toml_edit::ser::Error> {
333 req.write_dep(key, self)
334 }
335}
336
337impl AsMut<AtomReq> for AtomReq {
338 fn as_mut(&mut self) -> &mut AtomReq {
339 self
340 }
341}
342
343impl WriteDeps<Manifest> for AtomReq {
344 type Error = toml_edit::ser::Error;
345
346 fn write_dep(&self, key: &str, doc: &mut TypedDocument<Manifest>) -> Result<(), Self::Error> {
347 let doc = doc.as_mut();
348 let atom_table = toml_edit::ser::to_document(self)?.as_table().to_owned();
349
350 if !doc.contains_table("deps") {
351 doc["deps"] = toml_edit::table();
352 }
353
354 let deps = doc["deps"].as_table_mut().unwrap();
355 deps.set_implicit(true);
356 deps.set_position(deps.len() + 1);
357
358 doc["deps"][key] = toml_edit::Item::Table(atom_table);
359 Ok(())
360 }
361}
362
363fn not(b: &bool) -> bool {
364 !b
365}
366
367use serde::{Deserializer, Serializer};
368pub(crate) fn serialize_url<S>(url: &gix_url::Url, serializer: S) -> Result<S::Ok, S::Error>
369where
370 S: Serializer,
371{
372 let str = url.to_string();
373 serializer.serialize_str(&str)
374}
375
376pub(crate) fn deserialize_url<'de, D>(deserializer: D) -> Result<gix_url::Url, D::Error>
377where
378 D: Deserializer<'de>,
379{
380 use bstr::BString;
381 let name = BString::deserialize(deserializer)?;
382 gix_url::parse(name.as_bstr())
383 .map_err(|e| <D::Error as serde::de::Error>::custom(e.to_string()))
384}
385
386use std::path::Path;
387
388use crate::id::Name;
389use crate::uri::Uri;
390impl ManifestWriter {
391 /// Construct a new instance of a manifest writer ensuring all the constraints necessary to keep
392 /// the lock and manifest in sync are respected.
393 pub fn new(path: &Path) -> Result<Self, DocError> {
394 use std::ffi::OsStr;
395 use std::fs;
396 let path = if path.file_name() == Some(OsStr::new(crate::MANIFEST_NAME.as_str())) {
397 path.into()
398 } else {
399 path.join(crate::MANIFEST_NAME.as_str())
400 };
401 let lock_path = path.with_file_name(crate::LOCK_NAME.as_str());
402 let toml_str = fs::read_to_string(&path).inspect_err(|_| {
403 tracing::error!(message = "No atom exists", path = %path.display());
404 })?;
405 let (doc, manifest) = TypedDocument::new(&toml_str)?;
406
407 let mut lock = if let Ok(lock_str) = fs::read_to_string(&lock_path) {
408 toml_edit::de::from_str(&lock_str)?
409 } else {
410 Lockfile::default()
411 };
412 lock.sanitize(&manifest);
413
414 Ok(ManifestWriter { doc, lock, path })
415 }
416
417 /// After processing all changes, write the changes to the manifest and lock to disk. This
418 /// method should be called last, after processing any requested changes.
419 pub fn write_atomic(&mut self) -> Result<(), DocError> {
420 use std::io::Write;
421
422 use tempfile::NamedTempFile;
423
424 let dir = self
425 .path
426 .parent()
427 .ok_or(DocError::Missing(self.path.clone()))?;
428 let lock_path = self.path.with_file_name(crate::LOCK_NAME.as_str());
429 let mut tmp =
430 NamedTempFile::with_prefix_in(format!(".{}", crate::MANIFEST_NAME.as_str()), dir)?;
431 let mut tmp_lock =
432 NamedTempFile::with_prefix_in(format!(".{}", crate::LOCK_NAME.as_str()), dir)?;
433 tmp.write_all(self.doc.as_mut().to_string().as_bytes())?;
434 tmp_lock.write_all(toml_edit::ser::to_string_pretty(&self.lock)?.as_bytes())?;
435 tmp.persist(&self.path)?;
436 tmp_lock.persist(lock_path)?;
437 Ok(())
438 }
439
440 /// Function to add a user requested atom uri to the manifest and lock files, ensuring they
441 /// remain in sync.
442 pub fn add_uri(&mut self, uri: Uri, key: Option<Name>) -> Result<(), DocError> {
443 use crate::lock::Dep;
444
445 let tag = uri.tag();
446 let maybe_version = uri.version();
447 let url = uri.url();
448
449 let req = if let Some(v) = maybe_version {
450 v
451 } else {
452 &VersionReq::STAR
453 };
454
455 let key = if let Some(key) = key {
456 key
457 } else {
458 tag.to_owned()
459 };
460
461 if let Some(url) = url {
462 let mut atom: AtomReq = AtomReq::new(
463 req.to_owned(),
464 url.to_owned(),
465 (&key != tag).then(|| tag.to_owned()),
466 );
467 let lock_entry = atom.resolve(&key).map_err(Box::new)?;
468
469 if maybe_version.is_none() {
470 let version = VersionReq::parse(lock_entry.version.to_string().as_str())?;
471 atom.set_version(version);
472 };
473
474 self.doc.write_atom_dep(key.as_str(), &atom)?;
475 if self
476 .lock
477 .deps
478 .as_mut()
479 .insert(key.to_owned(), Dep::Atom(lock_entry))
480 .is_some()
481 {
482 tracing::warn!("updating lock entry for `{}`", key);
483 }
484 } else {
485 // search locally for atom tag
486 todo!()
487 }
488
489 Ok(())
490 }
491}