chore: add source
This commit is contained in:
parent
af18c7929b
commit
5a4d9df16e
2227
Cargo.lock
generated
Normal file
2227
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
23
Cargo.toml
Normal file
23
Cargo.toml
Normal file
@ -0,0 +1,23 @@
|
||||
[package]
|
||||
|
||||
name = "nu_plugin_kcl"
|
||||
version = "0.1.0"
|
||||
authors = ["Jesús Pérez <jpl@jesusperez.com>"]
|
||||
edition = "2024"
|
||||
description = "Nushell plugin for KCL CLI wrapper"
|
||||
repository = "https://github.com/jesusperez/nu_plugin_kcl"
|
||||
license = "MIT"
|
||||
|
||||
[dependencies]
|
||||
# for local development, you can use a path dependency
|
||||
nu-plugin = { path = "../nushell/crates/nu-plugin" }
|
||||
nu-protocol = { path = "../nushell/crates/nu-protocol", features = ["plugin"] }
|
||||
#nu-plugin = "0.104.0"
|
||||
#nu-protocol = { version = "0.104.0", features = ["plugin"] }
|
||||
|
||||
anyhow = "1.0"
|
||||
tempfile = "3"
|
||||
|
||||
[dev-dependencies]
|
||||
nu-plugin-test-support = { path = "../nushell/crates/nu-plugin-test-support" }
|
||||
#nu-plugin-test-support = { version = "0.104.0" }
|
144
src/helpers.rs
Normal file
144
src/helpers.rs
Normal file
@ -0,0 +1,144 @@
|
||||
// Helper functions using KCL CLI
|
||||
use anyhow::Result;
|
||||
use std::process::Command;
|
||||
|
||||
/// Run a KCL file using the KCL CLI.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `file` - Path to the KCL file to execute.
|
||||
/// * `format` - Output format (e.g., "yaml" or "json").
|
||||
/// * `output` - Optional output file path.
|
||||
/// * `defines` - List of variable definitions (e.g., ["foo=bar"]).
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Ok(String)` with the output or output file path on success.
|
||||
/// * `Err(anyhow::Error)` if the KCL command fails.
|
||||
pub(crate) fn run_kcl_command(
|
||||
file: &str,
|
||||
format: &str,
|
||||
output: &Option<String>,
|
||||
defines: &[String],
|
||||
) -> Result<String> {
|
||||
let mut cmd = Command::new("kcl");
|
||||
cmd.arg("run").arg(file).arg("--format").arg(format);
|
||||
|
||||
// Add defined variables
|
||||
for define in defines {
|
||||
cmd.arg("-D").arg(define);
|
||||
}
|
||||
|
||||
// Add output file if specified
|
||||
if let Some(output_file) = output {
|
||||
cmd.arg("-o").arg(output_file);
|
||||
}
|
||||
|
||||
let output_res = cmd
|
||||
.output()
|
||||
.map_err(|e| anyhow::anyhow!("Error executing kcl: {}", e))?;
|
||||
|
||||
if output_res.status.success() {
|
||||
if let Some(output_file) = output {
|
||||
Ok(format!("✅ {}", output_file))
|
||||
} else {
|
||||
Ok(format!(
|
||||
"✅ {}",
|
||||
String::from_utf8_lossy(&output_res.stdout)
|
||||
))
|
||||
}
|
||||
} else {
|
||||
Err(anyhow::anyhow!(
|
||||
"❌: {}",
|
||||
String::from_utf8_lossy(&output_res.stderr)
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// Format a KCL file using the KCL CLI.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `file` - Path to the KCL file to format.
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Ok(String)` with a success message if formatting succeeds.
|
||||
/// * `Err(anyhow::Error)` if formatting fails.
|
||||
pub(crate) fn format_kcl_file(file: &str) -> Result<String> {
|
||||
let output = Command::new("kcl")
|
||||
.arg("fmt")
|
||||
.arg(file)
|
||||
.output()
|
||||
.map_err(|e| anyhow::anyhow!("Error executing kcl fmt: {}", e))?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(anyhow::anyhow!(
|
||||
"KCL format failed: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
));
|
||||
}
|
||||
|
||||
Ok(format!("✅ File formatted: {}", file))
|
||||
}
|
||||
|
||||
/// Validate all KCL files in a directory using the KCL CLI.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `dir` - Path to the directory to search for KCL files.
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Ok(String)` with a summary of validation results if all files are valid or no files found.
|
||||
/// * `Err(anyhow::Error)` if validation fails for any file or if the find command fails.
|
||||
pub(crate) fn validate_kcl_project(dir: &str) -> Result<String> {
|
||||
// Find KCL files in directory
|
||||
let find_output = Command::new("find")
|
||||
.arg(dir)
|
||||
.arg("-name")
|
||||
.arg("*.k")
|
||||
.arg("-type")
|
||||
.arg("f")
|
||||
.output()
|
||||
.map_err(|e| anyhow::anyhow!("Error finding KCL files: {}", e))?;
|
||||
|
||||
let files = String::from_utf8_lossy(&find_output.stdout);
|
||||
let kcl_files: Vec<&str> = files.lines().filter(|line| !line.is_empty()).collect();
|
||||
|
||||
if kcl_files.is_empty() {
|
||||
return Ok(format!("No KCL files found in {}", dir));
|
||||
}
|
||||
|
||||
let mut results = Vec::new();
|
||||
let mut all_valid = true;
|
||||
|
||||
for file in &kcl_files {
|
||||
let output = Command::new("kcl")
|
||||
.arg("run")
|
||||
.arg(file)
|
||||
.arg("--format")
|
||||
.arg("yaml")
|
||||
.output();
|
||||
|
||||
match output {
|
||||
Ok(output) if output.status.success() => {
|
||||
results.push(format!("✅ {}", file));
|
||||
}
|
||||
Ok(output) => {
|
||||
results.push(format!(
|
||||
"❌ {}: {}",
|
||||
file,
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
));
|
||||
all_valid = false;
|
||||
}
|
||||
Err(e) => {
|
||||
results.push(format!("❌ {}: Execution error: {}", file, e));
|
||||
all_valid = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let summary = if all_valid {
|
||||
format!("✅ All {} files are valid", kcl_files.len())
|
||||
} else {
|
||||
format!("❌ Errors found in some files")
|
||||
};
|
||||
|
||||
Ok(format!("{}\n\n{}", summary, results.join("\n")))
|
||||
}
|
228
src/main.rs
Normal file
228
src/main.rs
Normal file
@ -0,0 +1,228 @@
|
||||
use nu_plugin::{
|
||||
EngineInterface, EvaluatedCall, MsgPackSerializer, Plugin, PluginCommand, SimplePluginCommand,
|
||||
serve_plugin,
|
||||
};
|
||||
use nu_protocol::{Category, Example, LabeledError, Signature, SyntaxShape, Type, Value};
|
||||
|
||||
use anyhow::Result;
|
||||
mod helpers;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
use crate::helpers::{format_kcl_file, run_kcl_command, validate_kcl_project};
|
||||
|
||||
/// Nushell plugin for running, formatting, and validating KCL files using the KCL CLI.
|
||||
///
|
||||
/// This plugin provides three commands:
|
||||
/// - `kcl-run`: Execute KCL files and return their output.
|
||||
/// - `kcl-format`: Format KCL files.
|
||||
/// - `kcl-validate`: Validate all KCL files in a directory.
|
||||
///
|
||||
/// See each command struct for more details and usage examples.
|
||||
struct KclWrapperPlugin;
|
||||
|
||||
/// Implements the Nushell Plugin trait for the KCL wrapper plugin.
|
||||
impl Plugin for KclWrapperPlugin {
|
||||
fn version(&self) -> String {
|
||||
// This automatically uses the version of your package from Cargo.toml as the plugin version
|
||||
// sent to Nushell
|
||||
env!("CARGO_PKG_VERSION").into()
|
||||
}
|
||||
|
||||
fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin = Self>>> {
|
||||
vec![Box::new(KclRun), Box::new(KclFormat), Box::new(KclValidate)]
|
||||
}
|
||||
}
|
||||
|
||||
/// Command to execute KCL files using the KCL CLI.
|
||||
///
|
||||
/// # Usage
|
||||
/// ```nu
|
||||
/// kcl-run myfile.k -D foo=bar -f json
|
||||
/// ```
|
||||
///
|
||||
/// See `examples()` for more.
|
||||
struct KclRun;
|
||||
|
||||
impl SimplePluginCommand for KclRun {
|
||||
type Plugin = KclWrapperPlugin;
|
||||
|
||||
fn name(&self) -> &str {
|
||||
"kcl-run"
|
||||
}
|
||||
|
||||
fn signature(&self) -> Signature {
|
||||
Signature::build(PluginCommand::name(self))
|
||||
.input_output_type(Type::Any, Type::String)
|
||||
.required("file", SyntaxShape::Filepath, "KCL file to execute")
|
||||
.named(
|
||||
"format",
|
||||
SyntaxShape::String,
|
||||
"Output format (yaml/json)",
|
||||
Some('f'),
|
||||
)
|
||||
.named("output", SyntaxShape::Filepath, "Output file", Some('o'))
|
||||
.named(
|
||||
"define",
|
||||
SyntaxShape::String,
|
||||
"Variables to define (key=value)",
|
||||
Some('D'),
|
||||
)
|
||||
.category(Category::Experimental)
|
||||
}
|
||||
fn description(&self) -> &str {
|
||||
"Execute KCL files using the CLI wrapper"
|
||||
}
|
||||
|
||||
fn examples(&self) -> Vec<Example> {
|
||||
vec![Example {
|
||||
example: "kcl-run myfile.k -D foo=bar -f json",
|
||||
description: "Run 'myfile.k' with variable 'foo=bar' and output as JSON.",
|
||||
result: Some(Value::test_string("{\n \"foo\": \"bar\"\n}")),
|
||||
}]
|
||||
}
|
||||
|
||||
fn run(
|
||||
&self,
|
||||
_plugin: &KclWrapperPlugin,
|
||||
_engine: &EngineInterface,
|
||||
call: &EvaluatedCall,
|
||||
_input: &Value,
|
||||
) -> Result<Value, LabeledError> {
|
||||
let file_path: String = call.req(0)?;
|
||||
let format = call
|
||||
.get_flag_value("format")
|
||||
.and_then(|v| v.as_str().ok().map(|s| s.to_string()))
|
||||
.unwrap_or_else(|| "yaml".to_string());
|
||||
let output = call
|
||||
.get_flag_value("output")
|
||||
.and_then(|v| v.as_str().ok().map(|s| s.to_string()));
|
||||
let defines: Vec<String> = call
|
||||
.get_flag_value("define")
|
||||
.and_then(|v| v.as_list().ok().map(|list| list.to_vec()))
|
||||
.map(|list| {
|
||||
list.into_iter()
|
||||
.filter_map(|v| v.as_str().ok().map(|s| s.to_string()))
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
match run_kcl_command(&file_path, &format, &output, &defines) {
|
||||
Ok(result) => Ok(Value::string(result, call.head)),
|
||||
Err(e) => {
|
||||
Err(LabeledError::new("Error executing KCL").with_label(e.to_string(), call.head))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Command to format KCL files using the KCL CLI.
|
||||
///
|
||||
/// # Usage
|
||||
/// ```nu
|
||||
/// kcl-format myfile.k
|
||||
/// ```
|
||||
///
|
||||
/// See `examples()` for more.
|
||||
struct KclFormat;
|
||||
|
||||
impl SimplePluginCommand for KclFormat {
|
||||
type Plugin = KclWrapperPlugin;
|
||||
|
||||
fn name(&self) -> &str {
|
||||
"kcl-format"
|
||||
}
|
||||
|
||||
fn description(&self) -> &str {
|
||||
"Format KCL files"
|
||||
}
|
||||
fn signature(&self) -> Signature {
|
||||
Signature::build(PluginCommand::name(self))
|
||||
.input_output_type(Type::String, Type::String)
|
||||
.required("file", SyntaxShape::Filepath, "KCL file to format")
|
||||
.category(Category::Experimental)
|
||||
}
|
||||
fn run(
|
||||
&self,
|
||||
_plugin: &KclWrapperPlugin,
|
||||
_engine: &EngineInterface,
|
||||
call: &EvaluatedCall,
|
||||
_input: &Value,
|
||||
) -> Result<Value, LabeledError> {
|
||||
let file_path: String = call.req(0)?;
|
||||
|
||||
match format_kcl_file(&file_path) {
|
||||
Ok(result) => Ok(Value::string(result, call.head)),
|
||||
Err(e) => {
|
||||
Err(LabeledError::new("Error formatting KCL").with_label(e.to_string(), call.head))
|
||||
}
|
||||
}
|
||||
}
|
||||
fn examples(&self) -> Vec<Example> {
|
||||
vec![Example {
|
||||
example: "kcl-format myfile.k",
|
||||
description: "Format the KCL file 'myfile.k'.",
|
||||
result: Some(Value::test_string("✅ File formatted: myfile.k")),
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
/// Command to validate all KCL files in a directory using the KCL CLI.
|
||||
///
|
||||
/// # Usage
|
||||
/// ```nu
|
||||
/// kcl-validate ./project_dir
|
||||
/// ```
|
||||
///
|
||||
/// See `examples()` for more.
|
||||
struct KclValidate;
|
||||
|
||||
impl SimplePluginCommand for KclValidate {
|
||||
type Plugin = KclWrapperPlugin;
|
||||
fn name(&self) -> &str {
|
||||
"kcl-validate"
|
||||
}
|
||||
fn description(&self) -> &str {
|
||||
"kcl validate"
|
||||
}
|
||||
|
||||
fn signature(&self) -> Signature {
|
||||
Signature::build(PluginCommand::name(self))
|
||||
.input_output_type(Type::Any, Type::String)
|
||||
.optional("dir", SyntaxShape::Directory, "Directory to validate")
|
||||
.category(Category::Experimental)
|
||||
}
|
||||
|
||||
fn run(
|
||||
&self,
|
||||
_plugin: &KclWrapperPlugin,
|
||||
_engine: &EngineInterface,
|
||||
call: &EvaluatedCall,
|
||||
_input: &Value,
|
||||
) -> Result<Value, LabeledError> {
|
||||
let dir = call.opt::<String>(0)?.unwrap_or_else(|| ".".to_string());
|
||||
|
||||
match validate_kcl_project(&dir) {
|
||||
Ok(result) => Ok(Value::string(result, call.head)),
|
||||
Err(e) => Err(LabeledError::new("Error validating KCL project")
|
||||
.with_label(e.to_string(), call.head)),
|
||||
}
|
||||
}
|
||||
fn examples(&self) -> Vec<Example> {
|
||||
vec![Example {
|
||||
example: "kcl-validate ./project_dir",
|
||||
description: "Validate all KCL files in the directory './project_dir'.",
|
||||
result: Some(Value::test_string(
|
||||
"✅ All 3 files are valid\n\n✅ ./project_dir/main.k\n✅ ./project_dir/vars.k\n✅ ./project_dir/other.k",
|
||||
)),
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
/// Entry point for the KCL Nushell plugin.
|
||||
///
|
||||
/// This function registers the plugin and its commands with Nushell.
|
||||
fn main() {
|
||||
serve_plugin(&KclWrapperPlugin, MsgPackSerializer);
|
||||
}
|
82
src/tests.rs
Normal file
82
src/tests.rs
Normal file
@ -0,0 +1,82 @@
|
||||
/// Unit tests for KCL plugin helpers.
|
||||
///
|
||||
/// These tests check the behavior of running, formatting, and validating KCL files
|
||||
/// using the KCL CLI. All tests are skipped if the `kcl` binary is not installed.
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
// use super::*;
|
||||
use crate::helpers::{format_kcl_file, run_kcl_command, validate_kcl_project};
|
||||
use std::io::Write;
|
||||
use std::process::Command;
|
||||
use tempfile::{NamedTempFile, tempdir};
|
||||
|
||||
/// Returns true if the `kcl` CLI is installed and available in PATH.
|
||||
fn kcl_installed() -> bool {
|
||||
Command::new("kcl").arg("--version").output().is_ok()
|
||||
}
|
||||
|
||||
/// Test that running a valid KCL file with `run_kcl_command` succeeds.
|
||||
#[test]
|
||||
fn test_run_kcl_command_success() {
|
||||
if !kcl_installed() {
|
||||
return;
|
||||
}
|
||||
let mut file = NamedTempFile::new().expect("Failed to create temp KCL file");
|
||||
writeln!(file, "a = 1").expect("Failed to write KCL code to temp file");
|
||||
let path = file
|
||||
.path()
|
||||
.to_str()
|
||||
.expect("Temp file path is not valid UTF-8");
|
||||
let res = run_kcl_command(path, "yaml", &None, &[]);
|
||||
assert!(res.is_ok(), "Expected Ok, got: {:?}", res);
|
||||
let out = res.expect("run_kcl_command returned Err unexpectedly");
|
||||
assert!(out.contains("a = 1") || out.contains("✅") || out.contains("a: 1"));
|
||||
}
|
||||
|
||||
/// Test that formatting a valid KCL file with `format_kcl_file` succeeds.
|
||||
#[test]
|
||||
fn test_format_kcl_file_success() {
|
||||
if !kcl_installed() {
|
||||
return;
|
||||
}
|
||||
let mut file = NamedTempFile::new().expect("Failed to create temp KCL file");
|
||||
writeln!(file, "a = 1").expect("Failed to write KCL code to temp file");
|
||||
let path = file
|
||||
.path()
|
||||
.to_str()
|
||||
.expect("Temp file path is not valid UTF-8");
|
||||
let res = format_kcl_file(path);
|
||||
assert!(res.is_ok(), "Expected Ok, got: {:?}", res);
|
||||
let out = res.expect("format_kcl_file returned Err unexpectedly");
|
||||
assert!(out.contains("formatted"));
|
||||
}
|
||||
|
||||
/// Test that validating a directory with a valid KCL file using `validate_kcl_project` succeeds.
|
||||
#[test]
|
||||
fn test_validate_kcl_project_success() {
|
||||
if !kcl_installed() {
|
||||
return;
|
||||
}
|
||||
let dir = tempdir().expect("Failed to create temp dir");
|
||||
let file_path = dir.path().join("test.k");
|
||||
std::fs::write(&file_path, "a = 1").expect("Failed to write KCL code to temp file");
|
||||
let res = validate_kcl_project(
|
||||
dir.path()
|
||||
.to_str()
|
||||
.expect("Temp dir path is not valid UTF-8"),
|
||||
);
|
||||
assert!(res.is_ok(), "Expected Ok, got: {:?}", res);
|
||||
let out = res.expect("validate_kcl_project returned Err unexpectedly");
|
||||
assert!(out.contains("valid") || out.contains("✅"));
|
||||
}
|
||||
|
||||
/// Test that running a nonexistent KCL file with `run_kcl_command` returns an error.
|
||||
#[test]
|
||||
fn test_run_kcl_command_fail() {
|
||||
if !kcl_installed() {
|
||||
return;
|
||||
}
|
||||
let res = run_kcl_command("nonexistent.k", "yaml", &None, &[]);
|
||||
assert!(res.is_err(), "Expected Err, got: {:?}", res);
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user