├── .config └── nextest.toml ├── .github └── workflows │ ├── linux.yml │ ├── macos.yml │ └── windows.yml ├── .gitignore ├── .gitmodules ├── Cargo.toml ├── LICENSE ├── README.md ├── build.rs ├── changelog.txt ├── deny.toml ├── example-forum-server ├── Cargo.toml ├── README.md ├── src │ ├── handler │ │ ├── mod.rs │ │ ├── thread.rs │ │ └── user.rs │ ├── lib.rs │ ├── main.rs │ ├── models │ │ ├── mod.rs │ │ ├── post.rs │ │ ├── stars.rs │ │ ├── thread.rs │ │ ├── thumb.rs │ │ └── user.rs │ └── test.rs └── tests │ └── example_forum_server.rs ├── rorm-macro-impl ├── Cargo.toml └── src │ ├── analyze │ ├── mod.rs │ └── model.rs │ ├── generate │ ├── db_enum.rs │ ├── mod.rs │ ├── model.rs │ ├── patch.rs │ └── utils.rs │ ├── lib.rs │ ├── parse │ ├── annotations.rs │ ├── db_enum.rs │ ├── mod.rs │ ├── model.rs │ └── patch.rs │ └── utils.rs ├── rorm-macro ├── Cargo.toml └── src │ └── lib.rs ├── src ├── conditions │ ├── collections.rs │ ├── in.rs │ └── mod.rs ├── crud │ ├── builder.rs │ ├── decoder.rs │ ├── delete.rs │ ├── insert.rs │ ├── mod.rs │ ├── query.rs │ ├── selector.rs │ └── update.rs ├── fields │ ├── mod.rs │ ├── proxy.rs │ ├── traits │ │ ├── aggregate.rs │ │ ├── cmp.rs │ │ └── mod.rs │ ├── types │ │ ├── back_ref.rs │ │ ├── chrono.rs │ │ ├── foreign_model.rs │ │ ├── json.rs │ │ ├── max_str.rs │ │ ├── max_str_impl.rs │ │ ├── mod.rs │ │ ├── msgpack.rs │ │ ├── postgres_only.rs │ │ ├── std.rs │ │ ├── time.rs │ │ ├── url.rs │ │ └── uuid.rs │ └── utils │ │ ├── check.rs │ │ ├── column_name.rs │ │ ├── const_fn.rs │ │ ├── get_annotations.rs │ │ ├── get_names.rs │ │ └── mod.rs ├── internal │ ├── const_concat.rs │ ├── djb2.rs │ ├── field │ │ ├── decoder.rs │ │ ├── fake_field.rs │ │ ├── foreign_model.rs │ │ ├── mod.rs │ │ └── mulit_column.rs │ ├── hmr │ │ ├── annotations.rs │ │ └── mod.rs │ ├── mod.rs │ ├── patch.rs │ ├── query_context │ │ ├── flat_conditions.rs │ │ └── mod.rs │ └── relation_path.rs ├── lib.rs └── model.rs └── tests ├── data └── derives │ ├── basic.rs │ ├── basic_expansions │ ├── BasicEnum.rs │ ├── BasicModel.rs │ └── BasicPatch.rs │ ├── experimental.rs │ └── experimental_expansions │ ├── Generic.rs │ └── Unregistered.rs └── derives.rs /.config/nextest.toml: -------------------------------------------------------------------------------- 1 | [profile.ci] 2 | # Do not cancel the test run on the first failure. 3 | fail-fast = false -------------------------------------------------------------------------------- /.github/workflows/linux.yml: -------------------------------------------------------------------------------- 1 | name: Build & Test for Linux 2 | on: 3 | push: 4 | paths-ignore: 5 | - "*.md" 6 | pull_request: 7 | 8 | jobs: 9 | build_rs: 10 | name: Build & Tests on linux 11 | runs-on: ubuntu-latest 12 | permissions: 13 | contents: read 14 | security-events: write 15 | actions: read 16 | env: 17 | CARGO_TERM_COLOR: always 18 | steps: 19 | - uses: actions/checkout@v4 20 | with: 21 | submodules: "recursive" 22 | 23 | - uses: moonrepo/setup-rust@v1 24 | with: 25 | components: clippy 26 | bins: cargo-deny, clippy-sarif, sarif-fmt, cargo-nextest 27 | cache-target: debug 28 | 29 | - name: Build rorm 30 | run: cargo build -p rorm 31 | 32 | - name: Run cargo nextest 33 | run: cargo nextest --profile ci run --workspace 34 | 35 | - name: Run cargo doctest 36 | run: cargo test --workspace --doc 37 | 38 | - name: Run rust-clippy 39 | run: cargo clippy --workspace --message-format=json | clippy-sarif | tee rust-clippy-results.sarif | sarif-fmt 40 | continue-on-error: true 41 | 42 | - name: Upload clippy analysis results to GitHub 43 | uses: github/codeql-action/upload-sarif@v2 44 | with: 45 | sarif_file: rust-clippy-results.sarif 46 | wait-for-processing: true 47 | 48 | - name: Run cargo deny check 49 | run: cargo deny check --hide-inclusion-graph -------------------------------------------------------------------------------- /.github/workflows/macos.yml: -------------------------------------------------------------------------------- 1 | name: Build & Test for MacOS 2 | on: 3 | push: 4 | paths-ignore: 5 | - "*.md" 6 | pull_request: 7 | 8 | jobs: 9 | build_rs: 10 | name: Build & Tests on MacOS 11 | runs-on: macos-latest 12 | permissions: 13 | contents: read 14 | security-events: write 15 | actions: read 16 | env: 17 | CARGO_TERM_COLOR: always 18 | steps: 19 | - uses: actions/checkout@v3 20 | with: 21 | submodules: "recursive" 22 | 23 | # When rustup is updated, it tries to replace its binary, which on Windows is somehow locked. 24 | # This can result in the CI failure, see: https://github.com/rust-lang/rustup/issues/3029 25 | - run: | 26 | rustup set auto-self-update disable 27 | rustup toolchain install stable --profile minimal 28 | 29 | - uses: Swatinem/rust-cache@v2 30 | 31 | - name: Build rorm 32 | run: cargo build -p rorm -------------------------------------------------------------------------------- /.github/workflows/windows.yml: -------------------------------------------------------------------------------- 1 | name: Build & Test for Windows 2 | on: 3 | push: 4 | paths-ignore: 5 | - "*.md" 6 | pull_request: 7 | 8 | jobs: 9 | build_rs: 10 | name: Build & Tests on Windows 11 | runs-on: windows-latest 12 | permissions: 13 | contents: read 14 | security-events: write 15 | actions: read 16 | env: 17 | CARGO_TERM_COLOR: always 18 | steps: 19 | - uses: actions/checkout@v3 20 | with: 21 | submodules: "recursive" 22 | 23 | # When rustup is updated, it tries to replace its binary, which on Windows is somehow locked. 24 | # This can result in the CI failure, see: https://github.com/rust-lang/rustup/issues/3029 25 | - run: | 26 | rustup set auto-self-update disable 27 | rustup toolchain install stable --profile minimal 28 | 29 | - uses: Swatinem/rust-cache@v2 30 | 31 | - name: Build rorm 32 | run: cargo build -p rorm -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.exe 2 | *.o 3 | *.obj 4 | *.lst 5 | *.db 6 | *.sqlite3* 7 | *.a 8 | .idea/ 9 | .vscode/ 10 | *.iml 11 | migrations/ 12 | !rorm-sample/migrations/ 13 | target/ 14 | .models.json 15 | database.toml 16 | *.h 17 | *.hpp 18 | .vagrant/ 19 | Cargo.lock 20 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "rorm-cli"] 2 | path = rorm-cli 3 | url = https://github.com/rorm-orm/rorm-cli 4 | [submodule "rorm-declaration"] 5 | path = rorm-declaration 6 | url = https://github.com/rorm-orm/rorm-declaration 7 | [submodule "rorm-sql"] 8 | path = rorm-sql 9 | url = https://github.com/rorm-orm/rorm-sql 10 | [submodule "rorm-db"] 11 | path = rorm-db 12 | url = https://github.com/rorm-orm/rorm-db 13 | [submodule "rorm-lib"] 14 | path = rorm-lib 15 | url = https://github.com/rorm-orm/rorm-lib.git 16 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | ".", 4 | "rorm-db", 5 | "rorm-declaration", 6 | "rorm-macro", 7 | "rorm-macro-impl", 8 | "rorm-sql", 9 | "example-forum-server", 10 | ] 11 | 12 | [package] 13 | name = "rorm" 14 | version = "0.7.1" 15 | edition = "2021" 16 | repository = "https://github.com/rorm-orm/rorm" 17 | authors = ["gammelalf", "myOmikron "] 18 | categories = ["database"] 19 | keywords = ["database", "orm", "async"] 20 | description = "A asynchronous declarative ORM written in pure rust." 21 | homepage = "https://rorm.rs" 22 | documentation = "https://docs.rorm.rs" 23 | license = "MIT" 24 | 25 | [lib] 26 | name = "rorm" 27 | path = "src/lib.rs" 28 | 29 | [dependencies] 30 | 31 | # Logging facade 32 | tracing = "~0.1" 33 | 34 | # Basic traits and types required for async 35 | futures-core = "~0.3" # Used for the stream trait 36 | 37 | # json serialization to communicate with the migrator 38 | serde_json = { version = "~1" } 39 | serde = { version = "~1" } 40 | 41 | # Macro using linker magic to collect items from different code locations into a slice 42 | linkme = { version = "~0.3" } # Used to collect all models declared by any crate part of the final binary 43 | 44 | # Macro implemeting pin-projection 45 | pin-project = { version = "~1" } # Used to wrap futures and streams which are !Unpin 46 | 47 | rorm-db = { version = "~0.10", path = "./rorm-db", features = ["serde"] } 48 | rorm-macro = { version = "~0.9", path = "./rorm-macro" } 49 | rorm-declaration = { version = "~0.4", path = "./rorm-declaration" } 50 | 51 | # rorm-cli exposes interfaces to integrate the cli as library 52 | rorm-cli = { version = "~0.8", path = "./rorm-cli", default-features = false, optional = true } 53 | 54 | # Mac address support (postgres-only) 55 | mac_address = { version = "~1", optional = true } 56 | 57 | # Bitvec support (postgres-only) 58 | bit-vec = { version = "~0.6", optional = true } 59 | 60 | # Ip network support (postgres-only) 61 | ipnetwork = { version = "~0.20", optional = true } 62 | 63 | # Date and time support 64 | chrono = { version = ">=0.4.20", default-features = false, optional = true } 65 | time = { version = "~0.3", optional = true } 66 | 67 | # Uuid support 68 | uuid = { version = "~1", optional = true } 69 | 70 | # Url support 71 | url = { version = "~2", optional = true } 72 | 73 | # `MessagePack` support 74 | rmp-serde = { version = "~1", optional = true } 75 | 76 | # `ToSchema` support for `MaxStr` 77 | utoipa = { version = "~4", optional = true } 78 | 79 | # `JsonSchema` support for `MaxStr` 80 | schemars = { version = "~0.8", optional = true } 81 | 82 | [build-dependencies] 83 | rustc_version = "0.4.0" 84 | 85 | [package.metadata.docs.rs] 86 | features = [ 87 | "all-drivers", 88 | "chrono", 89 | "time", 90 | "uuid", 91 | "url", 92 | "utoipa", 93 | "schemars", 94 | "msgpack", 95 | "cli", 96 | "rustls", 97 | "native-tls" 98 | ] 99 | 100 | [features] 101 | default = [ 102 | "all-drivers", 103 | "chrono", 104 | "time", 105 | "uuid", 106 | "url", 107 | ] 108 | 109 | # Drivers 110 | all-drivers = [ 111 | "rorm-db/postgres", 112 | "rorm-cli?/postgres", 113 | "rorm-db/mysql", 114 | "rorm-cli?/mysql", 115 | "rorm-db/sqlite", 116 | "rorm-cli?/sqlite", 117 | ] 118 | postgres-only = [ 119 | "rorm-db/postgres-only", 120 | "rorm-cli?/postgres", 121 | "dep:mac_address", 122 | "dep:ipnetwork", 123 | "dep:bit-vec", 124 | ] 125 | 126 | # Extensions 127 | chrono = ["dep:chrono"] 128 | time = ["dep:time"] 129 | uuid = ["dep:uuid"] 130 | url = ["dep:url"] 131 | utoipa = ["dep:utoipa"] 132 | schemars = ["dep:schemars"] 133 | 134 | msgpack = ["dep:rmp-serde"] 135 | cli = ["dep:rorm-cli"] 136 | 137 | # TLS libraries 138 | rustls = ["rorm-db/rustls"] 139 | native-tls = ["rorm-db/native-tls"] 140 | 141 | [profile.release-lto] 142 | inherits = "release" 143 | lto = "fat" 144 | 145 | [profile.release-debug] 146 | inherits = "release" 147 | debug = true 148 | 149 | [dev-dependencies] 150 | rorm = { path = "." } 151 | rorm-macro-impl = { path = "./rorm-macro-impl" } 152 | 153 | proc-macro2 = { version = "~1" } 154 | syn = { version = "~2" } # Parse files, search for derives and format the expansion with prettyplease 155 | prettyplease = { version = "~0.2" } # Simple code formatter taking syn as input 156 | trybuild = { version = "~1" } # Compiles a single rust file 157 | datatest-stable = { version = "~0.3" } # Test harness which generates cases from files 158 | 159 | [[test]] 160 | name = "derives" 161 | harness = false 162 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 myOmikron 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rorm 2 | 3 | [![License](https://img.shields.io/github/license/rorm-orm/rorm?label=License&color=blue)](https://github.com/rorm-orm/rorm/blob/dev/LICENSE) 4 | [![Crates.io](https://img.shields.io/crates/v/rorm?label=Crates.io)](https://crates.io/crates/rorm) 5 | [![Docs](https://img.shields.io/docsrs/rorm?label=Docs)](https://docs.rs/rorm/latest/rorm/) 6 | [![Linux Build](https://img.shields.io/github/actions/workflow/status/rorm-orm/rorm/linux.yml?branch=dev&label=Linux%20CI)](https://github.com/rorm-orm/rorm/actions/workflows/linux.yml) 7 | [![Windows Build](https://img.shields.io/github/actions/workflow/status/rorm-orm/rorm/windows.yml?branch=dev&label=Windows%20CI)](https://github.com/rorm-orm/rorm/actions/workflows/windows.yml) 8 | [![Windows Build](https://img.shields.io/github/actions/workflow/status/rorm-orm/rorm/macos.yml?branch=dev&label=MacOS%20CI)](https://github.com/rorm-orm/rorm/actions/workflows/macos.yml) 9 | 10 | `rorm` is an ORM (Object Relation Mapper) written in Rust. 11 | 12 | The following databases are currently supported: 13 | - SQLite 3 14 | - MariaDB 10.5 - 10.9 15 | - Postgres 11 - 15 16 | 17 | ## Documentation 18 | 19 | Take a look at [rorm-orm/docs](https://github.com/rorm-orm/docs) or just use the 20 | deployed documentation: [rorm.rs](https://rorm.rs). 21 | 22 | ## Contribution 23 | 24 | Before contribution, see the [development guidelines](https://rorm.rs/developer/guidelines). 25 | 26 | ## Contact 27 | 28 | You want to discuss something? Get in touch with us in our Matrix 29 | room [#rorm](https://matrix.to/#/#rorm:matrix.hopfenspace.org). 30 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | use rustc_version::{version_meta, Channel, VersionMeta}; 2 | 3 | fn main() { 4 | println!("cargo::rustc-check-cfg=cfg(CHANNEL_NIGHTLY)"); 5 | if matches!(version_meta(), Ok(VersionMeta { channel: Channel::Nightly, .. })) { 6 | println!("cargo:rustc-cfg=CHANNEL_NIGHTLY"); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /changelog.txt: -------------------------------------------------------------------------------- 1 | Since 0.7.1 2 | ----------- 3 | - Added `ForeignModelByField::query_as` 4 | - Added `ForeignModelByField::as_condition` 5 | - Added `ForeignModelByField::into_condition` 6 | - Enforced column names to be at most 63 bytes 7 | 8 | Notes for publishing 9 | -------------------- 10 | - don't forget to bump and publish rorm-macro! 11 | -------------------------------------------------------------------------------- /deny.toml: -------------------------------------------------------------------------------- 1 | [graph] 2 | features = ["default", "msgpack", "rustls", "native-tls"] 3 | 4 | [advisories] 5 | # The path where the advisory database is cloned/fetched into 6 | db-path = "~/.cargo/advisory-db" 7 | # The url(s) of the advisory databases to use 8 | db-urls = ["https://github.com/rustsec/advisory-db"] 9 | # The lint level for security vulnerabilities 10 | yanked = "warn" 11 | ignore = [ 12 | # marvin attack on rsa 13 | "RUSTSEC-2023-0071", # not patched yet 14 | 15 | # paste is unmaintained 16 | "RUSTSEC-2024-0436", # it is a tiny macro utility only used rmp internally and works for them 17 | ] 18 | 19 | [licenses] 20 | allow = [ 21 | "MIT", 22 | "Apache-2.0", 23 | "BSD-2-Clause", 24 | "BSD-3-Clause", 25 | "MPL-2.0", 26 | "ISC", 27 | "Unicode-3.0", 28 | "LicenseRef-ring", 29 | "Zlib", 30 | ] 31 | 32 | [[licenses.clarify]] 33 | name = "ring" 34 | expression = "LicenseRef-ring" 35 | license-files = [ 36 | { path = "LICENSE", hash = 0xbd0eed23 }, 37 | ] 38 | -------------------------------------------------------------------------------- /example-forum-server/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "example-forum-server" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | # Web framework 8 | axum = { version = "~0.7", features = ["macros"] } 9 | tower-sessions = { version = "~0.12" } 10 | 11 | # Async runtime 12 | tokio = { version = "~1", features = ["full"] } 13 | 14 | # Extension traits for `Future` and `Stream` 15 | futures-util = "0.3.30" # Used for consuming streams of db results 16 | 17 | # Database abstraction 18 | rorm = { path = "..", features = ["cli"] } 19 | 20 | # Cli argument parser 21 | clap = { version = "~4", features = ["derive"] } 22 | 23 | # Fancy boxed error 24 | anyhow = { version = "~1" } 25 | 26 | # Logging 27 | tracing = { version = "~0.1" } 28 | tracing-subscriber = { version = "~0.3", features = ["env-filter"] } 29 | 30 | # Json serialization 31 | serde = { version = "~1", features = ["derive"] } 32 | serde_json = { version = "~1" } 33 | 34 | # Web client 35 | reqwest = { version = "~0.12", features = ["json", "cookies"] } # Used by the test subcommand 36 | 37 | time = "~0.3" 38 | uuid = { version = "~1", features = ["v4", "serde"] } -------------------------------------------------------------------------------- /example-forum-server/README.md: -------------------------------------------------------------------------------- 1 | # Forum Server 2 | 3 | An example of how one could combine `rorm` with `axum` to implement a simple forum 4 | 5 | It is by no means "complete" and not intended be used as an inspiration for how to use `axum`. 6 | 7 | ## Usage 8 | 9 | Create the migrations 10 | `cargo run --package example-forum-server -- make-migrations migrations/` 11 | 12 | Apply them 13 | `cargo run --package example-forum-server -- migrate migrations/` 14 | 15 | Run the server 16 | `cargo run --package example-forum-server -- start` 17 | 18 | (Optional) Run a test client 19 | `cargo run --package example-forum-server -- test` 20 | 21 | ## CI 22 | 23 | This project is built and run in the CI. 24 | 25 | It stays up to date with the current version of `rorm` and serves as a test for `rorm`. -------------------------------------------------------------------------------- /example-forum-server/src/handler/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod thread; 2 | pub mod user; 3 | 4 | use axum::extract::FromRequestParts; 5 | use axum::http::request::Parts; 6 | use axum::http::StatusCode; 7 | use axum::response::{IntoResponse, Response}; 8 | use axum::routing::{delete, get, post, put}; 9 | use axum::Router; 10 | use rorm::Database; 11 | use tower_sessions::{session, Session}; 12 | 13 | use crate::models::user::User; 14 | 15 | pub fn get_router() -> Router { 16 | Router::new().nest( 17 | "/api", 18 | Router::new() 19 | .nest( 20 | "/user", 21 | Router::new() 22 | .route("/register", post(user::register)) 23 | .route("/login", post(user::login)) 24 | .route("/logout", put(user::logout)) 25 | .route("/delete", put(user::delete)) 26 | .route("/profile/:username", get(user::profile)), 27 | ) 28 | .nest( 29 | "/thread", 30 | Router::new() 31 | .route("/create", post(thread::create)) 32 | .route("/list", get(thread::list)) 33 | .route("/get/:identifier", get(thread::get)) 34 | .route("/posts/:identifier", post(thread::make_post)) 35 | .route("/delete/:identifier", delete(thread::delete)), 36 | ), 37 | ) 38 | } 39 | 40 | pub struct SessionUser(User); 41 | #[axum::async_trait] 42 | impl FromRequestParts for SessionUser { 43 | type Rejection = ApiError; 44 | 45 | async fn from_request_parts(parts: &mut Parts, db: &Database) -> Result { 46 | let session = Session::from_request_parts(parts, db) 47 | .await 48 | .map_err(|(_, msg)| ApiError::ServerError(format!("Session error: {msg}")))?; 49 | let user = rorm::query(db, User) 50 | .condition( 51 | User.id.equals( 52 | session 53 | .get::("user_id") 54 | .await? 55 | .ok_or_else(|| ApiError::BadRequest("Please login first".to_string()))?, 56 | ), 57 | ) 58 | .one() 59 | .await?; 60 | Ok(Self(user)) 61 | } 62 | } 63 | 64 | pub type ApiResult = Result; 65 | 66 | pub enum ApiError { 67 | BadRequest(String), 68 | ServerError(String), 69 | } 70 | 71 | impl IntoResponse for ApiError { 72 | fn into_response(self) -> Response { 73 | let (status, body) = match self { 74 | ApiError::BadRequest(body) => (StatusCode::BAD_REQUEST, body), 75 | ApiError::ServerError(body) => (StatusCode::INTERNAL_SERVER_ERROR, body), 76 | }; 77 | let mut response = Response::new(body.into()); 78 | *response.status_mut() = status; 79 | response 80 | } 81 | } 82 | 83 | impl From for ApiError { 84 | fn from(value: rorm::Error) -> Self { 85 | ApiError::ServerError(format!("Database error: {value}")) 86 | } 87 | } 88 | impl From for ApiError { 89 | fn from(value: session::Error) -> Self { 90 | ApiError::ServerError(format!("Session error: {value}")) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /example-forum-server/src/handler/thread.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use axum::extract::{Path, State}; 4 | use axum::Json; 5 | use futures_util::TryStreamExt; 6 | use rorm::fields::types::MaxStr; 7 | use rorm::prelude::ForeignModelByField; 8 | use rorm::{and, Database, Patch}; 9 | use serde::{Deserialize, Serialize}; 10 | use time::OffsetDateTime; 11 | use uuid::Uuid; 12 | 13 | use crate::handler::{ApiError, ApiResult, SessionUser}; 14 | use crate::models::post::{NewPost, Post}; 15 | use crate::models::thread::{NewThread, Thread}; 16 | use crate::models::user::User; 17 | 18 | #[derive(Serialize, Deserialize)] 19 | pub struct CreateThreadRequest { 20 | pub name: String, 21 | } 22 | 23 | pub async fn create( 24 | State(db): State, 25 | SessionUser(_user): SessionUser, 26 | Json(request): Json, 27 | ) -> ApiResult> { 28 | let mut tx = db.start_transaction().await?; 29 | let identifier = request.name.to_ascii_lowercase(); 30 | if rorm::query(&mut tx, Thread) 31 | .condition(Thread.identifier.equals(&identifier)) 32 | .optional() 33 | .await? 34 | .is_some() 35 | { 36 | return Err(ApiError::BadRequest( 37 | "Please choose another name".to_string(), 38 | )); 39 | } 40 | rorm::insert(&mut tx, Thread) 41 | .single(&NewThread { 42 | identifier: identifier.clone(), 43 | name: request.name, 44 | }) 45 | .await?; 46 | tx.commit().await?; 47 | Ok(Json(identifier)) 48 | } 49 | 50 | #[derive(Serialize, Deserialize)] 51 | pub struct ListResponse { 52 | pub threads: Vec, 53 | } 54 | 55 | #[derive(Serialize, Deserialize, Patch)] 56 | #[rorm(model = "Thread")] 57 | pub struct ListResponseItem { 58 | pub identifier: String, 59 | pub name: String, 60 | pub opened_at: OffsetDateTime, 61 | } 62 | 63 | pub async fn list( 64 | State(db): State, 65 | SessionUser(_user): SessionUser, 66 | ) -> ApiResult> { 67 | let threads = rorm::query(&db, ListResponseItem).all().await?; 68 | Ok(Json(ListResponse { threads })) 69 | } 70 | 71 | #[derive(Serialize, Deserialize)] 72 | pub struct GetResponse { 73 | pub identifier: String, 74 | pub name: String, 75 | pub opened_at: OffsetDateTime, 76 | pub posts: Vec, 77 | } 78 | #[derive(Serialize, Deserialize)] 79 | pub struct ThreadPost { 80 | pub uuid: Uuid, 81 | pub user: Option>, 82 | pub message: String, 83 | pub posted_at: OffsetDateTime, 84 | pub replies: i64, 85 | } 86 | pub async fn get( 87 | State(db): State, 88 | Path(thread): Path, 89 | ) -> ApiResult> { 90 | let mut tx = db.start_transaction().await?; 91 | 92 | let (name, opened_at) = rorm::query(&mut tx, (Thread.name, Thread.opened_at)) 93 | .condition(Thread.identifier.equals(&thread)) 94 | .optional() 95 | .await? 96 | .ok_or_else(|| ApiError::BadRequest("Unknown thread".to_ascii_lowercase()))?; 97 | 98 | let users = rorm::query(&mut tx, (User.id, User.username)) 99 | .stream() 100 | .try_collect::>() 101 | .await?; 102 | 103 | let posts: Vec<_> = rorm::query( 104 | &mut tx, 105 | (Post.uuid, Post.message, Post.user, Post.posted_at), 106 | ) 107 | .condition(Post.thread.equals(&thread)) 108 | .order_asc(Post.posted_at) 109 | .stream() 110 | .map_ok(|(uuid, message, user, posted_at)| ThreadPost { 111 | uuid, 112 | user: user.map(|ForeignModelByField(id)| users[&id].clone()), 113 | message: message.into_inner(), 114 | posted_at, 115 | replies: 0, 116 | }) 117 | .try_collect() 118 | .await?; 119 | 120 | tx.commit().await?; 121 | Ok(Json(GetResponse { 122 | identifier: thread, 123 | name, 124 | opened_at, 125 | posts, 126 | })) 127 | } 128 | 129 | #[derive(Serialize, Deserialize)] 130 | pub struct MakePostRequest { 131 | pub message: String, 132 | pub reply_to: Option, 133 | } 134 | pub async fn make_post( 135 | State(db): State, 136 | SessionUser(user): SessionUser, 137 | Path(thread): Path, 138 | Json(request): Json, 139 | ) -> ApiResult<()> { 140 | let mut tx = db.start_transaction().await?; 141 | 142 | rorm::query(&mut tx, Thread.identifier) 143 | .condition(Thread.identifier.equals(&thread)) 144 | .optional() 145 | .await? 146 | .ok_or_else(|| ApiError::BadRequest("Unknown thread".to_string()))?; 147 | 148 | if let Some(reply_to) = request.reply_to { 149 | rorm::query(&mut tx, Post.uuid) 150 | .condition(and![ 151 | Post.uuid.equals(reply_to), 152 | Post.thread.equals(&thread), 153 | ]) 154 | .optional() 155 | .await? 156 | .ok_or_else(|| ApiError::BadRequest("Unknown post".to_string()))?; 157 | } 158 | 159 | rorm::insert(&mut tx, Post) 160 | .return_nothing() 161 | .single(&NewPost { 162 | uuid: Uuid::new_v4(), 163 | message: MaxStr::new(request.message) 164 | .map_err(|_| ApiError::BadRequest("Post's message is too long".to_string()))?, 165 | user: Some(ForeignModelByField(user.id)), 166 | thread: ForeignModelByField(thread), 167 | reply_to: request.reply_to.map(ForeignModelByField), 168 | }) 169 | .await?; 170 | 171 | tx.commit().await?; 172 | Ok(()) 173 | } 174 | 175 | pub async fn delete( 176 | State(db): State, 177 | SessionUser(_user): SessionUser, 178 | Path(identifier): Path, 179 | ) -> ApiResult<()> { 180 | rorm::delete(&db, Thread) 181 | .condition(Thread.identifier.equals(&identifier)) 182 | .await?; 183 | Ok(()) 184 | } 185 | -------------------------------------------------------------------------------- /example-forum-server/src/handler/user.rs: -------------------------------------------------------------------------------- 1 | use axum::extract::{Path, State}; 2 | use axum::Json; 3 | use rorm::fields::types::MaxStr; 4 | use rorm::{and, Database}; 5 | use serde::{Deserialize, Serialize}; 6 | use tower_sessions::Session; 7 | 8 | use crate::handler::{ApiError, ApiResult, SessionUser}; 9 | use crate::models::user::{NewUser, User, UserRole}; 10 | 11 | #[derive(Serialize, Deserialize)] 12 | pub struct RegisterRequest { 13 | pub username: MaxStr<255>, 14 | pub password: String, 15 | } 16 | pub async fn register( 17 | State(db): State, 18 | session: Session, 19 | Json(request): Json, 20 | ) -> ApiResult<()> { 21 | let mut tx = db.start_transaction().await?; 22 | if rorm::query(&mut tx, User.id) 23 | .condition(User.username.equals(&*request.username)) 24 | .optional() 25 | .await? 26 | .is_some() 27 | { 28 | return Err(ApiError::BadRequest(format!( 29 | "The username `{}` is already taken", 30 | request.username 31 | ))); 32 | } 33 | let id = rorm::insert(&mut tx, User) 34 | .return_primary_key() 35 | .single(&NewUser { 36 | username: request.username, 37 | password: request.password, 38 | role: UserRole::User, 39 | }) 40 | .await?; 41 | session.insert("user_id", &id).await?; 42 | tx.commit().await?; 43 | Ok(()) 44 | } 45 | 46 | #[derive(Serialize, Deserialize)] 47 | pub struct LoginRequest { 48 | pub username: String, 49 | pub password: String, 50 | } 51 | pub async fn login( 52 | State(db): State, 53 | session: Session, 54 | Json(request): Json, 55 | ) -> ApiResult<()> { 56 | if let Some(id) = rorm::query(&db, User.id) 57 | .condition(and![ 58 | User.username.equals(&request.username), 59 | User.password.equals(&request.password) 60 | ]) 61 | .optional() 62 | .await? 63 | { 64 | session.insert("user_id", &id).await?; 65 | Ok(()) 66 | } else { 67 | Err(ApiError::BadRequest( 68 | "Invalid password or username".to_string(), 69 | )) 70 | } 71 | } 72 | 73 | pub async fn logout(session: Session) -> ApiResult<()> { 74 | session.flush().await?; 75 | Ok(()) 76 | } 77 | 78 | pub async fn delete( 79 | State(db): State, 80 | SessionUser(user): SessionUser, 81 | session: Session, 82 | ) -> ApiResult<()> { 83 | rorm::delete(&db, User) 84 | .condition(User.id.equals(user.id)) 85 | .await?; 86 | session.flush().await?; 87 | Ok(()) 88 | } 89 | 90 | #[derive(Serialize, Deserialize)] 91 | pub struct ProfileResponse { 92 | pub username: String, 93 | pub role: String, 94 | pub posts: i64, 95 | } 96 | pub async fn profile( 97 | State(db): State, 98 | SessionUser(_user): SessionUser, 99 | Path(username): Path, 100 | ) -> ApiResult> { 101 | let mut tx = db.start_transaction().await?; 102 | let (role,) = rorm::query(&mut tx, (User.role,)) 103 | .condition(User.username.equals(&username)) 104 | .optional() 105 | .await? 106 | .ok_or_else(|| ApiError::BadRequest(format!("Unknown user: {username}")))?; 107 | let posts = rorm::query(&mut tx, User.posts.uuid.count()) 108 | .condition(User.username.equals(&username)) 109 | .one() 110 | .await?; 111 | tx.commit().await?; 112 | Ok(Json(ProfileResponse { 113 | username, 114 | role: role.to_string(), 115 | posts, 116 | })) 117 | } 118 | -------------------------------------------------------------------------------- /example-forum-server/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod handler; 2 | mod models; 3 | mod test; 4 | 5 | use std::fs; 6 | 7 | use anyhow::Context; 8 | use clap::{Parser, Subcommand}; 9 | use rorm::config::DatabaseConfig; 10 | use rorm::{Database, DatabaseConfiguration, DatabaseDriver}; 11 | use tokio::net::TcpListener; 12 | use tower_sessions::{MemoryStore, SessionManagerLayer}; 13 | use tracing::info; 14 | 15 | /// The cli 16 | #[derive(Parser)] 17 | pub struct Cli { 18 | /// The path to the database config file 19 | #[clap(long)] 20 | pub db_config: Option, 21 | 22 | /// The available subcommands 23 | #[clap(subcommand)] 24 | pub command: Command, 25 | } 26 | 27 | /// All available commands 28 | #[derive(Subcommand)] 29 | pub enum Command { 30 | /// Run the migrations on the database 31 | Migrate { 32 | /// The directory where the migration files are located in 33 | migrations_dir: String, 34 | }, 35 | /// Create new migrations 36 | #[cfg(debug_assertions)] 37 | MakeMigrations { 38 | /// The directory where the migration files are located in 39 | migrations_dir: String, 40 | }, 41 | /// Start the server 42 | Start, 43 | /// Tests the server by sending it requests 44 | Test, 45 | } 46 | 47 | pub async fn run_main(cli: Cli) -> anyhow::Result<()> { 48 | let db_driver = match cli.db_config { 49 | Some(path) => { 50 | serde_json::from_reader(fs::File::open(path).context("Failed to open db config")?) 51 | .context("Failed to read db config")? 52 | } 53 | None => DatabaseDriver::SQLite { 54 | filename: "db.sqlite".to_string(), 55 | }, 56 | }; 57 | let db = Database::connect(DatabaseConfiguration::new(db_driver.clone())) 58 | .await 59 | .context("Failed to connect to db")?; 60 | 61 | match cli.command { 62 | #[cfg(debug_assertions)] 63 | Command::MakeMigrations { migrations_dir } => { 64 | use std::io::Write; 65 | 66 | const MODELS: &str = ".models.json"; 67 | 68 | let mut file = fs::File::create(MODELS)?; 69 | rorm::write_models(&mut file)?; 70 | file.flush()?; 71 | 72 | rorm::cli::make_migrations::run_make_migrations( 73 | rorm::cli::make_migrations::MakeMigrationsOptions { 74 | models_file: MODELS.to_string(), 75 | migration_dir: migrations_dir, 76 | name: None, 77 | non_interactive: false, 78 | warnings_disabled: false, 79 | }, 80 | )?; 81 | 82 | fs::remove_file(MODELS)?; 83 | } 84 | Command::Migrate { migrations_dir } => { 85 | rorm::cli::migrate::run_migrate_custom( 86 | DatabaseConfig { 87 | driver: db_driver, 88 | last_migration_table_name: None, 89 | }, 90 | migrations_dir, 91 | false, 92 | None, 93 | ) 94 | .await? 95 | } 96 | Command::Start => { 97 | info!("Starting server on http://localhost:8000"); 98 | 99 | axum::serve( 100 | TcpListener::bind(("localhost", 8000)) 101 | .await 102 | .context("Failed to bind to localhost:8000")?, 103 | handler::get_router() 104 | .with_state(db) 105 | .layer(SessionManagerLayer::new(MemoryStore::default()).with_secure(false)), 106 | ) 107 | .await? 108 | } 109 | Command::Test => test::run_test_client().await?, 110 | } 111 | 112 | Ok(()) 113 | } 114 | -------------------------------------------------------------------------------- /example-forum-server/src/main.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use example_forum_server::{run_main, Cli}; 3 | 4 | #[tokio::main] 5 | async fn main() -> anyhow::Result<()> { 6 | tracing_subscriber::fmt::init(); 7 | let cli = Cli::parse(); 8 | run_main(cli).await 9 | } 10 | -------------------------------------------------------------------------------- /example-forum-server/src/models/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod post; 2 | pub mod stars; 3 | pub mod thread; 4 | pub mod thumb; 5 | pub mod user; 6 | -------------------------------------------------------------------------------- /example-forum-server/src/models/post.rs: -------------------------------------------------------------------------------- 1 | use rorm::fields::types::MaxStr; 2 | use rorm::prelude::*; 3 | use time::OffsetDateTime; 4 | use uuid::Uuid; 5 | 6 | use crate::models::thread::Thread; 7 | use crate::models::user::User; 8 | 9 | #[derive(Model)] 10 | pub struct Post { 11 | /// An uuid identifying the post 12 | #[rorm(primary_key)] 13 | pub uuid: Uuid, 14 | 15 | /// The post's message 16 | pub message: MaxStr<1024>, 17 | 18 | /// The user how posted this post 19 | #[rorm(on_delete = "SetNull")] 20 | pub user: Option>, 21 | 22 | /// The thread this post was posted in 23 | #[rorm(on_delete = "Cascade")] 24 | pub thread: ForeignModel, 25 | 26 | /// The post this one is a reply to if it is a reply at all 27 | #[rorm(on_delete = "Cascade")] 28 | pub reply_to: Option>, 29 | 30 | /// When was this post posted? 31 | #[rorm(auto_create_time)] 32 | pub posted_at: OffsetDateTime, 33 | } 34 | 35 | #[derive(Patch)] 36 | #[rorm(model = "Post")] 37 | pub struct NewPost { 38 | pub uuid: Uuid, 39 | pub message: MaxStr<1024>, 40 | pub user: Option>, 41 | pub thread: ForeignModel, 42 | pub reply_to: Option>, 43 | } 44 | -------------------------------------------------------------------------------- /example-forum-server/src/models/stars.rs: -------------------------------------------------------------------------------- 1 | use rorm::conditions::Value; 2 | use rorm::db::sql::value::NullType; 3 | use rorm::fields::traits::{Array, FieldColumns, FieldType}; 4 | use rorm::fields::utils::check::shared_linter_check; 5 | use rorm::fields::utils::get_annotations::forward_annotations; 6 | use rorm::fields::utils::get_names::single_column_name; 7 | use rorm::prelude::ForeignModel; 8 | use rorm::{Model, Patch}; 9 | use serde::{Deserialize, Deserializer, Serialize}; 10 | 11 | use crate::models::post::Post; 12 | use crate::models::user::User; 13 | 14 | #[derive(Model)] 15 | pub struct Stars { 16 | /// Some internal id 17 | #[rorm(id)] 18 | pub id: i64, 19 | 20 | /// The number of stars given 21 | pub amount: StarsAmount, 22 | 23 | /// The user who gave the stars 24 | #[rorm(on_delete = "SetNull")] 25 | pub user: Option>, 26 | 27 | /// The post which received the stars 28 | #[rorm(on_delete = "Cascade")] 29 | pub post: ForeignModel, 30 | } 31 | 32 | #[derive(Patch)] 33 | #[rorm(model = "Stars")] 34 | pub struct NewStars { 35 | /// The number of stars given 36 | pub amount: StarsAmount, 37 | 38 | /// The user who gave the stars 39 | pub user: Option>, 40 | 41 | /// The post which received the stars 42 | pub post: ForeignModel, 43 | } 44 | 45 | /// Newtype to represent the number of stars a user gave a post 46 | /// 47 | /// It ranges from 0 to 5 (inclusive). 48 | #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize)] 49 | pub struct StarsAmount(i16); 50 | impl StarsAmount { 51 | pub fn new(value: i16) -> Option { 52 | (0..=5).contains(&value).then_some(Self(value)) 53 | } 54 | pub fn get(self) -> i16 { 55 | self.0 56 | } 57 | } 58 | impl<'de> Deserialize<'de> for StarsAmount { 59 | fn deserialize(deserializer: D) -> Result 60 | where 61 | D: Deserializer<'de>, 62 | { 63 | use serde::de::{Error, Unexpected}; 64 | i16::deserialize(deserializer).and_then(|value| { 65 | Self::new(value).ok_or(Error::invalid_value( 66 | Unexpected::Signed(value as i64), 67 | &"a number from 0 to 5", 68 | )) 69 | }) 70 | } 71 | } 72 | impl FieldType for StarsAmount { 73 | type Columns = Array<1>; 74 | 75 | const NULL: FieldColumns = [NullType::I16]; 76 | 77 | fn into_values<'a>(self) -> FieldColumns> { 78 | self.0.into_values() 79 | } 80 | 81 | fn as_values(&self) -> FieldColumns> { 82 | self.0.as_values() 83 | } 84 | 85 | type Decoder = StarsAmountDecoder; 86 | type GetNames = single_column_name; 87 | type GetAnnotations = forward_annotations<1>; 88 | type Check = shared_linter_check<1>; 89 | } 90 | rorm::new_converting_decoder! { 91 | pub StarsAmountDecoder, 92 | |value: i16| -> StarsAmount { 93 | StarsAmount::new(value).ok_or_else( 94 | || format!("Got invalid number of stars: {value}") 95 | ) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /example-forum-server/src/models/thread.rs: -------------------------------------------------------------------------------- 1 | use rorm::prelude::*; 2 | use time::OffsetDateTime; 3 | 4 | use crate::models::post::Post; 5 | 6 | #[derive(Model)] 7 | pub struct Thread { 8 | /// Let's use this normalized version of `name` as primary key 9 | #[rorm(primary_key, max_length = 255)] 10 | pub identifier: String, 11 | 12 | /// The thread's display name 13 | #[rorm(max_length = 255)] 14 | pub name: String, 15 | 16 | /// When was this thread opened? 17 | #[rorm(auto_create_time)] 18 | pub opened_at: OffsetDateTime, 19 | 20 | pub posts: BackRef, 21 | } 22 | 23 | #[derive(Patch)] 24 | #[rorm(model = "Thread")] 25 | pub struct NewThread { 26 | pub identifier: String, 27 | pub name: String, 28 | } 29 | -------------------------------------------------------------------------------- /example-forum-server/src/models/thumb.rs: -------------------------------------------------------------------------------- 1 | use rorm::prelude::*; 2 | 3 | use crate::models::post::Post; 4 | use crate::models::user::User; 5 | 6 | #[derive(Model)] 7 | pub struct Thumb { 8 | /// Some internal id 9 | #[rorm(id)] 10 | pub id: i64, 11 | 12 | /// Is the thumb pointing up? 13 | pub is_up: bool, 14 | 15 | /// The user who gave the thumb 16 | #[rorm(on_delete = "SetNull")] 17 | pub user: Option>, 18 | 19 | /// The post which received the thumb 20 | #[rorm(on_delete = "Cascade")] 21 | pub post: ForeignModel, 22 | } 23 | 24 | #[derive(Patch)] 25 | #[rorm(model = "Thumb")] 26 | pub struct NewThumb { 27 | pub is_up: bool, 28 | pub user: Option>, 29 | pub post: ForeignModel, 30 | } 31 | -------------------------------------------------------------------------------- /example-forum-server/src/models/user.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | use std::str::FromStr; 3 | 4 | use rorm::fields::types::MaxStr; 5 | use rorm::prelude::*; 6 | 7 | use crate::models::post::Post; 8 | 9 | #[derive(Model)] 10 | pub struct User { 11 | /// This is an auto-increment which should not be "leaked" to the user 12 | #[rorm(id)] 13 | pub id: i64, 14 | 15 | /// The user's unique identifier 16 | #[rorm(unique)] 17 | pub username: MaxStr<255>, 18 | 19 | /// Let's store passwords in plain text using a max length of 16. 20 | /// 21 | /// Everyone will love us for it <3 22 | #[rorm(max_length = 16)] 23 | pub password: String, 24 | 25 | /// What are the user's permissions and responsibilities? 26 | pub role: UserRole, 27 | 28 | pub posts: BackRef, 29 | } 30 | 31 | #[derive(Patch)] 32 | #[rorm(model = "User")] 33 | pub struct NewUser { 34 | pub username: MaxStr<255>, 35 | pub password: String, 36 | pub role: UserRole, 37 | } 38 | 39 | #[derive(DbEnum)] 40 | pub enum UserRole { 41 | User, 42 | Moderator, 43 | Admin, 44 | } 45 | impl fmt::Display for UserRole { 46 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 47 | f.write_str(match self { 48 | Self::User => "user", 49 | Self::Moderator => "moderator", 50 | Self::Admin => "admin", 51 | }) 52 | } 53 | } 54 | impl FromStr for UserRole { 55 | type Err = (); 56 | fn from_str(s: &str) -> Result { 57 | Ok(match s { 58 | "user" => Self::User, 59 | "moderator" => Self::Moderator, 60 | "admin" => Self::Admin, 61 | _ => return Err(()), 62 | }) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /example-forum-server/src/test.rs: -------------------------------------------------------------------------------- 1 | use rorm::fields::types::MaxStr; 2 | use tracing::info; 3 | 4 | use crate::handler::thread::{CreateThreadRequest, ListResponse, MakePostRequest}; 5 | use crate::handler::user::{LoginRequest, ProfileResponse, RegisterRequest}; 6 | 7 | const ORIGIN: &str = "http://localhost:8000"; 8 | const USERNAME: &str = "alice"; 9 | const PASSWORD: &str = "password"; 10 | const THREAD_NAME: &str = "Cats"; 11 | 12 | pub async fn run_test_client() -> anyhow::Result<()> { 13 | let client = reqwest::Client::builder().cookie_store(true).build()?; 14 | 15 | let response = client 16 | .post(format!("{ORIGIN}/api/user/login")) 17 | .json(&LoginRequest { 18 | username: USERNAME.to_string(), 19 | password: PASSWORD.to_string(), 20 | }) 21 | .send() 22 | .await?; 23 | assert!(!response.status().is_server_error()); 24 | if response.status().is_client_error() { 25 | client 26 | .post(format!("{ORIGIN}/api/user/register")) 27 | .json(&RegisterRequest { 28 | username: MaxStr::new(USERNAME.to_string()).unwrap(), 29 | password: PASSWORD.to_string(), 30 | }) 31 | .send() 32 | .await?; 33 | 34 | info!("Created new user account"); 35 | } else { 36 | info!("Logged into existing user account") 37 | } 38 | 39 | let ProfileResponse { 40 | username, 41 | role, 42 | posts: _, 43 | } = client 44 | .get(format!("{ORIGIN}/api/user/profile/{USERNAME}")) 45 | .send() 46 | .await? 47 | .error_for_status()? 48 | .json() 49 | .await?; 50 | assert_eq!(username, USERNAME); 51 | assert_eq!(role, "user"); 52 | 53 | let ListResponse { threads } = client 54 | .get(format!("{ORIGIN}/api/thread/list")) 55 | .send() 56 | .await? 57 | .error_for_status()? 58 | .json() 59 | .await?; 60 | 61 | let thread_id = if let Some(x) = threads.iter().find(|thread| thread.name == THREAD_NAME) { 62 | info!("Found existing thread"); 63 | x.identifier.clone() 64 | } else { 65 | let id = client 66 | .post(format!("{ORIGIN}/api/thread/create")) 67 | .json(&CreateThreadRequest { 68 | name: THREAD_NAME.to_string(), 69 | }) 70 | .send() 71 | .await? 72 | .error_for_status()? 73 | .json::() 74 | .await?; 75 | 76 | info!("Created new thread"); 77 | id 78 | }; 79 | 80 | client 81 | .post(format!("{ORIGIN}/api/thread/posts/{thread_id}")) 82 | .json(&MakePostRequest { 83 | message: "Look at this cute cat picture I found".to_string(), 84 | reply_to: None, 85 | }) 86 | .send() 87 | .await? 88 | .error_for_status()?; 89 | 90 | info!("Submitted a new post"); 91 | 92 | Ok(()) 93 | } 94 | -------------------------------------------------------------------------------- /example-forum-server/tests/example_forum_server.rs: -------------------------------------------------------------------------------- 1 | use std::env::temp_dir; 2 | use std::fs; 3 | use std::future::{poll_fn, Future}; 4 | use std::hash::{BuildHasher, Hasher, RandomState}; 5 | use std::pin::pin; 6 | use std::task::Poll; 7 | use std::time::Duration; 8 | 9 | use example_forum_server::{run_main, Cli, Command}; 10 | use serde_json::json; 11 | use tokio::time::{sleep, timeout}; 12 | 13 | #[tokio::test] 14 | async fn test_example_forum_server() { 15 | let working_dir = temp_dir().join(format!( 16 | "test-rorm-example-forum-server-{}", 17 | RandomState::new().build_hasher().finish() 18 | )); 19 | fs::create_dir(&working_dir).unwrap(); 20 | 21 | let db_sqlite = working_dir.join("db.sqlite").display().to_string(); 22 | let db_config = working_dir.join("db_config.json").display().to_string(); 23 | serde_json::to_writer( 24 | fs::File::create(&db_config).unwrap(), 25 | &json!({ 26 | "Driver": "SQLite", 27 | "Filename": db_sqlite, 28 | }), 29 | ) 30 | .unwrap(); 31 | 32 | let migrations_dir = working_dir.join("migrations").display().to_string(); 33 | fs::create_dir(&migrations_dir).unwrap(); 34 | run_main(Cli { 35 | db_config: Some(db_config.clone()), 36 | command: Command::MakeMigrations { 37 | migrations_dir: migrations_dir.clone(), 38 | }, 39 | }) 40 | .await 41 | .unwrap(); 42 | run_main(Cli { 43 | db_config: Some(db_config.clone()), 44 | command: Command::Migrate { 45 | migrations_dir: migrations_dir.clone(), 46 | }, 47 | }) 48 | .await 49 | .unwrap(); 50 | 51 | let mut server_future = pin!(run_main(Cli { 52 | db_config: Some(db_config.clone()), 53 | command: Command::Start {}, 54 | })); 55 | let mut client_future = pin!(async move { 56 | sleep(Duration::from_millis(500)).await; 57 | run_main(Cli { 58 | db_config: Some(db_config.clone()), 59 | command: Command::Test {}, 60 | }) 61 | .await?; 62 | run_main(Cli { 63 | db_config: Some(db_config.clone()), 64 | command: Command::Test {}, 65 | }) 66 | .await 67 | }); 68 | 69 | timeout( 70 | Duration::from_secs(10), 71 | poll_fn(|ctx| { 72 | match ( 73 | client_future.as_mut().poll(&mut *ctx), 74 | server_future.as_mut().poll(&mut *ctx), 75 | ) { 76 | (Poll::Pending, Poll::Pending) => Poll::Pending, 77 | (Poll::Ready(client_result), _) => Poll::Ready(client_result), 78 | (Poll::Pending, Poll::Ready(Err(server_error))) => Poll::Ready(Err(server_error)), 79 | (Poll::Pending, Poll::Ready(Ok(()))) => panic!("Server should not shut down"), 80 | } 81 | }), 82 | ) 83 | .await 84 | .unwrap() 85 | .unwrap(); 86 | 87 | fs::remove_dir_all(&working_dir).unwrap(); 88 | } 89 | -------------------------------------------------------------------------------- /rorm-macro-impl/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rorm-macro-impl" 3 | version = "0.9.0" 4 | edition = "2021" 5 | repository = "https://github.com/rorm-orm/rorm" 6 | authors = ["gammelalf", "myOmikron "] 7 | categories = ["database"] 8 | keywords = ["declarative", "orm", "database", "macros"] 9 | homepage = "https://rorm.rs" 10 | documentation = "https://docs.rorm.rs" 11 | license = "MIT" 12 | description = "Macro implementations for rorm." 13 | 14 | [dependencies] 15 | # syn builds rust syntax trees from strings or tokenstream 16 | syn = { version = "~2", features = ["full", "visit-mut"] } 17 | # quote provides a macro to write rust code with template variables which then produces a tokenstream 18 | quote = { version = "~1" } 19 | # a higher level wrapper for rust's proc-macro which is used by syn and quote 20 | proc-macro2 = { version = "~1" } 21 | # for simple parsing of attributes 22 | darling = { version = "~0.20" } 23 | -------------------------------------------------------------------------------- /rorm-macro-impl/src/analyze/mod.rs: -------------------------------------------------------------------------------- 1 | use quote::ToTokens; 2 | use syn::{VisRestricted, Visibility}; 3 | 4 | pub mod model; 5 | 6 | pub fn vis_to_display(vis: &Visibility) -> impl std::fmt::Display + '_ { 7 | DisplayableVisibility(vis) 8 | } 9 | struct DisplayableVisibility<'a>(&'a Visibility); 10 | impl std::fmt::Display for DisplayableVisibility<'_> { 11 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 12 | match self.0 { 13 | Visibility::Public(_) => f.write_str("pub "), 14 | Visibility::Restricted(VisRestricted { 15 | pub_token: _, 16 | paren_token: _, 17 | in_token, 18 | path, 19 | }) => { 20 | write!( 21 | f, "pub({in}{path}) ", 22 | in = if in_token.is_some() { "in " } else { "" }, 23 | path = path.to_token_stream()) 24 | } 25 | Visibility::Inherited => Ok(()), 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /rorm-macro-impl/src/analyze/model.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::Ident; 2 | use quote::format_ident; 3 | use syn::visit_mut::VisitMut; 4 | use syn::{Generics, LitInt, LitStr, Type, Visibility}; 5 | 6 | use crate::analyze::vis_to_display; 7 | use crate::parse::annotations::{Default, Index, OnAction}; 8 | use crate::parse::model::{ModelAnnotations, ModelFieldAnnotations, ParsedField, ParsedModel}; 9 | use crate::utils::to_db_name; 10 | 11 | pub fn analyze_model(parsed: ParsedModel) -> darling::Result { 12 | let ParsedModel { 13 | vis, 14 | ident, 15 | generics, 16 | annos: 17 | ModelAnnotations { 18 | rename, 19 | experimental_unregistered, 20 | experimental_generics, 21 | }, 22 | fields, 23 | } = parsed; 24 | let mut errors = darling::Error::accumulator(); 25 | 26 | if experimental_generics && !experimental_unregistered { 27 | errors.push(darling::Error::custom( 28 | "`experimental_generics` requires `experimental_unregistered`", 29 | )); 30 | } 31 | if generics.lt_token.is_some() && !experimental_generics { 32 | errors.push(darling::Error::custom("Generic models are not supported yet. You can try the `experimental_generics` attribute")); 33 | } 34 | 35 | // Get table name 36 | let table = rename.unwrap_or_else(|| LitStr::new(&to_db_name(ident.to_string()), ident.span())); 37 | if table.value().contains("__") { 38 | errors.push(darling::Error::custom("Table names can't contain a double underscore. If you need to name your model like this, consider using `#[rorm(rename = \"...\")]`.").with_span(&table)); 39 | } 40 | 41 | // Analyze fields 42 | let mut analyzed_fields = Vec::with_capacity( 43 | /* assuming most fields won't be ignored */ 44 | fields.len(), 45 | ); 46 | let model_ident = &ident; // alias to avoid shadowing in following loop 47 | for field in fields { 48 | let ParsedField { 49 | vis, 50 | ident, 51 | mut ty, 52 | annos: 53 | ModelFieldAnnotations { 54 | auto_create_time, 55 | auto_update_time, 56 | mut auto_increment, 57 | mut primary_key, 58 | unique, 59 | id, 60 | on_delete, 61 | on_update, 62 | rename, 63 | //ignore, 64 | default, 65 | max_length, 66 | index, 67 | }, 68 | } = field; 69 | // Get column name 70 | let column = 71 | rename.unwrap_or_else(|| LitStr::new(&to_db_name(ident.to_string()), ident.span())); 72 | if column.value().contains("__") { 73 | errors.push(darling::Error::custom("Column names can't contain a double underscore. If you need to name your field like this, consider using `#[rorm(rename = \"...\")]`.").with_span(&column)); 74 | } 75 | if column.value().len() > 63 { 76 | errors.push(darling::Error::custom("Column names can't be larger than 63 bytes. If you need to name your field like this, consider using `#[rorm(rename = \"...\")]`.").with_span(&column)); 77 | } 78 | 79 | // Handle #[rorm(id)] annotation 80 | if id { 81 | if primary_key { 82 | errors.push( 83 | darling::Error::custom( 84 | "`#[rorm(primary_key)]` is implied by `#[rorm(id)]`. Please remove one of them.", 85 | ) 86 | .with_span(&ident), 87 | ); 88 | } 89 | if auto_increment { 90 | errors.push( 91 | darling::Error::custom( 92 | "`#[rorm(auto_increment)]` is implied by `#[rorm(id)]`. Please remove one of them.", 93 | ) 94 | .with_span(&ident), 95 | ); 96 | } 97 | primary_key = true; 98 | auto_increment = true; 99 | } 100 | 101 | // Replace `Self` in the field's type to the model's identifier 102 | struct ReplaceSelf<'a>(&'a Ident); 103 | impl VisitMut for ReplaceSelf<'_> { 104 | fn visit_ident_mut(&mut self, i: &mut Ident) { 105 | if i == "Self" { 106 | *i = self.0.clone(); 107 | } 108 | } 109 | } 110 | ReplaceSelf(model_ident).visit_type_mut(&mut ty); 111 | 112 | analyzed_fields.push(AnalyzedField { 113 | vis, 114 | unit: format_ident!("__{}_{}", model_ident, ident), 115 | ident, 116 | column, 117 | ty, 118 | annos: AnalyzedModelFieldAnnotations { 119 | auto_create_time, 120 | auto_update_time, 121 | auto_increment, 122 | primary_key, 123 | unique, 124 | on_delete, 125 | on_update, 126 | default, 127 | max_length, 128 | index, 129 | }, 130 | }); 131 | } 132 | 133 | // Find the unique primary key 134 | let mut primary_keys = Vec::with_capacity(1); // Should be exactly one 135 | for (index, field) in analyzed_fields.iter().enumerate() { 136 | if field.annos.primary_key { 137 | primary_keys.push((index, field)); 138 | } 139 | } 140 | let mut primary_key = usize::MAX; // will only be returned if it is set properly 141 | match primary_keys.as_slice() { 142 | [(index, _)] => primary_key = *index, 143 | [] => errors.push( 144 | darling::Error::custom(format!( 145 | "Model misses a primary key. Try adding the default one:\n\n#[rorm(id)]\n{vis}id: i64,", vis = vis_to_display(&vis), 146 | )) 147 | .with_span(&ident), 148 | ), 149 | _ => errors.push(darling::Error::multiple( 150 | primary_keys 151 | .into_iter() 152 | .map(|(_, field)| { 153 | darling::Error::custom("Model has more than one primary key. Please remove all but one of them.") 154 | .with_span(&field.ident) 155 | }) 156 | .collect(), 157 | )), 158 | } 159 | 160 | errors.finish_with(AnalyzedModel { 161 | vis: vis.clone(), 162 | ident, 163 | table, 164 | fields: analyzed_fields, 165 | primary_key, 166 | experimental_unregistered, 167 | experimental_generics: generics, 168 | }) 169 | } 170 | 171 | pub struct AnalyzedModel { 172 | pub vis: Visibility, 173 | pub ident: Ident, 174 | pub table: LitStr, 175 | pub fields: Vec, 176 | /// the primary key's index 177 | pub primary_key: usize, 178 | 179 | pub experimental_unregistered: bool, 180 | pub experimental_generics: Generics, 181 | } 182 | 183 | pub struct AnalyzedField { 184 | pub vis: Visibility, 185 | pub ident: Ident, 186 | pub column: LitStr, 187 | pub unit: Ident, 188 | pub ty: Type, 189 | pub annos: AnalyzedModelFieldAnnotations, 190 | } 191 | 192 | pub struct AnalyzedModelFieldAnnotations { 193 | pub auto_create_time: bool, 194 | pub auto_update_time: bool, 195 | pub auto_increment: bool, 196 | pub primary_key: bool, 197 | pub unique: bool, 198 | pub on_delete: Option, 199 | pub on_update: Option, 200 | pub default: Option, 201 | pub max_length: Option, 202 | pub index: Option, 203 | } 204 | -------------------------------------------------------------------------------- /rorm-macro-impl/src/generate/db_enum.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | use quote::{format_ident, quote}; 3 | 4 | use crate::parse::db_enum::ParsedDbEnum; 5 | use crate::MacroConfig; 6 | 7 | pub fn generate_db_enum(parsed: &ParsedDbEnum, config: &MacroConfig) -> TokenStream { 8 | let MacroConfig { 9 | rorm_path, 10 | non_exhaustive: _, 11 | } = config; 12 | 13 | let ParsedDbEnum { 14 | vis, 15 | ident, 16 | variants, 17 | } = parsed; 18 | let decoder = format_ident!("__{ident}_Decoder"); 19 | 20 | quote! { 21 | const _: () = { 22 | const CHOICES: &'static [&'static str] = &[ 23 | #(stringify!(#variants)),* 24 | ]; 25 | 26 | impl #rorm_path::fields::traits::FieldType for #ident { 27 | type Columns = #rorm_path::fields::traits::Array<1>; 28 | 29 | const NULL: #rorm_path::fields::traits::FieldColumns = [ 30 | #rorm_path::db::sql::value::NullType::Choice 31 | ]; 32 | 33 | fn into_values<'a>(self) -> #rorm_path::fields::traits::FieldColumns> { 34 | [#rorm_path::conditions::Value::Choice(::std::borrow::Cow::Borrowed(match self { 35 | #( 36 | Self::#variants => stringify!(#variants), 37 | )* 38 | }))] 39 | } 40 | 41 | fn as_values(&self) -> #rorm_path::fields::traits::FieldColumns> { 42 | [#rorm_path::conditions::Value::Choice(::std::borrow::Cow::Borrowed(match self { 43 | #( 44 | Self::#variants => stringify!(#variants), 45 | )* 46 | }))] 47 | } 48 | 49 | type Decoder = #decoder; 50 | 51 | type GetAnnotations = get_db_enum_annotations; 52 | 53 | type Check = #rorm_path::fields::utils::check::shared_linter_check<1>; 54 | 55 | type GetNames = #rorm_path::fields::utils::get_names::single_column_name; 56 | } 57 | #rorm_path::new_converting_decoder!( 58 | #[doc(hidden)] 59 | #vis #decoder, 60 | |value: #rorm_path::db::choice::Choice| -> #ident { 61 | let value: String = value.0; 62 | match value.as_str() { 63 | #( 64 | stringify!(#variants) => Ok(#ident::#variants), 65 | )* 66 | _ => Err(format!("Invalid value '{}' for enum '{}'", value, stringify!(#ident))), 67 | } 68 | } 69 | ); 70 | #rorm_path::impl_FieldEq!(impl<'rhs> FieldEq<'rhs, #ident> for #ident { 71 | |value: #ident| { let [value] = <#ident as #rorm_path::fields::traits::FieldType>::into_values(value); value } 72 | }); 73 | 74 | #rorm_path::const_fn! { 75 | pub fn get_db_enum_annotations( 76 | field: #rorm_path::internal::hmr::annotations::Annotations 77 | ) -> [#rorm_path::internal::hmr::annotations::Annotations; 1] { 78 | let mut field = field; 79 | field.choices = Some(#rorm_path::internal::hmr::annotations::Choices(CHOICES)); 80 | [field] 81 | } 82 | } 83 | }; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /rorm-macro-impl/src/generate/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod db_enum; 2 | pub mod model; 3 | pub mod patch; 4 | mod utils; 5 | -------------------------------------------------------------------------------- /rorm-macro-impl/src/generate/patch.rs: -------------------------------------------------------------------------------- 1 | use std::array; 2 | 3 | use proc_macro2::{Ident, TokenStream}; 4 | use quote::{format_ident, quote, ToTokens}; 5 | use syn::{Generics, Type, Visibility}; 6 | 7 | use crate::parse::patch::ParsedPatch; 8 | use crate::MacroConfig; 9 | 10 | pub fn generate_patch(patch: &ParsedPatch, config: &MacroConfig) -> TokenStream { 11 | let MacroConfig { 12 | rorm_path, 13 | non_exhaustive: _, 14 | } = config; 15 | 16 | let ParsedPatch { 17 | vis, 18 | ident, 19 | model, 20 | fields, 21 | } = patch; 22 | 23 | let field_idents_1 = fields.iter().map(|field| &field.ident); 24 | let field_idents_2 = field_idents_1.clone(); 25 | let field_types = fields.iter().map(|field| &field.ty); 26 | 27 | let partial = partially_generate_patch( 28 | ident, 29 | model, 30 | vis, 31 | &Default::default(), 32 | field_idents_1.clone(), 33 | fields.iter().map(|field| &field.ty), 34 | config, 35 | ); 36 | 37 | quote! { 38 | #partial 39 | 40 | #( 41 | impl #rorm_path::model::GetField<#rorm_path::get_field!(#ident, #field_idents_2)> for #ident { 42 | fn get_field(self) -> #field_types { 43 | self.#field_idents_2 44 | } 45 | fn borrow_field(&self) -> &#field_types { 46 | &self.#field_idents_2 47 | } 48 | fn borrow_field_mut(&mut self) -> &mut #field_types { 49 | &mut self.#field_idents_2 50 | } 51 | } 52 | )* 53 | } 54 | } 55 | 56 | pub fn partially_generate_patch<'a>( 57 | patch: &Ident, 58 | model: &impl ToTokens, // Ident or Path 59 | vis: &Visibility, 60 | generics: &Generics, 61 | fields: impl Iterator + Clone, 62 | types: impl Iterator + Clone, 63 | config: &MacroConfig, 64 | ) -> TokenStream { 65 | let MacroConfig { 66 | rorm_path, 67 | non_exhaustive: _, 68 | } = config; 69 | 70 | let value_space_impl = format_ident!("__{patch}_ValueSpaceImpl"); 71 | let value_space_marker_impl = format_ident!("__{patch}_ValueSpaceImplMarker"); 72 | 73 | let decoder = format_ident!("__{patch}_Decoder"); 74 | 75 | let [fields_1, fields_2, fields_3, fields_4, fields_5, fields_6, fields_7] = 76 | array::from_fn(|_| fields.clone()); 77 | let (impl_generics, type_generics, where_clause) = generics.split_for_impl(); 78 | let lifetime_generics = { 79 | let mut tokens = impl_generics 80 | .to_token_stream() 81 | .into_iter() 82 | .collect::>(); 83 | if tokens.is_empty() { 84 | quote! {<'a>} 85 | } else { 86 | tokens.remove(0); 87 | tokens.pop(); 88 | quote! {<'a, #(#tokens)*>} 89 | } 90 | }; 91 | quote! { 92 | // Credit and explanation: https://github.com/dtolnay/case-studies/tree/master/unit-type-parameters 93 | #[doc(hidden)] 94 | #[allow(non_camel_case_types)] 95 | #vis enum #value_space_impl #impl_generics #where_clause { 96 | #patch, 97 | 98 | #[allow(dead_code)] 99 | #[doc(hidden)] 100 | #value_space_marker_impl(::std::marker::PhantomData<#patch #type_generics>), 101 | } 102 | #vis use #value_space_impl::*; 103 | 104 | #[doc(hidden)] 105 | #vis struct #decoder #impl_generics #where_clause { 106 | #( 107 | #fields_1: <#types as #rorm_path::fields::traits::FieldType>::Decoder, 108 | )* 109 | } 110 | 111 | impl #impl_generics #rorm_path::crud::selector::Selector for #value_space_impl #type_generics #where_clause { 112 | type Result = #patch #type_generics; 113 | type Model = #model #type_generics; 114 | type Decoder = #decoder #type_generics; 115 | const INSERT_COMPATIBLE: bool = true; 116 | fn select(self, ctx: &mut #rorm_path::internal::query_context::QueryContext) -> Self::Decoder { 117 | #decoder {#( 118 | #fields_4: <#model #type_generics as #rorm_path::model::Model>::FIELDS.#fields_4.select(&mut *ctx), 119 | )*} 120 | } 121 | } 122 | 123 | impl #impl_generics ::std::default::Default for #value_space_impl #type_generics #where_clause { 124 | fn default() -> Self { 125 | Self::#patch 126 | } 127 | } 128 | 129 | impl #impl_generics #rorm_path::crud::decoder::Decoder for #decoder #type_generics #where_clause { 130 | type Result = #patch #type_generics; 131 | 132 | fn by_name<'index>(&'index self, row: &'_ #rorm_path::db::Row) -> Result> { 133 | Ok(#patch {#( 134 | #fields_2: self.#fields_2.by_name(row)?, 135 | )*}) 136 | } 137 | 138 | fn by_index<'index>(&'index self, row: &'_ #rorm_path::db::Row) -> Result> { 139 | Ok(#patch {#( 140 | #fields_3: self.#fields_3.by_index(row)?, 141 | )*}) 142 | } 143 | } 144 | 145 | impl #impl_generics #rorm_path::model::Patch for #patch #type_generics #where_clause { 146 | type Model = #model #type_generics; 147 | 148 | type ValueSpaceImpl = #value_space_impl #type_generics; 149 | 150 | fn push_columns(columns: &mut Vec<#rorm_path::fields::utils::column_name::ColumnName>) {#( 151 | columns.extend( 152 | #rorm_path::fields::proxy::columns(|| <::Model as #rorm_path::model::Model>::FIELDS.#fields_5) 153 | ); 154 | )*} 155 | 156 | fn push_references<'a>(&'a self, values: &mut Vec<#rorm_path::conditions::Value<'a>>) { 157 | #( 158 | values.extend(#rorm_path::fields::traits::FieldType::as_values(&self.#fields_6)); 159 | )* 160 | } 161 | 162 | fn push_values(self, values: &mut Vec<#rorm_path::conditions::Value>) { 163 | #( 164 | values.extend(#rorm_path::fields::traits::FieldType::into_values(self.#fields_7)); 165 | )* 166 | } 167 | } 168 | 169 | impl #lifetime_generics #rorm_path::internal::patch::IntoPatchCow<'a> for #patch #type_generics #where_clause { 170 | type Patch = #patch #type_generics; 171 | 172 | fn into_patch_cow(self) -> #rorm_path::internal::patch::PatchCow<'a, #patch #type_generics> { 173 | #rorm_path::internal::patch::PatchCow::Owned(self) 174 | } 175 | } 176 | impl #lifetime_generics #rorm_path::internal::patch::IntoPatchCow<'a> for &'a #patch #type_generics #where_clause { 177 | type Patch = #patch #type_generics; 178 | 179 | fn into_patch_cow(self) -> #rorm_path::internal::patch::PatchCow<'a, #patch #type_generics> { 180 | #rorm_path::internal::patch::PatchCow::Borrowed(self) 181 | } 182 | } 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /rorm-macro-impl/src/generate/utils.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::{Span, TokenStream}; 2 | use quote::{quote, quote_spanned}; 3 | use syn::Generics; 4 | 5 | use crate::MacroConfig; 6 | 7 | /// Creates a ZST which captures all generics 8 | /// 9 | /// I.e. `PhantomData<(&'a (), T)>` 10 | pub fn phantom_data(generics: &Generics) -> TokenStream { 11 | let mut tokens = TokenStream::new(); 12 | tokens.extend(generics.lifetimes().map(|lifetime| { 13 | let lifetime = &lifetime.lifetime; 14 | quote! { & #lifetime (), } 15 | })); 16 | tokens.extend(generics.type_params().map(|parameter| { 17 | let parameter = ¶meter.ident; 18 | quote! { #parameter, } 19 | })); 20 | quote! { 21 | ::std::marker::PhantomData<( #tokens )> 22 | } 23 | } 24 | 25 | /// Creates an expression for a `Source` instance from a span 26 | pub fn get_source(span: Span, config: &MacroConfig) -> TokenStream { 27 | let MacroConfig { 28 | rorm_path, 29 | non_exhaustive: _, 30 | } = config; 31 | quote_spanned! {span=> 32 | #rorm_path::internal::hmr::Source { 33 | file: ::std::file!(), 34 | line: ::std::line!() as usize, 35 | column: ::std::column!() as usize, 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /rorm-macro-impl/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! This crate tries to follow the base layout proposed by a [ferrous-systems.com](https://ferrous-systems.com/blog/testing-proc-macros/#the-pipeline) blog post. 2 | 3 | use proc_macro2::TokenStream; 4 | use quote::quote; 5 | 6 | use crate::analyze::model::analyze_model; 7 | use crate::generate::db_enum::generate_db_enum; 8 | use crate::generate::model::generate_model; 9 | use crate::generate::patch::generate_patch; 10 | use crate::parse::db_enum::parse_db_enum; 11 | use crate::parse::model::parse_model; 12 | use crate::parse::patch::parse_patch; 13 | 14 | mod analyze; 15 | mod generate; 16 | mod parse; 17 | mod utils; 18 | 19 | /// Implementation of `rorm`'s `#[derive(DbEnum)]` macro 20 | pub fn derive_db_enum(input: TokenStream, config: MacroConfig) -> TokenStream { 21 | match parse_db_enum(input) { 22 | Ok(model) => generate_db_enum(&model, &config), 23 | Err(error) => error.write_errors(), 24 | } 25 | } 26 | 27 | /// Implementation of `rorm`'s `#[derive(Model)]` macro 28 | pub fn derive_model(input: TokenStream, config: MacroConfig) -> TokenStream { 29 | match parse_model(input).and_then(analyze_model) { 30 | Ok(model) => generate_model(&model, &config), 31 | Err(error) => error.write_errors(), 32 | } 33 | } 34 | 35 | /// Implementation of `rorm`'s `#[derive(Patch)]` macro 36 | pub fn derive_patch(input: TokenStream, config: MacroConfig) -> TokenStream { 37 | match parse_patch(input) { 38 | Ok(patch) => generate_patch(&patch, &config), 39 | Err(error) => error.write_errors(), 40 | } 41 | } 42 | 43 | /// Configuration for `rorm`'s macros 44 | /// 45 | /// This struct can be useful for other crates wrapping `rorm`'s macros to tweak their behaviour. 46 | #[cfg_attr(doc, non_exhaustive)] 47 | pub struct MacroConfig { 48 | /// Path to the `rorm` crate 49 | /// 50 | /// This path can be overwritten by another library which wraps and re-exports `rorm`. 51 | /// 52 | /// Defaults to `::rorm` which requires `rorm` to be a direct dependency 53 | /// of any crate using `rorm`'s macros. 54 | pub rorm_path: TokenStream, 55 | 56 | #[cfg(not(doc))] 57 | pub non_exhaustive: private::NonExhaustive, 58 | } 59 | 60 | mod private { 61 | pub struct NonExhaustive; 62 | } 63 | 64 | impl Default for MacroConfig { 65 | fn default() -> Self { 66 | Self { 67 | rorm_path: quote! { ::rorm }, 68 | non_exhaustive: private::NonExhaustive, 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /rorm-macro-impl/src/parse/annotations.rs: -------------------------------------------------------------------------------- 1 | use darling::ast::NestedMeta; 2 | use darling::{Error, FromAttributes, FromMeta}; 3 | use proc_macro2::Ident; 4 | use syn::{Lit, LitInt, LitStr}; 5 | 6 | #[derive(FromAttributes, Debug)] 7 | #[darling(attributes(rorm))] 8 | pub struct NoAnnotations; 9 | 10 | #[derive(Debug)] 11 | pub struct Default { 12 | pub variant: &'static str, 13 | pub literal: Lit, 14 | } 15 | impl FromMeta for Default { 16 | fn from_value(value: &Lit) -> darling::Result { 17 | Ok(Default { 18 | variant: match value { 19 | Lit::Str(_) => Ok("String"), 20 | Lit::Int(_) => Ok("Integer"), 21 | Lit::Float(_) => Ok("Float"), 22 | Lit::Bool(_) => Ok("Boolean"), 23 | _ => Err(Error::unexpected_lit_type(value)), 24 | }?, 25 | literal: value.clone(), 26 | }) 27 | } 28 | } 29 | 30 | #[derive(Debug)] 31 | pub struct OnAction(pub Ident); 32 | impl FromMeta for OnAction { 33 | fn from_value(lit: &Lit) -> darling::Result { 34 | static OPTIONS: [&str; 4] = ["Restrict", "Cascade", "SetNull", "SetDefault"]; 35 | (match lit { 36 | Lit::Str(string) => { 37 | let string = string.value(); 38 | let value = string.as_str(); 39 | if OPTIONS.contains(&value) { 40 | Ok(OnAction(Ident::new(value, lit.span()))) 41 | } else { 42 | Err(Error::unknown_field_with_alts(value, &OPTIONS)) 43 | } 44 | } 45 | _ => Err(Error::unexpected_lit_type(lit)), 46 | }) 47 | .map_err(|e| e.with_span(lit)) 48 | } 49 | } 50 | 51 | #[derive(Default, Debug)] 52 | pub struct Index(pub Option); 53 | impl FromMeta for Index { 54 | fn from_word() -> darling::Result { 55 | Ok(Index(None)) 56 | } 57 | 58 | fn from_list(items: &[NestedMeta]) -> darling::Result { 59 | if items.is_empty() { 60 | Ok(Index(None)) 61 | } else { 62 | Ok(Index(Some(NamedIndex::from_list(items)?))) 63 | } 64 | } 65 | } 66 | 67 | #[derive(FromMeta, Debug)] 68 | pub struct NamedIndex { 69 | pub name: LitStr, 70 | pub priority: Option, 71 | } 72 | -------------------------------------------------------------------------------- /rorm-macro-impl/src/parse/db_enum.rs: -------------------------------------------------------------------------------- 1 | use darling::FromAttributes; 2 | use proc_macro2::{Ident, TokenStream}; 3 | use syn::{ItemEnum, Variant, Visibility}; 4 | 5 | use crate::parse::annotations::NoAnnotations; 6 | 7 | pub fn parse_db_enum(tokens: TokenStream) -> darling::Result { 8 | let ItemEnum { 9 | attrs, 10 | vis, 11 | enum_token: _, 12 | ident, 13 | generics, 14 | brace_token: _, 15 | variants, 16 | } = syn::parse2(tokens)?; 17 | let mut errors = darling::Error::accumulator(); 18 | 19 | // check absence of #[rorm(..)] attributes 20 | let _ = errors.handle(NoAnnotations::from_attributes(&attrs)); 21 | 22 | // check absence of generics 23 | if generics.lt_token.is_some() { 24 | errors.push(darling::Error::unsupported_shape_with_expected( 25 | "generic struct", 26 | &"struct without generics", 27 | )) 28 | } 29 | 30 | // parse variants 31 | let mut parsed_variants = Vec::with_capacity(variants.len()); 32 | for variant in variants { 33 | let Variant { 34 | attrs, 35 | ident, 36 | fields, 37 | discriminant: _, // TODO maybe warn, that they aren't used? 38 | } = variant; 39 | 40 | // check absence of #[rorm(..)] attributes 41 | let _ = errors.handle(NoAnnotations::from_attributes(&attrs)); 42 | 43 | // check absence of fields 44 | if !fields.is_empty() { 45 | errors.push( 46 | darling::Error::unsupported_shape("A DbEnum's variants can't contain fields") 47 | .with_span(&fields), 48 | ); 49 | } 50 | 51 | parsed_variants.push(ident); 52 | } 53 | 54 | errors.finish_with(ParsedDbEnum { 55 | vis, 56 | ident, 57 | variants: parsed_variants, 58 | }) 59 | } 60 | 61 | pub struct ParsedDbEnum { 62 | pub vis: Visibility, 63 | pub ident: Ident, 64 | pub variants: Vec, 65 | } 66 | -------------------------------------------------------------------------------- /rorm-macro-impl/src/parse/mod.rs: -------------------------------------------------------------------------------- 1 | use syn::{Fields, FieldsNamed, Generics}; 2 | 3 | pub mod annotations; 4 | pub mod db_enum; 5 | pub mod model; 6 | pub mod patch; 7 | 8 | /// Get the [`Fields::Named(..)`](Fields::Named) variant's data or produce an error 9 | pub fn get_fields_named(fields: Fields) -> darling::Result { 10 | match fields { 11 | Fields::Named(fields) => Ok(fields), 12 | Fields::Unnamed(_) => Err(darling::Error::unsupported_shape_with_expected( 13 | "named tuple", 14 | &"struct with named fields", 15 | ) 16 | .with_span(&fields)), 17 | Fields::Unit => Err(darling::Error::unsupported_shape_with_expected( 18 | "unit struct", 19 | &"struct with named fields", 20 | ) 21 | .with_span(&fields)), 22 | } 23 | } 24 | 25 | /// Check a struct or enum to don't have [`Generics`] 26 | pub fn check_non_generic(generics: Generics) -> darling::Result<()> { 27 | if generics.lt_token.is_none() { 28 | Ok(()) 29 | } else { 30 | Err(darling::Error::unsupported_shape_with_expected( 31 | "generic struct", 32 | &"struct without generics", 33 | ) 34 | .with_span(&generics)) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /rorm-macro-impl/src/parse/model.rs: -------------------------------------------------------------------------------- 1 | use darling::FromAttributes; 2 | use proc_macro2::{Ident, TokenStream}; 3 | use syn::{parse2, Field, Generics, ItemStruct, LitInt, LitStr, Type, Visibility}; 4 | 5 | use crate::parse::annotations::{Default, Index, OnAction}; 6 | use crate::parse::get_fields_named; 7 | 8 | pub fn parse_model(tokens: TokenStream) -> darling::Result { 9 | let ItemStruct { 10 | struct_token: _, 11 | generics, 12 | fields, 13 | ident, 14 | vis, 15 | attrs, 16 | semi_token: _, 17 | } = parse2(tokens)?; 18 | let mut errors = darling::Error::accumulator(); 19 | 20 | // parse struct annotations 21 | let annos = errors 22 | .handle(ModelAnnotations::from_attributes(&attrs)) 23 | .unwrap_or_default(); 24 | 25 | // parse field annotations 26 | let mut parsed_fields = Vec::new(); 27 | if let Some(raw_fields) = errors.handle(get_fields_named(fields)) { 28 | parsed_fields.reserve_exact(raw_fields.named.len()); 29 | for field in raw_fields.named { 30 | let Field { 31 | attrs, 32 | vis, 33 | mutability: _, 34 | ident, 35 | colon_token: _, 36 | ty, 37 | } = field; 38 | let Some(annos) = errors.handle(ModelFieldAnnotations::from_attributes(&attrs)) else { 39 | continue; 40 | }; 41 | let ident = ident.expect("Fields::Named should contain named fields"); 42 | parsed_fields.push(ParsedField { 43 | vis, 44 | ident, 45 | ty, 46 | annos, 47 | }); 48 | } 49 | } 50 | 51 | errors.finish_with(ParsedModel { 52 | vis, 53 | ident, 54 | generics, 55 | annos, 56 | fields: parsed_fields, 57 | }) 58 | } 59 | 60 | pub struct ParsedModel { 61 | pub vis: Visibility, 62 | pub ident: Ident, 63 | pub generics: Generics, 64 | pub annos: ModelAnnotations, 65 | pub fields: Vec, 66 | } 67 | 68 | #[derive(FromAttributes, Debug, Default)] 69 | #[darling(attributes(rorm), default)] 70 | pub struct ModelAnnotations { 71 | pub rename: Option, 72 | 73 | pub experimental_unregistered: bool, 74 | pub experimental_generics: bool, 75 | } 76 | 77 | pub struct ParsedField { 78 | pub vis: Visibility, 79 | pub ident: Ident, 80 | pub ty: Type, 81 | pub annos: ModelFieldAnnotations, 82 | } 83 | 84 | #[derive(FromAttributes, Debug, Default)] 85 | #[darling(attributes(rorm), default)] 86 | pub struct ModelFieldAnnotations { 87 | /// `#[rorm(auto_create_time)]` 88 | pub auto_create_time: bool, 89 | 90 | /// `#[rorm(auto_update_time)]` 91 | pub auto_update_time: bool, 92 | 93 | /// `#[rorm(auto_increment)]` 94 | pub auto_increment: bool, 95 | 96 | /// `#[rorm(primary_key)]` 97 | pub primary_key: bool, 98 | 99 | /// `#[rorm(unique)]` 100 | pub unique: bool, 101 | 102 | /// `#[rorm(id)]` 103 | pub id: bool, 104 | 105 | /// `#[rorm(on_delete = "..")]` 106 | pub on_delete: Option, 107 | 108 | /// `#[rorm(on_update = "..")]` 109 | pub on_update: Option, 110 | 111 | /// `#[rorm(rename = "..")]` 112 | pub rename: Option, 113 | 114 | // /// `#[rorm(ignore)]` 115 | // pub ignore: bool, 116 | // 117 | /// Parse the `#[rorm(default = ..)]` annotation. 118 | /// 119 | /// It accepts a single literal as argument. 120 | /// Currently the only supported literal types are: 121 | /// - String 122 | /// - Integer 123 | /// - Floating Point Number 124 | /// - Boolean 125 | /// 126 | /// TODO: Figure out how to check the literal's type is compatible with the annotated field's type 127 | pub default: Option, 128 | 129 | /// Parse the `#[rorm(max_length = ..)]` annotation. 130 | /// 131 | /// It accepts a single integer literal as argument. 132 | pub max_length: Option, 133 | 134 | /// Parse the `#[rorm(index)]` annotation. 135 | /// 136 | /// It accepts four different syntax's: 137 | /// - `#[rorm(index)]` 138 | /// - `#[rorm(index())]` 139 | /// *(semantically identical to first one)* 140 | /// - `#[rorm(index(name = ))]` 141 | /// - `#[rorm(index(name = , priority = ))]` 142 | /// *(insensitive to argument order)* 143 | pub index: Option, 144 | } 145 | -------------------------------------------------------------------------------- /rorm-macro-impl/src/parse/patch.rs: -------------------------------------------------------------------------------- 1 | use darling::FromAttributes; 2 | use proc_macro2::{Ident, TokenStream}; 3 | use quote::format_ident; 4 | use syn::{parse2, Field, ItemStruct, Path, PathSegment, Type, Visibility}; 5 | 6 | use crate::parse::annotations::NoAnnotations; 7 | use crate::parse::{check_non_generic, get_fields_named}; 8 | 9 | pub fn parse_patch(tokens: TokenStream) -> darling::Result { 10 | let ItemStruct { 11 | attrs, 12 | vis, 13 | struct_token: _, 14 | ident, 15 | generics, 16 | fields, 17 | semi_token: _, 18 | } = parse2(tokens)?; 19 | let mut errors = darling::Error::accumulator(); 20 | 21 | // Parse annotations 22 | let annos = errors.handle(PatchAnnotations::from_attributes(&attrs)); 23 | let model = annos.map(|annos| annos.model).unwrap_or_else(|| { 24 | PathSegment { 25 | ident: format_ident!(""), 26 | arguments: Default::default(), 27 | } 28 | .into() 29 | }); 30 | 31 | // Check absence of generics 32 | errors.handle(check_non_generic(generics)); 33 | 34 | // Parse fields 35 | let mut parsed_fields = Vec::new(); 36 | if let Some(raw_fields) = errors.handle(get_fields_named(fields)) { 37 | parsed_fields.reserve_exact(raw_fields.named.len()); 38 | for field in raw_fields.named { 39 | let Field { 40 | attrs, 41 | vis: _, 42 | mutability: _, 43 | ident, 44 | colon_token: _, 45 | ty, 46 | } = field; 47 | 48 | // Patch fields don't accept annotations 49 | errors.handle(NoAnnotations::from_attributes(&attrs)); 50 | 51 | let ident = ident.expect("Fields::Named should contain named fields"); 52 | parsed_fields.push(ParsedPatchField { ident, ty }); 53 | } 54 | } 55 | 56 | errors.finish_with(ParsedPatch { 57 | vis, 58 | ident, 59 | model, 60 | fields: parsed_fields, 61 | }) 62 | } 63 | 64 | pub struct ParsedPatch { 65 | pub vis: Visibility, 66 | pub ident: Ident, 67 | pub model: Path, 68 | pub fields: Vec, 69 | } 70 | 71 | pub struct ParsedPatchField { 72 | pub ident: Ident, 73 | pub ty: Type, 74 | } 75 | 76 | #[derive(FromAttributes, Debug)] 77 | #[darling(attributes(rorm))] 78 | pub struct PatchAnnotations { 79 | pub model: Path, 80 | } 81 | -------------------------------------------------------------------------------- /rorm-macro-impl/src/utils.rs: -------------------------------------------------------------------------------- 1 | pub fn to_db_name(name: String) -> String { 2 | let mut name = name; 3 | name.make_ascii_lowercase(); 4 | name 5 | } 6 | -------------------------------------------------------------------------------- /rorm-macro/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rorm-macro" 3 | version = "0.9.0" 4 | edition = "2021" 5 | repository = "https://github.com/rorm-orm/rorm" 6 | authors = ["gammelalf", "myOmikron "] 7 | categories = ["database"] 8 | keywords = ["declarative", "orm", "database", "macros"] 9 | homepage = "https://rorm.rs" 10 | documentation = "https://docs.rorm.rs" 11 | license = "MIT" 12 | description = "Macro definitions for rorm." 13 | 14 | [lib] 15 | proc-macro = true 16 | 17 | [dependencies] 18 | rorm-macro-impl = { version = "~0.9", path = "../rorm-macro-impl" } 19 | 20 | # syn builds rust syntax trees from strings or tokenstream 21 | syn = { version = "~2", features = ["full", "visit-mut"] } 22 | # quote provides a macro to write rust code with template variables which then produces a tokenstream 23 | quote = { version = "~1" } 24 | # a higher level wrapper for rust's proc-macro which is used by syn and quote 25 | proc-macro2 = { version = "~1" } 26 | -------------------------------------------------------------------------------- /rorm-macro/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! This crate tries to follow the base layout proposed by a [ferrous-systems.com](https://ferrous-systems.com/blog/testing-proc-macros/#the-pipeline) blog post. 2 | extern crate proc_macro; 3 | use proc_macro::TokenStream; 4 | use proc_macro2::Span; 5 | use quote::quote; 6 | 7 | #[proc_macro_derive(DbEnum)] 8 | pub fn derive_db_enum(input: TokenStream) -> TokenStream { 9 | rorm_macro_impl::derive_db_enum(input.into(), rorm_macro_impl::MacroConfig::default()).into() 10 | } 11 | 12 | #[proc_macro_derive(Model, attributes(rorm))] 13 | pub fn derive_model(input: TokenStream) -> TokenStream { 14 | rorm_macro_impl::derive_model(input.into(), rorm_macro_impl::MacroConfig::default()).into() 15 | } 16 | 17 | #[proc_macro_derive(Patch, attributes(rorm))] 18 | pub fn derive_patch(input: TokenStream) -> TokenStream { 19 | rorm_macro_impl::derive_patch(input.into(), rorm_macro_impl::MacroConfig::default()).into() 20 | } 21 | 22 | #[proc_macro_attribute] 23 | pub fn rorm_main(args: TokenStream, item: TokenStream) -> TokenStream { 24 | let main = syn::parse_macro_input!(item as syn::ItemFn); 25 | let feature = syn::parse::(args) 26 | .unwrap_or_else(|_| syn::LitStr::new("rorm-main", Span::call_site())); 27 | 28 | (if main.sig.ident == "main" { 29 | quote! { 30 | #[cfg(feature = #feature)] 31 | fn main() -> Result<(), String> { 32 | let mut file = ::std::fs::File::create(".models.json").map_err(|err| err.to_string())?; 33 | ::rorm::write_models(&mut file)?; 34 | return Ok(()); 35 | } 36 | #[cfg(not(feature = #feature))] 37 | #main 38 | } 39 | } else { 40 | quote! { 41 | compile_error!("only allowed on main function"); 42 | #main 43 | } 44 | }).into() 45 | } 46 | 47 | /// ```ignored 48 | /// impl_tuple!(some_macro, 2..5); 49 | /// 50 | /// // produces 51 | /// 52 | /// some_macro!(0: T0, 1: T1); // tuple of length 2 53 | /// some_macro!(0: T0, 1: T1, 2: T2); // tuple of length 3 54 | /// some_macro!(0: T0, 1: T1, 2: T2, 3: T3); // tuple of length 4 55 | /// ``` 56 | #[proc_macro] 57 | pub fn impl_tuple(args: TokenStream) -> TokenStream { 58 | // handwritten without dependencies just for fun 59 | use proc_macro::{Delimiter, Group, Ident, Literal, Punct, Spacing, Span, TokenTree as TT}; 60 | 61 | let args = Vec::from_iter(args); 62 | let [TT::Ident(macro_ident), TT::Punct(comma), TT::Literal(start), TT::Punct(fst_dot), TT::Punct(snd_dot), TT::Literal(end)] = 63 | &args[..] 64 | else { 65 | panic!() 66 | }; 67 | if *comma != ',' 68 | || *fst_dot != '.' 69 | || *snd_dot != '.' && matches!(fst_dot.spacing(), Spacing::Alone) 70 | { 71 | panic!(); 72 | } 73 | 74 | let start: usize = start.to_string().parse().unwrap(); 75 | let end: usize = end.to_string().parse().unwrap(); 76 | 77 | let mut tokens = TokenStream::default(); 78 | for until in start..end { 79 | let mut impl_args = TokenStream::new(); 80 | for index in 0..until { 81 | impl_args.extend([ 82 | TT::Literal(Literal::usize_unsuffixed(index)), 83 | TT::Punct(Punct::new(':', Spacing::Alone)), 84 | TT::Ident(Ident::new(&format!("T{index}"), Span::call_site())), 85 | TT::Punct(Punct::new(',', Spacing::Alone)), 86 | ]); 87 | } 88 | tokens.extend([ 89 | TT::Ident(macro_ident.clone()), 90 | TT::Punct(Punct::new('!', Spacing::Alone)), 91 | TT::Group(Group::new(Delimiter::Parenthesis, impl_args)), 92 | TT::Punct(Punct::new(';', Spacing::Alone)), 93 | ]); 94 | } 95 | tokens 96 | } 97 | -------------------------------------------------------------------------------- /src/conditions/in.rs: -------------------------------------------------------------------------------- 1 | use crate::conditions::collections::CollectionOperator; 2 | use crate::conditions::{BinaryOperator, Condition, Value}; 3 | use crate::internal::query_context::flat_conditions::FlatCondition; 4 | use crate::internal::query_context::ConditionBuilder; 5 | 6 | /// An "IN" expression 7 | /// 8 | /// The implementation will definitely change, 9 | /// but it's better to have some form of "IN" than none. 10 | #[derive(Clone, Debug)] 11 | pub struct In { 12 | /// SQL operator to use 13 | pub operator: InOperator, 14 | /// The left side of the operator 15 | pub fst_arg: A, 16 | /// The mulidple values on the operator's right side 17 | pub snd_arg: Vec, 18 | } 19 | 20 | /// The operator of an "IN" expression 21 | #[derive(Copy, Clone, Debug)] 22 | pub enum InOperator { 23 | /// Representation of "{} IN ({}, ...)" in SQL 24 | In, 25 | /// Representation of "{} NOT IN ({}, ...)" in SQL 26 | NotIn, 27 | } 28 | 29 | impl<'a, A, B> Condition<'a> for In 30 | where 31 | A: Condition<'a>, 32 | B: Condition<'a>, 33 | { 34 | fn build(&self, mut builder: ConditionBuilder<'_, 'a>) { 35 | if self.snd_arg.is_empty() { 36 | Value::Bool(false).build(builder); 37 | } else { 38 | builder.push_condition(FlatCondition::StartCollection(CollectionOperator::Or)); 39 | for snd_arg in self.snd_arg.iter() { 40 | builder.push_condition(FlatCondition::BinaryCondition(BinaryOperator::Equals)); 41 | self.fst_arg.build(builder.reborrow()); 42 | snd_arg.build(builder.reborrow()); 43 | } 44 | builder.push_condition(FlatCondition::EndCollection); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/crud/builder.rs: -------------------------------------------------------------------------------- 1 | //! This module provides primitives used by the various builder. 2 | 3 | use crate::conditions::Condition; 4 | use crate::internal::query_context::QueryContext; 5 | use crate::sealed; 6 | 7 | /// Marker for the generic parameter storing an optional [`Condition`] 8 | pub trait ConditionMarker<'a>: Send { 9 | sealed!(trait); 10 | 11 | /// Calls [`Condition::build`] if `Self: Condition` 12 | /// or returns `None` if `Self = ()` 13 | fn build(&self, context: &mut QueryContext<'a>) -> Option; 14 | } 15 | 16 | impl<'a> ConditionMarker<'a> for () { 17 | sealed!(impl); 18 | 19 | fn build(&self, _context: &mut QueryContext<'a>) -> Option { 20 | None 21 | } 22 | } 23 | 24 | impl<'a, T: Condition<'a>> ConditionMarker<'a> for T { 25 | sealed!(impl); 26 | 27 | fn build(&self, context: &mut QueryContext<'a>) -> Option { 28 | Some(context.add_condition(self)) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/crud/decoder.rs: -------------------------------------------------------------------------------- 1 | //! [`Decoder`] trait and some basic implementations 2 | 3 | use std::marker::PhantomData; 4 | 5 | use rorm_db::row::{DecodeOwned, RowError}; 6 | use rorm_db::Row; 7 | 8 | /// Something which decodes a [value](Self::Result) from a [`&Row`](rorm_db::Row) 9 | /// 10 | /// It is basically a closure `Fn(&Row) -> Result`. 11 | /// Sadly we need to support decoding via indexes so this trait actually has two method. 12 | /// One for decoding [`by_name`](Self::by_name) and another one for decoding [`by_index`](Self::by_index). 13 | /// 14 | /// This trait does not manage 15 | /// a) how the decoder is constructed 16 | /// and b) that the row contains the columns which the decoder will access 17 | /// 18 | /// These concerns are delegated to further sub-traits, namely: 19 | /// - [`Selector`](super::selector::Selector) which constructs a [`Decoder`] and configures the [`QueryContext`](crate::internal::query_context::QueryContext) appropriately 20 | /// - [`FieldDecoder`](crate::internal::field::decoder::FieldDecoder) which decodes and is associated to single fields through [`FieldType::Decoder`](crate::fields::traits::FieldType::Decoder) 21 | pub trait Decoder { 22 | /// The value decoded from a row 23 | type Result; 24 | 25 | /// Decode a value from a row using select aliases to access the columns 26 | fn by_name<'index>(&'index self, row: &'_ Row) -> Result>; 27 | 28 | /// Decode a value from a row using indexes to access the columns 29 | fn by_index<'index>(&'index self, row: &'_ Row) -> Result>; 30 | } 31 | 32 | impl Decoder for &'_ D { 33 | type Result = D::Result; 34 | 35 | fn by_name<'index>(&'index self, row: &'_ Row) -> Result> { 36 | D::by_name(self, row) 37 | } 38 | 39 | fn by_index<'index>(&'index self, row: &'_ Row) -> Result> { 40 | D::by_index(self, row) 41 | } 42 | } 43 | 44 | /// A [`Decoder`] which directly decodes a [`T: DecodedOwned`](DecodeOwned) 45 | pub struct DirectDecoder { 46 | pub(crate) result: PhantomData, 47 | pub(crate) column: String, 48 | pub(crate) index: usize, 49 | } 50 | impl Decoder for DirectDecoder 51 | where 52 | T: DecodeOwned, 53 | { 54 | type Result = T; 55 | 56 | fn by_name<'index>(&'index self, row: &'_ Row) -> Result> { 57 | row.get(self.column.as_str()) 58 | } 59 | 60 | fn by_index<'index>(&'index self, row: &'_ Row) -> Result> { 61 | row.get(self.index) 62 | } 63 | } 64 | 65 | /// A [`Decoder`] which "decodes" a value by using the [`Default`] trait 66 | /// 67 | /// This is a "noop" which doesn't touch the [`&Row`](rorm_db::Row) at all 68 | pub struct NoopDecoder(pub(crate) PhantomData); 69 | impl Decoder for NoopDecoder 70 | where 71 | T: Default, 72 | { 73 | type Result = T; 74 | 75 | fn by_name<'index>(&'index self, _row: &'_ Row) -> Result> { 76 | Ok(Default::default()) 77 | } 78 | 79 | fn by_index<'index>(&'index self, _row: &'_ Row) -> Result> { 80 | Ok(Default::default()) 81 | } 82 | } 83 | 84 | macro_rules! decoder { 85 | ($($index:tt : $S:ident,)+) => { 86 | impl<$($S: Decoder),+> Decoder for ($($S,)+) { 87 | type Result = ($( 88 | $S::Result, 89 | )+); 90 | 91 | fn by_name<'index>(&'index self, row: &'_ Row) -> Result> { 92 | Ok(($( 93 | self.$index.by_name(row)?, 94 | )+)) 95 | } 96 | 97 | fn by_index<'index>(&'index self, row: &'_ Row) -> Result> { 98 | Ok(($( 99 | self.$index.by_index(row)?, 100 | )+)) 101 | } 102 | } 103 | }; 104 | } 105 | rorm_macro::impl_tuple!(decoder, 1..33); 106 | 107 | /// Extension trait for [`Decoder`] 108 | /// 109 | /// It provides combinators to tweak a decoder's behaviour. 110 | /// 111 | /// This is an extension trait instead of part of the base trait, 112 | /// because I'm not sure yet, if, how and by whom those combinators would be used. 113 | pub trait DecoderExt: Decoder + Sized { 114 | /// Borrows the decoder 115 | /// 116 | /// This method is an alternative to taking a reference 117 | /// which might look awkward in a builder expression. 118 | fn by_ref(&self) -> &Self { 119 | self 120 | } 121 | 122 | /// Construct a decoder which applies a function to the result of `Self`. 123 | fn map(self, f: F) -> Map 124 | where 125 | F: Fn(Self::Result) -> U, 126 | { 127 | Map { 128 | decoder: self, 129 | function: f, 130 | } 131 | } 132 | 133 | /// Construct a decoder which handles a `RowError::UnexpectedNull` by producing `None` 134 | fn optional(self) -> Optional { 135 | Optional { decoder: self } 136 | } 137 | 138 | // TODO: Where should RowError get its lifetime from? 139 | // 140 | // fn and_then(self, f: F) -> AndThen 141 | // where 142 | // F: Fn(Self::Result) -> Result>, 143 | // { 144 | // AndThen { 145 | // decoder: self, 146 | // function: f, 147 | // } 148 | // } 149 | } 150 | 151 | /// [`Decoder`] returned by [`DecoderExt::map`] 152 | pub struct Map { 153 | decoder: D, 154 | function: F, 155 | } 156 | impl Decoder for Map 157 | where 158 | D: Decoder, 159 | F: Fn(D::Result) -> T, 160 | { 161 | type Result = T; 162 | 163 | fn by_name<'index>(&'index self, row: &'_ Row) -> Result> { 164 | self.decoder.by_name(row).map(&self.function) 165 | } 166 | 167 | fn by_index<'index>(&'index self, row: &'_ Row) -> Result> { 168 | self.decoder.by_index(row).map(&self.function) 169 | } 170 | } 171 | 172 | /// [`Decoder`] returned by [`DecoderExt::optional`] 173 | pub struct Optional { 174 | decoder: D, 175 | } 176 | impl Decoder for Optional 177 | where 178 | D: Decoder, 179 | { 180 | type Result = Option; 181 | 182 | fn by_name<'index>(&'index self, row: &'_ Row) -> Result> { 183 | match self.decoder.by_name(row) { 184 | Ok(result) => Ok(Some(result)), 185 | Err(RowError::UnexpectedNull { .. }) => Ok(None), 186 | Err(error) => Err(error), 187 | } 188 | } 189 | 190 | fn by_index<'index>(&'index self, row: &'_ Row) -> Result> { 191 | match self.decoder.by_index(row) { 192 | Ok(result) => Ok(Some(result)), 193 | Err(RowError::UnexpectedNull { .. }) => Ok(None), 194 | Err(error) => Err(error), 195 | } 196 | } 197 | } 198 | 199 | // /// [`Decoder`] returned by [`DecoderExt::and_then`] 200 | // pub struct AndThen { 201 | // decoder: D, 202 | // function: F, 203 | // } 204 | // 205 | // impl Decoder for AndThen 206 | // where 207 | // D: Decoder, 208 | // F: Fn(D::Result) -> Result>, 209 | // { 210 | // type Result = T; 211 | // 212 | // TODO: RowError requires a single index, what to do when `D` decodes more than one column? 213 | // 214 | // fn by_name<'index>(&'index self, row: &'_ Row) -> Result> { 215 | // self.decoder.by_name(row).and_then(&self.function) 216 | // } 217 | // 218 | // fn by_index<'index>(&'index self, row: &'_ Row) -> Result> { 219 | // self.decoder.by_name(row).and_then(&self.function) 220 | // } 221 | // } 222 | -------------------------------------------------------------------------------- /src/crud/delete.rs: -------------------------------------------------------------------------------- 1 | //! Delete builder and macro 2 | 3 | use std::marker::PhantomData; 4 | 5 | use rorm_db::database; 6 | use rorm_db::error::Error; 7 | use rorm_db::executor::Executor; 8 | 9 | use crate::conditions::{Condition, DynamicCollection}; 10 | use crate::crud::selector::Selector; 11 | use crate::internal::patch::{IntoPatchCow, PatchCow}; 12 | use crate::internal::query_context::QueryContext; 13 | use crate::model::{Identifiable, Model}; 14 | use crate::Patch; 15 | 16 | /// Create a DELETE query. 17 | /// 18 | /// # Usage 19 | /// ```no_run 20 | /// # use rorm::{Model, Patch, Database, delete}; 21 | /// # #[derive(Model)] pub struct User { #[rorm(id)] id: i64, age: i32, } 22 | /// # #[derive(Patch)] #[rorm(model = "User")] pub struct UserPatch { id: i64, } 23 | /// pub async fn delete_single_user(db: &Database, user: &UserPatch) { 24 | /// delete(db, User) 25 | /// .single(user) 26 | /// .await 27 | /// .unwrap(); 28 | /// } 29 | /// pub async fn delete_many_users(db: &Database, users: &[UserPatch]) { 30 | /// delete(db, User) 31 | /// .bulk(users) 32 | /// .await 33 | /// .unwrap(); 34 | /// } 35 | /// pub async fn delete_underage(db: &Database) { 36 | /// let num_deleted: u64 = delete(db, User) 37 | /// .condition(User.age.less_equals(18)) 38 | /// .await 39 | /// .unwrap(); 40 | /// } 41 | ///``` 42 | /// 43 | /// Like every crud macro `delete!` starts a [builder](DeleteBuilder) which is consumed to execute the query. 44 | /// 45 | /// `delete!`'s first argument is a reference to the [`Database`](crate::Database). 46 | /// Its second is the [`Model`] type of whose table you want to delete columns from. 47 | /// 48 | /// To specify what rows to delete use the following methods, 49 | /// which will consume the builder and execute the query: 50 | /// - [`single`](DeleteBuilder::single): Delete a single row identified by a patch instance 51 | /// - [`bulk`](DeleteBuilder::bulk): Delete a bulk of rows identified by patch instances 52 | /// - [`condition`](DeleteBuilder::condition): Delete all rows matching a condition 53 | /// - [`all`](DeleteBuilder::all): Unconditionally delete all rows 54 | pub fn delete<'ex, E, S>(executor: E, _: S) -> DeleteBuilder 55 | where 56 | E: Executor<'ex>, 57 | S: Selector>, 58 | { 59 | DeleteBuilder { 60 | executor, 61 | 62 | _phantom: PhantomData, 63 | } 64 | } 65 | 66 | /// Builder for delete queries 67 | /// 68 | /// To create a builder use [`delete`] 69 | /// 70 | /// ## Generics 71 | /// - `E`: [`Executor`] 72 | /// 73 | /// The executor to query with. 74 | /// 75 | /// - `M`: [`Model`] 76 | /// 77 | /// The model from whose table to delete rows. 78 | /// 79 | #[must_use] 80 | pub struct DeleteBuilder { 81 | executor: E, 82 | 83 | _phantom: PhantomData, 84 | } 85 | 86 | impl<'ex, E, M> DeleteBuilder 87 | where 88 | E: Executor<'ex>, 89 | M: Model, 90 | { 91 | /// Delete a single row identified by a patch instance 92 | /// 93 | /// Note: The patch only provides the primary key, its other values will be ignored. 94 | pub async fn single

(self, patch: &P) -> Result 95 | where 96 | P: Patch + Identifiable, 97 | { 98 | self.condition(patch.as_condition()).await 99 | } 100 | 101 | /// Delete a bulk of rows identified by patch instances 102 | /// 103 | /// Note: The patches only provide the primary key, their other values will be ignored. 104 | /// 105 | /// # Argument 106 | /// This method accepts anything which can be used to iterate 107 | /// over instances or references of your [`Patch`]. 108 | /// 109 | /// **Examples**: (where `P` is your patch) 110 | /// - `Vec

` 111 | /// - `&[P]` 112 | /// - A [`map`](Iterator::map) iterator yielding `P` or `&P` 113 | pub async fn bulk<'p, I, P>(self, patches: I) -> Result 114 | where 115 | I: IntoIterator, 116 | I::Item: IntoPatchCow<'p, Patch = P>, 117 | P: Patch + Identifiable, 118 | { 119 | let mut owned = Vec::new(); 120 | let mut conditions = Vec::new(); 121 | for patch in patches { 122 | match patch.into_patch_cow() { 123 | PatchCow::Borrowed(patch) => conditions.push(patch.as_condition()), 124 | PatchCow::Owned(patch) => owned.push(patch), 125 | } 126 | } 127 | for patch in &owned { 128 | conditions.push(patch.as_condition()); 129 | } 130 | if conditions.is_empty() { 131 | Ok(0) 132 | } else { 133 | self.condition(DynamicCollection::or(conditions)).await 134 | } 135 | } 136 | 137 | /// Delete all rows matching a condition 138 | pub async fn condition<'c, C: Condition<'c>>(self, condition: C) -> Result { 139 | let mut context = QueryContext::new(); 140 | let condition_index = context.add_condition(&condition); 141 | database::delete( 142 | self.executor, 143 | M::TABLE, 144 | Some(&context.get_condition(condition_index)), 145 | ) 146 | .await 147 | } 148 | 149 | /// Delete all rows 150 | pub async fn all(self) -> Result { 151 | database::delete(self.executor, M::TABLE, None).await 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/crud/mod.rs: -------------------------------------------------------------------------------- 1 | //! This module holds the CRUD interface. 2 | //! 3 | //! rorm's crud is entirely based on the builder pattern. 4 | //! This means for every crud query ([INSERT](insert), [SELECT](query), [UPDATE](update), [DELETE](delete)) there exists a builder struct 5 | //! whose methods allow you to set the various parameters. 6 | //! 7 | //! To begin a builder use their associated functions [`insert`], [`query`], [`update`] and [`delete`]. 8 | pub mod builder; 9 | pub mod decoder; 10 | pub mod delete; 11 | pub mod insert; 12 | pub mod query; 13 | pub mod selector; 14 | pub mod update; 15 | -------------------------------------------------------------------------------- /src/crud/selector.rs: -------------------------------------------------------------------------------- 1 | //! Trait for selecting stuff 2 | 3 | use std::marker::PhantomData; 4 | 5 | use rorm_db::row::DecodeOwned; 6 | use rorm_db::sql::aggregation::SelectAggregator; 7 | 8 | use crate::crud::decoder::{Decoder, DirectDecoder}; 9 | use crate::fields::proxy::{FieldProxy, FieldProxyImpl}; 10 | use crate::fields::traits::FieldType; 11 | use crate::internal::field::decoder::FieldDecoder; 12 | use crate::internal::field::Field; 13 | use crate::internal::query_context::QueryContext; 14 | use crate::internal::relation_path::Path; 15 | use crate::model::Model; 16 | 17 | /// Something which "selects" a value from a certain table, 18 | /// by configuring a [`QueryContext`] and providing a [`Decoder`] 19 | pub trait Selector { 20 | /// The value selected by this selector 21 | type Result; 22 | 23 | /// [`Model`] from whose table to select from 24 | type Model: Model; 25 | 26 | /// [`Decoder`] to decode the selected value from a [`&Row`](rorm_db::Row) 27 | type Decoder: Decoder; 28 | 29 | /// Can this selector be used in insert queries to specify the returning expression? 30 | const INSERT_COMPATIBLE: bool; 31 | 32 | /// Constructs a decoder and configures a [`QueryContext`] to query the required columns 33 | fn select(self, ctx: &mut QueryContext) -> Self::Decoder; 34 | } 35 | 36 | /// Combinator which wraps a selector to apply a path to it. 37 | pub struct PathedSelector { 38 | /// The wrapped selector 39 | pub selector: S, 40 | pub(crate) path: PhantomData

, 41 | } 42 | 43 | impl Selector for PathedSelector 44 | where 45 | S: Selector, 46 | P: Path, 47 | { 48 | type Result = S::Result; 49 | type Model = P::Origin; 50 | type Decoder = S::Decoder; 51 | const INSERT_COMPATIBLE: bool = P::IS_ORIGIN && S::INSERT_COMPATIBLE; 52 | 53 | fn select(self, ctx: &mut QueryContext) -> Self::Decoder { 54 | let mut ctx = ctx.with_base_path::

(); 55 | self.selector.select(&mut ctx) 56 | } 57 | } 58 | 59 | impl Selector for FieldProxy 60 | where 61 | T: FieldType, 62 | F: Field, 63 | P: Path, 64 | I: FieldProxyImpl, 65 | { 66 | type Result = F::Type; 67 | type Model = P::Origin; 68 | type Decoder = T::Decoder; 69 | const INSERT_COMPATIBLE: bool = P::IS_ORIGIN; 70 | 71 | fn select(self, ctx: &mut QueryContext) -> Self::Decoder { 72 | FieldDecoder::new(ctx, self) 73 | } 74 | } 75 | 76 | /// A column to select and call an aggregation function on 77 | #[derive(Copy, Clone)] 78 | pub struct AggregatedColumn { 79 | pub(crate) sql: SelectAggregator, 80 | pub(crate) alias: &'static str, 81 | pub(crate) field: FieldProxy, 82 | pub(crate) result: PhantomData, 83 | } 84 | impl Selector for AggregatedColumn 85 | where 86 | I: FieldProxyImpl, 87 | R: DecodeOwned, 88 | { 89 | type Result = R; 90 | type Model = ::Origin; 91 | type Decoder = DirectDecoder; 92 | const INSERT_COMPATIBLE: bool = false; 93 | 94 | fn select(self, ctx: &mut QueryContext) -> Self::Decoder { 95 | let (index, column) = ctx.select_aggregation(self); 96 | DirectDecoder { 97 | result: PhantomData, 98 | column, 99 | index, 100 | } 101 | } 102 | } 103 | 104 | macro_rules! selectable { 105 | ($($index:tt : $S:ident,)+) => { 106 | impl),+> Selector for ($($S,)+) 107 | { 108 | type Result = ($( 109 | $S::Result, 110 | )+); 111 | 112 | type Model = M; 113 | 114 | type Decoder = ($( 115 | $S::Decoder, 116 | )+); 117 | 118 | const INSERT_COMPATIBLE: bool = $($S::INSERT_COMPATIBLE &&)+ true; 119 | 120 | fn select(self, ctx: &mut QueryContext) -> Self::Decoder { 121 | ($( 122 | self.$index.select(ctx), 123 | )+) 124 | } 125 | } 126 | }; 127 | } 128 | rorm_macro::impl_tuple!(selectable, 1..33); 129 | -------------------------------------------------------------------------------- /src/fields/mod.rs: -------------------------------------------------------------------------------- 1 | //! All types valid as model fields and traits to make them valid. 2 | //! 3 | //! # Std types 4 | //! - [`bool`] 5 | //! - [`i16`] 6 | //! - [`i32`] 7 | //! - [`i64`] 8 | //! - [`f32`] 9 | //! - [`f64`] 10 | //! - [`String`] 11 | //! - [`Vec`] 12 | //! - [`Option`] where `T` is on this list 13 | //! 14 | //! # Our types 15 | //! - [`ForeignModel`](types::ForeignModel) 16 | //! - [`BackRef`](types::BackRef) (doesn't work inside an [`Option`]) 17 | //! - [`Json`](types::Json) 18 | //! - [`MsgPack`](types::MsgPack) (requires the "msgpack" feature) 19 | //! - [`MaxStr`](types::MaxStr) 20 | //! 21 | //! # chrono types (requires the "chrono" feature) 22 | //! - [`NaiveDateTime`](chrono::NaiveDateTime) 23 | //! - [`NaiveTime`](chrono::NaiveTime) 24 | //! - [`NaiveDate`](chrono::NaiveDate) 25 | //! - [`DateTime`](chrono::DateTime) 26 | //! 27 | //! # time types (requires the "time" feature) 28 | //! - [`PrimitiveDateTime`](time::PrimitiveDateTime) 29 | //! - [`Time`](time::Time) 30 | //! - [`Date`](time::Date) 31 | //! - [`OffsetDateTime`](time::OffsetDateTime) 32 | //! 33 | //! # uuid types (requires the "uuid" feature) 34 | //! - [`Uuid`](uuid::Uuid) 35 | //! 36 | //! # url types (requires the "url" feature) 37 | //! - [`Url`](url::Url) 38 | //! 39 | //! --- 40 | //! 41 | //! ```no_run 42 | //! use serde::{Deserialize, Serialize}; 43 | //! use rorm::{Model, field}; 44 | //! use rorm::fields::types::*; 45 | //! 46 | //! #[derive(Model)] 47 | //! pub struct SomeModel { 48 | //! #[rorm(id)] 49 | //! id: i64, 50 | //! 51 | //! // std 52 | //! boolean: bool, 53 | //! integer: i32, 54 | //! float: f64, 55 | //! #[rorm(max_length = 255)] 56 | //! string: String, 57 | //! binary: Vec, 58 | //! 59 | //! // times 60 | //! time: chrono::NaiveTime, 61 | //! date: chrono::NaiveDate, 62 | //! datetime: chrono::DateTime, 63 | //! 64 | //! // relations 65 | //! other_model: ForeignModel, 66 | //! also_other_model: ForeignModelByField, 67 | //! other_model_set: BackRef, 68 | //! 69 | //! // serde 70 | //! data: Json, 71 | //! } 72 | //! 73 | //! #[derive(Model)] 74 | //! pub struct OtherModel { 75 | //! #[rorm(id)] 76 | //! id: i64, 77 | //! 78 | //! #[rorm(max_length = 255)] 79 | //! name: String, 80 | //! 81 | //! some_model: ForeignModel, 82 | //! } 83 | //! 84 | //! #[derive(Serialize, Deserialize)] 85 | //! pub struct Data { 86 | //! stuff: String, 87 | //! } 88 | //! ``` 89 | 90 | pub mod proxy; 91 | pub mod traits; 92 | pub mod types; 93 | pub mod utils; 94 | -------------------------------------------------------------------------------- /src/fields/traits/aggregate.rs: -------------------------------------------------------------------------------- 1 | //! Marker traits which can be implemented on a [`FieldType`] to allow the usage of various aggregation functions. 2 | //! 3 | //! ## Using 4 | //! The traits don't prodived any methods. Instead use the corresponding method on [`FieldProxy`]. 5 | 6 | use rorm_db::row::DecodeOwned; 7 | 8 | #[cfg(doc)] 9 | use crate::fields::proxy::FieldProxy; 10 | use crate::fields::traits::{Array, FieldType}; 11 | 12 | /// Marker for [`FieldProxy::count`] 13 | /// 14 | /// This is implemented for every [`SingleColumnFieldType`](crate::internal::field::SingleColumnField) 15 | pub trait FieldCount: FieldType {} 16 | impl FieldCount for T where T: FieldType> {} 17 | 18 | /// Marker for [`FieldProxy::sum`] 19 | pub trait FieldSum: FieldType> { 20 | /// The aggregation result's type 21 | /// 22 | /// If `Self` is not `Option`, then this should be `Option`. 23 | /// If `Self` is a `Option`, then this should be just `Self`. 24 | type Result: DecodeOwned; 25 | } 26 | 27 | /// Marker for [`FieldProxy::avg`] 28 | pub trait FieldAvg: FieldType> {} 29 | 30 | /// Marker for [`FieldProxy::max`] 31 | pub trait FieldMax: FieldType> { 32 | /// The aggregation result's type 33 | /// 34 | /// If `Self` is not `Option`, then this should be `Option`. 35 | /// If `Self` is a `Option`, then this should be just `Self`. 36 | type Result: DecodeOwned; 37 | } 38 | 39 | /// Marker for [`FieldProxy::min`] 40 | pub trait FieldMin: FieldType> { 41 | /// The aggregation result's type 42 | /// 43 | /// If `Self` is not `Option`, then this should be `Option`. 44 | /// If `Self` is a `Option`, then this should be just `Self`. 45 | type Result: DecodeOwned; 46 | } 47 | 48 | /// Implements [`FieldSum`] and [`FieldAvg`] for its argument `T` and `Option` 49 | /// 50 | /// # Syntax 51 | /// Pass the type to implement as first argument and pass the [`FieldSum::Result`] type with 52 | /// the `sum_result` key: 53 | /// ```compile_fail 54 | /// impl_FieldSum_FieldAvg!(i16, sum_result: i64); 55 | /// ``` 56 | #[allow(non_snake_case)] 57 | #[macro_export] 58 | macro_rules! impl_FieldSum_FieldAvg { 59 | ($arg:ty, sum_result: $ret:ty) => { 60 | impl $crate::fields::traits::FieldSum for $arg { 61 | type Result = Option<$ret>; 62 | } 63 | impl $crate::fields::traits::FieldSum for Option<$arg> { 64 | type Result = Option<$ret>; 65 | } 66 | impl $crate::fields::traits::FieldAvg for $arg {} 67 | impl $crate::fields::traits::FieldAvg for Option<$arg> {} 68 | }; 69 | } 70 | 71 | /// Implements [`FieldMin`] and [`FieldMax`] for its argument `T` and `Option`. 72 | /// 73 | /// (The `Result` will always be `Option`.) 74 | /// 75 | /// # Syntax 76 | /// For a type without generics simple pass it as argument: 77 | /// ```fail_compile 78 | /// impl_FieldMin_FieldMax!(String); 79 | /// ``` 80 | /// 81 | /// If your type is generic, you have to add a dummy `impl<...>` before the type: 82 | /// ```fail_compile 83 | /// impl_FieldMin_FieldMax!(impl MaxStr); 84 | /// ``` 85 | #[allow(non_snake_case)] 86 | #[macro_export] 87 | macro_rules! impl_FieldMin_FieldMax { 88 | (impl<$($generic:ident $( $const_name:ident : $const_type:ty )?),*> $arg:ty) => { 89 | $crate::impl_FieldMin_FieldMax!(@internal [<$($generic $( $const_name : $const_type )?),*>] $arg); 90 | }; 91 | ($arg:ty) => { 92 | $crate::impl_FieldMin_FieldMax!(@internal [] $arg); 93 | }; 94 | (@internal [$($generic:tt)*] $arg:ty) => { 95 | impl $($generic)* $crate::fields::traits::FieldMin for $arg 96 | where 97 | $arg: $crate::fields::traits::FieldType, 98 | Option<$arg>: $crate::db::row::DecodeOwned, 99 | { 100 | type Result = Option<$arg>; 101 | } 102 | impl $($generic)* $crate::fields::traits::FieldMin for Option<$arg> 103 | where 104 | Option<$arg>: $crate::fields::traits::FieldType, 105 | Option<$arg>: $crate::db::row::DecodeOwned, 106 | { 107 | type Result = Option<$arg>; 108 | } 109 | impl $($generic)* $crate::fields::traits::FieldMax for $arg 110 | where 111 | $arg: $crate::fields::traits::FieldType, 112 | Option<$arg>: $crate::db::row::DecodeOwned, 113 | { 114 | type Result = Option<$arg>; 115 | } 116 | impl $($generic)* $crate::fields::traits::FieldMax for Option<$arg> 117 | where 118 | Option<$arg>: $crate::fields::traits::FieldType, 119 | Option<$arg>: $crate::db::row::DecodeOwned, 120 | { 121 | type Result = Option<$arg>; 122 | } 123 | }; 124 | } 125 | -------------------------------------------------------------------------------- /src/fields/types/chrono.rs: -------------------------------------------------------------------------------- 1 | use chrono::{DateTime, NaiveDate, NaiveDateTime, NaiveTime, Utc}; 2 | use rorm_db::sql::value::NullType; 3 | 4 | use crate::conditions::Value; 5 | use crate::{impl_FieldEq, impl_FieldMin_FieldMax, impl_FieldOrd, impl_FieldType}; 6 | 7 | impl_FieldType!(NaiveTime, ChronoNaiveTime, Value::ChronoNaiveTime); 8 | impl_FieldEq!(impl<'rhs> FieldEq<'rhs, NaiveTime> for NaiveTime { Value::ChronoNaiveTime }); 9 | impl_FieldEq!(impl<'rhs> FieldEq<'rhs, Option> for Option { |option: Self| option.map(Value::ChronoNaiveTime).unwrap_or(Value::Null(NullType::ChronoNaiveTime)) }); 10 | impl_FieldOrd!(NaiveTime, NaiveTime, Value::ChronoNaiveTime); 11 | impl_FieldOrd!(Option, Option, |option: Self| option 12 | .map(Value::ChronoNaiveTime) 13 | .unwrap_or(Value::Null(NullType::ChronoNaiveTime))); 14 | impl_FieldMin_FieldMax!(NaiveTime); 15 | 16 | impl_FieldType!(NaiveDate, ChronoNaiveDate, Value::ChronoNaiveDate); 17 | impl_FieldEq!(impl<'rhs> FieldEq<'rhs, NaiveDate> for NaiveDate { Value::ChronoNaiveDate }); 18 | impl_FieldEq!(impl<'rhs> FieldEq<'rhs, Option> for Option { |option: Self| option.map(Value::ChronoNaiveDate).unwrap_or(Value::Null(NullType::ChronoNaiveDate)) }); 19 | impl_FieldOrd!(NaiveDate, NaiveDate, Value::ChronoNaiveDate); 20 | impl_FieldOrd!(Option, Option, |option: Self| option 21 | .map(Value::ChronoNaiveDate) 22 | .unwrap_or(Value::Null(NullType::ChronoNaiveDate))); 23 | impl_FieldMin_FieldMax!(NaiveDate); 24 | 25 | impl_FieldType!( 26 | NaiveDateTime, 27 | ChronoNaiveDateTime, 28 | Value::ChronoNaiveDateTime 29 | ); 30 | impl_FieldEq!(impl<'rhs> FieldEq<'rhs, NaiveDateTime> for NaiveDateTime { Value::ChronoNaiveDateTime }); 31 | impl_FieldEq!(impl<'rhs> FieldEq<'rhs, Option> for Option { |option: Self| option.map(Value::ChronoNaiveDateTime).unwrap_or(Value::Null(NullType::ChronoNaiveDateTime)) }); 32 | impl_FieldOrd!(NaiveDateTime, NaiveDateTime, Value::ChronoNaiveDateTime); 33 | impl_FieldOrd!( 34 | Option, 35 | Option, 36 | |option: Self| option 37 | .map(Value::ChronoNaiveDateTime) 38 | .unwrap_or(Value::Null(NullType::ChronoNaiveDateTime)) 39 | ); 40 | impl_FieldMin_FieldMax!(NaiveDateTime); 41 | 42 | impl_FieldType!(DateTime, ChronoDateTime, Value::ChronoDateTime); 43 | impl_FieldEq!(impl<'rhs> FieldEq<'rhs, DateTime> for DateTime { Value::ChronoDateTime }); 44 | impl_FieldEq!(impl<'rhs> FieldEq<'rhs, Option>> for Option> { |option: Self| option.map(Value::ChronoDateTime).unwrap_or(Value::Null(NullType::ChronoDateTime)) }); 45 | impl_FieldOrd!(DateTime, DateTime, Value::ChronoDateTime); 46 | impl_FieldOrd!( 47 | Option>, 48 | Option>, 49 | |option: Self| option 50 | .map(Value::ChronoDateTime) 51 | .unwrap_or(Value::Null(NullType::ChronoDateTime)) 52 | ); 53 | impl_FieldMin_FieldMax!(DateTime); 54 | -------------------------------------------------------------------------------- /src/fields/types/foreign_model.rs: -------------------------------------------------------------------------------- 1 | //! The [ForeignModel] field type 2 | 3 | use std::fmt; 4 | 5 | use rorm_db::Executor; 6 | 7 | use crate::conditions::{Binary, BinaryOperator, Column, Condition}; 8 | use crate::crud::query::query; 9 | use crate::crud::selector::Selector; 10 | use crate::fields::proxy; 11 | use crate::internal::field::SingleColumnField; 12 | use crate::model::Model; 13 | use crate::Patch; 14 | 15 | /// Alias for [ForeignModelByField] which only takes a model uses to its primary key. 16 | pub type ForeignModel = ForeignModelByField<::Primary>; 17 | 18 | /// Stores a link to another model in a field. 19 | /// 20 | /// In database language, this is a many to one relation. 21 | pub struct ForeignModelByField(pub FF::Type); 22 | 23 | impl ForeignModelByField { 24 | /// Queries the associated model 25 | pub async fn query(self, executor: impl Executor<'_>) -> Result { 26 | self.query_as(executor, ::ValueSpaceImpl::default()) 27 | .await 28 | } 29 | 30 | /// Queries the associated model using `selector` 31 | pub async fn query_as( 32 | self, 33 | executor: impl Executor<'_>, 34 | selector: S, 35 | ) -> Result 36 | where 37 | S: Selector, 38 | { 39 | query(executor, selector) 40 | .condition(self.into_condition()) 41 | .one() 42 | .await 43 | } 44 | 45 | /// Constructs a condition to query the associated model 46 | pub fn as_condition(&self) -> impl Condition<'_> { 47 | Binary { 48 | operator: BinaryOperator::Equals, 49 | fst_arg: Column(proxy::new::<(FF, FF::Model)>()), 50 | snd_arg: FF::type_as_value(&self.0), 51 | } 52 | } 53 | 54 | /// Constructs a condition to query the associated model 55 | pub fn into_condition<'a>(self) -> impl Condition<'a> 56 | where 57 | FF::Type: 'a, 58 | { 59 | Binary { 60 | operator: BinaryOperator::Equals, 61 | fst_arg: Column(proxy::new::<(FF, FF::Model)>()), 62 | snd_arg: FF::type_into_value(self.0), 63 | } 64 | } 65 | } 66 | 67 | impl fmt::Debug for ForeignModelByField 68 | where 69 | FF::Type: fmt::Debug, 70 | { 71 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 72 | f.debug_tuple("ForeignModelByField").field(&self.0).finish() 73 | } 74 | } 75 | impl Clone for ForeignModelByField 76 | where 77 | FF::Type: Clone, 78 | { 79 | fn clone(&self) -> Self { 80 | Self(self.0.clone()) 81 | } 82 | } 83 | impl Copy for ForeignModelByField where FF::Type: Copy {} 84 | -------------------------------------------------------------------------------- /src/fields/types/json.rs: -------------------------------------------------------------------------------- 1 | //! The [`Json`] wrapper to store json data in the db 2 | 3 | use std::borrow::Cow; 4 | use std::ops::{Deref, DerefMut}; 5 | 6 | use rorm_db::sql::value::NullType; 7 | use serde::de::DeserializeOwned; 8 | use serde::Serialize; 9 | 10 | use crate::conditions::Value; 11 | use crate::fields::traits::{Array, FieldColumns, FieldType}; 12 | use crate::fields::utils::check::shared_linter_check; 13 | use crate::fields::utils::get_annotations::forward_annotations; 14 | use crate::fields::utils::get_names::single_column_name; 15 | use crate::new_converting_decoder; 16 | 17 | /// Stores data by serializing it to json. 18 | /// 19 | /// This is just a convenience wrapper around [serde_json] and `Vec`. 20 | /// 21 | /// ```no_run 22 | /// # use std::collections::HashMap; 23 | /// use rorm::Model; 24 | /// use rorm::fields::types::Json; 25 | /// 26 | /// #[derive(Model)] 27 | /// pub struct Session { 28 | /// #[rorm(id)] 29 | /// pub id: i64, 30 | /// 31 | /// pub data: Json>, 32 | /// } 33 | /// ``` 34 | #[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] 35 | pub struct Json(pub T); 36 | 37 | impl Json { 38 | /// Unwrap into inner T value. 39 | pub fn into_inner(self) -> T { 40 | self.0 41 | } 42 | } 43 | 44 | new_converting_decoder!( 45 | pub JsonDecoder, 46 | |value: Vec| -> Json { 47 | serde_json::from_slice(&value) 48 | .map(Json) 49 | .map_err(|err| format!("Couldn't decoder json: {err}")) 50 | } 51 | ); 52 | impl FieldType for Json { 53 | type Columns = Array<1>; 54 | 55 | const NULL: FieldColumns = [NullType::Binary]; 56 | 57 | fn into_values<'a>(self) -> FieldColumns> { 58 | [Value::Binary(Cow::Owned( 59 | serde_json::to_vec(&self.0).unwrap(), 60 | ))] // TODO propagate error? 61 | } 62 | 63 | fn as_values(&self) -> FieldColumns> { 64 | [Value::Binary(Cow::Owned( 65 | serde_json::to_vec(&self.0).unwrap(), 66 | ))] // TODO propagate error? 67 | } 68 | 69 | type Decoder = JsonDecoder; 70 | 71 | type GetAnnotations = forward_annotations<1>; 72 | 73 | type Check = shared_linter_check<1>; 74 | 75 | type GetNames = single_column_name; 76 | } 77 | 78 | new_converting_decoder!( 79 | pub OptionJsonDecoder, 80 | |value: Option>| -> Option> { 81 | value 82 | .map(|value| { 83 | serde_json::from_slice(&value) 84 | .map(Json) 85 | .map_err(|err| format!("Couldn't decoder json: {err}")) 86 | }) 87 | .transpose() 88 | } 89 | ); 90 | 91 | // From 92 | impl From for Json { 93 | fn from(value: T) -> Self { 94 | Self(value) 95 | } 96 | } 97 | 98 | // Deref 99 | impl Deref for Json { 100 | type Target = T; 101 | 102 | fn deref(&self) -> &Self::Target { 103 | &self.0 104 | } 105 | } 106 | impl DerefMut for Json { 107 | fn deref_mut(&mut self) -> &mut Self::Target { 108 | &mut self.0 109 | } 110 | } 111 | 112 | // AsRef 113 | impl AsRef for Json { 114 | fn as_ref(&self) -> &T { 115 | &self.0 116 | } 117 | } 118 | impl AsMut for Json { 119 | fn as_mut(&mut self) -> &mut T { 120 | &mut self.0 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/fields/types/max_str_impl.rs: -------------------------------------------------------------------------------- 1 | //! Trait and some implementations used in [`MaxStr`](super::MaxStr) 2 | 3 | /// Implementation used by [`MaxStr`](super::MaxStr) to retrieve the wrapped string's length. 4 | /// 5 | /// - [`NumBytes`] uses the number of bytes (this is what [`str::len`] does) 6 | /// - [`NumChars`] uses the number of unicode code points 7 | pub trait LenImpl { 8 | /// Returns the string's length. 9 | fn len(&self, string: &str) -> usize; 10 | } 11 | 12 | /// [`LenImpl`] which uses the number of bytes (this is what [`str::len`] does) 13 | #[derive(Copy, Clone, Debug, Default)] 14 | pub struct NumBytes; 15 | 16 | impl LenImpl for NumBytes { 17 | fn len(&self, string: &str) -> usize { 18 | #[expect(clippy::needless_as_bytes, reason = "Makes the intent more explicit")] 19 | string.as_bytes().len() 20 | } 21 | } 22 | 23 | /// [`LenImpl`] which uses the number of unicode code points 24 | #[derive(Copy, Clone, Debug, Default)] 25 | pub struct NumChars; 26 | 27 | impl LenImpl for NumChars { 28 | fn len(&self, string: &str) -> usize { 29 | string.chars().count() 30 | } 31 | } 32 | 33 | impl LenImpl for &T { 34 | fn len(&self, string: &str) -> usize { 35 | T::len(self, string) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/fields/types/mod.rs: -------------------------------------------------------------------------------- 1 | //! Field types which are provided by `rorm` 2 | //! 3 | //! See [`rorm::fields`](crate::fields) for full list of supported field types 4 | 5 | mod back_ref; 6 | #[cfg(feature = "chrono")] 7 | mod chrono; 8 | mod foreign_model; 9 | mod json; 10 | mod max_str; 11 | pub mod max_str_impl; 12 | #[cfg(feature = "msgpack")] 13 | mod msgpack; 14 | #[cfg(feature = "postgres-only")] 15 | pub(crate) mod postgres_only; 16 | mod std; 17 | #[cfg(feature = "time")] 18 | mod time; 19 | #[cfg(feature = "url")] 20 | mod url; 21 | #[cfg(feature = "uuid")] 22 | mod uuid; 23 | 24 | pub use back_ref::BackRef; 25 | pub use foreign_model::{ForeignModel, ForeignModelByField}; 26 | pub use json::Json; 27 | pub use max_str::MaxStr; 28 | #[cfg(feature = "msgpack")] 29 | pub use msgpack::MsgPack; 30 | -------------------------------------------------------------------------------- /src/fields/types/msgpack.rs: -------------------------------------------------------------------------------- 1 | //! The [`MsgPack`] wrapper to store message pack data in the db 2 | 3 | use std::borrow::Cow; 4 | use std::ops::{Deref, DerefMut}; 5 | 6 | use rorm_db::sql::value::NullType; 7 | use serde::de::DeserializeOwned; 8 | use serde::Serialize; 9 | 10 | use crate::conditions::Value; 11 | use crate::fields::traits::{Array, FieldColumns, FieldType}; 12 | use crate::fields::utils::check::shared_linter_check; 13 | use crate::fields::utils::get_annotations::forward_annotations; 14 | use crate::fields::utils::get_names::single_column_name; 15 | use crate::new_converting_decoder; 16 | 17 | /// Stores data by serializing it to message pack. 18 | /// 19 | /// This is just a convenience wrapper around [rmp_serde] and `Vec`. 20 | /// 21 | /// ```no_run 22 | /// # use std::collections::HashMap; 23 | /// use rorm::Model; 24 | /// use rorm::fields::types::MsgPack; 25 | /// 26 | /// #[derive(Model)] 27 | /// pub struct Session { 28 | /// #[rorm(id)] 29 | /// pub id: i64, 30 | /// 31 | /// pub data: MsgPack>, 32 | /// } 33 | /// ``` 34 | #[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] 35 | pub struct MsgPack(pub T); 36 | 37 | impl MsgPack { 38 | /// Unwrap into inner T value. 39 | pub fn into_inner(self) -> T { 40 | self.0 41 | } 42 | } 43 | 44 | new_converting_decoder!( 45 | pub MsgPackDecoder, 46 | |value: Vec| -> MsgPack { 47 | rmp_serde::from_slice(&value) 48 | .map(MsgPack) 49 | .map_err(|err| format!("Couldn't decode msg pack: {err}")) 50 | } 51 | ); 52 | impl FieldType for MsgPack { 53 | type Columns = Array<1>; 54 | 55 | const NULL: FieldColumns = [NullType::Binary]; 56 | 57 | fn into_values<'a>(self) -> FieldColumns> { 58 | [Value::Binary(Cow::Owned( 59 | rmp_serde::to_vec(&self.0).unwrap(), // TODO propagate error? 60 | ))] 61 | } 62 | 63 | fn as_values(&self) -> FieldColumns> { 64 | [Value::Binary(Cow::Owned( 65 | rmp_serde::to_vec(&self.0).unwrap(), // TODO propagate error? 66 | ))] 67 | } 68 | 69 | type Decoder = MsgPackDecoder; 70 | 71 | type GetAnnotations = forward_annotations<1>; 72 | type Check = shared_linter_check<1>; 73 | type GetNames = single_column_name; 74 | } 75 | 76 | new_converting_decoder!( 77 | pub OptionMsgPackDecoder, 78 | |value: Option>| -> Option> { 79 | value 80 | .map(|value| { 81 | rmp_serde::from_slice(&value) 82 | .map(MsgPack) 83 | .map_err(|err| format!("Couldn't decode msg pack: {err}")) 84 | }) 85 | .transpose() 86 | } 87 | ); 88 | 89 | // From 90 | impl From for MsgPack { 91 | fn from(value: T) -> Self { 92 | Self(value) 93 | } 94 | } 95 | 96 | // Deref 97 | impl Deref for MsgPack { 98 | type Target = T; 99 | 100 | fn deref(&self) -> &Self::Target { 101 | &self.0 102 | } 103 | } 104 | impl DerefMut for MsgPack { 105 | fn deref_mut(&mut self) -> &mut Self::Target { 106 | &mut self.0 107 | } 108 | } 109 | 110 | // AsRef 111 | impl AsRef for MsgPack { 112 | fn as_ref(&self) -> &T { 113 | &self.0 114 | } 115 | } 116 | impl AsMut for MsgPack { 117 | fn as_mut(&mut self) -> &mut T { 118 | &mut self.0 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/fields/types/postgres_only.rs: -------------------------------------------------------------------------------- 1 | use bit_vec::BitVec; 2 | use ipnetwork::IpNetwork; 3 | use mac_address::MacAddress; 4 | 5 | use crate::conditions::Value; 6 | use crate::{impl_FieldEq, impl_FieldType}; 7 | 8 | impl_FieldType!(MacAddress, MacAddress, Value::MacAddress); 9 | impl_FieldEq!(impl<'rhs> FieldEq<'rhs, MacAddress> for MacAddress { Value::MacAddress }); 10 | 11 | impl_FieldType!(IpNetwork, IpNetwork, Value::IpNetwork); 12 | impl_FieldEq!(impl<'rhs> FieldEq<'rhs, IpNetwork> for IpNetwork { Value::IpNetwork }); 13 | 14 | impl_FieldType!( 15 | BitVec, 16 | BitVec, 17 | |vec| Value::BitVec(BitCow::Owned(vec)), 18 | |vec| Value::BitVec(BitCow::Borrowed(vec)) 19 | ); 20 | impl_FieldEq!(impl<'rhs> FieldEq<'rhs, &'rhs BitVec> for BitVec { |vec| Value::BitVec(BitCow::Borrowed(vec)) }); 21 | impl_FieldEq!(impl<'rhs> FieldEq<'rhs, BitVec> for BitVec { |vec| Value::BitVec(BitCow::Owned(vec)) }); 22 | 23 | #[derive(Clone, Debug)] 24 | pub enum BitCow<'a> { 25 | Borrowed(&'a BitVec), 26 | Owned(BitVec), 27 | } 28 | 29 | impl AsRef for BitCow<'_> { 30 | fn as_ref(&self) -> &BitVec { 31 | match self { 32 | BitCow::Borrowed(bit_vec) => bit_vec, 33 | BitCow::Owned(bit_vec) => bit_vec, 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/fields/types/std.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | 3 | use crate::conditions::Value; 4 | use crate::db::sql::value::NullType; 5 | use crate::fields::utils::check; 6 | use crate::{ 7 | impl_FieldEq, impl_FieldMin_FieldMax, impl_FieldOrd, impl_FieldSum_FieldAvg, impl_FieldType, 8 | }; 9 | 10 | impl_FieldType!(bool, Bool, Value::Bool); 11 | impl_FieldEq!(impl<'rhs> FieldEq<'rhs, bool> for bool { Value::Bool }); 12 | impl_FieldEq!(impl<'rhs> FieldEq<'rhs, Option> for Option { |option: Self| option.map(Value::Bool).unwrap_or(Value::Null(NullType::Bool)) }); 13 | 14 | impl_FieldType!(i16, I16, Value::I16); 15 | impl_FieldEq!(impl<'rhs> FieldEq<'rhs, i16> for i16 { Value::I16 }); 16 | impl_FieldEq!(impl<'rhs> FieldEq<'rhs, Option> for Option { |option: Self| option.map(Value::I16).unwrap_or(Value::Null(NullType::I16)) }); 17 | impl_FieldOrd!(i16, i16, Value::I16); 18 | impl_FieldOrd!(Option, Option, |option: Self| option 19 | .map(Value::I16) 20 | .unwrap_or(Value::Null(NullType::I16))); 21 | impl_FieldSum_FieldAvg!(i16, sum_result: i64); 22 | impl_FieldMin_FieldMax!(i16); 23 | 24 | impl_FieldType!(i32, I32, Value::I32); 25 | impl_FieldEq!(impl<'rhs> FieldEq<'rhs, i32> for i32 { Value::I32 }); 26 | impl_FieldEq!(impl<'rhs> FieldEq<'rhs, Option> for Option { |option: Self| option.map(Value::I32).unwrap_or(Value::Null(NullType::I32)) }); 27 | impl_FieldOrd!(i32, i32, Value::I32); 28 | impl_FieldOrd!(Option, Option, |option: Self| option 29 | .map(Value::I32) 30 | .unwrap_or(Value::Null(NullType::I32))); 31 | impl_FieldSum_FieldAvg!(i32, sum_result: i64); 32 | impl_FieldMin_FieldMax!(i32); 33 | 34 | impl_FieldType!(i64, I64, Value::I64); 35 | impl_FieldEq!(impl<'rhs> FieldEq<'rhs, i64> for i64 { Value::I64 }); 36 | impl_FieldEq!(impl<'rhs> FieldEq<'rhs, Option> for Option { |option: Self| option.map(Value::I64).unwrap_or(Value::Null(NullType::I64)) }); 37 | impl_FieldOrd!(i64, i64, Value::I64); 38 | impl_FieldOrd!(Option, Option, |option: Self| option 39 | .map(Value::I64) 40 | .unwrap_or(Value::Null(NullType::I64))); 41 | impl_FieldSum_FieldAvg!(i64, sum_result: f64); 42 | impl_FieldMin_FieldMax!(i64); 43 | 44 | impl_FieldType!(f32, F32, Value::F32); 45 | impl_FieldEq!(impl<'rhs> FieldEq<'rhs, f32> for f32 { Value::F32 }); 46 | impl_FieldEq!(impl<'rhs> FieldEq<'rhs, Option> for Option { |option: Self| option.map(Value::F32).unwrap_or(Value::Null(NullType::F32)) }); 47 | impl_FieldOrd!(f32, f32, Value::F32); 48 | impl_FieldOrd!(Option, Option, |option: Self| option 49 | .map(Value::F32) 50 | .unwrap_or(Value::Null(NullType::F32))); 51 | impl_FieldSum_FieldAvg!(f32, sum_result: f32); 52 | impl_FieldMin_FieldMax!(f32); 53 | 54 | impl_FieldType!(f64, F64, Value::F64); 55 | impl_FieldEq!(impl<'rhs> FieldEq<'rhs, f64> for f64 { Value::F64 }); 56 | impl_FieldEq!(impl<'rhs> FieldEq<'rhs, Option> for Option { |option: Self| option.map(Value::F64).unwrap_or(Value::Null(NullType::F64)) }); 57 | impl_FieldOrd!(f64, f64, Value::F64); 58 | impl_FieldOrd!(Option, Option, |option: Self| option 59 | .map(Value::F64) 60 | .unwrap_or(Value::Null(NullType::F64))); 61 | impl_FieldSum_FieldAvg!(f64, sum_result: f64); 62 | impl_FieldMin_FieldMax!(f64); 63 | 64 | impl_FieldType!( 65 | String, 66 | String, 67 | conv_string, 68 | conv_string, 69 | check::string_check 70 | ); 71 | impl_FieldEq!(impl<'rhs> FieldEq<'rhs, &'rhs str> for String { conv_string }); 72 | impl_FieldEq!(impl<'rhs> FieldEq<'rhs, &'rhs String> for String { conv_string }); 73 | impl_FieldEq!(impl<'rhs> FieldEq<'rhs, String> for String { conv_string }); 74 | impl_FieldEq!(impl<'rhs> FieldEq<'rhs, Cow<'rhs, str>> for String { conv_string }); 75 | impl_FieldEq!(impl<'rhs> FieldEq<'rhs, Option<&'rhs str>> for Option { |option: Option<_>| option.map(conv_string).unwrap_or(Value::Null(NullType::String)) }); 76 | impl_FieldEq!(impl<'rhs> FieldEq<'rhs, Option<&'rhs String>> for Option { |option: Option<_>| option.map(conv_string).unwrap_or(Value::Null(NullType::String)) }); 77 | impl_FieldEq!(impl<'rhs> FieldEq<'rhs, Option> for Option { |option: Option<_>| option.map(conv_string).unwrap_or(Value::Null(NullType::String)) }); 78 | impl_FieldEq!(impl<'rhs> FieldEq<'rhs, Option>> for Option { |option: Option<_>| option.map(conv_string).unwrap_or(Value::Null(NullType::String)) }); 79 | impl_FieldOrd!(String, &'rhs str, conv_string); 80 | impl_FieldOrd!(String, &'rhs String, conv_string); 81 | impl_FieldOrd!(String, String, conv_string); 82 | impl_FieldOrd!(String, Cow<'rhs, str>, conv_string); 83 | impl_FieldMin_FieldMax!(String); 84 | fn conv_string<'a>(value: impl Into>) -> Value<'a> { 85 | Value::String(value.into()) 86 | } 87 | 88 | impl_FieldType!(Vec, Binary, conv_bytes, conv_bytes); 89 | impl_FieldEq!(impl<'rhs> FieldEq<'rhs, &'rhs [u8]> for Vec { conv_bytes }); 90 | impl_FieldEq!(impl<'rhs> FieldEq<'rhs, &'rhs Vec> for Vec { conv_bytes }); 91 | impl_FieldEq!(impl<'rhs> FieldEq<'rhs, Vec> for Vec { conv_bytes }); 92 | impl_FieldEq!(impl<'rhs> FieldEq<'rhs, Cow<'rhs, [u8]>> for Vec { conv_bytes }); 93 | impl_FieldEq!(impl<'rhs> FieldEq<'rhs, Option<&'rhs [u8]>> for Option> { |option: Option<_>| option.map(conv_bytes).unwrap_or(Value::Null(NullType::Binary)) }); 94 | impl_FieldEq!(impl<'rhs> FieldEq<'rhs, Option<&'rhs Vec>> for Option> { |option: Option<_>| option.map(conv_bytes).unwrap_or(Value::Null(NullType::Binary)) }); 95 | impl_FieldEq!(impl<'rhs> FieldEq<'rhs, Option>> for Option> { |option: Option<_>| option.map(conv_bytes).unwrap_or(Value::Null(NullType::Binary)) }); 96 | impl_FieldEq!(impl<'rhs> FieldEq<'rhs, Option>> for Option> { |option: Option<_>| option.map(conv_bytes).unwrap_or(Value::Null(NullType::Binary)) }); 97 | impl_FieldOrd!(Vec, &'rhs [u8], conv_bytes); 98 | impl_FieldOrd!(Vec, &'rhs Vec, conv_bytes); 99 | impl_FieldOrd!(Vec, Vec, conv_bytes); 100 | impl_FieldOrd!(Vec, Cow<'rhs, [u8]>, conv_bytes); 101 | fn conv_bytes<'a>(value: impl Into>) -> Value<'a> { 102 | Value::Binary(value.into()) 103 | } 104 | -------------------------------------------------------------------------------- /src/fields/types/time.rs: -------------------------------------------------------------------------------- 1 | use rorm_db::sql::value::NullType; 2 | use time::{Date, OffsetDateTime, PrimitiveDateTime, Time}; 3 | 4 | use crate::conditions::Value; 5 | use crate::{impl_FieldEq, impl_FieldMin_FieldMax, impl_FieldOrd, impl_FieldType}; 6 | 7 | impl_FieldType!(Time, TimeTime, Value::TimeTime); 8 | impl_FieldEq!(impl<'rhs> FieldEq<'rhs, Time> for Time { Value::TimeTime }); 9 | impl_FieldEq!(impl<'rhs> FieldEq<'rhs, Option

::Model)>, Value<'a>>; 59 | 60 | /// Trait implementing most database interactions for a struct. 61 | /// 62 | /// It should only ever be generated using [`derive(Model)`](rorm_macro::Model). 63 | pub trait Model: Patch { 64 | /// The primary key 65 | type Primary: Field + SingleColumnField; 66 | 67 | /// A struct which "maps" field identifiers their descriptions (i.e. [`Field`](crate::internal::field::Field)). 68 | /// 69 | /// The struct is constructed once in the [`Model::FIELDS`] constant. 70 | type Fields: ConstNew; 71 | 72 | /// A constant struct which "maps" field identifiers their descriptions (i.e. [`Field`](crate::internal::field::Field)). 73 | const FIELDS: Self::Fields; 74 | 75 | /// Shorthand version of [`FIELDS`] 76 | /// 77 | /// [`FIELDS`]: Model::FIELDS 78 | #[deprecated(note = "Use `Model.field` instead of `Model::F.field`")] 79 | const F: Self::Fields; 80 | 81 | /// The model's table name 82 | const TABLE: &'static str; 83 | 84 | /// Location of the model in the source code 85 | const SOURCE: Source; 86 | 87 | /// Push the model's fields' imr representation onto a vec 88 | fn push_fields_imr(fields: &mut Vec); 89 | 90 | /// Returns the model's intermediate representation 91 | /// 92 | /// As library user you probably won't need this. You might want to look at [`write_models`]. 93 | /// 94 | /// [`write_models`]: crate::write_models 95 | fn get_imr() -> imr::Model { 96 | let mut fields = Vec::new(); 97 | Self::push_fields_imr(&mut fields); 98 | imr::Model { 99 | name: Self::TABLE.to_string(), 100 | fields, 101 | source_defined_at: Some(Self::SOURCE.as_imr()), 102 | } 103 | } 104 | } 105 | 106 | /// Expose a models' fields on the type level using indexes 107 | pub trait FieldByIndex: Model { 108 | /// The model's field at `INDEX` 109 | type Field: Field; 110 | } 111 | 112 | /// Generic access to a patch's fields 113 | /// 114 | /// This enables generic code to check if a patch contains a certain field 115 | /// (for example the model's primary key, see [Identifiable]) 116 | /// and gain access to it. 117 | pub trait GetField: Patch { 118 | /// Take the field by ownership 119 | fn get_field(self) -> F::Type; 120 | 121 | /// Borrow the field 122 | fn borrow_field(&self) -> &F::Type; 123 | 124 | /// Borrow the field mutably 125 | fn borrow_field_mut(&mut self) -> &mut F::Type; 126 | } 127 | 128 | /// Update a model's field based on the model's primary key 129 | /// 130 | /// This trait is similar to [`GetField::borrow_field_mut`]. 131 | /// But [`GetField::borrow_field_mut`] only allows access to one field at a time, 132 | /// because the method hides the fact, that the mutual borrow only applies to a single field. 133 | /// This trait provides a solution to this problem, for a common scenario: 134 | /// The need for an additional immutable borrow to the primary key. 135 | pub trait UpdateField>: Model { 136 | /// Update a model's field based on the model's primary key 137 | fn update_field<'m, T>( 138 | &'m mut self, 139 | update: impl FnOnce(&'m <::Primary as Field>::Type, &'m mut F::Type) -> T, 140 | ) -> T; 141 | } 142 | 143 | /// A patch which contains its model's primary key. 144 | pub trait Identifiable: Patch { 145 | /// Get a reference to the primary key 146 | fn get_primary_key(&self) -> &<::Primary as Field>::Type; 147 | 148 | /// Build a [Condition](crate::conditions::Condition) 149 | /// which only applies to this instance by comparing the primary key. 150 | fn as_condition(&self) -> PatchAsCondition { 151 | Binary { 152 | operator: BinaryOperator::Equals, 153 | fst_arg: Column(proxy::new()), 154 | snd_arg: ::Primary::type_as_value(self.get_primary_key()), 155 | } 156 | } 157 | } 158 | 159 | impl + GetField> Identifiable for P { 160 | fn get_primary_key(&self) -> &::Type { 161 | >::borrow_field(self) 162 | } 163 | } 164 | 165 | /// exposes a `NEW` constant, which act like [Default::default] but constant. 166 | /// 167 | /// It's workaround for not having const methods in traits 168 | pub trait ConstNew: 'static { 169 | /// A new or default instance 170 | const NEW: Self; 171 | 172 | /// A static reference to an default instance 173 | /// 174 | /// Sadly writing `const REF: &'static Self = &Self::NEW;` doesn't work for all `Self`. 175 | /// Rust doesn't allow references to types with interior mutability to be stored in constants. 176 | /// Since this can't be enforced by generic, `ConstNew` impls have to write this line themselves. 177 | const REF: &'static Self; 178 | } 179 | -------------------------------------------------------------------------------- /tests/data/derives/basic.rs: -------------------------------------------------------------------------------- 1 | use rorm::DbEnum; 2 | use rorm::Model; 3 | use rorm::Patch; 4 | 5 | #[derive(Model)] 6 | pub struct BasicModel { 7 | #[rorm(id)] 8 | pub id: i64, 9 | } 10 | 11 | #[derive(Patch)] 12 | #[rorm(model = "BasicModel")] 13 | pub struct BasicPatch {} 14 | 15 | #[derive(DbEnum)] 16 | enum BasicEnum { 17 | Foo, 18 | Bar, 19 | Baz, 20 | } 21 | 22 | fn main() {} 23 | -------------------------------------------------------------------------------- /tests/data/derives/basic_expansions/BasicEnum.rs: -------------------------------------------------------------------------------- 1 | const _: () = { 2 | const CHOICES: &'static [&'static str] = &[ 3 | stringify!(Foo), 4 | stringify!(Bar), 5 | stringify!(Baz), 6 | ]; 7 | impl ::rorm::fields::traits::FieldType for BasicEnum { 8 | type Columns = ::rorm::fields::traits::Array<1>; 9 | const NULL: ::rorm::fields::traits::FieldColumns< 10 | Self, 11 | ::rorm::db::sql::value::NullType, 12 | > = [::rorm::db::sql::value::NullType::Choice]; 13 | fn into_values<'a>( 14 | self, 15 | ) -> ::rorm::fields::traits::FieldColumns> { 16 | [ 17 | ::rorm::conditions::Value::Choice( 18 | ::std::borrow::Cow::Borrowed( 19 | match self { 20 | Self::Foo => stringify!(Foo), 21 | Self::Bar => stringify!(Bar), 22 | Self::Baz => stringify!(Baz), 23 | }, 24 | ), 25 | ), 26 | ] 27 | } 28 | fn as_values( 29 | &self, 30 | ) -> ::rorm::fields::traits::FieldColumns> { 31 | [ 32 | ::rorm::conditions::Value::Choice( 33 | ::std::borrow::Cow::Borrowed( 34 | match self { 35 | Self::Foo => stringify!(Foo), 36 | Self::Bar => stringify!(Bar), 37 | Self::Baz => stringify!(Baz), 38 | }, 39 | ), 40 | ), 41 | ] 42 | } 43 | type Decoder = __BasicEnum_Decoder; 44 | type GetAnnotations = get_db_enum_annotations; 45 | type Check = ::rorm::fields::utils::check::shared_linter_check<1>; 46 | type GetNames = ::rorm::fields::utils::get_names::single_column_name; 47 | } 48 | ::rorm::new_converting_decoder!( 49 | #[doc(hidden)] __BasicEnum_Decoder, | value : ::rorm::db::choice::Choice | -> 50 | BasicEnum { let value : String = value.0; match value.as_str() { stringify!(Foo) 51 | => Ok(BasicEnum::Foo), stringify!(Bar) => Ok(BasicEnum::Bar), stringify!(Baz) => 52 | Ok(BasicEnum::Baz), _ => Err(format!("Invalid value '{}' for enum '{}'", value, 53 | stringify!(BasicEnum))), } } 54 | ); 55 | ::rorm::impl_FieldEq!( 56 | impl < 'rhs > FieldEq < 'rhs, BasicEnum > for BasicEnum { | value : BasicEnum | { 57 | let [value] = < BasicEnum as ::rorm::fields::traits::FieldType > 58 | ::into_values(value); value } } 59 | ); 60 | ::rorm::const_fn! { 61 | pub fn get_db_enum_annotations(field : 62 | ::rorm::internal::hmr::annotations::Annotations) -> 63 | [::rorm::internal::hmr::annotations::Annotations; 1] { let mut field = field; 64 | field.choices = Some(::rorm::internal::hmr::annotations::Choices(CHOICES)); 65 | [field] } 66 | } 67 | }; 68 | -------------------------------------------------------------------------------- /tests/data/derives/basic_expansions/BasicModel.rs: -------------------------------------------------------------------------------- 1 | ///rorm's representation of [`BasicModel`]'s `id` field 2 | #[allow(non_camel_case_types)] 3 | pub struct __BasicModel_id(::std::marker::PhantomData<()>); 4 | impl ::std::clone::Clone for __BasicModel_id { 5 | fn clone(&self) -> Self { 6 | *self 7 | } 8 | } 9 | impl ::std::marker::Copy for __BasicModel_id {} 10 | impl ::rorm::internal::field::Field for __BasicModel_id { 11 | type Type = i64; 12 | type Model = BasicModel; 13 | const INDEX: usize = 0usize; 14 | const NAME: &'static str = "id"; 15 | const EXPLICIT_ANNOTATIONS: ::rorm::internal::hmr::annotations::Annotations = ::rorm::internal::hmr::annotations::Annotations { 16 | auto_create_time: None, 17 | auto_update_time: None, 18 | auto_increment: Some(::rorm::internal::hmr::annotations::AutoIncrement), 19 | choices: None, 20 | default: None, 21 | index: None, 22 | max_length: None, 23 | on_delete: None, 24 | on_update: None, 25 | primary_key: Some(::rorm::internal::hmr::annotations::PrimaryKey), 26 | unique: None, 27 | nullable: false, 28 | foreign: None, 29 | }; 30 | const SOURCE: ::rorm::internal::hmr::Source = ::rorm::internal::hmr::Source { 31 | file: ::std::file!(), 32 | line: ::std::line!() as usize, 33 | column: ::std::column!() as usize, 34 | }; 35 | fn new() -> Self { 36 | Self(::std::marker::PhantomData) 37 | } 38 | } 39 | const _: () = { 40 | if let Err(err) = ::rorm::internal::field::check::<__BasicModel_id>() { 41 | panic!("{}", err.as_str()); 42 | } 43 | }; 44 | ///[`BasicModel`]'s [`Fields`](:: rorm::model::Model::Fields) struct. 45 | #[allow(non_camel_case_types)] 46 | pub struct __BasicModel_Fields_Struct { 47 | ///[`BasicModel`]'s `id` field 48 | pub id: ::rorm::fields::proxy::FieldProxy<(__BasicModel_id, Path)>, 49 | } 50 | impl ::rorm::model::ConstNew 51 | for __BasicModel_Fields_Struct { 52 | const NEW: Self = Self { 53 | id: ::rorm::fields::proxy::new(), 54 | }; 55 | const REF: &'static Self = &Self::NEW; 56 | } 57 | impl ::std::ops::Deref for __BasicModel_ValueSpaceImpl { 58 | type Target = ::Fields; 59 | fn deref(&self) -> &Self::Target { 60 | ::rorm::model::ConstNew::REF 61 | } 62 | } 63 | impl ::rorm::model::Model for BasicModel { 64 | type Primary = __BasicModel_id; 65 | type Fields = __BasicModel_Fields_Struct< 66 | P, 67 | >; 68 | const F: __BasicModel_Fields_Struct = ::rorm::model::ConstNew::NEW; 69 | const FIELDS: __BasicModel_Fields_Struct = ::rorm::model::ConstNew::NEW; 70 | const TABLE: &'static str = "basicmodel"; 71 | const SOURCE: ::rorm::internal::hmr::Source = ::rorm::internal::hmr::Source { 72 | file: ::std::file!(), 73 | line: ::std::line!() as usize, 74 | column: ::std::column!() as usize, 75 | }; 76 | fn push_fields_imr(fields: &mut Vec<::rorm::imr::Field>) { 77 | ::rorm::internal::field::push_imr::<__BasicModel_id>(&mut *fields); 78 | } 79 | } 80 | #[doc(hidden)] 81 | #[allow(non_camel_case_types)] 82 | pub enum __BasicModel_ValueSpaceImpl { 83 | BasicModel, 84 | #[allow(dead_code)] 85 | #[doc(hidden)] 86 | __BasicModel_ValueSpaceImplMarker(::std::marker::PhantomData), 87 | } 88 | pub use __BasicModel_ValueSpaceImpl::*; 89 | #[doc(hidden)] 90 | pub struct __BasicModel_Decoder { 91 | id: ::Decoder, 92 | } 93 | impl ::rorm::crud::selector::Selector for __BasicModel_ValueSpaceImpl { 94 | type Result = BasicModel; 95 | type Model = BasicModel; 96 | type Decoder = __BasicModel_Decoder; 97 | const INSERT_COMPATIBLE: bool = true; 98 | fn select( 99 | self, 100 | ctx: &mut ::rorm::internal::query_context::QueryContext, 101 | ) -> Self::Decoder { 102 | __BasicModel_Decoder { 103 | id: ::FIELDS.id.select(&mut *ctx), 104 | } 105 | } 106 | } 107 | impl ::std::default::Default for __BasicModel_ValueSpaceImpl { 108 | fn default() -> Self { 109 | Self::BasicModel 110 | } 111 | } 112 | impl ::rorm::crud::decoder::Decoder for __BasicModel_Decoder { 113 | type Result = BasicModel; 114 | fn by_name<'index>( 115 | &'index self, 116 | row: &'_ ::rorm::db::Row, 117 | ) -> Result> { 118 | Ok(BasicModel { 119 | id: self.id.by_name(row)?, 120 | }) 121 | } 122 | fn by_index<'index>( 123 | &'index self, 124 | row: &'_ ::rorm::db::Row, 125 | ) -> Result> { 126 | Ok(BasicModel { 127 | id: self.id.by_index(row)?, 128 | }) 129 | } 130 | } 131 | impl ::rorm::model::Patch for BasicModel { 132 | type Model = BasicModel; 133 | type ValueSpaceImpl = __BasicModel_ValueSpaceImpl; 134 | fn push_columns(columns: &mut Vec<::rorm::fields::utils::column_name::ColumnName>) { 135 | columns 136 | .extend( 137 | ::rorm::fields::proxy::columns(|| { 138 | <::Model as ::rorm::model::Model>::FIELDS 139 | .id 140 | }), 141 | ); 142 | } 143 | fn push_references<'a>(&'a self, values: &mut Vec<::rorm::conditions::Value<'a>>) { 144 | values.extend(::rorm::fields::traits::FieldType::as_values(&self.id)); 145 | } 146 | fn push_values(self, values: &mut Vec<::rorm::conditions::Value>) { 147 | values.extend(::rorm::fields::traits::FieldType::into_values(self.id)); 148 | } 149 | } 150 | impl<'a> ::rorm::internal::patch::IntoPatchCow<'a> for BasicModel { 151 | type Patch = BasicModel; 152 | fn into_patch_cow(self) -> ::rorm::internal::patch::PatchCow<'a, BasicModel> { 153 | ::rorm::internal::patch::PatchCow::Owned(self) 154 | } 155 | } 156 | impl<'a> ::rorm::internal::patch::IntoPatchCow<'a> for &'a BasicModel { 157 | type Patch = BasicModel; 158 | fn into_patch_cow(self) -> ::rorm::internal::patch::PatchCow<'a, BasicModel> { 159 | ::rorm::internal::patch::PatchCow::Borrowed(self) 160 | } 161 | } 162 | const _: () = { 163 | #[::rorm::linkme::distributed_slice(::rorm::MODELS)] 164 | #[linkme(crate = ::rorm::linkme)] 165 | static __get_imr: fn() -> ::rorm::imr::Model = ::get_imr; 166 | let mut count_auto_increment = 0; 167 | let mut annos_slice = <__BasicModel_id as ::rorm::internal::field::Field>::EFFECTIVE_ANNOTATIONS 168 | .as_slice(); 169 | while let [annos, tail @ ..] = annos_slice { 170 | annos_slice = tail; 171 | if annos.auto_increment.is_some() { 172 | count_auto_increment += 1; 173 | } 174 | } 175 | assert!( 176 | count_auto_increment <= 1, "\"auto_increment\" can only be set once per model" 177 | ); 178 | }; 179 | impl ::rorm::model::FieldByIndex<{ 0usize }> for BasicModel { 180 | type Field = __BasicModel_id; 181 | } 182 | impl ::rorm::model::GetField<__BasicModel_id> for BasicModel { 183 | fn get_field(self) -> i64 { 184 | self.id 185 | } 186 | fn borrow_field(&self) -> &i64 { 187 | &self.id 188 | } 189 | fn borrow_field_mut(&mut self) -> &mut i64 { 190 | &mut self.id 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /tests/data/derives/basic_expansions/BasicPatch.rs: -------------------------------------------------------------------------------- 1 | #[doc(hidden)] 2 | #[allow(non_camel_case_types)] 3 | pub enum __BasicPatch_ValueSpaceImpl { 4 | BasicPatch, 5 | #[allow(dead_code)] 6 | #[doc(hidden)] 7 | __BasicPatch_ValueSpaceImplMarker(::std::marker::PhantomData), 8 | } 9 | pub use __BasicPatch_ValueSpaceImpl::*; 10 | #[doc(hidden)] 11 | pub struct __BasicPatch_Decoder {} 12 | impl ::rorm::crud::selector::Selector for __BasicPatch_ValueSpaceImpl { 13 | type Result = BasicPatch; 14 | type Model = BasicModel; 15 | type Decoder = __BasicPatch_Decoder; 16 | const INSERT_COMPATIBLE: bool = true; 17 | fn select( 18 | self, 19 | ctx: &mut ::rorm::internal::query_context::QueryContext, 20 | ) -> Self::Decoder { 21 | __BasicPatch_Decoder {} 22 | } 23 | } 24 | impl ::std::default::Default for __BasicPatch_ValueSpaceImpl { 25 | fn default() -> Self { 26 | Self::BasicPatch 27 | } 28 | } 29 | impl ::rorm::crud::decoder::Decoder for __BasicPatch_Decoder { 30 | type Result = BasicPatch; 31 | fn by_name<'index>( 32 | &'index self, 33 | row: &'_ ::rorm::db::Row, 34 | ) -> Result> { 35 | Ok(BasicPatch {}) 36 | } 37 | fn by_index<'index>( 38 | &'index self, 39 | row: &'_ ::rorm::db::Row, 40 | ) -> Result> { 41 | Ok(BasicPatch {}) 42 | } 43 | } 44 | impl ::rorm::model::Patch for BasicPatch { 45 | type Model = BasicModel; 46 | type ValueSpaceImpl = __BasicPatch_ValueSpaceImpl; 47 | fn push_columns(columns: &mut Vec<::rorm::fields::utils::column_name::ColumnName>) {} 48 | fn push_references<'a>(&'a self, values: &mut Vec<::rorm::conditions::Value<'a>>) {} 49 | fn push_values(self, values: &mut Vec<::rorm::conditions::Value>) {} 50 | } 51 | impl<'a> ::rorm::internal::patch::IntoPatchCow<'a> for BasicPatch { 52 | type Patch = BasicPatch; 53 | fn into_patch_cow(self) -> ::rorm::internal::patch::PatchCow<'a, BasicPatch> { 54 | ::rorm::internal::patch::PatchCow::Owned(self) 55 | } 56 | } 57 | impl<'a> ::rorm::internal::patch::IntoPatchCow<'a> for &'a BasicPatch { 58 | type Patch = BasicPatch; 59 | fn into_patch_cow(self) -> ::rorm::internal::patch::PatchCow<'a, BasicPatch> { 60 | ::rorm::internal::patch::PatchCow::Borrowed(self) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /tests/data/derives/experimental.rs: -------------------------------------------------------------------------------- 1 | #[derive(rorm::Model)] 2 | #[rorm(experimental_unregistered)] 3 | pub struct Unregistered { 4 | #[rorm(id)] 5 | pub id: i64, 6 | } 7 | 8 | #[derive(rorm::Model)] 9 | #[rorm(experimental_generics, experimental_unregistered)] 10 | pub struct Generic { 11 | #[rorm(id)] 12 | pub id: i64, 13 | 14 | pub x: X, 15 | } 16 | 17 | fn main() {} 18 | -------------------------------------------------------------------------------- /tests/data/derives/experimental_expansions/Unregistered.rs: -------------------------------------------------------------------------------- 1 | ///rorm's representation of [`Unregistered`]'s `id` field 2 | #[allow(non_camel_case_types)] 3 | pub struct __Unregistered_id(::std::marker::PhantomData<()>); 4 | impl ::std::clone::Clone for __Unregistered_id { 5 | fn clone(&self) -> Self { 6 | *self 7 | } 8 | } 9 | impl ::std::marker::Copy for __Unregistered_id {} 10 | impl ::rorm::internal::field::Field for __Unregistered_id { 11 | type Type = i64; 12 | type Model = Unregistered; 13 | const INDEX: usize = 0usize; 14 | const NAME: &'static str = "id"; 15 | const EXPLICIT_ANNOTATIONS: ::rorm::internal::hmr::annotations::Annotations = ::rorm::internal::hmr::annotations::Annotations { 16 | auto_create_time: None, 17 | auto_update_time: None, 18 | auto_increment: Some(::rorm::internal::hmr::annotations::AutoIncrement), 19 | choices: None, 20 | default: None, 21 | index: None, 22 | max_length: None, 23 | on_delete: None, 24 | on_update: None, 25 | primary_key: Some(::rorm::internal::hmr::annotations::PrimaryKey), 26 | unique: None, 27 | nullable: false, 28 | foreign: None, 29 | }; 30 | const SOURCE: ::rorm::internal::hmr::Source = ::rorm::internal::hmr::Source { 31 | file: ::std::file!(), 32 | line: ::std::line!() as usize, 33 | column: ::std::column!() as usize, 34 | }; 35 | fn new() -> Self { 36 | Self(::std::marker::PhantomData) 37 | } 38 | } 39 | ///[`Unregistered`]'s [`Fields`](:: rorm::model::Model::Fields) struct. 40 | #[allow(non_camel_case_types)] 41 | pub struct __Unregistered_Fields_Struct { 42 | ///[`Unregistered`]'s `id` field 43 | pub id: ::rorm::fields::proxy::FieldProxy<(__Unregistered_id, Path)>, 44 | } 45 | impl ::rorm::model::ConstNew 46 | for __Unregistered_Fields_Struct { 47 | const NEW: Self = Self { 48 | id: ::rorm::fields::proxy::new(), 49 | }; 50 | const REF: &'static Self = &Self::NEW; 51 | } 52 | impl ::std::ops::Deref for __Unregistered_ValueSpaceImpl { 53 | type Target = ::Fields; 54 | fn deref(&self) -> &Self::Target { 55 | ::rorm::model::ConstNew::REF 56 | } 57 | } 58 | impl ::rorm::model::Model for Unregistered { 59 | type Primary = __Unregistered_id; 60 | type Fields = __Unregistered_Fields_Struct< 61 | P, 62 | >; 63 | const F: __Unregistered_Fields_Struct = ::rorm::model::ConstNew::NEW; 64 | const FIELDS: __Unregistered_Fields_Struct = ::rorm::model::ConstNew::NEW; 65 | const TABLE: &'static str = "unregistered"; 66 | const SOURCE: ::rorm::internal::hmr::Source = ::rorm::internal::hmr::Source { 67 | file: ::std::file!(), 68 | line: ::std::line!() as usize, 69 | column: ::std::column!() as usize, 70 | }; 71 | fn push_fields_imr(fields: &mut Vec<::rorm::imr::Field>) { 72 | ::rorm::internal::field::push_imr::<__Unregistered_id>(&mut *fields); 73 | } 74 | } 75 | #[doc(hidden)] 76 | #[allow(non_camel_case_types)] 77 | pub enum __Unregistered_ValueSpaceImpl { 78 | Unregistered, 79 | #[allow(dead_code)] 80 | #[doc(hidden)] 81 | __Unregistered_ValueSpaceImplMarker(::std::marker::PhantomData), 82 | } 83 | pub use __Unregistered_ValueSpaceImpl::*; 84 | #[doc(hidden)] 85 | pub struct __Unregistered_Decoder { 86 | id: ::Decoder, 87 | } 88 | impl ::rorm::crud::selector::Selector for __Unregistered_ValueSpaceImpl { 89 | type Result = Unregistered; 90 | type Model = Unregistered; 91 | type Decoder = __Unregistered_Decoder; 92 | const INSERT_COMPATIBLE: bool = true; 93 | fn select( 94 | self, 95 | ctx: &mut ::rorm::internal::query_context::QueryContext, 96 | ) -> Self::Decoder { 97 | __Unregistered_Decoder { 98 | id: ::FIELDS.id.select(&mut *ctx), 99 | } 100 | } 101 | } 102 | impl ::std::default::Default for __Unregistered_ValueSpaceImpl { 103 | fn default() -> Self { 104 | Self::Unregistered 105 | } 106 | } 107 | impl ::rorm::crud::decoder::Decoder for __Unregistered_Decoder { 108 | type Result = Unregistered; 109 | fn by_name<'index>( 110 | &'index self, 111 | row: &'_ ::rorm::db::Row, 112 | ) -> Result> { 113 | Ok(Unregistered { 114 | id: self.id.by_name(row)?, 115 | }) 116 | } 117 | fn by_index<'index>( 118 | &'index self, 119 | row: &'_ ::rorm::db::Row, 120 | ) -> Result> { 121 | Ok(Unregistered { 122 | id: self.id.by_index(row)?, 123 | }) 124 | } 125 | } 126 | impl ::rorm::model::Patch for Unregistered { 127 | type Model = Unregistered; 128 | type ValueSpaceImpl = __Unregistered_ValueSpaceImpl; 129 | fn push_columns(columns: &mut Vec<::rorm::fields::utils::column_name::ColumnName>) { 130 | columns 131 | .extend( 132 | ::rorm::fields::proxy::columns(|| { 133 | <::Model as ::rorm::model::Model>::FIELDS 134 | .id 135 | }), 136 | ); 137 | } 138 | fn push_references<'a>(&'a self, values: &mut Vec<::rorm::conditions::Value<'a>>) { 139 | values.extend(::rorm::fields::traits::FieldType::as_values(&self.id)); 140 | } 141 | fn push_values(self, values: &mut Vec<::rorm::conditions::Value>) { 142 | values.extend(::rorm::fields::traits::FieldType::into_values(self.id)); 143 | } 144 | } 145 | impl<'a> ::rorm::internal::patch::IntoPatchCow<'a> for Unregistered { 146 | type Patch = Unregistered; 147 | fn into_patch_cow(self) -> ::rorm::internal::patch::PatchCow<'a, Unregistered> { 148 | ::rorm::internal::patch::PatchCow::Owned(self) 149 | } 150 | } 151 | impl<'a> ::rorm::internal::patch::IntoPatchCow<'a> for &'a Unregistered { 152 | type Patch = Unregistered; 153 | fn into_patch_cow(self) -> ::rorm::internal::patch::PatchCow<'a, Unregistered> { 154 | ::rorm::internal::patch::PatchCow::Borrowed(self) 155 | } 156 | } 157 | impl ::rorm::model::FieldByIndex<{ 0usize }> for Unregistered { 158 | type Field = __Unregistered_id; 159 | } 160 | impl ::rorm::model::GetField<__Unregistered_id> for Unregistered { 161 | fn get_field(self) -> i64 { 162 | self.id 163 | } 164 | fn borrow_field(&self) -> &i64 { 165 | &self.id 166 | } 167 | fn borrow_field_mut(&mut self) -> &mut i64 { 168 | &mut self.id 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /tests/derives.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | use std::{env, fmt, fs}; 3 | 4 | use datatest_stable::{harness, Result, Utf8Path}; 5 | use proc_macro2::{Ident, TokenStream}; 6 | use syn::__private::ToTokens; 7 | 8 | harness! { 9 | { test = compile, root = "tests/data/derives/", pattern = "^[^/]+$" }, 10 | { test = expand, root = "tests/data/derives/", pattern = "^[^/]+$" }, 11 | } 12 | 13 | fn compile(path: &Path) -> Result<()> { 14 | trybuild::TestCases::new().pass(path); 15 | Ok(()) 16 | } 17 | 18 | fn expand(input_file: &Utf8Path, input_str: String) -> Result<()> { 19 | let expansions_dir = 20 | input_file.with_file_name(format!("{}_expansions", input_file.file_stem().unwrap())); 21 | 22 | let input_ast = syn::parse_file(&input_str).context("Failed to parse input")?; 23 | 24 | for item in &input_ast.items { 25 | let Some((ident, derive_fn)) = get_derive_fn(item)? else { 26 | continue; 27 | }; 28 | 29 | let expansion = derive_fn(item.to_token_stream()); 30 | 31 | let expansion_str = 32 | prettyplease::unparse(&syn::parse2(expansion).context("Failed to parse expansion")?); 33 | 34 | let expansion_path = expansions_dir.join(format!("{ident}.rs")); 35 | 36 | if !expansion_path.exists() 37 | || &fs::read_to_string(&expansion_path).context("Failed to read expansion from file")? 38 | != &expansion_str 39 | { 40 | if env::var_os("RORM_WRITE_EXPANSION").is_some() { 41 | fs::create_dir_all(&expansions_dir) 42 | .context("Failed to create directory for expansions")?; 43 | fs::write(&expansion_path, &expansion_str) 44 | .context("Failed to write expansion to file")?; 45 | } else { 46 | return Err(format!("Expansion of {ident} doesn't match").into()); 47 | } 48 | } 49 | } 50 | 51 | Ok(()) 52 | } 53 | 54 | fn get_derive_fn(item: &syn::Item) -> Result TokenStream)>> { 55 | fn is_derive(attr: &&syn::Attribute) -> bool { 56 | matches!(attr.path().get_ident(), Some(ident) if ident == "derive") 57 | } 58 | 59 | let Some((derive_attr, item_ident)) = (match &item { 60 | syn::Item::Struct(item) => item.attrs.iter().find(is_derive).zip(Some(&item.ident)), 61 | syn::Item::Enum(item) => item.attrs.iter().find(is_derive).zip(Some(&item.ident)), 62 | _ => None, 63 | }) else { 64 | return Ok(None); 65 | }; 66 | 67 | let derive_paths = derive_attr 68 | .parse_args_with(|parser: syn::parse::ParseStream| { 69 | let mut paths = Vec::new(); 70 | while !parser.is_empty() { 71 | let path: syn::Path = parser.parse()?; 72 | paths.push(path); 73 | 74 | if parser.peek(syn::Token![,]) { 75 | let _: syn::Token![,] = parser.parse()?; 76 | } else { 77 | break; 78 | } 79 | } 80 | Ok(paths) 81 | }) 82 | .context("Derive attribute is malformed")?; 83 | 84 | for path in derive_paths { 85 | let Some(segment) = path.segments.last() else { 86 | continue; 87 | }; 88 | let ident = &segment.ident; 89 | 90 | return Ok(Some(( 91 | item_ident.clone(), 92 | if ident == "Model" { 93 | |input: TokenStream| rorm_macro_impl::derive_model(input, Default::default()) 94 | } else if ident == "Patch" { 95 | |input: TokenStream| rorm_macro_impl::derive_patch(input, Default::default()) 96 | } else if ident == "DbEnum" { 97 | |input: TokenStream| rorm_macro_impl::derive_db_enum(input, Default::default()) 98 | } else { 99 | continue; 100 | }, 101 | ))); 102 | } 103 | 104 | Ok(None) 105 | } 106 | 107 | trait ResultExt { 108 | type Ok; 109 | fn context(self, context: &'static str) -> Result; 110 | } 111 | impl ResultExt for std::result::Result { 112 | type Ok = T; 113 | fn context(self, context: &'static str) -> Result { 114 | Ok(self.map_err(|e| format!("{context}: {e}"))?) 115 | } 116 | } 117 | --------------------------------------------------------------------------------