atom/
manifest.rs

1//! # Atom Manifest
2//!
3//! This module provides the core types for working with an Atom's manifest format.
4//! The manifest is a TOML file that describes an atom's metadata and dependencies.
5//!
6//! ## Manifest Structure
7//!
8//! Every atom must have a manifest file named `atom.toml` that contains at minimum
9//! an `[atom]` section with the atom's ID, version, and optional description.
10//! Additional sections can specify dependencies and other configuration.
11//!
12//! ## Key Types
13//!
14//! - [`Manifest`] - The complete manifest structure
15//! - [`Atom`] - The core atom metadata (id, version, description)
16//! - [`AtomError`] - Errors that can occur during manifest processing
17//!
18//! ## Example Manifest
19//!
20//! ```toml
21//! [atom]
22//! tag = "my-atom"
23//! version = "1.0.0"
24//! description = "A sample atom for demonstration"
25//!
26//! [deps.atoms]
27//! other-atom = { version = "^1.0.0", path = "../other-atom" }
28//!
29//! [deps.pins]
30//! external-lib = { url = "https://example.com/lib.tar.gz", hash = "sha256:abc123..." }
31//! ```
32//!
33//! ## Validation
34//!
35//! Manifests are strictly validated to ensure they contain all required fields
36//! and have valid data. The `#[serde(deny_unknown_fields)]` attribute ensures
37//! that only known fields are accepted, preventing typos and invalid configurations.
38//!
39//! ## Usage
40//!
41//! ```rust,no_run
42//! use atom::manifest::Manifest;
43//! use atom::{Atom, AtomTag};
44//! use semver::Version;
45//!
46//! // Create a manifest programmatically
47//! let manifest = Manifest::new(
48//!     AtomTag::try_from("my-atom").unwrap(),
49//!     Version::new(1, 0, 0),
50//!     Some("My first atom".to_string()),
51//! );
52//!
53//! // Parse a manifest from a string
54//! let manifest_str = r#"
55//! [atom]
56//! tag = "parsed-atom"
57//! version = "2.0.0"
58//! "#;
59//! let parsed: Manifest = manifest_str.parse().unwrap();
60//! ```
61
62pub mod deps;
63use std::collections::HashMap;
64use std::path::PathBuf;
65use std::str::FromStr;
66
67use semver::Version;
68use serde::{Deserialize, Serialize};
69use thiserror::Error;
70use toml_edit::{DocumentMut, de};
71
72use crate::{Atom, AtomTag};
73
74/// Errors which occur during manifest (de)serialization.
75#[derive(Error, Debug)]
76pub enum AtomError {
77    /// The manifest is missing the required \[atom] key.
78    #[error("Manifest is missing the `[atom]` key")]
79    Missing,
80    /// One of the fields in the required \[atom] key is missing or invalid.
81    #[error(transparent)]
82    InvalidAtom(#[from] de::Error),
83    /// The manifest is not valid TOML.
84    #[error(transparent)]
85    InvalidToml(#[from] toml_edit::TomlError),
86    /// The manifest could not be read.
87    #[error(transparent)]
88    Io(#[from] std::io::Error),
89}
90
91type AtomResult<T> = Result<T, AtomError>;
92use crate::id::Name;
93
94/// The type representing the required fields of an Atom's manifest.
95#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
96#[serde(deny_unknown_fields)]
97pub struct Manifest {
98    /// The required \[atom] key of the TOML manifest.
99    pub atom: Atom,
100    /// The dependencies of the Atom.
101    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
102    pub(crate) deps: HashMap<Name, deps::Dependency>,
103}
104
105impl Manifest {
106    /// Create a new atom Manifest with the given values.
107    pub fn new(tag: AtomTag, version: Version, description: Option<String>) -> Self {
108        Manifest {
109            atom: Atom {
110                tag,
111                version,
112                description,
113            },
114            deps: HashMap::new(),
115        }
116    }
117
118    /// Build an Atom struct from the \[atom] key of a TOML manifest,
119    /// ignoring other fields or keys].
120    ///
121    /// # Errors
122    ///
123    /// This function will return an error if the content is invalid
124    /// TOML, or if the \[atom] key is missing.
125    pub(crate) fn get_atom(content: &str) -> AtomResult<Atom> {
126        let doc = content.parse::<DocumentMut>()?;
127
128        if let Some(v) = doc.get("atom").map(ToString::to_string) {
129            let atom = de::from_str::<Atom>(&v)?;
130            Ok(atom)
131        } else {
132            Err(AtomError::Missing)
133        }
134    }
135}
136
137impl FromStr for Manifest {
138    type Err = de::Error;
139
140    fn from_str(s: &str) -> Result<Self, Self::Err> {
141        de::from_str(s)
142    }
143}
144
145impl TryFrom<PathBuf> for Manifest {
146    type Error = AtomError;
147
148    fn try_from(path: PathBuf) -> Result<Self, Self::Error> {
149        let content = std::fs::read_to_string(path)?;
150        Ok(Manifest::from_str(&content)?)
151    }
152}