cage 0.3.6

Develop multi-pod docker-compose apps
Documentation
//! Plugin support.

use compose_yml::v2 as dc;
use std::fmt;
use std::io;
use std::marker::PhantomData;

use crate::errors::*;
use crate::pod::Pod;
use crate::project::Project;
use crate::template::Template;

pub mod transform;

/// The context in which a plugin is being applied.
#[derive(Debug)]
pub struct Context<'a> {
    /// The project to which we're applying this plugin.
    pub project: &'a Project,
    /// The pod to which we're applying this plugin.
    pub pod: &'a Pod,
    /// The subcommand to which we're applying this plugin.
    pub subcommand: String,
    /// PRIVATE. Allow future extensibility without breaking the API.
    _nonexclusive: PhantomData<()>,
}

impl<'a> Context<'a> {
    /// Create a new plugin context.
    pub fn new(project: &'a Project, pod: &'a Pod, subcommand: &str) -> Context<'a> {
        Context {
            project,
            pod,
            subcommand: subcommand.to_string(),
            _nonexclusive: PhantomData,
        }
    }
}

/// What kind of transform operation are we performing?  (Adding new kinds
/// of operations will be a breaking API change for plugins.)
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Operation {
    /// We're outputting a file for our internal usage.
    Output,
    /// We're exporting a file for use by another tool.
    Export,
}

/// The "super-trait" of all our specific plugin traits.  This needs to be
/// usable as a [trait object][], so it may not contain any static "class"
/// methods or methods with type parameters.  Those can be found in
/// `PluginNew` instead.  This trait needs to be usable as a trait object
/// so that we can create a `Vec` containing multiple different plugin
/// implementations.  Trait objects are as close as Rust comes to object
/// orientation.
///
/// [trait object]: https://doc.rust-lang.org/book/trait-objects.html
pub trait Plugin: Sync {
    /// The name of this plugin (available after we create an instance).
    fn name(&self) -> &'static str;
}

/// Initialization for `Plugin`.  These methods can't be part of `Plugin`
/// itself, because they would prevent us from using `Plugin` as a [trait
/// object][].
///
/// [trait object]: https://doc.rust-lang.org/book/trait-objects.html
pub trait PluginNew: Plugin + Sized + fmt::Debug {
    /// The name of this plugin (available before we create an instance).
    fn plugin_name() -> &'static str;

    /// Has this plugin been configured for this project?  This will be
    /// called before instantiating any plugin type except
    /// `PluginGenerate`.
    fn is_configured_for(_project: &Project) -> Result<bool> {
        Ok(true)
    }

    /// Create a new plugin.
    fn new(project: &Project) -> Result<Self>;
}

/// A plugin which transforms a `dc::File` object.
pub trait PluginTransform: Plugin {
    /// Transform the specified file.
    fn transform(
        &self,
        op: Operation,
        ctx: &Context<'_>,
        file: &mut dc::File,
    ) -> Result<()>;
}

/// A plugin which can generate source code.
pub trait PluginGenerate: Plugin {
    /// A short, human-readable description of what this generator does in
    /// fewer than 60 characters (for display on monospaced terminals).
    fn generator_description(&self) -> &'static str;

    /// Generate source code.  The default implementation generates the
    /// template of the same name as the plugin, using the project as
    /// input.  This is a good default.
    fn generate(&self, project: &Project, out: &mut dyn io::Write) -> Result<()> {
        let mut proj_tmpl = Template::new(self.name())?;
        proj_tmpl.generate(project.root_dir(), project, out)?;
        Ok(())
    }
}

/// A collection of plugins, normally associated with a project.
pub struct Manager {
    /// Our `dc::File` transforming plugins.
    transforms: Vec<Box<dyn PluginTransform>>,

    /// Our code generator plugins.
    generators: Vec<Box<dyn PluginGenerate>>,
}

impl Manager {
    /// Create a new manager for the specified project.
    pub fn new(proj: &Project) -> Result<Manager> {
        let mut manager = Manager {
            transforms: vec![],
            generators: vec![],
        };
        // We instantiate some of these plugins twice, could we be more
        // clever about it?
        manager.register_generator::<transform::secrets::Plugin>(proj)?;
        manager.register_generator::<transform::vault::Plugin>(proj)?;

        manager.register_transform::<transform::abs_path::Plugin>(proj)?;
        manager.register_transform::<transform::default_tags::Plugin>(proj)?;
        manager.register_transform::<transform::host_dns::Plugin>(proj)?;
        manager.register_transform::<transform::sources::Plugin>(proj)?;
        manager.register_transform::<transform::secrets::Plugin>(proj)?;
        manager.register_transform::<transform::remove_build::Plugin>(proj)?;
        manager.register_transform::<transform::vault::Plugin>(proj)?;

        // Run this last, in case it wants to remove any labels used by
        // other plugins.
        manager.register_transform::<transform::labels::Plugin>(proj)?;

        Ok(manager)
    }

    /// Get the generators registered with this plugin manager.
    pub fn generators(&self) -> &[Box<dyn PluginGenerate>] {
        &self.generators
    }

    /// Create a new plugin, returning a reasonably helpful error if we
    /// fail.
    fn new_plugin<T>(&self, proj: &Project) -> Result<T>
    where
        T: PluginNew + 'static,
    {
        T::new(proj).chain_err(|| ErrorKind::PluginFailed(T::plugin_name().to_owned()))
    }

    /// Register a generator with this manager.
    fn register_generator<T>(&mut self, proj: &Project) -> Result<()>
    where
        T: PluginNew + PluginGenerate + 'static,
    {
        let plugin: T = self.new_plugin(proj)?;
        self.generators.push(Box::new(plugin));
        Ok(())
    }

    /// Register a transform with this manager.
    fn register_transform<T>(&mut self, proj: &Project) -> Result<()>
    where
        T: PluginNew + PluginTransform + 'static,
    {
        if T::is_configured_for(proj)? {
            let plugin: T = self.new_plugin(proj)?;
            self.transforms.push(Box::new(plugin));
        }
        Ok(())
    }

    /// A plugin was missing, so build an appropriate error message.
    fn missing_plugin(&self, name: &str) -> ErrorKind {
        if name == "vault" {
            ErrorKind::FeatureDisabled
        } else {
            unreachable!("Cannot find a generator named {}", name)
        }
    }

    /// Run the specified generator in the current project.
    pub fn generate(
        &self,
        project: &Project,
        name: &str,
        out: &mut dyn io::Write,
    ) -> Result<()> {
        let generator = self
            .generators
            .iter()
            .find(|g| g.name() == name)
            .ok_or_else(|| self.missing_plugin(name))?;
        debug!("Generating {}", generator.name());
        generator.generate(project, out)
    }

    /// Apply all our transform plugins.
    pub fn transform(
        &self,
        op: Operation,
        ctx: &Context<'_>,
        file: &mut dc::File,
    ) -> Result<()> {
        for plugin in &self.transforms {
            trace!("transforming '{}' with {}", ctx.pod.name(), plugin.name());
            plugin
                .transform(op, ctx, file)
                .chain_err(|| ErrorKind::PluginFailed(plugin.name().to_owned()))?;
        }
        Ok(())
    }
}

impl fmt::Debug for Manager {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let mut names: Vec<_> = vec![];
        names.extend_from_slice(
            &self.transforms.iter().map(|p| p.name()).collect::<Vec<_>>(),
        );
        write!(f, "plugins::Manager {{ {:?} }}", &names)
    }
}