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}