atom/publish/mod.rs
1//! # Atom Publishing
2//!
3//! This module provides the types and logic necessary to efficiently publish atoms
4//! to store implementations. The publishing system is designed to be safe, atomic,
5//! and provide detailed feedback about the publishing process.
6//!
7//! ## Architecture
8//!
9//! The publishing system is built around two main traits:
10//!
11//! - [`Builder`] - Constructs and validates publishers before publishing
12//! - [`Publish`] - Handles the actual publishing of atoms to stores
13//!
14//! ## Publishing Process
15//!
16//! 1. **Validation** - All atoms in the workspace are validated for consistency
17//! 2. **Deduplication** - Duplicate atoms are detected and skipped
18//! 3. **Publishing** - Valid atoms are published to the target store
19//! 4. **Reporting** - Detailed statistics and results are provided
20//!
21//! ## Key Types
22//!
23//! - [`Record`] - Contains the result of publishing a single atom
24//! - [`Stats`] - Aggregated statistics for a publishing operation
25//! - [`PublishOutcome`] - Result type for individual atom publishing attempts
26//! - [`Content`] - Backend-specific content information
27//!
28//! ## Current Backends
29//!
30//! - **Git** - Publishes atoms as Git objects in repositories (when `git` feature is enabled)
31//!
32//! ## Future Backends
33//!
34//! The architecture is designed to support additional backends:
35//! - **HTTP/HTTPS** - REST APIs for atom storage
36//! - **S3-compatible** - Cloud storage backends
37//! - **IPFS** - Distributed storage networks
38//!
39//! ## Safety Features
40//!
41//! - **Atomic operations** - Failed publishes don't leave partial state
42//! - **Duplicate detection** - Prevents accidental overwrites
43//! - **Comprehensive validation** - Ensures atom consistency before publishing
44//! - **Detailed error reporting** - Clear feedback on what succeeded or failed
45//!
46//! ## Example Usage
47//!
48//! ```rust,no_run
49//! use std::path::PathBuf;
50//!
51//! use atom::publish::git::GitPublisher;
52//! use atom::publish::{Builder, Publish, Stats};
53//! use atom::store::QueryVersion;
54//! use atom::store::git::Root;
55//!
56//! let repo = gix::open(".")?;
57//! // Create a publisher for a Git repository
58//! let progress_span = tracing::info_span!("test");
59//! let publisher = GitPublisher::new(&repo, "origin", "main", &progress_span)?;
60//!
61//! // Build and validate the publisher
62//! let (valid_atoms, publisher) = publisher.build()?;
63//!
64//! // query upstream store for remote atom refs to compare against
65//! let remote = publisher.remote();
66//! let remote_atoms = remote.remote_atoms(None);
67//!
68//! // Publish all atoms
69//! let results = publisher.publish(vec![PathBuf::from("/path/to/atom")], remote_atoms);
70//!
71//! // Check results
72//! let stats = Stats::default();
73//! for result in results {
74//! match result {
75//! Ok(outcome) => todo!(), // e.g. log `outcome`
76//! Err(e) => println!("Failed: {:?}", e),
77//! }
78//! }
79//! # Ok::<(), Box<dyn std::error::Error>>(())
80//! ```
81pub mod error;
82
83pub mod git;
84
85use std::collections::HashMap;
86use std::path::{Path, PathBuf};
87
88use git::GitContent;
89
90use crate::AtomId;
91use crate::id::AtomTag;
92
93/// The results of Atom publishing, for reporting to the user.
94pub struct Record<R> {
95 id: AtomId<R>,
96 content: Content,
97}
98
99/// Basic statistics collected during a publishing request.
100#[derive(Default)]
101pub struct Stats {
102 /// How many Atoms were actually published.
103 pub published: u32,
104 /// How many Atoms were safely skipped because they already existed.
105 pub skipped: u32,
106 /// How many Atoms failed to publish due to some error condition.
107 pub failed: u32,
108}
109
110/// A Result is used over an Option here mainly so we can report which
111/// Atom was skipped, but it does not represent a true failure condition
112type MaybeSkipped<T> = Result<T, AtomTag>;
113
114/// A Record that signifies whether an Atom was published or safetly skipped.
115type PublishOutcome<R> = MaybeSkipped<Record<R>>;
116
117/// A [`HashMap`] containing all valid Atoms in the current store.
118type ValidAtoms = HashMap<AtomTag, PathBuf>;
119
120/// Contains the content pertinent to a specific implementation for reporting results
121/// to the user.
122pub enum Content {
123 /// Content specific to the Git implementation.
124 Git(GitContent),
125}
126
127/// A [`Builder`] produces a [`Publish`] implementation, which has no other constructor.
128/// This is critical to ensure that vital invariants necessary for maintaining a clean
129/// and consistent state in the Ekala store are verified before publishing can occur.
130pub trait Builder<'a, R> {
131 /// The error type returned by the [`Builder::build`] method.
132 type Error;
133 /// The [`Publish`] implementation to construct.
134 type Publisher: Publish<R>;
135
136 /// Collect all the Atoms in the worktree into a set.
137 ///
138 /// This function must be called before `Publish::publish` to ensure that there are
139 /// no duplicates, as this is the only way to construct an implementation.
140 fn build(self) -> Result<(ValidAtoms, Self::Publisher), Self::Error>;
141}
142
143trait StateValidator<R> {
144 type Error;
145 type Publisher: Publish<R>;
146 /// Validate the state of the Atom source.
147 ///
148 /// This function is called during construction to ensure that we
149 /// never allow for an inconsistent state in the final Ekala store.
150 ///
151 /// Any conditions that would result in an inconsistent state will
152 /// result in an error, making it impossible to construct a publisher
153 /// until the state is corrected.
154 fn validate(publisher: &Self::Publisher) -> Result<ValidAtoms, Self::Error>;
155}
156
157mod private {
158 /// a marker trait to seal the [`Publish<R>`] trait
159 pub trait Sealed {}
160}
161
162/// The trait primarily responsible for exposing Atom publishing logic for a given store.
163pub trait Publish<R>: private::Sealed {
164 /// The error type returned by the publisher.
165 type Error;
166 /// The type representing the machine-readable identity for a specific version of an atom.
167 type Id;
168
169 /// Publishes Atoms.
170 ///
171 /// This function processes a collection of paths, each representing an Atom to be published.
172 /// Internally the implementation calls [`Publish::publish_atom`] for each path.
173 ///
174 /// # Error Handling
175 /// - The function aims to process all provided paths, even if some fail.
176 /// - Errors and skipped Atoms are collected as results but do not halt the overall process.
177 /// - The function continues until all the Atoms have been processed.
178 ///
179 /// # Return Value
180 /// Returns a vector of results types, where the outter result represents whether an Atom has
181 /// failed, and the inner result determines whether an Atom was safely skipped, e.g. because it
182 /// already exists.
183 fn publish<C>(
184 &self,
185 paths: C,
186 remotes: HashMap<AtomTag, (semver::Version, Self::Id)>,
187 ) -> Vec<Result<PublishOutcome<R>, Self::Error>>
188 where
189 C: IntoIterator<Item = PathBuf>;
190
191 /// Publish an Atom.
192 ///
193 /// This function takes a single path and publishes the Atom located there, if possible.
194 ///
195 /// # Return Value
196 /// - An outcome is either the record ([`Record<R>`]) of the successfully publish Atom or the
197 /// [`crate::AtomId`] if it was safely skipped.
198 ///
199 /// - The function will return an error ([`Self::Error`]) if the Atom could not be published for
200 /// any reason, e.g. invalid manifests.
201 fn publish_atom<P: AsRef<Path>>(
202 &self,
203 path: P,
204 remotes: &HashMap<AtomTag, (semver::Version, Self::Id)>,
205 ) -> Result<PublishOutcome<R>, Self::Error>;
206}
207
208impl<R> Record<R> {
209 /// Return a reference to the [`AtomId`] in the record.
210 pub fn id(&self) -> &AtomId<R> {
211 &self.id
212 }
213
214 /// Return a reference to the [`Content`] of the record.
215 pub fn content(&self) -> &Content {
216 &self.content
217 }
218}
219
220use std::sync::LazyLock;
221
222const EMPTY_SIG: &str = "";
223const STORE_ROOT: &str = "eka";
224const ATOM_FORMAT_VERSION: &str = "pre1.0";
225const ATOM_REF: &str = "atoms";
226const ATOM_MANIFEST: &str = "manifest";
227const ATOM_META_REF: &str = "meta";
228const ATOM_ORIGIN: &str = "origin";
229static REF_ROOT: LazyLock<String> = LazyLock::new(|| format!("refs/{}", STORE_ROOT));
230/// the default location where atom refs are stored
231pub static ATOM_REFS: LazyLock<String> =
232 LazyLock::new(|| format!("{}/{}", REF_ROOT.as_str(), ATOM_REF));
233static META_REFS: LazyLock<String> =
234 LazyLock::new(|| format!("{}/{}", REF_ROOT.as_str(), ATOM_META_REF));