automaat_core/
lib.rs

1//! # Automaat
2//!
3//! Automaat can help you automate mundane and repeated tasks in a flexible way.
4//!
5//! Its goal is to provide a simplified, user-friendly, and highly-customisable
6//! interface that combines "customer support" software, job schedulers and
7//! ad-hoc shell scripts you might currently be using at your organisation.
8//!
9//! Automaat consists of several core crates:
10//!
11//! * [`automaat-core`][c] (this one) – Provides the basic building blocks for
12//!   the functionality of the other crates.
13//! * [`automaat-server`][s] – A server application, with an API to run
14//!   processors, and persistent storage.
15//! * [`automaat-web-client`][w] – A WebAssembly-based application to interact
16//!   with the server, and run processors.
17//!
18//! [c]: https://docs.rs/automaat-core
19//! [s]: https://docs.rs/automaat-server
20//! [w]: https://docs.rs/automaat-web-client
21//!
22//! There are also serveral existing processor implementations, each in their
23//! own crate:
24//!
25//! * [`automaat-processor-git-clone`][pg] – Clone any Git repository to the
26//!   processor workspace.
27//! * [`automaat-processor-shell-command`][ps] – Execute a shell command.
28//! * [`automaat-processor-redis-command`][pr] – Execute a Redis command.
29//! * [`automaat-processor-string-regex`][px] – Match (and replace) a string.
30//! * [`automaat-processor-print-output`][po] – Return a pre-defined string.
31//!
32//! Using the `automaat-server` crate, you can combine multiple processors into
33//! a single `Pipeline`, combined with a set of runtime `Variable`s, to create
34//! easy-to-use workflows to perform a specific task.
35//!
36//! [pg]: https://docs.rs/automaat-processor-shell-command
37//! [pr]: https://docs.rs/automaat-processor-redis-command
38//! [px]: https://docs.rs/automaat-processor-string-regex
39//! [po]: https://docs.rs/automaat-processor-print-output
40//!
41//! # Core
42//!
43//! This crate, `automaat-core`, provides the main [`Processor`] trait to create
44//! new processors, and run them.
45//!
46//! It also provides access to the [`Context`] object, to share state between
47//! multiple processors in a single run.
48//!
49//! If you want to write your own processor, be sure to check out the
50//! documentation of the [`Processor`] trait.
51
52#![deny(
53    clippy::all,
54    clippy::cargo,
55    clippy::nursery,
56    clippy::pedantic,
57    rust_2018_idioms,
58    warnings
59)]
60#![allow(clippy::multiple_crate_versions)]
61
62use serde::{Deserialize, Serialize};
63use std::{error, fmt, io, path};
64use tempfile::{tempdir, TempDir};
65
66/// The main trait to implement when creating a new Automaat processor.
67///
68/// Implementing the `Processor` trait makes it possible to use that processor
69/// in the `automaat-server` application.
70pub trait Processor<'de>: Clone + fmt::Debug + Serialize + Deserialize<'de> {
71    /// The human-formatted name of the processor, used to visually identify
72    /// this processor amongst others.
73    const NAME: &'static str;
74
75    /// If a processor fails its intended purpose, the returned error is turned
76    /// into a string, and shown in the `automaat-web-client` application.
77    type Error: error::Error;
78
79    /// The processor can return any (successful) output it wants, as long as
80    /// that type implements the [`std::fmt::Display`] trait.
81    ///
82    /// In the `automaat-server` application, the output is turned into a
83    /// string, and is processed as markdown.
84    ///
85    /// While not required, it's best-practice to take advantage of this fact,
86    /// to format the output in a pleasant way for users.
87    type Output: fmt::Display;
88
89    /// Actually runs the pipeline, performing whatever side-effects are defined
90    /// in this specific processor.
91    ///
92    /// The [`Context`] object can be used to access a temporary workspace
93    /// directory that is shared across all processors using the same context
94    /// object.
95    ///
96    /// # Errors
97    ///
98    /// When a processor has run to completion, it is supposed to return
99    /// whatever valuable information could be used via `Self::Output`. If an
100    /// unexpected result occurred, `Self::Error` should be returned.
101    fn run(&self, context: &Context) -> Result<Option<Self::Output>, Self::Error>;
102
103    /// The `validate` method is used by the `automaat-server` application to do
104    /// a runtime check to make sure that the processor is correctly configured
105    /// before running it.
106    ///
107    /// This is an additional validation, on top of whatever invariant is
108    /// guaranteed using the type system.
109    ///
110    /// The default implementation of this method always returns `Ok`.
111    ///
112    /// # Errors
113    ///
114    /// If validation fails, an error should be returned. The error message can
115    /// be used by clients such as `automaat-web-client` to show an informative
116    /// message to the user.
117    fn validate(&self) -> Result<(), Self::Error> {
118        Ok(())
119    }
120}
121
122/// The `Context` is an object that can be shared across multiple processor runs
123/// for any required shared state.
124///
125/// At the moment, it is used to provide a shared location on the local
126/// file system to store and retrieve data from.
127#[derive(Debug)]
128pub struct Context {
129    workspace: TempDir,
130}
131
132impl Context {
133    /// Create a new `Context` object.
134    ///
135    /// # Errors
136    ///
137    /// If the file system cannot be written to, or something else prevents the
138    /// temporary directory from being created, a [`ContextError`] enum is
139    /// returned. Specifically the `ContextError::Io` variant.
140    pub fn new() -> Result<Self, ContextError> {
141        Ok(Self {
142            workspace: tempdir()?,
143        })
144    }
145
146    /// Returns a [`std::path::Path`] reference to the shared workspace.
147    pub fn workspace_path(&self) -> &path::Path {
148        self.workspace.path()
149    }
150}
151
152/// Represents all the ways that a [`Context`] can fail.
153///
154/// This type is not intended to be exhaustively matched, and new variants may
155/// be added in the future without a major version bump.
156#[derive(Debug)]
157pub enum ContextError {
158    /// An error occurred during IO activities.
159    Io(io::Error),
160
161    #[doc(hidden)]
162    __Unknown, // Match against _ instead, more variants may be added in the future.
163}
164
165impl fmt::Display for ContextError {
166    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
167        match *self {
168            ContextError::Io(ref err) => write!(f, "IO error: {}", err),
169            ContextError::__Unknown => unreachable!(),
170        }
171    }
172}
173
174impl error::Error for ContextError {
175    fn description(&self) -> &str {
176        match *self {
177            ContextError::Io(ref err) => err.description(),
178            ContextError::__Unknown => unreachable!(),
179        }
180    }
181
182    fn cause(&self) -> Option<&dyn error::Error> {
183        match *self {
184            ContextError::Io(ref err) => Some(err),
185            ContextError::__Unknown => unreachable!(),
186        }
187    }
188}
189
190impl From<io::Error> for ContextError {
191    fn from(err: std::io::Error) -> Self {
192        ContextError::Io(err)
193    }
194}