iqan 0.2.0

Sync Nix flake pins
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 {
            // Handle "input.url".
            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 {
                    // TODO: Allow ignoring, or parse both sides to match.
                    return Err(anyhow!(
                        "input url value {url} didn't match expected {existing}"
                    ));
                }

                let tr = url_str.syntax().text_range();
                return Ok(tr.into());
            }

            // TODO: Handle "input = { ... }".
        }
    }

    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"; })
      );
    };
}
"#;