Skip to main content

sui_compat/
flake_ref.rs

1//! Flake reference parser for CLI-style references like `.#cid`.
2//!
3//! Parses the `<path>#<attribute>` format used by `nix build`, `nix eval`,
4//! and `sui system rebuild --flake`.
5
6use std::path::PathBuf;
7
8/// A parsed flake reference like `.#cid` or `path/to/flake#hostname`.
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct FlakeRef {
11    /// The directory containing the flake.
12    pub flake_dir: PathBuf,
13    /// The attribute path after the `#`.
14    pub attribute: String,
15}
16
17impl FlakeRef {
18    /// Parse a CLI-style flake reference.
19    ///
20    /// # Format
21    ///
22    /// `<path>#<attribute>` where `<path>` is a filesystem path and
23    /// `<attribute>` is a dot-separated Nix attribute path.
24    ///
25    /// # Examples
26    ///
27    /// - `.#cid` — current directory, attribute `cid`
28    /// - `/path/to/nix#cid` — absolute path, attribute `cid`
29    /// - `relative/path#attr` — relative path, attribute `attr`
30    /// - `.#` — current directory, empty attribute (allowed)
31    ///
32    /// # Errors
33    ///
34    /// Returns [`FlakeRefError::MissingAttribute`] if the input does not
35    /// contain a `#` separator, and [`FlakeRefError::InvalidPath`] if the
36    /// current directory cannot be resolved (only when path is `.` or empty).
37    pub fn parse(input: &str) -> Result<Self, FlakeRefError> {
38        if let Some((path_part, attr)) = input.split_once('#') {
39            let dir = if path_part == "." || path_part.is_empty() {
40                std::env::current_dir()
41                    .map_err(|e| FlakeRefError::InvalidPath(e.to_string()))?
42            } else {
43                // Strip the explicit `path:` scheme — both forms are
44                // valid CLI input (`nix build path:/dir#attr` and
45                // `nix build /dir#attr` mean the same thing).  Without
46                // this, `PathBuf::from("path:/dir")` produces a literal
47                // `path:/dir/flake.nix` join target that doesn't exist.
48                let raw = path_part.strip_prefix("path:").unwrap_or(path_part);
49                PathBuf::from(raw)
50            };
51            Ok(Self {
52                flake_dir: dir,
53                attribute: attr.to_string(),
54            })
55        } else {
56            Err(FlakeRefError::MissingAttribute(input.to_string()))
57        }
58    }
59}
60
61/// Errors from parsing a flake reference.
62#[derive(Debug, thiserror::Error)]
63pub enum FlakeRefError {
64    /// The input string did not contain a `#` separator.
65    #[error("flake reference missing '#attribute': {0}")]
66    MissingAttribute(String),
67    /// The path component could not be resolved.
68    #[error("invalid flake path: {0}")]
69    InvalidPath(String),
70}
71
72#[cfg(test)]
73mod tests {
74    use super::*;
75
76    #[test]
77    fn parse_dot_hash_cid() {
78        let fr = FlakeRef::parse(".#cid").unwrap();
79        // flake_dir should be the current working directory
80        assert_eq!(fr.flake_dir, std::env::current_dir().unwrap());
81        assert_eq!(fr.attribute, "cid");
82    }
83
84    #[test]
85    fn parse_absolute_path() {
86        let fr = FlakeRef::parse("/absolute/path#attr").unwrap();
87        assert_eq!(fr.flake_dir, PathBuf::from("/absolute/path"));
88        assert_eq!(fr.attribute, "attr");
89    }
90
91    #[test]
92    fn parse_relative_path() {
93        let fr = FlakeRef::parse("relative/path#attr").unwrap();
94        assert_eq!(fr.flake_dir, PathBuf::from("relative/path"));
95        assert_eq!(fr.attribute, "attr");
96    }
97
98    #[test]
99    fn parse_missing_hash_returns_error() {
100        let err = FlakeRef::parse("no-hash-here").unwrap_err();
101        assert!(matches!(err, FlakeRefError::MissingAttribute(_)));
102        assert!(err.to_string().contains("no-hash-here"));
103    }
104
105    #[test]
106    fn parse_empty_attribute_allowed() {
107        let fr = FlakeRef::parse(".#").unwrap();
108        assert_eq!(fr.attribute, "");
109    }
110
111    #[test]
112    fn parse_empty_path_uses_cwd() {
113        let fr = FlakeRef::parse("#attr").unwrap();
114        assert_eq!(fr.flake_dir, std::env::current_dir().unwrap());
115        assert_eq!(fr.attribute, "attr");
116    }
117
118    #[test]
119    fn parse_dotted_attribute() {
120        let fr = FlakeRef::parse("/nix#darwinConfigurations.cid.system").unwrap();
121        assert_eq!(fr.flake_dir, PathBuf::from("/nix"));
122        assert_eq!(fr.attribute, "darwinConfigurations.cid.system");
123    }
124
125    #[test]
126    fn parse_strips_path_scheme() {
127        let fr = FlakeRef::parse("path:/etc/nixos#nixosConfigurations.rio").unwrap();
128        assert_eq!(fr.flake_dir, PathBuf::from("/etc/nixos"));
129        assert_eq!(fr.attribute, "nixosConfigurations.rio");
130    }
131
132    #[test]
133    fn parse_strips_path_scheme_relative() {
134        let fr = FlakeRef::parse("path:./config#attr").unwrap();
135        assert_eq!(fr.flake_dir, PathBuf::from("./config"));
136        assert_eq!(fr.attribute, "attr");
137    }
138
139    #[test]
140    fn parse_multiple_hashes_splits_on_first() {
141        let fr = FlakeRef::parse("/path#attr#extra").unwrap();
142        assert_eq!(fr.flake_dir, PathBuf::from("/path"));
143        assert_eq!(fr.attribute, "attr#extra");
144    }
145
146    #[test]
147    fn error_display_missing_attribute() {
148        let err = FlakeRefError::MissingAttribute("foo".into());
149        assert!(err.to_string().contains("missing '#attribute'"));
150        assert!(err.to_string().contains("foo"));
151    }
152
153    #[test]
154    fn error_display_invalid_path() {
155        let err = FlakeRefError::InvalidPath("bad path".into());
156        assert!(err.to_string().contains("invalid flake path"));
157        assert!(err.to_string().contains("bad path"));
158    }
159}