evix 1.0.1

Library-first async Nix evaluation engine
use std::collections::BTreeMap;

use anyhow::{Context as _, Result, anyhow};
use serde_json::{Map, Value as Json, json};

use crate::{Derivation, Diff, EvalError, Event};

pub fn derivation_value(d: &Derivation) -> Json {
  let mut outputs = Map::new();
  for (k, v) in &d.outputs {
    outputs.insert(
      k.clone(),
      v.as_ref().map_or(Json::Null, |p| Json::String(p.clone())),
    );
  }

  let mut obj = Map::new();
  obj.insert("attr".into(), json!(d.attr));
  obj.insert("attrPath".into(), json!(d.attr_path));
  obj.insert("name".into(), json!(d.name));
  obj.insert("system".into(), json!(d.system));
  obj.insert("drvPath".into(), json!(d.drv_path));
  obj.insert("outputs".into(), Json::Object(outputs));
  if let Some(meta) = &d.meta {
    obj.insert("meta".into(), meta.clone());
  }
  if !d.input_drvs.is_empty() {
    let drvs: Map<String, Json> = d.input_drvs.clone().into_iter().collect();
    obj.insert("inputDrvs".into(), Json::Object(drvs));
  }
  if let Some(constituents) = &d.constituents {
    obj.insert("constituents".into(), json!(constituents));
  }
  if let Some(error) = &d.gc_root_error {
    obj.insert("gcRootError".into(), json!(error));
  }
  Json::Object(obj)
}

pub fn event_value(event: &Event) -> Json {
  match event {
    Event::Derivation(d) => derivation_value(d),
    Event::AttrSet {
      attr,
      attr_path,
      attrs,
    } => {
      json!({
        "attr": attr,
        "attrPath": attr_path,
        "attrs": attrs,
      })
    },
    Event::Error(e) => {
      json!({
        "attr": e.attr,
        "attrPath": e.attr_path,
        "error": e.error,
        "fatal": e.fatal,
      })
    },
  }
}

pub fn event_line(event: &Event) -> String {
  event_value(event).to_string()
}

pub fn diff_value(diff: &Diff) -> Json {
  json!({
    "added": diff.added.iter().map(derivation_value).collect::<Vec<_>>(),
    "removed": diff.removed.iter().map(derivation_value).collect::<Vec<_>>(),
    "errors": diff.errors,
  })
}

pub fn diff_line(diff: &Diff) -> String {
  diff_value(diff).to_string()
}

pub fn parse_event_line(line: &str) -> Result<Event> {
  let value: Json = serde_json::from_str(line).context("parsing event line")?;
  parse_event_value(value)
}

pub fn parse_event_value(value: Json) -> Result<Event> {
  if value.get("drvPath").is_some() {
    return parse_derivation(value).map(Event::Derivation);
  }
  if value.get("error").is_some() {
    return Ok(Event::Error(EvalError {
      attr:      string_field(&value, "attr")?,
      attr_path: string_vec_field(&value, "attrPath")?,
      error:     string_field(&value, "error")?,
      fatal:     value.get("fatal").and_then(Json::as_bool).unwrap_or(false),
    }));
  }
  Ok(Event::AttrSet {
    attr:      string_field(&value, "attr")?,
    attr_path: string_vec_field(&value, "attrPath")?,
    attrs:     string_vec_field(&value, "attrs").unwrap_or_default(),
  })
}

fn parse_derivation(value: Json) -> Result<Derivation> {
  let outputs = value
    .get("outputs")
    .and_then(Json::as_object)
    .map(|outputs| {
      outputs
        .iter()
        .map(|(name, path)| {
          let path = if path.is_null() {
            None
          } else {
            path.as_str().map(str::to_owned)
          };
          (name.clone(), path)
        })
        .collect()
    })
    .unwrap_or_default();
  let input_drvs = value
    .get("inputDrvs")
    .and_then(Json::as_object)
    .map(|drvs| {
      drvs
        .iter()
        .map(|(path, value)| (path.clone(), value.clone()))
        .collect::<BTreeMap<_, _>>()
    })
    .unwrap_or_default();
  let constituents = value
    .get("constituents")
    .cloned()
    .map(serde_json::from_value)
    .transpose()
    .context("parsing constituents")?;

  Ok(Derivation {
    attr: string_field(&value, "attr")?,
    attr_path: string_vec_field(&value, "attrPath")?,
    name: string_field(&value, "name")?,
    system: string_field(&value, "system")?,
    drv_path: string_field(&value, "drvPath")?,
    outputs,
    meta: value.get("meta").cloned(),
    input_drvs,
    constituents,
    gc_root_error: value
      .get("gcRootError")
      .and_then(Json::as_str)
      .map(str::to_owned),
  })
}

fn string_field(value: &Json, name: &str) -> Result<String> {
  value
    .get(name)
    .and_then(Json::as_str)
    .map(str::to_owned)
    .ok_or_else(|| anyhow!("missing string field {name:?}"))
}

fn string_vec_field(value: &Json, name: &str) -> Result<Vec<String>> {
  value
    .get(name)
    .cloned()
    .map(serde_json::from_value)
    .transpose()
    .with_context(|| format!("parsing field {name:?}"))?
    .ok_or_else(|| anyhow!("missing string list field {name:?}"))
}

#[cfg(test)]
mod tests {
  use super::parse_event_line;
  use crate::Event;

  #[test]
  fn parses_flat_derivation_event() {
    let event = parse_event_line(
      r#"{"attr":"pkg","attrPath":["pkg"],"name":"pkg","system":"x86_64-linux","drvPath":"/nix/store/pkg.drv","outputs":{"out":null},"gcRootError":"link failed"}"#,
    )
    .unwrap();

    let Event::Derivation(drv) = event else {
      panic!("expected derivation");
    };
    assert_eq!(drv.attr, "pkg");
    assert_eq!(drv.system, "x86_64-linux");
    assert_eq!(drv.outputs.get("out"), Some(&None));
    assert_eq!(drv.gc_root_error.as_deref(), Some("link failed"));
  }

  #[test]
  fn parses_flat_error_event() {
    let event = parse_event_line(
      r#"{"attr":"bad","attrPath":["bad"],"error":"boom","fatal":false}"#,
    )
    .unwrap();

    let Event::Error(error) = event else {
      panic!("expected error");
    };
    assert_eq!(error.error, "boom");
    assert!(!error.fatal);
  }
}