├── domain ├── src │ ├── lib.rs │ └── executor │ │ ├── service │ │ ├── mod.rs │ │ └── task_execution.rs │ │ ├── model │ │ ├── mod.rs │ │ ├── error.rs │ │ └── model.rs │ │ ├── ports │ │ ├── mod.rs │ │ ├── primary.rs │ │ └── secondary.rs │ │ └── mod.rs └── Cargo.toml ├── infra ├── src │ ├── secondary │ │ ├── mod.rs │ │ └── adapter │ │ │ ├── mod.rs │ │ │ ├── storage │ │ │ ├── database │ │ │ │ ├── schema.rs │ │ │ │ ├── mod.rs │ │ │ │ └── commands.rs │ │ │ ├── mod.rs │ │ │ └── memory.rs │ │ │ ├── id_generator.rs │ │ │ └── execution.rs │ ├── primary │ │ ├── mod.rs │ │ ├── settings.rs │ │ └── cli.rs │ └── main.rs └── Cargo.toml ├── migrations └── 20200329_init_db │ ├── down.sql │ └── up.sql ├── Cargo.toml ├── settings.toml ├── .gitignore ├── doc ├── domain_schema.png ├── infra_schema.png ├── domain_schema.drawio └── infra_schema.drawio ├── LICENCE ├── README.md └── Cargo.lock /domain/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod executor; -------------------------------------------------------------------------------- /infra/src/secondary/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod adapter; -------------------------------------------------------------------------------- /migrations/20200329_init_db/down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE tasks -------------------------------------------------------------------------------- /domain/src/executor/service/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod task_execution; -------------------------------------------------------------------------------- /infra/src/primary/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod cli; 2 | pub mod settings; -------------------------------------------------------------------------------- /domain/src/executor/model/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod model; 2 | pub mod error; -------------------------------------------------------------------------------- /domain/src/executor/ports/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod primary; 2 | pub mod secondary; -------------------------------------------------------------------------------- /domain/src/executor/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod model; 2 | pub mod ports; 3 | pub mod service; -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "domain", 4 | "infra" 5 | ] 6 | -------------------------------------------------------------------------------- /settings.toml: -------------------------------------------------------------------------------- 1 | storage = "database" # "inmemory" 2 | 3 | [database] 4 | url = "test.db" -------------------------------------------------------------------------------- /infra/src/secondary/adapter/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod storage; 2 | pub mod execution; 3 | pub mod id_generator; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | 4 | #Development 5 | test.db 6 | 7 | # IntelliJ 8 | .idea 9 | *.iml -------------------------------------------------------------------------------- /doc/domain_schema.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fteychene/blueprint-hexagonal-rust/HEAD/doc/domain_schema.png -------------------------------------------------------------------------------- /doc/infra_schema.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fteychene/blueprint-hexagonal-rust/HEAD/doc/infra_schema.png -------------------------------------------------------------------------------- /migrations/20200329_init_db/up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE tasks ( 2 | id VARCHAR NOT NULL PRIMARY KEY, 3 | name VARCHAR, 4 | command VARCHAR NOT NULL, 5 | env VARCHAR, 6 | status VARCHAR NOT NULL, 7 | status_log VARCHAR 8 | ) -------------------------------------------------------------------------------- /infra/src/secondary/adapter/storage/database/schema.rs: -------------------------------------------------------------------------------- 1 | table! { 2 | tasks (id) { 3 | id -> Text, 4 | name -> Nullable, 5 | command -> Text, 6 | env -> Nullable, 7 | status -> Text, 8 | status_log -> Nullable, 9 | } 10 | } -------------------------------------------------------------------------------- /domain/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "blueprint-hexagonal-domain" 3 | version = "0.1.0" 4 | authors = ["fteychene "] 5 | license = "MIT OR Apache-2.0" 6 | edition = "2018" 7 | 8 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 9 | 10 | [dependencies] 11 | im = "14.3" 12 | anyhow = "1.0" 13 | thiserror = "1.0" 14 | mockall = "0.7" 15 | mockall_derive = "0.7" -------------------------------------------------------------------------------- /infra/src/secondary/adapter/id_generator.rs: -------------------------------------------------------------------------------- 1 | use domain::executor::ports::secondary::IdGeneratorPort; 2 | use uuid::Uuid; 3 | 4 | pub struct UUIDGeneratorAdapter; 5 | 6 | impl IdGeneratorPort for UUIDGeneratorAdapter { 7 | fn generate_id(&self) -> String { 8 | Uuid::new_v4().to_string() 9 | } 10 | } 11 | 12 | impl UUIDGeneratorAdapter { 13 | pub fn new() -> UUIDGeneratorAdapter { 14 | UUIDGeneratorAdapter {} 15 | } 16 | } -------------------------------------------------------------------------------- /domain/src/executor/ports/primary.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Error; 2 | use im::HashMap; 3 | 4 | use crate::executor::model::model::{TaskId, TaskStatus}; 5 | 6 | pub trait TaskSchedulerPort { 7 | fn schedule_task(&mut self, input_task: T) -> Result 8 | where T: Into; 9 | 10 | fn task_status(&mut self, id: T) -> Result 11 | where T: Into ; 12 | } 13 | 14 | pub struct TaskInput { 15 | pub name: Option, 16 | pub command: String, 17 | pub env: Option>, 18 | } -------------------------------------------------------------------------------- /domain/src/executor/model/error.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | #[derive(Error, Debug)] 4 | pub enum TaskError { 5 | #[error("Error running the command. Logs : \n {0}")] 6 | CommandError(String), 7 | #[error("Error executing the command")] 8 | ExecutionError { 9 | source: anyhow::Error 10 | }, 11 | #[error("Unexpected error while processing the command")] 12 | UnexpectedError { 13 | source: Box 14 | } 15 | } 16 | 17 | 18 | unsafe impl Sync for TaskError { 19 | 20 | } 21 | 22 | unsafe impl Send for TaskError { 23 | 24 | } -------------------------------------------------------------------------------- /infra/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "blueprint-hexagonal-infra" 3 | version = "0.1.0" 4 | authors = ["fteychene "] 5 | license = "MIT OR Apache-2.0" 6 | edition = "2018" 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | blueprint-hexagonal-domain = { version = "0.1", path = "../domain"} 11 | im = "14.3" 12 | anyhow = "1.0" 13 | uuid = {version = "0.8", features = ["v4"] } 14 | diesel = { version = "1.4", features = ["sqlite", "r2d2"] } 15 | diesel_migrations = "1.4" 16 | structopt = "0.3" 17 | itertools = "0.10" 18 | config = "0.10" 19 | serde = "1.0" -------------------------------------------------------------------------------- /domain/src/executor/ports/secondary.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Error; 2 | use mockall::*; 3 | 4 | use crate::executor::model::model::{Task, TaskId, TaskStatus}; 5 | 6 | #[automock] 7 | pub trait TaskStoragePort { 8 | fn save(&mut self, task: Task) -> Result; 9 | 10 | fn status(&mut self, id: TaskId) -> Result; 11 | 12 | fn complete(&mut self, task: &Task, status: TaskStatus) -> Result<(), Error>; 13 | 14 | } 15 | 16 | #[automock] 17 | pub trait TaskExecutionPort { 18 | fn execute(&self, task: &Task) -> Result; 19 | } 20 | 21 | #[automock] 22 | pub trait IdGeneratorPort { 23 | fn generate_id(&self) -> String; 24 | } -------------------------------------------------------------------------------- /domain/src/executor/model/model.rs: -------------------------------------------------------------------------------- 1 | use im::HashMap; 2 | 3 | #[derive(Clone, Debug, PartialEq)] 4 | pub struct Task { 5 | pub id: String, 6 | pub name: Option, 7 | pub command: String, 8 | pub env: Option>, 9 | } 10 | 11 | #[derive(Clone, Debug)] 12 | pub enum TaskStatus { 13 | Scheduled, 14 | Success(String), 15 | Error(String), 16 | } 17 | 18 | #[derive(Clone, Debug, PartialEq)] 19 | pub enum TaskId { 20 | Id(String), 21 | Name(String) 22 | } 23 | 24 | impl From<&Task> for TaskId { 25 | fn from(task: &Task) -> Self { 26 | if let Some(ref name) = task.name { 27 | TaskId::Name(name.clone()) 28 | } else { 29 | TaskId::Id(task.id.clone()) 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /infra/src/secondary/adapter/storage/mod.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Error; 2 | 3 | use domain::executor::ports::secondary::TaskStoragePort; 4 | 5 | use crate::secondary::adapter::storage::database::SqliteStorageAdapter; 6 | use crate::secondary::adapter::storage::memory::InMemoryStorageAdapter; 7 | use crate::primary::settings::StorageConfiguration; 8 | 9 | pub mod database; 10 | pub mod memory; 11 | 12 | pub fn new_storage_adapter(storage_type: StorageConfiguration) -> Result, Error> { 13 | match storage_type { 14 | StorageConfiguration::Database { database_url } => { 15 | // Result => Result, Error> == Result, Error> 16 | // Why does it ot work with this code, check type at compile 17 | // SqliteStorageAdapter::new(&database_url).map(|adapter| Box::new(adapter)) 18 | Ok(Box::new(SqliteStorageAdapter::new(&database_url)?)) 19 | 20 | }, 21 | StorageConfiguration::InMemory => Ok(Box::new(InMemoryStorageAdapter::new())) 22 | } 23 | } -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 xyz.fteychene 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. -------------------------------------------------------------------------------- /doc/domain_schema.drawio: -------------------------------------------------------------------------------- 1 | 5VnbbqMwEP2aPEYKEAh9LOllV9tKrbLSSvvmgANWDabGufXrdxxs7tk2WdJs26fg4/HYnDNjj8PAmsabW47S6J4FmA7MUbAZWFcD0zRHjgk/EtnmiDFy3RwJOQkUVgIz8oK1oUKXJMBZzVAwRgVJ66DPkgT7ooYhztm6brZgtD5rikLcAmY+om30FwlElKOuOSnxb5iEkZ7ZcC7ynhhpY/UmWYQCtq5A1vXAmnLGRP4Ub6aYSvY0L/m4mz29xcI4TsRbBvje/HGx/vH82330vfsbbL8Ms6Hh5G5WiC7VG6vViq2mACfBpWQSWglLAPQClEVYujWgEYmYqsd8KA5alJZrNIo3h5jBLMaCb8FkXXJrK76iCq0a45giQVZ190hJHBbuihkeGIGJzZGKR0troaLRsiZ1Fxlbch+rUVUuX3FkNPwIxEMsWn7gofLWJbRT6hDVOkRzqJBiQJcTyqfrDfaXgnGZfCxGJNEmMGNppcFUA1fKdpT5EY5RZVBajmkEiMAbUQ+ETHD2hKeMwvxF1CwIpQ0oS5FPkhAAu2z9ZCkAQ6DY8tYREXgGuJxqDXsMYGyF+YLu4jEiQYATwDhbJsEuJEfFCsEMVnZoJOoBDYFVsxKnRWJXA3Uy2h+TtSg4WPIuzV9N1I+cm6OecnP8ttwE5tC2YpZKg2z/eo2L5hYwaYice+w38c1PHgV2X1HQdHSiKHDsc0SB9cmj4LUUPnEQ9KbT+Ivp1Fu2thydWim7rdTUHFx6giMi5KNXlEpz3iye5hr4ibKnGdRNwZJi/sC4qJRP873lU0vwfytcJnUqXcVkJSKM8V9Cov/KZdKRBTktC7YLgpIL53nJdMcw210GL8HAsNNN2amJfOAkRnKBBce5v96rVERJCEXmlQ8SYMA9KRCBO+Kl6oihDJXTdJar9dJULnGmFmX2ILe5L3Mqersdco9PJrd7zKb3UW+UrY3r2Btl09E73yj17L0n6Qz7LAm+fJruPeCq27Lxnnlqdlwojz3yBOMoxGc68GyrTq1z7gNPb3g9MJv/iUNYci5u3cauZJ6d3I67zxHkfg9ucYI5gsj9T8LWtM5ObdfJfeAZMO4+A/iKwF77oU6AHiS2XKcmsd1W2HbaAlsnE7h9H4VNJipuLC0lsjWJKcrZrhyIkh4/IjS4Q1u2lKvOBPKfdMuLGCcvYI9K/RAX6tuO6XRrVw6aSWdqGo5lcD1oyo0GdI82NcM7lAm9QEYpSjMy3y1ZDoTLQkgSjwnB4h5Fbv4D1KFydx6bB8sMzfJ7UV76lZ/drOs/ -------------------------------------------------------------------------------- /infra/src/primary/settings.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Error, Context}; 2 | 3 | use config::Config; 4 | use std::convert::TryFrom; 5 | 6 | 7 | #[derive(Debug)] 8 | pub enum StorageConfiguration { 9 | Database { 10 | database_url: String 11 | }, 12 | InMemory, 13 | } 14 | 15 | #[derive(Debug)] 16 | pub struct Configuration { 17 | pub storage: StorageConfiguration 18 | } 19 | 20 | pub fn load_settings() -> Result { 21 | let mut settings = Config::default(); 22 | settings 23 | .merge(config::File::with_name("settings"))? 24 | .merge(config::Environment::default().separator("_"))?; 25 | 26 | Configuration::try_from(settings) 27 | } 28 | 29 | impl TryFrom for Configuration { 30 | type Error = Error; 31 | 32 | fn try_from(value: Config) -> Result { 33 | StorageConfiguration::try_from(value) 34 | .map(|storage| Configuration { storage }) 35 | .context("Error loading settings") 36 | } 37 | } 38 | 39 | impl TryFrom for StorageConfiguration { 40 | type Error = Error; 41 | 42 | fn try_from(value: Config) -> Result { 43 | match value.get_str("storage")?.as_str() { 44 | "database" => Ok(StorageConfiguration::Database { database_url: value.get_str("DATABASE_URL").or(value.get_str("database.url"))? }), 45 | "inmemory" => Ok(StorageConfiguration::InMemory), 46 | other => Err(anyhow!("{} is not a valid configuration for storage", other)) 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /infra/src/primary/cli.rs: -------------------------------------------------------------------------------- 1 | use domain::executor::ports::primary::TaskInput; 2 | use domain::executor::model::model::TaskId; 3 | use structopt::StructOpt; 4 | 5 | #[derive(StructOpt, Debug, Clone)] 6 | pub struct TaskRunOpt { 7 | /// Command to be executed by the task 8 | #[structopt(required = true)] 9 | command: Vec, 10 | /// Name of the task for later querying 11 | #[structopt(short, long)] 12 | name: Option, 13 | /// Wait the execution of the task and print status 14 | #[structopt(short, long)] 15 | pub wait: bool, 16 | } 17 | 18 | #[derive(Debug, StructOpt)] 19 | pub enum TaskStatusOpt { 20 | Id { 21 | #[structopt(required = true)] 22 | id: String 23 | }, 24 | Name { 25 | #[structopt(required = true)] 26 | name: String 27 | }, 28 | } 29 | 30 | #[derive(Debug, StructOpt)] 31 | #[structopt(name = "tasc")] 32 | pub enum CliOpt { 33 | #[structopt(name = "run")] 34 | Run(TaskRunOpt), 35 | #[structopt(name = "status")] 36 | Status(TaskStatusOpt), 37 | } 38 | 39 | 40 | pub fn parse_cli_opts() -> CliOpt { 41 | CliOpt::from_args() 42 | } 43 | 44 | impl Into for TaskRunOpt { 45 | fn into(self) -> TaskInput { 46 | TaskInput { 47 | command: self.command.join(" "), 48 | name: self.name, 49 | env: None, 50 | } 51 | } 52 | } 53 | 54 | impl Into for TaskStatusOpt { 55 | fn into(self) -> TaskId { 56 | match self { 57 | TaskStatusOpt::Id { id } => TaskId::Id(id), 58 | TaskStatusOpt::Name { name } => TaskId::Name(name) 59 | } 60 | } 61 | } -------------------------------------------------------------------------------- /infra/src/secondary/adapter/storage/database/mod.rs: -------------------------------------------------------------------------------- 1 | use diesel::SqliteConnection; 2 | use anyhow::Error; 3 | use domain::executor::model::model::{Task, TaskId, TaskStatus}; 4 | use domain::executor::ports::secondary::TaskStoragePort; 5 | 6 | mod schema; 7 | mod commands; 8 | 9 | embed_migrations!("../migrations"); 10 | 11 | pub struct SqliteStorageAdapter { 12 | connection: SqliteConnection 13 | } 14 | 15 | impl TaskStoragePort for SqliteStorageAdapter { 16 | fn save(&mut self, task: Task) -> Result { 17 | commands::create_task(&self.connection, &task) 18 | .map(|_| task) 19 | } 20 | 21 | fn status(&mut self, id: TaskId) -> Result { 22 | commands::get_task(&self.connection, &id) 23 | } 24 | 25 | fn complete(&mut self, task: &Task, status: TaskStatus) -> Result<(), Error> { 26 | match status { 27 | TaskStatus::Scheduled => commands::update_task(&self.connection, task.id.as_str(), commands::SCHEDULED, None), 28 | TaskStatus::Success(stdout) => commands::update_task(&self.connection, task.id.as_str(), commands::SUCCESS, Some(stdout.as_str())), 29 | TaskStatus::Error(stderr) => commands::update_task(&self.connection, task.id.as_str(), commands::ERROR, Some(stderr.as_str())), 30 | } 31 | } 32 | } 33 | 34 | impl SqliteStorageAdapter { 35 | pub fn new(database_url: &str) -> Result { 36 | let database_connection = commands::establish_connection(database_url)?; 37 | embedded_migrations::run_with_output(&database_connection, &mut std::io::stdout())?; 38 | Ok(SqliteStorageAdapter { 39 | connection: database_connection 40 | }) 41 | } 42 | } -------------------------------------------------------------------------------- /doc/infra_schema.drawio: -------------------------------------------------------------------------------- 1 | 7Vpdk6I4FP01Pk6XSfjysdXeHrd6aqfKntrpx7REoQYJG2Kr++s3CAESsLEdENvaJ8k1H+Tck3tPEgZost49Mhx536hLggEcursBmg4ghEMLip/Esk8tYOg4qWXFfDezFYa5/y+RFTPrxndJrFTklAbcj1TjgoYhWXDFhhmjW7XakgbqqBFekYphvsBB1fq373IvtTrQLuxfib/y5MjAGqX/rLGsnM0k9rBLtyUTehigCaOUp0/r3YQECXoSl7TdH0f+zV+MkZCf0uAlnH390548/Xz+65+55S4ncWx9ydzzhoNNNuEBtALR39gTA1ir5OlhRxYbTlniRrrGfiiriLGKWtIYScMsXDKczHvhkTUutYmKJgdc+F6CzcnuMDRfB8IAxGPMGf1FJjQQw6NpSENRc7z0g0AzxRFe+OFKGMyi9EwjYfgicEHjredzMhf2ZKitIKuw0TfClsHBJ57vuiQUNkY3oUsSyIb5G5YxzmAXLcXLlkwZ5o+Erglne1FlpxJ5rxa3BZlyhnglItmZDWf8XeUdFy4WD5mXP+BxVONxzQ0kdO+TpVPAW/LIUUiIq6ykKiClKZs1M5Y2RgLM/Td1/dXBkI3wnfriTXK8kaMCjkxL7SKmG7YgWavyemnoCACodsQxWxFe6UhAh/elalFSIT7+wnCkjgMNS3Ny2mPh8hzT81lg3DgLTNgSC/SOumKBBXUWwO5ZYN44CxqXcNcsaMtR1s07Ct5Bs60V29xXx+6yq+6awMH9mDPs8+RxnMulV6YLqFdpeMbxr7kQT+4mIOw7ZbykoV6PaqhTvH66eskdLIkv5W2JF8B4hxit6xcA2gNXiFqh33uC1rA1aGHv0NbsBs6ENt0z+DTsC1wt8kOzd3BrhPcZ4M7cRxIShgV1e4IWOaM7Lbyi6qYGQFPWugy8N58iNQliyHDx4X2N3pHmkpYELdBXoKEcT3QjaEFN5r0pFpijllhQ6agbFtioDxY4XbEgRfedoaFzVXRpLWg08q5jRQ1GN76wm6Jyx+u6LT/BuoMlXcKE38iaJmDoaicT5PcujjhhfchGLVwZNarRrBU2Os6tCRtYd0ijITrFHL/imFwhopahIgokQ/qEtE4rapA+0QUOqnjm+xoN0apivyDG5sg8RZFfGuW6kK2hPHmaiRqzMNrwq0BSv61xatY/qEkCnYGIak47mvOei2PvcIcF1CRIdj7/KZ6Hd7aZFV9Kf00TFIaysJeFUEwkbWXK4kv5v6LZoSTbHXVHs4jKUE9z1TsV7etK4VrqNdGZOdzQYmZ+mHGhJI5gq6Rrmz+NtADXJe1M+bGAFlQ+SgvLrhcnl2LFWTfHTaEIlOJQRpATIpHCo4JWrUcicCrlpEq7EsrdTig666L6hFBkK7EINDCoSJywnDiH79L1NxKgdWqkg//TrhPanXUz/tEM2MS638iA6Kp4YZhQcSfS3flZeAHrTha1Pc2PH7NpdTNTure5ip0Osg1tz2jUfJp26T0j6uwWpzmD3+jJrTXs9+QWnXUl03YoLRL4RfK38znj9CfI36JYfDKdVi++PEcP/wE= -------------------------------------------------------------------------------- /infra/src/secondary/adapter/execution.rs: -------------------------------------------------------------------------------- 1 | use domain::executor::ports::secondary::TaskExecutionPort; 2 | use domain::executor::model::model::{Task, TaskStatus}; 3 | use domain::executor::model::error::TaskError; 4 | use std::process::{Command, Output}; 5 | use anyhow::{anyhow, Error, Context}; 6 | use im::Vector; 7 | use std::iter::FromIterator; 8 | 9 | pub struct LocalExecutionAdapter {} 10 | 11 | impl TaskExecutionPort for LocalExecutionAdapter { 12 | fn execute(&self, task: &Task) -> Result { 13 | let command_splitted = Vector::from_iter(task.command.split_whitespace().into_iter()); 14 | let main_command: &str = command_splitted.head() 15 | .ok_or(TaskError::CommandError("Command can't be empty".to_string())) 16 | .context("Error during command validation")?; 17 | Command::new(main_command) 18 | .args(command_splitted.split_at(1).1) 19 | .output() 20 | .map_err(|err| TaskError::ExecutionError { source: anyhow!("{:?}", err) }) 21 | .and_then(|output| validate_output(output)) 22 | .context("Error during command execution") 23 | } 24 | } 25 | 26 | impl LocalExecutionAdapter { 27 | pub fn new() -> LocalExecutionAdapter { 28 | LocalExecutionAdapter {} 29 | } 30 | } 31 | 32 | // TaskError -> anyhow::Error 33 | // FromUTF8Error ? -> anyhow::Error 34 | fn validate_output(output: Output) -> Result { 35 | match output.status.success() { 36 | true => String::from_utf8(output.stdout) 37 | .map_err(|err| TaskError::UnexpectedError { source: Box::new(err) }) 38 | .map(|output_string| TaskStatus::Success(output_string)), 39 | false => String::from_utf8(output.stderr) 40 | .map_err(|err| TaskError::UnexpectedError { source: Box::new(err) }) 41 | .and_then(|stderr_string| Err(TaskError::CommandError(stderr_string))) 42 | } 43 | } -------------------------------------------------------------------------------- /infra/src/main.rs: -------------------------------------------------------------------------------- 1 | extern crate blueprint_hexagonal_domain as domain; 2 | #[macro_use] 3 | extern crate diesel; 4 | #[macro_use] 5 | extern crate diesel_migrations; 6 | 7 | use std::borrow::Borrow; 8 | 9 | use anyhow::Error; 10 | use itertools::Itertools; 11 | use itertools::Chunks; 12 | 13 | use domain::executor::model::model::{TaskId, TaskStatus}; 14 | use domain::executor::ports::primary::TaskSchedulerPort; 15 | use domain::executor::service::task_execution::TaskScheduler; 16 | 17 | use crate::primary::cli::{CliOpt, parse_cli_opts, TaskRunOpt, TaskStatusOpt}; 18 | use crate::secondary::adapter::execution::LocalExecutionAdapter; 19 | use crate::secondary::adapter::id_generator::UUIDGeneratorAdapter; 20 | use crate::secondary::adapter::storage::new_storage_adapter; 21 | use diesel::IntoSql; 22 | 23 | 24 | mod secondary; 25 | mod primary; 26 | 27 | fn main() -> Result<(), Error> { 28 | let configuration = primary::settings::load_settings()?; 29 | 30 | let mut storage = new_storage_adapter(configuration.storage)?; 31 | let execution = LocalExecutionAdapter::new(); 32 | let id_generator = UUIDGeneratorAdapter::new(); 33 | let service = TaskScheduler::new( 34 | storage.as_mut(), 35 | execution.borrow(), 36 | id_generator.borrow(), 37 | ); 38 | run(service) 39 | } 40 | 41 | fn run(mut port: impl TaskSchedulerPort) -> Result<(), Error> { 42 | match parse_cli_opts() { 43 | CliOpt::Run(ref task_run_input) => port.schedule_task::(task_run_input.clone()) 44 | .map(|result| 45 | if task_run_input.wait { 46 | if let Err(err) = port.task_status(result).map(display_task_status) { 47 | eprintln!("Error waiting status of task : {:?}", err) 48 | }; 49 | } else { 50 | match result { 51 | TaskId::Id(id) => println!("Task with id {} scheduled", id), 52 | TaskId::Name(name) => println!("Task with name {} scheduled", name), 53 | } 54 | } 55 | ), 56 | CliOpt::Status(task_status_input) => port.task_status::(task_status_input) 57 | .map(display_task_status) 58 | } 59 | } 60 | 61 | fn display_task_status(status: TaskStatus) { 62 | match status { 63 | TaskStatus::Success(stdout) => println!("Task was successfully run :\n {}", stdout.lines().into_iter().map(|line| format!("\t{}", line)).join("\n")), 64 | TaskStatus::Scheduled => println!("Task is scheduled"), 65 | TaskStatus::Error(stderr) => eprintln!("Task was in error :\n {}", stderr.lines().map(|line| format!("\t{}", line)).join("\n")) 66 | } 67 | } -------------------------------------------------------------------------------- /infra/src/secondary/adapter/storage/memory.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Error, Context}; 2 | use im::HashMap; 3 | use im::Vector; 4 | 5 | use domain::executor::model::model::{Task, TaskId, TaskStatus}; 6 | use domain::executor::ports::secondary::TaskStoragePort; 7 | 8 | 9 | #[derive(Clone)] 10 | struct StoredTask { 11 | id: String, 12 | name: Option, 13 | command: String, 14 | env: Option>, 15 | status: TaskStatus, 16 | } 17 | 18 | pub struct InMemoryStorageAdapter { 19 | tasks: Vector 20 | } 21 | 22 | impl TaskStoragePort for InMemoryStorageAdapter { 23 | fn save(&mut self, task: Task) -> Result { 24 | self.tasks.push_back(StoredTask::from(&task)); 25 | Ok(task) 26 | } 27 | 28 | fn status(&mut self, id: TaskId) -> Result { 29 | let kept_id = id.clone(); 30 | self.find_only_one(id).context(format!("Error searching for id {:?}", kept_id)) 31 | .map(|stored_task| stored_task.status.clone()) 32 | } 33 | 34 | 35 | fn complete(&mut self, task: &Task, status: TaskStatus) -> Result<(), Error> { 36 | let id = task.id.clone(); 37 | self.tasks.iter().position(|stored_task| stored_task == TaskId::from(task)) 38 | .map(|index| { 39 | let mut stored_task = StoredTask::from(task); 40 | stored_task.status = status; 41 | self.tasks.set(index, stored_task); 42 | () 43 | }).context(format!("Error completing task {:?}", id)) 44 | } 45 | } 46 | 47 | impl InMemoryStorageAdapter { 48 | pub fn new() -> InMemoryStorageAdapter { 49 | InMemoryStorageAdapter { 50 | tasks: Vector::new() 51 | } 52 | } 53 | 54 | fn find_only_one(&mut self, id: TaskId) -> Result<&StoredTask, Error> { 55 | let result: Vec<&StoredTask> = self.tasks.iter() 56 | .filter(|stored_task| *stored_task == id).collect(); 57 | match result.as_slice() { 58 | [value] => Ok(value), 59 | [] => Err(anyhow!("No task correspond to your selection")), 60 | _ => Err(anyhow!("More than 1 task correspond to your selection")) 61 | } 62 | 63 | } 64 | } 65 | 66 | impl From<&Task> for StoredTask { 67 | fn from(task: &Task) -> Self { 68 | StoredTask { 69 | id: task.id.clone(), 70 | name: task.name.clone(), 71 | command: task.command.clone(), 72 | env: task.env.clone(), 73 | status: TaskStatus::Scheduled, 74 | } 75 | } 76 | } 77 | 78 | impl From for StoredTask { 79 | fn from(task: Task) -> Self { 80 | From::from(&task) 81 | } 82 | } 83 | 84 | impl PartialEq for &StoredTask { 85 | fn eq(&self, other: &TaskId) -> bool { 86 | match other { 87 | TaskId::Id(ref id) => self.id == id.clone(), 88 | TaskId::Name(ref name) => self.name == Some(name.clone()) 89 | } 90 | } 91 | } -------------------------------------------------------------------------------- /infra/src/secondary/adapter/storage/database/commands.rs: -------------------------------------------------------------------------------- 1 | use super::schema::tasks; 2 | use diesel::{SqliteConnection, Connection, RunQueryDsl}; 3 | use anyhow::{anyhow, Error, Context}; 4 | use domain::executor::model::model::{Task, TaskId, TaskStatus}; 5 | use std::convert::TryInto; 6 | use crate::diesel::*; 7 | use im::HashMap; 8 | 9 | 10 | pub const SCHEDULED: &str = "SCHEDULED"; 11 | pub const SUCCESS: &str = "SUCCESS"; 12 | pub const ERROR: &str = "ERROR"; 13 | 14 | #[derive(Queryable, Insertable)] 15 | #[table_name = "tasks"] 16 | struct DbTask { 17 | id: String, 18 | name: Option, 19 | command: String, 20 | env: Option, 21 | status: String, 22 | status_log: Option, 23 | } 24 | 25 | pub fn establish_connection(database_url: &str) -> Result { 26 | SqliteConnection::establish(database_url) 27 | .context("Error connecting to database") 28 | } 29 | 30 | pub fn create_task(conn: &SqliteConnection, new_task: &Task) -> Result { 31 | let insertable_task: DbTask = (new_task, &TaskStatus::Scheduled).into(); 32 | diesel::insert_into(tasks::table) 33 | .values(&insertable_task) 34 | .execute(conn) 35 | .context(format!("Error inserting in db task {:?}", new_task)) 36 | } 37 | 38 | pub fn get_task(conn: &SqliteConnection, task_id: &TaskId) -> Result { 39 | use super::schema::tasks::dsl::*; 40 | let (_, status_value) = match task_id { 41 | TaskId::Id(id_value) => tasks.filter(id.eq(id_value)) 42 | .limit(1) 43 | .first::(conn) 44 | .context(format!("Error loading from database id {}", id_value))?.try_into(), 45 | TaskId::Name(name_value) => tasks.filter(name.nullable().eq(name_value)) 46 | .limit(1) 47 | .first::(conn) 48 | .context(format!("Error loading from database name {}", name_value))?.try_into() 49 | }?; 50 | return Ok(status_value); 51 | } 52 | 53 | #[derive(AsChangeset)] 54 | #[table_name = "tasks"] 55 | struct TaskStatusUpdate<'a> { 56 | status: &'a str, 57 | status_log: Option<&'a str>, 58 | } 59 | 60 | pub fn update_task(conn: &SqliteConnection, id_value: &str, status: &str, status_log: Option<&str>) -> Result<(), Error> { 61 | use super::schema::tasks::dsl as tasks_dsl; 62 | diesel::update(tasks_dsl::tasks.find(id_value)) 63 | .set(&TaskStatusUpdate { status, status_log }) 64 | .execute(conn) 65 | .map(|_| ()) 66 | .context(format!("Error update in database for task id {}", id_value)) 67 | } 68 | 69 | impl From<(&Task, &TaskStatus)> for DbTask { 70 | fn from(insertable_value: (&Task, &TaskStatus)) -> Self { 71 | let (task, status) = insertable_value; 72 | let (status, status_log) = match status { 73 | TaskStatus::Scheduled => (SCHEDULED.to_string(), None), 74 | TaskStatus::Success(ref stdout) => (SUCCESS.to_string(), Some(stdout.clone())), 75 | TaskStatus::Error(ref stderr) => (ERROR.to_string(), Some(stderr.clone())), 76 | }; 77 | DbTask { 78 | id: task.id.clone(), 79 | name: task.name.clone(), 80 | command: task.command.clone(), 81 | // KEY=VAL;KEY2=VAL2;KEY3=VAL 82 | env: task.env.as_ref().map(|env_vars| env_vars.iter() 83 | .fold(vec![], |mut acc, (key, value)| { 84 | acc.push(format!("{}:{}", key, value)); 85 | acc 86 | }) 87 | .join(";")), 88 | status, 89 | status_log, 90 | } 91 | } 92 | } 93 | 94 | impl TryInto<(Task, TaskStatus)> for DbTask { 95 | type Error = Error; 96 | 97 | fn try_into(self) -> Result<(Task, TaskStatus), Self::Error> { 98 | let status = match self.status.as_str() { 99 | SCHEDULED => Ok(TaskStatus::Scheduled), 100 | SUCCESS => self.status_log.ok_or_else(|| anyhow!("Task {} is defined in database as SUCCES but doesn't have any status_log")) 101 | .map(|stdout| TaskStatus::Success(stdout)), 102 | ERROR => self.status_log.ok_or_else(|| anyhow!("Task {} is defined in database as ERROR but doesn't have any status_log")) 103 | .map(|stdout| TaskStatus::Error(stdout)), 104 | _ => Err(anyhow!("{} is not a valid status", self.status)) 105 | }?; 106 | let task = Task { 107 | id: self.id, 108 | name: self.name, 109 | command: self.command, 110 | env: self.env.map(parse_env_var), 111 | }; 112 | Ok((task, status)) 113 | } 114 | } 115 | 116 | fn parse_env_var(source: String) -> HashMap { 117 | From::from(source.split(";").map(|key_val| { 118 | let splited: Vec<&str> = key_val.splitn(2, "=").collect(); 119 | match splited.as_slice() { 120 | [key, value] => (String::from(*key), String::from(*value)), 121 | _ => (String::from(""), String::from("")) 122 | } 123 | }).collect::>()) 124 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Blueprint hexagonal architecture in Rust 2 | 3 | :wave: This project goal is to provide a blueprint of an implementation of hexagonal architecture in Rust. 4 | :warning: This project is in a work in progress mode and will be updated when I will find the time (and the courage) to work on it 5 | 6 | This project is a tiny task scheduler, you can create some tasks that will be run and get their status. 7 | 8 | ## Hexagonal architecture 9 | 10 | For hexagonal architecture presentation please refer to [Alistair Cockburn presentation](https://alistair.cockburn.us/hexagonal-architecture/) 11 | 12 | _This project:_ 13 | Domain & Infra code are split in two projects. 14 | I know it is not perfect and it could be improved (and it will be) but there is all the basics of hexagonal architecture from my point of view : 15 | - Separation of domain logic and infrastructure (side-effects) code 16 | - Portable domain 17 | - Testable domain 18 | - Composition in infra code to execute as wanted 19 | - Each secondary.adapter have a proper model for its purpose 20 | 21 | ### Domain 22 | ![domain schema](doc/domain_schema.png) 23 | 24 | _Ports_ : 25 | - __TaskSchedulePort (_executor::ports::primary::TaskSchedulerPort_)__ : Contract to schedule some Task and get their status 26 | - __TaskStoragePort (_executor::ports::secondary::TaskStoragePort_)__ : Contract to store tasks and their executions 27 | - __TaskExecutionPort (_executor::ports::secondary::TaskExecutionPort_)__ : Contract for task execution 28 | - __IdGeneratorPort (_executor::ports::secondary::IdGeneratorPort_)__ : Contract to generate ids for tasks 29 | 30 | 31 | ### Infra 32 | ![infra schema](doc/infra_schema.png) 33 | 34 | _Adapters_ : 35 | - __CLI Input (_primary::cli::CliOpt_)__ : Input of the application via command line 36 | - __UUID IdGenerator (_secondary::adapter::id_generator::UUIDGeneratorAdapter_)__ : Ig generator based on UUID 37 | - __Local ExecutionAdapter (_secondary::adapter::execution::LocalExecutionAdapter_)__ : Task execution secondary.adapter on local machine 38 | - __Database StorageAdapter (_secondary::adapter::storage::database::SqliteStorageAdapter_)__ : Database storage 39 | - __InMemory StorageAdapter (_secondary::adapter::storage::memory::InMemoryStorageAdapter_)__ : InMemory storage 40 | 41 | ### Composability 42 | 43 | Storage can be in memory using `secondary::adapter::storage::memory::InMemoryStorageAdapter` or with sqlitedb using `secondary::adapter::storage::database::SqliteStorageAdapter`. 44 | This behavior is configured by the `new_storage_adapter` in `secondary::adapter::storage` module. 45 | Currently, the choice is hard coded but it could be configurable. 46 | 47 | ## Setup 48 | 49 | ### Database init (Optional) 50 | 51 | Application roll automatically database migration but you can manualy do the needed migrations. 52 | 53 | To initialize database please install [`cargo install diesel_cli for sqlite`](https://github.com/diesel-rs/diesel/tree/master/diesel_cli#installation). 54 | 55 | Run the migrations at root of the project: `diesel migration run --database-url ` 56 | 57 | ### Build 58 | 59 | Use cargo for build : `cargo build` 60 | 61 | ### Configuration 62 | 63 | The configuration of the application is loaded from [settings.toml](settings.toml) file. 64 | 65 | You can override configuration by env var (example `export DATABASE_URL=override.db` override `database.url` settings) 66 | 67 | ### Execute 68 | 69 | _Run from `cargo` :_ `cargo run -- ` 70 | 71 | _Command line execution :_ ` ./target/debug/blueprint-hexagonal-infra ` 72 | 73 | ### Usage 74 | 75 | __Run a task__ : 76 | ``` 77 | blueprint-hexagonal-infra-run 0.1.0 78 | 79 | USAGE: 80 | blueprint-hexagonal-infra run [FLAGS] [OPTIONS] ... 81 | 82 | FLAGS: 83 | -w, --wait Wait the execution of the task and print status 84 | 85 | OPTIONS: 86 | -n, --name Name of the task for later querying 87 | 88 | ARGS: 89 | ... Command to be executed by the task 90 | 91 | ``` 92 | 93 | _Example_ : `./target/debug/blueprint-hexagonal-infra run ls /` 94 | 95 | __Status of a task__ : 96 | ``` 97 | USAGE: 98 | blueprint-hexagonal-infra status id 99 | blueprint-hexagonal-infra status name 100 | ``` 101 | 102 | _Example_ : `./target/debug/blueprint-hexagonal-infra status id f340a3d3-f5ca-42b1-9a3b-312112836cd8` 103 | 104 | ### Database connection 105 | 106 | ``` 107 |  sqlite3 test.db 108 | SQLite version 3.31.1 2020-01-27 19:55:54 109 | Enter ".help" for usage hints. 110 | sqlite> select * from tasks; 111 | 1477bb1b-73b2-468e-a4e2-fc571423da25||ls /home||SUCCESS|fteychene 112 | linuxbrew 113 | ``` 114 | 115 | ## TODO 116 | 117 | - [x] Improve genericity for the domain using `Into` and `From` (limitation on secondary ports see [#limitations](#into-for-secondary-ports)) 118 | - [x] Add unit tests 119 | - [x] Add real life secondary.adapter 120 | - [x] CLI Adapter for input 121 | - [x] Improve error management 122 | - [x] Improve documentation 123 | - [x] Run migration through code for database 124 | - [ ] Add input validation 125 | - [ ] Split task execution 126 | - [ ] Split code to client and server 127 | - [ ] Make connection through unix socket from cli to server 128 | 129 | ## Limitations 130 | 131 | ### Into for secondary ports 132 | 133 | [Concerned code part](domain/src/executor/ports/secondary.rs#L11) 134 | 135 | __Goal__ : 136 | Main goal was to provide a way for adapters to not adapt their internal domains to application domain as function result. 137 | To provide this feature we would like to provide functions of secondary ports to return types with only `Into` constraint. 138 | 139 | __Issue__ : 140 | ``` 141 | error[E0038]: the trait `std::convert::Into` cannot be made into an object 142 | --> infra/src/main.rs:37:1 143 | | 144 | 37 | fn test() -> Box> { 145 | | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `std::convert::Into` cannot be made into an object 146 | | 147 | = note: the trait cannot require that `Self : Sized` 148 | 149 | error: aborting due to previous error 150 | ``` 151 | 152 | The trait `Into` cannot be used as part of a dynamic type due to [error E0038](https://doc.rust-lang.org/error-index.html#E0038) -------------------------------------------------------------------------------- /domain/src/executor/service/task_execution.rs: -------------------------------------------------------------------------------- 1 | use crate::executor::ports::secondary::{TaskStoragePort, TaskExecutionPort, IdGeneratorPort}; 2 | use crate::executor::ports::primary::{TaskSchedulerPort, TaskInput}; 3 | use crate::executor::model::model::{Task, TaskId, TaskStatus}; 4 | use anyhow::{Error, Context}; 5 | 6 | pub struct TaskScheduler<'a> { 7 | storage: &'a mut dyn TaskStoragePort, 8 | execution: &'a dyn TaskExecutionPort, 9 | id_generator: &'a dyn IdGeneratorPort, 10 | } 11 | 12 | impl TaskSchedulerPort for TaskScheduler<'_> { 13 | fn schedule_task(&mut self, input_task: T) -> Result 14 | where T: Into { 15 | self.storage.save(task(input_task.into(), self.id_generator.generate_id())).context("Error storing task during schedule") 16 | // No rule logic for the moment, execute after 17 | .and_then(|into_task| execute_task(into_task.into(), self.execution, self.storage)).context("Error during task execution") 18 | } 19 | 20 | fn task_status(&mut self, id: T) -> Result 21 | where T: Into { 22 | self.storage.status(id.into()).context("Error on task status") 23 | } 24 | } 25 | 26 | impl TaskScheduler<'_> { 27 | pub fn new<'a>(storage: &'a mut dyn TaskStoragePort, execution: &'a dyn TaskExecutionPort, id_generator: &'a dyn IdGeneratorPort) -> TaskScheduler<'a> { 28 | TaskScheduler { 29 | storage, 30 | execution, 31 | id_generator, 32 | } 33 | } 34 | } 35 | 36 | 37 | fn task(input: TaskInput, id: String) -> Task { 38 | Task { 39 | id, 40 | command: input.command, 41 | name: input.name, 42 | env: input.env, 43 | } 44 | } 45 | 46 | fn execute_task(task: Task, executor: &dyn TaskExecutionPort, storage: &mut dyn TaskStoragePort) -> Result { 47 | executor.execute(&task) 48 | .map_err(|error| { 49 | match storage.complete(&task, TaskStatus::Error(error.to_string())) { 50 | Ok(_) => error.context(format!("Error during task {} execution", task.id)), 51 | Err(err) => err.context(format!("Error executing task {} and during status save execution", task.id)) 52 | } 53 | }) 54 | .and_then(|result| storage.complete(&task, result)) 55 | .map(|_| TaskId::from(&task)) 56 | } 57 | 58 | #[cfg(test)] 59 | mod tests { 60 | use super::*; 61 | use crate::executor::model::error::TaskError; 62 | use mockall::*; 63 | use crate::executor::ports::secondary::{MockTaskExecutionPort, MockTaskStoragePort, MockIdGeneratorPort}; 64 | 65 | // TODO add storage of commands to check num of interaction on tests impls 66 | #[test] 67 | fn test_execute_task() { 68 | let input_task = Task { 69 | id: "test_id".to_string(), 70 | name: None, 71 | command: "ls /home".to_string(), 72 | env: None, 73 | }; 74 | 75 | let mut execution_mock = MockTaskExecutionPort::new(); 76 | execution_mock.expect_execute() 77 | .times(1) 78 | .returning(|_| Ok(TaskStatus::Success("Coucou".to_string()))); 79 | 80 | let mut storage_mock = MockTaskStoragePort::new(); 81 | storage_mock.expect_complete() 82 | .times(1) 83 | .returning(|_, _| Ok(())); 84 | 85 | assert_eq!(execute_task(input_task, &execution_mock, &mut storage_mock).unwrap(), TaskId::Id("test_id".to_string())); 86 | } 87 | 88 | #[test] 89 | fn test_execute_task_with_execution_failure() { 90 | let mut execution_mock = MockTaskExecutionPort::new(); 91 | execution_mock.expect_execute() 92 | .times(1) 93 | .returning(|_| Err(TaskError::CommandError("Cannot move /test/inexistant, file does not exists".to_string()))); 94 | 95 | let mut storage_mock = MockTaskStoragePort::new(); 96 | storage_mock.expect_complete() 97 | .times(1) 98 | .returning(|_, _| Ok(())); 99 | 100 | let input_task = Task { 101 | id: "test_id".to_string(), 102 | name: None, 103 | command: "mv /test/inexistant".to_string(), 104 | env: None, 105 | }; 106 | assert_eq!(format!("{}", execute_task(input_task, &execution_mock, &mut storage_mock).unwrap_err()), "Error during task test_id execution"); 107 | } 108 | 109 | #[test] 110 | fn test_execute_task_with_execution_failure_and_storage_failure() { 111 | let mut execution_mock = MockTaskExecutionPort::new(); 112 | execution_mock.expect_execute() 113 | .times(1) 114 | .returning(|_| Err(TaskError::CommandError("Cannot move /test/inexistant, file does not exists".to_string()))); 115 | 116 | let mut storage_mock = MockTaskStoragePort::new(); 117 | storage_mock.expect_complete() 118 | .times(1) 119 | .returning(|_, _| Err(anyhow!("Storage failed"))); 120 | 121 | let input_task = Task { 122 | id: "test_id".to_string(), 123 | name: None, 124 | command: "mv /test/inexistant".to_string(), 125 | env: None, 126 | }; 127 | assert_eq!(format!("{}", execute_task(input_task, &execution_mock, &mut storage_mock).unwrap_err()), "Error executing task test_id and during status save execution"); 128 | } 129 | 130 | #[test] 131 | fn test_execute_task_with_execution_success_and_storage_failure() { 132 | let mut execution_mock = MockTaskExecutionPort::new(); 133 | execution_mock.expect_execute() 134 | .times(1) 135 | .returning(|_| Ok(TaskStatus::Success("Coucou".to_string()))); 136 | 137 | let mut storage_mock = MockTaskStoragePort::new(); 138 | storage_mock.expect_complete() 139 | .times(1) 140 | .returning(|_, _| Err(anyhow!("Storage failed"))); 141 | 142 | let input_task = Task { 143 | id: "test_id".to_string(), 144 | name: None, 145 | command: "mv /test/inexistant".to_string(), 146 | env: None, 147 | }; 148 | assert_eq!(format!("{}", execute_task(input_task, &execution_mock, &mut storage_mock).unwrap_err()), "Storage failed"); 149 | } 150 | 151 | #[test] 152 | fn test_task_scheduler_schedule_task_should_execute_just_after() { 153 | let mut execution_mock = MockTaskExecutionPort::new(); 154 | execution_mock.expect_execute() 155 | .times(1) 156 | .returning(|_| Ok(TaskStatus::Success("Coucou".to_string()))); 157 | 158 | let mut storage_mock = MockTaskStoragePort::new(); 159 | storage_mock.expect_save() 160 | .times(1) 161 | .returning(|x| Ok(x)); 162 | storage_mock.expect_complete() 163 | .times(1) 164 | .returning(|_, _| Ok(())); 165 | 166 | let mut id_mock = MockIdGeneratorPort::new(); 167 | id_mock.expect_generate_id() 168 | .times(1) 169 | .returning(|| "test_id".to_string()); 170 | 171 | let mut service = TaskScheduler::new(&mut storage_mock, &execution_mock, &id_mock); 172 | 173 | let input_task = TaskInput { 174 | name: None, 175 | command: "ls /home".to_string(), 176 | env: None 177 | }; 178 | assert_eq!(service.schedule_task(input_task).unwrap(), TaskId::Id("test_id".to_string())); 179 | } 180 | } -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | [[package]] 4 | name = "aho-corasick" 5 | version = "0.7.10" 6 | source = "registry+https://github.com/rust-lang/crates.io-index" 7 | checksum = "8716408b8bc624ed7f65d223ddb9ac2d044c0547b6fa4b0d554f3a9540496ada" 8 | dependencies = [ 9 | "memchr", 10 | ] 11 | 12 | [[package]] 13 | name = "ansi_term" 14 | version = "0.11.0" 15 | source = "registry+https://github.com/rust-lang/crates.io-index" 16 | checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" 17 | dependencies = [ 18 | "winapi", 19 | ] 20 | 21 | [[package]] 22 | name = "anyhow" 23 | version = "1.0.27" 24 | source = "registry+https://github.com/rust-lang/crates.io-index" 25 | checksum = "013a6e0a2cbe3d20f9c60b65458f7a7f7a5e636c5d0f45a5a6aee5d4b1f01785" 26 | 27 | [[package]] 28 | name = "arrayvec" 29 | version = "0.4.12" 30 | source = "registry+https://github.com/rust-lang/crates.io-index" 31 | checksum = "cd9fd44efafa8690358b7408d253adf110036b88f55672a933f01d616ad9b1b9" 32 | dependencies = [ 33 | "nodrop", 34 | ] 35 | 36 | [[package]] 37 | name = "atty" 38 | version = "0.2.14" 39 | source = "registry+https://github.com/rust-lang/crates.io-index" 40 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 41 | dependencies = [ 42 | "hermit-abi", 43 | "libc", 44 | "winapi", 45 | ] 46 | 47 | [[package]] 48 | name = "autocfg" 49 | version = "1.0.0" 50 | source = "registry+https://github.com/rust-lang/crates.io-index" 51 | checksum = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d" 52 | 53 | [[package]] 54 | name = "bitflags" 55 | version = "1.2.1" 56 | source = "registry+https://github.com/rust-lang/crates.io-index" 57 | checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" 58 | 59 | [[package]] 60 | name = "bitmaps" 61 | version = "2.1.0" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "031043d04099746d8db04daf1fa424b2bc8bd69d92b25962dcde24da39ab64a2" 64 | dependencies = [ 65 | "typenum", 66 | ] 67 | 68 | [[package]] 69 | name = "blueprint-hexagonal-domain" 70 | version = "0.1.0" 71 | dependencies = [ 72 | "anyhow", 73 | "im", 74 | "mockall", 75 | "mockall_derive", 76 | "thiserror", 77 | ] 78 | 79 | [[package]] 80 | name = "blueprint-hexagonal-infra" 81 | version = "0.1.0" 82 | dependencies = [ 83 | "anyhow", 84 | "blueprint-hexagonal-domain", 85 | "config", 86 | "diesel", 87 | "diesel_migrations", 88 | "im", 89 | "itertools", 90 | "serde 1.0.105", 91 | "structopt", 92 | "uuid", 93 | ] 94 | 95 | [[package]] 96 | name = "byteorder" 97 | version = "1.3.4" 98 | source = "registry+https://github.com/rust-lang/crates.io-index" 99 | checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de" 100 | 101 | [[package]] 102 | name = "cfg-if" 103 | version = "0.1.10" 104 | source = "registry+https://github.com/rust-lang/crates.io-index" 105 | checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" 106 | 107 | [[package]] 108 | name = "clap" 109 | version = "2.33.0" 110 | source = "registry+https://github.com/rust-lang/crates.io-index" 111 | checksum = "5067f5bb2d80ef5d68b4c87db81601f0b75bca627bc2ef76b141d7b846a3c6d9" 112 | dependencies = [ 113 | "ansi_term", 114 | "atty", 115 | "bitflags", 116 | "strsim", 117 | "textwrap", 118 | "unicode-width", 119 | "vec_map", 120 | ] 121 | 122 | [[package]] 123 | name = "cloudabi" 124 | version = "0.0.3" 125 | source = "registry+https://github.com/rust-lang/crates.io-index" 126 | checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" 127 | dependencies = [ 128 | "bitflags", 129 | ] 130 | 131 | [[package]] 132 | name = "config" 133 | version = "0.10.1" 134 | source = "registry+https://github.com/rust-lang/crates.io-index" 135 | checksum = "19b076e143e1d9538dde65da30f8481c2a6c44040edb8e02b9bf1351edb92ce3" 136 | dependencies = [ 137 | "lazy_static", 138 | "nom", 139 | "rust-ini", 140 | "serde 1.0.105", 141 | "serde-hjson", 142 | "serde_json", 143 | "toml", 144 | "yaml-rust", 145 | ] 146 | 147 | [[package]] 148 | name = "diesel" 149 | version = "1.4.4" 150 | source = "registry+https://github.com/rust-lang/crates.io-index" 151 | checksum = "33d7ca63eb2efea87a7f56a283acc49e2ce4b2bd54adf7465dc1d81fef13d8fc" 152 | dependencies = [ 153 | "byteorder", 154 | "diesel_derives", 155 | "libsqlite3-sys", 156 | "r2d2", 157 | ] 158 | 159 | [[package]] 160 | name = "diesel_derives" 161 | version = "1.4.1" 162 | source = "registry+https://github.com/rust-lang/crates.io-index" 163 | checksum = "45f5098f628d02a7a0f68ddba586fb61e80edec3bdc1be3b921f4ceec60858d3" 164 | dependencies = [ 165 | "proc-macro2", 166 | "quote", 167 | "syn", 168 | ] 169 | 170 | [[package]] 171 | name = "diesel_migrations" 172 | version = "1.4.0" 173 | source = "registry+https://github.com/rust-lang/crates.io-index" 174 | checksum = "bf3cde8413353dc7f5d72fa8ce0b99a560a359d2c5ef1e5817ca731cd9008f4c" 175 | dependencies = [ 176 | "migrations_internals", 177 | "migrations_macros", 178 | ] 179 | 180 | [[package]] 181 | name = "difference" 182 | version = "2.0.0" 183 | source = "registry+https://github.com/rust-lang/crates.io-index" 184 | checksum = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198" 185 | 186 | [[package]] 187 | name = "downcast" 188 | version = "0.10.0" 189 | source = "registry+https://github.com/rust-lang/crates.io-index" 190 | checksum = "4bb454f0228b18c7f4c3b0ebbee346ed9c52e7443b0999cd543ff3571205701d" 191 | 192 | [[package]] 193 | name = "either" 194 | version = "1.5.3" 195 | source = "registry+https://github.com/rust-lang/crates.io-index" 196 | checksum = "bb1f6b1ce1c140482ea30ddd3335fc0024ac7ee112895426e0a629a6c20adfe3" 197 | 198 | [[package]] 199 | name = "float-cmp" 200 | version = "0.6.0" 201 | source = "registry+https://github.com/rust-lang/crates.io-index" 202 | checksum = "da62c4f1b81918835a8c6a484a397775fff5953fe83529afd51b05f5c6a6617d" 203 | dependencies = [ 204 | "num-traits 0.2.11", 205 | ] 206 | 207 | [[package]] 208 | name = "fragile" 209 | version = "1.0.0" 210 | source = "registry+https://github.com/rust-lang/crates.io-index" 211 | checksum = "69a039c3498dc930fe810151a34ba0c1c70b02b8625035592e74432f678591f2" 212 | 213 | [[package]] 214 | name = "getrandom" 215 | version = "0.1.14" 216 | source = "registry+https://github.com/rust-lang/crates.io-index" 217 | checksum = "7abc8dd8451921606d809ba32e95b6111925cd2906060d2dcc29c070220503eb" 218 | dependencies = [ 219 | "cfg-if", 220 | "libc", 221 | "wasi", 222 | ] 223 | 224 | [[package]] 225 | name = "heck" 226 | version = "0.3.1" 227 | source = "registry+https://github.com/rust-lang/crates.io-index" 228 | checksum = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205" 229 | dependencies = [ 230 | "unicode-segmentation", 231 | ] 232 | 233 | [[package]] 234 | name = "hermit-abi" 235 | version = "0.1.10" 236 | source = "registry+https://github.com/rust-lang/crates.io-index" 237 | checksum = "725cf19794cf90aa94e65050cb4191ff5d8fa87a498383774c47b332e3af952e" 238 | dependencies = [ 239 | "libc", 240 | ] 241 | 242 | [[package]] 243 | name = "im" 244 | version = "14.3.0" 245 | source = "registry+https://github.com/rust-lang/crates.io-index" 246 | checksum = "696059c87b83c5a258817ecd67c3af915e3ed141891fc35a1e79908801cf0ce7" 247 | dependencies = [ 248 | "bitmaps", 249 | "rand_core", 250 | "rand_xoshiro", 251 | "sized-chunks", 252 | "typenum", 253 | "version_check", 254 | ] 255 | 256 | [[package]] 257 | name = "itertools" 258 | version = "0.10.0" 259 | source = "registry+https://github.com/rust-lang/crates.io-index" 260 | checksum = "37d572918e350e82412fe766d24b15e6682fb2ed2bbe018280caa810397cb319" 261 | dependencies = [ 262 | "either", 263 | ] 264 | 265 | [[package]] 266 | name = "itoa" 267 | version = "0.4.5" 268 | source = "registry+https://github.com/rust-lang/crates.io-index" 269 | checksum = "b8b7a7c0c47db5545ed3fef7468ee7bb5b74691498139e4b3f6a20685dc6dd8e" 270 | 271 | [[package]] 272 | name = "lazy_static" 273 | version = "1.4.0" 274 | source = "registry+https://github.com/rust-lang/crates.io-index" 275 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 276 | 277 | [[package]] 278 | name = "lexical-core" 279 | version = "0.6.2" 280 | source = "registry+https://github.com/rust-lang/crates.io-index" 281 | checksum = "d7043aa5c05dd34fb73b47acb8c3708eac428de4545ea3682ed2f11293ebd890" 282 | dependencies = [ 283 | "arrayvec", 284 | "cfg-if", 285 | "rustc_version", 286 | "ryu", 287 | "static_assertions", 288 | ] 289 | 290 | [[package]] 291 | name = "libc" 292 | version = "0.2.68" 293 | source = "registry+https://github.com/rust-lang/crates.io-index" 294 | checksum = "dea0c0405123bba743ee3f91f49b1c7cfb684eef0da0a50110f758ccf24cdff0" 295 | 296 | [[package]] 297 | name = "libsqlite3-sys" 298 | version = "0.17.1" 299 | source = "registry+https://github.com/rust-lang/crates.io-index" 300 | checksum = "266eb8c361198e8d1f682bc974e5d9e2ae90049fb1943890904d11dad7d4a77d" 301 | dependencies = [ 302 | "pkg-config", 303 | "vcpkg", 304 | ] 305 | 306 | [[package]] 307 | name = "linked-hash-map" 308 | version = "0.3.0" 309 | source = "registry+https://github.com/rust-lang/crates.io-index" 310 | checksum = "6d262045c5b87c0861b3f004610afd0e2c851e2908d08b6c870cbb9d5f494ecd" 311 | dependencies = [ 312 | "serde 0.8.23", 313 | "serde_test", 314 | ] 315 | 316 | [[package]] 317 | name = "linked-hash-map" 318 | version = "0.5.2" 319 | source = "registry+https://github.com/rust-lang/crates.io-index" 320 | checksum = "ae91b68aebc4ddb91978b11a1b02ddd8602a05ec19002801c5666000e05e0f83" 321 | 322 | [[package]] 323 | name = "lock_api" 324 | version = "0.3.3" 325 | source = "registry+https://github.com/rust-lang/crates.io-index" 326 | checksum = "79b2de95ecb4691949fea4716ca53cdbcfccb2c612e19644a8bad05edcf9f47b" 327 | dependencies = [ 328 | "scopeguard", 329 | ] 330 | 331 | [[package]] 332 | name = "log" 333 | version = "0.4.8" 334 | source = "registry+https://github.com/rust-lang/crates.io-index" 335 | checksum = "14b6052be84e6b71ab17edffc2eeabf5c2c3ae1fdb464aae35ac50c67a44e1f7" 336 | dependencies = [ 337 | "cfg-if", 338 | ] 339 | 340 | [[package]] 341 | name = "memchr" 342 | version = "2.3.3" 343 | source = "registry+https://github.com/rust-lang/crates.io-index" 344 | checksum = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400" 345 | 346 | [[package]] 347 | name = "migrations_internals" 348 | version = "1.4.1" 349 | source = "registry+https://github.com/rust-lang/crates.io-index" 350 | checksum = "2b4fc84e4af020b837029e017966f86a1c2d5e83e64b589963d5047525995860" 351 | dependencies = [ 352 | "diesel", 353 | ] 354 | 355 | [[package]] 356 | name = "migrations_macros" 357 | version = "1.4.2" 358 | source = "registry+https://github.com/rust-lang/crates.io-index" 359 | checksum = "9753f12909fd8d923f75ae5c3258cae1ed3c8ec052e1b38c93c21a6d157f789c" 360 | dependencies = [ 361 | "migrations_internals", 362 | "proc-macro2", 363 | "quote", 364 | "syn", 365 | ] 366 | 367 | [[package]] 368 | name = "mockall" 369 | version = "0.7.0" 370 | source = "registry+https://github.com/rust-lang/crates.io-index" 371 | checksum = "48c9eefc7768ee7a28a09d64e40203d57ad20af8525b7428d5f2f55d8c621984" 372 | dependencies = [ 373 | "cfg-if", 374 | "downcast", 375 | "fragile", 376 | "lazy_static", 377 | "mockall_derive", 378 | "predicates", 379 | "predicates-tree", 380 | ] 381 | 382 | [[package]] 383 | name = "mockall_derive" 384 | version = "0.7.0" 385 | source = "registry+https://github.com/rust-lang/crates.io-index" 386 | checksum = "447326d4e6d99ea272b6e5599cbbfc1e3407c23a856ccf1eb9427ad73267376f" 387 | dependencies = [ 388 | "cfg-if", 389 | "proc-macro2", 390 | "quote", 391 | "syn", 392 | ] 393 | 394 | [[package]] 395 | name = "nodrop" 396 | version = "0.1.14" 397 | source = "registry+https://github.com/rust-lang/crates.io-index" 398 | checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" 399 | 400 | [[package]] 401 | name = "nom" 402 | version = "5.1.1" 403 | source = "registry+https://github.com/rust-lang/crates.io-index" 404 | checksum = "0b471253da97532da4b61552249c521e01e736071f71c1a4f7ebbfbf0a06aad6" 405 | dependencies = [ 406 | "lexical-core", 407 | "memchr", 408 | "version_check", 409 | ] 410 | 411 | [[package]] 412 | name = "normalize-line-endings" 413 | version = "0.3.0" 414 | source = "registry+https://github.com/rust-lang/crates.io-index" 415 | checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" 416 | 417 | [[package]] 418 | name = "num-traits" 419 | version = "0.1.43" 420 | source = "registry+https://github.com/rust-lang/crates.io-index" 421 | checksum = "92e5113e9fd4cc14ded8e499429f396a20f98c772a47cc8622a736e1ec843c31" 422 | dependencies = [ 423 | "num-traits 0.2.11", 424 | ] 425 | 426 | [[package]] 427 | name = "num-traits" 428 | version = "0.2.11" 429 | source = "registry+https://github.com/rust-lang/crates.io-index" 430 | checksum = "c62be47e61d1842b9170f0fdeec8eba98e60e90e5446449a0545e5152acd7096" 431 | dependencies = [ 432 | "autocfg", 433 | ] 434 | 435 | [[package]] 436 | name = "parking_lot" 437 | version = "0.10.0" 438 | source = "registry+https://github.com/rust-lang/crates.io-index" 439 | checksum = "92e98c49ab0b7ce5b222f2cc9193fc4efe11c6d0bd4f648e374684a6857b1cfc" 440 | dependencies = [ 441 | "lock_api", 442 | "parking_lot_core", 443 | ] 444 | 445 | [[package]] 446 | name = "parking_lot_core" 447 | version = "0.7.0" 448 | source = "registry+https://github.com/rust-lang/crates.io-index" 449 | checksum = "7582838484df45743c8434fbff785e8edf260c28748353d44bc0da32e0ceabf1" 450 | dependencies = [ 451 | "cfg-if", 452 | "cloudabi", 453 | "libc", 454 | "redox_syscall", 455 | "smallvec", 456 | "winapi", 457 | ] 458 | 459 | [[package]] 460 | name = "pkg-config" 461 | version = "0.3.17" 462 | source = "registry+https://github.com/rust-lang/crates.io-index" 463 | checksum = "05da548ad6865900e60eaba7f589cc0783590a92e940c26953ff81ddbab2d677" 464 | 465 | [[package]] 466 | name = "ppv-lite86" 467 | version = "0.2.6" 468 | source = "registry+https://github.com/rust-lang/crates.io-index" 469 | checksum = "74490b50b9fbe561ac330df47c08f3f33073d2d00c150f719147d7c54522fa1b" 470 | 471 | [[package]] 472 | name = "predicates" 473 | version = "1.0.4" 474 | source = "registry+https://github.com/rust-lang/crates.io-index" 475 | checksum = "347a1b6f0b21e636bc9872fb60b83b8e185f6f5516298b8238699f7f9a531030" 476 | dependencies = [ 477 | "difference", 478 | "float-cmp", 479 | "normalize-line-endings", 480 | "predicates-core", 481 | "regex", 482 | ] 483 | 484 | [[package]] 485 | name = "predicates-core" 486 | version = "1.0.0" 487 | source = "registry+https://github.com/rust-lang/crates.io-index" 488 | checksum = "06075c3a3e92559ff8929e7a280684489ea27fe44805174c3ebd9328dcb37178" 489 | 490 | [[package]] 491 | name = "predicates-tree" 492 | version = "1.0.0" 493 | source = "registry+https://github.com/rust-lang/crates.io-index" 494 | checksum = "8e63c4859013b38a76eca2414c64911fba30def9e3202ac461a2d22831220124" 495 | dependencies = [ 496 | "predicates-core", 497 | "treeline", 498 | ] 499 | 500 | [[package]] 501 | name = "proc-macro-error" 502 | version = "0.4.12" 503 | source = "registry+https://github.com/rust-lang/crates.io-index" 504 | checksum = "18f33027081eba0a6d8aba6d1b1c3a3be58cbb12106341c2d5759fcd9b5277e7" 505 | dependencies = [ 506 | "proc-macro-error-attr", 507 | "proc-macro2", 508 | "quote", 509 | "syn", 510 | "version_check", 511 | ] 512 | 513 | [[package]] 514 | name = "proc-macro-error-attr" 515 | version = "0.4.12" 516 | source = "registry+https://github.com/rust-lang/crates.io-index" 517 | checksum = "8a5b4b77fdb63c1eca72173d68d24501c54ab1269409f6b672c85deb18af69de" 518 | dependencies = [ 519 | "proc-macro2", 520 | "quote", 521 | "syn", 522 | "syn-mid", 523 | "version_check", 524 | ] 525 | 526 | [[package]] 527 | name = "proc-macro2" 528 | version = "1.0.9" 529 | source = "registry+https://github.com/rust-lang/crates.io-index" 530 | checksum = "6c09721c6781493a2a492a96b5a5bf19b65917fe6728884e7c44dd0c60ca3435" 531 | dependencies = [ 532 | "unicode-xid", 533 | ] 534 | 535 | [[package]] 536 | name = "quote" 537 | version = "1.0.3" 538 | source = "registry+https://github.com/rust-lang/crates.io-index" 539 | checksum = "2bdc6c187c65bca4260c9011c9e3132efe4909da44726bad24cf7572ae338d7f" 540 | dependencies = [ 541 | "proc-macro2", 542 | ] 543 | 544 | [[package]] 545 | name = "r2d2" 546 | version = "0.8.8" 547 | source = "registry+https://github.com/rust-lang/crates.io-index" 548 | checksum = "1497e40855348e4a8a40767d8e55174bce1e445a3ac9254ad44ad468ee0485af" 549 | dependencies = [ 550 | "log", 551 | "parking_lot", 552 | "scheduled-thread-pool", 553 | ] 554 | 555 | [[package]] 556 | name = "rand" 557 | version = "0.7.3" 558 | source = "registry+https://github.com/rust-lang/crates.io-index" 559 | checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" 560 | dependencies = [ 561 | "getrandom", 562 | "libc", 563 | "rand_chacha", 564 | "rand_core", 565 | "rand_hc", 566 | ] 567 | 568 | [[package]] 569 | name = "rand_chacha" 570 | version = "0.2.2" 571 | source = "registry+https://github.com/rust-lang/crates.io-index" 572 | checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" 573 | dependencies = [ 574 | "ppv-lite86", 575 | "rand_core", 576 | ] 577 | 578 | [[package]] 579 | name = "rand_core" 580 | version = "0.5.1" 581 | source = "registry+https://github.com/rust-lang/crates.io-index" 582 | checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" 583 | dependencies = [ 584 | "getrandom", 585 | ] 586 | 587 | [[package]] 588 | name = "rand_hc" 589 | version = "0.2.0" 590 | source = "registry+https://github.com/rust-lang/crates.io-index" 591 | checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" 592 | dependencies = [ 593 | "rand_core", 594 | ] 595 | 596 | [[package]] 597 | name = "rand_xoshiro" 598 | version = "0.4.0" 599 | source = "registry+https://github.com/rust-lang/crates.io-index" 600 | checksum = "a9fcdd2e881d02f1d9390ae47ad8e5696a9e4be7b547a1da2afbc61973217004" 601 | dependencies = [ 602 | "rand_core", 603 | ] 604 | 605 | [[package]] 606 | name = "redox_syscall" 607 | version = "0.1.56" 608 | source = "registry+https://github.com/rust-lang/crates.io-index" 609 | checksum = "2439c63f3f6139d1b57529d16bc3b8bb855230c8efcc5d3a896c8bea7c3b1e84" 610 | 611 | [[package]] 612 | name = "regex" 613 | version = "1.3.6" 614 | source = "registry+https://github.com/rust-lang/crates.io-index" 615 | checksum = "7f6946991529684867e47d86474e3a6d0c0ab9b82d5821e314b1ede31fa3a4b3" 616 | dependencies = [ 617 | "aho-corasick", 618 | "memchr", 619 | "regex-syntax", 620 | "thread_local", 621 | ] 622 | 623 | [[package]] 624 | name = "regex-syntax" 625 | version = "0.6.17" 626 | source = "registry+https://github.com/rust-lang/crates.io-index" 627 | checksum = "7fe5bd57d1d7414c6b5ed48563a2c855d995ff777729dcd91c369ec7fea395ae" 628 | 629 | [[package]] 630 | name = "rust-ini" 631 | version = "0.13.0" 632 | source = "registry+https://github.com/rust-lang/crates.io-index" 633 | checksum = "3e52c148ef37f8c375d49d5a73aa70713125b7f19095948a923f80afdeb22ec2" 634 | 635 | [[package]] 636 | name = "rustc_version" 637 | version = "0.2.3" 638 | source = "registry+https://github.com/rust-lang/crates.io-index" 639 | checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" 640 | dependencies = [ 641 | "semver", 642 | ] 643 | 644 | [[package]] 645 | name = "ryu" 646 | version = "1.0.3" 647 | source = "registry+https://github.com/rust-lang/crates.io-index" 648 | checksum = "535622e6be132bccd223f4bb2b8ac8d53cda3c7a6394944d3b2b33fb974f9d76" 649 | 650 | [[package]] 651 | name = "scheduled-thread-pool" 652 | version = "0.2.4" 653 | source = "registry+https://github.com/rust-lang/crates.io-index" 654 | checksum = "0988d7fdf88d5e5fcf5923a0f1e8ab345f3e98ab4bc6bc45a2d5ff7f7458fbf6" 655 | dependencies = [ 656 | "parking_lot", 657 | ] 658 | 659 | [[package]] 660 | name = "scopeguard" 661 | version = "1.1.0" 662 | source = "registry+https://github.com/rust-lang/crates.io-index" 663 | checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" 664 | 665 | [[package]] 666 | name = "semver" 667 | version = "0.9.0" 668 | source = "registry+https://github.com/rust-lang/crates.io-index" 669 | checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" 670 | dependencies = [ 671 | "semver-parser", 672 | ] 673 | 674 | [[package]] 675 | name = "semver-parser" 676 | version = "0.7.0" 677 | source = "registry+https://github.com/rust-lang/crates.io-index" 678 | checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" 679 | 680 | [[package]] 681 | name = "serde" 682 | version = "0.8.23" 683 | source = "registry+https://github.com/rust-lang/crates.io-index" 684 | checksum = "9dad3f759919b92c3068c696c15c3d17238234498bbdcc80f2c469606f948ac8" 685 | 686 | [[package]] 687 | name = "serde" 688 | version = "1.0.105" 689 | source = "registry+https://github.com/rust-lang/crates.io-index" 690 | checksum = "e707fbbf255b8fc8c3b99abb91e7257a622caeb20a9818cbadbeeede4e0932ff" 691 | 692 | [[package]] 693 | name = "serde-hjson" 694 | version = "0.9.1" 695 | source = "registry+https://github.com/rust-lang/crates.io-index" 696 | checksum = "6a3a4e0ea8a88553209f6cc6cfe8724ecad22e1acf372793c27d995290fe74f8" 697 | dependencies = [ 698 | "lazy_static", 699 | "linked-hash-map 0.3.0", 700 | "num-traits 0.1.43", 701 | "regex", 702 | "serde 0.8.23", 703 | ] 704 | 705 | [[package]] 706 | name = "serde_json" 707 | version = "1.0.50" 708 | source = "registry+https://github.com/rust-lang/crates.io-index" 709 | checksum = "78a7a12c167809363ec3bd7329fc0a3369056996de43c4b37ef3cd54a6ce4867" 710 | dependencies = [ 711 | "itoa", 712 | "ryu", 713 | "serde 1.0.105", 714 | ] 715 | 716 | [[package]] 717 | name = "serde_test" 718 | version = "0.8.23" 719 | source = "registry+https://github.com/rust-lang/crates.io-index" 720 | checksum = "110b3dbdf8607ec493c22d5d947753282f3bae73c0f56d322af1e8c78e4c23d5" 721 | dependencies = [ 722 | "serde 0.8.23", 723 | ] 724 | 725 | [[package]] 726 | name = "sized-chunks" 727 | version = "0.5.3" 728 | source = "registry+https://github.com/rust-lang/crates.io-index" 729 | checksum = "d59044ea371ad781ff976f7b06480b9f0180e834eda94114f2afb4afc12b7718" 730 | dependencies = [ 731 | "bitmaps", 732 | "typenum", 733 | ] 734 | 735 | [[package]] 736 | name = "smallvec" 737 | version = "1.2.0" 738 | source = "registry+https://github.com/rust-lang/crates.io-index" 739 | checksum = "5c2fb2ec9bcd216a5b0d0ccf31ab17b5ed1d627960edff65bbe95d3ce221cefc" 740 | 741 | [[package]] 742 | name = "static_assertions" 743 | version = "0.3.4" 744 | source = "registry+https://github.com/rust-lang/crates.io-index" 745 | checksum = "7f3eb36b47e512f8f1c9e3d10c2c1965bc992bd9cdb024fa581e2194501c83d3" 746 | 747 | [[package]] 748 | name = "strsim" 749 | version = "0.8.0" 750 | source = "registry+https://github.com/rust-lang/crates.io-index" 751 | checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" 752 | 753 | [[package]] 754 | name = "structopt" 755 | version = "0.3.12" 756 | source = "registry+https://github.com/rust-lang/crates.io-index" 757 | checksum = "c8faa2719539bbe9d77869bfb15d4ee769f99525e707931452c97b693b3f159d" 758 | dependencies = [ 759 | "clap", 760 | "lazy_static", 761 | "structopt-derive", 762 | ] 763 | 764 | [[package]] 765 | name = "structopt-derive" 766 | version = "0.4.5" 767 | source = "registry+https://github.com/rust-lang/crates.io-index" 768 | checksum = "3f88b8e18c69496aad6f9ddf4630dd7d585bcaf765786cb415b9aec2fe5a0430" 769 | dependencies = [ 770 | "heck", 771 | "proc-macro-error", 772 | "proc-macro2", 773 | "quote", 774 | "syn", 775 | ] 776 | 777 | [[package]] 778 | name = "syn" 779 | version = "1.0.17" 780 | source = "registry+https://github.com/rust-lang/crates.io-index" 781 | checksum = "0df0eb663f387145cab623dea85b09c2c5b4b0aef44e945d928e682fce71bb03" 782 | dependencies = [ 783 | "proc-macro2", 784 | "quote", 785 | "unicode-xid", 786 | ] 787 | 788 | [[package]] 789 | name = "syn-mid" 790 | version = "0.5.0" 791 | source = "registry+https://github.com/rust-lang/crates.io-index" 792 | checksum = "7be3539f6c128a931cf19dcee741c1af532c7fd387baa739c03dd2e96479338a" 793 | dependencies = [ 794 | "proc-macro2", 795 | "quote", 796 | "syn", 797 | ] 798 | 799 | [[package]] 800 | name = "textwrap" 801 | version = "0.11.0" 802 | source = "registry+https://github.com/rust-lang/crates.io-index" 803 | checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" 804 | dependencies = [ 805 | "unicode-width", 806 | ] 807 | 808 | [[package]] 809 | name = "thiserror" 810 | version = "1.0.13" 811 | source = "registry+https://github.com/rust-lang/crates.io-index" 812 | checksum = "e3711fd1c4e75b3eff12ba5c40dba762b6b65c5476e8174c1a664772060c49bf" 813 | dependencies = [ 814 | "thiserror-impl", 815 | ] 816 | 817 | [[package]] 818 | name = "thiserror-impl" 819 | version = "1.0.13" 820 | source = "registry+https://github.com/rust-lang/crates.io-index" 821 | checksum = "ae2b85ba4c9aa32dd3343bd80eb8d22e9b54b7688c17ea3907f236885353b233" 822 | dependencies = [ 823 | "proc-macro2", 824 | "quote", 825 | "syn", 826 | ] 827 | 828 | [[package]] 829 | name = "thread_local" 830 | version = "1.0.1" 831 | source = "registry+https://github.com/rust-lang/crates.io-index" 832 | checksum = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14" 833 | dependencies = [ 834 | "lazy_static", 835 | ] 836 | 837 | [[package]] 838 | name = "toml" 839 | version = "0.5.6" 840 | source = "registry+https://github.com/rust-lang/crates.io-index" 841 | checksum = "ffc92d160b1eef40665be3a05630d003936a3bc7da7421277846c2613e92c71a" 842 | dependencies = [ 843 | "serde 1.0.105", 844 | ] 845 | 846 | [[package]] 847 | name = "treeline" 848 | version = "0.1.0" 849 | source = "registry+https://github.com/rust-lang/crates.io-index" 850 | checksum = "a7f741b240f1a48843f9b8e0444fb55fb2a4ff67293b50a9179dfd5ea67f8d41" 851 | 852 | [[package]] 853 | name = "typenum" 854 | version = "1.11.2" 855 | source = "registry+https://github.com/rust-lang/crates.io-index" 856 | checksum = "6d2783fe2d6b8c1101136184eb41be8b1ad379e4657050b8aaff0c79ee7575f9" 857 | 858 | [[package]] 859 | name = "unicode-segmentation" 860 | version = "1.6.0" 861 | source = "registry+https://github.com/rust-lang/crates.io-index" 862 | checksum = "e83e153d1053cbb5a118eeff7fd5be06ed99153f00dbcd8ae310c5fb2b22edc0" 863 | 864 | [[package]] 865 | name = "unicode-width" 866 | version = "0.1.7" 867 | source = "registry+https://github.com/rust-lang/crates.io-index" 868 | checksum = "caaa9d531767d1ff2150b9332433f32a24622147e5ebb1f26409d5da67afd479" 869 | 870 | [[package]] 871 | name = "unicode-xid" 872 | version = "0.2.0" 873 | source = "registry+https://github.com/rust-lang/crates.io-index" 874 | checksum = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c" 875 | 876 | [[package]] 877 | name = "uuid" 878 | version = "0.8.1" 879 | source = "registry+https://github.com/rust-lang/crates.io-index" 880 | checksum = "9fde2f6a4bea1d6e007c4ad38c6839fa71cbb63b6dbf5b595aa38dc9b1093c11" 881 | dependencies = [ 882 | "rand", 883 | ] 884 | 885 | [[package]] 886 | name = "vcpkg" 887 | version = "0.2.8" 888 | source = "registry+https://github.com/rust-lang/crates.io-index" 889 | checksum = "3fc439f2794e98976c88a2a2dafce96b930fe8010b0a256b3c2199a773933168" 890 | 891 | [[package]] 892 | name = "vec_map" 893 | version = "0.8.1" 894 | source = "registry+https://github.com/rust-lang/crates.io-index" 895 | checksum = "05c78687fb1a80548ae3250346c3db86a80a7cdd77bda190189f2d0a0987c81a" 896 | 897 | [[package]] 898 | name = "version_check" 899 | version = "0.9.1" 900 | source = "registry+https://github.com/rust-lang/crates.io-index" 901 | checksum = "078775d0255232fb988e6fccf26ddc9d1ac274299aaedcedce21c6f72cc533ce" 902 | 903 | [[package]] 904 | name = "wasi" 905 | version = "0.9.0+wasi-snapshot-preview1" 906 | source = "registry+https://github.com/rust-lang/crates.io-index" 907 | checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" 908 | 909 | [[package]] 910 | name = "winapi" 911 | version = "0.3.8" 912 | source = "registry+https://github.com/rust-lang/crates.io-index" 913 | checksum = "8093091eeb260906a183e6ae1abdba2ef5ef2257a21801128899c3fc699229c6" 914 | dependencies = [ 915 | "winapi-i686-pc-windows-gnu", 916 | "winapi-x86_64-pc-windows-gnu", 917 | ] 918 | 919 | [[package]] 920 | name = "winapi-i686-pc-windows-gnu" 921 | version = "0.4.0" 922 | source = "registry+https://github.com/rust-lang/crates.io-index" 923 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 924 | 925 | [[package]] 926 | name = "winapi-x86_64-pc-windows-gnu" 927 | version = "0.4.0" 928 | source = "registry+https://github.com/rust-lang/crates.io-index" 929 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 930 | 931 | [[package]] 932 | name = "yaml-rust" 933 | version = "0.4.3" 934 | source = "registry+https://github.com/rust-lang/crates.io-index" 935 | checksum = "65923dd1784f44da1d2c3dbbc5e822045628c590ba72123e1c73d3c230c4434d" 936 | dependencies = [ 937 | "linked-hash-map 0.5.2", 938 | ] 939 | --------------------------------------------------------------------------------