nu_plugin_handlebars 0.3.0

An `handlebars` command for Nushell for rendering Nu values using Handlebars templates
Documentation
use std::any::TypeId;
use std::path::PathBuf;

use handlebars::Template;
use hashbrown::HashMap;
use hashbrown::hash_map::Entry;
use nu_plugin::{EngineInterface, EvaluatedCall, Plugin, PluginCommand};
use nu_protocol::{CustomValue, LabeledError, PipelineData, Signature, Spanned, Value};

use crate::custom_value::{
    CustomCollections, CustomReference, RegistryReference, TemplateReference,
};

mod compile;
mod eval;
mod helper;
mod new;
mod partial;
mod render;

type BoxedDropHandler = Box<
    dyn 'static
        + Send
        + Sync
        + Fn(&CustomCollections, &EngineInterface, &dyn CustomValue) -> Result<(), LabeledError>,
>;

pub struct HandlebarsPlugin {
    pub collections: CustomCollections,
    drop_handlers: HashMap<TypeId, BoxedDropHandler>,
}

impl Plugin for HandlebarsPlugin {
    fn version(&self) -> String {
        env!("CARGO_PKG_VERSION").to_owned()
    }

    fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin = Self>>> {
        [
            Box::new(MidNodeCommand {
                name: "handlebars",
                description: "Operate with the Handlebars template engine",
            }) as Box<dyn PluginCommand<Plugin = Self>>,
            Box::new(new::HandlebarsNewCommand),
            Box::new(helper::HandlebarsHelperCommand),
            Box::new(partial::HandlebarsPartialCommand),
            Box::new(eval::HandlebarsEvalCommand),
            Box::new(compile::HandlebarsCompileCommand),
            Box::new(render::HandlebarsRenderCommand),
        ]
        .into_iter()
        .collect()
    }

    fn custom_value_dropped(
        &self,
        engine: &EngineInterface,
        custom_value: Box<dyn CustomValue>,
    ) -> Result<(), nu_protocol::LabeledError> {
        let Some(handler) = self.drop_handlers.get(&custom_value.type_id()) else {
            return Ok(());
        };
        let result = handler(&self.collections, engine, custom_value.as_ref());
        if self.collections.is_empty() {
            engine.set_gc_disabled(false)?;
        }
        result
    }
}

impl HandlebarsPlugin {
    #[allow(clippy::new_without_default)]
    pub fn new() -> Self {
        Self {
            collections: Default::default(),
            drop_handlers: [
                Self::gen_handler(Self::handle_drop_registry),
                Self::gen_handler(Self::handle_drop_template),
            ]
            .into_iter()
            .collect(),
        }
    }

    fn gen_handler<C: CustomValue>(
        dlg: fn(&CustomCollections, &EngineInterface, &C) -> Result<(), LabeledError>,
    ) -> (TypeId, BoxedDropHandler) {
        (
            TypeId::of::<C>(),
            Box::new(move |collections, engine, custom_value| {
                let concrete_value = custom_value
                    .as_any()
                    .downcast_ref::<C>()
                    .expect("should have been of the correct type");
                dlg(collections, engine, concrete_value)
            }),
        )
    }

    fn handle_drop_registry(
        collections: &CustomCollections,
        _engine: &EngineInterface,
        registry_reference: &RegistryReference,
    ) -> Result<(), LabeledError> {
        let mut registries = collections.registries.write().unwrap();
        let Entry::Occupied(mut entry) = registries.entry(*registry_reference.uuid()) else {
            return Ok(());
        };
        let entry_mut = entry.get_mut();
        if 1 < entry_mut.refcount {
            entry_mut.refcount -= 1;
        } else {
            entry.remove();
        }
        Ok(())
    }

    fn handle_drop_template(
        collections: &CustomCollections,
        _engine: &EngineInterface,
        template_reference: &TemplateReference,
    ) -> Result<(), LabeledError> {
        let mut templates = collections.templates.write().unwrap();
        let Entry::Occupied(mut entry) = templates.entry(*template_reference.uuid()) else {
            return Ok(());
        };
        let entry_mut = entry.get_mut();
        if 1 < entry_mut.refcount {
            entry_mut.refcount -= 1;
        } else {
            if let Some(registry_reference) = &entry_mut.data.registry {
                Self::handle_drop_registry(collections, _engine, registry_reference)?;
            }

            entry.remove();
        }
        Ok(())
    }
}

struct MidNodeCommand {
    name: &'static str,
    description: &'static str,
}

impl PluginCommand for MidNodeCommand {
    type Plugin = HandlebarsPlugin;

    fn name(&self) -> &str {
        self.name
    }

    fn signature(&self) -> nu_protocol::Signature {
        Signature::build(self.name()).add_help()
    }

    fn description(&self) -> &str {
        self.description
    }

    fn run(
        &self,
        _plugin: &Self::Plugin,
        engine: &EngineInterface,
        call: &nu_plugin::EvaluatedCall,
        _input: nu_protocol::PipelineData,
    ) -> Result<nu_protocol::PipelineData, nu_protocol::LabeledError> {
        Ok(PipelineData::value(
            Value::string(engine.get_help()?, call.head),
            None,
        ))
    }
}

fn extract_reference_from_input<C: CustomValue + CustomReference>(
    input: &PipelineData,
) -> Result<Option<&C>, LabeledError> {
    match input {
        PipelineData::Empty => Ok(None),
        PipelineData::Value(
            Value::Custom {
                val: input,
                internal_span: _,
                ..
            },
            _,
        ) if let Some(reference) = input.as_any().downcast_ref::<C>() => Ok(Some(reference)),
        _ => Err(LabeledError::new(format!("Expected {}", C::NAME))
            .with_label(format!("not {}", C::NAME), input.span().unwrap_or_default())),
    }
}

pub fn compile_template_from_evaluated_call(
    engine: &EngineInterface,
    call: &EvaluatedCall,
) -> Result<Template, LabeledError> {
    Ok(
        match (
            call.get_flag::<Spanned<String>>("text")?,
            call.get_flag::<Spanned<PathBuf>>("file")?,
        ) {
            (None, None) => {
                return Err(LabeledError::new("Need either `--text` or `--file`")
                    .with_label("for this", call.head));
            }
            (Some(text), Some(file)) => {
                return Err({
                    LabeledError::new("Do not provide both `--text` and `--file`")
                        .with_label("cannot have both", text.span)
                        .with_label("cannot have both", file.span)
                });
            }
            (Some(text), None) => Template::compile(&text.item)
                .map_err(|err| LabeledError::new(err.to_string()).with_label("here", text.span))?,
            (None, Some(file)) => {
                let full_path = std::path::Path::new(&engine.get_current_dir()?).join(&file.item);
                Template::compile_with_name(
                    std::fs::read_to_string(full_path).map_err(|err| {
                        LabeledError::new(err.to_string()).with_label("this", file.span)
                    })?,
                    file.item.to_string_lossy().into_owned(),
                )
                .map_err(|err| {
                    LabeledError::new(err.to_string()).with_label("this file", file.span)
                })?
            }
        },
    )
}