119 lines
3.8 KiB
Rust
119 lines
3.8 KiB
Rust
|
|
//! Nickel configuration loader.
|
||
|
|
//!
|
||
|
|
//! Evaluates `.ncl` files to JSON via the `nickel export` CLI.
|
||
|
|
//! Optionally resolves OCI imports via `ncl-import-resolver` when a
|
||
|
|
//! `resolver-manifest.json` file is present alongside the config.
|
||
|
|
//!
|
||
|
|
//! # Requirements
|
||
|
|
//!
|
||
|
|
//! - `nickel` must be on `PATH`.
|
||
|
|
//! - `ncl-import-resolver` must be on `PATH` when OCI imports are used.
|
||
|
|
|
||
|
|
use std::path::Path;
|
||
|
|
|
||
|
|
use anyhow::{anyhow, Context};
|
||
|
|
use serde::de::DeserializeOwned;
|
||
|
|
use tokio::process::Command;
|
||
|
|
|
||
|
|
/// Evaluate a Nickel configuration file and deserialize the result into `T`.
|
||
|
|
///
|
||
|
|
/// Steps:
|
||
|
|
/// 1. If a `resolver-manifest.json` exists beside the `.ncl` file, run
|
||
|
|
/// `ncl-import-resolver <manifest>` to resolve OCI imports.
|
||
|
|
/// 2. Run `nickel export --format json <ncl_path>` and capture stdout.
|
||
|
|
/// 3. Deserialize the JSON output via `serde_json`.
|
||
|
|
///
|
||
|
|
/// # Errors
|
||
|
|
///
|
||
|
|
/// Returns an error if:
|
||
|
|
/// - `ncl-import-resolver` exits non-zero (when `resolver-manifest.json` exists).
|
||
|
|
/// - `nickel export` exits non-zero or cannot be found.
|
||
|
|
/// - The JSON output cannot be deserialized into `T`.
|
||
|
|
pub async fn load_nickel_config<T: DeserializeOwned>(ncl_path: &Path) -> anyhow::Result<T> {
|
||
|
|
// Resolve OCI imports if a manifest is co-located with the config file.
|
||
|
|
if let Some(parent) = ncl_path.parent() {
|
||
|
|
let manifest = parent.join("resolver-manifest.json");
|
||
|
|
if manifest.exists() {
|
||
|
|
let status = Command::new("ncl-import-resolver")
|
||
|
|
.arg(&manifest)
|
||
|
|
.status()
|
||
|
|
.await
|
||
|
|
.context("launching ncl-import-resolver")?;
|
||
|
|
|
||
|
|
if !status.success() {
|
||
|
|
return Err(anyhow!(
|
||
|
|
"ncl-import-resolver failed (exit {:?}) on '{}'",
|
||
|
|
status.code(),
|
||
|
|
manifest.display()
|
||
|
|
));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Export the Nickel config to JSON.
|
||
|
|
let out = Command::new("nickel")
|
||
|
|
.args(["export", "--format", "json"])
|
||
|
|
.arg(ncl_path)
|
||
|
|
.output()
|
||
|
|
.await
|
||
|
|
.with_context(|| {
|
||
|
|
format!(
|
||
|
|
"launching `nickel export` on '{}'",
|
||
|
|
ncl_path.display()
|
||
|
|
)
|
||
|
|
})?;
|
||
|
|
|
||
|
|
if !out.status.success() {
|
||
|
|
let stderr = String::from_utf8_lossy(&out.stderr);
|
||
|
|
return Err(anyhow!(
|
||
|
|
"`nickel export` failed on '{}': {}",
|
||
|
|
ncl_path.display(),
|
||
|
|
stderr.trim()
|
||
|
|
));
|
||
|
|
}
|
||
|
|
|
||
|
|
serde_json::from_slice::<T>(&out.stdout).with_context(|| {
|
||
|
|
format!(
|
||
|
|
"deserializing Nickel output from '{}' into {}",
|
||
|
|
ncl_path.display(),
|
||
|
|
std::any::type_name::<T>()
|
||
|
|
)
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
#[cfg(test)]
|
||
|
|
mod tests {
|
||
|
|
use super::*;
|
||
|
|
use serde::Deserialize;
|
||
|
|
use std::io::Write;
|
||
|
|
|
||
|
|
#[derive(Debug, Deserialize, PartialEq)]
|
||
|
|
struct Simple {
|
||
|
|
name: String,
|
||
|
|
value: u32,
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Verifies the error path when `nickel` is not installed or the file
|
||
|
|
/// doesn't exist (CI without nickel will hit this branch).
|
||
|
|
#[tokio::test]
|
||
|
|
async fn test_load_nickel_config_missing_file() {
|
||
|
|
let result = load_nickel_config::<Simple>(Path::new("/nonexistent/path.ncl")).await;
|
||
|
|
assert!(
|
||
|
|
result.is_err(),
|
||
|
|
"expected error for non-existent .ncl file"
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Verifies resolver-manifest detection: if no manifest exists alongside the
|
||
|
|
/// config file, resolver is skipped (only nickel export runs).
|
||
|
|
#[tokio::test]
|
||
|
|
async fn test_load_nickel_config_no_manifest_skips_resolver() {
|
||
|
|
let dir = tempfile::tempdir().unwrap();
|
||
|
|
let ncl_path = dir.path().join("config.ncl");
|
||
|
|
std::fs::write(&ncl_path, r#"{ name = "test", value = 42 }"#).unwrap();
|
||
|
|
|
||
|
|
// nickel may or may not be installed; either way, no manifest → no resolver invoked.
|
||
|
|
let _ = load_nickel_config::<Simple>(&ncl_path).await;
|
||
|
|
}
|
||
|
|
}
|