use std::rc::Rc;
use super::*;
use sui_spec::{HasName, Spec};
pub(crate) fn load_format<F: Spec + HasName>(
fmt_name: &str,
bridge: &str,
) -> Result<F, EvalError> {
F::load_named(fmt_name).map_err(|e| {
EvalError::type_error(format!("{bridge}: load `{fmt_name}`: {e:?}"))
})
}
pub(crate) fn attrs_required_string(
attrs: &NixAttrs,
key: &str,
bridge: &str,
) -> Result<String, EvalError> {
let v = attrs.get(key).ok_or_else(|| EvalError::type_error(format!(
"{bridge}: missing required field `{key}`",
)))?;
match crate::eval::force_value(v)? {
Value::String(s) => Ok(s.chars.to_string()),
other => Err(EvalError::type_error(format!(
"{bridge}: field `{key}` must be a string, got {}",
other.type_name(),
))),
}
}
pub(crate) fn attrs_optional_string(
attrs: &NixAttrs,
key: &str,
bridge: &str,
) -> Result<Option<String>, EvalError> {
match attrs.get(key) {
Some(v) => match crate::eval::force_value(v)? {
Value::String(s) => Ok(Some(s.chars.to_string())),
other => Err(EvalError::type_error(format!(
"{bridge}: field `{key}` must be a string, got {}",
other.type_name(),
))),
},
None => Ok(None),
}
}
pub(crate) fn attrs_required_attrs(
attrs: &NixAttrs,
key: &str,
bridge: &str,
) -> Result<Rc<NixAttrs>, EvalError> {
let v = attrs.get(key).ok_or_else(|| EvalError::type_error(format!(
"{bridge}: missing required field `{key}`",
)))?;
match crate::eval::force_value(v)? {
Value::Attrs(a) => Ok(a),
other => Err(EvalError::type_error(format!(
"{bridge}: field `{key}` must be an attrset, got {}",
other.type_name(),
))),
}
}
pub(crate) fn as_attrs(value: &Value, bridge: &str) -> Result<Rc<NixAttrs>, EvalError> {
match crate::eval::force_value(value)? {
Value::Attrs(a) => Ok(a),
other => Err(EvalError::type_error(format!(
"{bridge}: expected attrset, got {}",
other.type_name(),
))),
}
}
pub(crate) fn as_string(value: &Value, bridge: &str) -> Result<String, EvalError> {
match crate::eval::force_value(value)? {
Value::String(s) => Ok(s.chars.to_string()),
other => Err(EvalError::type_error(format!(
"{bridge}: expected string, got {}",
other.type_name(),
))),
}
}
pub(crate) fn attrs_required_int(
attrs: &NixAttrs,
key: &str,
bridge: &str,
) -> Result<i64, EvalError> {
let v = attrs.get(key).ok_or_else(|| EvalError::type_error(format!(
"{bridge}: missing required field `{key}`",
)))?;
match crate::eval::force_value(v)? {
Value::Int(n) => Ok(n),
other => Err(EvalError::type_error(format!(
"{bridge}: field `{key}` must be an int, got {}",
other.type_name(),
))),
}
}
pub(crate) fn attrs_optional_int(
attrs: &NixAttrs,
key: &str,
bridge: &str,
) -> Result<Option<i64>, EvalError> {
match attrs.get(key) {
Some(v) => match crate::eval::force_value(v)? {
Value::Int(n) => Ok(Some(n)),
other => Err(EvalError::type_error(format!(
"{bridge}: field `{key}` must be an int, got {}",
other.type_name(),
))),
},
None => Ok(None),
}
}
pub(crate) fn attrs_required_bool(
attrs: &NixAttrs,
key: &str,
bridge: &str,
) -> Result<bool, EvalError> {
let v = attrs.get(key).ok_or_else(|| EvalError::type_error(format!(
"{bridge}: missing required field `{key}`",
)))?;
match crate::eval::force_value(v)? {
Value::Bool(b) => Ok(b),
other => Err(EvalError::type_error(format!(
"{bridge}: field `{key}` must be a bool, got {}",
other.type_name(),
))),
}
}
pub(crate) fn attrs_bool_or_default(
attrs: &NixAttrs,
key: &str,
default: bool,
bridge: &str,
) -> Result<bool, EvalError> {
match attrs.get(key) {
Some(v) => match crate::eval::force_value(v)? {
Value::Bool(b) => Ok(b),
other => Err(EvalError::type_error(format!(
"{bridge}: field `{key}` must be a bool, got {}",
other.type_name(),
))),
},
None => Ok(default),
}
}
pub(crate) fn attrs_required_list(
attrs: &NixAttrs,
key: &str,
bridge: &str,
) -> Result<Rc<NixList>, EvalError> {
let v = attrs.get(key).ok_or_else(|| EvalError::type_error(format!(
"{bridge}: missing required field `{key}`",
)))?;
match crate::eval::force_value(v)? {
Value::List(l) => Ok(l),
other => Err(EvalError::type_error(format!(
"{bridge}: field `{key}` must be a list, got {}",
other.type_name(),
))),
}
}
pub(crate) fn as_list(value: &Value, bridge: &str) -> Result<Rc<NixList>, EvalError> {
match crate::eval::force_value(value)? {
Value::List(l) => Ok(l),
other => Err(EvalError::type_error(format!(
"{bridge}: expected list, got {}",
other.type_name(),
))),
}
}
pub(crate) fn attrs_string_list(
attrs: &NixAttrs,
key: &str,
bridge: &str,
) -> Result<Vec<String>, EvalError> {
let Some(v) = attrs.get(key) else { return Ok(Vec::new()); };
let list = match crate::eval::force_value(v)? {
Value::List(l) => l,
other => return Err(EvalError::type_error(format!(
"{bridge}: field `{key}` must be a list, got {}",
other.type_name(),
))),
};
let mut out = Vec::with_capacity(list.len());
for item in list.iter() {
out.push(as_string(item, bridge)?);
}
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
fn attrs_of(pairs: &[(&str, Value)]) -> Rc<NixAttrs> {
let mut a = NixAttrs::new();
for (k, v) in pairs {
a.insert(k.to_string(), v.clone());
}
Rc::new(a)
}
#[test]
fn required_string_finds_value() {
let a = attrs_of(&[("name", Value::string("hello"))]);
let s = attrs_required_string(&a, "name", "test").unwrap();
assert_eq!(s, "hello");
}
#[test]
fn required_string_missing_field_errors() {
let a = attrs_of(&[]);
let err = attrs_required_string(&a, "name", "test").unwrap_err();
let msg = format!("{err:?}");
assert!(msg.contains("test:"));
assert!(msg.contains("missing required field"));
assert!(msg.contains("`name`"));
}
#[test]
fn required_string_wrong_type_errors() {
let a = attrs_of(&[("name", Value::Int(42))]);
let err = attrs_required_string(&a, "name", "test").unwrap_err();
let msg = format!("{err:?}");
assert!(msg.contains("must be a string"));
}
#[test]
fn optional_string_returns_none_when_absent() {
let a = attrs_of(&[]);
let o = attrs_optional_string(&a, "name", "test").unwrap();
assert!(o.is_none());
}
#[test]
fn optional_string_returns_some_when_present() {
let a = attrs_of(&[("name", Value::string("x"))]);
let o = attrs_optional_string(&a, "name", "test").unwrap();
assert_eq!(o.as_deref(), Some("x"));
}
#[test]
fn required_attrs_extracts_sub_attrset() {
let inner = attrs_of(&[("k", Value::Int(1))]);
let a = attrs_of(&[("sub", Value::Attrs(inner))]);
let sub = attrs_required_attrs(&a, "sub", "test").unwrap();
assert!(sub.get("k").is_some());
}
#[test]
fn bridge_name_appears_in_every_error() {
let a = attrs_of(&[]);
let err = attrs_required_string(&a, "x", "builtins.sui.foo").unwrap_err();
assert!(format!("{err:?}").contains("builtins.sui.foo"));
let err = attrs_required_attrs(&a, "x", "builtins.sui.foo").unwrap_err();
assert!(format!("{err:?}").contains("builtins.sui.foo"));
}
#[test]
fn required_int_finds_value() {
let a = attrs_of(&[("port", Value::Int(8080))]);
let n = attrs_required_int(&a, "port", "test").unwrap();
assert_eq!(n, 8080);
}
#[test]
fn required_int_wrong_type_errors() {
let a = attrs_of(&[("port", Value::string("8080"))]);
let err = attrs_required_int(&a, "port", "test").unwrap_err();
let msg = format!("{err:?}");
assert!(msg.contains("must be an int"));
}
#[test]
fn optional_int_returns_none_when_absent() {
let a = attrs_of(&[]);
let r = attrs_optional_int(&a, "port", "test").unwrap();
assert!(r.is_none());
}
#[test]
fn required_bool_finds_value() {
let a = attrs_of(&[("enable", Value::Bool(true))]);
let b = attrs_required_bool(&a, "enable", "test").unwrap();
assert!(b);
}
#[test]
fn bool_or_default_falls_back_when_absent() {
let a = attrs_of(&[]);
let b = attrs_bool_or_default(&a, "enable", true, "test").unwrap();
assert!(b);
let b = attrs_bool_or_default(&a, "enable", false, "test").unwrap();
assert!(!b);
}
#[test]
fn required_list_finds_value() {
let a = attrs_of(&[("xs", Value::list(vec![Value::Int(1), Value::Int(2)]))]);
let l = attrs_required_list(&a, "xs", "test").unwrap();
assert_eq!(l.len(), 2);
}
#[test]
fn string_list_collects_strings() {
let a = attrs_of(&[("refs", Value::list(vec![
Value::string("a"),
Value::string("b"),
]))]);
let v = attrs_string_list(&a, "refs", "test").unwrap();
assert_eq!(v, vec!["a".to_string(), "b".to_string()]);
}
#[test]
fn string_list_returns_empty_when_absent() {
let a = attrs_of(&[]);
let v = attrs_string_list(&a, "refs", "test").unwrap();
assert!(v.is_empty());
}
#[test]
fn string_list_errors_on_non_string_element() {
let a = attrs_of(&[("refs", Value::list(vec![
Value::string("a"),
Value::Int(42),
]))]);
let err = attrs_string_list(&a, "refs", "test").unwrap_err();
let msg = format!("{err:?}");
assert!(msg.contains("expected string"));
}
#[test]
fn load_format_finds_canonical_lock_file_format() {
use sui_spec::lock_file::LockFileFormat;
let fmt: LockFileFormat = load_format(
"cppnix-flake-lock-v7",
"test",
).unwrap();
assert_eq!(fmt.name, "cppnix-flake-lock-v7");
assert_eq!(fmt.version, 7);
}
#[test]
fn load_format_errors_with_bridge_name_when_missing() {
use sui_spec::lock_file::LockFileFormat;
let err: EvalError = load_format::<LockFileFormat>(
"no-such-lockfile-format",
"builtins.sui.test.x",
).unwrap_err();
let msg = format!("{err:?}");
assert!(msg.contains("builtins.sui.test.x"),
"missing bridge name in: {msg}");
assert!(msg.contains("no-such-lockfile-format"),
"missing format name in: {msg}");
}
#[test]
fn load_format_works_for_narinfo() {
use sui_spec::narinfo::NarinfoFormat;
let fmt: NarinfoFormat = load_format(
"cppnix-narinfo-v1",
"test",
).unwrap();
assert_eq!(fmt.name, "cppnix-narinfo-v1");
}
#[test]
fn load_format_works_for_realisation() {
use sui_spec::realisation::RealisationFormat;
let fmt: RealisationFormat = load_format(
"cppnix-realisation-v1",
"test",
).unwrap();
assert_eq!(fmt.name, "cppnix-realisation-v1");
}
}