flake_edit/
change.rs

1use crate::walk::Context;
2
3#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)]
4pub enum Change {
5    #[default]
6    None,
7    Add {
8        id: Option<String>,
9        uri: Option<String>,
10        // Add an input as a flake.
11        flake: bool,
12    },
13    Remove {
14        ids: Vec<ChangeId>,
15    },
16    Change {
17        id: Option<String>,
18        uri: Option<String>,
19        ref_or_rev: Option<String>,
20    },
21    /// Add a follows relationship to an input.
22    /// Example: `flake-edit follow rust-overlay.nixpkgs nixpkgs`
23    /// Creates: `rust-overlay.inputs.nixpkgs.follows = "nixpkgs";`
24    Follows {
25        /// The input path (e.g., "rust-overlay.nixpkgs" for rust-overlay's nixpkgs input)
26        input: ChangeId,
27        /// The target input to follow (e.g., "nixpkgs")
28        target: String,
29    },
30}
31
32#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize, PartialEq)]
33pub struct ChangeId(String);
34
35impl ChangeId {
36    /// Get the part after the dot (e.g., "nixpkgs" from "poetry2nix.nixpkgs").
37    pub fn follows(&self) -> Option<&str> {
38        self.0.split_once('.').map(|(_, post)| post)
39    }
40
41    /// Get the input part (before the dot, or the whole thing if no dot).
42    pub fn input(&self) -> &str {
43        self.0.split_once('.').map_or(&self.0, |(pre, _)| pre)
44    }
45
46    /// Check if this ChangeId matches the given input and optional follows.
47    fn matches(&self, input: &str, follows: Option<&str>) -> bool {
48        if self.input() != input {
49            return false;
50        }
51        match (self.follows(), follows) {
52            (Some(self_follows), Some(f)) => self_follows == f,
53            (Some(_), None) => false,
54            (None, _) => true,
55        }
56    }
57
58    pub fn matches_with_follows(&self, input: &str, follows: Option<String>) -> bool {
59        self.matches(input, follows.as_deref())
60    }
61
62    /// Match against context. The context carries the input attribute.
63    pub fn matches_with_ctx(&self, follows: &str, ctx: Option<Context>) -> bool {
64        let ctx_input = ctx.and_then(|f| f.level().first().cloned());
65        match ctx_input {
66            Some(input) => self.matches(&input, Some(follows)),
67            None => self.input() == follows,
68        }
69    }
70}
71
72impl std::fmt::Display for ChangeId {
73    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
74        write!(f, "{}", self.0)
75    }
76}
77
78impl From<String> for ChangeId {
79    fn from(value: String) -> Self {
80        ChangeId(value)
81    }
82}
83
84impl Change {
85    pub fn id(&self) -> Option<ChangeId> {
86        match self {
87            Change::None => None,
88            Change::Add { id, .. } => id.clone().map(|id| id.into()),
89            Change::Remove { ids } => ids.first().cloned(),
90            Change::Change { id, .. } => id.clone().map(|id| id.into()),
91            Change::Follows { input, .. } => Some(input.clone()),
92        }
93    }
94
95    pub fn ids(&self) -> Vec<ChangeId> {
96        match self {
97            Change::Remove { ids } => ids.clone(),
98            Change::Follows { input, .. } => vec![input.clone()],
99            _ => self.id().into_iter().collect(),
100        }
101    }
102    pub fn is_remove(&self) -> bool {
103        matches!(self, Change::Remove { .. })
104    }
105    pub fn is_some(&self) -> bool {
106        !matches!(self, Change::None)
107    }
108    pub fn is_add(&self) -> bool {
109        matches!(self, Change::Add { .. })
110    }
111    pub fn is_change(&self) -> bool {
112        matches!(self, Change::Change { .. })
113    }
114    pub fn is_follows(&self) -> bool {
115        matches!(self, Change::Follows { .. })
116    }
117    pub fn uri(&self) -> Option<&String> {
118        match self {
119            Change::Change { uri, .. } | Change::Add { uri, .. } => uri.as_ref(),
120            _ => None,
121        }
122    }
123    pub fn follows_target(&self) -> Option<&String> {
124        match self {
125            Change::Follows { target, .. } => Some(target),
126            _ => None,
127        }
128    }
129
130    pub fn success_messages(&self) -> Vec<String> {
131        match self {
132            Change::Add { id, uri, .. } => {
133                vec![format!(
134                    "Added input: {} = {}",
135                    id.as_deref().unwrap_or("?"),
136                    uri.as_deref().unwrap_or("?")
137                )]
138            }
139            Change::Remove { ids } => ids
140                .iter()
141                .map(|id| format!("Removed input: {}", id))
142                .collect(),
143            Change::Change { id, uri, .. } => {
144                vec![format!(
145                    "Changed input: {} -> {}",
146                    id.as_deref().unwrap_or("?"),
147                    uri.as_deref().unwrap_or("?")
148                )]
149            }
150            Change::Follows { input, target } => {
151                vec![format!(
152                    "Added follows: {}.inputs.{}.follows = \"{}\"",
153                    input.input(),
154                    input.follows().unwrap_or("?"),
155                    target
156                )]
157            }
158            Change::None => vec![],
159        }
160    }
161}