From b4ea9d69ca3b191f2c1f267f28e58a37f005619b Mon Sep 17 00:00:00 2001 From: Timo Borer Date: Mon, 22 Sep 2025 20:14:37 +0200 Subject: [PATCH 01/20] feat: Split crate into crane_bricks and crane_cli This change was done so bricks can easily be used a library without needing the crane config etc. I also started to work on integration tests for the lib, as well as better documented structs and enums. --- Cargo.lock | 171 +++++++++++++++--- Cargo.toml | 20 +- crane_bricks/Cargo.toml | 14 ++ crane_bricks/src/actions/common.rs | 26 +++ crane_bricks/src/actions/insert_file.rs | 85 +++++++++ crane_bricks/src/actions/mod.rs | 30 +++ crane_bricks/src/actions/modify_file.rs | 70 +++++++ src/bricks.rs => crane_bricks/src/brick.rs | 73 +++++--- crane_bricks/src/context.rs | 10 + crane_bricks/src/file_utils.rs | 78 ++++++++ crane_bricks/src/lib.rs | 4 + .../tests/bricks/insert_no_config/TEST_B | 1 + .../tests/bricks/insert_with_config/TEST_A | 1 + .../bricks/insert_with_config/brick.toml | 5 + crane_bricks/tests/integration_test.rs | 63 +++++++ crane_cli/Cargo.toml | 15 ++ {src => crane_cli/src}/cmd/add.rs | 11 +- {src => crane_cli/src}/cmd/cmd.rs | 0 {src => crane_cli/src}/cmd/list.rs | 2 +- {src => crane_cli/src}/cmd/mod.rs | 0 {src => crane_cli/src}/config.rs | 0 {src => crane_cli/src}/main.rs | 2 - {src => crane_cli/src}/utils.rs | 0 rustfmt.toml | 1 + src/files.rs | 119 ------------ 25 files changed, 605 insertions(+), 196 deletions(-) create mode 100644 crane_bricks/Cargo.toml create mode 100644 crane_bricks/src/actions/common.rs create mode 100644 crane_bricks/src/actions/insert_file.rs create mode 100644 crane_bricks/src/actions/mod.rs create mode 100644 crane_bricks/src/actions/modify_file.rs rename src/bricks.rs => crane_bricks/src/brick.rs (57%) create mode 100644 crane_bricks/src/context.rs create mode 100644 crane_bricks/src/file_utils.rs create mode 100644 crane_bricks/src/lib.rs create mode 100644 crane_bricks/tests/bricks/insert_no_config/TEST_B create mode 100644 crane_bricks/tests/bricks/insert_with_config/TEST_A create mode 100644 crane_bricks/tests/bricks/insert_with_config/brick.toml create mode 100644 crane_bricks/tests/integration_test.rs create mode 100644 crane_cli/Cargo.toml rename {src => crane_cli/src}/cmd/add.rs (91%) rename {src => crane_cli/src}/cmd/cmd.rs (100%) rename {src => crane_cli/src}/cmd/list.rs (90%) rename {src => crane_cli/src}/cmd/mod.rs (100%) rename {src => crane_cli/src}/config.rs (100%) rename {src => crane_cli/src}/main.rs (91%) rename {src => crane_cli/src}/utils.rs (100%) create mode 100644 rustfmt.toml delete mode 100644 src/files.rs diff --git a/Cargo.lock b/Cargo.lock index eb60a8e..1fb7e1b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -67,6 +67,12 @@ version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +[[package]] +name = "bitflags" +version = "2.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" + [[package]] name = "cfg-if" version = "1.0.3" @@ -75,9 +81,9 @@ checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" [[package]] name = "clap" -version = "4.5.47" +version = "4.5.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7eac00902d9d136acd712710d71823fb8ac8004ca445a89e73a41d45aa712931" +checksum = "e2134bb3ea021b78629caa971416385309e0131b351b25e01dc16fb54e1b5fae" dependencies = [ "clap_builder", "clap_derive", @@ -96,9 +102,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.47" +version = "4.5.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ad9bbf750e73b5884fb8a211a9424a1906c1e156724260fdae972f31d70e1d6" +checksum = "c2ba64afa3c0a6df7fa517765e31314e983f51dda798ffba27b988194fb65dc9" dependencies = [ "anstream", "anstyle", @@ -131,12 +137,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" [[package]] -name = "crane" +name = "crane_bricks" +version = "0.1.0" +dependencies = [ + "anyhow", + "env_logger", + "log", + "serde", + "tempfile", + "toml", +] + +[[package]] +name = "crane_cli" version = "0.1.0" dependencies = [ "anyhow", "clap", "clap-verbosity", + "crane_bricks", "env_logger", "fuzzy-matcher", "log", @@ -173,6 +192,22 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + [[package]] name = "fuzzy-matcher" version = "0.3.7" @@ -182,11 +217,23 @@ dependencies = [ "thread_local", ] +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi", +] + [[package]] name = "hashbrown" -version = "0.15.5" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" [[package]] name = "heck" @@ -196,9 +243,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "indexmap" -version = "2.11.3" +version = "2.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92119844f513ffa41556430369ab02c295a3578af21cf945caa3e9e0c2481ac3" +checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" dependencies = [ "equivalent", "hashbrown", @@ -234,6 +281,18 @@ dependencies = [ "syn", ] +[[package]] +name = "libc" +version = "0.2.175" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" + +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + [[package]] name = "log" version = "0.4.28" @@ -246,6 +305,12 @@ version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + [[package]] name = "once_cell_polyfill" version = "1.70.1" @@ -285,6 +350,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "regex" version = "1.11.2" @@ -314,11 +385,24 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" +[[package]] +name = "rustix" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + [[package]] name = "serde" -version = "1.0.225" +version = "1.0.226" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd6c24dee235d0da097043389623fb913daddf92c76e9f5a1db88607a0bcbd1d" +checksum = "0dca6411025b24b60bfa7ec1fe1f8e710ac09782dca409ee8237ba74b51295fd" dependencies = [ "serde_core", "serde_derive", @@ -326,18 +410,18 @@ dependencies = [ [[package]] name = "serde_core" -version = "1.0.225" +version = "1.0.226" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "659356f9a0cb1e529b24c01e43ad2bdf520ec4ceaf83047b83ddcc2251f96383" +checksum = "ba2ba63999edb9dac981fb34b3e5c0d111a69b0924e253ed29d83f7c99e966a4" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.225" +version = "1.0.226" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ea936adf78b1f766949a4977b91d2f5595825bd6ec079aa9543ad2685fc4516" +checksum = "8db53ae22f34573731bafa1db20f04027b2d25e02d8205921b569171699cdb33" dependencies = [ "proc-macro2", "quote", @@ -346,9 +430,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2789234a13a53fc4be1b51ea1bab45a3c338bdb884862a257d10e5a74ae009e6" +checksum = "5417783452c2be558477e104686f7de5dae53dba813c28435e0e70f82d9b04ee" dependencies = [ "serde_core", ] @@ -370,6 +454,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tempfile" +version = "3.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84fa4d11fadde498443cca10fd3ac23c951f0dc59e080e9f4b93d4df4e4eea53" +dependencies = [ + "fastrand", + "getrandom", + "once_cell", + "rustix", + "windows-sys", +] + [[package]] name = "thread_local" version = "1.1.9" @@ -381,9 +478,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.9.6" +version = "0.9.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae2a4cf385da23d1d53bc15cdfa5c2109e93d8d362393c801e87da2f72f0e201" +checksum = "00e5e5d9bf2475ac9d4f0d9edab68cc573dc2fd644b0dba36b0c30a92dd9eaa0" dependencies = [ "indexmap", "serde_core", @@ -396,27 +493,27 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a197c0ec7d131bfc6f7e82c8442ba1595aeab35da7adbf05b6b73cd06a16b6be" +checksum = "32f1085dec27c2b6632b04c80b3bb1b4300d6495d1e129693bdda7d91e72eec1" dependencies = [ "serde_core", ] [[package]] name = "toml_parser" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b551886f449aa90d4fe2bdaa9f4a2577ad2dde302c61ecf262d80b116db95c10" +checksum = "4cf893c33be71572e0e9aa6dd15e6677937abd686b066eac3f8cd3531688a627" dependencies = [ "winnow", ] [[package]] name = "toml_writer" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcc842091f2def52017664b53082ecbbeb5c7731092bad69d2c63050401dfd64" +checksum = "d163a63c116ce562a22cda521fcc4d79152e7aba014456fb5eb442f6d6a10109" [[package]] name = "unicode-ident" @@ -430,6 +527,24 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "wasi" +version = "0.14.7+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" +dependencies = [ + "wasip2", +] + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + [[package]] name = "windows-link" version = "0.1.3" @@ -515,3 +630,9 @@ name = "winnow" version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" diff --git a/Cargo.toml b/Cargo.toml index 1e1bd2f..4a51360 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,14 +1,6 @@ -[package] -name = "crane" -version = "0.1.0" -edition = "2024" - -[dependencies] -serde = { version = "1.0", features = ["derive"] } -clap = { version = "4.5.47", features = ["derive"] } -fuzzy-matcher = "0.3.7" -anyhow = "1.0.99" -toml = "0.9.6" -clap-verbosity = "2.1.0" -env_logger = "0.11.8" -log = "0.4.28" +[workspace] +resolver = "3" +members = [ + "crane_bricks", + "crane_cli" +] \ No newline at end of file diff --git a/crane_bricks/Cargo.toml b/crane_bricks/Cargo.toml new file mode 100644 index 0000000..a7bf667 --- /dev/null +++ b/crane_bricks/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "crane_bricks" +version = "0.1.0" +edition = "2024" + +[dependencies] +serde = { version = "1.0", features = ["derive"] } +toml = "0.9.6" +anyhow = "1.0.99" +log = "0.4.28" + +[dev-dependencies] +tempfile = "3" +env_logger = "0.11.8" diff --git a/crane_bricks/src/actions/common.rs b/crane_bricks/src/actions/common.rs new file mode 100644 index 0000000..2d26d07 --- /dev/null +++ b/crane_bricks/src/actions/common.rs @@ -0,0 +1,26 @@ +use serde::Deserialize; + +#[derive(Debug, Deserialize, Default, Clone, PartialEq, Eq)] +pub struct Common { + /// Relative path from where you run crane to where the files should go + /// + /// ```toml + /// [[actions]] + /// working_dir = "./src/" + /// ``` + #[serde(default)] + pub working_dir: Option, + + /// List of paths including for which files the action should run + /// Empty means all files (except config file) will be included + /// + /// ```toml + /// [[actions]] + /// sources = [ "README.md", "LICENSE" ] + /// + /// # Or regex + /// sources = [ "re:.+\.md", "LICENSE"] + /// ``` + #[serde(default)] + pub sources: Vec +} diff --git a/crane_bricks/src/actions/insert_file.rs b/crane_bricks/src/actions/insert_file.rs new file mode 100644 index 0000000..3c5d0a1 --- /dev/null +++ b/crane_bricks/src/actions/insert_file.rs @@ -0,0 +1,85 @@ +use std::path::Path; + +use log::debug; +use serde::Deserialize; + +use crate::{ + actions::{ExecuteAction, common::Common}, + brick::Brick, + context::ActionContext, + file_utils::{file_append_content, file_create_new, file_replace_content}, +}; + +/// Creates a new file. +/// +/// ## Example +/// +/// ### Config +/// +/// ```toml +/// [[actions]] +/// sources = [ +/// "LICENSE" +/// ] +/// if_file_exists = "replace" +/// ``` +/// +/// ### Result +/// +/// Will create the LICENSE file. If it already exists, it replaces it. +#[derive(Debug, Deserialize, Clone, PartialEq, Eq)] +pub struct InsertFileAction { + #[serde(flatten)] + pub common: Common, + + /// Define what happens if the file already exists + #[serde(default)] + pub if_file_exists: FileExistsAction, +} + +#[derive(Debug, Deserialize, Clone, Default, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum FileExistsAction { + #[default] + Append, + Replace, + Pass, +} + +impl ExecuteAction for InsertFileAction { + fn execute( + &self, + context: &ActionContext, + brick: &Brick, + cwd: &Path, + ) -> anyhow::Result<()> { + let mut files = brick.files(); + if !&self.common.sources.is_empty() { + files = files + .into_iter() + .filter(|file| { + *&self.common.sources.contains(&file.name().to_string()) + }) + .collect(); + } + debug!("{} executing for {} files", brick.name(), files.len()); + for file in files { + let target_path = cwd.join(file.name()); + let content = file.content().to_string(); + if !target_path.exists() { + file_create_new(context, &target_path, Some(content))?; + continue; + } + match &self.if_file_exists { + FileExistsAction::Append => { + file_append_content(context, &target_path, &content)? + } + FileExistsAction::Replace => { + file_replace_content(context, &target_path, &content)? + } + FileExistsAction::Pass => continue, + } + } + Ok(()) + } +} diff --git a/crane_bricks/src/actions/mod.rs b/crane_bricks/src/actions/mod.rs new file mode 100644 index 0000000..7266024 --- /dev/null +++ b/crane_bricks/src/actions/mod.rs @@ -0,0 +1,30 @@ + +pub mod common; +pub mod insert_file; +pub mod modify_file; + +use std::path::Path; + +use serde::Deserialize; + +use crate::{actions::{insert_file::InsertFileAction, modify_file::ModifyFileAction}, brick::Brick, context::ActionContext}; + +pub trait ExecuteAction { + fn execute(&self, context: &ActionContext, brick: &Brick, cwd: &Path) -> anyhow::Result<()>; +} + +#[derive(Debug, Deserialize, Clone, PartialEq, Eq)] +#[serde(tag = "action", rename_all = "snake_case")] +pub enum Action { + InsertFile(InsertFileAction), + ModifyFile(ModifyFileAction) +} + +impl ExecuteAction for Action { + fn execute(&self, context: &ActionContext, brick: &Brick, cwd: &Path) -> anyhow::Result<()> { + match &self { + Action::InsertFile(action) => action.execute(context, brick, cwd), + Action::ModifyFile(action) => todo!(), + } + } +} \ No newline at end of file diff --git a/crane_bricks/src/actions/modify_file.rs b/crane_bricks/src/actions/modify_file.rs new file mode 100644 index 0000000..b53a9fb --- /dev/null +++ b/crane_bricks/src/actions/modify_file.rs @@ -0,0 +1,70 @@ +use serde::Deserialize; + +use crate::actions::common::Common; + +/// Modify a file by inserting content at a specific location. +/// +/// ## Example +/// +/// ### Config +/// +/// ```toml +/// [[actions]] +/// # For this action, the name of the files that the modification +/// # will apply +/// sources = [ +/// "Cargo.toml" +/// ] +/// type = "append" +/// content = "\nserde = \"1\"" +/// selector = "[dependencies]" +/// location = "after" +/// ``` +/// +/// ### Result +/// +/// ```toml +/// # Before +/// [dependencies] +/// crane = "9.9.9" +/// +/// # After +/// [dependencies] +/// serde = "1" +/// crane = "9.9.9" +/// ``` +#[derive(Debug, Deserialize, Default, Clone, PartialEq, Eq)] +pub struct ModifyFileAction { + #[serde(flatten)] + pub common: Common, + + /// If the modification should append something next to the + /// selector or if it should replace it. + pub r#type: ModifyType, + + pub content: Option, + + /// The content selector for the modification, must be unique. + /// Can be regex if prefix with "re:". + pub selector: Option, + + /// If the modification should happen "before" or "after" (default) + /// the given selector. + pub location: ModifyLocation, +} + +#[derive(Debug, Deserialize, Default, Clone, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +enum ModifyType { + #[default] + Append, + Replace, +} + +#[derive(Debug, Deserialize, Default, Clone, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +enum ModifyLocation { + #[default] + After, + Before, +} diff --git a/src/bricks.rs b/crane_bricks/src/brick.rs similarity index 57% rename from src/bricks.rs rename to crane_bricks/src/brick.rs index 2d87656..d3e1d95 100644 --- a/src/bricks.rs +++ b/crane_bricks/src/brick.rs @@ -1,61 +1,78 @@ -use std::{fs, path::PathBuf}; +use std::{fs, path::{Path, PathBuf}}; use anyhow::anyhow; -use serde::{Deserialize, Serialize}; +use log::debug; +use serde::Deserialize; use crate::{ - files::BrickFile, - utils::{sub_dirs, sub_paths}, + actions::{Action, ExecuteAction}, context::ActionContext, file_utils::{sub_dirs, sub_paths} }; const BRICK_CONFIG_FILE: &'static str = "brick.toml"; -#[derive(Serialize, Deserialize, Debug, Clone, Copy, Default)] -pub enum FileAction { - #[default] - Replace, - Append, - // Regex { - // regex: String, - // position: After | Replace | Before, - // } -} - -#[derive(Serialize, Deserialize, Debug, Clone, Default)] +#[derive(Deserialize, Debug, Clone, Default, PartialEq, Eq)] pub struct BrickConfig { pub name: String, - #[serde(default)] - pub action: FileAction, - // pub requires: Vec + pub actions: Vec, +} + +#[derive(Debug, Clone)] +pub struct BrickFile { + name: String, + content: String, +} + +impl BrickFile { + pub fn new(name: String, content: String) -> Self { + Self { + name, + content, + } + } + + pub fn name(&self) -> &str { + &self.name + } + + pub fn content(&self) -> &str { + &self.content + } } #[derive(Debug, Clone)] pub struct Brick { config: BrickConfig, - path: PathBuf, + source_path: PathBuf, } impl Brick { - pub fn new(name: String, path: PathBuf) -> Self { + pub fn new(name: String, source_path: PathBuf) -> Self { Brick { config: BrickConfig { name, ..BrickConfig::default() }, - path, + source_path, } } + + pub fn new_with_config(config: BrickConfig, source_path: PathBuf) -> Self { + Brick { config, source_path } + } pub fn name(&self) -> &str { &self.config.name } pub fn path(&self) -> &PathBuf { - &self.path + &self.source_path } - pub fn action(&self) -> &FileAction { - &self.config.action + pub fn execute(&self, context: &ActionContext, cwd: &Path) -> anyhow::Result<()> { + for action in &self.config.actions { + action.execute(context, &self, cwd)?; + } + Ok(()) } /// Returns a list of all files that @@ -72,7 +89,7 @@ impl Brick { } let content = fs::read_to_string(path).unwrap_or_default(); - Some(BrickFile::new(name, content, self.action().clone())) + Some(BrickFile::new(name, content)) }) .collect() } @@ -84,14 +101,16 @@ impl TryFrom for Brick { fn try_from(value: PathBuf) -> Result { let config_file = value.join(BRICK_CONFIG_FILE); if !config_file.exists() { + debug!("Brick config file not found at '{:?}'", config_file.display()); let name = value .as_path() .file_name() .ok_or_else(|| anyhow!("Could not read brick dir name!"))?; return Ok(Brick::new(name.display().to_string(), value)); } + debug!("Creating Brick from config file"); let config: BrickConfig = toml::from_str(fs::read_to_string(config_file)?.as_str())?; - Ok(Brick::new(config.name, value)) + Ok(Brick::new_with_config(config, value)) } } diff --git a/crane_bricks/src/context.rs b/crane_bricks/src/context.rs new file mode 100644 index 0000000..29e8e7c --- /dev/null +++ b/crane_bricks/src/context.rs @@ -0,0 +1,10 @@ + +pub struct ActionContext { + pub dry_run: bool, +} + +impl ActionContext { + pub fn new(dry_run: bool) -> Self { + Self { dry_run } + } +} diff --git a/crane_bricks/src/file_utils.rs b/crane_bricks/src/file_utils.rs new file mode 100644 index 0000000..cc1b372 --- /dev/null +++ b/crane_bricks/src/file_utils.rs @@ -0,0 +1,78 @@ +use std::{ + fs::{self, File}, + io::{self, Write}, + path::{Path, PathBuf}, +}; + +use anyhow::anyhow; +use log::debug; + +use crate::context::ActionContext; + +pub fn sub_dirs(dir: &Path) -> anyhow::Result> { + Ok(sub_paths(dir)? + .into_iter() + .filter(|path| path.is_dir()) + .collect::>()) +} + +/// Get a vec of all files and folders in the given dir if valid +pub fn sub_paths(dir: &Path) -> anyhow::Result> { + if !dir.exists() || !dir.is_dir() { + return Err(anyhow!("Target does not exist or not a directory")); + } + let dirs = dir.read_dir()?; + Ok(dirs + .filter_map(|entry_res| Some(entry_res.ok()?.path())) + .collect()) +} + +pub fn file_create_new(ctx: &ActionContext, path: &Path, content: Option) -> anyhow::Result<()> { + if !ctx.dry_run { + debug!("Creating new file '{:?}'", path); + let mut file = File::create_new(path)?; + file.write(content.unwrap_or_default().as_bytes())?; + } + Ok(()) +} + +pub fn file_read_content(ctx: &ActionContext, path: &Path) -> anyhow::Result { + if ctx.dry_run && !path.exists() { + return Ok(String::new()); + } + if !path.exists() { + return Err(anyhow::Error::new(io::Error::new( + io::ErrorKind::NotFound, + "Target file not found", + ))); + } + debug!("Reading content of file"); + Ok(fs::read_to_string(path).unwrap_or_default()) +} + +pub fn file_replace_content( + ctx: &ActionContext, + path: &Path, + content: &String, +) -> anyhow::Result<()> { + debug!("Replacing contents of '{:?}'", path.display()); + if ctx.dry_run { + return Ok(()); + } + let mut file = File::options().write(true).create(true).open(&path)?; + file.write(content.as_bytes())?; + Ok(()) +} + +pub fn file_append_content( + ctx: &ActionContext, + path: &Path, + content: &String, +) -> anyhow::Result<()> { + if ctx.dry_run { + return Ok(()); + } + let mut file = File::options().append(true).create(true).open(&path)?; + file.write(content.as_bytes())?; + Ok(()) +} diff --git a/crane_bricks/src/lib.rs b/crane_bricks/src/lib.rs new file mode 100644 index 0000000..dc9e2a5 --- /dev/null +++ b/crane_bricks/src/lib.rs @@ -0,0 +1,4 @@ +pub mod actions; +pub mod context; +pub mod brick; +pub mod file_utils; \ No newline at end of file diff --git a/crane_bricks/tests/bricks/insert_no_config/TEST_B b/crane_bricks/tests/bricks/insert_no_config/TEST_B new file mode 100644 index 0000000..5e1c309 --- /dev/null +++ b/crane_bricks/tests/bricks/insert_no_config/TEST_B @@ -0,0 +1 @@ +Hello World \ No newline at end of file diff --git a/crane_bricks/tests/bricks/insert_with_config/TEST_A b/crane_bricks/tests/bricks/insert_with_config/TEST_A new file mode 100644 index 0000000..3b12464 --- /dev/null +++ b/crane_bricks/tests/bricks/insert_with_config/TEST_A @@ -0,0 +1 @@ +TEST \ No newline at end of file diff --git a/crane_bricks/tests/bricks/insert_with_config/brick.toml b/crane_bricks/tests/bricks/insert_with_config/brick.toml new file mode 100644 index 0000000..7aee887 --- /dev/null +++ b/crane_bricks/tests/bricks/insert_with_config/brick.toml @@ -0,0 +1,5 @@ +name = "test" + +[[actions]] +action = "insert_file" +if_file_exists = "replace" diff --git a/crane_bricks/tests/integration_test.rs b/crane_bricks/tests/integration_test.rs new file mode 100644 index 0000000..beb3090 --- /dev/null +++ b/crane_bricks/tests/integration_test.rs @@ -0,0 +1,63 @@ +use std::{fs, path::PathBuf, vec}; + +use crane_bricks::{ + actions::{common::Common, insert_file::{FileExistsAction, InsertFileAction}, Action}, + brick::{Brick, BrickConfig}, + context::ActionContext, +}; +use log::debug; + +fn init_logger() { + let _ = env_logger::builder() + // Include all events in tests + .filter_level(log::LevelFilter::max()) + // Ensure events are captured by `cargo test` + .is_test(true) + // Ignore errors initializing the logger if tests race to configure it + .try_init(); +} + +fn test_dir() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests") +} + +#[test] +fn test_actions_parse() { + init_logger(); + + let config = r#" +name = "hi" + +[[actions]] +action = "insert_file" + +"#; + let config_parsed: BrickConfig = toml::from_str(config).unwrap(); + let config: BrickConfig = BrickConfig { + name: String::from("hi"), + actions: vec![Action::InsertFile(InsertFileAction { + common: Common::default(), + if_file_exists: FileExistsAction::Append, + })], + }; + assert_eq!(config_parsed, config); +} + +#[test] +fn test_insert_file() { + init_logger(); + + + + let brick = + Brick::try_from(test_dir().join("bricks/insert_with_config/")).unwrap(); + + debug!("{:?}", brick); + let ctx = ActionContext { dry_run: false }; + let tmpdir = tempfile::tempdir().unwrap(); + let res = brick.execute(&ctx, tmpdir.path()); + assert!(res.is_ok()); + assert!(tmpdir.path().join("TEST_A").exists()); + assert!(!tmpdir.path().join("TEST_B").exists()); + assert!(!tmpdir.path().join("brick.toml").exists()); +} diff --git a/crane_cli/Cargo.toml b/crane_cli/Cargo.toml new file mode 100644 index 0000000..b188a84 --- /dev/null +++ b/crane_cli/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "crane_cli" +version = "0.1.0" +edition = "2024" + +[dependencies] +serde = { version = "1.0", features = ["derive"] } +toml = "0.9.6" +clap = { version = "4.5.47", features = ["derive"] } +fuzzy-matcher = "0.3.7" +anyhow = "1.0.99" +clap-verbosity = "2.1.0" +env_logger = "0.11.8" +log = "0.4.28" +crane_bricks = { path = "../crane_bricks/" } diff --git a/src/cmd/add.rs b/crane_cli/src/cmd/add.rs similarity index 91% rename from src/cmd/add.rs rename to crane_cli/src/cmd/add.rs index 16145b9..959f18c 100644 --- a/src/cmd/add.rs +++ b/crane_cli/src/cmd/add.rs @@ -3,8 +3,8 @@ use std::{env, path::PathBuf}; use fuzzy_matcher::{FuzzyMatcher, skim::SkimMatcherV2}; use log::{debug, error, info, warn}; +use crane_bricks::brick::{Brick, bricks}; use crate::{ - bricks::{Brick, bricks}, cmd::{Add, Run}, config::CraneConfig, }; @@ -71,13 +71,8 @@ impl Run for Add { } fn add_brick(brick: Brick, target_dir: &PathBuf, dry_run: bool) { - info!("Adding brick '{}'", brick.name()); - for file in brick.files() { - match file.create(target_dir.clone(), dry_run) { - Ok(_) => {} - Err(err) => error!("{}", err), - } - } + info!("Adding brick '{}', {}, {:?}", brick.name(), dry_run, target_dir); + } fn no_matches_found(query: String) { diff --git a/src/cmd/cmd.rs b/crane_cli/src/cmd/cmd.rs similarity index 100% rename from src/cmd/cmd.rs rename to crane_cli/src/cmd/cmd.rs diff --git a/src/cmd/list.rs b/crane_cli/src/cmd/list.rs similarity index 90% rename from src/cmd/list.rs rename to crane_cli/src/cmd/list.rs index 7971334..68a22c4 100644 --- a/src/cmd/list.rs +++ b/crane_cli/src/cmd/list.rs @@ -1,7 +1,7 @@ use log::info; +use crane_bricks::brick::bricks; use crate::{ - bricks::bricks, cmd::{List, Run}, }; diff --git a/src/cmd/mod.rs b/crane_cli/src/cmd/mod.rs similarity index 100% rename from src/cmd/mod.rs rename to crane_cli/src/cmd/mod.rs diff --git a/src/config.rs b/crane_cli/src/config.rs similarity index 100% rename from src/config.rs rename to crane_cli/src/config.rs diff --git a/src/main.rs b/crane_cli/src/main.rs similarity index 91% rename from src/main.rs rename to crane_cli/src/main.rs index 1080d50..6195fc7 100644 --- a/src/main.rs +++ b/crane_cli/src/main.rs @@ -2,10 +2,8 @@ use clap::Parser; use crate::cmd::{CraneCli, Run}; -mod bricks; mod cmd; mod config; -mod files; mod utils; fn main() { diff --git a/src/utils.rs b/crane_cli/src/utils.rs similarity index 100% rename from src/utils.rs rename to crane_cli/src/utils.rs diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..5c8d931 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1 @@ +max_width = 80 \ No newline at end of file diff --git a/src/files.rs b/src/files.rs deleted file mode 100644 index bcfbced..0000000 --- a/src/files.rs +++ /dev/null @@ -1,119 +0,0 @@ -use std::{ - fs::{self, File}, - io::{self, Read, Write}, - path::{Path, PathBuf}, -}; - -use anyhow::Ok; -use log::{debug, info}; - -use crate::bricks::FileAction; - -/// The object that contains all information except the target location -/// to execute a brick -#[derive(Debug, Clone)] -pub struct BrickFile { - name: String, - content: String, - action: FileAction, -} - -struct FileUtility { - dry_run: bool, -} - -impl FileUtility { - pub fn new(dry_run: bool) -> Self { - Self { dry_run } - } - - pub fn create_new(&self, path: &Path) -> anyhow::Result<()> { - if !self.dry_run { - File::create_new(path)?; - } - Ok(()) - } - - pub fn read_content(&self, path: &Path) -> anyhow::Result { - if self.dry_run && !path.exists() { - return Ok(String::new()); - } - if !path.exists() { - return Err(anyhow::Error::new(io::Error::new( - io::ErrorKind::NotFound, - "Target file not found", - ))); - } - debug!("Reading content of file"); - Ok(fs::read_to_string(path).unwrap_or_default()) - } - - pub fn replace_content(&self, path: &Path, content: &String) -> anyhow::Result<()> { - debug!("Replacing contents of '{:?}'", path.display()); - if self.dry_run { - return Ok(()); - } - let mut file = File::options().write(true).create(true).open(&path)?; - file.write(content.as_bytes())?; - Ok(()) - } - pub fn append_content(&self, path: &Path, content: &String) -> anyhow::Result<()> { - if self.dry_run { - return Ok(()); - } - let mut file = File::options().append(true).create(true).open(&path)?; - file.write(content.as_bytes())?; - Ok(()) - } -} - -impl BrickFile { - pub fn new(name: String, content: String, action: FileAction) -> Self { - Self { - name, - content, - action, - } - } - - /// Check if this file can be executed without the target - /// file already existing - fn needs_existing_file(&self) -> bool { - match &self.action { - FileAction::Replace | FileAction::Append => false, - } - } - - fn parse_content(&self) -> &String { - &self.content - } - - // TODO: Create with some kind of context? - pub fn create(&self, path: PathBuf, dry_run: bool) -> anyhow::Result<()> { - let file_util = FileUtility::new(dry_run); - let target_file = path.join(&self.name); - if !target_file.exists() { - if self.needs_existing_file() { - return Err(anyhow::Error::msg("File does not exist")); - } - info!( - "Target file does not exist, creating it at '{:?}'", - target_file - ); - file_util.create_new(&target_file).unwrap(); - } - - // let content = file_util.read_content(&path).unwrap_or_default(); - - match self.action { - FileAction::Replace => { - file_util.replace_content(&target_file, &self.parse_content())?; - } - FileAction::Append => { - file_util.append_content(&target_file, &self.parse_content())?; - } - }; - - Ok(()) - } -} From ec72dd95f899370217e8e82c66f0e4dd3c933e43 Mon Sep 17 00:00:00 2001 From: Timo Borer Date: Mon, 22 Sep 2025 20:15:59 +0200 Subject: [PATCH 02/20] docs: Add basic mdbook I needed to define what crane and bricks really is, so I wrote parts of the documentation for it. This is not accurate right now, but I'll commit it anyway so I have it just in case. --- book/.gitignore | 1 + book/book.toml | 5 ++ book/src/SUMMARY.md | 9 +++ book/src/configuration/README.md | 1 + .../src/configuration/brick_actions/README.md | 66 +++++++++++++++++++ book/src/configuration/config_file.md | 13 ++++ book/src/configuration/file_only.md | 1 + book/src/intro.md | 11 ++++ 8 files changed, 107 insertions(+) create mode 100644 book/.gitignore create mode 100644 book/book.toml create mode 100644 book/src/SUMMARY.md create mode 100644 book/src/configuration/README.md create mode 100644 book/src/configuration/brick_actions/README.md create mode 100644 book/src/configuration/config_file.md create mode 100644 book/src/configuration/file_only.md create mode 100644 book/src/intro.md diff --git a/book/.gitignore b/book/.gitignore new file mode 100644 index 0000000..7585238 --- /dev/null +++ b/book/.gitignore @@ -0,0 +1 @@ +book diff --git a/book/book.toml b/book/book.toml new file mode 100644 index 0000000..817f2bd --- /dev/null +++ b/book/book.toml @@ -0,0 +1,5 @@ +[book] +authors = ["timothebot"] +language = "en" +src = "src" +title = "crane Documentation" diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md new file mode 100644 index 0000000..a072ebd --- /dev/null +++ b/book/src/SUMMARY.md @@ -0,0 +1,9 @@ +# Summary + +- [Introduction](./intro.md) + +# User Guide + +- [Configuration](./configuration/README.md) + - [Crane Config File](./configuration/config_file.md) + - [Simple Add File](./configuration/file_only.md) diff --git a/book/src/configuration/README.md b/book/src/configuration/README.md new file mode 100644 index 0000000..a025a48 --- /dev/null +++ b/book/src/configuration/README.md @@ -0,0 +1 @@ +# Configuration diff --git a/book/src/configuration/brick_actions/README.md b/book/src/configuration/brick_actions/README.md new file mode 100644 index 0000000..88373eb --- /dev/null +++ b/book/src/configuration/brick_actions/README.md @@ -0,0 +1,66 @@ +# Brick Config + +Brick actions defines what the brick should do. + +Replace / Append is file action + +- Add files +- Replace Files +- Inject into file at specific position +- Replace at specific position +- Run commands +- ~~Combine Bricks~~ => not brick + +## Shared options + +```toml +# Name must always be set if a brick.toml exists +name = "" + +# Define where this brick should be executed, eg. in a subfolder (prefix regex with re:) +target_location = "" +``` + +## Actions + +### Insert File + +```toml +[[actions]] +action = "insert_file" + +insert.if_file_exists = "append" # or "replace" or "fail" +``` + +### Modify File + +```toml +[[actions]] +action = "modify_file" + +modify = { + content = "text" # or file (prefix path with file:) + + # Where the modification should happen + selector = "" # either unique text or regex (prefix with re:) + + # Modify can be either type "append" or "replace": + # append text inside file + type = "append" + location = "before" # or after + + # replace text inside file + type = "replace" +} +``` + +### Run Script + +```toml +[[actions]] +action = "run_command" + +script = { + command = "echo 'hi'" # command or a script file (prefix with file:) +} +``` diff --git a/book/src/configuration/config_file.md b/book/src/configuration/config_file.md new file mode 100644 index 0000000..31086ed --- /dev/null +++ b/book/src/configuration/config_file.md @@ -0,0 +1,13 @@ +# Crane Config File + +The crane config file is located by default at `~/.config/crane/config.toml`, but the config directory can be changed by setting the `CRANE_CONFIG_DIR` env variable. + +## Brick Directories + +You can define where crane should look for bricks. If no paths are set, crane will look for a `bricks` folder in the same directory as the config is placed. + +```toml +brick_dirs = [ + "./bricks" +] +``` diff --git a/book/src/configuration/file_only.md b/book/src/configuration/file_only.md new file mode 100644 index 0000000..f7794f7 --- /dev/null +++ b/book/src/configuration/file_only.md @@ -0,0 +1 @@ +# File Only diff --git a/book/src/intro.md b/book/src/intro.md new file mode 100644 index 0000000..ec49c5e --- /dev/null +++ b/book/src/intro.md @@ -0,0 +1,11 @@ +# Introduction + +Crane is a CLI tool that allows you to create *bricks* that you can later add to any project. + +A brick is an instruction. It can be a file that gets added, a command that executes or lines getting replaced in a target file. + +## Quick Examples + +### License + +Instead of having to look up and copy your desired license from the web, you can create a brick out of it and then run `crane add some-license`. From 886ff9475d1ea50e1f7c5845da340a32a2fc7b8a Mon Sep 17 00:00:00 2001 From: Timo Borer Date: Tue, 23 Sep 2025 09:18:49 +0200 Subject: [PATCH 03/20] style: Apply cargo fmt to all files --- crane_bricks/src/actions/common.rs | 8 +++--- crane_bricks/src/actions/insert_file.rs | 4 +-- crane_bricks/src/actions/mod.rs | 25 ++++++++++++++----- crane_bricks/src/actions/modify_file.rs | 14 +++++------ crane_bricks/src/brick.rs | 33 ++++++++++++++++--------- crane_bricks/src/context.rs | 1 - crane_bricks/src/file_utils.rs | 6 ++++- crane_bricks/src/lib.rs | 4 +-- crane_bricks/tests/integration_test.rs | 11 +++++---- crane_cli/src/cmd/add.rs | 17 +++++++++---- crane_cli/src/cmd/cmd.rs | 2 +- crane_cli/src/cmd/list.rs | 4 +-- crane_cli/src/cmd/mod.rs | 2 +- crane_cli/src/utils.rs | 17 ++++++------- example/bricks/serde/brick.toml | 4 +++ rustfmt.toml | 2 +- 16 files changed, 92 insertions(+), 62 deletions(-) create mode 100644 example/bricks/serde/brick.toml diff --git a/crane_bricks/src/actions/common.rs b/crane_bricks/src/actions/common.rs index 2d26d07..a0e0390 100644 --- a/crane_bricks/src/actions/common.rs +++ b/crane_bricks/src/actions/common.rs @@ -3,7 +3,7 @@ use serde::Deserialize; #[derive(Debug, Deserialize, Default, Clone, PartialEq, Eq)] pub struct Common { /// Relative path from where you run crane to where the files should go - /// + /// /// ```toml /// [[actions]] /// working_dir = "./src/" @@ -13,14 +13,14 @@ pub struct Common { /// List of paths including for which files the action should run /// Empty means all files (except config file) will be included - /// + /// /// ```toml /// [[actions]] /// sources = [ "README.md", "LICENSE" ] - /// + /// /// # Or regex /// sources = [ "re:.+\.md", "LICENSE"] /// ``` #[serde(default)] - pub sources: Vec + pub sources: Vec, } diff --git a/crane_bricks/src/actions/insert_file.rs b/crane_bricks/src/actions/insert_file.rs index 3c5d0a1..6e8df3b 100644 --- a/crane_bricks/src/actions/insert_file.rs +++ b/crane_bricks/src/actions/insert_file.rs @@ -57,9 +57,7 @@ impl ExecuteAction for InsertFileAction { if !&self.common.sources.is_empty() { files = files .into_iter() - .filter(|file| { - *&self.common.sources.contains(&file.name().to_string()) - }) + .filter(|file| *&self.common.sources.contains(&file.name().to_string())) .collect(); } debug!("{} executing for {} files", brick.name(), files.len()); diff --git a/crane_bricks/src/actions/mod.rs b/crane_bricks/src/actions/mod.rs index 7266024..c2f74ca 100644 --- a/crane_bricks/src/actions/mod.rs +++ b/crane_bricks/src/actions/mod.rs @@ -1,4 +1,3 @@ - pub mod common; pub mod insert_file; pub mod modify_file; @@ -7,24 +6,38 @@ use std::path::Path; use serde::Deserialize; -use crate::{actions::{insert_file::InsertFileAction, modify_file::ModifyFileAction}, brick::Brick, context::ActionContext}; +use crate::{ + actions::{insert_file::InsertFileAction, modify_file::ModifyFileAction}, + brick::Brick, + context::ActionContext, +}; pub trait ExecuteAction { - fn execute(&self, context: &ActionContext, brick: &Brick, cwd: &Path) -> anyhow::Result<()>; + fn execute( + &self, + context: &ActionContext, + brick: &Brick, + cwd: &Path, + ) -> anyhow::Result<()>; } #[derive(Debug, Deserialize, Clone, PartialEq, Eq)] #[serde(tag = "action", rename_all = "snake_case")] pub enum Action { InsertFile(InsertFileAction), - ModifyFile(ModifyFileAction) + ModifyFile(ModifyFileAction), } impl ExecuteAction for Action { - fn execute(&self, context: &ActionContext, brick: &Brick, cwd: &Path) -> anyhow::Result<()> { + fn execute( + &self, + context: &ActionContext, + brick: &Brick, + cwd: &Path, + ) -> anyhow::Result<()> { match &self { Action::InsertFile(action) => action.execute(context, brick, cwd), Action::ModifyFile(action) => todo!(), } } -} \ No newline at end of file +} diff --git a/crane_bricks/src/actions/modify_file.rs b/crane_bricks/src/actions/modify_file.rs index b53a9fb..2950ba3 100644 --- a/crane_bricks/src/actions/modify_file.rs +++ b/crane_bricks/src/actions/modify_file.rs @@ -3,11 +3,11 @@ use serde::Deserialize; use crate::actions::common::Common; /// Modify a file by inserting content at a specific location. -/// +/// /// ## Example -/// +/// /// ### Config -/// +/// /// ```toml /// [[actions]] /// # For this action, the name of the files that the modification @@ -20,15 +20,15 @@ use crate::actions::common::Common; /// selector = "[dependencies]" /// location = "after" /// ``` -/// +/// /// ### Result -/// +/// /// ```toml /// # Before /// [dependencies] /// crane = "9.9.9" -/// -/// # After +/// +/// # After /// [dependencies] /// serde = "1" /// crane = "9.9.9" diff --git a/crane_bricks/src/brick.rs b/crane_bricks/src/brick.rs index d3e1d95..909c432 100644 --- a/crane_bricks/src/brick.rs +++ b/crane_bricks/src/brick.rs @@ -1,11 +1,16 @@ -use std::{fs, path::{Path, PathBuf}}; +use std::{ + fs, + path::{Path, PathBuf}, +}; use anyhow::anyhow; use log::debug; use serde::Deserialize; use crate::{ - actions::{Action, ExecuteAction}, context::ActionContext, file_utils::{sub_dirs, sub_paths} + actions::{Action, ExecuteAction}, + context::ActionContext, + file_utils::{sub_dirs, sub_paths}, }; const BRICK_CONFIG_FILE: &'static str = "brick.toml"; @@ -24,16 +29,13 @@ pub struct BrickFile { impl BrickFile { pub fn new(name: String, content: String) -> Self { - Self { - name, - content, - } + Self { name, content } } - + pub fn name(&self) -> &str { &self.name } - + pub fn content(&self) -> &str { &self.content } @@ -55,9 +57,12 @@ impl Brick { source_path, } } - + pub fn new_with_config(config: BrickConfig, source_path: PathBuf) -> Self { - Brick { config, source_path } + Brick { + config, + source_path, + } } pub fn name(&self) -> &str { @@ -101,7 +106,10 @@ impl TryFrom for Brick { fn try_from(value: PathBuf) -> Result { let config_file = value.join(BRICK_CONFIG_FILE); if !config_file.exists() { - debug!("Brick config file not found at '{:?}'", config_file.display()); + debug!( + "Brick config file not found at '{:?}'", + config_file.display() + ); let name = value .as_path() .file_name() @@ -109,7 +117,8 @@ impl TryFrom for Brick { return Ok(Brick::new(name.display().to_string(), value)); } debug!("Creating Brick from config file"); - let config: BrickConfig = toml::from_str(fs::read_to_string(config_file)?.as_str())?; + let config: BrickConfig = + toml::from_str(fs::read_to_string(config_file)?.as_str())?; Ok(Brick::new_with_config(config, value)) } } diff --git a/crane_bricks/src/context.rs b/crane_bricks/src/context.rs index 29e8e7c..b0770a0 100644 --- a/crane_bricks/src/context.rs +++ b/crane_bricks/src/context.rs @@ -1,4 +1,3 @@ - pub struct ActionContext { pub dry_run: bool, } diff --git a/crane_bricks/src/file_utils.rs b/crane_bricks/src/file_utils.rs index cc1b372..6448e23 100644 --- a/crane_bricks/src/file_utils.rs +++ b/crane_bricks/src/file_utils.rs @@ -27,7 +27,11 @@ pub fn sub_paths(dir: &Path) -> anyhow::Result> { .collect()) } -pub fn file_create_new(ctx: &ActionContext, path: &Path, content: Option) -> anyhow::Result<()> { +pub fn file_create_new( + ctx: &ActionContext, + path: &Path, + content: Option, +) -> anyhow::Result<()> { if !ctx.dry_run { debug!("Creating new file '{:?}'", path); let mut file = File::create_new(path)?; diff --git a/crane_bricks/src/lib.rs b/crane_bricks/src/lib.rs index dc9e2a5..1faeb38 100644 --- a/crane_bricks/src/lib.rs +++ b/crane_bricks/src/lib.rs @@ -1,4 +1,4 @@ pub mod actions; -pub mod context; pub mod brick; -pub mod file_utils; \ No newline at end of file +pub mod context; +pub mod file_utils; diff --git a/crane_bricks/tests/integration_test.rs b/crane_bricks/tests/integration_test.rs index beb3090..4603bd7 100644 --- a/crane_bricks/tests/integration_test.rs +++ b/crane_bricks/tests/integration_test.rs @@ -1,7 +1,11 @@ use std::{fs, path::PathBuf, vec}; use crane_bricks::{ - actions::{common::Common, insert_file::{FileExistsAction, InsertFileAction}, Action}, + actions::{ + Action, + common::Common, + insert_file::{FileExistsAction, InsertFileAction}, + }, brick::{Brick, BrickConfig}, context::ActionContext, }; @@ -47,10 +51,7 @@ action = "insert_file" fn test_insert_file() { init_logger(); - - - let brick = - Brick::try_from(test_dir().join("bricks/insert_with_config/")).unwrap(); + let brick = Brick::try_from(test_dir().join("bricks/insert_with_config/")).unwrap(); debug!("{:?}", brick); let ctx = ActionContext { dry_run: false }; diff --git a/crane_cli/src/cmd/add.rs b/crane_cli/src/cmd/add.rs index 959f18c..e50b3ee 100644 --- a/crane_cli/src/cmd/add.rs +++ b/crane_cli/src/cmd/add.rs @@ -3,11 +3,11 @@ use std::{env, path::PathBuf}; use fuzzy_matcher::{FuzzyMatcher, skim::SkimMatcherV2}; use log::{debug, error, info, warn}; -use crane_bricks::brick::{Brick, bricks}; use crate::{ cmd::{Add, Run}, config::CraneConfig, }; +use crane_bricks::brick::{Brick, bricks}; impl Run for Add { fn run(&self) { @@ -31,7 +31,8 @@ impl Run for Add { None => &env::current_dir().unwrap(), }; - let bricks: Vec = brick_dirs.iter().map(|dir| bricks(dir)).flatten().collect(); + let bricks: Vec = + brick_dirs.iter().map(|dir| bricks(dir)).flatten().collect(); debug!( "Found bricks:\n* {}", @@ -48,7 +49,9 @@ impl Run for Add { let mut matches: Vec<(Brick, i64)> = Vec::new(); let mut highest_score: i64 = 0; for brick in &bricks { - if let Some(score) = matcher.fuzzy_match(brick.name(), brick_query.as_str()) { + if let Some(score) = + matcher.fuzzy_match(brick.name(), brick_query.as_str()) + { if score >= highest_score { matches.push((brick.clone(), score)); highest_score = score; @@ -71,8 +74,12 @@ impl Run for Add { } fn add_brick(brick: Brick, target_dir: &PathBuf, dry_run: bool) { - info!("Adding brick '{}', {}, {:?}", brick.name(), dry_run, target_dir); - + info!( + "Adding brick '{}', {}, {:?}", + brick.name(), + dry_run, + target_dir + ); } fn no_matches_found(query: String) { diff --git a/crane_cli/src/cmd/cmd.rs b/crane_cli/src/cmd/cmd.rs index ee44ae9..72f91b1 100644 --- a/crane_cli/src/cmd/cmd.rs +++ b/crane_cli/src/cmd/cmd.rs @@ -31,7 +31,7 @@ pub struct Add { #[arg(short, long, value_hint=ValueHint::DirPath)] pub target_dir: Option, - #[arg(short='n', long)] + #[arg(short = 'n', long)] pub dry_run: bool, } diff --git a/crane_cli/src/cmd/list.rs b/crane_cli/src/cmd/list.rs index 68a22c4..5e32f70 100644 --- a/crane_cli/src/cmd/list.rs +++ b/crane_cli/src/cmd/list.rs @@ -1,9 +1,7 @@ use log::info; +use crate::cmd::{List, Run}; use crane_bricks::brick::bricks; -use crate::{ - cmd::{List, Run}, -}; impl Run for List { fn run(&self) { diff --git a/crane_cli/src/cmd/mod.rs b/crane_cli/src/cmd/mod.rs index 9699b61..8c6082c 100644 --- a/crane_cli/src/cmd/mod.rs +++ b/crane_cli/src/cmd/mod.rs @@ -12,7 +12,7 @@ impl Run for CraneCli { fn run(&self) { match &self.command { CraneCommand::Add(cmd) => cmd.run(), - CraneCommand::List(cmd) => cmd.run() + CraneCommand::List(cmd) => cmd.run(), } } } diff --git a/crane_cli/src/utils.rs b/crane_cli/src/utils.rs index 7f5bb0c..88f58e6 100644 --- a/crane_cli/src/utils.rs +++ b/crane_cli/src/utils.rs @@ -3,12 +3,10 @@ use std::path::{Path, PathBuf}; use anyhow::anyhow; pub fn sub_dirs(dir: &Path) -> anyhow::Result> { - Ok( - sub_paths(dir)? - .into_iter() - .filter(|path| path.is_dir()) - .collect::>() - ) + Ok(sub_paths(dir)? + .into_iter() + .filter(|path| path.is_dir()) + .collect::>()) } /// Get a vec of all files and folders in the given dir if valid @@ -17,8 +15,7 @@ pub fn sub_paths(dir: &Path) -> anyhow::Result> { return Err(anyhow!("Target does not exist or not a directory")); } let dirs = dir.read_dir()?; - Ok( - dirs.filter_map(|entry_res| Some(entry_res.ok()?.path())) - .collect(), - ) + Ok(dirs + .filter_map(|entry_res| Some(entry_res.ok()?.path())) + .collect()) } diff --git a/example/bricks/serde/brick.toml b/example/bricks/serde/brick.toml new file mode 100644 index 0000000..5bdc256 --- /dev/null +++ b/example/bricks/serde/brick.toml @@ -0,0 +1,4 @@ +name = "serde" + +action = "command" +action_settings = "echo 'hi'" \ No newline at end of file diff --git a/rustfmt.toml b/rustfmt.toml index 5c8d931..8518ab0 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -1 +1 @@ -max_width = 80 \ No newline at end of file +max_width = 90 \ No newline at end of file From 88b50442c0e27d7be6e90c08a1cfa93017b41e6f Mon Sep 17 00:00:00 2001 From: Timo Borer Date: Tue, 23 Sep 2025 09:34:24 +0200 Subject: [PATCH 04/20] feat: Implement test and functionality for brick without config --- crane_bricks/src/actions/insert_file.rs | 2 +- crane_bricks/src/actions/mod.rs | 2 +- crane_bricks/src/brick.rs | 29 ++++++++++++++++-- crane_bricks/tests/integration_test.rs | 39 +++++++++++++++++++------ 4 files changed, 58 insertions(+), 14 deletions(-) diff --git a/crane_bricks/src/actions/insert_file.rs b/crane_bricks/src/actions/insert_file.rs index 6e8df3b..344dded 100644 --- a/crane_bricks/src/actions/insert_file.rs +++ b/crane_bricks/src/actions/insert_file.rs @@ -27,7 +27,7 @@ use crate::{ /// ### Result /// /// Will create the LICENSE file. If it already exists, it replaces it. -#[derive(Debug, Deserialize, Clone, PartialEq, Eq)] +#[derive(Debug, Deserialize, Default, Clone, PartialEq, Eq)] pub struct InsertFileAction { #[serde(flatten)] pub common: Common, diff --git a/crane_bricks/src/actions/mod.rs b/crane_bricks/src/actions/mod.rs index c2f74ca..2cfe3da 100644 --- a/crane_bricks/src/actions/mod.rs +++ b/crane_bricks/src/actions/mod.rs @@ -37,7 +37,7 @@ impl ExecuteAction for Action { ) -> anyhow::Result<()> { match &self { Action::InsertFile(action) => action.execute(context, brick, cwd), - Action::ModifyFile(action) => todo!(), + Action::ModifyFile(_action) => todo!(), } } } diff --git a/crane_bricks/src/brick.rs b/crane_bricks/src/brick.rs index 909c432..ef7abf1 100644 --- a/crane_bricks/src/brick.rs +++ b/crane_bricks/src/brick.rs @@ -8,7 +8,7 @@ use log::debug; use serde::Deserialize; use crate::{ - actions::{Action, ExecuteAction}, + actions::{insert_file::InsertFileAction, Action, ExecuteAction}, context::ActionContext, file_utils::{sub_dirs, sub_paths}, }; @@ -17,8 +17,22 @@ const BRICK_CONFIG_FILE: &'static str = "brick.toml"; #[derive(Deserialize, Debug, Clone, Default, PartialEq, Eq)] pub struct BrickConfig { - pub name: String, - pub actions: Vec, + name: String, + actions: Vec, +} + +impl BrickConfig { + pub fn new(name: String, actions: Vec) -> Self { + Self { name, actions } + } + + pub fn name(&self) -> &str { + &self.name + } + + pub fn actions(&self) -> &[Action] { + &self.actions + } } #[derive(Debug, Clone)] @@ -52,6 +66,10 @@ impl Brick { Brick { config: BrickConfig { name, + // If no action is configured, InsertFileAction is default + actions: vec![ + Action::InsertFile(InsertFileAction::default()) + ], ..BrickConfig::default() }, source_path, @@ -73,6 +91,10 @@ impl Brick { &self.source_path } + pub fn config(&self) -> &BrickConfig { + &self.config + } + pub fn execute(&self, context: &ActionContext, cwd: &Path) -> anyhow::Result<()> { for action in &self.config.actions { action.execute(context, &self, cwd)?; @@ -123,6 +145,7 @@ impl TryFrom for Brick { } } +/// Get all bricks in a directory pub fn bricks(dir: &PathBuf) -> Vec { let Ok(dirs) = sub_dirs(dir) else { return vec![]; diff --git a/crane_bricks/tests/integration_test.rs b/crane_bricks/tests/integration_test.rs index 4603bd7..910f0f4 100644 --- a/crane_bricks/tests/integration_test.rs +++ b/crane_bricks/tests/integration_test.rs @@ -1,4 +1,7 @@ -use std::{fs, path::PathBuf, vec}; +use std::{ + path::PathBuf, + vec, +}; use crane_bricks::{ actions::{ @@ -25,6 +28,10 @@ fn test_dir() -> PathBuf { PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests") } +fn brick_dir(brick: &str) -> PathBuf { + test_dir().join("bricks/").join(brick) +} + #[test] fn test_actions_parse() { init_logger(); @@ -37,13 +44,13 @@ action = "insert_file" "#; let config_parsed: BrickConfig = toml::from_str(config).unwrap(); - let config: BrickConfig = BrickConfig { - name: String::from("hi"), - actions: vec![Action::InsertFile(InsertFileAction { + let config: BrickConfig = BrickConfig::new( + String::from("hi"), + vec![Action::InsertFile(InsertFileAction { common: Common::default(), if_file_exists: FileExistsAction::Append, })], - }; + ); assert_eq!(config_parsed, config); } @@ -51,14 +58,28 @@ action = "insert_file" fn test_insert_file() { init_logger(); - let brick = Brick::try_from(test_dir().join("bricks/insert_with_config/")).unwrap(); - + let brick = Brick::try_from(brick_dir("insert_with_config")).unwrap(); debug!("{:?}", brick); + let ctx = ActionContext { dry_run: false }; let tmpdir = tempfile::tempdir().unwrap(); - let res = brick.execute(&ctx, tmpdir.path()); - assert!(res.is_ok()); + brick.execute(&ctx, tmpdir.path()).unwrap(); assert!(tmpdir.path().join("TEST_A").exists()); assert!(!tmpdir.path().join("TEST_B").exists()); assert!(!tmpdir.path().join("brick.toml").exists()); } + +#[test] +fn test_without_config() { + init_logger(); + + let brick = Brick::try_from(brick_dir("insert_no_config")).unwrap(); + debug!("{:?}", brick); + + assert_eq!(1, brick.config().actions().len()); + + let ctx = ActionContext { dry_run: false }; + let tmpdir = tempfile::tempdir().unwrap(); + brick.execute(&ctx, tmpdir.path()).unwrap(); + assert!(tmpdir.path().join("TEST_B").exists()); +} From 5cce939a6b6d7c888f191da284248a1577b7557a Mon Sep 17 00:00:00 2001 From: Timo Borer Date: Tue, 23 Sep 2025 11:49:40 +0200 Subject: [PATCH 05/20] feat: Add more tests for modify_action --- crane_bricks/src/actions/mod.rs | 4 +- crane_bricks/src/actions/modify_file.rs | 85 ++++++++++++++++++- .../tests/bricks/modify_append/brick.toml | 9 ++ .../tests/bricks/modify_prepend/brick.toml | 9 ++ .../tests/bricks/modify_replace/brick.toml | 8 ++ crane_bricks/tests/common/mod.rs | 39 +++++++++ crane_bricks/tests/data/Test.toml | 7 ++ crane_bricks/tests/integration_test.rs | 81 +++++++++++++----- crane_cli/src/main.rs | 1 - crane_cli/src/utils.rs | 21 ----- 10 files changed, 218 insertions(+), 46 deletions(-) create mode 100644 crane_bricks/tests/bricks/modify_append/brick.toml create mode 100644 crane_bricks/tests/bricks/modify_prepend/brick.toml create mode 100644 crane_bricks/tests/bricks/modify_replace/brick.toml create mode 100644 crane_bricks/tests/common/mod.rs create mode 100644 crane_bricks/tests/data/Test.toml delete mode 100644 crane_cli/src/utils.rs diff --git a/crane_bricks/src/actions/mod.rs b/crane_bricks/src/actions/mod.rs index 2cfe3da..dde047d 100644 --- a/crane_bricks/src/actions/mod.rs +++ b/crane_bricks/src/actions/mod.rs @@ -4,6 +4,7 @@ pub mod modify_file; use std::path::Path; +use log::debug; use serde::Deserialize; use crate::{ @@ -35,9 +36,10 @@ impl ExecuteAction for Action { brick: &Brick, cwd: &Path, ) -> anyhow::Result<()> { + debug!("Executing '{}' brick action '{:#?}'", brick.name(), &self); match &self { Action::InsertFile(action) => action.execute(context, brick, cwd), - Action::ModifyFile(_action) => todo!(), + Action::ModifyFile(action) => action.execute(context, brick, cwd), } } } diff --git a/crane_bricks/src/actions/modify_file.rs b/crane_bricks/src/actions/modify_file.rs index 2950ba3..3032031 100644 --- a/crane_bricks/src/actions/modify_file.rs +++ b/crane_bricks/src/actions/modify_file.rs @@ -1,6 +1,11 @@ +use anyhow::{Ok, anyhow}; +use log::debug; use serde::Deserialize; -use crate::actions::common::Common; +use crate::{ + actions::{ExecuteAction, common::Common}, + file_utils::{file_read_content, file_replace_content}, +}; /// Modify a file by inserting content at a specific location. /// @@ -40,17 +45,18 @@ pub struct ModifyFileAction { /// If the modification should append something next to the /// selector or if it should replace it. - pub r#type: ModifyType, + pub(self) r#type: ModifyType, pub content: Option, /// The content selector for the modification, must be unique. /// Can be regex if prefix with "re:". - pub selector: Option, + pub selector: String, /// If the modification should happen "before" or "after" (default) /// the given selector. - pub location: ModifyLocation, + #[serde(default)] + pub(self) location: ModifyLocation, } #[derive(Debug, Deserialize, Default, Clone, PartialEq, Eq)] @@ -68,3 +74,74 @@ enum ModifyLocation { After, Before, } + +impl ModifyFileAction { + pub fn content(&self) -> String { + // TODO: Get content from somewhere else if not set + self.content.clone().unwrap_or_default() + } + + pub fn modify_content(&self, source_text: String) -> anyhow::Result { + // TODO: Handle regex + // TODO: insert for all or just one? + + let locations: Vec<(usize, &str)> = + source_text.match_indices(&self.selector).collect(); + + debug!("Found {} matches", locations.len()); + if locations.len() == 0 { + return Err(anyhow!("No selector matches in target file!")); + } + + let mut output = source_text.clone(); + let start_length = source_text.len(); + + for (index, selected) in locations { + // This is to account for new inserted text, which + // means the index has shifted. + let modified_index = index + output.len().abs_diff(start_length); + match &self.r#type { + ModifyType::Append => match &self.location { + ModifyLocation::After => { + output.insert_str( + (modified_index + selected.len()).max(0), + &self.content(), + ); + } + ModifyLocation::Before => { + output.insert_str(modified_index, &self.content()); + } + }, + ModifyType::Replace => { + output.replace_range(modified_index..(modified_index + selected.len()).max(0), &self.content()); + } + } + } + Ok(output) + } +} + +impl ExecuteAction for ModifyFileAction { + fn execute( + &self, + context: &crate::context::ActionContext, + brick: &crate::brick::Brick, + cwd: &std::path::Path, + ) -> anyhow::Result<()> { + let mut files: Vec = brick + .files() + .iter() + .map(|brick_file| brick_file.name().to_string()) + .collect(); + files.extend(self.common.sources.clone().into_iter()); + for file in files { + let target_path = cwd.join(file); + if !target_path.exists() { + return Err(anyhow!("Target file does not exist!")); + } + let content = file_read_content(context, &target_path)?; + file_replace_content(context, &target_path, &self.modify_content(content)?)?; + } + Ok(()) + } +} diff --git a/crane_bricks/tests/bricks/modify_append/brick.toml b/crane_bricks/tests/bricks/modify_append/brick.toml new file mode 100644 index 0000000..7314f04 --- /dev/null +++ b/crane_bricks/tests/bricks/modify_append/brick.toml @@ -0,0 +1,9 @@ +name = "test" + +[[actions]] +sources = ["Test.toml"] +action = "modify_file" +type = "append" +content = "\nserde = \"1\"" +selector = "[dependencies]" +location = "after" diff --git a/crane_bricks/tests/bricks/modify_prepend/brick.toml b/crane_bricks/tests/bricks/modify_prepend/brick.toml new file mode 100644 index 0000000..b1ca42a --- /dev/null +++ b/crane_bricks/tests/bricks/modify_prepend/brick.toml @@ -0,0 +1,9 @@ +name = "test" + +[[actions]] +sources = ["Test.toml"] +action = "modify_file" +type = "append" +content = "\nserde = \"1\"\n" +selector = "[dependencies]" +location = "before" diff --git a/crane_bricks/tests/bricks/modify_replace/brick.toml b/crane_bricks/tests/bricks/modify_replace/brick.toml new file mode 100644 index 0000000..9543bce --- /dev/null +++ b/crane_bricks/tests/bricks/modify_replace/brick.toml @@ -0,0 +1,8 @@ +name = "test" + +[[actions]] +sources = ["Test.toml"] +action = "modify_file" +type = "replace" +content = "[dev-dependencies]" +selector = "[dependencies]" diff --git a/crane_bricks/tests/common/mod.rs b/crane_bricks/tests/common/mod.rs new file mode 100644 index 0000000..d83ef22 --- /dev/null +++ b/crane_bricks/tests/common/mod.rs @@ -0,0 +1,39 @@ +use std::{fs::File, io::{Read, Write}, path::{Path, PathBuf}}; + +pub fn init_logger() { + let _ = env_logger::builder() + // Include all events in tests + .filter_level(log::LevelFilter::max()) + // Ensure events are captured by `cargo test` + .is_test(true) + // Ignore errors initializing the logger if tests race to configure it + .try_init(); +} + +pub fn test_dir() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests") +} + +pub fn brick_dir(brick: &str) -> PathBuf { + test_dir().join("bricks/").join(brick) +} + +/// Add a file from the tests/data dir to the temp dir +pub fn add_test_data(temp: &Path, file: &str) { + let mut data_file = File::options() + .read(true) + .open(test_dir().join("data/").join(file)) + .unwrap(); + let mut content = String::new(); + data_file.read_to_string(&mut content).unwrap(); + + let mut target_file = File::create(temp.join(file)).unwrap(); + target_file.write(content.as_bytes()).unwrap(); +} + +pub fn file_content(path: &Path) -> String { + let mut file = File::options().read(true).open(path).unwrap(); + let mut output = String::new(); + file.read_to_string(&mut output).unwrap(); + output +} diff --git a/crane_bricks/tests/data/Test.toml b/crane_bricks/tests/data/Test.toml new file mode 100644 index 0000000..e9b98cc --- /dev/null +++ b/crane_bricks/tests/data/Test.toml @@ -0,0 +1,7 @@ +[package] +name = "test" +version = "0.1.0" +edition = "2024" + +[dependencies] +toml = "0" diff --git a/crane_bricks/tests/integration_test.rs b/crane_bricks/tests/integration_test.rs index 910f0f4..22bc1f8 100644 --- a/crane_bricks/tests/integration_test.rs +++ b/crane_bricks/tests/integration_test.rs @@ -1,7 +1,4 @@ -use std::{ - path::PathBuf, - vec, -}; +use std::vec; use crane_bricks::{ actions::{ @@ -14,22 +11,16 @@ use crane_bricks::{ }; use log::debug; -fn init_logger() { - let _ = env_logger::builder() - // Include all events in tests - .filter_level(log::LevelFilter::max()) - // Ensure events are captured by `cargo test` - .is_test(true) - // Ignore errors initializing the logger if tests race to configure it - .try_init(); -} +use common::*; -fn test_dir() -> PathBuf { - PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests") -} +mod common; + +#[test] +fn test_test_functions() { + let tmpdir = tempfile::tempdir().unwrap(); + add_test_data(tmpdir.path(), "Test.toml"); -fn brick_dir(brick: &str) -> PathBuf { - test_dir().join("bricks/").join(brick) + assert!(tmpdir.path().join("Test.toml").exists()); } #[test] @@ -77,9 +68,61 @@ fn test_without_config() { debug!("{:?}", brick); assert_eq!(1, brick.config().actions().len()); - + let ctx = ActionContext { dry_run: false }; let tmpdir = tempfile::tempdir().unwrap(); brick.execute(&ctx, tmpdir.path()).unwrap(); assert!(tmpdir.path().join("TEST_B").exists()); } + +#[test] +fn test_modify_append() { + init_logger(); + + let brick = Brick::try_from(brick_dir("modify_append")).unwrap(); + + let tmpdir = tempfile::tempdir().unwrap(); + add_test_data(tmpdir.path(), "Test.toml"); + let ctx = ActionContext { dry_run: false }; + + brick.execute(&ctx, tmpdir.path()).unwrap(); + let res_content = file_content(&tmpdir.path().join("Test.toml")); + debug!("{}", res_content); + assert!(res_content.contains("[dependencies]\nserde = \"1\"\n")) +} + +#[test] +fn test_modify_prepend() { + init_logger(); + + let brick = Brick::try_from(brick_dir("modify_prepend")).unwrap(); + + let tmpdir = tempfile::tempdir().unwrap(); + add_test_data(tmpdir.path(), "Test.toml"); + let ctx = ActionContext { dry_run: false }; + + brick.execute(&ctx, tmpdir.path()).unwrap(); + let res_content = file_content(&tmpdir.path().join("Test.toml")); + debug!("{}", res_content); + assert!(res_content.contains("serde = \"1\"\n[dependencies]")) +} + +#[test] +fn test_modify_replace() { + init_logger(); + + let brick = Brick::try_from(brick_dir("modify_replace")).unwrap(); + + let tmpdir = tempfile::tempdir().unwrap(); + add_test_data(tmpdir.path(), "Test.toml"); + let ctx = ActionContext { dry_run: false }; + + brick.execute(&ctx, tmpdir.path()).unwrap(); + let res_content = file_content(&tmpdir.path().join("Test.toml")); + debug!("{}", res_content); + assert!(!res_content.contains("[dependencies]")); + assert!(res_content.contains("[dev-dependencies]")); +} + +#[test] +fn test_command() {} diff --git a/crane_cli/src/main.rs b/crane_cli/src/main.rs index 6195fc7..f244a9a 100644 --- a/crane_cli/src/main.rs +++ b/crane_cli/src/main.rs @@ -4,7 +4,6 @@ use crate::cmd::{CraneCli, Run}; mod cmd; mod config; -mod utils; fn main() { let cli = CraneCli::parse(); diff --git a/crane_cli/src/utils.rs b/crane_cli/src/utils.rs deleted file mode 100644 index 88f58e6..0000000 --- a/crane_cli/src/utils.rs +++ /dev/null @@ -1,21 +0,0 @@ -use std::path::{Path, PathBuf}; - -use anyhow::anyhow; - -pub fn sub_dirs(dir: &Path) -> anyhow::Result> { - Ok(sub_paths(dir)? - .into_iter() - .filter(|path| path.is_dir()) - .collect::>()) -} - -/// Get a vec of all files and folders in the given dir if valid -pub fn sub_paths(dir: &Path) -> anyhow::Result> { - if !dir.exists() || !dir.is_dir() { - return Err(anyhow!("Target does not exist or not a directory")); - } - let dirs = dir.read_dir()?; - Ok(dirs - .filter_map(|entry_res| Some(entry_res.ok()?.path())) - .collect()) -} From 0428ee282bb6f3d7af44d23a5e9a62366664105f Mon Sep 17 00:00:00 2001 From: Timo Borer Date: Tue, 23 Sep 2025 13:50:38 +0200 Subject: [PATCH 06/20] feat: Add run command action and tests --- crane_bricks/src/actions/mod.rs | 5 +- crane_bricks/src/actions/run_command.rs | 51 +++++++++++++++++++ .../tests/bricks/run_command/brick.toml | 5 ++ crane_bricks/tests/integration_test.rs | 12 ++++- 4 files changed, 71 insertions(+), 2 deletions(-) create mode 100644 crane_bricks/src/actions/run_command.rs create mode 100644 crane_bricks/tests/bricks/run_command/brick.toml diff --git a/crane_bricks/src/actions/mod.rs b/crane_bricks/src/actions/mod.rs index dde047d..48b14e3 100644 --- a/crane_bricks/src/actions/mod.rs +++ b/crane_bricks/src/actions/mod.rs @@ -1,6 +1,7 @@ pub mod common; pub mod insert_file; pub mod modify_file; +pub mod run_command; use std::path::Path; @@ -8,7 +9,7 @@ use log::debug; use serde::Deserialize; use crate::{ - actions::{insert_file::InsertFileAction, modify_file::ModifyFileAction}, + actions::{insert_file::InsertFileAction, modify_file::ModifyFileAction, run_command::RunCommandAction}, brick::Brick, context::ActionContext, }; @@ -27,6 +28,7 @@ pub trait ExecuteAction { pub enum Action { InsertFile(InsertFileAction), ModifyFile(ModifyFileAction), + RunCommand(RunCommandAction), } impl ExecuteAction for Action { @@ -40,6 +42,7 @@ impl ExecuteAction for Action { match &self { Action::InsertFile(action) => action.execute(context, brick, cwd), Action::ModifyFile(action) => action.execute(context, brick, cwd), + Action::RunCommand(action) => action.execute(context, brick, cwd), } } } diff --git a/crane_bricks/src/actions/run_command.rs b/crane_bricks/src/actions/run_command.rs new file mode 100644 index 0000000..969bdaa --- /dev/null +++ b/crane_bricks/src/actions/run_command.rs @@ -0,0 +1,51 @@ +use std::{path::Path, process::Command}; + +use serde::Deserialize; + +use crate::{ + actions::{ExecuteAction, common::Common}, + brick::Brick, + context::ActionContext, +}; + +/// Run a command +/// +/// ## Example +/// +/// ### Config +/// +/// ```toml +/// [[actions]] +/// command = "echo hi > test.txt" +/// ``` +/// +/// ### Result +/// +/// Will run echo hi and write the stdout into test.txt +#[derive(Debug, Deserialize, Default, Clone, PartialEq, Eq)] +pub struct RunCommandAction { + #[serde(flatten)] + pub common: Common, + + pub command: String, +} + +impl ExecuteAction for RunCommandAction { + fn execute( + &self, + context: &ActionContext, + _brick: &Brick, + cwd: &Path, + ) -> anyhow::Result<()> { + if context.dry_run { + return Ok(()); + } + Command::new("sh") + .arg("-c") + .arg(&self.command) + .current_dir(cwd) + .output()?; + + Ok(()) + } +} diff --git a/crane_bricks/tests/bricks/run_command/brick.toml b/crane_bricks/tests/bricks/run_command/brick.toml new file mode 100644 index 0000000..bc5ba76 --- /dev/null +++ b/crane_bricks/tests/bricks/run_command/brick.toml @@ -0,0 +1,5 @@ +name = "test" + +[[actions]] +action = "run_command" +command = "echo hi > test.txt" diff --git a/crane_bricks/tests/integration_test.rs b/crane_bricks/tests/integration_test.rs index 22bc1f8..053466f 100644 --- a/crane_bricks/tests/integration_test.rs +++ b/crane_bricks/tests/integration_test.rs @@ -125,4 +125,14 @@ fn test_modify_replace() { } #[test] -fn test_command() {} +fn test_command() { + init_logger(); + + let brick = Brick::try_from(brick_dir("run_command")).unwrap(); + + let tmpdir = tempfile::tempdir().unwrap(); + let ctx = ActionContext { dry_run: false }; + + brick.execute(&ctx, tmpdir.path()).unwrap(); + assert!(tmpdir.path().join("test.txt").exists()); +} From a7dfb9cda236f3eb8e5bcd305ce5bb7768dc80d1 Mon Sep 17 00:00:00 2001 From: Timo Borer Date: Tue, 23 Sep 2025 13:54:30 +0200 Subject: [PATCH 07/20] feat: Remove location and add prepend type to modify action There are only three options you can do, it didn't make sense to have a location option --- crane_bricks/src/actions/modify_file.rs | 35 ++++++------------- .../tests/bricks/modify_append/brick.toml | 1 - .../tests/bricks/modify_prepend/brick.toml | 3 +- 3 files changed, 11 insertions(+), 28 deletions(-) diff --git a/crane_bricks/src/actions/modify_file.rs b/crane_bricks/src/actions/modify_file.rs index 3032031..b64f855 100644 --- a/crane_bricks/src/actions/modify_file.rs +++ b/crane_bricks/src/actions/modify_file.rs @@ -23,7 +23,6 @@ use crate::{ /// type = "append" /// content = "\nserde = \"1\"" /// selector = "[dependencies]" -/// location = "after" /// ``` /// /// ### Result @@ -43,7 +42,7 @@ pub struct ModifyFileAction { #[serde(flatten)] pub common: Common, - /// If the modification should append something next to the + /// If the modification should append or prepend text next to the /// selector or if it should replace it. pub(self) r#type: ModifyType, @@ -52,11 +51,6 @@ pub struct ModifyFileAction { /// The content selector for the modification, must be unique. /// Can be regex if prefix with "re:". pub selector: String, - - /// If the modification should happen "before" or "after" (default) - /// the given selector. - #[serde(default)] - pub(self) location: ModifyLocation, } #[derive(Debug, Deserialize, Default, Clone, PartialEq, Eq)] @@ -64,17 +58,10 @@ pub struct ModifyFileAction { enum ModifyType { #[default] Append, + Prepend, Replace, } -#[derive(Debug, Deserialize, Default, Clone, PartialEq, Eq)] -#[serde(rename_all = "snake_case")] -enum ModifyLocation { - #[default] - After, - Before, -} - impl ModifyFileAction { pub fn content(&self) -> String { // TODO: Get content from somewhere else if not set @@ -101,16 +88,14 @@ impl ModifyFileAction { // means the index has shifted. let modified_index = index + output.len().abs_diff(start_length); match &self.r#type { - ModifyType::Append => match &self.location { - ModifyLocation::After => { - output.insert_str( - (modified_index + selected.len()).max(0), - &self.content(), - ); - } - ModifyLocation::Before => { - output.insert_str(modified_index, &self.content()); - } + ModifyType::Append => { + output.insert_str( + (modified_index + selected.len()).max(0), + &self.content(), + ); + }, + ModifyType::Prepend => { + output.insert_str(modified_index, &self.content()); }, ModifyType::Replace => { output.replace_range(modified_index..(modified_index + selected.len()).max(0), &self.content()); diff --git a/crane_bricks/tests/bricks/modify_append/brick.toml b/crane_bricks/tests/bricks/modify_append/brick.toml index 7314f04..774975c 100644 --- a/crane_bricks/tests/bricks/modify_append/brick.toml +++ b/crane_bricks/tests/bricks/modify_append/brick.toml @@ -6,4 +6,3 @@ action = "modify_file" type = "append" content = "\nserde = \"1\"" selector = "[dependencies]" -location = "after" diff --git a/crane_bricks/tests/bricks/modify_prepend/brick.toml b/crane_bricks/tests/bricks/modify_prepend/brick.toml index b1ca42a..ca71d52 100644 --- a/crane_bricks/tests/bricks/modify_prepend/brick.toml +++ b/crane_bricks/tests/bricks/modify_prepend/brick.toml @@ -3,7 +3,6 @@ name = "test" [[actions]] sources = ["Test.toml"] action = "modify_file" -type = "append" +type = "prepend" content = "\nserde = \"1\"\n" selector = "[dependencies]" -location = "before" From 754639397d853152fd1bfa28e75275f988a30985 Mon Sep 17 00:00:00 2001 From: Timo Borer Date: Tue, 23 Sep 2025 14:28:35 +0200 Subject: [PATCH 08/20] docs: Update mdbook to match code --- book/book.toml | 2 +- book/src/SUMMARY.md | 4 +- .../src/configuration/brick_actions/README.md | 66 -------------- book/src/configuration/brick_config.md | 91 +++++++++++++++++++ .../{config_file.md => crane_config.md} | 10 ++ book/src/configuration/file_only.md | 1 - 6 files changed, 104 insertions(+), 70 deletions(-) delete mode 100644 book/src/configuration/brick_actions/README.md create mode 100644 book/src/configuration/brick_config.md rename book/src/configuration/{config_file.md => crane_config.md} (75%) delete mode 100644 book/src/configuration/file_only.md diff --git a/book/book.toml b/book/book.toml index 817f2bd..dfc511b 100644 --- a/book/book.toml +++ b/book/book.toml @@ -2,4 +2,4 @@ authors = ["timothebot"] language = "en" src = "src" -title = "crane Documentation" +title = "Crane Documentation 🏗️" diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md index a072ebd..78f0619 100644 --- a/book/src/SUMMARY.md +++ b/book/src/SUMMARY.md @@ -5,5 +5,5 @@ # User Guide - [Configuration](./configuration/README.md) - - [Crane Config File](./configuration/config_file.md) - - [Simple Add File](./configuration/file_only.md) + - [Crane Config](./configuration/crane_config.md) + - [Brick Config](./configuration/brick_config.md) diff --git a/book/src/configuration/brick_actions/README.md b/book/src/configuration/brick_actions/README.md deleted file mode 100644 index 88373eb..0000000 --- a/book/src/configuration/brick_actions/README.md +++ /dev/null @@ -1,66 +0,0 @@ -# Brick Config - -Brick actions defines what the brick should do. - -Replace / Append is file action - -- Add files -- Replace Files -- Inject into file at specific position -- Replace at specific position -- Run commands -- ~~Combine Bricks~~ => not brick - -## Shared options - -```toml -# Name must always be set if a brick.toml exists -name = "" - -# Define where this brick should be executed, eg. in a subfolder (prefix regex with re:) -target_location = "" -``` - -## Actions - -### Insert File - -```toml -[[actions]] -action = "insert_file" - -insert.if_file_exists = "append" # or "replace" or "fail" -``` - -### Modify File - -```toml -[[actions]] -action = "modify_file" - -modify = { - content = "text" # or file (prefix path with file:) - - # Where the modification should happen - selector = "" # either unique text or regex (prefix with re:) - - # Modify can be either type "append" or "replace": - # append text inside file - type = "append" - location = "before" # or after - - # replace text inside file - type = "replace" -} -``` - -### Run Script - -```toml -[[actions]] -action = "run_command" - -script = { - command = "echo 'hi'" # command or a script file (prefix with file:) -} -``` diff --git a/book/src/configuration/brick_config.md b/book/src/configuration/brick_config.md new file mode 100644 index 0000000..e4c8d1d --- /dev/null +++ b/book/src/configuration/brick_config.md @@ -0,0 +1,91 @@ +# Brick Config + +The brick config defines what the brick should do. The file must be called `brick.toml` +and must be located at the root of the brick directory. + +To get started, create a new directory in a [defined brick directory](./crane_config.md#brick-directories). +Inside, create a file called `brick.toml` and add a name for your brick. + +```toml +name = "my_brick_name" +``` + +## No Config + +If you have a brick directory without a `brick.toml` file, it will still work. By default, this will take the directory name as brick name and add the [insert file](#insert-file) action. + +## Shared options + +```toml +# Name must always be set if a brick.toml exists +name = "" +``` + +## Actions + +You can define as many actions as you want. For all actions, you may specify a specific `working_dir`, +which is a relative path from where the action would execute to where it should actually execute. + +This is useful if you want to add files from a project root that are located in subfolders. + +```toml +working_dir = "./src/" +``` + +### Insert File + +```toml +[[actions]] +action = "insert_file" + +if_file_exists = "append" # or "replace" or "pass" + +# Which files should be inserted +sources = [ + "file.txt" +] +``` + +If no `sources` are defined, it will use all files in the brick directory (except the config file). + +### Modify File + +Allows you to modify a specific part of a file. +You can do different operations, like *replace*, *append* or *prepend*. + +```toml +[[actions]] +action = "modify_file" + +# The content that will be inserted into the file. +content = "text" # or file (prefix path with file:) + +# Where the modification should happen +selector = "[dependencies]" # either text or regex (prefix with re:) + +# Modify can be type "append" (default), "prepend" or "replace": +# append text inside file +type = "append" + + +# Specify which files this action applies to +sources = [ + "file.txt" +] +``` + +If no `sources` are defined, it will use all files in the brick directory (except the config file). + +### Run Script + +Allows you to run a command or a script file. + +```toml +[[actions]] +action = "run_command" + +command = "echo 'hi'" # command or a script file (prefix with file:) +``` + +This is by far the most simple yet powerful action. +If you need more complex behaviour, you can add a custom script that does what you need. diff --git a/book/src/configuration/config_file.md b/book/src/configuration/crane_config.md similarity index 75% rename from book/src/configuration/config_file.md rename to book/src/configuration/crane_config.md index 31086ed..ae9a437 100644 --- a/book/src/configuration/config_file.md +++ b/book/src/configuration/crane_config.md @@ -11,3 +11,13 @@ brick_dirs = [ "./bricks" ] ``` + +## Aliases + +You can define aliases for multiple bricks. + +```toml +[[alias]] +name = "rust" +bricks = [ "mit", "rustfmt", "serde" ] +``` diff --git a/book/src/configuration/file_only.md b/book/src/configuration/file_only.md deleted file mode 100644 index f7794f7..0000000 --- a/book/src/configuration/file_only.md +++ /dev/null @@ -1 +0,0 @@ -# File Only From a4892cd68d262bb18787adfff47785dbcacb33b1 Mon Sep 17 00:00:00 2001 From: Timo Borer Date: Thu, 25 Sep 2025 20:57:12 +0200 Subject: [PATCH 09/20] feat: Improve logging output to make it more nice to look at --- Cargo.lock | 123 +++++++++++++++++++++--- crane_bricks/src/actions/insert_file.rs | 24 ++++- crane_bricks/src/actions/mod.rs | 1 - crane_bricks/src/actions/modify_file.rs | 40 +++++++- crane_bricks/src/actions/run_command.rs | 1 + crane_bricks/src/brick.rs | 17 +++- crane_bricks/src/file_utils.rs | 1 - crane_bricks/src/lib.rs | 3 + crane_cli/Cargo.toml | 7 ++ crane_cli/src/cmd/add.rs | 119 ++++++++++++++--------- crane_cli/src/cmd/cmd.rs | 8 +- crane_cli/src/config.rs | 22 +++++ crane_cli/src/logging.rs | 32 ++++++ crane_cli/src/main.rs | 9 +- example/bricks/serde/brick.toml | 20 +++- 15 files changed, 343 insertions(+), 84 deletions(-) create mode 100644 crane_cli/src/logging.rs diff --git a/Cargo.lock b/Cargo.lock index 1fb7e1b..b46ada3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -47,7 +47,7 @@ version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" dependencies = [ - "windows-sys", + "windows-sys 0.60.2", ] [[package]] @@ -58,7 +58,7 @@ checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys", + "windows-sys 0.60.2", ] [[package]] @@ -130,12 +130,32 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" +[[package]] +name = "colog" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df62599ba6adc9c6c04a54278c8209125343dc4775f57b9d76c9a4287e58f2bd" +dependencies = [ + "colored", + "env_logger", + "log", +] + [[package]] name = "colorchoice" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +[[package]] +name = "colored" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e" +dependencies = [ + "windows-sys 0.59.0", +] + [[package]] name = "crane_bricks" version = "0.1.0" @@ -155,6 +175,8 @@ dependencies = [ "anyhow", "clap", "clap-verbosity", + "colog", + "colored", "crane_bricks", "env_logger", "fuzzy-matcher", @@ -199,7 +221,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.60.2", ] [[package]] @@ -395,7 +417,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys", + "windows-sys 0.60.2", ] [[package]] @@ -464,7 +486,7 @@ dependencies = [ "getrandom", "once_cell", "rustix", - "windows-sys", + "windows-sys 0.60.2", ] [[package]] @@ -551,13 +573,38 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "windows-targets", + "windows-targets 0.53.3", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", ] [[package]] @@ -567,58 +614,106 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" dependencies = [ "windows-link", - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.53.0", + "windows_aarch64_msvc 0.53.0", + "windows_i686_gnu 0.53.0", + "windows_i686_gnullvm 0.53.0", + "windows_i686_msvc 0.53.0", + "windows_x86_64_gnu 0.53.0", + "windows_x86_64_gnullvm 0.53.0", + "windows_x86_64_msvc 0.53.0", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + [[package]] name = "windows_aarch64_gnullvm" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + [[package]] name = "windows_aarch64_msvc" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + [[package]] name = "windows_i686_gnu" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + [[package]] name = "windows_i686_gnullvm" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + [[package]] name = "windows_i686_msvc" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + [[package]] name = "windows_x86_64_gnu" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + [[package]] name = "windows_x86_64_gnullvm" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + [[package]] name = "windows_x86_64_msvc" version = "0.53.0" diff --git a/crane_bricks/src/actions/insert_file.rs b/crane_bricks/src/actions/insert_file.rs index 344dded..c83972e 100644 --- a/crane_bricks/src/actions/insert_file.rs +++ b/crane_bricks/src/actions/insert_file.rs @@ -1,6 +1,5 @@ use std::path::Path; -use log::debug; use serde::Deserialize; use crate::{ @@ -61,21 +60,42 @@ impl ExecuteAction for InsertFileAction { .collect(); } debug!("{} executing for {} files", brick.name(), files.len()); + if files.len() > 1 { + info!( + "Inserting files: '{}'", + files + .iter() + .map(|file| file.name().to_string()) + .collect::>() + .join("', '") + ) + } else if files.len() == 1 { + info!("Inserting file '{}'", files.first().unwrap().name()); + } else { + warn!("No files found to insert!"); + } for file in files { let target_path = cwd.join(file.name()); let content = file.content().to_string(); if !target_path.exists() { + info!("Created file '{}'", file.name()); file_create_new(context, &target_path, Some(content))?; continue; } + warn!("File '{}' already exists", file.name()); match &self.if_file_exists { FileExistsAction::Append => { + info!("Appending content to file"); file_append_content(context, &target_path, &content)? } FileExistsAction::Replace => { + info!("Replacing all content of file"); file_replace_content(context, &target_path, &content)? } - FileExistsAction::Pass => continue, + FileExistsAction::Pass => { + info!("Continuing"); + continue + }, } } Ok(()) diff --git a/crane_bricks/src/actions/mod.rs b/crane_bricks/src/actions/mod.rs index 48b14e3..fb7ce69 100644 --- a/crane_bricks/src/actions/mod.rs +++ b/crane_bricks/src/actions/mod.rs @@ -5,7 +5,6 @@ pub mod run_command; use std::path::Path; -use log::debug; use serde::Deserialize; use crate::{ diff --git a/crane_bricks/src/actions/modify_file.rs b/crane_bricks/src/actions/modify_file.rs index b64f855..e132c44 100644 --- a/crane_bricks/src/actions/modify_file.rs +++ b/crane_bricks/src/actions/modify_file.rs @@ -1,5 +1,4 @@ use anyhow::{Ok, anyhow}; -use log::debug; use serde::Deserialize; use crate::{ @@ -75,10 +74,14 @@ impl ModifyFileAction { let locations: Vec<(usize, &str)> = source_text.match_indices(&self.selector).collect(); - debug!("Found {} matches", locations.len()); if locations.len() == 0 { return Err(anyhow!("No selector matches in target file!")); } + if locations.len() > 1 { + info!("Found {} matches", locations.len()); + } else { + info!("Found {} match", locations.len()); + } let mut output = source_text.clone(); let start_length = source_text.len(); @@ -93,15 +96,41 @@ impl ModifyFileAction { (modified_index + selected.len()).max(0), &self.content(), ); - }, + } ModifyType::Prepend => { output.insert_str(modified_index, &self.content()); - }, + } ModifyType::Replace => { - output.replace_range(modified_index..(modified_index + selected.len()).max(0), &self.content()); + // TODO: Something isnt right here but im so tired rn pls + // future tiimo fix this + debug!( + "replacing from {} to {} (total chars {})", + modified_index, + (modified_index + selected.len()).max(0), + output.len() + ); + if modified_index > output.len() { + output.insert_str(output.len(), &self.content()); + } else { + output.replace_range( + modified_index..(modified_index + selected.len()), + &self.content(), + ); + } } } } + match &self.r#type { + ModifyType::Append => { + info!("Appended to all matches"); + } + ModifyType::Prepend => { + info!("Prepended to all matches"); + } + ModifyType::Replace => { + info!("Replaced all matches"); + } + } Ok(output) } } @@ -124,6 +153,7 @@ impl ExecuteAction for ModifyFileAction { if !target_path.exists() { return Err(anyhow!("Target file does not exist!")); } + info!("Modifying file '{}'", target_path.display()); let content = file_read_content(context, &target_path)?; file_replace_content(context, &target_path, &self.modify_content(content)?)?; } diff --git a/crane_bricks/src/actions/run_command.rs b/crane_bricks/src/actions/run_command.rs index 969bdaa..72aaea9 100644 --- a/crane_bricks/src/actions/run_command.rs +++ b/crane_bricks/src/actions/run_command.rs @@ -37,6 +37,7 @@ impl ExecuteAction for RunCommandAction { _brick: &Brick, cwd: &Path, ) -> anyhow::Result<()> { + info!("Running command"); if context.dry_run { return Ok(()); } diff --git a/crane_bricks/src/brick.rs b/crane_bricks/src/brick.rs index ef7abf1..127b92d 100644 --- a/crane_bricks/src/brick.rs +++ b/crane_bricks/src/brick.rs @@ -4,7 +4,6 @@ use std::{ }; use anyhow::anyhow; -use log::debug; use serde::Deserialize; use crate::{ @@ -18,6 +17,8 @@ const BRICK_CONFIG_FILE: &'static str = "brick.toml"; #[derive(Deserialize, Debug, Clone, Default, PartialEq, Eq)] pub struct BrickConfig { name: String, + + #[serde(default)] actions: Vec, } @@ -25,11 +26,11 @@ impl BrickConfig { pub fn new(name: String, actions: Vec) -> Self { Self { name, actions } } - + pub fn name(&self) -> &str { &self.name } - + pub fn actions(&self) -> &[Action] { &self.actions } @@ -151,6 +152,14 @@ pub fn bricks(dir: &PathBuf) -> Vec { return vec![]; }; dirs.iter() - .filter_map(|dir| Brick::try_from(dir.clone()).ok()) + .filter_map(|dir| { + match Brick::try_from(dir.clone()) { + Ok(brick) => Some(brick), + Err(error) => { + warn!("Failed to create brick at '{}'. Error: {}", dir.display(), error); + None + }, + } + }) .collect::>() } diff --git a/crane_bricks/src/file_utils.rs b/crane_bricks/src/file_utils.rs index 6448e23..dfeca91 100644 --- a/crane_bricks/src/file_utils.rs +++ b/crane_bricks/src/file_utils.rs @@ -5,7 +5,6 @@ use std::{ }; use anyhow::anyhow; -use log::debug; use crate::context::ActionContext; diff --git a/crane_bricks/src/lib.rs b/crane_bricks/src/lib.rs index 1faeb38..df5eede 100644 --- a/crane_bricks/src/lib.rs +++ b/crane_bricks/src/lib.rs @@ -1,3 +1,6 @@ +#[macro_use] +extern crate log; + pub mod actions; pub mod brick; pub mod context; diff --git a/crane_cli/Cargo.toml b/crane_cli/Cargo.toml index b188a84..e08aff8 100644 --- a/crane_cli/Cargo.toml +++ b/crane_cli/Cargo.toml @@ -2,6 +2,11 @@ name = "crane_cli" version = "0.1.0" edition = "2024" +author = "timothebot" + +[[bin]] +name = "crane" +path = "src/main.rs" [dependencies] serde = { version = "1.0", features = ["derive"] } @@ -13,3 +18,5 @@ clap-verbosity = "2.1.0" env_logger = "0.11.8" log = "0.4.28" crane_bricks = { path = "../crane_bricks/" } +colog = "1.4.0" +colored = "3.0.0" diff --git a/crane_cli/src/cmd/add.rs b/crane_cli/src/cmd/add.rs index e50b3ee..a4b25de 100644 --- a/crane_cli/src/cmd/add.rs +++ b/crane_cli/src/cmd/add.rs @@ -1,5 +1,6 @@ use std::{env, path::PathBuf}; +use colored::Colorize; use fuzzy_matcher::{FuzzyMatcher, skim::SkimMatcherV2}; use log::{debug, error, info, warn}; @@ -7,13 +8,18 @@ use crate::{ cmd::{Add, Run}, config::CraneConfig, }; -use crane_bricks::brick::{Brick, bricks}; +use crane_bricks::{ + brick::{Brick, bricks}, + context::ActionContext, +}; impl Run for Add { fn run(&self) { let config = CraneConfig::new(); - let brick_dirs = if self.brick_dirs.len() > 0 { - &self.brick_dirs + let brick_dirs = if let Some(brick_dirs) = &self.brick_dirs + && brick_dirs.len() > 0 + { + brick_dirs } else { &config.brick_dirs().to_vec() }; @@ -43,57 +49,78 @@ impl Run for Add { .join("\n* ") ); - let matcher = SkimMatcherV2::default(); + let brick_queries: Vec = self + .bricks + .iter() + .map(|query| { + for alias in config.alias() { + if alias.name().to_lowercase() == query.to_lowercase() { + return alias.bricks().to_vec(); + } + } + return vec![query.clone()]; + }) + .flatten() + .collect(); - for brick_query in &self.bricks { - let mut matches: Vec<(Brick, i64)> = Vec::new(); - let mut highest_score: i64 = 0; + let mut bricks_to_execute: Vec<&Brick> = Vec::new(); + for brick_query in brick_queries { + let mut found = false; for brick in &bricks { - if let Some(score) = - matcher.fuzzy_match(brick.name(), brick_query.as_str()) - { - if score >= highest_score { - matches.push((brick.clone(), score)); - highest_score = score; - } + if brick.name().to_lowercase() == brick_query.to_lowercase() { + bricks_to_execute.push(brick); + found = true; + break; } } - if matches.len() == 1 { - add_brick( - matches.first().unwrap().0.clone(), - &target_dir, - self.dry_run, - ); - } else if matches.len() > 1 { - multiple_matches_found(brick_query.to_string(), matches); - } else { - no_matches_found(brick_query.to_string()); + if !found { + eprintln!("{} Could not find brick '{}'", "⚠".red(), brick_query); } } - } -} - -fn add_brick(brick: Brick, target_dir: &PathBuf, dry_run: bool) { - info!( - "Adding brick '{}', {}, {:?}", - brick.name(), - dry_run, - target_dir - ); -} + /* TODO: render aliases like this: + → Executing 4 bricks + • MIT + • rust (alias) + ◦ author-rust + ◦ serde + • rustfmt + */ + let plural = if bricks_to_execute.len() > 1 { + "s" + } else { + "" + }; + println!( + "{} Executing {} brick{}", + "→".green(), + bricks_to_execute.len().to_string().purple(), + plural + ); + for brick in &bricks_to_execute { + println!(" {} {}", "•".dimmed(), brick.name()) + } -fn no_matches_found(query: String) { - error!("No possible bricks found for '{}'", query); + let context = ActionContext::new(self.dry_run); + for brick in bricks_to_execute { + execute_brick(brick, &context, &target_dir); + } + } } -fn multiple_matches_found(query: String, matches: Vec<(Brick, i64)>) { - warn!( - "Multiple possible bricks found for '{}'\n* {}", - query, - matches - .iter() - .map(|(brick, score)| format!("{} ({})", brick.name(), score)) - .collect::>() - .join("\n* ") +fn execute_brick(brick: &Brick, context: &ActionContext, cwd: &PathBuf) { + println!( + "\n{} Executing brick '{}'", + "→".green(), + brick.name().purple() ); + match brick.execute(context, &cwd) { + Ok(_) => println!( + "{}", + format!("✔ Successfully executed '{}'! ◝(°ᗜ°)◜", brick.name().bold()).green() + ), + Err(_) => eprintln!( + "{}", + format!("✘ Failed to execute '{}'! ヽ(°〇°)ノ", brick.name().bold()).red() + ), + } } diff --git a/crane_cli/src/cmd/cmd.rs b/crane_cli/src/cmd/cmd.rs index 72f91b1..27767a7 100644 --- a/crane_cli/src/cmd/cmd.rs +++ b/crane_cli/src/cmd/cmd.rs @@ -1,7 +1,7 @@ use std::path::PathBuf; use clap::{Parser, Subcommand, ValueHint, command}; -use clap_verbosity::Verbosity; +use clap_verbosity::{InfoLevel, Verbosity}; #[derive(Debug, Parser)] #[command(version, about, long_about = None)] @@ -10,7 +10,7 @@ pub struct CraneCli { pub command: CraneCommand, #[command(flatten)] - pub verbose: Verbosity, + pub verbose: Verbosity, } #[derive(Subcommand, Debug)] @@ -25,8 +25,8 @@ pub struct Add { #[clap(num_args = 1.., required = true)] pub bricks: Vec, - #[arg(short, long, value_hint=ValueHint::DirPath, value_terminator=",", default_value="")] - pub brick_dirs: Vec, + #[arg(short, long, value_hint=ValueHint::DirPath, value_terminator=",")] + pub brick_dirs: Option>, #[arg(short, long, value_hint=ValueHint::DirPath)] pub target_dir: Option, diff --git a/crane_cli/src/config.rs b/crane_cli/src/config.rs index d498b25..c522303 100644 --- a/crane_cli/src/config.rs +++ b/crane_cli/src/config.rs @@ -15,9 +15,26 @@ pub fn config_dir() -> PathBuf { } } +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct Alias { + name: String, + bricks: Vec, +} + +impl Alias { + pub fn name(&self) -> &str { + &self.name + } + + pub fn bricks(&self) -> &[String] { + &self.bricks + } +} + #[derive(Serialize, Deserialize, Debug, Clone)] pub struct CraneConfig { brick_dirs: Vec, + alias: Vec, } impl CraneConfig { @@ -32,12 +49,17 @@ impl CraneConfig { } Self { brick_dirs: vec![config_dir().join("bricks")], + alias: vec![] } } pub fn brick_dirs(&self) -> &[PathBuf] { &self.brick_dirs } + + pub fn alias(&self) -> &Vec { + &self.alias + } } #[cfg(test)] diff --git a/crane_cli/src/logging.rs b/crane_cli/src/logging.rs new file mode 100644 index 0000000..6b2a5a5 --- /dev/null +++ b/crane_cli/src/logging.rs @@ -0,0 +1,32 @@ +use clap_verbosity::{InfoLevel, Verbosity}; +use colog::format::CologStyle; +use colored::Colorize; +use env_logger::Builder; +use log::Level; + +struct CustomLogStyle {} + +impl CologStyle for CustomLogStyle { + fn prefix_token(&self, level: &Level) -> String { + let prefix = match level { + Level::Error => "⚠".red(), + Level::Warn => "⚠".yellow(), + Level::Info => ">".dimmed(), + Level::Debug => { + return format!("{}", "[DEBUG]".red()); + } + Level::Trace => { + return format!("{}", "[TRACE]".red()); + } + }; + + format!(" {}", prefix) + } +} + +pub fn setup(verbose: &Verbosity) { + let mut builder = Builder::new(); + builder.filter_level(verbose.log_level_filter()); + builder.format(colog::formatter(CustomLogStyle {})); + builder.init(); +} diff --git a/crane_cli/src/main.rs b/crane_cli/src/main.rs index f244a9a..8910b17 100644 --- a/crane_cli/src/main.rs +++ b/crane_cli/src/main.rs @@ -1,16 +1,15 @@ + + use clap::Parser; use crate::cmd::{CraneCli, Run}; mod cmd; mod config; +mod logging; fn main() { let cli = CraneCli::parse(); - - env_logger::Builder::new() - .filter_level(cli.verbose.log_level_filter()) - .init(); - + logging::setup(&cli.verbose); cli.run(); } diff --git a/example/bricks/serde/brick.toml b/example/bricks/serde/brick.toml index 5bdc256..768563e 100644 --- a/example/bricks/serde/brick.toml +++ b/example/bricks/serde/brick.toml @@ -1,4 +1,20 @@ name = "serde" -action = "command" -action_settings = "echo 'hi'" \ No newline at end of file +[[actions]] +action = "modify_file" + +# The content that will be inserted into the file. +content = "a" # or file (prefix path with file:) + +# Where the modification should happen +selector = "MIT license" # either text or regex (prefix with re:) + +# Modify can be type "append" (default), "prepend" or "replace": +# append text inside file +type = "replace" + + +# Specify which files this action applies to +sources = [ + "LICENSE" +] \ No newline at end of file From e4b66cf0dd36341ba1e1d609255f3351b8167c94 Mon Sep 17 00:00:00 2001 From: Timo Borer Date: Mon, 13 Oct 2025 15:31:36 +0200 Subject: [PATCH 10/20] feat: Add aliases, improve general structure, improve docs, update examples --- Cargo.lock | 18 +-- Cargo.toml | 4 +- README.md | 14 +++ book/src/intro.md | 20 ++- {crane_cli => crane}/Cargo.toml | 3 +- {crane_cli => crane}/src/cmd/add.rs | 7 +- {crane_cli => crane}/src/cmd/cmd.rs | 4 +- crane/src/cmd/list.rs | 43 +++++++ {crane_cli => crane}/src/cmd/mod.rs | 0 crane/src/config.rs | 149 +++++++++++++++++++++++ {crane_cli => crane}/src/logging.rs | 0 {crane_cli => crane}/src/main.rs | 0 crane_bricks/src/brick.rs | 2 +- crane_cli/src/cmd/list.rs | 16 --- crane_cli/src/config.rs | 80 ------------ example/config.toml | 6 + example/foo/other_bricks/smile/smile.txt | 1 + 17 files changed, 250 insertions(+), 117 deletions(-) rename {crane_cli => crane}/Cargo.toml (90%) rename {crane_cli => crane}/src/cmd/add.rs (94%) rename {crane_cli => crane}/src/cmd/cmd.rs (88%) create mode 100644 crane/src/cmd/list.rs rename {crane_cli => crane}/src/cmd/mod.rs (100%) create mode 100644 crane/src/config.rs rename {crane_cli => crane}/src/logging.rs (100%) rename {crane_cli => crane}/src/main.rs (100%) delete mode 100644 crane_cli/src/cmd/list.rs delete mode 100644 crane_cli/src/config.rs create mode 100644 example/foo/other_bricks/smile/smile.txt diff --git a/Cargo.lock b/Cargo.lock index b46ada3..9e4f2ec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -157,31 +157,31 @@ dependencies = [ ] [[package]] -name = "crane_bricks" +name = "crane" version = "0.1.0" dependencies = [ "anyhow", + "clap", + "clap-verbosity", + "colog", + "colored", + "crane_bricks", "env_logger", + "fuzzy-matcher", "log", "serde", - "tempfile", "toml", ] [[package]] -name = "crane_cli" +name = "crane_bricks" version = "0.1.0" dependencies = [ "anyhow", - "clap", - "clap-verbosity", - "colog", - "colored", - "crane_bricks", "env_logger", - "fuzzy-matcher", "log", "serde", + "tempfile", "toml", ] diff --git a/Cargo.toml b/Cargo.toml index 4a51360..7c701cf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,5 +2,5 @@ resolver = "3" members = [ "crane_bricks", - "crane_cli" -] \ No newline at end of file + "crane" +] diff --git a/README.md b/README.md index 45271d2..a0a0d9e 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,17 @@ # crane 🏗️ Easily add bricks (files or snippets) to your projects. + +🧱 A brick is an instruction. It can be a file that gets added, a command that +executes or lines getting replaced in a target file. + +> 🚧 crane is currently being worked on :D +> Some features that are already documented don't work yet +> and everything could change until a stable release! + +## ToDo + +- [ ] Regex support +- [ ] Path support +- [ ] Improve readme +- [ ] Variables support diff --git a/book/src/intro.md b/book/src/intro.md index ec49c5e..60d1efa 100644 --- a/book/src/intro.md +++ b/book/src/intro.md @@ -6,6 +6,24 @@ A brick is an instruction. It can be a file that gets added, a command that exec ## Quick Examples -### License +### License brick Instead of having to look up and copy your desired license from the web, you can create a brick out of it and then run `crane add some-license`. + +### Language specific bricks + +You can create multiple bricks and combine them behind an alias. + +This way, you can easily bootstrap new projects! + +```shell +$ cargo new my_project && cd my_project +# ... +$ crane add rust +→ Executing 4 bricks + • mit + • serde + • rustfmt + • rustauthor +# ... +``` \ No newline at end of file diff --git a/crane_cli/Cargo.toml b/crane/Cargo.toml similarity index 90% rename from crane_cli/Cargo.toml rename to crane/Cargo.toml index e08aff8..9232e6e 100644 --- a/crane_cli/Cargo.toml +++ b/crane/Cargo.toml @@ -1,8 +1,7 @@ [package] -name = "crane_cli" +name = "crane" version = "0.1.0" edition = "2024" -author = "timothebot" [[bin]] name = "crane" diff --git a/crane_cli/src/cmd/add.rs b/crane/src/cmd/add.rs similarity index 94% rename from crane_cli/src/cmd/add.rs rename to crane/src/cmd/add.rs index a4b25de..030f8f9 100644 --- a/crane_cli/src/cmd/add.rs +++ b/crane/src/cmd/add.rs @@ -1,15 +1,14 @@ use std::{env, path::PathBuf}; use colored::Colorize; -use fuzzy_matcher::{FuzzyMatcher, skim::SkimMatcherV2}; -use log::{debug, error, info, warn}; +use log::debug; use crate::{ cmd::{Add, Run}, config::CraneConfig, }; use crane_bricks::{ - brick::{Brick, bricks}, + brick::{Brick, bricks_in_dir}, context::ActionContext, }; @@ -38,7 +37,7 @@ impl Run for Add { }; let bricks: Vec = - brick_dirs.iter().map(|dir| bricks(dir)).flatten().collect(); + brick_dirs.iter().map(|dir| bricks_in_dir(dir)).flatten().collect(); debug!( "Found bricks:\n* {}", diff --git a/crane_cli/src/cmd/cmd.rs b/crane/src/cmd/cmd.rs similarity index 88% rename from crane_cli/src/cmd/cmd.rs rename to crane/src/cmd/cmd.rs index 27767a7..b00174c 100644 --- a/crane_cli/src/cmd/cmd.rs +++ b/crane/src/cmd/cmd.rs @@ -38,6 +38,6 @@ pub struct Add { /// List all available bricks #[derive(Debug, Parser, Clone)] pub struct List { - #[arg(short, long, value_hint=ValueHint::DirPath)] - pub brick_dirs: Option, + #[arg(short, long, value_hint=ValueHint::DirPath, value_terminator=",")] + pub brick_dirs: Option>, } diff --git a/crane/src/cmd/list.rs b/crane/src/cmd/list.rs new file mode 100644 index 0000000..8a6d456 --- /dev/null +++ b/crane/src/cmd/list.rs @@ -0,0 +1,43 @@ +use std::fs; + +use colored::Colorize; +use log::info; + +use crate::{ + cmd::{List, Run}, + config::{map_aliases, CraneConfig}, +}; +use crane_bricks::brick::bricks_in_dir; + +impl Run for List { + fn run(&self) { + let config = CraneConfig::new(); + let brick_dirs = if let Some(brick_dirs) = &self.brick_dirs + && brick_dirs.len() > 0 + { + brick_dirs + } else { + &config.brick_dirs().to_vec() + }; + + let alias_mapped = map_aliases(config.alias()); + + for brick_dir in brick_dirs { + println!( + "{} Found brick directory at {}", + "→".green(), + fs::canonicalize(brick_dir) + .unwrap_or(brick_dir.to_path_buf()) + .display() + ); + for brick in bricks_in_dir(brick_dir) { + let mut affix = String::new(); + if let Some(aliases) = alias_mapped.get(brick.name()) { + affix = format!(" (aliased in '{}')", aliases.join("', '")); + } + info!("{}{}", brick.name(), affix.dimmed()); + } + println!() + } + } +} diff --git a/crane_cli/src/cmd/mod.rs b/crane/src/cmd/mod.rs similarity index 100% rename from crane_cli/src/cmd/mod.rs rename to crane/src/cmd/mod.rs diff --git a/crane/src/config.rs b/crane/src/config.rs new file mode 100644 index 0000000..39decfb --- /dev/null +++ b/crane/src/config.rs @@ -0,0 +1,149 @@ +use std::{collections::HashMap, env, fs, path::PathBuf}; + +use serde::{Deserialize, Serialize}; + +const ENV_KEY_CONFIG_DIR: &'static str = "CRANE_CONFIG_DIR"; + +fn config_path_from_env() -> anyhow::Result { + Ok(PathBuf::try_from(env::var(ENV_KEY_CONFIG_DIR)?)?) +} + +pub fn config_dir() -> PathBuf { + match config_path_from_env() { + Ok(path) => path, + Err(_) => PathBuf::from("~/.config/crane"), + } +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct Alias { + name: String, + bricks: Vec, +} + +impl Alias { + #[allow(dead_code)] + pub fn new(name: String, bricks: Vec) -> Self { + Self { name, bricks } + } + + pub fn name(&self) -> &str { + &self.name + } + + pub fn bricks(&self) -> &[String] { + &self.bricks + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, Default)] +pub struct CraneConfig { + #[serde(default)] + brick_dirs: Vec, + + #[serde(default)] + alias: Vec, +} + +impl CraneConfig { + pub fn new() -> Self { + let cnf_dir = config_dir(); + let config_file = cnf_dir.join("config.toml"); + let mut config = { + if config_file.exists() + && let Ok(parsed_config) = toml::from_str::( + fs::read_to_string(config_file).unwrap_or_default().as_str(), + ) + { + parsed_config + } else { + CraneConfig::default() + } + }; + + if config.brick_dirs.is_empty() { + config.brick_dirs.push(PathBuf::from("./bricks")); + } + + config.brick_dirs = config + .brick_dirs() + .into_iter() + .map(|brick_dir| { + if brick_dir.is_absolute() { + brick_dir.clone() + } else { + cnf_dir.join(brick_dir) + } + }) + .collect(); + config + } + + pub fn brick_dirs(&self) -> &[PathBuf] { + &self.brick_dirs + } + + pub fn alias(&self) -> &[Alias] { + &self.alias + } +} + +/// Converts a list of aliases to a map where the brick +/// name is the key and value all aliases +pub fn map_aliases(aliases: &[Alias]) -> HashMap> { + let mut brick_map: HashMap> = HashMap::new(); + for alias in aliases { + for brick in alias.bricks() { + if brick_map.contains_key(brick) { + brick_map + .get_mut(brick) + .unwrap() + .push(alias.name().to_string()); + } else { + brick_map.insert(brick.to_string(), vec![alias.name().to_string()]); + } + } + } + brick_map +} + +#[cfg(test)] +mod tests { + use std::collections::HashMap; + + use super::*; + + #[test] + fn test_config_dir_from_env() { + // should be safe as long as it is only one test (of this kind) + unsafe { + env::set_var(ENV_KEY_CONFIG_DIR, "~/.crane"); + }; + assert_eq!( + format!("{}", config_dir().display()), + String::from("~/.crane") + ) + } + + #[test] + fn test_alias_map() { + let aliases = vec![ + Alias::new( + String::from("hello"), + vec![String::from("brick_a"), String::from("brick_b")], + ), + Alias::new( + String::from("world"), + vec![String::from("brick_c"), String::from("brick_b")], + ), + ]; + let mut brick_map: HashMap> = HashMap::new(); + brick_map.insert(String::from("brick_a"), vec![String::from("hello")]); + brick_map.insert( + String::from("brick_b"), + vec![String::from("hello"), String::from("world")], + ); + brick_map.insert(String::from("brick_c"), vec![String::from("world")]); + assert_eq!(map_aliases(&aliases), brick_map); + } +} diff --git a/crane_cli/src/logging.rs b/crane/src/logging.rs similarity index 100% rename from crane_cli/src/logging.rs rename to crane/src/logging.rs diff --git a/crane_cli/src/main.rs b/crane/src/main.rs similarity index 100% rename from crane_cli/src/main.rs rename to crane/src/main.rs diff --git a/crane_bricks/src/brick.rs b/crane_bricks/src/brick.rs index 127b92d..f856bf1 100644 --- a/crane_bricks/src/brick.rs +++ b/crane_bricks/src/brick.rs @@ -147,7 +147,7 @@ impl TryFrom for Brick { } /// Get all bricks in a directory -pub fn bricks(dir: &PathBuf) -> Vec { +pub fn bricks_in_dir(dir: &PathBuf) -> Vec { let Ok(dirs) = sub_dirs(dir) else { return vec![]; }; diff --git a/crane_cli/src/cmd/list.rs b/crane_cli/src/cmd/list.rs deleted file mode 100644 index 5e32f70..0000000 --- a/crane_cli/src/cmd/list.rs +++ /dev/null @@ -1,16 +0,0 @@ -use log::info; - -use crate::cmd::{List, Run}; -use crane_bricks::brick::bricks; - -impl Run for List { - fn run(&self) { - let Some(path) = self.brick_dirs.clone() else { - return; - }; - for brick in bricks(&path) { - info!("{:?}", brick); - info!("{:#?}", brick.files()); - } - } -} diff --git a/crane_cli/src/config.rs b/crane_cli/src/config.rs deleted file mode 100644 index c522303..0000000 --- a/crane_cli/src/config.rs +++ /dev/null @@ -1,80 +0,0 @@ -use std::{env, fs, path::PathBuf}; - -use serde::{Deserialize, Serialize}; - -const ENV_KEY_CONFIG_DIR: &'static str = "CRANE_CONFIG_DIR"; - -fn config_path_from_env() -> anyhow::Result { - Ok(PathBuf::try_from(env::var(ENV_KEY_CONFIG_DIR)?)?) -} - -pub fn config_dir() -> PathBuf { - match config_path_from_env() { - Ok(path) => path, - Err(_) => PathBuf::from("~/.config/crane"), - } -} - -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct Alias { - name: String, - bricks: Vec, -} - -impl Alias { - pub fn name(&self) -> &str { - &self.name - } - - pub fn bricks(&self) -> &[String] { - &self.bricks - } -} - -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct CraneConfig { - brick_dirs: Vec, - alias: Vec, -} - -impl CraneConfig { - pub fn new() -> Self { - let config_file = config_dir().join("config.toml"); - if config_file.exists() { - if let Ok(parsed_config) = toml::from_str::( - fs::read_to_string(config_file).unwrap_or_default().as_str(), - ) { - return parsed_config; - } - } - Self { - brick_dirs: vec![config_dir().join("bricks")], - alias: vec![] - } - } - - pub fn brick_dirs(&self) -> &[PathBuf] { - &self.brick_dirs - } - - pub fn alias(&self) -> &Vec { - &self.alias - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_config_dir_from_env() { - // should be safe as long as it is only one test (of this kind) - unsafe { - env::set_var(ENV_KEY_CONFIG_DIR, "~/.crane"); - }; - assert_eq!( - format!("{}", config_dir().display()), - String::from("~/.crane") - ) - } -} diff --git a/example/config.toml b/example/config.toml index e69de29..d8cef00 100644 --- a/example/config.toml +++ b/example/config.toml @@ -0,0 +1,6 @@ + +brick_dirs = ["./foo/other_bricks/", "./bricks"] + +[[alias]] +name = "rust" +bricks = ["mit", "serde"] diff --git a/example/foo/other_bricks/smile/smile.txt b/example/foo/other_bricks/smile/smile.txt new file mode 100644 index 0000000..93c710d --- /dev/null +++ b/example/foo/other_bricks/smile/smile.txt @@ -0,0 +1 @@ +:D \ No newline at end of file From e45f43425820c8058fb6e890274d46c1a102838e Mon Sep 17 00:00:00 2001 From: Timo Borer Date: Mon, 13 Oct 2025 15:32:22 +0200 Subject: [PATCH 11/20] ci: Add GitHub workflows + Deploy GH Book + Release action + Lint & Test action --- .github/workflows/book.yml | 39 +++++++++++++ .github/workflows/release.yml | 102 ++++++++++++++++++++++++++++++++++ .github/workflows/rust-ci.yml | 62 +++++++++++++++++++++ 3 files changed, 203 insertions(+) create mode 100644 .github/workflows/book.yml create mode 100644 .github/workflows/release.yml create mode 100644 .github/workflows/rust-ci.yml diff --git a/.github/workflows/book.yml b/.github/workflows/book.yml new file mode 100644 index 0000000..8a1a892 --- /dev/null +++ b/.github/workflows/book.yml @@ -0,0 +1,39 @@ +# From https://github.com/rust-lang/mdBook/wiki/Automated-Deployment%3A-GitHub-Actions#GitHub-Pages-Deploy +name: Deploy GH Pages +on: + push: + branches: + - main + +jobs: + deploy: + runs-on: ubuntu-latest + permissions: + contents: write # To push a branch + pages: write # To push to a GitHub Pages site + id-token: write # To update the deployment status + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Install latest mdbook + run: | + tag=$(curl 'https://api.github.com/repos/rust-lang/mdbook/releases/latest' | jq -r '.tag_name') + url="https://github.com/rust-lang/mdbook/releases/download/${tag}/mdbook-${tag}-x86_64-unknown-linux-gnu.tar.gz" + mkdir mdbook + curl -sSL $url | tar -xz --directory=./mdbook + echo `pwd`/mdbook >> $GITHUB_PATH + - name: Build Book + run: | + cd book + mdbook build + - name: Setup Pages + uses: actions/configure-pages@v4 + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + # Upload entire repository + path: 'book' + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..78b228a --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,102 @@ +name: Release + +on: + workflow_dispatch: + +env: + CARGO_INCREMENTAL: 0 + +permissions: + contents: write + +jobs: + release: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + - target: x86_64-apple-darwin + os: macos-latest + - target: aarch64-apple-darwin + os: macos-latest + - target: x86_64-unknown-linux-gnu + os: ubuntu-latest + - target: aarch64-unknown-linux-gnu + os: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v5 + with: + fetch-depth: 0 + - name: Get version + id: get_version + uses: SebRollen/toml-action@v1.2.0 + with: + file: Cargo.toml + field: package.version + - name: Install Rust + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + profile: minimal + override: true + target: ${{ matrix.target }} + - name: Setup cache + uses: Swatinem/rust-cache@v2.8.1 + with: + key: ${{ matrix.target }} + - name: Install cross + if: ${{ runner.os == 'Linux' }} + uses: actions-rs/cargo@v1 + with: + command: install + args: --color=always --git=https://github.com/cross-rs/cross.git --locked --rev=e281947ca900da425e4ecea7483cfde646c8a1ea --verbose cross + - name: Build binary + uses: actions-rs/cargo@v1 + with: + command: build + args: --release --locked --target=${{ matrix.target }} --color=always --verbose + use-cross: ${{ runner.os == 'Linux' }} + - name: Package (*nix) + env: + BINARY_NAME: "crane" + run: | + tar -c -C target/${{ matrix.target }}/release/ $BINARY_NAME | + gzip --best > \ + $BINARY_NAME-${{ steps.get_version.outputs.value }}-${{ matrix.target }}.tar.gz + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: ${{ matrix.target }} + path: | + *.tar.gz + *.zip + Cargo.lock + - name: Create release + uses: softprops/action-gh-release@v2 + with: + draft: true + files: | + *.tar.gz + *.zip + Cargo.lock + name: v${{ steps.get_version.outputs.value }} + tag_name: ${{ github.ref_name }} + + publish-crates: + name: Publish to crates.io + runs-on: ubuntu-latest + needs: [release] + # Remove this file to enable crates.io publishing + if: false + steps: + - name: Checkout repository + uses: actions/checkout@v5 + with: + fetch-depth: 0 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + - name: Publish to crates.io + run: cargo publish --token ${{ secrets.CARGO_REGISTRY_TOKEN }} diff --git a/.github/workflows/rust-ci.yml b/.github/workflows/rust-ci.yml new file mode 100644 index 0000000..5713c5e --- /dev/null +++ b/.github/workflows/rust-ci.yml @@ -0,0 +1,62 @@ +name: Rust Lint & Test + +on: + pull_request: + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + lint: + name: Lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + + # make sure all code has been formatted with rustfmt + - run: rustup component add rustfmt + - name: check rustfmt + run: cargo fmt -- --check --color always + + # run clippy to verify we have no warnings + - run: rustup component add clippy + - run: cargo fetch + - name: cargo clippy + run: cargo clippy --all-targets -- -D warnings + + test: + name: Test + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + - target: x86_64-apple-darwin + os: macos-latest + - target: aarch64-apple-darwin + os: macos-latest + - target: x86_64-unknown-linux-gnu + os: ubuntu-latest + - target: aarch64-unknown-linux-gnu + os: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + - run: cargo fetch + - name: cargo test build + run: cargo build --tests --release + - name: cargo test + shell: bash + run: cargo test --release + - name: detects powershell + if: ${{ matrix.os != 'macos-14' }} + shell: pwsh + run: cargo test --release -- --ignored is_powershell_true + - name: doesn't detect powershell + if: ${{ matrix.os != 'macos-14' }} + shell: bash + run: cargo test --release -- --ignored is_powershell_false \ No newline at end of file From 7705ee862228fd3597ec9146d3e39b2e9e7b6591 Mon Sep 17 00:00:00 2001 From: Timo Borer Date: Mon, 13 Oct 2025 15:44:02 +0200 Subject: [PATCH 12/20] chore: Add metadata to packages --- crane/Cargo.toml | 6 ++++++ crane_bricks/Cargo.toml | 3 +++ crane_bricks/README.md | 3 +++ 3 files changed, 12 insertions(+) create mode 100644 crane_bricks/README.md diff --git a/crane/Cargo.toml b/crane/Cargo.toml index 9232e6e..c2b4ba6 100644 --- a/crane/Cargo.toml +++ b/crane/Cargo.toml @@ -1,7 +1,13 @@ [package] name = "crane" +description = "Easily add bricks (files or snippets) to your projects!" version = "0.1.0" edition = "2024" +license = "mit" +readme = "../README.md" +repository = "https://github.com/timothebot/crane" +categories = ["command-line-utilities"] +keywords = ["cli", "tool", "utility"] [[bin]] name = "crane" diff --git a/crane_bricks/Cargo.toml b/crane_bricks/Cargo.toml index a7bf667..2e36e3e 100644 --- a/crane_bricks/Cargo.toml +++ b/crane_bricks/Cargo.toml @@ -1,7 +1,10 @@ [package] name = "crane_bricks" +description = "Execute bricks" version = "0.1.0" edition = "2024" +license = "mit" +repository = "https://github.com/timothebot/crane" [dependencies] serde = { version = "1.0", features = ["derive"] } diff --git a/crane_bricks/README.md b/crane_bricks/README.md new file mode 100644 index 0000000..3a2dc07 --- /dev/null +++ b/crane_bricks/README.md @@ -0,0 +1,3 @@ +# crane bricks 🧱 + +Library for https://github.com/timothebot/crane. From af194ac37e47047221417bb73e0ebe695d36374f Mon Sep 17 00:00:00 2001 From: Timo Borer Date: Mon, 13 Oct 2025 15:44:36 +0200 Subject: [PATCH 13/20] fix: License spelling --- crane/Cargo.toml | 2 +- crane_bricks/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crane/Cargo.toml b/crane/Cargo.toml index c2b4ba6..abc8ffc 100644 --- a/crane/Cargo.toml +++ b/crane/Cargo.toml @@ -3,7 +3,7 @@ name = "crane" description = "Easily add bricks (files or snippets) to your projects!" version = "0.1.0" edition = "2024" -license = "mit" +license = "MIT" readme = "../README.md" repository = "https://github.com/timothebot/crane" categories = ["command-line-utilities"] diff --git a/crane_bricks/Cargo.toml b/crane_bricks/Cargo.toml index 2e36e3e..79cb497 100644 --- a/crane_bricks/Cargo.toml +++ b/crane_bricks/Cargo.toml @@ -3,7 +3,7 @@ name = "crane_bricks" description = "Execute bricks" version = "0.1.0" edition = "2024" -license = "mit" +license = "MIT" repository = "https://github.com/timothebot/crane" [dependencies] From f311a90c5142af853f4b694ecd0c4c706ed35a56 Mon Sep 17 00:00:00 2001 From: Timo Borer Date: Mon, 13 Oct 2025 15:53:13 +0200 Subject: [PATCH 14/20] ci: Split release and publish to crates.io into two workflows --- .github/workflows/publish-crate.yml | 33 +++++++++++++++++++++++++++++ .github/workflows/release.yml | 16 -------------- 2 files changed, 33 insertions(+), 16 deletions(-) create mode 100644 .github/workflows/publish-crate.yml diff --git a/.github/workflows/publish-crate.yml b/.github/workflows/publish-crate.yml new file mode 100644 index 0000000..3bcc0a6 --- /dev/null +++ b/.github/workflows/publish-crate.yml @@ -0,0 +1,33 @@ +name: Publish to crates.io + +on: + workflow_dispatch: + inputs: + crate: + description: 'Which crate to publish' + required: true + default: 'crane' + type: choice + options: + - crane + - crane_bricks + +env: + CARGO_INCREMENTAL: 0 + +permissions: + contents: write + +jobs: + publish-crates: + name: Publish to crates.io + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v5 + with: + fetch-depth: 0 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + - name: Publish to crates.io + run: cargo publish -p ${{ inputs.crate }} --token ${{ secrets.CARGO_REGISTRY_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 78b228a..5dd43c5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -84,19 +84,3 @@ jobs: Cargo.lock name: v${{ steps.get_version.outputs.value }} tag_name: ${{ github.ref_name }} - - publish-crates: - name: Publish to crates.io - runs-on: ubuntu-latest - needs: [release] - # Remove this file to enable crates.io publishing - if: false - steps: - - name: Checkout repository - uses: actions/checkout@v5 - with: - fetch-depth: 0 - - uses: dtolnay/rust-toolchain@stable - - uses: Swatinem/rust-cache@v2 - - name: Publish to crates.io - run: cargo publish --token ${{ secrets.CARGO_REGISTRY_TOKEN }} From 2be15ed604a137682434ebc4e24ed1e7d27d3fe0 Mon Sep 17 00:00:00 2001 From: Timo Borer Date: Mon, 13 Oct 2025 15:55:51 +0200 Subject: [PATCH 15/20] chore: Specify crane_bricks lib crate version --- crane/Cargo.toml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/crane/Cargo.toml b/crane/Cargo.toml index abc8ffc..cb38d2e 100644 --- a/crane/Cargo.toml +++ b/crane/Cargo.toml @@ -9,10 +9,6 @@ repository = "https://github.com/timothebot/crane" categories = ["command-line-utilities"] keywords = ["cli", "tool", "utility"] -[[bin]] -name = "crane" -path = "src/main.rs" - [dependencies] serde = { version = "1.0", features = ["derive"] } toml = "0.9.6" @@ -22,6 +18,6 @@ anyhow = "1.0.99" clap-verbosity = "2.1.0" env_logger = "0.11.8" log = "0.4.28" -crane_bricks = { path = "../crane_bricks/" } +crane_bricks = { path = "../crane_bricks/", version = "0.1.0"} colog = "1.4.0" colored = "3.0.0" From e9ea8d54a7f032e59e9556d9829800b29a031c25 Mon Sep 17 00:00:00 2001 From: Timo Borer Date: Mon, 13 Oct 2025 15:56:50 +0200 Subject: [PATCH 16/20] chore: Bump version to 0.2.0 because 0.1.0 already existed This is because the crate was transfered to me --- Cargo.lock | 2 +- crane/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9e4f2ec..8439a03 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -158,7 +158,7 @@ dependencies = [ [[package]] name = "crane" -version = "0.1.0" +version = "0.2.0" dependencies = [ "anyhow", "clap", diff --git a/crane/Cargo.toml b/crane/Cargo.toml index cb38d2e..47822ac 100644 --- a/crane/Cargo.toml +++ b/crane/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "crane" description = "Easily add bricks (files or snippets) to your projects!" -version = "0.1.0" +version = "0.2.0" edition = "2024" license = "MIT" readme = "../README.md" From 53521c754fec789c2ff90c01d1a3a76f538546f4 Mon Sep 17 00:00:00 2001 From: Timo Borer Date: Mon, 13 Oct 2025 16:23:35 +0200 Subject: [PATCH 17/20] feat: Add ability to parse ~ paths, format code and add LICENSE file (using crane btw) --- Cargo.lock | 99 ++++++++++++++++++++++++- LICENSE | 21 ++++++ crane/src/cmd/add.rs | 13 ++-- crane/src/cmd/list.rs | 2 +- crane/src/config.rs | 5 +- crane/src/main.rs | 2 - crane_bricks/Cargo.toml | 1 + crane_bricks/src/actions/insert_file.rs | 4 +- crane_bricks/src/actions/mod.rs | 5 +- crane_bricks/src/brick.rs | 23 +++--- crane_bricks/src/file_utils.rs | 1 + crane_bricks/tests/common/mod.rs | 6 +- 12 files changed, 153 insertions(+), 29 deletions(-) create mode 100644 LICENSE diff --git a/Cargo.lock b/Cargo.lock index 8439a03..38d711c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -181,10 +181,32 @@ dependencies = [ "env_logger", "log", "serde", + "shellexpand", "tempfile", "toml", ] +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.60.2", +] + [[package]] name = "env_filter" version = "0.1.3" @@ -239,6 +261,17 @@ dependencies = [ "thread_local", ] +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", +] + [[package]] name = "getrandom" version = "0.3.3" @@ -248,7 +281,7 @@ dependencies = [ "cfg-if", "libc", "r-efi", - "wasi", + "wasi 0.14.7+wasi-0.2.4", ] [[package]] @@ -309,6 +342,16 @@ version = "0.2.175" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" +[[package]] +name = "libredox" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" +dependencies = [ + "bitflags", + "libc", +] + [[package]] name = "linux-raw-sys" version = "0.11.0" @@ -339,6 +382,12 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + [[package]] name = "portable-atomic" version = "1.11.1" @@ -378,6 +427,17 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.16", + "libredox", + "thiserror", +] + [[package]] name = "regex" version = "1.11.2" @@ -459,6 +519,15 @@ dependencies = [ "serde_core", ] +[[package]] +name = "shellexpand" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b1fdf65dd6331831494dd616b30351c38e96e45921a27745cf98490458b90bb" +dependencies = [ + "dirs", +] + [[package]] name = "strsim" version = "0.11.1" @@ -483,12 +552,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "84fa4d11fadde498443cca10fd3ac23c951f0dc59e080e9f4b93d4df4e4eea53" dependencies = [ "fastrand", - "getrandom", + "getrandom 0.3.3", "once_cell", "rustix", "windows-sys 0.60.2", ] +[[package]] +name = "thiserror" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "thread_local" version = "1.1.9" @@ -549,6 +638,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + [[package]] name = "wasi" version = "0.14.7+wasi-0.2.4" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..145534d --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 timothebot + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/crane/src/cmd/add.rs b/crane/src/cmd/add.rs index 030f8f9..9065013 100644 --- a/crane/src/cmd/add.rs +++ b/crane/src/cmd/add.rs @@ -36,8 +36,11 @@ impl Run for Add { None => &env::current_dir().unwrap(), }; - let bricks: Vec = - brick_dirs.iter().map(|dir| bricks_in_dir(dir)).flatten().collect(); + let bricks: Vec = brick_dirs + .iter() + .map(|dir| bricks_in_dir(dir)) + .flatten() + .collect(); debug!( "Found bricks:\n* {}", @@ -84,11 +87,7 @@ impl Run for Add { ◦ serde • rustfmt */ - let plural = if bricks_to_execute.len() > 1 { - "s" - } else { - "" - }; + let plural = if bricks_to_execute.len() > 1 { "s" } else { "" }; println!( "{} Executing {} brick{}", "→".green(), diff --git a/crane/src/cmd/list.rs b/crane/src/cmd/list.rs index 8a6d456..83e3b82 100644 --- a/crane/src/cmd/list.rs +++ b/crane/src/cmd/list.rs @@ -5,7 +5,7 @@ use log::info; use crate::{ cmd::{List, Run}, - config::{map_aliases, CraneConfig}, + config::{CraneConfig, map_aliases}, }; use crane_bricks::brick::bricks_in_dir; diff --git a/crane/src/config.rs b/crane/src/config.rs index 39decfb..2d74776 100644 --- a/crane/src/config.rs +++ b/crane/src/config.rs @@ -62,7 +62,7 @@ impl CraneConfig { }; if config.brick_dirs.is_empty() { - config.brick_dirs.push(PathBuf::from("./bricks")); + config.brick_dirs.push(PathBuf::from("bricks")); } config.brick_dirs = config @@ -117,6 +117,7 @@ mod tests { fn test_config_dir_from_env() { // should be safe as long as it is only one test (of this kind) unsafe { + // THIS IS NOT THE DEFAULT PATH! env::set_var(ENV_KEY_CONFIG_DIR, "~/.crane"); }; assert_eq!( @@ -124,7 +125,7 @@ mod tests { String::from("~/.crane") ) } - + #[test] fn test_alias_map() { let aliases = vec![ diff --git a/crane/src/main.rs b/crane/src/main.rs index 8910b17..5666ab9 100644 --- a/crane/src/main.rs +++ b/crane/src/main.rs @@ -1,5 +1,3 @@ - - use clap::Parser; use crate::cmd::{CraneCli, Run}; diff --git a/crane_bricks/Cargo.toml b/crane_bricks/Cargo.toml index 79cb497..468441d 100644 --- a/crane_bricks/Cargo.toml +++ b/crane_bricks/Cargo.toml @@ -11,6 +11,7 @@ serde = { version = "1.0", features = ["derive"] } toml = "0.9.6" anyhow = "1.0.99" log = "0.4.28" +shellexpand = "3.1.1" [dev-dependencies] tempfile = "3" diff --git a/crane_bricks/src/actions/insert_file.rs b/crane_bricks/src/actions/insert_file.rs index c83972e..232e0f4 100644 --- a/crane_bricks/src/actions/insert_file.rs +++ b/crane_bricks/src/actions/insert_file.rs @@ -94,8 +94,8 @@ impl ExecuteAction for InsertFileAction { } FileExistsAction::Pass => { info!("Continuing"); - continue - }, + continue; + } } } Ok(()) diff --git a/crane_bricks/src/actions/mod.rs b/crane_bricks/src/actions/mod.rs index fb7ce69..4cf536b 100644 --- a/crane_bricks/src/actions/mod.rs +++ b/crane_bricks/src/actions/mod.rs @@ -8,7 +8,10 @@ use std::path::Path; use serde::Deserialize; use crate::{ - actions::{insert_file::InsertFileAction, modify_file::ModifyFileAction, run_command::RunCommandAction}, + actions::{ + insert_file::InsertFileAction, modify_file::ModifyFileAction, + run_command::RunCommandAction, + }, brick::Brick, context::ActionContext, }; diff --git a/crane_bricks/src/brick.rs b/crane_bricks/src/brick.rs index f856bf1..8850d3b 100644 --- a/crane_bricks/src/brick.rs +++ b/crane_bricks/src/brick.rs @@ -7,7 +7,7 @@ use anyhow::anyhow; use serde::Deserialize; use crate::{ - actions::{insert_file::InsertFileAction, Action, ExecuteAction}, + actions::{Action, ExecuteAction, insert_file::InsertFileAction}, context::ActionContext, file_utils::{sub_dirs, sub_paths}, }; @@ -68,9 +68,7 @@ impl Brick { config: BrickConfig { name, // If no action is configured, InsertFileAction is default - actions: vec![ - Action::InsertFile(InsertFileAction::default()) - ], + actions: vec![Action::InsertFile(InsertFileAction::default())], ..BrickConfig::default() }, source_path, @@ -148,17 +146,20 @@ impl TryFrom for Brick { /// Get all bricks in a directory pub fn bricks_in_dir(dir: &PathBuf) -> Vec { + debug!("{:#?}", sub_dirs(dir)); let Ok(dirs) = sub_dirs(dir) else { return vec![]; }; dirs.iter() - .filter_map(|dir| { - match Brick::try_from(dir.clone()) { - Ok(brick) => Some(brick), - Err(error) => { - warn!("Failed to create brick at '{}'. Error: {}", dir.display(), error); - None - }, + .filter_map(|dir| match Brick::try_from(dir.clone()) { + Ok(brick) => Some(brick), + Err(error) => { + warn!( + "Failed to create brick at '{}'. Error: {}", + dir.display(), + error + ); + None } }) .collect::>() diff --git a/crane_bricks/src/file_utils.rs b/crane_bricks/src/file_utils.rs index dfeca91..53ac479 100644 --- a/crane_bricks/src/file_utils.rs +++ b/crane_bricks/src/file_utils.rs @@ -17,6 +17,7 @@ pub fn sub_dirs(dir: &Path) -> anyhow::Result> { /// Get a vec of all files and folders in the given dir if valid pub fn sub_paths(dir: &Path) -> anyhow::Result> { + let dir = PathBuf::from(shellexpand::tilde(&dir.display().to_string()).to_string()); if !dir.exists() || !dir.is_dir() { return Err(anyhow!("Target does not exist or not a directory")); } diff --git a/crane_bricks/tests/common/mod.rs b/crane_bricks/tests/common/mod.rs index d83ef22..e662d8f 100644 --- a/crane_bricks/tests/common/mod.rs +++ b/crane_bricks/tests/common/mod.rs @@ -1,4 +1,8 @@ -use std::{fs::File, io::{Read, Write}, path::{Path, PathBuf}}; +use std::{ + fs::File, + io::{Read, Write}, + path::{Path, PathBuf}, +}; pub fn init_logger() { let _ = env_logger::builder() From 96f2c43c602fa14e45c266543a490f92cc340fb4 Mon Sep 17 00:00:00 2001 From: Timo Borer Date: Mon, 13 Oct 2025 16:40:43 +0200 Subject: [PATCH 18/20] refactor: Fix clippy lints --- crane/src/cmd/add.rs | 18 ++++++++---------- crane/src/cmd/{cmd.rs => commands.rs} | 0 crane/src/cmd/list.rs | 2 +- crane/src/cmd/mod.rs | 4 ++-- crane/src/config.rs | 6 +++--- crane_bricks/src/actions/insert_file.rs | 5 +---- crane_bricks/src/actions/modify_file.rs | 4 ++-- crane_bricks/src/brick.rs | 9 ++++----- crane_bricks/src/file_utils.rs | 10 +++++----- 9 files changed, 26 insertions(+), 32 deletions(-) rename crane/src/cmd/{cmd.rs => commands.rs} (100%) diff --git a/crane/src/cmd/add.rs b/crane/src/cmd/add.rs index 9065013..259aee9 100644 --- a/crane/src/cmd/add.rs +++ b/crane/src/cmd/add.rs @@ -1,4 +1,4 @@ -use std::{env, path::PathBuf}; +use std::{env, path::Path}; use colored::Colorize; use log::debug; @@ -16,7 +16,7 @@ impl Run for Add { fn run(&self) { let config = CraneConfig::new(); let brick_dirs = if let Some(brick_dirs) = &self.brick_dirs - && brick_dirs.len() > 0 + && brick_dirs.is_empty() { brick_dirs } else { @@ -38,8 +38,7 @@ impl Run for Add { let bricks: Vec = brick_dirs .iter() - .map(|dir| bricks_in_dir(dir)) - .flatten() + .flat_map(|dir| bricks_in_dir(dir)) .collect(); debug!( @@ -54,15 +53,14 @@ impl Run for Add { let brick_queries: Vec = self .bricks .iter() - .map(|query| { + .flat_map(|query| { for alias in config.alias() { if alias.name().to_lowercase() == query.to_lowercase() { return alias.bricks().to_vec(); } } - return vec![query.clone()]; + vec![query.clone()] }) - .flatten() .collect(); let mut bricks_to_execute: Vec<&Brick> = Vec::new(); @@ -100,18 +98,18 @@ impl Run for Add { let context = ActionContext::new(self.dry_run); for brick in bricks_to_execute { - execute_brick(brick, &context, &target_dir); + execute_brick(brick, &context, target_dir); } } } -fn execute_brick(brick: &Brick, context: &ActionContext, cwd: &PathBuf) { +fn execute_brick(brick: &Brick, context: &ActionContext, cwd: &Path) { println!( "\n{} Executing brick '{}'", "→".green(), brick.name().purple() ); - match brick.execute(context, &cwd) { + match brick.execute(context, cwd) { Ok(_) => println!( "{}", format!("✔ Successfully executed '{}'! ◝(°ᗜ°)◜", brick.name().bold()).green() diff --git a/crane/src/cmd/cmd.rs b/crane/src/cmd/commands.rs similarity index 100% rename from crane/src/cmd/cmd.rs rename to crane/src/cmd/commands.rs diff --git a/crane/src/cmd/list.rs b/crane/src/cmd/list.rs index 83e3b82..c2a8704 100644 --- a/crane/src/cmd/list.rs +++ b/crane/src/cmd/list.rs @@ -13,7 +13,7 @@ impl Run for List { fn run(&self) { let config = CraneConfig::new(); let brick_dirs = if let Some(brick_dirs) = &self.brick_dirs - && brick_dirs.len() > 0 + && brick_dirs.is_empty() { brick_dirs } else { diff --git a/crane/src/cmd/mod.rs b/crane/src/cmd/mod.rs index 8c6082c..b87c666 100644 --- a/crane/src/cmd/mod.rs +++ b/crane/src/cmd/mod.rs @@ -1,8 +1,8 @@ mod add; -mod cmd; +mod commands; mod list; -pub use crate::cmd::cmd::*; +pub use crate::cmd::commands::*; pub trait Run { fn run(&self); diff --git a/crane/src/config.rs b/crane/src/config.rs index 2d74776..ef4b14d 100644 --- a/crane/src/config.rs +++ b/crane/src/config.rs @@ -2,10 +2,10 @@ use std::{collections::HashMap, env, fs, path::PathBuf}; use serde::{Deserialize, Serialize}; -const ENV_KEY_CONFIG_DIR: &'static str = "CRANE_CONFIG_DIR"; +const ENV_KEY_CONFIG_DIR: &str = "CRANE_CONFIG_DIR"; fn config_path_from_env() -> anyhow::Result { - Ok(PathBuf::try_from(env::var(ENV_KEY_CONFIG_DIR)?)?) + Ok(PathBuf::from(env::var(ENV_KEY_CONFIG_DIR)?)) } pub fn config_dir() -> PathBuf { @@ -67,7 +67,7 @@ impl CraneConfig { config.brick_dirs = config .brick_dirs() - .into_iter() + .iter() .map(|brick_dir| { if brick_dir.is_absolute() { brick_dir.clone() diff --git a/crane_bricks/src/actions/insert_file.rs b/crane_bricks/src/actions/insert_file.rs index 232e0f4..a676312 100644 --- a/crane_bricks/src/actions/insert_file.rs +++ b/crane_bricks/src/actions/insert_file.rs @@ -54,10 +54,7 @@ impl ExecuteAction for InsertFileAction { ) -> anyhow::Result<()> { let mut files = brick.files(); if !&self.common.sources.is_empty() { - files = files - .into_iter() - .filter(|file| *&self.common.sources.contains(&file.name().to_string())) - .collect(); + files.retain(|file| self.common.sources.contains(&file.name().to_string())); } debug!("{} executing for {} files", brick.name(), files.len()); if files.len() > 1 { diff --git a/crane_bricks/src/actions/modify_file.rs b/crane_bricks/src/actions/modify_file.rs index e132c44..36eaedb 100644 --- a/crane_bricks/src/actions/modify_file.rs +++ b/crane_bricks/src/actions/modify_file.rs @@ -74,7 +74,7 @@ impl ModifyFileAction { let locations: Vec<(usize, &str)> = source_text.match_indices(&self.selector).collect(); - if locations.len() == 0 { + if locations.is_empty() { return Err(anyhow!("No selector matches in target file!")); } if locations.len() > 1 { @@ -147,7 +147,7 @@ impl ExecuteAction for ModifyFileAction { .iter() .map(|brick_file| brick_file.name().to_string()) .collect(); - files.extend(self.common.sources.clone().into_iter()); + files.extend(self.common.sources.clone()); for file in files { let target_path = cwd.join(file); if !target_path.exists() { diff --git a/crane_bricks/src/brick.rs b/crane_bricks/src/brick.rs index 8850d3b..15cbad3 100644 --- a/crane_bricks/src/brick.rs +++ b/crane_bricks/src/brick.rs @@ -12,7 +12,7 @@ use crate::{ file_utils::{sub_dirs, sub_paths}, }; -const BRICK_CONFIG_FILE: &'static str = "brick.toml"; +const BRICK_CONFIG_FILE: &str = "brick.toml"; #[derive(Deserialize, Debug, Clone, Default, PartialEq, Eq)] pub struct BrickConfig { @@ -69,7 +69,6 @@ impl Brick { name, // If no action is configured, InsertFileAction is default actions: vec![Action::InsertFile(InsertFileAction::default())], - ..BrickConfig::default() }, source_path, } @@ -96,14 +95,14 @@ impl Brick { pub fn execute(&self, context: &ActionContext, cwd: &Path) -> anyhow::Result<()> { for action in &self.config.actions { - action.execute(context, &self, cwd)?; + action.execute(context, self, cwd)?; } Ok(()) } /// Returns a list of all files that pub fn files(&self) -> Vec { - let Ok(paths) = sub_paths(&self.path()) else { + let Ok(paths) = sub_paths(self.path()) else { return vec![]; }; paths @@ -145,7 +144,7 @@ impl TryFrom for Brick { } /// Get all bricks in a directory -pub fn bricks_in_dir(dir: &PathBuf) -> Vec { +pub fn bricks_in_dir(dir: &Path) -> Vec { debug!("{:#?}", sub_dirs(dir)); let Ok(dirs) = sub_dirs(dir) else { return vec![]; diff --git a/crane_bricks/src/file_utils.rs b/crane_bricks/src/file_utils.rs index 53ac479..5cb8c20 100644 --- a/crane_bricks/src/file_utils.rs +++ b/crane_bricks/src/file_utils.rs @@ -35,7 +35,7 @@ pub fn file_create_new( if !ctx.dry_run { debug!("Creating new file '{:?}'", path); let mut file = File::create_new(path)?; - file.write(content.unwrap_or_default().as_bytes())?; + file.write_all(content.unwrap_or_default().as_bytes())?; } Ok(()) } @@ -63,8 +63,8 @@ pub fn file_replace_content( if ctx.dry_run { return Ok(()); } - let mut file = File::options().write(true).create(true).open(&path)?; - file.write(content.as_bytes())?; + let mut file = File::options().write(true).create(true).truncate(true).open(path)?; + file.write_all(content.as_bytes())?; Ok(()) } @@ -76,7 +76,7 @@ pub fn file_append_content( if ctx.dry_run { return Ok(()); } - let mut file = File::options().append(true).create(true).open(&path)?; - file.write(content.as_bytes())?; + let mut file = File::options().append(true).create(true).open(path)?; + file.write_all(content.as_bytes())?; Ok(()) } From d3f1b1dd0af2b2f6e6aade0a1ff40f404c09d1d8 Mon Sep 17 00:00:00 2001 From: Timo Borer Date: Mon, 13 Oct 2025 16:42:09 +0200 Subject: [PATCH 19/20] style: Run cargo fmt again --- crane_bricks/src/file_utils.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crane_bricks/src/file_utils.rs b/crane_bricks/src/file_utils.rs index 5cb8c20..afdaf90 100644 --- a/crane_bricks/src/file_utils.rs +++ b/crane_bricks/src/file_utils.rs @@ -63,7 +63,11 @@ pub fn file_replace_content( if ctx.dry_run { return Ok(()); } - let mut file = File::options().write(true).create(true).truncate(true).open(path)?; + let mut file = File::options() + .write(true) + .create(true) + .truncate(true) + .open(path)?; file.write_all(content.as_bytes())?; Ok(()) } From 5436cb72045cb68afcfa23e0373280990a58c055 Mon Sep 17 00:00:00 2001 From: Timo Borer Date: Mon, 13 Oct 2025 16:45:51 +0200 Subject: [PATCH 20/20] refactor: Fix clippy issue in test env --- crane_bricks/tests/common/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crane_bricks/tests/common/mod.rs b/crane_bricks/tests/common/mod.rs index e662d8f..79eb07f 100644 --- a/crane_bricks/tests/common/mod.rs +++ b/crane_bricks/tests/common/mod.rs @@ -32,7 +32,7 @@ pub fn add_test_data(temp: &Path, file: &str) { data_file.read_to_string(&mut content).unwrap(); let mut target_file = File::create(temp.join(file)).unwrap(); - target_file.write(content.as_bytes()).unwrap(); + target_file.write_all(content.as_bytes()).unwrap(); } pub fn file_content(path: &Path) -> String {