2025-06-27 02:12:44 +01:00

182 lines
7.5 KiB
Rust

use nu_plugin::{
EngineInterface, EvaluatedCall, MsgPackSerializer, Plugin, PluginCommand, SimplePluginCommand,
serve_plugin,
};
use nu_protocol::{Category, Example, LabeledError, Signature, SyntaxShape, Type, Value};
use std::fs;
use tera::Tera;
mod helpers;
use crate::helpers::{unwrap_value_key, value_to_serde_json, wrap_top_level_if_needed};
#[cfg(test)]
mod tests;
/// Nushell plugin for rendering Tera templates with structured data.
pub struct TeraPlugin;
impl Plugin for TeraPlugin {
/// Returns the plugin version from Cargo.toml.
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()
}
/// Returns the list of commands provided by this plugin.
fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin = Self>>> {
vec![
// Commands should be added here
Box::new(Render),
]
}
}
/// The main render command for the Tera plugin.
pub struct Render;
impl SimplePluginCommand for Render {
type Plugin = TeraPlugin;
/// The name of the command as used in Nushell.
fn name(&self) -> &str {
"tera-render"
}
/// The Nushell signature for the command, describing its parameters and usage.
fn signature(&self) -> Signature {
Signature::build(PluginCommand::name(self))
.input_output_type(Type::Any, Type::String)
.required("template", SyntaxShape::Filepath, "Ruta al archivo .tera")
// .switch("shout", "(FIXME) Yell it instead", None)
.optional(
"context",
SyntaxShape::Any,
"Datos de contexto (record o JSON path)",
)
.category(Category::Experimental)
}
/// A short description of the command for Nushell help.
fn description(&self) -> &str {
"(FIXME) help text for render"
}
/// Example usages of the command for Nushell help and testing.
fn examples(&self) -> Vec<Example> {
vec![Example {
example: "{ name: 'Akasha', projects: [ {'name': 'TheProject' , 'status': 'active' }] } | tera-render example/template.tera",
description: "Render template.tera with a record as context from the pipeline.\n\n\
template.tera:\n\
Hello, {{ name }}!Projects:\n\
{% for project in projects -%}\n\
- {{ project.name }} ({{ project.status }})\n\
{% endfor %}\n\n\
Other options:\n\
open data.json | wrap value | tera-render template.tera\n\
open data.json | tera-render template.tera\n\
",
result: Some(Value::test_string(
"Hello, Akasha!\nProjects:\n- TheProject (active)\n\n",
)),
}]
}
/// The main entry point for the command. Handles reading the template, context, and rendering.
fn run(
&self,
_plugin: &TeraPlugin,
_engine: &EngineInterface,
call: &EvaluatedCall,
input: &Value,
) -> Result<Value, LabeledError> {
let template_path: String = call.req(0)?;
let context_arg: Option<Value> = call.opt(1)?;
// if call.has_flag("shout")? {
// Read template
let template_content = fs::read_to_string(&template_path)
.map_err(|e| LabeledError::new("Read error").with_label(e.to_string(), call.head))?;
// Get data context (input pipeline or argument)
let context = match context_arg {
Some(val) => {
if let Value::String { val: ref s, .. } = val {
if s.ends_with(".json") {
let file_content = std::fs::read_to_string(s).map_err(|e| {
LabeledError::new("Failed to read JSON file")
.with_label(e.to_string(), val.span())
})?;
let json: serde_json::Value =
serde_json::from_str(&file_content).map_err(|e| {
LabeledError::new("Failed to parse JSON file")
.with_label(e.to_string(), val.span())
})?;
let context_json = unwrap_value_key(wrap_top_level_if_needed(json));
// println!("DEBUG context: {}", context_json);
let mut tera = Tera::default();
tera.add_raw_template(&template_path, &template_content)
.map_err(|e| {
LabeledError::new("Template error")
.with_label(e.to_string(), call.head)
})?;
let context = tera::Context::from_serialize(context_json).map_err(|e| {
LabeledError::new("Tera context error")
.with_label(e.to_string(), val.span())
})?;
let output = tera.render(&template_path, &context).map_err(|e| {
LabeledError::new("Render error").with_label(e.to_string(), call.head)
})?;
return Ok(Value::string(output, call.head));
} else if s.ends_with(".yaml")
|| s.ends_with(".yml")
|| s.ends_with(".toml")
|| s.ends_with(".csv")
|| std::path::Path::new(s).exists()
{
return Err(LabeledError::new("Context is a file path, not data")
.with_label(
format!("You passed a file path ('{}') as context. Use 'open' to read the file: open {} | tera-render ...", s, s),
val.span()
));
}
}
// Default context handling if not a file path string
let context_json =
unwrap_value_key(wrap_top_level_if_needed(value_to_serde_json(val.clone())?));
// println!("DEBUG context: {}", context_json);
tera::Context::from_serialize(context_json).map_err(|e| {
LabeledError::new("Tera context error").with_label(e.to_string(), val.span())
})?
}
None => {
let context_json = unwrap_value_key(wrap_top_level_if_needed(value_to_serde_json(
input.clone(),
)?));
//println!("DEBUG context: {}", context_json);
tera::Context::from_serialize(context_json).map_err(|e| {
LabeledError::new("Tera context error").with_label(e.to_string(), input.span())
})?
}
};
// Render with Tera
let mut tera = Tera::default();
tera.add_raw_template(&template_path, &template_content)
.map_err(|e| {
LabeledError::new("Template error").with_label(e.to_string(), call.head)
})?;
let output = tera
.render(&template_path, &context)
.map_err(|e| LabeledError::new("Render error").with_label(e.to_string(), call.head))?;
Ok(Value::string(output, call.head))
}
}
/// Entry point for the plugin binary.
fn main() {
serve_plugin(&TeraPlugin, MsgPackSerializer);
}