Skip to main content

cljrs_deps/
lib.rs

1//! `cljrs.edn` project configuration: types, parser, and discovery.
2//!
3//! # Format
4//!
5//! ```edn
6//! {:paths ["src" "resources"]
7//!
8//!  :deps
9//!  {my.lib      {:git/url "https://github.com/user/my-lib" :git/sha "abc1234ef"}
10//!   local.utils {:local/root "../local-utils"}}
11//!
12//!  :aliases
13//!  {:dev  {:extra-paths ["dev"]}
14//!   :test {:extra-paths ["test"]
15//!          :extra-deps  {test-tools {:git/url "..." :git/sha "..."}}}}}
16//! ```
17
18use std::path::{Path, PathBuf};
19use std::sync::Arc;
20
21use thiserror::Error;
22
23mod parse;
24pub use parse::parse_config;
25
26// ── Error ─────────────────────────────────────────────────────────────────────
27
28#[derive(Debug, Error)]
29pub enum DepsError {
30    #[error("could not read cljrs.edn: {0}")]
31    Io(#[from] std::io::Error),
32    #[error("parse error in cljrs.edn: {0}")]
33    Parse(String),
34}
35
36pub type DepsResult<T> = Result<T, DepsError>;
37
38// ── Types ─────────────────────────────────────────────────────────────────────
39
40/// A git-hosted dependency with a pinned commit SHA.
41#[derive(Debug, Clone, PartialEq, Eq)]
42pub struct GitDep {
43    pub url: Arc<str>,
44    pub sha: Arc<str>,
45    /// `:rust/init` — fully-qualified path to the dep's native init function
46    /// (e.g. `"my_crate::cljrs_init"`), when the dep ships Rust code.
47    pub rust_init: Option<Arc<str>>,
48    /// `:rust/crate` — directory of the dep's Cargo.toml relative to its
49    /// repository root (defaults to the root).
50    pub rust_crate_dir: Option<Arc<str>>,
51    /// `:rust/load :dylib` — opt in to **pinned native code**: when a
52    /// versioned symbol resolves into this dep's namespace and falls back to
53    /// a native function, build the dep's crate at the pinned commit as a
54    /// cdylib and load it instead of using the current binary's
55    /// implementation.
56    pub rust_load_dylib: bool,
57}
58
59/// A dependency declared in `:deps` or `:extra-deps`.
60#[derive(Debug, Clone, PartialEq, Eq)]
61pub enum Dependency {
62    Git(GitDep),
63    /// A local directory on disk, resolved relative to the `cljrs.edn` file.
64    Local {
65        root: PathBuf,
66    },
67}
68
69/// A trusted commit signer declared in `:trusted-signers`.
70///
71/// Used (with `:verify-commit-signatures true`) to decide which keys are
72/// allowed to sign versioned dependency commits. Each entry is either an
73/// inline public key or a path to a key file resolved relative to the
74/// `cljrs.edn` directory.
75#[derive(Debug, Clone, PartialEq, Eq)]
76pub enum TrustedSigner {
77    /// An inline public key: an armored PGP block or an OpenSSH public key line.
78    Inline(String),
79    /// A path to a public-key file (PGP `.asc` or OpenSSH `.pub`).
80    File(PathBuf),
81}
82
83/// A single alias entry from the `:aliases` map.
84#[derive(Debug, Clone, Default)]
85pub struct Alias {
86    pub extra_paths: Vec<PathBuf>,
87    pub extra_deps: Vec<(Arc<str>, Dependency)>,
88}
89
90/// Rust-crate configuration for mixed Rust/Clojure projects.
91///
92/// The `:init` value is a Rust path like `"my_project::cljrs_init"`. The
93/// first `::` segment is the crate name used in `Cargo.toml` and when
94/// looking for the compiled shared library on disk.
95///
96/// Parsed from the `:rust` key in `cljrs.edn`:
97///
98/// ```edn
99/// :rust {:crate "."                        ; path to directory with Cargo.toml
100///        :init  "my_project::cljrs_init"}  ; optional hook-registration fn
101/// ```
102#[derive(Debug, Clone)]
103pub struct RustConfig {
104    /// Directory containing the user's `Cargo.toml`, resolved relative to the
105    /// `cljrs.edn` file.  Defaults to the `cljrs.edn` directory (`"."`).
106    pub crate_dir: PathBuf,
107    /// Fully-qualified Rust path to the init function, e.g.
108    /// `"my_project::cljrs_init"`.  When present, `cljrs compile` emits a
109    /// call to this function in the generated `main.rs` before loading any
110    /// Clojure source.  Omit if you rely solely on `#[cljrs::export]`
111    /// inventory-based registration (future feature).
112    pub init_fn: Option<Arc<str>>,
113}
114
115impl RustConfig {
116    /// Derive the Rust crate name from the init function path.
117    ///
118    /// `"my_project::cljrs_init"` → `Some("my_project")`.
119    /// Returns `None` when `init_fn` is absent.
120    pub fn crate_name(&self) -> Option<&str> {
121        self.init_fn
122            .as_deref()
123            .map(|s| s.split("::").next().unwrap_or(s))
124    }
125}
126
127/// The fully parsed contents of a `cljrs.edn` file.
128#[derive(Debug, Clone, Default)]
129pub struct DepsConfig {
130    /// Source/resource paths relative to the `cljrs.edn` file.
131    pub paths: Vec<PathBuf>,
132    /// Project-level dependency declarations.
133    pub deps: Vec<(Arc<str>, Dependency)>,
134    /// Named aliases (e.g. `:dev`, `:test`).
135    pub aliases: Vec<(Arc<str>, Alias)>,
136    /// When true, every versioned-symbol or versioned-namespace resolution
137    /// must carry a valid commit signature (verified natively against
138    /// `trusted_signers`) before historical code is executed.  Equivalent to
139    /// the `--verify-commit-signatures` CLI flag.
140    pub verify_commit_signatures: bool,
141    /// Public keys trusted to sign versioned dependency commits, from
142    /// `:trusted-signers`.  Consulted only when `verify_commit_signatures` is
143    /// on; an empty set means no commit can be verified.
144    pub trusted_signers: Vec<TrustedSigner>,
145    /// When true, a pinned lookup of a native (Rust-backed) function whose
146    /// recorded provenance does not match the requested commit is an error
147    /// instead of a warning.  Equivalent to the `--enforce-native-versions`
148    /// CLI flag.
149    pub enforce_native_versions: bool,
150    /// Optional Rust-crate configuration for mixed Rust/Clojure projects.
151    pub rust: Option<RustConfig>,
152}
153
154impl DepsConfig {
155    /// Find a dependency by namespace-prefix name.
156    pub fn find_dep(&self, name: &str) -> Option<&Dependency> {
157        self.deps
158            .iter()
159            .find(|(n, _)| n.as_ref() == name)
160            .map(|(_, d)| d)
161    }
162}
163
164// ── Discovery ─────────────────────────────────────────────────────────────────
165
166/// Walk up the directory tree from `start`, returning the path of the first
167/// `cljrs.edn` file found, or `None` if no config exists in any ancestor.
168pub fn find_config_file(start: &Path) -> Option<PathBuf> {
169    let mut dir: &Path = if start.is_file() {
170        start.parent()?
171    } else {
172        start
173    };
174    loop {
175        let candidate = dir.join("cljrs.edn");
176        if candidate.exists() {
177            return Some(candidate);
178        }
179        dir = dir.parent()?;
180    }
181}
182
183/// Load and parse the `cljrs.edn` closest to `start`, returning `None` if
184/// no config file is found.
185pub fn load_config(start: &Path) -> DepsResult<Option<DepsConfig>> {
186    match find_config_file(start) {
187        None => Ok(None),
188        Some(path) => {
189            let src = std::fs::read_to_string(&path)?;
190            let config = parse_config(&src, &path).map_err(DepsError::Parse)?;
191            Ok(Some(config))
192        }
193    }
194}