diff --git a/.dockerignore b/.dockerignore index 9b55c91..af416e3 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,5 +1,6 @@ -# Rust build dir +# Rust /target +/rust-toolchain.toml # Rust tests /tests/ @@ -10,3 +11,8 @@ # Docker /Dockerfile /docker-compose.yml + +# Moon +/.moon/cache +/.moon/hooks +/.moon/docker \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bd2862c..b90dca1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,20 +14,27 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v4 + with: + fetch-depth: 0 - name: Setup Rust Cache uses: Swatinem/rust-cache@v2 with: + shared-key: cd cache-on-failure: true + cache-all-crates: true + cache-workspace-crates: true - - name: Check code for errors - run: cargo check - - - name: Check code format - run: cargo fmt --check + - name: Set up toolchain + uses: moonrepo/setup-toolchain@v0 + with: + auto-install: true - - name: Check code for lint errors - run: cargo clippy -- --deny warnings + - name: Checks + run: moon ci --color - - name: Run tests - run: cargo test + - name: Run report + uses: moonrepo/run-report-action@v1 + if: success() || failure() + with: + access-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index 82be490..e365106 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,13 @@ # Rust /target +/rust-toolchain.toml # environment variables .env* !.env.example -!.env.test \ No newline at end of file +!.env.test + +# Moon +/.moon/cache +/.moon/hooks +/.moon/docker diff --git a/.moon/tasks/rust-backend-application.yml b/.moon/tasks/rust-backend-application.yml new file mode 100644 index 0000000..ac07f97 --- /dev/null +++ b/.moon/tasks/rust-backend-application.yml @@ -0,0 +1,7 @@ +# yaml-language-server: $schema=https://moonrepo.dev/schemas/tasks.json + +tasks: + run: + command: cargo run + description: Run a binary or example of the local package + preset: server diff --git a/.moon/tasks/rust.yml b/.moon/tasks/rust.yml new file mode 100644 index 0000000..0ba16aa --- /dev/null +++ b/.moon/tasks/rust.yml @@ -0,0 +1,58 @@ +# yaml-language-server: $schema=https://moonrepo.dev/schemas/tasks.json + +fileGroups: + sources: + - src/**/* + - Cargo.toml + - .env + tests: + - tests/**/* + +tasks: + build: + command: cargo build + description: Compile a local package and all of its dependencies + local: true + inputs: + - "@globs(sources)" + run: + command: cargo run + description: Run a binary or example of the local package + local: true + inputs: + - "@globs(sources)" + test: + command: cargo test + description: Execute all unit and integration tests and build examples of a local package + inputs: + - "@globs(sources)" + - "@globs(tests)" + bacon: + command: bacon + description: bacon watches your project and runs jobs in background + preset: watcher + check: + command: cargo check + description: Check a local package and all of its dependencies for errors + inputs: + - "@globs(sources)" + fmt: + command: cargo fmt + description: This utility formats all bin and lib files of the current crate using rustfmt + local: true + inputs: + - "@globs(sources)" + fmt/check: + extends: fmt + local: false + args: ["--check"] + clippy: + command: cargo clippy + description: hecks a package to catch common mistakes and improve your Rust code + local: true + inputs: + - "@globs(sources)" + clippy/check: + extends: clippy + local: false + args: ["--", "--deny", "warnings"] diff --git a/.moon/toolchain.yml b/.moon/toolchain.yml new file mode 100644 index 0000000..addb98b --- /dev/null +++ b/.moon/toolchain.yml @@ -0,0 +1,18 @@ +# yaml-language-server: $schema=https://moonrepo.dev/schemas/toolchain.json + +rust: + version: 1.89.0 + syncToolchainConfig: true + binstallVersion: 1.14.4 + components: + - clippy + - rustfmt + bins: + - bin: bacon@3.17.0 + local: true + - bin: cargo-edit@0.13.6 + local: true + - bin: surrealdb-migrations@2.3.0 + local: true + - bin: systemfd@0.4.6 + local: true diff --git a/.moon/workspace.yml b/.moon/workspace.yml new file mode 100644 index 0000000..8a6e3cf --- /dev/null +++ b/.moon/workspace.yml @@ -0,0 +1,17 @@ +# yaml-language-server: $schema=https://moonrepo.dev/schemas/workspace.json + +projects: + - "." + +experiments: + fasterGlobWalk: true + gitV2: true + +vcs: + defaultBranch: main + manager: git + provider: github + syncHooks: true + hooks: + pre-commit: + - moon :check :fmt/check :clippy/check :test --affected --status=staged --no-bail diff --git a/.prototools b/.prototools new file mode 100644 index 0000000..7da32fa --- /dev/null +++ b/.prototools @@ -0,0 +1,5 @@ +moon = "1.39.4" +proto = "0.51.6" + +[settings] +auto-install = true diff --git a/.zed/tasks.json b/.zed/tasks.json new file mode 100644 index 0000000..859124f --- /dev/null +++ b/.zed/tasks.json @@ -0,0 +1,6 @@ +[ + { + "label": "moon subscriptions:bacon", + "command": "moon subscriptions:bacon" + } +] diff --git a/Cargo.lock b/Cargo.lock index 378efb2..469f376 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -242,9 +242,9 @@ dependencies = [ [[package]] name = "async-executor" -version = "1.13.2" +version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb812ffb58524bdd10860d7d974e2f01cc0950c2438a74ee5ec2e2280c6c4ffa" +checksum = "497c00e0fd83a72a79a39fcbd8e3e2f055d6f6c7e025f3b3d91f4f8e76527fb8" dependencies = [ "async-task", "concurrent-queue", @@ -271,7 +271,7 @@ dependencies = [ "futures-timer", "futures-util", "http", - "indexmap 2.10.0", + "indexmap 2.11.0", "mime", "multer", "num-traits", @@ -320,7 +320,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34ecdaff7c9cffa3614a9f9999bf9ee4c3078fe3ce4d6a6e161736b56febf2de" dependencies = [ "bytes", - "indexmap 2.10.0", + "indexmap 2.11.0", "serde", "serde_json", ] @@ -513,9 +513,9 @@ checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" [[package]] name = "bitflags" -version = "2.9.2" +version = "2.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a65b545ab31d687cff52899d4890855fec459eb6afe0da6417b8a18da87aa29" +checksum = "34efbcccd345379ca2868b2b2c9d3782e9cc58ba87bc7d79d5b53d9c9ae6f25d" dependencies = [ "serde", ] @@ -795,9 +795,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.45" +version = "4.5.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fc0e74a703892159f5ae7d3aac52c8e6c392f5ae5f359c70b5881d60aaac318" +checksum = "2c5e4fcf9c21d2e544ca1ee9d8552de13019a42aa7dbf32747fa7aaf1df76e57" dependencies = [ "clap_builder", "clap_derive", @@ -805,9 +805,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.44" +version = "4.5.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3e7f4214277f3c7aa526a59dd3fbe306a370daee1f8b7b8c987069cd8e888a8" +checksum = "fecb53a0e6fcfb055f686001bc2e2592fa527efaf38dbe81a6a9563562e57d41" dependencies = [ "anstream", "anstyle", @@ -2004,9 +2004,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" +checksum = "f2481980430f9f78649238835720ddccc57e52df14ffce1c6f37391d61b563e9" dependencies = [ "equivalent", "hashbrown 0.15.5", @@ -2024,9 +2024,9 @@ dependencies = [ [[package]] name = "io-uring" -version = "0.7.9" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4" +checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" dependencies = [ "bitflags", "cfg-if", @@ -2155,7 +2155,7 @@ dependencies = [ "petgraph", "pico-args", "regex", - "regex-syntax 0.8.5", + "regex-syntax 0.8.6", "string_cache", "term", "tiny-keccak", @@ -2169,7 +2169,7 @@ version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "507460a910eb7b32ee961886ff48539633b788a36b65692b95f225b844c82553" dependencies = [ - "regex-automata 0.4.9", + "regex-automata 0.4.10", ] [[package]] @@ -2776,7 +2776,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" dependencies = [ "fixedbitset", - "indexmap 2.10.0", + "indexmap 2.11.0", ] [[package]] @@ -3188,14 +3188,14 @@ dependencies = [ [[package]] name = "regex" -version = "1.11.1" +version = "1.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.9", - "regex-syntax 0.8.5", + "regex-automata 0.4.10", + "regex-syntax 0.8.6", ] [[package]] @@ -3209,20 +3209,20 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.5", + "regex-syntax 0.8.6", ] [[package]] name = "regex-lite" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a" +checksum = "943f41321c63ef1c92fd763bfe054d2668f7f225a5c29f0105903dc2fc04ba30" [[package]] name = "regex-syntax" @@ -3232,9 +3232,9 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" [[package]] name = "rend" @@ -3713,7 +3713,7 @@ version = "1.0.143" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a" dependencies = [ - "indexmap 2.10.0", + "indexmap 2.11.0", "itoa", "memchr", "ryu", @@ -3761,7 +3761,7 @@ dependencies = [ "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.10.0", + "indexmap 2.11.0", "schemars 0.9.0", "schemars 1.0.4", "serde", @@ -4060,7 +4060,7 @@ dependencies = [ "futures", "geo", "getrandom 0.3.3", - "indexmap 2.10.0", + "indexmap 2.11.0", "path-clean", "pharos", "reblessive", @@ -4551,7 +4551,7 @@ version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ - "indexmap 2.10.0", + "indexmap 2.11.0", "toml_datetime 0.6.11", "winnow", ] @@ -5379,9 +5379,9 @@ checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" [[package]] name = "winnow" -version = "0.7.12" +version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95" +checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" dependencies = [ "memchr", ] diff --git a/Cargo.toml b/Cargo.toml index d600500..3f9ebb6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" publish = false authors = ["Mustapha Annouaoui "] edition = "2024" +license = "MIT" [dependencies] axum = { version = "0.8.4", features = ["tracing"] } diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..3924e9c --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Mustapha Annouaoui + +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/Makefile.toml b/Makefile.toml deleted file mode 100644 index 0517a33..0000000 --- a/Makefile.toml +++ /dev/null @@ -1,15 +0,0 @@ -[tasks.migrations] -install_crate = "surrealdb-migrations@2.3.0" -command = "surrealdb-migrations" -args = ["${@}"] -env_files = [".env"] - -[tasks.bacon] -install_crate = "bacon@3.17.0" -command = "bacon" -args = ["${@}"] - -[tasks.upgrade] -install_crate = "cargo-edit@0.13.6" -command = "cargo" -args = ["upgrade", "${@}"] \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..54ca0db --- /dev/null +++ b/README.md @@ -0,0 +1,17 @@ +# Subscriptions + +This project is a learning exercise following the principles and best practices outlined in the "Zero to Production in Rust" book by Luca Palmieri. The goal is to build a robust and production-ready subscription service while exploring the Rust ecosystem. + +## Technologies Used + +* **Language:** [Rust](https://www.rust-lang.org/) +* **Web Framework:** [Axum](https://github.com/tokio-rs/axum) +* **Async Runtime:** [Tokio](https://tokio.rs/) +* **Database:** [SurrealDB](https://surrealdb.com/) +* **Containerization:** [Docker](https://www.docker.com/) +* **CI/CD:** [GitHub Actions](https://github.com/features/actions) +* **Task Runner:** [Moon](https://moonrepo.dev/) + +## License + +This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details. diff --git a/moon.yml b/moon.yml new file mode 100644 index 0000000..3f3e3a7 --- /dev/null +++ b/moon.yml @@ -0,0 +1,4 @@ +# yaml-language-server: $schema=https://moonrepo.dev/schemas/project.json + +stack: backend +layer: application diff --git a/src/handlers.rs b/src/handlers.rs index f1301b1..2effae0 100644 --- a/src/handlers.rs +++ b/src/handlers.rs @@ -1,5 +1,5 @@ -use crate::Result; use crate::model::ModelManager; +use crate::{Result, model::Subscription}; use axum::{Form, extract::State, http::StatusCode}; use serde::Deserialize; use std::sync::Arc; @@ -15,13 +15,8 @@ pub async fn subscribe( State(mm): State>, Form(form): Form, ) -> Result { - mm.db() - .await? - .query("CREATE subscriptions SET name = $name, email = $email") - .bind(("name", form.name)) - .bind(("email", form.email)) - .await? - .check()?; + let subscription = Subscription::new(form.name, form.email); + mm.create_subscription(subscription).await?; Ok(StatusCode::CREATED) } diff --git a/src/main.rs b/src/main.rs index b542bf0..15ddfb7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,39 +15,31 @@ async fn main() { .init(); // Initialize configuration - let config = match Config::load() { - Ok(config) => config, - Err(error) => { - tracing::error!(%error, "Failed to load configuration"); - process::exit(1); - } - }; + let config = Config::load().unwrap_or_else(|error| { + tracing::error!(%error, "Failed to load configuration"); + process::exit(1); + }); // Initialize application - let (router, _) = match subscriptions::init(&config).await { - Ok(result) => result, - Err(error) => { - tracing::error!(%error, "Failed to initialize application"); - process::exit(1); - } - }; + let (router, _) = subscriptions::init(&config).await.unwrap_or_else(|error| { + tracing::error!(%error, "Failed to initialize application"); + process::exit(1); + }); // Bind address - let listener = match TcpListener::bind((config.host.clone(), config.port)).await { - Ok(listener) => listener, - Err(error) => { + let listener = TcpListener::bind((config.host.clone(), config.port)) + .await + .unwrap_or_else(|error| { tracing::error!(%error, "Failed to bind address `{}:{}`", config.host, config.port); process::exit(1); - } - }; + }); // Start server tracing::info!("Start listening on: http://{}:{}", config.host, config.port); - match axum::serve(listener, router).await { - Ok(()) => tracing::info!("Server gracefully shutdown"), - Err(error) => { - tracing::error!(%error, "Failed to start server"); - process::exit(1); - } + if let Err(error) = axum::serve(listener, router).await { + tracing::error!(%error, "Failed to start server"); + process::exit(1); } + + tracing::info!("Server gracefully shutdown"); } diff --git a/src/model.rs b/src/model.rs index f7cec72..95b9d29 100644 --- a/src/model.rs +++ b/src/model.rs @@ -4,6 +4,19 @@ use surrealdb_migrations::MigrationRunner; use tokio::sync::OnceCell; use crate::{Error, Result, config::DatabaseConfig}; +use serde::Serialize; + +#[derive(Debug, Serialize)] +pub struct Subscription { + name: String, + email: String, +} + +impl Subscription { + pub fn new(name: String, email: String) -> Self { + Self { name, email } + } +} #[derive(Debug, Clone)] pub struct ModelManager { @@ -23,6 +36,17 @@ impl ModelManager { self.db.get_or_try_init(async || self.connect().await).await } + pub async fn create_subscription(&self, subscription: Subscription) -> Result<()> { + self.db() + .await? + .query("CREATE subscriptions SET name = $name, email = $email") + .bind(("name", subscription.name)) + .bind(("email", subscription.email)) + .await?; + + Ok(()) + } + async fn connect(&self) -> Result> { let config = &self.config; let db = Surreal::::init();