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}