atom/
store.rs

1//! # Atom Store Interface
2//!
3//! This module defines the core traits and interfaces for implementing storage
4//! backends for atoms. The store abstraction allows atoms to be stored and
5//! retrieved from different types of storage systems.
6//!
7//! ## Architecture
8//!
9//! The store system is designed around four core traits:
10//!
11//! - [`Init`] - Handles store initialization and root calculation
12//! - [`QueryStore`] - Queries references from remote stores (most important for store operations)
13//! - [`QueryVersion`] - Provides high-level atom version querying and filtering
14//! - [`NormalizeStorePath`] - Normalizes paths relative to store roots
15//!
16//! ## Storage Backends
17//!
18//! Currently supported backends:
19//! - **Git** - Stores atoms as Git objects in repositories (when `git` feature is enabled)
20//!
21//! Future backends may include:
22//! - **HTTP/HTTPS** - Remote storage over HTTP APIs
23//! - **Local filesystem** - Direct filesystem storage
24//! - **S3-compatible** - Cloud storage backends
25//!
26//! ## Key Concepts
27//!
28//! **Store Root**: A unique identifier that represents the base commit or
29//! state of the store. This is used as part of atom identity calculation.
30//!
31//! **Reference Querying**: The ability to efficiently query references from remote
32//! stores with different network strategies - lightweight queries for metadata
33//! or full fetches for complete store access.
34//!
35//! **Version Management**: High-level operations for discovering, filtering, and
36//! selecting atom versions based on semantic version constraints and tags.
37//!
38//! **Path Normalization**: Converting user-provided paths to canonical paths
39//! relative to the store root, handling both relative and absolute paths correctly.
40//!
41//! ## Usage
42//!
43//! ```rust,no_run
44//! use atom::AtomTag;
45//! use atom::store::git::Root;
46//! use atom::store::{Init, NormalizeStorePath, QueryStore, QueryVersion};
47//! use gix::{Remote, Url};
48//! use semver::VersionReq;
49//!
50//! // Initialize a Git store
51//! let repo = gix::open(".")?;
52//! let remote = repo.find_remote("origin")?;
53//! remote.ekala_init(None)?;
54//! let root = remote.ekala_root(None)?;
55//!
56//! // Query references from a remote store
57//! let url = gix::url::parse("https://github.com/example/repo.git".into())?;
58//! let refs = url.get_refs(["main", "refs/tags/v1.0"], None)?;
59//! for ref_info in refs {
60//!     let (name, target, peeled) = ref_info.unpack();
61//!     println!("Ref: {}, Target: {}", name, peeled.or(target).unwrap());
62//! }
63//!
64//! // Query atom versions from a remote store
65//! let atoms = url.get_atoms(None)?;
66//! for (tag, version, id) in atoms {
67//!     println!("Atom: {} v{} -> {}", tag, version, id);
68//! }
69//!
70//! // Find highest version matching requirements
71//! let req = VersionReq::parse(">=1.0.0,<2.0.0")?;
72//! if let Some((version, id)) = url.get_highest_match(&AtomTag::try_from("mylib")?, &req, None) {
73//!     println!("Selected version {} with id {}", version, id);
74//! }
75//!
76//! // Normalize a path
77//! let normalized = repo.normalize("path/to/atom")?;
78//! # Ok::<(), Box<dyn std::error::Error>>(())
79//! ```
80
81pub mod git;
82use std::path::{Path, PathBuf};
83
84use bstr::BStr;
85use semver::{Version, VersionReq};
86
87use crate::AtomTag;
88
89/// Type alias for unpacked atom reference information.
90type UnpackedRef<Id> = (AtomTag, Version, Id);
91
92/// A trait representing the methods required to initialize an Ekala store.
93pub trait Init<R, O, T: Send> {
94    /// The error type returned by the methods of this trait.
95    type Error;
96    /// Sync with the Ekala store, for implementations that require it.
97    fn sync(&self, transport: Option<&mut T>) -> Result<O, Self::Error>;
98    /// Initialize the Ekala store.
99    fn ekala_init(&self, transport: Option<&mut T>) -> Result<(), Self::Error>;
100    /// Returns the root as reported by the remote store, or an error if it is inconsistent.
101    fn ekala_root(&self, transport: Option<&mut T>) -> Result<R, Self::Error>;
102}
103
104/// A trait containing a path normalization method, to normalize paths in an Ekala store
105/// relative to its root.
106pub trait NormalizeStorePath {
107    /// The error type returned by the [`NormalizeStorePath::normalize`] function.
108    type Error;
109    /// Normalizes a given path to be relative to the store root.
110    ///
111    /// This function takes a path (relative or absolute) and attempts to normalize it
112    /// relative to the store root, based on the current working directory within
113    /// the store within system.
114    ///
115    /// # Behavior:
116    /// - For relative paths (e.g., "foo/bar" or "../foo"):
117    ///   - Interpreted as relative to the current working directory within the repository.
118    ///   - Computed relative to the repository root.
119    ///
120    /// - For absolute paths (e.g., "/foo/bar"):
121    ///   - Treated as if the repository root is the filesystem root.
122    ///   - The leading slash is ignored, and the path is considered relative to the repo root.
123    fn normalize<P: AsRef<Path>>(&self, path: P) -> Result<PathBuf, Self::Error>;
124}
125
126/// A trait for querying remote stores to retrieve references.
127///
128/// This trait provides a unified interface for querying references from remote
129/// stores. It supports different query strategies depending on the implementation.
130///
131/// ## Examples
132///
133/// ```rust,no_run
134/// use atom::store::QueryStore;
135/// use gix::Url;
136///
137/// // Lightweight reference query
138/// let url = gix::url::parse("https://github.com/example/repo.git".into())?;
139/// let refs = url.get_refs(["main", "refs/tags/v1.0"], None)?;
140/// for ref_info in refs {
141///     let (name, target, peeled) = ref_info.unpack();
142///     println!("Ref: {}, Target: {}", name, peeled.or(target).unwrap());
143/// }
144/// # Ok::<(), Box<dyn std::error::Error>>(())
145/// ```
146pub trait QueryStore<Ref, T: Send> {
147    /// The error type representing errors which can occur during query operations.
148    type Error;
149
150    /// Query a remote store for multiple references.
151    ///
152    /// This method retrieves information about the requested references from
153    /// the remote store. The exact network behavior depends on the implementation.
154    ///
155    /// # Arguments
156    /// * `targets` - An iterator of reference specifications (e.g., "main", "refs/tags/v1.0")
157    /// * `transport` - An optional mutable reference to a persistent transport (obtained by
158    ///   `QueryStore::get_transport`). If omitted an ephemeral connection is established.
159    ///
160    /// # Returns
161    /// An iterator over the requested references, or an error if the query fails.
162    fn get_refs<Spec>(
163        &self,
164        targets: impl IntoIterator<Item = Spec>,
165        transport: Option<&mut T>,
166    ) -> Result<impl IntoIterator<Item = Ref>, Self::Error>
167    where
168        Spec: AsRef<BStr>;
169
170    /// Establish a persistent connection to a git server.
171    fn get_transport(&self) -> Result<T, Self::Error>;
172
173    /// Query a remote store for a single reference.
174    ///
175    /// This is a convenience method that queries for a single reference and returns
176    /// the first result. See [`get_refs`] for details on network behavior.
177    ///
178    /// # Arguments
179    /// * `target` - The reference specification to query (e.g., "main", "refs/tags/v1.0")
180    /// * `transport` - An optional mutable reference to a persistent transport (obtained by
181    ///   `QueryStore::get_transport`). If omitted an ephemeral connection is established.
182    ///
183    /// # Returns
184    /// The requested reference, or an error if not found or if the query fails.
185    fn get_ref<Spec>(&self, target: Spec, transport: Option<&mut T>) -> Result<Ref, Self::Error>
186    where
187        Spec: AsRef<BStr>;
188}
189
190use std::collections::HashMap;
191/// A trait for querying version information about atoms in remote stores.
192///
193/// This trait extends [`QueryStore`] to provide high-level operations for working
194/// with atom versions. It enables efficient querying and filtering of atom versions
195/// based on tags and semantic version requirements.
196///
197/// ## Key Features
198///
199/// - **Atom Discovery**: Automatically discovers all atoms in the store using the standard
200///   reference pattern
201/// - **Version Filtering**: Find atoms matching specific tags and version requirements
202/// - **Semantic Versioning**: Full support for semantic version constraints and comparison
203/// - **Type Safety**: Strongly typed atom identifiers and version information
204///
205/// ## Architecture
206///
207/// The trait builds on top of [`QueryStore`] to provide:
208/// 1. Low-level reference querying (from QueryStore)
209/// 2. Atom reference parsing and unpacking
210/// 3. Version-based filtering and selection
211/// 4. Semantic version constraint matching
212///
213/// ## Use Cases
214///
215/// - **Dependency Resolution**: Find the highest version of an atom matching version requirements
216/// - **Atom Discovery**: Enumerate all available atoms in a store
217/// - **Version Management**: Check for atom updates and new versions
218/// - **Lock File Generation**: Resolve atom versions for reproducible builds
219///
220/// ## Examples
221///
222/// ```rust,no_run
223/// use atom::AtomTag;
224/// use atom::store::{QueryStore, QueryVersion};
225/// use gix::Url;
226/// use semver::VersionReq;
227///
228/// // Query for atoms in a remote store
229/// let url = gix::url::parse("https://github.com/example/atoms.git".into())?;
230///
231/// // Get all available atoms
232/// let atoms = url.get_atoms(None)?;
233/// for (tag, version, id) in atoms {
234///     println!("Atom: {} v{} -> {}", tag, version, id);
235/// }
236///
237/// // Find the highest version matching a requirement
238/// let req = VersionReq::parse(">=1.0.0,<2.0.0")?;
239///
240/// if let Some((version, id)) = url.get_highest_match(&AtomTag::try_from("mylib")?, &req, None) {
241///     println!("Selected version {} with id {}", version, id);
242/// }
243/// # Ok::<(), Box<dyn std::error::Error>>(())
244/// ```
245pub trait QueryVersion<Ref, Id, C, T>: QueryStore<Ref, T>
246where
247    C: FromIterator<UnpackedRef<Id>> + IntoIterator<Item = UnpackedRef<Id>>,
248    Ref: UnpackRef<Id> + std::fmt::Debug,
249    Self: std::fmt::Debug,
250    T: Send,
251{
252    /// Hello world
253    fn process_atoms(refs: impl IntoIterator<Item = Ref>) -> <C as IntoIterator>::IntoIter {
254        refs.into_iter()
255            .filter_map(|x| x.unpack_atom_ref())
256            .collect::<C>()
257            .into_iter()
258    }
259    /// Retrieves all atoms available in the remote store.
260    ///
261    /// This method queries the store for all atom references using the standard
262    /// `refs/atoms/*` pattern and unpacks them into structured atom information.
263    ///
264    /// # Returns
265    /// An iterator over all discovered atoms, where each atom contains:
266    /// - `AtomTag`: The atom's identifier/tag name
267    /// - `Version`: The semantic version of the atom
268    /// - `Id`: The unique identifier for this atom version
269    ///
270    /// # Errors
271    /// Returns an error if the reference query fails or if reference parsing fails.
272    fn get_atoms(
273        &self,
274        transport: Option<&mut T>,
275    ) -> Result<<C as IntoIterator>::IntoIter, <Self as QueryStore<Ref, T>>::Error> {
276        let r = format!("{}/*", crate::ATOM_REFS.as_str());
277        let a = format!("{}:{}", r, r);
278
279        let ro = format!("{}:{}", git::V1_ROOT, git::V1_ROOT);
280
281        let query = [a.as_str(), ro.as_str()];
282        let refs = self.get_refs(&query, transport)?;
283        let atoms = Self::process_atoms(refs);
284        Ok(atoms)
285    }
286
287    /// foobar
288    fn process_highest_match(
289        atoms: <C as IntoIterator>::IntoIter,
290        tag: &AtomTag,
291        req: &VersionReq,
292    ) -> Option<(Version, Id)> {
293        atoms
294            .filter_map(|(t, v, id)| (&t == tag && req.matches(&v)).then_some((v, id)))
295            .max_by_key(|(ref version, _)| version.to_owned())
296    }
297
298    /// find the root commit of the remote repo
299    fn process_root(mut atoms: <C as IntoIterator>::IntoIter) -> Option<Id> {
300        atoms.find_map(|(n, _, id)| (n.is_root()).then_some(id))
301    }
302
303    /// Finds the highest version of an atom matching the given version requirement.
304    ///
305    /// This method searches through all available atom versions for a specific tag
306    /// and returns the highest version that satisfies the provided version requirement.
307    /// Uses semantic version comparison to determine "highest" version.
308    ///
309    /// # Arguments
310    /// * `tag` - The atom tag to search for (e.g., "mylib", "database")
311    /// * `req` - The version requirement to match (e.g., ">=1.0.0", "~2.1.0")
312    ///
313    /// # Returns
314    /// The highest matching version and its identifier, or `None` if no versions match.
315    ///
316    /// # Examples
317    /// ```rust,no_run
318    /// use atom::AtomTag;
319    /// use atom::store::QueryVersion;
320    /// use semver::VersionReq;
321    ///
322    /// let url = gix::url::parse("https://example.com/my-repo.git".into())?;
323    /// let req = VersionReq::parse(">=1.0.0,<2.0.0")?;
324    /// let result = url.get_highest_match(&AtomTag::try_from("mylib")?, &req, None);
325    /// # Ok::<(), Box<dyn std::error::Error>>(())
326    /// ```
327    fn get_highest_match(
328        &self,
329        tag: &AtomTag,
330        req: &VersionReq,
331        transport: Option<&mut T>,
332    ) -> Option<(Version, Id)> {
333        let atoms = self.get_atoms(transport).ok()?;
334        Self::process_highest_match(atoms, tag, req)
335    }
336
337    /// Retrieves all atoms from the remote store and maps them by their tag.
338    ///
339    /// This method provides a convenient way to get a comprehensive overview of all
340    /// atoms available in the remote store, organized by their unique `AtomTag`.
341    /// If an atom has multiple versions, only the highest version is returned.
342    ///
343    /// # Returns
344    ///
345    /// A `HashMap` where:
346    /// - The key is the `AtomTag`, representing the unique identifier of the atom.
347    /// - The value is a tuple containing the `Version` and `Id` of the atom.
348    ///
349    /// If the remote store cannot be reached or if there are no atoms, an empty
350    /// `HashMap` is returned.
351    fn remote_atoms(&self, transport: Option<&mut T>) -> HashMap<AtomTag, (Version, Id)> {
352        if let Ok(refs) = self.get_atoms(transport) {
353            let iter = refs.into_iter();
354            let s = match iter.size_hint() {
355                (l, None) => l,
356                (_, Some(u)) => u,
357            };
358            iter.fold(HashMap::with_capacity(s), |mut acc, (t, v, id)| {
359                acc.insert(t, (v, id));
360                acc
361            })
362        } else {
363            HashMap::new()
364        }
365    }
366}
367
368/// A trait for unpacking atom references into structured version information.
369///
370/// This trait defines how to parse atom references (from git refs) into
371/// structured atom data including tags, versions, and identifiers.
372///
373/// ## Reference Format
374///
375/// Atom references follow the pattern: `refs/atoms/{tag}/{version}/{id}`
376/// where:
377/// - `tag` is the atom identifier (e.g., "mylib", "database")
378/// - `version` is a semantic version (e.g., "1.2.3")
379/// - `id` is a unique identifier for this atom version
380pub trait UnpackRef<Id> {
381    /// Attempts to unpack this reference as an atom reference.
382    ///
383    /// # Returns
384    /// - `Some((tag, version, id))` if the reference follows atom reference format
385    /// - `None` if the reference is not an atom reference or is malformed
386    fn unpack_atom_ref(&self) -> Option<UnpackedRef<Id>>;
387    /// placeholder
388    fn find_root_ref(&self) -> Option<Id>;
389}