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}