diff --git a/CHANGELOG.md b/CHANGELOG.md index fd6b3d90d..c69fcf82d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## Unreleased + +* `loco doctor` now checks for app-specific minimum dependency versions. This should help in upgrades. `doctor` also supports "production only" checks which you can run in production with `loco doctor --production`. This, for example, will check your connections but will not check dependencies. +* + ## v0.12.0 This release have been primarily about cleanups and simplification. diff --git a/Cargo.toml b/Cargo.toml index 0a1fd061c..ea8c63ca5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -62,6 +62,7 @@ serde = { workspace = true } serde_json = { workspace = true } serde_yaml = "0.9" serde_variant = "0.1.2" +toml = "0.8" async-trait = { workspace = true } @@ -69,7 +70,6 @@ async-trait = { workspace = true } axum = { workspace = true } axum-extra = { version = "0.9", features = ["cookie"] } regex = { workspace = true } -lazy_static = { workspace = true } fs-err = "2.11.0" # mailer tera = "1.19.1" @@ -143,7 +143,6 @@ regex = "1" thiserror = "1" serde = "1" serde_json = "1" -lazy_static = "1.4.0" async-trait = { version = "0.1.74" } axum = { version = "0.7.5", features = ["macros"] } tower = "0.4" diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index aeca31b39..95074a7cf 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -1,3 +1,21 @@ +## Blessed depdenencies maintenance and `loco doctor` + +Loco contain a few major and "blessed" dependencies, these appear **both** in an app that was generated at the surface level in their `Cargo.toml` and in the core Loco framework. + +If stale, may require an upgrade as a must. + +Example for such dependencies: + +* The `sea-orm-cli` - while Loco uses `SeaORM`, it uses the `SeaORM` CLI to generate entities, and so there may be an incompatibility if `SeaORM` has a too large breaking change between their CLI (which ships separately) and their framework. +* `axum` +* etc. + +This is why we are checking these automatically as part of `loco doctor`. + +We keep minimal version requirements for these. As a maintainer, you can update these **minimal** versions, only if required in [`doctor.rs`](src/doctor.rs). + + + ## Running Tests Before running tests make sure that: diff --git a/docs-site/content/docs/infrastructure/deployment.md b/docs-site/content/docs/infrastructure/deployment.md index b9ae14394..e960be42f 100644 --- a/docs-site/content/docs/infrastructure/deployment.md +++ b/docs-site/content/docs/infrastructure/deployment.md @@ -137,6 +137,13 @@ auth: ``` +## Running `loco doctor` + +You can run `loco doctor` in your server to check the connection health of your environment. + +```sh +$ myapp doctor --production +``` ## Generate diff --git a/examples/demo/Cargo.lock b/examples/demo/Cargo.lock index 579deb61b..51b6663af 100644 --- a/examples/demo/Cargo.lock +++ b/examples/demo/Cargo.lock @@ -2602,13 +2602,12 @@ dependencies = [ [[package]] name = "loco-gen" -version = "0.11.1" +version = "0.12.0" dependencies = [ "chrono", "clap", "dialoguer", "duct", - "lazy_static", "regex", "rrgen", "serde", @@ -2619,7 +2618,7 @@ dependencies = [ [[package]] name = "loco-rs" -version = "0.11.1" +version = "0.12.0" dependencies = [ "argon2", "async-trait", @@ -2644,7 +2643,6 @@ dependencies = [ "include_dir", "ipnetwork", "jsonwebtoken", - "lazy_static", "lettre", "loco-gen", "mime", @@ -2665,6 +2663,7 @@ dependencies = [ "thiserror", "tokio", "tokio-cron-scheduler", + "toml", "tower 0.4.13", "tower-http", "tracing", @@ -5044,6 +5043,18 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac2caab0bf757388c6c0ae23b3293fdb463fee59434529014f85e3263b995c28" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit 0.22.16", +] + [[package]] name = "toml_datetime" version = "0.6.6" @@ -5066,9 +5077,9 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.22.14" +version = "0.22.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f21c7aaf97f1bd9ca9d4f9e73b0a6c74bd5afef56f2bc931943a6e1c37e04e38" +checksum = "278f3d518e152219c994ce877758516bca5e118eaed6996192a774fb9fbf0788" dependencies = [ "indexmap", "serde", @@ -5260,7 +5271,7 @@ dependencies = [ "serde", "shlex", "snapbox", - "toml_edit 0.22.14", + "toml_edit 0.22.16", ] [[package]] diff --git a/examples/demo/src/app.rs b/examples/demo/src/app.rs index b2ba52f48..6649c3223 100644 --- a/examples/demo/src/app.rs +++ b/examples/demo/src/app.rs @@ -42,8 +42,8 @@ impl Hooks for App { } // - async fn initializers(ctx: &AppContext) -> Result>> { - let mut initializers: Vec> = vec![ + async fn initializers(_ctx: &AppContext) -> Result>> { + let initializers: Vec> = vec![ Box::new(initializers::axum_session::AxumSessionInitializer), Box::new(initializers::view_engine::ViewEngineInitializer), Box::new(initializers::hello_view_engine::HelloViewEngineInitializer), diff --git a/examples/demo/tests/cmd/cli.trycmd b/examples/demo/tests/cmd/cli.trycmd index 035e830b2..d3be0c88d 100644 --- a/examples/demo/tests/cmd/cli.trycmd +++ b/examples/demo/tests/cmd/cli.trycmd @@ -131,6 +131,8 @@ $ demo_app-cli doctor ✅ SeaORM CLI is installed ✅ DB connection: success ✅ redis queue: queue connection: success +✅ Dependencies + ``` diff --git a/examples/demo/tests/requests/notes.rs b/examples/demo/tests/requests/notes.rs index 8b7352ef4..ec7ec2f83 100644 --- a/examples/demo/tests/requests/notes.rs +++ b/examples/demo/tests/requests/notes.rs @@ -3,7 +3,6 @@ use insta::{assert_debug_snapshot, with_settings}; use loco_rs::testing; use rstest::rstest; use sea_orm::entity::prelude::*; -use serde_json; use serial_test::serial; // TODO: see how to dedup / extract this to app-local test utils @@ -34,7 +33,7 @@ async fn can_get_notes(#[case] test_name: &str, #[case] params: serde_json::Valu with_settings!({ filters => { - let mut combined_filters = testing::CLEANUP_DATE.to_vec(); + let mut combined_filters = testing::get_cleanup_date().clone(); combined_filters.extend(vec![(r#"\"id\\":\d+"#, r#""id\":ID"#)]); combined_filters } @@ -62,7 +61,7 @@ async fn can_add_note() { with_settings!({ filters => { - let mut combined_filters = testing::CLEANUP_DATE.to_vec(); + let mut combined_filters = testing::get_cleanup_date().clone(); combined_filters.extend(vec![(r#"\"id\\":\d+"#, r#""id\":ID"#)]); combined_filters } @@ -87,7 +86,7 @@ async fn can_get_note() { with_settings!({ filters => { - let mut combined_filters = testing::CLEANUP_DATE.to_vec(); + let mut combined_filters = testing::get_cleanup_date().clone(); combined_filters.extend(vec![(r#"\"id\\":\d+"#, r#""id\":ID"#)]); combined_filters } @@ -113,7 +112,7 @@ async fn can_delete_note() { with_settings!({ filters => { - let mut combined_filters = testing::CLEANUP_DATE.to_vec(); + let mut combined_filters = testing::get_cleanup_date().clone(); combined_filters.extend(vec![(r#"\"id\\":\d+"#, r#""id\":ID"#)]); combined_filters } diff --git a/loco-cli/src/messages.rs b/loco-cli/src/messages.rs index a977e061d..a6358c305 100644 --- a/loco-cli/src/messages.rs +++ b/loco-cli/src/messages.rs @@ -37,7 +37,7 @@ pub fn for_options( match assetopt { AssetsOption::Clientside => res.push(format!( "{}: You've selected `{}` for your asset serving configuration.\n\nNext step, build \ - your frontend:\n $ cd {}\n $ npm install && npm build\n", + your frontend:\n $ cd {}\n $ npm install && npm run build\n", "assets".underline(), "clientside".yellow(), "frontend/".yellow() diff --git a/loco-gen/Cargo.toml b/loco-gen/Cargo.toml index 07038d80a..2f14271ca 100644 --- a/loco-gen/Cargo.toml +++ b/loco-gen/Cargo.toml @@ -14,7 +14,6 @@ path = "src/lib.rs" [dependencies] -lazy_static = { workspace = true } rrgen = "0.5.3" serde = { workspace = true } serde_json = { workspace = true } diff --git a/loco-gen/src/lib.rs b/loco-gen/src/lib.rs index c59ce1e09..2f5f11daa 100644 --- a/loco-gen/src/lib.rs +++ b/loco-gen/src/lib.rs @@ -2,7 +2,6 @@ // TODO: should be more properly aligned with extracting out the db-related gen // code and then feature toggling it #![allow(dead_code)] -use lazy_static::lazy_static; use rrgen::{GenResult, RRgen}; use serde::{Deserialize, Serialize}; use serde_json::json; @@ -14,7 +13,7 @@ mod model; mod scaffold; #[cfg(test)] mod testutil; -use std::str::FromStr; +use std::{str::FromStr, sync::OnceLock}; const CONTROLLER_T: &str = include_str!("templates/controller.t"); const CONTROLLER_TEST_T: &str = include_str!("templates/request_test.t"); @@ -109,11 +108,13 @@ impl Mappings { } } -lazy_static! { - static ref MAPPINGS: Mappings = { +static MAPPINGS: OnceLock = OnceLock::new(); + +fn get_mappings() -> &'static Mappings { + MAPPINGS.get_or_init(|| { let json_data = include_str!("./mappings.json"); serde_json::from_str(json_data).expect("JSON was not well-formatted") - }; + }) } #[derive(clap::ValueEnum, Clone, Debug)] diff --git a/loco-gen/src/model.rs b/loco-gen/src/model.rs index 8b91ef241..a689bc126 100644 --- a/loco-gen/src/model.rs +++ b/loco-gen/src/model.rs @@ -6,11 +6,12 @@ use rrgen::RRgen; use serde_json::json; use super::{Error, Result}; +use crate::get_mappings; const MODEL_T: &str = include_str!("templates/model.t"); const MODEL_TEST_T: &str = include_str!("templates/model_test.t"); -use super::{collect_messages, AppInfo, MAPPINGS}; +use super::{collect_messages, AppInfo}; /// skipping some fields from the generated models. /// For example, the `created_at` and `updated_at` fields are automatically @@ -44,11 +45,12 @@ pub fn generate( // user, user_id references.push((fname, fkey)); } else { - let schema_type = MAPPINGS.schema_field(ftype.as_str()).ok_or_else(|| { + let mappings = get_mappings(); + let schema_type = mappings.schema_field(ftype.as_str()).ok_or_else(|| { Error::Message(format!( "type: {} not found. try any of: {:?}", ftype, - MAPPINGS.schema_fields() + mappings.schema_fields() )) })?; columns.push((fname.to_string(), schema_type.as_str())); diff --git a/loco-gen/src/scaffold.rs b/loco-gen/src/scaffold.rs index d057f482b..bf4d945ad 100644 --- a/loco-gen/src/scaffold.rs +++ b/loco-gen/src/scaffold.rs @@ -1,7 +1,7 @@ use rrgen::RRgen; use serde_json::json; -use crate as gen; +use crate::{self as gen, get_mappings}; const API_CONTROLLER_SCAFFOLD_T: &str = include_str!("templates/scaffold/api/controller.t"); const API_CONTROLLER_TEST_T: &str = include_str!("templates/scaffold/api/test.t"); @@ -22,7 +22,7 @@ const HTML_VIEW_CREATE_SCAFFOLD_T: &str = include_str!("templates/scaffold/html/ const HTML_VIEW_SHOW_SCAFFOLD_T: &str = include_str!("templates/scaffold/html/view_show.t"); const HTML_VIEW_LIST_SCAFFOLD_T: &str = include_str!("templates/scaffold/html/view_list.t"); -use super::{collect_messages, model, AppInfo, Error, Result, MAPPINGS}; +use super::{collect_messages, model, AppInfo, Error, Result}; pub fn generate( rrgen: &RRgen, @@ -35,6 +35,7 @@ pub fn generate( // - never run with migration_only, because the controllers will refer to the // models. the models only arrive after migration and entities sync. let model_messages = model::generate(rrgen, name, false, false, fields, appinfo)?; + let mappings = get_mappings(); let mut columns = Vec::new(); for (fname, ftype) in fields { @@ -46,11 +47,11 @@ pub fn generate( continue; } if ftype != "references" { - let schema_type = MAPPINGS.rust_field(ftype.as_str()).ok_or_else(|| { + let schema_type = mappings.rust_field(ftype.as_str()).ok_or_else(|| { Error::Message(format!( "type: {} not found. try any of: {:?}", ftype, - MAPPINGS.rust_fields() + mappings.rust_fields() )) })?; columns.push((fname.to_string(), schema_type.as_str(), ftype)); diff --git a/loco-gen/src/templates/controller/api/test.t b/loco-gen/src/templates/controller/api/test.t index 568e98d5c..006ccba28 100644 --- a/loco-gen/src/templates/controller/api/test.t +++ b/loco-gen/src/templates/controller/api/test.t @@ -2,7 +2,7 @@ {% set module_name = file_name | pascal_case -%} to: tests/requests/{{ file_name }}.rs skip_exists: true -message: "Tests for controller `{{module_name}}` was added successfully. Run `cargo run test`." +message: "Tests for controller `{{module_name}}` was added successfully. Run `cargo test`." injections: - into: tests/requests/mod.rs append: true @@ -16,7 +16,7 @@ use serial_test::serial; #[serial] async fn can_get_{{ name | plural | snake_case }}() { testing::request::(|request, _ctx| async move { - let res = request.get("/{{ name | plural | snake_case }}/").await; + let res = request.get("/api/{{ name | plural | snake_case }}/").await; assert_eq!(res.status_code(), 200); // you can assert content like this: diff --git a/loco-gen/src/templates/request_test.t b/loco-gen/src/templates/request_test.t index 7dd7a3f73..43302faf3 100644 --- a/loco-gen/src/templates/request_test.t +++ b/loco-gen/src/templates/request_test.t @@ -2,7 +2,7 @@ {% set module_name = file_name | pascal_case -%} to: tests/requests/{{ file_name }}.rs skip_exists: true -message: "Tests for controller `{{module_name}}` was added successfully. Run `cargo run test`." +message: "Tests for controller `{{module_name}}` was added successfully. Run `cargo test`." injections: - into: tests/requests/mod.rs append: true diff --git a/loco-gen/src/templates/scaffold/api/test.t b/loco-gen/src/templates/scaffold/api/test.t index 9fdfe805c..a0c672c3f 100644 --- a/loco-gen/src/templates/scaffold/api/test.t +++ b/loco-gen/src/templates/scaffold/api/test.t @@ -2,7 +2,7 @@ {% set module_name = file_name | pascal_case -%} to: tests/requests/{{ file_name }}.rs skip_exists: true -message: "Tests for controller `{{module_name}}` was added successfully. Run `cargo run test`." +message: "Tests for controller `{{module_name}}` was added successfully. Run `cargo test`." injections: - into: tests/requests/mod.rs append: true @@ -16,7 +16,7 @@ use serial_test::serial; #[serial] async fn can_get_{{ name | plural | snake_case }}() { testing::request::(|request, _ctx| async move { - let res = request.get("/{{ name | plural | snake_case }}/").await; + let res = request.get("/api/{{ name | plural | snake_case }}/").await; assert_eq!(res.status_code(), 200); // you can assert content like this: diff --git a/loco-gen/src/templates/scaffold/html/base.t b/loco-gen/src/templates/scaffold/html/base.t index 16123c442..af33dc648 100644 --- a/loco-gen/src/templates/scaffold/html/base.t +++ b/loco-gen/src/templates/scaffold/html/base.t @@ -7,6 +7,8 @@ message: "Base template was added successfully." + + {% raw %}{% block title %}{% endblock title %}{% endraw %} {% raw %}{% block head %}{% endraw %} diff --git a/loco-gen/src/templates/scaffold/htmx/base.t b/loco-gen/src/templates/scaffold/htmx/base.t index b0b96c332..e360b023b 100644 --- a/loco-gen/src/templates/scaffold/htmx/base.t +++ b/loco-gen/src/templates/scaffold/htmx/base.t @@ -7,6 +7,8 @@ message: "Base template was added successfully." + + {% raw %}{% block title %}{% endblock title %}{% endraw %} diff --git a/loco-gen/src/templates/task_test.t b/loco-gen/src/templates/task_test.t index 171d7c519..69eee2d05 100644 --- a/loco-gen/src/templates/task_test.t +++ b/loco-gen/src/templates/task_test.t @@ -2,7 +2,7 @@ {% set module_name = file_name | pascal_case -%} to: tests/tasks/{{ file_name }}.rs skip_exists: true -message: "Tests for task `{{module_name}}` was added successfully. Run `cargo run test`." +message: "Tests for task `{{module_name}}` was added successfully. Run `cargo test`." injections: - into: tests/tasks/mod.rs append: true diff --git a/loco-gen/src/templates/worker_test.t b/loco-gen/src/templates/worker_test.t index b8f13bad2..d302ae079 100644 --- a/loco-gen/src/templates/worker_test.t +++ b/loco-gen/src/templates/worker_test.t @@ -2,7 +2,7 @@ {% set struct_name = module_name | pascal_case -%} to: "tests/workers/{{module_name}}.rs" skip_exists: true -message: "Test for worker `{{struct_name}}` was added successfully. Run `cargo run test`." +message: "Test for worker `{{struct_name}}` was added successfully. Run `cargo test`." injections: - into: tests/workers/mod.rs append: true diff --git a/src/cli.rs b/src/cli.rs index f178d65e4..c093bb998 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -136,6 +136,8 @@ enum Commands { /// print out the current configurations. #[arg(short, long, action)] config: bool, + #[arg(short, long, action)] + production: bool, }, /// Display the app version Version {}, @@ -540,13 +542,16 @@ pub async fn main() -> crate::Result<()> { }, )?; } - Commands::Doctor { config: config_arg } => { + Commands::Doctor { + config: config_arg, + production, + } => { if config_arg { println!("{}", &config); println!("Environment: {}", &environment); } else { let mut should_exit = false; - for (_, check) in doctor::run_all(&config).await? { + for (_, check) in doctor::run_all(&config, production).await? { if !should_exit && !check.valid() { should_exit = true; } diff --git a/src/config.rs b/src/config.rs index 0b5322e31..e3faec3e4 100644 --- a/src/config.rs +++ b/src/config.rs @@ -24,20 +24,21 @@ Notes: use std::{ collections::BTreeMap, path::{Path, PathBuf}, + sync::OnceLock, }; use fs_err as fs; -use lazy_static::lazy_static; use serde::{Deserialize, Serialize}; use serde_json::json; use tracing::info; use crate::{controller::middleware, environment::Environment, logger, scheduler, Error, Result}; -lazy_static! { - static ref DEFAULT_FOLDER: PathBuf = PathBuf::from("config"); -} +static DEFAULT_FOLDER: OnceLock = OnceLock::new(); +fn get_default_folder() -> &'static PathBuf { + DEFAULT_FOLDER.get_or_init(|| PathBuf::from("config")) +} /// Main application configuration structure. /// /// This struct encapsulates various configuration settings. The configuration @@ -496,7 +497,7 @@ impl Config { /// Config::new(environment).expect("configuration loading") /// } pub fn new(env: &Environment) -> Result { - let config = Self::from_folder(env, DEFAULT_FOLDER.as_path())?; + let config = Self::from_folder(env, get_default_folder().as_path())?; Ok(config) } diff --git a/src/controller/app_routes.rs b/src/controller/app_routes.rs index 5031aaaeb..32b6cfa5e 100644 --- a/src/controller/app_routes.rs +++ b/src/controller/app_routes.rs @@ -2,10 +2,9 @@ //! configuring routes in an Axum application. It allows you to define route //! prefixes, add routes, and configure middlewares for the application. -use std::fmt; +use std::{fmt, sync::OnceLock}; use axum::Router as AXRouter; -use lazy_static::lazy_static; use regex::Regex; #[cfg(feature = "channels")] @@ -16,8 +15,10 @@ use crate::{ Result, }; -lazy_static! { - static ref NORMALIZE_URL: Regex = Regex::new(r"/+").unwrap(); +static NORMALIZE_URL: OnceLock = OnceLock::new(); + +fn get_normalize_url() -> &'static Regex { + NORMALIZE_URL.get_or_init(|| Regex::new(r"/+").unwrap()) } /// Represents the routes of the application. @@ -91,7 +92,7 @@ impl AppRoutes { parts.push(handler.uri.to_string()); let joined_parts = parts.join("/"); - let normalized = NORMALIZE_URL.replace_all(&joined_parts, "/"); + let normalized = get_normalize_url().replace_all(&joined_parts, "/"); let uri = if normalized == "/" { normalized.to_string() } else { diff --git a/src/controller/backtrace.rs b/src/controller/backtrace.rs index 778d745f4..10df3f7a3 100644 --- a/src/controller/backtrace.rs +++ b/src/controller/backtrace.rs @@ -1,44 +1,67 @@ -use lazy_static::lazy_static; +use std::sync::OnceLock; + use regex::Regex; use crate::{Error, Result}; -lazy_static! { - static ref NAME_BLOCKLIST: Vec = [ - "^___rust_try", - "^__pthread", - "^__clone", - "^>(); - static ref FILE_BLOCKLIST: Vec = ["axum-.*$", "tower-.*$", "hyper-.*$", "tokio-.*$", "futures-.*$", "^/rustc"] +static NAME_BLOCKLIST: OnceLock> = OnceLock::new(); +static FILE_BLOCKLIST: OnceLock> = OnceLock::new(); + +fn get_name_blocklist() -> &'static Vec { + NAME_BLOCKLIST.get_or_init(|| { + [ + "^___rust_try", + "^__pthread", + "^__clone", + "^>() + }) +} + +fn get_file_blocklist() -> &'static Vec { + FILE_BLOCKLIST.get_or_init(|| { + [ + "axum-.*$", + "tower-.*$", + "hyper-.*$", + "tokio-.*$", + "futures-.*$", + "^/rustc", + ] .iter() .map(|s| Regex::new(s).unwrap()) - .collect::>(); + .collect::>() + }) } pub fn print_backtrace(bt: &std::backtrace::Backtrace) -> Result<()> { - backtrace_printer::print_backtrace(&mut std::io::stdout(), bt, &NAME_BLOCKLIST, &FILE_BLOCKLIST) - .map_err(Error::msg) + backtrace_printer::print_backtrace( + &mut std::io::stdout(), + bt, + get_name_blocklist(), + get_file_blocklist(), + ) + .map_err(Error::msg) } diff --git a/src/controller/describe.rs b/src/controller/describe.rs index 43aec5b29..dc168cf35 100644 --- a/src/controller/describe.rs +++ b/src/controller/describe.rs @@ -1,11 +1,14 @@ +use std::sync::OnceLock; + use axum::{http, routing::MethodRouter}; -use lazy_static::lazy_static; use regex::Regex; use crate::app::AppContext; -lazy_static! { - static ref DESCRIBE_METHOD_ACTION: Regex = Regex::new(r"\b(\w+):\s*BoxedHandler\b").unwrap(); +static DESCRIBE_METHOD_ACTION: OnceLock = OnceLock::new(); + +fn get_describe_method_action() -> &'static Regex { + DESCRIBE_METHOD_ACTION.get_or_init(|| Regex::new(r"\b(\w+):\s*BoxedHandler\b").unwrap()) } /// Extract the allow list method actions from [`MethodRouter`]. @@ -16,7 +19,7 @@ lazy_static! { pub fn method_action(method: &MethodRouter) -> Vec { let method_str = format!("{method:?}"); - DESCRIBE_METHOD_ACTION + get_describe_method_action() .captures(&method_str) .and_then(|captures| captures.get(1).map(|m| m.as_str().to_lowercase())) .and_then(|method_name| match method_name.as_str() { diff --git a/src/controller/middleware/powered_by.rs b/src/controller/middleware/powered_by.rs index 0656a93de..5cb2ce01f 100644 --- a/src/controller/middleware/powered_by.rs +++ b/src/controller/middleware/powered_by.rs @@ -6,6 +6,8 @@ //! custom identifier string or defaults to "loco.rs" if no identifier is //! provided. +use std::sync::OnceLock; + use axum::{ http::header::{HeaderName, HeaderValue}, Router as AXRouter, @@ -14,9 +16,10 @@ use tower_http::set_header::SetResponseHeaderLayer; use crate::{app::AppContext, controller::middleware::MiddlewareLayer, Result}; -lazy_static::lazy_static! { - static ref DEFAULT_IDENT_HEADER_VALUE: HeaderValue = - HeaderValue::from_static("loco.rs"); +static DEFAULT_IDENT_HEADER_VALUE: OnceLock = OnceLock::new(); + +fn get_default_ident_header_value() -> &'static HeaderValue { + DEFAULT_IDENT_HEADER_VALUE.get_or_init(|| HeaderValue::from_static("loco.rs")) } /// [`Middleware`] struct responsible for managing the identifier value for the @@ -31,7 +34,7 @@ pub struct Middleware { #[must_use] pub fn new(ident: Option<&str>) -> Middleware { let ident_value = ident.map_or_else( - || Some(DEFAULT_IDENT_HEADER_VALUE.clone()), + || Some(get_default_ident_header_value().clone()), |ident| { if ident.is_empty() { None @@ -44,7 +47,7 @@ pub fn new(ident: Option<&str>) -> Middleware { val = ident, "could not set custom ident header" ); - Some(DEFAULT_IDENT_HEADER_VALUE.clone()) + Some(get_default_ident_header_value().clone()) } } } @@ -79,7 +82,7 @@ impl MiddlewareLayer for Middleware { HeaderName::from_static("x-powered-by"), self.ident .clone() - .unwrap_or_else(|| DEFAULT_IDENT_HEADER_VALUE.clone()), + .unwrap_or_else(|| get_default_ident_header_value().clone()), ))) } } diff --git a/src/controller/middleware/remote_ip.rs b/src/controller/middleware/remote_ip.rs index a4a71bf95..ce2415918 100644 --- a/src/controller/middleware/remote_ip.rs +++ b/src/controller/middleware/remote_ip.rs @@ -13,6 +13,7 @@ use std::{ iter::Iterator, net::{IpAddr, SocketAddr}, str::FromStr, + sync::OnceLock, task::{Context, Poll}, }; @@ -27,28 +28,30 @@ use axum::{ use futures_util::future::BoxFuture; use hyper::HeaderMap; use ipnetwork::IpNetwork; -use lazy_static::lazy_static; use serde::{Deserialize, Serialize}; use tower::{Layer, Service}; use tracing::error; use crate::{app::AppContext, controller::middleware::MiddlewareLayer, Error, Result}; -lazy_static! { -// matching what Rails does is probably a smart idea: -// https://github.com/rails/rails/blob/main/actionpack/lib/action_dispatch/middleware/remote_ip.rb#L40 -static ref LOCAL_TRUSTED_PROXIES: Vec = [ - "127.0.0.0/8", // localhost IPv4 range, per RFC-3330 - "::1", // localhost IPv6 - "fc00::/7", // private IPv6 range fc00::/7 - "10.0.0.0/8", // private IPv4 range 10.x.x.x - "172.16.0.0/12", // private IPv4 range 172.16.0.0 .. 172.31.255.255 - "192.168.0.0/16" - ] - .iter() - .map(|ip| IpNetwork::from_str(ip).unwrap()) - .collect(); +static LOCAL_TRUSTED_PROXIES: OnceLock> = OnceLock::new(); + +fn get_local_trusted_proxies() -> &'static Vec { + LOCAL_TRUSTED_PROXIES.get_or_init(|| { + [ + "127.0.0.0/8", // localhost IPv4 range, per RFC-3330 + "::1", // localhost IPv6 + "fc00::/7", // private IPv6 range fc00::/7 + "10.0.0.0/8", // private IPv4 range 10.x.x.x + "172.16.0.0/12", // private IPv4 range 172.16.0.0 .. 172.31.255.255 + "192.168.0.0/16", + ] + .iter() + .map(|ip| IpNetwork::from_str(ip).unwrap()) + .collect() + }) } + const X_FORWARDED_FOR: &str = "X-Forwarded-For"; /// @@ -160,7 +163,9 @@ fn maybe_get_forwarded( */ .filter(|ip| { // trusted proxies provided REPLACES our default local proxies - let proxies = trusted_proxies.as_ref().unwrap_or(&LOCAL_TRUSTED_PROXIES); + let proxies = trusted_proxies + .as_ref() + .unwrap_or_else(|| get_local_trusted_proxies()); !proxies .iter() .any(|trusted_proxy| trusted_proxy.contains(*ip)) diff --git a/src/controller/middleware/request_id.rs b/src/controller/middleware/request_id.rs index eb548d10f..d0f1740f5 100644 --- a/src/controller/middleware/request_id.rs +++ b/src/controller/middleware/request_id.rs @@ -9,7 +9,6 @@ use axum::{ extract::Request, http::HeaderValue, middleware::Next, response::Response, Router as AXRouter, }; -use lazy_static::lazy_static; use regex::Regex; use serde::{Deserialize, Serialize}; use uuid::Uuid; @@ -19,10 +18,13 @@ use crate::{app::AppContext, controller::middleware::MiddlewareLayer, Result}; const X_REQUEST_ID: &str = "x-request-id"; const MAX_LEN: usize = 255; -lazy_static! { - static ref ID_CLEANUP: Regex = Regex::new(r"[^\w\-@]").unwrap(); -} +use std::sync::OnceLock; + +static ID_CLEANUP: OnceLock = OnceLock::new(); +fn get_id_cleanup() -> &'static Regex { + ID_CLEANUP.get_or_init(|| Regex::new(r"[^\w\-@]").unwrap()) +} #[derive(Debug, Clone, Deserialize, Serialize)] pub struct RequestId { #[serde(default)] @@ -97,7 +99,7 @@ fn make_request_id(maybe_request_id: Option) -> String { .and_then(|hdr| { // see: https://github.com/rails/rails/blob/main/actionpack/lib/action_dispatch/middleware/request_id.rb#L39 let id: Option = hdr.to_str().ok().map(|s| { - ID_CLEANUP + get_id_cleanup() .replace_all(s, "") .chars() .take(MAX_LEN) diff --git a/src/controller/middleware/secure_headers.rs b/src/controller/middleware/secure_headers.rs index 8fdda57bc..a899395d2 100644 --- a/src/controller/middleware/secure_headers.rs +++ b/src/controller/middleware/secure_headers.rs @@ -5,6 +5,7 @@ use std::{ collections::{BTreeMap, HashMap}, + sync::OnceLock, task::{Context, Poll}, }; @@ -15,19 +16,19 @@ use axum::{ Router as AXRouter, }; use futures_util::future::BoxFuture; -use lazy_static::lazy_static; use serde::{Deserialize, Serialize}; use serde_json::{self, json}; use tower::{Layer, Service}; use crate::{app::AppContext, controller::middleware::MiddlewareLayer, Error, Result}; -lazy_static! { - /// Predefined secure header presets loaded from `secure_headers.json` - static ref PRESETS: HashMap> = - serde_json::from_str(include_str!("secure_headers.json")).unwrap(); +static PRESETS: OnceLock>> = OnceLock::new(); +fn get_presets() -> &'static HashMap> { + PRESETS.get_or_init(|| { + let json_data = include_str!("secure_headers.json"); + serde_json::from_str(json_data).unwrap() + }) } - /// Sets a predefined or custom set of secure headers. /// /// We recommend our `github` preset. Presets values are derived @@ -123,7 +124,7 @@ impl SecureHeader { let mut headers = vec![]; let preset = &self.preset; - let p = PRESETS.get(preset).ok_or_else(|| { + let p = get_presets().get(preset).ok_or_else(|| { Error::Message(format!( "secure_headers: a preset named `{preset}` does not exist" )) diff --git a/src/db.rs b/src/db.rs index 2bf7ca6dc..a87fbc8da 100644 --- a/src/db.rs +++ b/src/db.rs @@ -3,11 +3,10 @@ //! This module defines functions and operations related to the application's //! database interactions. -use std::{collections::HashMap, fs::File, path::Path, time::Duration}; +use std::{collections::HashMap, fs::File, path::Path, sync::OnceLock, time::Duration}; use duct::cmd; use fs_err as fs; -use lazy_static::lazy_static; use regex::Regex; use sea_orm::{ ActiveModelTrait, ConnectOptions, ConnectionTrait, Database, DatabaseBackend, @@ -23,13 +22,10 @@ use crate::{ errors::Error, }; -lazy_static! { - // Getting the table name from the environment configuration. - // For example: - // postgres://loco:loco@localhost:5432/loco_app - // mysql://loco:loco@localhost:3306/loco_app - // the results will be loco_app - pub static ref EXTRACT_DB_NAME: Regex = Regex::new(r"/([^/]+)$").unwrap(); +pub static EXTRACT_DB_NAME: OnceLock = OnceLock::new(); + +fn get_extract_db_name() -> &'static Regex { + EXTRACT_DB_NAME.get_or_init(|| Regex::new(r"/([^/]+)$").unwrap()) } #[derive(Default, Clone, Debug)] @@ -175,7 +171,7 @@ pub async fn create(db_uri: &str) -> AppResult<()> { "Only Postgres databases are supported for table creation", )); } - let db_name = EXTRACT_DB_NAME + let db_name = get_extract_db_name() .captures(db_uri) .and_then(|cap| cap.get(1).map(|db| db.as_str())) .ok_or_else(|| { @@ -184,7 +180,9 @@ pub async fn create(db_uri: &str) -> AppResult<()> { ) })?; - let conn = EXTRACT_DB_NAME.replace(db_uri, "/postgres").to_string(); + let conn = get_extract_db_name() + .replace(db_uri, "/postgres") + .to_string(); let db = Database::connect(conn).await?; Ok(create_postgres_database(db_name, &db).await?) diff --git a/src/depcheck.rs b/src/depcheck.rs new file mode 100644 index 000000000..e717bc289 --- /dev/null +++ b/src/depcheck.rs @@ -0,0 +1,221 @@ +use std::collections::HashMap; + +use semver::{Version, VersionReq}; +use thiserror::Error; +use toml::Value; + +#[derive(Debug, PartialEq, Eq, Ord, PartialOrd)] +pub enum VersionStatus { + NotFound, + Invalid { + version: String, + min_version: String, + }, + Ok(String), +} + +#[derive(Debug, PartialEq, Eq, Ord, PartialOrd)] +pub struct CrateStatus { + pub crate_name: String, + pub status: VersionStatus, +} + +#[derive(Error, Debug)] +pub enum VersionCheckError { + #[error("Failed to parse Cargo.lock: {0}")] + ParseError(#[from] toml::de::Error), + + #[error("Error with crate {crate_name}: {msg}")] + CrateError { crate_name: String, msg: String }, +} + +pub type Result = std::result::Result; + +pub fn check_crate_versions( + cargo_lock_content: &str, + min_versions: HashMap<&str, &str>, +) -> Result> { + let lock_file: Value = cargo_lock_content.parse()?; + + let packages = lock_file + .get("package") + .and_then(|v| v.as_array()) + .ok_or_else(|| { + VersionCheckError::ParseError(serde::de::Error::custom( + "Missing package array in Cargo.lock", + )) + })?; + + let mut results = Vec::new(); + + for (crate_name, min_version) in min_versions { + let min_version_req = + VersionReq::parse(min_version).map_err(|_| VersionCheckError::CrateError { + crate_name: crate_name.to_string(), + msg: format!("Invalid minimum version format: {min_version}"), + })?; + + let mut found = false; + for package in packages { + if let Some(name) = package.get("name").and_then(|v| v.as_str()) { + if name == crate_name { + found = true; + let version_str = + package + .get("version") + .and_then(|v| v.as_str()) + .ok_or_else(|| VersionCheckError::CrateError { + crate_name: crate_name.to_string(), + msg: "Invalid version format in Cargo.lock".to_string(), + })?; + + let version = + Version::parse(version_str).map_err(|_| VersionCheckError::CrateError { + crate_name: crate_name.to_string(), + msg: format!("Invalid version format in Cargo.lock: {version_str}"), + })?; + + let status = if min_version_req.matches(&version) { + VersionStatus::Ok(version.to_string()) + } else { + VersionStatus::Invalid { + version: version.to_string(), + min_version: min_version.to_string(), + } + }; + results.push(CrateStatus { + crate_name: crate_name.to_string(), + status, + }); + break; + } + } + } + + if !found { + results.push(CrateStatus { + crate_name: crate_name.to_string(), + status: VersionStatus::NotFound, + }); + } + } + + Ok(results) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_multiple_crates_mixed_results() { + let cargo_lock_content = r#" + [[package]] + name = "serde" + version = "1.0.130" + + [[package]] + name = "tokio" + version = "0.3.0" + + [[package]] + name = "rand" + version = "0.8.4" + "#; + + let mut min_versions = HashMap::new(); + min_versions.insert("serde", "1.0.130"); + min_versions.insert("tokio", "1.0"); + min_versions.insert("rand", "0.8.0"); + + let mut result = check_crate_versions(cargo_lock_content, min_versions).unwrap(); + result.sort(); + assert_eq!( + result, + vec![ + CrateStatus { + crate_name: "rand".to_string(), + status: VersionStatus::Ok("0.8.4".to_string()) + }, + CrateStatus { + crate_name: "serde".to_string(), + status: VersionStatus::Ok("1.0.130".to_string()) + }, + CrateStatus { + crate_name: "tokio".to_string(), + status: VersionStatus::Invalid { + version: "0.3.0".to_string(), + min_version: "1.0".to_string() + } + } + ] + ); + } + + #[test] + fn test_invalid_version_format_in_cargo_lock() { + let cargo_lock_content = r#" + [[package]] + name = "serde" + version = "1.0.x" + "#; + + let mut min_versions = HashMap::new(); + min_versions.insert("serde", "1.0.0"); + + let result = check_crate_versions(cargo_lock_content, min_versions); + assert!(matches!( + result, + Err(VersionCheckError::CrateError { crate_name, msg }) if crate_name == "serde" && msg.contains("Invalid version format") + )); + } + + #[test] + fn test_no_package_section_in_cargo_lock() { + let cargo_lock_content = r" + # No packages listed in this Cargo.lock + "; + + let mut min_versions = HashMap::new(); + min_versions.insert("serde", "1.0.130"); + + let result = check_crate_versions(cargo_lock_content, min_versions); + assert!(matches!(result, Err(VersionCheckError::ParseError(_)))); + } + + #[test] + fn test_exact_version_match_for_minimum_requirement() { + let cargo_lock_content = r#" + [[package]] + name = "serde" + version = "1.0.130" + "#; + + let mut min_versions = HashMap::new(); + min_versions.insert("serde", "1.0.130"); + + let mut result = check_crate_versions(cargo_lock_content, min_versions).unwrap(); + result.sort(); + assert_eq!( + result, + vec![CrateStatus { + crate_name: "serde".to_string(), + status: VersionStatus::Ok("1.0.130".to_string()), + }] + ); + } + + #[test] + fn test_no_crates_in_min_versions_map() { + let cargo_lock_content = r#" + [[package]] + name = "serde" + version = "1.0.130" + "#; + + let min_versions = HashMap::new(); // Empty map + + let result = check_crate_versions(cargo_lock_content, min_versions).unwrap(); + assert!(result.is_empty()); + } +} diff --git a/src/doctor.rs b/src/doctor.rs index 78019b332..67707992f 100644 --- a/src/doctor.rs +++ b/src/doctor.rs @@ -1,12 +1,17 @@ -use std::{collections::BTreeMap, process::Command}; +use std::{ + collections::{BTreeMap, HashMap}, + process::Command, + sync::OnceLock, +}; +use colored::Colorize; use regex::Regex; use semver::Version; use crate::{ bgworker, config::{self, Config, Database}, - db, Error, Result, + db, depcheck, Error, Result, }; const SEAORM_INSTALLED: &str = "SeaORM CLI is installed"; @@ -19,12 +24,30 @@ const QUEUE_CONN_OK: &str = "queue connection: success"; const QUEUE_CONN_FAILED: &str = "queue connection: failed"; const QUEUE_NOT_CONFIGURED: &str = "queue not configured?"; +// versions health +const MIN_SEAORMCLI_VER: &str = "1.1.0"; +static MIN_DEP_VERSIONS: OnceLock> = OnceLock::new(); + +fn get_min_dep_versions() -> &'static HashMap<&'static str, &'static str> { + MIN_DEP_VERSIONS.get_or_init(|| { + let mut min_vers = HashMap::new(); + + min_vers.insert("tokio", "1.33.0"); + min_vers.insert("sea-orm", "1.1.0"); + min_vers.insert("validator", "0.18.0"); + min_vers.insert("axum", "0.7.5"); + + min_vers + }) +} + /// Represents different resources that can be checked. #[derive(PartialOrd, PartialEq, Eq, Ord, Debug)] pub enum Resource { SeaOrmCLI, Database, - Redis, + Queue, + Deps, } /// Represents the status of a resource check. @@ -93,19 +116,61 @@ impl std::fmt::Display for Check { /// Runs checks for all configured resources. /// # Errors /// Error when one of the checks fail -pub async fn run_all(config: &Config) -> Result> { - let mut checks = BTreeMap::from([ - (Resource::SeaOrmCLI, check_seaorm_cli()?), - (Resource::Database, check_db(&config.database).await), - ]); +pub async fn run_all(config: &Config, production: bool) -> Result> { + let mut checks = BTreeMap::from([(Resource::Database, check_db(&config.database).await)]); if config.workers.mode == config::WorkerMode::BackgroundQueue { - checks.insert(Resource::Redis, check_queue(config).await); + checks.insert(Resource::Queue, check_queue(config).await); + } + + if !production { + checks.insert(Resource::Deps, check_deps()?); + checks.insert(Resource::SeaOrmCLI, check_seaorm_cli()?); } Ok(checks) } +/// Checks "blessed" / major dependencies in a Loco app Cargo.toml, and +/// recommend to update. +/// Only if a dep exists, we check it against a min version +/// # Errors +/// Returns error if fails +pub fn check_deps() -> Result { + let cargolock = fs_err::read_to_string("Cargo.lock")?; + + let crate_statuses = + depcheck::check_crate_versions(&cargolock, get_min_dep_versions().clone())?; + let mut report = String::new(); + report.push_str("Dependencies\n"); + let mut all_ok = true; + + for status in &crate_statuses { + if let depcheck::VersionStatus::Invalid { + version, + min_version, + } = &status.status + { + report.push_str(&format!( + " {}: version {} does not meet minimum version {}\n", + status.crate_name.yellow(), + version.red(), + min_version.green() + )); + all_ok = false; + } + } + Ok(Check { + status: if all_ok { + CheckStatus::Ok + } else { + CheckStatus::NotOk + }, + message: report, + description: None, + }) +} + /// Checks the database connection. pub async fn check_db(config: &Database) -> Check { match db::connect(config).await { @@ -160,7 +225,6 @@ pub async fn check_queue(config: &Config) -> Check { } } -const MIN_SEAORMCLI_VER: &str = "1.1.0"; /// Checks the presence and version of `SeaORM` CLI. /// # Panics /// On illegal regex diff --git a/src/errors.rs b/src/errors.rs index 58fe9770e..a70aa02c2 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -10,7 +10,7 @@ use axum::{ }; use lettre::{address::AddressError, transport::smtp}; -use crate::controller::ErrorDetail; +use crate::{controller::ErrorDetail, depcheck}; /* backtrace principles: @@ -145,6 +145,9 @@ pub enum Error { #[error(transparent)] Generators(#[from] loco_gen::Error), + #[error(transparent)] + VersionCheck(#[from] depcheck::VersionCheckError), + #[error(transparent)] Any(#[from] Box), } diff --git a/src/lib.rs b/src/lib.rs index dd90cb83e..40c1da358 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,6 +6,7 @@ pub use self::errors::Error; mod banner; pub mod bgworker; +mod depcheck; pub mod initializers; pub mod prelude; diff --git a/src/scheduler.rs b/src/scheduler.rs index 25c9656c0..367e04b77 100644 --- a/src/scheduler.rs +++ b/src/scheduler.rs @@ -5,6 +5,7 @@ use std::{ collections::HashMap, fmt, io, path::{Path, PathBuf}, + sync::OnceLock, time::Instant, }; @@ -14,8 +15,10 @@ use tokio_cron_scheduler::{JobScheduler, JobSchedulerError}; use crate::{app::Hooks, environment::Environment, task::Tasks}; -lazy_static::lazy_static! { - static ref RE_IS_CRON_SYNTAX: Regex = Regex::new(r"^[\*\d]").unwrap(); +static RE_IS_CRON_SYNTAX: OnceLock = OnceLock::new(); + +fn get_re_is_cron_syntax() -> &'static Regex { + RE_IS_CRON_SYNTAX.get_or_init(|| Regex::new(r"^[\*\d]").unwrap()) } /// Errors that may occur while operating the scheduler. @@ -291,7 +294,7 @@ impl Scheduler { let job_description = job.prepare_command(&self.binary_path, &self.default_output, &self.environment); - let cron_syntax = if RE_IS_CRON_SYNTAX.is_match(&job.cron) { + let cron_syntax = if get_re_is_cron_syntax().is_match(&job.cron) { job.cron.clone() } else { english_to_cron::str_cron_syntax(&job.cron).map_err(|err| { diff --git a/src/testing.rs b/src/testing.rs index 6d41f815e..46dce0093 100644 --- a/src/testing.rs +++ b/src/testing.rs @@ -4,8 +4,9 @@ //! purposes, including cleaning up data patterns and bootstrapping the //! application for testing. +use std::sync::OnceLock; + use axum_test::{TestServer, TestServerConfig}; -use lazy_static::lazy_static; #[cfg(feature = "with-db")] use sea_orm::DatabaseConnection; @@ -16,38 +17,59 @@ use crate::{ Result, }; -// Lazy-static constants for data cleanup patterns -lazy_static! { - /// Constants for cleaning up user model data, replacing certain patterns with placeholders. - pub static ref CLEANUP_USER_MODEL: Vec<(&'static str, &'static str)> = vec![ - ( - r"([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})", - "PID" - ), - (r"password: (.*{60}),", "password: \"PASSWORD\","), - (r"([A-Za-z0-9-_]*\.[A-Za-z0-9-_]*\.[A-Za-z0-9-_]*)","TOKEN") - ]; - - /// Constants for cleaning up date data, replacing date-time patterns with placeholders. - pub static ref CLEANUP_DATE: Vec<(&'static str, &'static str)> = +static CLEANUP_USER_MODEL: OnceLock> = OnceLock::new(); +static CLEANUP_DATE: OnceLock> = OnceLock::new(); +static CLEANUP_MODEL: OnceLock> = OnceLock::new(); +static CLEANUP_MAIL: OnceLock> = OnceLock::new(); + +pub fn get_cleanup_user_model() -> &'static Vec<(&'static str, &'static str)> { + CLEANUP_USER_MODEL.get_or_init(|| { + vec![ + ( + r"([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})", + "PID", + ), + (r"password: (.*{60}),", "password: \"PASSWORD\","), + (r"([A-Za-z0-9-_]*\.[A-Za-z0-9-_]*\.[A-Za-z0-9-_]*)", "TOKEN"), + ] + }) +} + +pub fn get_cleanup_date() -> &'static Vec<(&'static str, &'static str)> { + CLEANUP_DATE.get_or_init(|| { + vec![ + ( + r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?\+\d{2}:\d{2}", + "DATE", + ), // with tz + (r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+", "DATE"), + (r"(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})", "DATE"), + ] + }) +} + +pub fn get_cleanup_model() -> &'static Vec<(&'static str, &'static str)> { + CLEANUP_MODEL.get_or_init(|| vec![(r"id: \d+,", "id: ID")]) +} + +pub fn get_cleanup_mail() -> &'static Vec<(&'static str, &'static str)> { + CLEANUP_MAIL.get_or_init(|| { vec![ - (r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?\+\d{2}:\d{2}", "DATE"), // with tz - (r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d+", "DATE"), - (r"(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})", "DATE") - ]; - - /// Constants for cleaning up generals model data, replacing IDs with placeholders. - pub static ref CLEANUP_MODEL: Vec<(&'static str, &'static str)> = vec![(r"id: \d+,", "id: ID")]; - pub static ref CLEANUP_MAIL: Vec<(&'static str, &'static str)> = vec![ (r"[0-9A-Za-z]+{40}", "IDENTIFIER"), - (r"\w+, \d{1,2} \w+ \d{4} \d{2}:\d{2}:\d{2} [+-]\d{4}", "DATE"), - (r"([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})","RANDOM_ID"), - - // also handles line break in text-format emails, where they break into a new line and then use '=' as continuation symbol. - // #6c23875d-3523-4805-8527-f2=\r\n82d3aa7514 - // #6c23875d-3523-4805-8527-f282d3aa75=\r\n14 (note postfix after '=' can be short) - (r"([0-9a-fA-F]{8}-[0-9a-fA-F]{4})-[0-9a-fA-F]{4}-.*[0-9a-fA-F]{2}", "RANDOM_ID") - ]; + ( + r"\w+, \d{1,2} \w+ \d{4} \d{2}:\d{2}:\d{2} [+-]\d{4}", + "DATE", + ), + ( + r"([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})", + "RANDOM_ID", + ), + ( + r"([0-9a-fA-F]{8}-[0-9a-fA-F]{4})-[0-9a-fA-F]{4}-.*[0-9a-fA-F]{2}", + "RANDOM_ID", + ), + ] + }) } /// Combines cleanup filters from various categories (user model, date, and @@ -83,17 +105,17 @@ lazy_static! { /// ``` #[must_use] pub fn cleanup_user_model() -> Vec<(&'static str, &'static str)> { - let mut combined_filters = CLEANUP_USER_MODEL.to_vec(); - combined_filters.extend(CLEANUP_DATE.iter().copied()); - combined_filters.extend(CLEANUP_MODEL.iter().copied()); + let mut combined_filters = get_cleanup_user_model().clone(); + combined_filters.extend(get_cleanup_date().iter().copied()); + combined_filters.extend(get_cleanup_model().iter().copied()); combined_filters } /// Combines cleanup filters from emails that can be dynamic #[must_use] pub fn cleanup_email() -> Vec<(&'static str, &'static str)> { - let mut combined_filters = CLEANUP_MAIL.to_vec(); - combined_filters.extend(CLEANUP_DATE.iter().copied()); + let mut combined_filters = get_cleanup_mail().clone(); + combined_filters.extend(get_cleanup_date().iter().copied()); combined_filters } diff --git a/starters/lightweight-service/Cargo.lock b/starters/lightweight-service/Cargo.lock index b8e74c8c4..5bc0bb43d 100644 --- a/starters/lightweight-service/Cargo.lock +++ b/starters/lightweight-service/Cargo.lock @@ -1388,6 +1388,8 @@ dependencies = [ [[package]] name = "loco-gen" version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1815e1bd51c41fceb096b27cf98b52ab23d272025337ef5106e549e697e8ee9f" dependencies = [ "chrono", "clap", @@ -1405,6 +1407,8 @@ dependencies = [ [[package]] name = "loco-rs" version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4aa14998bbea030a703e8017a5a86c368b022c74b6848f5c16f07adfbc3b98b3" dependencies = [ "argon2", "async-trait", diff --git a/starters/lightweight-service/Cargo.toml b/starters/lightweight-service/Cargo.toml index 0bf777207..74930491d 100644 --- a/starters/lightweight-service/Cargo.toml +++ b/starters/lightweight-service/Cargo.toml @@ -9,9 +9,12 @@ default-run = "loco_starter_template-cli" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[workspace.dependencies] +loco-rs = { version = "0.12.0", default-features = false } + [dependencies] -loco-rs = { version = "0.12.0", default-features = false, features = ["cli"] } +loco-rs = { workspace = true, features = ["cli"] } serde = "1" serde_json = "1" tokio = { version = "1.33.0", default-features = false, features = [ @@ -35,8 +38,5 @@ required-features = [] [dev-dependencies] serial_test = "3.1.1" rstest = "0.21.0" -loco-rs = { version = "0.12.0", default-features = false, features = [ - "testing", - "cli", -] } +loco-rs = { workspace = true, features = ["testing", "cli"] } insta = { version = "*", features = ["redactions", "yaml", "filters"] } diff --git a/starters/rest-api/Cargo.toml b/starters/rest-api/Cargo.toml index efcec573f..7396081b9 100644 --- a/starters/rest-api/Cargo.toml +++ b/starters/rest-api/Cargo.toml @@ -9,9 +9,12 @@ default-run = "loco_starter_template-cli" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[workspace.dependencies] +loco-rs = { version = "0.12.0" } + [dependencies] -loco-rs = { version = "0.12.0" } +loco-rs = { workspace = true } migration = { path = "migration" } serde = { version = "1", features = ["derive"] } @@ -46,5 +49,5 @@ required-features = [] [dev-dependencies] serial_test = "3.1.1" rstest = "0.21.0" -loco-rs = { version = "0.12.0", features = ["testing"] } +loco-rs = { workspace = true, features = ["testing"] } insta = { version = "1.34.0", features = ["redactions", "yaml", "filters"] } diff --git a/starters/rest-api/migration/Cargo.toml b/starters/rest-api/migration/Cargo.toml index 01ed5e185..edbaa4b0e 100644 --- a/starters/rest-api/migration/Cargo.toml +++ b/starters/rest-api/migration/Cargo.toml @@ -10,7 +10,7 @@ path = "src/lib.rs" [dependencies] async-std = { version = "1", features = ["attributes", "tokio1"] } -loco-rs = { version = "0.12.0" } +loco-rs = { workspace = true } [dependencies.sea-orm-migration] version = "1.1.0" diff --git a/starters/saas/Cargo.toml b/starters/saas/Cargo.toml index 44910d272..3296ce791 100644 --- a/starters/saas/Cargo.toml +++ b/starters/saas/Cargo.toml @@ -9,9 +9,12 @@ default-run = "loco_starter_template-cli" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[workspace.dependencies] +loco-rs = { version = "0.12.0" } + [dependencies] -loco-rs = { version = "0.12.0" } +loco-rs = { workspace = true } migration = { path = "migration" } serde = { version = "1", features = ["derive"] } @@ -51,5 +54,5 @@ required-features = [] [dev-dependencies] serial_test = "3.1.1" rstest = "0.21.0" -loco-rs = { version = "0.12.0", features = ["testing"] } +loco-rs = { workspace = true, features = ["testing"] } insta = { version = "1.34.0", features = ["redactions", "yaml", "filters"] } diff --git a/starters/saas/migration/Cargo.toml b/starters/saas/migration/Cargo.toml index 01ed5e185..edbaa4b0e 100644 --- a/starters/saas/migration/Cargo.toml +++ b/starters/saas/migration/Cargo.toml @@ -10,7 +10,7 @@ path = "src/lib.rs" [dependencies] async-std = { version = "1", features = ["attributes", "tokio1"] } -loco-rs = { version = "0.12.0" } +loco-rs = { workspace = true } [dependencies.sea-orm-migration] version = "1.1.0" diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml index b59f1859c..9040ff170 100644 --- a/xtask/Cargo.toml +++ b/xtask/Cargo.toml @@ -20,7 +20,6 @@ duct = "0.13.6" cargo_metadata = "0.18.1" requestty = "0.5.0" regex = { version = "1.10.2" } -lazy_static = "1.4.0" thiserror = "1" tabled = "0.14.0" colored = "2.1.0" diff --git a/xtask/src/bin/main.rs b/xtask/src/bin/main.rs index 0babb5fa0..790b44abb 100644 --- a/xtask/src/bin/main.rs +++ b/xtask/src/bin/main.rs @@ -24,7 +24,7 @@ enum Commands { quick: bool, }, /// Bump loco version in all dependencies places - BumpVersion { + DeprecatedBumpVersion { #[arg(name = "VERSION")] new_version: Version, #[arg(short, long, action = SetFalse)] @@ -51,7 +51,7 @@ fn main() -> eyre::Result<()> { println!("{}", xtask::out::print_ci_results(&res)); xtask::CmdExit::ok() } - Commands::BumpVersion { + Commands::DeprecatedBumpVersion { new_version, exclude_starters, } => { @@ -75,7 +75,18 @@ fn main() -> eyre::Result<()> { xtask::CmdExit::ok() } Commands::Bump { new_version } => { - versions::bump_version(&new_version.to_string()); + let meta = MetadataCommand::new() + .manifest_path("./Cargo.toml") + .current_dir(&project_dir) + .exec() + .unwrap(); + let root: &Package = meta.root_package().unwrap(); + if xtask::prompt::confirmation(&format!( + "upgrading loco version from {} to {}", + root.version, new_version, + ))? { + versions::bump_version(&new_version.to_string())?; + } xtask::CmdExit::ok() } }; diff --git a/xtask/src/bump_version.rs b/xtask/src/bump_version.rs index c55392cd1..aed5e3777 100644 --- a/xtask/src/bump_version.rs +++ b/xtask/src/bump_version.rs @@ -2,11 +2,11 @@ use std::{ fs, io::{Read, Write}, path::{Path, PathBuf}, + sync::OnceLock, }; use cargo_metadata::semver::Version; use colored::Colorize; -use lazy_static::lazy_static; use regex::Regex; use crate::{ @@ -15,19 +15,22 @@ use crate::{ out, utils, }; -lazy_static! { - /// Regular expression for replacing the version in the root package's Cargo.toml file. - static ref REPLACE_LOCO_LIB_VERSION_: Regex = Regex::new( - r#"(?Pname\s*=\s*".+\s+version\s*=\s*")(?P[0-9]+\.[0-9]+\.[0-9]+)"# - ) - .unwrap(); - - /// Regular expression for updating the version in loco-rs package dependencies in Cargo.toml files. - static ref REPLACE_LOCO_PACKAGE_VERSION: Regex = - Regex::new(r#"loco-rs = \{ (version|path) = "[^"]+""#).unwrap(); +static REPLACE_LOCO_LIB_VERSION_: OnceLock = OnceLock::new(); +static REPLACE_LOCO_PACKAGE_VERSION: OnceLock = OnceLock::new(); +fn get_replace_loco_lib_version() -> &'static Regex { + REPLACE_LOCO_LIB_VERSION_.get_or_init(|| { + Regex::new( + r#"(?Pname\s*=\s*".+\s+version\s*=\s*")(?P[0-9]+\.[0-9]+\.[0-9]+)"#, + ) + .unwrap() + }) } +fn get_replace_loco_package_version() -> &'static Regex { + REPLACE_LOCO_PACKAGE_VERSION + .get_or_init(|| Regex::new(r#"loco-rs = \{ (version|path) = "[^"]+""#).unwrap()) +} pub struct BumpVersion { pub base_dir: PathBuf, pub version: Version, @@ -94,14 +97,14 @@ impl BumpVersion { let cargo_toml_file = self.base_dir.join(path).join("Cargo.toml"); fs::File::open(&cargo_toml_file)?.read_to_string(&mut content)?; - if !REPLACE_LOCO_LIB_VERSION_.is_match(&content) { + if !get_replace_loco_lib_version().is_match(&content) { return Err(Error::BumpVersion { path: cargo_toml_file, package: "root_package".to_string(), }); } - let content = REPLACE_LOCO_LIB_VERSION_ + let content = get_replace_loco_lib_version() .replace(&content, |captures: ®ex::Captures<'_>| { format!("{}{}", &captures["name"], self.version) }); @@ -167,14 +170,6 @@ impl BumpVersion { for starter_project in starter_projects { Self::replace_loco_rs_version(&starter_project, replace_with)?; - - let migration_lock_file = starter_project.join("migration"); - if migration_lock_file.exists() { - Self::replace_loco_rs_version( - &migration_lock_file, - replace_migrator.unwrap_or(replace_with), - )?; - } } Ok(()) @@ -185,13 +180,13 @@ impl BumpVersion { let cargo_toml_file = path.join("Cargo.toml"); fs::File::open(&cargo_toml_file)?.read_to_string(&mut content)?; - if !REPLACE_LOCO_PACKAGE_VERSION.is_match(&content) { + if !get_replace_loco_package_version().is_match(&content) { return Err(Error::BumpVersion { path: cargo_toml_file, package: "loco-rs".to_string(), }); } - content = REPLACE_LOCO_PACKAGE_VERSION + content = get_replace_loco_package_version() .replace_all(&content, |_captures: ®ex::Captures<'_>| { replace_with.to_string() }) diff --git a/xtask/src/versions.rs b/xtask/src/versions.rs index 8865b2ae2..260541700 100644 --- a/xtask/src/versions.rs +++ b/xtask/src/versions.rs @@ -2,6 +2,12 @@ use std::path::Path; use regex::Regex; +use crate::{ + ci, + errors::{Error, Result}, + out, +}; + fn bump_version_in_file( file_path: &str, version_regex: &str, @@ -31,12 +37,15 @@ fn bump_version_in_file( } } -pub fn bump_version(version: &str) { - for cargo in [ +pub fn bump_version(version: &str) -> Result<()> { + let starters = [ "starters/saas/Cargo.toml", - "starters/saas/migration/Cargo.toml", - ] { - // turn starters to local + "starters/rest-api/Cargo.toml", + "starters/lightweight-service/Cargo.toml", + ]; + + // turn starters to local "../../" version for testing + for cargo in starters { bump_version_in_file( cargo, // loco-rs = { version =".." @@ -44,8 +53,25 @@ pub fn bump_version(version: &str) { r#"loco-rs = { path="../../""#, false, ); + } + + println!("Testing starters CI"); + let starter_projects: Vec = ci::run_all_in_folder(Path::new("starters"))?; + + println!("Starters CI results:"); + println!("{}", out::print_ci_results(&starter_projects)); + for starter in &starter_projects { + if !starter.is_valid() { + return Err(Error::Message(format!( + "starter {} ins not passing the CI", + starter.path.display() + ))); + } + } - // turn starters from local to version + // all oK + // turn starters from local to version + for cargo in starters { bump_version_in_file( cargo, // loco-rs = { path =".." @@ -69,4 +95,5 @@ pub fn bump_version(version: &str) { // sync new version to subcrates in main Cargo.toml let loco_gen_dep = format!(r#"loco-gen = {{ version = "{version}","#); bump_version_in_file("Cargo.toml", r"(?m)^loco-gen [^,]*,", &loco_gen_dep, false); + Ok(()) }