chore: add source
This commit is contained in:
parent
af18c7929b
commit
5a4d9df16e
5 changed files with 2704 additions and 0 deletions
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…
Add table
Reference in a new issue