use std::collections::HashMap;
use crate::change::Change;
use crate::error::FlakeEditError;
use crate::input::{Follows, Input};
use crate::validate;
use crate::walk::Walker;
pub struct FlakeEdit {
changes: Vec<Change>,
walker: Walker,
}
#[derive(Default, Debug)]
pub enum Outputs {
#[default]
None,
Multiple(Vec<String>),
Any(Vec<String>),
}
pub type InputMap = HashMap<String, Input>;
pub fn sorted_input_ids(inputs: &InputMap) -> Vec<&String> {
let mut keys: Vec<_> = inputs.keys().collect();
keys.sort();
keys
}
pub fn sorted_input_ids_owned(inputs: &InputMap) -> Vec<String> {
let mut keys: Vec<String> = inputs.keys().cloned().collect();
keys.sort();
keys
}
#[derive(Default, Debug)]
pub enum OutputChange {
#[default]
None,
Add(String),
Remove(String),
}
impl FlakeEdit {
pub fn new(changes: Vec<Change>, walker: Walker) -> Self {
Self { changes, walker }
}
pub fn from_text(stream: &str) -> Result<Self, FlakeEditError> {
let validation = validate::validate(stream);
if validation.has_errors() {
return Err(FlakeEditError::Validation(validation.errors));
}
let walker = Walker::new(stream);
Ok(Self::new(Vec::new(), walker))
}
pub fn changes(&self) -> &[Change] {
self.changes.as_ref()
}
pub fn add_change(&mut self, change: Change) {
self.changes.push(change);
}
pub fn source_text(&self) -> String {
self.walker.root.to_string()
}
pub fn curr_list(&self) -> &InputMap {
&self.walker.inputs
}
pub fn list(&mut self) -> &InputMap {
self.walker.inputs.clear();
assert!(self.walker.walk(&Change::None).ok().flatten().is_none());
&self.walker.inputs
}
pub fn apply_change(&mut self, change: Change) -> Result<Option<String>, FlakeEditError> {
match change {
Change::None => Ok(None),
Change::Add { .. } => {
if let Some(input_id) = change.id() {
self.ensure_inputs_populated()?;
let input_id_string = input_id.to_string();
if self.walker.inputs.contains_key(&input_id_string) {
return Err(FlakeEditError::DuplicateInput(input_id_string));
}
}
if let Some(maybe_changed_node) = self.walker.walk(&change.clone())? {
let outputs = self.walker.list_outputs()?;
match outputs {
Outputs::Multiple(out) => {
let id = change.id().unwrap().to_string();
if !out.contains(&id) {
self.walker.root = maybe_changed_node.clone();
if let Some(maybe_changed_node) =
self.walker.change_outputs(OutputChange::Add(id))?
{
return Ok(Some(maybe_changed_node.to_string()));
}
}
}
Outputs::None | Outputs::Any(_) => {}
}
Ok(Some(maybe_changed_node.to_string()))
} else {
self.walker.add_toplevel = true;
let maybe_changed_node = self.walker.walk(&change)?;
Ok(maybe_changed_node.map(|n| n.to_string()))
}
}
Change::Remove { .. } => {
self.ensure_inputs_populated()?;
let removed_id = change.id().unwrap().to_string();
let mut res = None;
while let Some(changed_node) = self.walker.walk(&change)? {
if res == Some(changed_node.clone()) {
break;
}
res = Some(changed_node.clone());
self.walker.root = changed_node.clone();
}
let outputs = self.walker.list_outputs()?;
match outputs {
Outputs::Multiple(out) | Outputs::Any(out) => {
if out.contains(&removed_id)
&& let Some(changed_node) = self
.walker
.change_outputs(OutputChange::Remove(removed_id.clone()))?
{
res = Some(changed_node.clone());
self.walker.root = changed_node.clone();
}
}
Outputs::None => {}
}
let orphaned_follows = self.collect_orphaned_follows(&removed_id);
for orphan_change in orphaned_follows {
while let Some(changed_node) = self.walker.walk(&orphan_change)? {
if res == Some(changed_node.clone()) {
break;
}
res = Some(changed_node.clone());
self.walker.root = changed_node.clone();
}
}
Ok(res.map(|n| n.to_string()))
}
Change::Follows { ref input, .. } => {
self.ensure_inputs_populated()?;
let parent_id = input.input();
if !self.walker.inputs.contains_key(parent_id) {
return Err(FlakeEditError::InputNotFound(parent_id.to_string()));
}
if let Some(maybe_changed_node) = self.walker.walk(&change)? {
Ok(Some(maybe_changed_node.to_string()))
} else {
Ok(None)
}
}
Change::Change { .. } => {
if let Some(input_id) = change.id() {
self.ensure_inputs_populated()?;
let input_id_string = input_id.to_string();
if !self.walker.inputs.contains_key(&input_id_string) {
return Err(FlakeEditError::InputNotFound(input_id_string));
}
}
if let Some(maybe_changed_node) = self.walker.walk(&change)? {
Ok(Some(maybe_changed_node.to_string()))
} else {
Ok(None)
}
}
}
}
pub fn walker(&self) -> &Walker {
&self.walker
}
fn ensure_inputs_populated(&mut self) -> Result<(), FlakeEditError> {
if self.walker.inputs.is_empty() {
let _ = self.walker.walk(&Change::None)?;
}
Ok(())
}
fn collect_orphaned_follows(&self, removed_id: &str) -> Vec<Change> {
let mut orphaned = Vec::new();
for (input_id, input) in &self.walker.inputs {
for follows in input.follows() {
if let Follows::Indirect(follows_name, target) = follows {
if target.trim_matches('"') == removed_id {
let nested_id = format!("{}.{}", input_id, follows_name);
orphaned.push(Change::Remove {
ids: vec![nested_id.into()],
});
}
}
}
}
orphaned
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn already_follows_is_noop() {
let flake = r#"{
inputs = {
nixpkgs.url = "github:nixos/nixpkgs";
crane = {
url = "github:ipetkov/crane";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs = { ... }: { };
}"#;
let mut fe = FlakeEdit::from_text(flake).unwrap();
let original = fe.source_text();
let change = Change::Follows {
input: "crane.nixpkgs".to_string().into(),
target: "nixpkgs".to_string(),
};
let result = fe.apply_change(change).unwrap();
match result {
Some(text) => assert_eq!(text, original, "text should be unchanged"),
None => {} }
}
#[test]
fn new_follows_succeeds() {
let flake = r#"{
inputs = {
nixpkgs.url = "github:nixos/nixpkgs";
crane = {
url = "github:ipetkov/crane";
};
};
outputs = { ... }: { };
}"#;
let mut fe = FlakeEdit::from_text(flake).unwrap();
let change = Change::Follows {
input: "crane.nixpkgs".to_string().into(),
target: "nixpkgs".to_string(),
};
let result = fe.apply_change(change);
assert!(result.is_ok(), "expected Ok, got: {:?}", result);
let text = result.unwrap().unwrap();
assert!(text.contains("inputs.nixpkgs.follows = \"nixpkgs\""));
}
}