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}