automaat-server 0.1.0

HTTP API for the Automaat automation utility.
use automaat_core::{Context, Processor as CoreProcessor};
use juniper::GraphQLInputObject;
use processor_git_clone_v1::{GitClone, Input as GitCloneInput};
use processor_print_output_v1::{Input as PrintOutputInput, PrintOutput};
use processor_redis_command_v1::{Input as RedisCommandInput, RedisCommand};
use processor_shell_command_v1::{Input as ShellCommandInput, ShellCommand};
use processor_string_regex_v1::{Input as StringRegexInput, StringRegex};
use serde::{Deserialize, Serialize};
use std::convert::TryFrom;
use std::error;

// Macro to create all required processor implementations without having to
// change tens of lines for every new processor added.
//
// See the end of this file for the actual usage.
macro_rules! impl_processors {
    ($($name:ident: $processor:ident),+) => {
        #[derive(Clone, Debug, Serialize, Deserialize)]
        pub(crate) enum Processor {
            $($processor($processor)),+
        }

        impl Processor {
            pub(crate) fn run(
                self,
                context: &Context,
            ) -> Result<Option<String>, Box<dyn error::Error>> {
                match self {
                    $(Processor::$processor(p) => p.run(context).map_err(Into::into)),+
                }
            }

            pub(crate) fn validate(&self) -> Result<(), Box<dyn error::Error>> {
                match self {
                    $(Processor::$processor(p) => p.validate().map_err(Into::into)),+
                }
            }
        }

        // Dynamically construct items by combinding `$processor` and `Input` to
        // create types such as `GitClineInput`, etc...
        paste::item! {
            // NOTE: GraphQL does not support union input types, so this struct
            // with one field for each (optional) processor type is the best we
            // can do for now without giving up the typed nature of the input
            // object.
            //
            // see: https://github.com/graphql/graphql-spec/issues/488
            #[derive(Clone, Debug, Serialize, Deserialize, GraphQLInputObject)]
            #[graphql(name = "ProcessorInput")]
            pub(crate) struct Input {
                $($name: Option<[<$processor Input>]>),+
            }
        }

        // Given a GraphQL processor input object `ProcessorInput`, check if it
        // contains exactly _one_ processor configuration, and convert from that
        // processor's input type into a regular processor type.
        //
        // For example, this is not allowed and will return an error:
        //
        // ```
        // ProcessorInput {
        //     git_clone: GitCloneInput { ... },
        //     shell_command: ShellCommandInput { ... },
        // }
        // ```
        //
        // But this is:
        //
        // ```
        // ProcessorInput {
        //     git_clone: GitCloneInput { ... },
        //     shell_command: null,
        // }
        // ```
        //
        // This is done to create a strongly-typed union-like input object when
        // creating pipelines using processor configurations.
        impl TryFrom<Input> for Processor {
            type Error = String;

            fn try_from(input: Input) -> Result<Self, Self::Error> {
                let mut i = 0;
                $(i = input.$name.iter().fold(i, |i, _| i + 1));+;

                if i != 1 {
                    return Err("must provide exactly one processor input value".into());
                }

                $(if let Some(processor) = input.$name {
                    return Ok(Processor::$processor(processor.into()));
                })+

                unreachable!()
            }
        }

        juniper::graphql_union!(Processor: () where Scalar = <S> |&self| {
            instance_resolvers: |_| {
                $(&$processor => match *self {
                    Processor::$processor(ref p) => Some(p),
                    _ => None
                }),+
            }
        });
    }
}

// This creates all "v1" processor types, and exposes them over GraphQL.
//
// Version 1 processors use "naked" names, meaning they are used as `GitClone`,
// not `GitCloneV1`. If we ever have a need to do breaking changes, we'll add a
// `GitClonev2` option alongside the regular `GitClone` one, and deprecate the
// regular one.
impl_processors! {
    git_clone:     GitClone,
    print_output:  PrintOutput,
    shell_command: ShellCommand,
    redis_command: RedisCommand,
    string_regex:  StringRegex
}