255 lines
7.6 KiB
Rust
255 lines
7.6 KiB
Rust
// vapora-shared: Nickel CLI bridge - Execute Nickel commands for schema
|
|
// operations Provides interface to nickel query, typecheck, and export commands
|
|
|
|
use std::path::{Path, PathBuf};
|
|
use std::process::Stdio;
|
|
|
|
use tokio::process::Command;
|
|
use tracing::debug;
|
|
|
|
use crate::error::{Result, VaporaError};
|
|
|
|
/// Nickel CLI bridge for schema operations
|
|
#[derive(Debug, Clone)]
|
|
pub struct NickelCli {
|
|
/// Path to nickel executable
|
|
nickel_path: PathBuf,
|
|
|
|
/// Timeout for nickel commands (milliseconds)
|
|
timeout_ms: u64,
|
|
}
|
|
|
|
impl NickelCli {
|
|
/// Create new Nickel CLI bridge with default path
|
|
pub fn new() -> Self {
|
|
Self {
|
|
nickel_path: PathBuf::from("nickel"),
|
|
timeout_ms: 30_000, // 30 seconds
|
|
}
|
|
}
|
|
|
|
/// Create Nickel CLI bridge with custom path
|
|
pub fn with_path(nickel_path: PathBuf) -> Self {
|
|
Self {
|
|
nickel_path,
|
|
timeout_ms: 30_000,
|
|
}
|
|
}
|
|
|
|
/// Set timeout for Nickel commands
|
|
pub fn with_timeout(mut self, timeout_ms: u64) -> Self {
|
|
self.timeout_ms = timeout_ms;
|
|
self
|
|
}
|
|
|
|
/// Execute `nickel query` to extract schema metadata
|
|
///
|
|
/// Returns JSON representation of Nickel value
|
|
pub async fn query(&self, path: &Path, field: Option<&str>) -> Result<serde_json::Value> {
|
|
let mut args = vec!["query", "--format", "json"];
|
|
|
|
if let Some(f) = field {
|
|
args.push("--field");
|
|
args.push(f);
|
|
}
|
|
|
|
let path_str = path.to_str().ok_or_else(|| {
|
|
VaporaError::ValidationError(format!("Invalid path: {}", path.display()))
|
|
})?;
|
|
args.push(path_str);
|
|
|
|
debug!("Executing: nickel {}", args.join(" "));
|
|
|
|
let output = self.execute_with_timeout(&args).await?;
|
|
|
|
if !output.status.success() {
|
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
return Err(VaporaError::ValidationError(format!(
|
|
"Nickel query failed: {}",
|
|
stderr
|
|
)));
|
|
}
|
|
|
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
|
|
serde_json::from_str(&stdout).map_err(|e| {
|
|
VaporaError::ValidationError(format!("Failed to parse Nickel output: {}", e))
|
|
})
|
|
}
|
|
|
|
/// Execute `nickel typecheck` to validate schema
|
|
///
|
|
/// Returns Ok(()) if typecheck passes, Err with details if it fails
|
|
pub async fn typecheck(&self, path: &Path) -> Result<()> {
|
|
let path_str = path.to_str().ok_or_else(|| {
|
|
VaporaError::ValidationError(format!("Invalid path: {}", path.display()))
|
|
})?;
|
|
|
|
let args = vec!["typecheck", path_str];
|
|
|
|
debug!("Executing: nickel {}", args.join(" "));
|
|
|
|
let output = self.execute_with_timeout(&args).await?;
|
|
|
|
if !output.status.success() {
|
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
return Err(VaporaError::ValidationError(format!(
|
|
"Nickel typecheck failed: {}",
|
|
stderr
|
|
)));
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Execute `nickel export` to generate JSON output
|
|
///
|
|
/// Returns exported JSON value
|
|
pub async fn export(&self, path: &Path) -> Result<serde_json::Value> {
|
|
let path_str = path.to_str().ok_or_else(|| {
|
|
VaporaError::ValidationError(format!("Invalid path: {}", path.display()))
|
|
})?;
|
|
|
|
let args = vec!["export", "--format", "json", path_str];
|
|
|
|
debug!("Executing: nickel {}", args.join(" "));
|
|
|
|
let output = self.execute_with_timeout(&args).await?;
|
|
|
|
if !output.status.success() {
|
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
return Err(VaporaError::ValidationError(format!(
|
|
"Nickel export failed: {}",
|
|
stderr
|
|
)));
|
|
}
|
|
|
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
|
|
serde_json::from_str(&stdout).map_err(|e| {
|
|
VaporaError::ValidationError(format!("Failed to parse Nickel export: {}", e))
|
|
})
|
|
}
|
|
|
|
/// Check if Nickel CLI is available
|
|
pub async fn is_available(&self) -> bool {
|
|
let result = Command::new(&self.nickel_path)
|
|
.arg("--version")
|
|
.stdout(Stdio::null())
|
|
.stderr(Stdio::null())
|
|
.status()
|
|
.await;
|
|
|
|
match result {
|
|
Ok(status) => status.success(),
|
|
Err(_) => false,
|
|
}
|
|
}
|
|
|
|
/// Get Nickel CLI version
|
|
pub async fn version(&self) -> Result<String> {
|
|
let output = Command::new(&self.nickel_path)
|
|
.arg("--version")
|
|
.output()
|
|
.await
|
|
.map_err(|e| {
|
|
VaporaError::ValidationError(format!("Failed to get Nickel version: {}", e))
|
|
})?;
|
|
|
|
if !output.status.success() {
|
|
return Err(VaporaError::ValidationError(
|
|
"Failed to get Nickel version".to_string(),
|
|
));
|
|
}
|
|
|
|
let version = String::from_utf8_lossy(&output.stdout);
|
|
Ok(version.trim().to_string())
|
|
}
|
|
|
|
/// Execute Nickel command with timeout
|
|
async fn execute_with_timeout(&self, args: &[&str]) -> Result<std::process::Output> {
|
|
let child = Command::new(&self.nickel_path)
|
|
.args(args)
|
|
.stdout(Stdio::piped())
|
|
.stderr(Stdio::piped())
|
|
.spawn()
|
|
.map_err(|e| {
|
|
VaporaError::ValidationError(format!("Failed to execute Nickel: {}", e))
|
|
})?;
|
|
|
|
// Wait with timeout
|
|
let timeout = tokio::time::Duration::from_millis(self.timeout_ms);
|
|
let result = tokio::time::timeout(timeout, child.wait_with_output()).await;
|
|
|
|
match result {
|
|
Ok(Ok(output)) => Ok(output),
|
|
Ok(Err(e)) => Err(VaporaError::ValidationError(format!(
|
|
"Nickel command failed: {}",
|
|
e
|
|
))),
|
|
Err(_) => {
|
|
// Timeout occurred - process is still running but we give up
|
|
Err(VaporaError::ValidationError(format!(
|
|
"Nickel command timed out after {}ms",
|
|
self.timeout_ms
|
|
)))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Default for NickelCli {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[tokio::test]
|
|
async fn test_nickel_cli_creation() {
|
|
let cli = NickelCli::new();
|
|
assert_eq!(cli.nickel_path, PathBuf::from("nickel"));
|
|
assert_eq!(cli.timeout_ms, 30_000);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_nickel_cli_with_path() {
|
|
let custom_path = PathBuf::from("/usr/local/bin/nickel");
|
|
let cli = NickelCli::with_path(custom_path.clone());
|
|
assert_eq!(cli.nickel_path, custom_path);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_nickel_cli_with_timeout() {
|
|
let cli = NickelCli::new().with_timeout(5_000);
|
|
assert_eq!(cli.timeout_ms, 5_000);
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[ignore] // Requires Nickel CLI installed
|
|
async fn test_nickel_is_available() {
|
|
let cli = NickelCli::new();
|
|
let available = cli.is_available().await;
|
|
// This will pass if nickel is in PATH
|
|
if available {
|
|
println!("Nickel CLI is available");
|
|
} else {
|
|
println!("Nickel CLI is not available (install it to run this test)");
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[ignore] // Requires Nickel CLI installed
|
|
async fn test_nickel_version() {
|
|
let cli = NickelCli::new();
|
|
if cli.is_available().await {
|
|
let version = cli.version().await.unwrap();
|
|
println!("Nickel version: {}", version);
|
|
assert!(!version.is_empty());
|
|
}
|
|
}
|
|
}
|