nu_plugin_nuts 0.1.0

Blazingly fast Nats client as a nushell plugin
Documentation
use anyhow::Context;
use async_nats::ConnectOptions;
use nu_plugin::{EngineInterface, EvaluatedCall, PluginCommand};
use nu_protocol::{LabeledError, PipelineData, ShellError, Signature, SyntaxShape};

use crate::Nuts;

#[derive(Debug)]
pub(crate) struct Connect;

impl PluginCommand for Connect {
    type Plugin = Nuts;

    fn name(&self) -> &str {
        "nuts connect"
    }

    fn signature(&self) -> Signature {
        Signature::build(self.name())
            .optional("url", SyntaxShape::String, "Connection URL")
            .named(
                "user",
                SyntaxShape::String,
                "Username or token to authenticate with",
                None,
            )
            .named(
                "password",
                SyntaxShape::String,
                "Password to authenticate with",
                None,
            )
            .named(
                "creds",
                SyntaxShape::String,
                "Token to authenticate with",
                None,
            )
            .named(
                "nkey",
                SyntaxShape::String,
                "NKEY to authenticate with",
                None,
            )
    }

    fn description(&self) -> &str {
        "Connect to a NATS server"
    }

    fn run(
        &self,
        plugin: &Self::Plugin,
        engine: &EngineInterface,
        call: &EvaluatedCall,
        input: PipelineData,
    ) -> Result<PipelineData, LabeledError> {
        let url: String = call.opt(0)?.unwrap_or_else(|| String::from("localhost"));
        let user = get_flag_or_env_var("user", "NATS_USER", call, engine)?;
        let password = get_flag_or_env_var("password", "NATS_PASSWORD", call, engine)?;
        let credentials = get_flag_or_env_var("creds", "NATS_CREDS", call, engine)?;
        let nkey = get_flag_or_env_var("nkey", "NATS_NKEY", call, engine)?;

        let options = ConnectOptions::new();
        let options = match (user, password) {
            (None, None) => options,
            (Some(user), Some(password)) => options.user_and_password(user, password),
            (Some(_), None) => {
                return Err(LabeledError::new(
                    "Missing `--password` argument for basic authentication",
                ));
            }
            (None, Some(_)) => {
                return Err(LabeledError::new(
                    "Missing `--user` argument for basic authentication",
                ));
            }
        };
        let options = match credentials {
            Some(credentials) => options
                .credentials(&credentials)
                .map_err(|err| LabeledError::new(err.to_string()))?,
            None => options,
        };
        let options = match nkey {
            Some(nkey) => options.nkey(nkey),
            None => options,
        };
        let client = plugin
            .runtime
            .block_on(async move {
                async_nats::connect_with_options(url, options)
                    .await
                    .context("Failed to connect to NATS server")
            })
            .map_err(|error| LabeledError::new(error.to_string()))?;
        *plugin.nats.write().unwrap() = Some(client);

        Ok(input)
    }
}

fn get_flag_or_env_var(
    flag: &str,
    env_var: impl Into<String>,
    call: &EvaluatedCall,
    engine: &EngineInterface,
) -> Result<Option<String>, ShellError> {
    let value: Option<String> = match call.get_flag(flag)? {
        Some(nkey) => Some(nkey),
        None => engine
            .get_env_var(env_var)?
            .map(|value| value.coerce_into_string())
            .transpose()?,
    };
    Ok(value)
}