Skip to main content

spec_driven_docs/
self_depend.rs

1//! How a consumer obtains this tool, and how its pin stays fresh.
2//!
3//! A project pins this tool through the manager it already runs, at a
4//! version its manager file records. This module reads that pin, serves the
5//! fragment a manager needs to take one, and moves the pin to a newer
6//! release in one transaction. It writes into a file the project owns only
7//! where the project authorized it: the pin the sync line moves.
8//!
9//! Two independent axes decide every verdict. A manager is what the project
10//! declares its development tools in ([`manager::Manager`]). A venue is where
11//! a release of this tool is published ([`venue::Venue`]). Their cross is the
12//! matrix in [`venue`], and every pair carries a verdict there and nowhere
13//! else.
14
15pub mod fragments;
16pub mod leftovers;
17pub mod manager;
18pub mod pin;
19pub mod registry;
20pub mod stamp;
21pub mod status;
22pub mod txn;
23pub mod venue;
24
25use crate::domain::manifest::CANON_SOURCE;
26
27/// The binary this crate installs, as an archive names it.
28pub const BINARY_NAME: &str = "sdd";
29
30/// The line a project's shell loader runs on every directory entry.
31pub const SYNC_LINE: &str = "sdd self-depend sync --apply || true";
32
33/// The shell loader file a project keeps at its root.
34pub const ENVRC: &str = ".envrc";
35
36/// The forge owner and repository this tool is published from, read from
37/// the one place the canon's home is declared.
38#[must_use]
39pub fn coordinates() -> (&'static str, &'static str) {
40    let path = CANON_SOURCE
41        .trim_start_matches("https://github.com/")
42        .trim_end_matches(".git");
43    path.split_once('/').unwrap_or((path, ""))
44}
45
46/// The `owner/repo` slug the forge and the flake reference use.
47#[must_use]
48pub fn slug() -> String {
49    let (owner, repo) = coordinates();
50    format!("{owner}/{repo}")
51}
52
53/// The version a release tag names, from any of the spellings a caller
54/// uses: `v0.2.16`, `0.2.16`, or the release page URL ending in the tag.
55///
56/// # Errors
57///
58/// A message naming the value where it is not a released triple.
59pub fn parse_tag(value: &str) -> Result<semver::Version, String> {
60    let tail = value.trim().rsplit('/').next().unwrap_or(value).trim();
61    let bare = tail.strip_prefix('v').unwrap_or(tail);
62    let version: semver::Version = bare
63        .parse()
64        .map_err(|_| format!("{value} is not a release tag: expected v<major>.<minor>.<patch>"))?;
65    if !version.pre.is_empty() || !version.build.is_empty() {
66        return Err(format!("{value} is not a released triple"));
67    }
68    Ok(version)
69}
70
71#[cfg(test)]
72mod tests {
73    #![allow(clippy::unwrap_used, reason = "a test panics as its failure signal")]
74
75    use super::*;
76
77    #[test]
78    fn the_coordinates_come_from_the_canon_source() {
79        let (owner, repo) = coordinates();
80        assert!(!owner.is_empty());
81        assert_eq!(repo, "spec-driven-docs");
82        assert!(CANON_SOURCE.ends_with(&slug()));
83    }
84
85    #[test]
86    fn a_tag_parses_in_every_spelling() {
87        assert_eq!(parse_tag("v0.2.16").unwrap().to_string(), "0.2.16");
88        assert_eq!(parse_tag("0.2.16").unwrap().to_string(), "0.2.16");
89        assert_eq!(
90            parse_tag("https://example.invalid/releases/tag/v1.2.3")
91                .unwrap()
92                .to_string(),
93            "1.2.3"
94        );
95        assert!(parse_tag("v1.2.3-rc.1").is_err());
96        assert!(parse_tag("latest").is_err());
97    }
98}