use anyhow::{Context, Result, anyhow};
use rnix::ast::{self, HasEntry};
use rowan::ast::AstNode;
use std::ops::Range;
pub(crate) fn replace_input(
flake_nix_content: &str,
input_name: &str,
input_existing: &str,
input_new: &str,
) -> Result<String> {
let tree = rnix::Root::parse(flake_nix_content).ok()?;
let root = tree.expr().context("root")?;
let ast::Expr::AttrSet(ref set) = root else {
return Err(anyhow!("root not attrset"));
};
for entry in set.entries() {
if let ast::Entry::AttrpathValue(attrpath_value) = entry {
if let Some(attrpath) = attrpath_value.attrpath()
&& attrpath.to_string() == "inputs"
{
let ast::Expr::AttrSet(inputs_set) =
attrpath_value.value().context("inputs empty?")?
else {
return Err(anyhow!("inputs not attrset"));
};
let tr = locate_input_text_range(inputs_set, input_name, input_existing)?;
return replace_input_value(flake_nix_content, tr, input_new);
}
}
}
Err(anyhow!("never found inputs in flake"))
}
fn locate_input_text_range(
inputs: ast::AttrSet,
name: &str,
existing: &str,
) -> Result<std::ops::Range<usize>> {
for input in inputs.entries() {
if let ast::Entry::AttrpathValue(attrpath_value) = input {
if let Some(attrpath) = attrpath_value.attrpath()
&& attrpath.attrs().map(|a| a.to_string()).collect::<Vec<_>>() == &[name, "url"]
{
let ast::Expr::Str(url_str) = attrpath_value.value().context("input url empty?")?
else {
return Err(anyhow!("input url not string"));
};
let url_parts = url_str.normalized_parts();
if url_parts.len() != 1 {
return Err(anyhow!("input url wasn't a simple string"));
}
let ast::InterpolPart::Literal(url) = &url_parts[0] else {
return Err(anyhow!("input url wasn't a simple string"));
};
if url != existing {
return Err(anyhow!(
"input url value {url} didn't match expected {existing}"
));
}
let tr = url_str.syntax().text_range();
return Ok(tr.into());
}
}
}
Err(anyhow!("input url not found"))
}
fn replace_input_value(subject: &str, tr: Range<usize>, replacement: &str) -> Result<String> {
if subject.as_bytes()[tr.start] != b'"' {
return Err(anyhow!(
"replacement subject didn't start with double quote"
));
}
if subject.as_bytes()[tr.end - 1] != b'"' {
return Err(anyhow!("replacement subject didn't end with double quote"));
}
let mut subject = subject.to_string();
subject.drain((tr.start + 1)..(tr.end - 1));
subject.insert_str(tr.start + 1, replacement);
Ok(subject)
}
#[test]
fn does_the_replacement_ok() {
assert_eq!(
CHOG_FLAKE_NIX.replace(CHOG_NIXPKGS, CHOG_DELIGHT),
replace_input(CHOG_FLAKE_NIX, "nixpkgs", CHOG_NIXPKGS, CHOG_DELIGHT).unwrap()
);
}
#[cfg(test)]
const CHOG_NIXPKGS: &str = "github:NixOS/nixpkgs/cb82756ecc37fa623f8cf3e88854f9bf7f64af93";
#[cfg(test)]
const CHOG_DELIGHT: &str = "delighted:nossa";
#[cfg(test)]
const CHOG_FLAKE_NIX: &str = r#"
{
description = "chog";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/cb82756ecc37fa623f8cf3e88854f9bf7f64af93";
};
outputs =
{
self,
nixpkgs,
}:
let
systems = [
"aarch64-darwin"
"aarch64-linux"
"x86_64-darwin"
"x86_64-linux"
];
eachSystem = nixpkgs.lib.genAttrs systems;
partsForSystem = system: rec {
pkgs = nixpkgs.legacyPackages.${system};
erlang = pkgs.beam_minimal.interpreters.erlang_27;
beamPackages = pkgs.beam_minimal.packagesWith erlang;
elixir = beamPackages.elixir_1_18;
};
in
{
formatter = eachSystem (system: nixpkgs.legacyPackages.${system}.nixfmt-rfc-style);
packages = eachSystem (
system:
let
inherit (partsForSystem system)
pkgs
erlang
beamPackages
elixir
;
package = pkgs.callPackage ./nix/package.nix {
inherit
beamPackages
erlang
elixir
;
};
in
rec {
default = chog;
chog = package;
chog-docker = pkgs.callPackage ./nix/docker.nix {
chog-rev = self.rev or "dev";
inherit chog;
};
}
);
devShell = eachSystem (
system: import ./nix/shell.nix (partsForSystem system // { mixEnv = "dev"; })
);
};
}
"#;