syntaxis/shared/rust/nickel.rs
Jesús Pérez 3faf7a5fc9
Some checks failed
Build - Verify Code & Build Binaries / Check Code Format (push) Has been cancelled
Build - Verify Code & Build Binaries / Lint with Clippy (push) Has been cancelled
Build - Verify Code & Build Binaries / Test Suite (push) Has been cancelled
Build - Verify Code & Build Binaries / Cargo Check (push) Has been cancelled
Build - Verify Code & Build Binaries / Security Audit (push) Has been cancelled
Build - Verify Code & Build Binaries / Build (Debug) - macos-latest (push) Has been cancelled
Build - Verify Code & Build Binaries / Build (Debug) - ubuntu-latest (push) Has been cancelled
Build - Verify Code & Build Binaries / Build (Debug) - windows-latest (push) Has been cancelled
Build - Verify Code & Build Binaries / Build (Release) - macos-latest (push) Has been cancelled
Build - Verify Code & Build Binaries / Build (Release) - ubuntu-latest (push) Has been cancelled
Build - Verify Code & Build Binaries / Build (Release) - windows-latest (push) Has been cancelled
CI/CD with Staging Preset / Validate Installation with Staging Preset (macos-latest) (push) Has been cancelled
CI/CD with Staging Preset / Validate Installation with Staging Preset (ubuntu-latest) (push) Has been cancelled
CI/CD with Staging Preset / Validate Documentation (push) Has been cancelled
Build - Verify Code & Build Binaries / All Checks Passed (push) Has been cancelled
CI/CD with Staging Preset / Build and Test with Staging Preset (push) Has been cancelled
CI/CD with Staging Preset / Integration Test with Docker Compose (push) Has been cancelled
CI/CD with Staging Preset / Test Summary (push) Has been cancelled
feat: integrate stratum orchestration, kogral bridge, and NATS
platform

  - Add orchestration.rs and kogral_bridge.rs to syntaxis-vapora
  - Replace async-nats with platform-nats (NKey auth support)
  - Wire stratum-orchestrator, stratum-graph, stratum-state deps
  - Upgrade surrealdb 2.3 → 3 with kv-surrealkv and rustls features
  - Consolidate core/Cargo.toml into root workspace (remove virtual manifest)
  - Add shared/rust/nickel.rs for Nickel config integration
  - Rename CLI binary from syntaxis-cli to syntaxis
2026-02-22 22:01:02 +00:00

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;
}
}