chore: add api_nu_plugin_kcl
This commit is contained in:
parent
4acf51fbdc
commit
7aafe5523f
2
api_nu_plugin_kcl/.gitignore
vendored
Normal file
2
api_nu_plugin_kcl/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
/target
|
||||
.DS_Store
|
6457
api_nu_plugin_kcl/Cargo.lock
generated
Normal file
6457
api_nu_plugin_kcl/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
27
api_nu_plugin_kcl/Cargo.toml
Normal file
27
api_nu_plugin_kcl/Cargo.toml
Normal file
@ -0,0 +1,27 @@
|
||||
[package]
|
||||
|
||||
name = "nu_plugin_kcl"
|
||||
version = "0.1.0"
|
||||
authors = ["Jesús Pérez <jpl@jesusperez.com>"]
|
||||
edition = "2024"
|
||||
description = "a nushell plugin called kcl"
|
||||
repository = "https://github.com/jesusperez/nu_plugin_kcl"
|
||||
license = "MIT"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[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"] }
|
||||
kcl-lang = { git = "https://github.com/kcl-lang/lib" }
|
||||
anyhow = "1.0"
|
||||
toml = "0.8"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
tempfile = "3"
|
||||
|
||||
[dev-dependencies]
|
||||
nu-plugin-test-support = { path = "../nushell/crates/nu-plugin-test-support" }
|
||||
#nu-plugin-test-support = { version = "0.104.0" }
|
21
api_nu_plugin_kcl/LICENSE
Normal file
21
api_nu_plugin_kcl/LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019 - 2022 The Nushell Project Developers
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
23
api_nu_plugin_kcl/README.md
Normal file
23
api_nu_plugin_kcl/README.md
Normal file
@ -0,0 +1,23 @@
|
||||
# nu_plugin_kcl
|
||||
|
||||
This is a [Nushell](https://nushell.sh/) plugin called "kcl".
|
||||
|
||||
## Installing
|
||||
|
||||
```nushell
|
||||
> cargo install --path .
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
FIXME: This reflects the demo functionality generated with the template. Update this documentation
|
||||
once you have implemented the actual plugin functionality.
|
||||
|
||||
```nushell
|
||||
> plugin add ~/.cargo/bin/nu_plugin_kcl
|
||||
> plugin use kcl
|
||||
> kcl-exec Ellie
|
||||
Hello, Ellie. How are you today?
|
||||
> kcl-exec --shout Ellie
|
||||
HELLO, ELLIE. HOW ARE YOU TODAY?
|
||||
```
|
433
api_nu_plugin_kcl/src/main.rs
Normal file
433
api_nu_plugin_kcl/src/main.rs
Normal file
@ -0,0 +1,433 @@
|
||||
use kcl_lang::{API, ExecProgramArgs, ExternalPkg};
|
||||
use nu_plugin::{EngineInterface, EvaluatedCall, SimplePluginCommand};
|
||||
use nu_plugin::{MsgPackSerializer, Plugin, PluginCommand, serve_plugin};
|
||||
use nu_protocol::IntoPipelineData;
|
||||
use nu_protocol::{
|
||||
Category, Example, LabeledError, PipelineData, Signature, SyntaxShape, Type, Value,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use std::collections::HashMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// Nushell plugin for executing KCL files and inline KCL code.
|
||||
///
|
||||
/// This plugin integrates [KCL](https://kcl-lang.io/) (Kusion Configuration Language) with [Nushell](https://www.nushell.sh/), allowing you to execute KCL files or inline KCL code directly from your Nushell pipeline.
|
||||
///
|
||||
/// # Provided Commands
|
||||
///
|
||||
/// - `kcl-exec`: Execute one or more KCL files and return the result as YAML. Accepts an array of file paths and an optional `--work_dir` flag to specify the working directory.
|
||||
/// - `kcl-run`: Execute inline KCL code from the pipeline and return the result as YAML.
|
||||
///
|
||||
/// # Usage
|
||||
///
|
||||
/// ## kcl-exec
|
||||
///
|
||||
/// Run multiple KCL files in the current directory:
|
||||
///
|
||||
/// ```shell
|
||||
/// kcl-exec [settings.k defs/aws_defaults.k]
|
||||
/// ```
|
||||
///
|
||||
/// Run multiple KCL files from any directory, specifying the project root:
|
||||
///
|
||||
/// ```shell
|
||||
/// kcl-exec [settings.k defs/aws_defaults.k] --work_dir /path/to/project
|
||||
/// ```
|
||||
///
|
||||
/// Run a single KCL file:
|
||||
///
|
||||
/// ```shell
|
||||
/// kcl-exec [settings.k] --work_dir /path/to/project
|
||||
/// ```
|
||||
///
|
||||
/// ## kcl-run
|
||||
///
|
||||
/// Run inline KCL code:
|
||||
///
|
||||
/// ```shell
|
||||
/// 'a = 1' | kcl-run
|
||||
/// ```
|
||||
///
|
||||
/// Run a KCL file from the pipeline:
|
||||
///
|
||||
/// ```shell
|
||||
/// open settings.k | kcl-run
|
||||
/// ```
|
||||
///
|
||||
/// # Limitations
|
||||
///
|
||||
/// - The `kcl-exec` command requires the specified files to exist and be accessible.
|
||||
/// - The `kcl-run` command expects a string of KCL code from the pipeline input.
|
||||
/// - Example-based tests that require actual files or pipeline input may not run in all test environments.
|
||||
///
|
||||
/// # Resources
|
||||
///
|
||||
/// - [KCL Language Documentation](https://kcl-lang.io/docs/)
|
||||
/// - [Nushell Plugin Development](https://www.nushell.sh/book/plugins.html)
|
||||
pub struct KclPlugin;
|
||||
|
||||
impl Plugin for KclPlugin {
|
||||
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![
|
||||
// Commands should be added here
|
||||
Box::new(KclExec),
|
||||
Box::new(KclValidate),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
/// Command to execute KCL files from a file path.
|
||||
///
|
||||
/// This command takes a path to a KCL file and executes it, returning the result as YAML.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```shell
|
||||
/// kcl-exec ./src/myfile.k
|
||||
/// ```
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// - `file`: Path to the KCL file to execute (required).
|
||||
/// - `work_dir`: Optional working directory for KCL execution.
|
||||
///
|
||||
/// # Limitations
|
||||
///
|
||||
/// The file must exist and be readable by the plugin process.
|
||||
/// - If `--work_dir` is not provided, the plugin will automatically search for `kcl.mod` upwards from the first file in the files array and use its directory as the working directory.
|
||||
pub struct KclExec;
|
||||
|
||||
impl SimplePluginCommand for KclExec {
|
||||
type Plugin = KclPlugin;
|
||||
|
||||
fn name(&self) -> &str {
|
||||
"kcl-exec"
|
||||
}
|
||||
|
||||
fn signature(&self) -> Signature {
|
||||
Signature::build(PluginCommand::name(self))
|
||||
.input_output_type(Type::Any, Type::String)
|
||||
.required(
|
||||
"files",
|
||||
SyntaxShape::List(Box::new(SyntaxShape::Filepath)),
|
||||
"KCL files to execute (array)",
|
||||
)
|
||||
.named(
|
||||
"work_dir",
|
||||
SyntaxShape::Directory,
|
||||
"Work directory",
|
||||
Some('w'),
|
||||
)
|
||||
.category(Category::Experimental)
|
||||
}
|
||||
|
||||
fn description(&self) -> &str {
|
||||
"Exec KCL files and return result in YAML"
|
||||
}
|
||||
|
||||
fn examples(&self) -> Vec<Example> {
|
||||
let examples: Vec<Example> = vec![Example {
|
||||
example: "kcl-exec [settings.k defs/aws_defaults.k] --work_dir .",
|
||||
description: "Execute multiple KCL files in the current directory",
|
||||
result: None,
|
||||
}];
|
||||
examples
|
||||
}
|
||||
|
||||
fn run(
|
||||
&self,
|
||||
_plugin: &KclPlugin,
|
||||
_engine: &EngineInterface,
|
||||
call: &EvaluatedCall,
|
||||
_input: &Value,
|
||||
) -> Result<Value, LabeledError> {
|
||||
let files: Vec<String> = call.req(0)?;
|
||||
let work_dir: Option<String> = call.get_flag("work_dir")?;
|
||||
match exec_kcl_file(&files, work_dir.as_deref()) {
|
||||
Ok(yaml) => Ok(Value::string(yaml, call.head)),
|
||||
Err(e) => Err(LabeledError::new("KCL code execution error").with_label(e, call.head)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Command to execute inline KCL code from the Nushell pipeline.
|
||||
///
|
||||
/// This command takes a string of KCL code from the pipeline and executes it, returning the result as YAML.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```shell
|
||||
/// open myfile.k | kcl-run
|
||||
/// ```
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// - `filename`: Optional virtual filename for the KCL code (default: `main.k`).
|
||||
///
|
||||
/// # Limitations
|
||||
///
|
||||
/// The input must be a string containing valid KCL code.
|
||||
struct KclValidate;
|
||||
|
||||
impl PluginCommand for KclValidate {
|
||||
type Plugin = KclPlugin;
|
||||
|
||||
fn name(&self) -> &str {
|
||||
"kcl-run"
|
||||
}
|
||||
fn description(&self) -> &str {
|
||||
"Exec KCL inline code from pipeline"
|
||||
}
|
||||
fn signature(&self) -> Signature {
|
||||
Signature::build(PluginCommand::name(self))
|
||||
.input_output_type(Type::String, Type::String)
|
||||
.optional(
|
||||
"filename",
|
||||
SyntaxShape::String,
|
||||
"Nombre virtual del archivo",
|
||||
)
|
||||
.category(Category::Experimental)
|
||||
}
|
||||
|
||||
fn run(
|
||||
&self,
|
||||
_plugin: &KclPlugin,
|
||||
_engine: &EngineInterface,
|
||||
call: &EvaluatedCall,
|
||||
input: PipelineData,
|
||||
) -> Result<PipelineData, LabeledError> {
|
||||
let filename = call
|
||||
.opt::<String>(0)?
|
||||
.unwrap_or_else(|| "main.k".to_string());
|
||||
let value = input.into_value(call.head);
|
||||
let code = match value {
|
||||
Ok(Value::String { val, .. }) => val,
|
||||
Ok(_) => {
|
||||
return Err(LabeledError::new("KCL code expected as string")
|
||||
.with_label("Use a string as KCL code", call.head));
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(LabeledError::new("Failed to extract input value")
|
||||
.with_label(e.to_string(), call.head));
|
||||
}
|
||||
};
|
||||
match exec_kcl_inline(&code, &filename) {
|
||||
Ok(yaml) => Ok(Value::string(yaml, call.head).into_pipeline_data()),
|
||||
Err(e) => Err(LabeledError::new("KCL code execution error").with_label(e, call.head)),
|
||||
}
|
||||
}
|
||||
|
||||
fn examples(&self) -> Vec<Example> {
|
||||
vec![Example {
|
||||
example: "'a = 1' | kcl-run",
|
||||
description: "Run inline KCL code from the pipeline",
|
||||
result: None,
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
serve_plugin(&KclPlugin, MsgPackSerializer);
|
||||
}
|
||||
|
||||
fn exec_kcl_file(file_paths: &[String], work_dir: Option<&str>) -> Result<String, String> {
|
||||
let work_dir = match work_dir {
|
||||
Some(dir) => dir.to_string(),
|
||||
None => {
|
||||
// Try to find kcl.mod upwards from the first file
|
||||
if let Some(first_file) = file_paths.get(0) {
|
||||
match find_kcl_mod_dir(first_file) {
|
||||
Some(dir) => dir,
|
||||
None => return Err("No kcl.mod found in parent directories of the first file, and --work_dir not provided".to_string()),
|
||||
}
|
||||
} else {
|
||||
return Err("No files provided to execute".to_string());
|
||||
}
|
||||
}
|
||||
};
|
||||
let external_pkgs = resolve_dependencies_toml(&work_dir)?;
|
||||
// Debug: mostrar dependencias resueltas
|
||||
for pkg in &external_pkgs {
|
||||
println!("Dependencies: {} -> {}", pkg.pkg_name, pkg.pkg_path);
|
||||
println!("Existe: {}", Path::new(&pkg.pkg_path).exists());
|
||||
}
|
||||
let api = API::default();
|
||||
let args = ExecProgramArgs {
|
||||
k_filename_list: file_paths.to_vec(),
|
||||
work_dir,
|
||||
..Default::default()
|
||||
};
|
||||
match api.exec_program(&args) {
|
||||
Ok(result) => Ok(result.yaml_result),
|
||||
Err(e) => Err(e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Search upwards from the given file for a kcl.mod file and return its directory as a String.
|
||||
fn find_kcl_mod_dir<P: AsRef<Path>>(start_file: P) -> Option<String> {
|
||||
let mut dir = PathBuf::from(start_file.as_ref()).parent()?.to_path_buf();
|
||||
loop {
|
||||
let kcl_mod = dir.join("kcl.mod");
|
||||
if kcl_mod.exists() {
|
||||
return Some(dir.to_string_lossy().to_string());
|
||||
}
|
||||
if !dir.pop() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn exec_kcl_inline(code: &str, filename: &str) -> Result<String, String> {
|
||||
let api = API::default();
|
||||
let args = ExecProgramArgs {
|
||||
k_filename_list: vec![filename.to_string()],
|
||||
k_code_list: vec![code.to_string()],
|
||||
..Default::default()
|
||||
};
|
||||
match api.exec_program(&args) {
|
||||
Ok(result) => Ok(result.yaml_result),
|
||||
Err(e) => Err(e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Deserialize)]
|
||||
struct KclMod {
|
||||
package: Package,
|
||||
dependencies: Option<HashMap<String, Dependency>>,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Deserialize)]
|
||||
struct Package {
|
||||
name: String,
|
||||
edition: String,
|
||||
version: String,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Deserialize)]
|
||||
struct Dependency {
|
||||
path: Option<String>,
|
||||
version: String,
|
||||
}
|
||||
|
||||
fn resolve_dependencies_toml(work_dir: &str) -> Result<Vec<ExternalPkg>, String> {
|
||||
let kcl_mod_path = Path::new(work_dir).join("kcl.mod");
|
||||
|
||||
if !kcl_mod_path.exists() {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
|
||||
let content = std::fs::read_to_string(&kcl_mod_path)
|
||||
.map_err(|e| format!("Error reading kcl.mod: {}", e))?;
|
||||
|
||||
let kcl_mod: KclMod =
|
||||
toml::from_str(&content).map_err(|e| format!("Error parsing kcl.mod: {}", e))?;
|
||||
|
||||
let mut external_pkgs = Vec::new();
|
||||
|
||||
if let Some(dependencies) = kcl_mod.dependencies {
|
||||
for (pkg_name, dep) in dependencies {
|
||||
if let Some(relative_path) = dep.path {
|
||||
let absolute_path = Path::new(work_dir)
|
||||
.join(relative_path)
|
||||
.canonicalize()
|
||||
.map_err(|e| format!("Error resolving path for {}: {}", pkg_name, e))?
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
|
||||
external_pkgs.push(ExternalPkg {
|
||||
pkg_name,
|
||||
pkg_path: absolute_path,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
dbg!(&external_pkgs);
|
||||
|
||||
Ok(external_pkgs)
|
||||
}
|
||||
|
||||
/// Test all examples for the `kcl-exec` command.
|
||||
///
|
||||
/// This test runs the examples provided in the `examples` method of `KclExec`.
|
||||
/// Note: Examples that require actual files may not pass unless the files exist.
|
||||
#[test]
|
||||
fn test_examples() -> Result<(), nu_protocol::ShellError> {
|
||||
use nu_plugin_test_support::PluginTest;
|
||||
|
||||
PluginTest::new("kcl", KclPlugin.into())?.test_command_examples(&KclExec)
|
||||
}
|
||||
/// Test all examples for the `kcl-run` command.
|
||||
///
|
||||
/// This test runs the examples provided in the `examples` method of `KclValidate`.
|
||||
/// Note: Examples that require pipeline input may not be fully testable in all environments.
|
||||
#[test]
|
||||
fn test_kcl_validate_examples() -> Result<(), nu_protocol::ShellError> {
|
||||
use nu_plugin_test_support::PluginTest;
|
||||
|
||||
PluginTest::new("kcl", KclPlugin.into())?.test_command_examples(&KclValidate)
|
||||
}
|
||||
|
||||
/// Entry point for the KCL Nushell plugin.
|
||||
///
|
||||
/// This function starts the plugin and serves it to Nushell using message pack serialization.
|
||||
|
||||
#[cfg(test)]
|
||||
mod unit_tests {
|
||||
use super::*;
|
||||
use std::fs;
|
||||
use std::io::Write;
|
||||
extern crate tempfile;
|
||||
use tempfile::tempdir;
|
||||
|
||||
#[test]
|
||||
fn test_exec_kcl_file_nonexistent() {
|
||||
let result = exec_kcl_file(&vec!["/tmp/does_not_exist.k".to_string()], None);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_exec_kcl_inline_invalid_code() {
|
||||
let result = exec_kcl_inline("not valid kcl", "main.k");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resolve_dependencies_toml_success() {
|
||||
let dir = tempdir().unwrap();
|
||||
let dep_dir = dir.path().join("dep");
|
||||
fs::create_dir_all(&dep_dir).unwrap();
|
||||
let kcl_mod_content = r#"
|
||||
[package]
|
||||
name = "testpkg"
|
||||
edition = "v0.9.0"
|
||||
version = "0.1.0"
|
||||
|
||||
[dependencies]
|
||||
foo = { path = "dep" , version = "0.1.0" }
|
||||
"#;
|
||||
let kcl_mod_path = dir.path().join("kcl.mod");
|
||||
let mut file = fs::File::create(&kcl_mod_path).unwrap();
|
||||
file.write_all(kcl_mod_content.as_bytes()).unwrap();
|
||||
let pkgs = resolve_dependencies_toml(dir.path().to_str().unwrap()).unwrap();
|
||||
assert_eq!(pkgs.len(), 1);
|
||||
assert_eq!(pkgs[0].pkg_name, "foo");
|
||||
assert!(pkgs[0].pkg_path.ends_with("dep"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resolve_dependencies_toml_no_kcl_mod() {
|
||||
let dir = tempdir().unwrap();
|
||||
let pkgs = resolve_dependencies_toml(dir.path().to_str().unwrap()).unwrap();
|
||||
assert!(pkgs.is_empty());
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user