From 3ef21ea010740f18282d487c13f924f9653133e9 Mon Sep 17 00:00:00 2001 From: Lucien Cartier-Tilet Date: Wed, 15 Jan 2025 03:40:36 +0100 Subject: [PATCH] feat: add new crud macro for easier entity manipulation in DB --- Cargo.lock | 270 ++++++++++++++++++++---------- Cargo.toml | 3 +- gejdr-core/Cargo.toml | 3 +- gejdr-core/src/models/accounts.rs | 79 ++------- gejdr-core/src/models/mod.rs | 72 ++++++++ gejdr-macros/Cargo.toml | 18 ++ gejdr-macros/src/crud/ir.rs | 23 +++ gejdr-macros/src/crud/mod.rs | 195 +++++++++++++++++++++ gejdr-macros/src/lib.rs | 19 +++ 9 files changed, 520 insertions(+), 162 deletions(-) create mode 100644 gejdr-macros/Cargo.toml create mode 100644 gejdr-macros/src/crud/ir.rs create mode 100644 gejdr-macros/src/crud/mod.rs create mode 100644 gejdr-macros/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 4d405d0..2d159b8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -135,6 +135,12 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + [[package]] name = "async-trait" version = "0.1.83" @@ -143,7 +149,7 @@ checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.96", ] [[package]] @@ -572,8 +578,8 @@ dependencies = [ "ident_case", "proc-macro2", "quote", - "strsim", - "syn 2.0.89", + "strsim 0.11.1", + "syn 2.0.96", ] [[package]] @@ -584,7 +590,7 @@ checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ "darling_core", "quote", - "syn 2.0.89", + "syn 2.0.96", ] [[package]] @@ -593,6 +599,47 @@ version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" +[[package]] +name = "deluxe" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed332aaf752b459088acf3dd4eca323e3ef4b83c70a84ca48fb0ec5305f1488" +dependencies = [ + "deluxe-core", + "deluxe-macros", + "once_cell", + "proc-macro2", + "syn 2.0.96", +] + +[[package]] +name = "deluxe-core" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddada51c8576df9d6a8450c351ff63042b092c9458b8ac7d20f89cbd0ffd313" +dependencies = [ + "arrayvec", + "proc-macro2", + "quote", + "strsim 0.10.0", + "syn 2.0.96", +] + +[[package]] +name = "deluxe-macros" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87546d9c837f0b7557e47b8bd6eae52c3c223141b76aa233c345c9ab41d9117" +dependencies = [ + "deluxe-core", + "heck 0.4.1", + "if_chain", + "proc-macro-crate 1.3.1", + "proc-macro2", + "quote", + "syn 2.0.96", +] + [[package]] name = "der" version = "0.7.9" @@ -630,7 +677,7 @@ checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" dependencies = [ "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.96", "unicode-xid", ] @@ -663,7 +710,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.96", ] [[package]] @@ -760,6 +807,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f" + [[package]] name = "form_urlencoded" version = "1.2.1" @@ -836,7 +889,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.96", ] [[package]] @@ -912,6 +965,7 @@ name = "gejdr-core" version = "0.1.0" dependencies = [ "chrono", + "gejdr-macros", "serde", "sqlx", "tracing", @@ -919,6 +973,17 @@ dependencies = [ "uuid", ] +[[package]] +name = "gejdr-macros" +version = "0.1.0" +dependencies = [ + "deluxe", + "proc-macro2", + "quote", + "sqlx", + "syn 2.0.96", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -1021,6 +1086,11 @@ name = "hashbrown" version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a9bfc1af68b1726ea47d3d5109de126281def866b33970e10fbab11b5dafab3" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] [[package]] name = "hashlink" @@ -1033,11 +1103,11 @@ dependencies = [ [[package]] name = "hashlink" -version = "0.9.1" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" dependencies = [ - "hashbrown 0.14.5", + "hashbrown 0.15.1", ] [[package]] @@ -1064,6 +1134,12 @@ dependencies = [ "http 1.1.0", ] +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + [[package]] name = "heck" version = "0.5.0" @@ -1421,7 +1497,7 @@ checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.96", ] [[package]] @@ -1451,6 +1527,12 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "if_chain" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb56e1aa765b4b4f3aadfab769793b7087bb03a4ea4920644a6d238e2df5b9ed" + [[package]] name = "indexmap" version = "2.6.0" @@ -1529,7 +1611,6 @@ version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" dependencies = [ - "cc", "pkg-config", "vcpkg", ] @@ -1809,12 +1890,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "paste" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" - [[package]] name = "pathdiff" version = "0.2.2" @@ -1867,7 +1942,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.96", ] [[package]] @@ -1994,10 +2069,10 @@ version = "3.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f2553c04acbd3887e2ad1959ff007fb9ec05d15d67931b6fdd6eb47de138649" dependencies = [ - "proc-macro-crate", + "proc-macro-crate 3.2.0", "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.96", ] [[package]] @@ -2037,11 +2112,11 @@ dependencies = [ "http 1.1.0", "indexmap", "mime", - "proc-macro-crate", + "proc-macro-crate 3.2.0", "proc-macro2", "quote", "regex", - "syn 2.0.89", + "syn 2.0.96", "thiserror 1.0.69", ] @@ -2106,20 +2181,30 @@ dependencies = [ "indexmap", ] +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + [[package]] name = "proc-macro-crate" version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" dependencies = [ - "toml_edit", + "toml_edit 0.22.22", ] [[package]] name = "proc-macro2" -version = "1.0.92" +version = "1.0.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" +checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" dependencies = [ "unicode-ident", ] @@ -2188,9 +2273,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.37" +version = "1.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" dependencies = [ "proc-macro2", ] @@ -2581,7 +2666,7 @@ checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.96", ] [[package]] @@ -2747,21 +2832,11 @@ dependencies = [ "der", ] -[[package]] -name = "sqlformat" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bba3a93db0cc4f7bdece8bb09e77e2e785c20bfebf79eb8340ed80708048790" -dependencies = [ - "nom", - "unicode_categories", -] - [[package]] name = "sqlx" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93334716a037193fac19df402f8571269c84a00852f6a7066b5d2616dcd64d3e" +checksum = "4410e73b3c0d8442c5f99b425d7a435b5ee0ae4167b3196771dd3f7a01be745f" dependencies = [ "sqlx-core", "sqlx-macros", @@ -2772,38 +2847,32 @@ dependencies = [ [[package]] name = "sqlx-core" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4d8060b456358185f7d50c55d9b5066ad956956fddec42ee2e8567134a8936e" +checksum = "6a007b6936676aa9ab40207cde35daab0a04b823be8ae004368c0793b96a61e0" dependencies = [ - "atoi", - "byteorder", "bytes 1.8.0", "chrono", "crc", "crossbeam-queue", "either", "event-listener", - "futures-channel", "futures-core", "futures-intrusive", "futures-io", "futures-util", - "hashbrown 0.14.5", - "hashlink 0.9.1", - "hex", + "hashbrown 0.15.1", + "hashlink 0.10.0", "indexmap", "log", "memchr", "once_cell", - "paste", "percent-encoding", "serde", "serde_json", "sha2 0.10.8", "smallvec", - "sqlformat", - "thiserror 1.0.69", + "thiserror 2.0.3", "tokio", "tokio-stream", "tracing", @@ -2813,26 +2882,26 @@ dependencies = [ [[package]] name = "sqlx-macros" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cac0692bcc9de3b073e8d747391827297e075c7710ff6276d9f7a1f3d58c6657" +checksum = "3112e2ad78643fef903618d78cf0aec1cb3134b019730edb039b69eaf531f310" dependencies = [ "proc-macro2", "quote", "sqlx-core", "sqlx-macros-core", - "syn 2.0.89", + "syn 2.0.96", ] [[package]] name = "sqlx-macros-core" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1804e8a7c7865599c9c79be146dc8a9fd8cc86935fa641d3ea58e5f0688abaa5" +checksum = "4e9f90acc5ab146a99bf5061a7eb4976b573f560bc898ef3bf8435448dd5e7ad" dependencies = [ "dotenvy", "either", - "heck", + "heck 0.5.0", "hex", "once_cell", "proc-macro2", @@ -2844,7 +2913,7 @@ dependencies = [ "sqlx-mysql", "sqlx-postgres", "sqlx-sqlite", - "syn 2.0.89", + "syn 2.0.96", "tempfile", "tokio", "url", @@ -2852,9 +2921,9 @@ dependencies = [ [[package]] name = "sqlx-mysql" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64bb4714269afa44aef2755150a0fc19d756fb580a67db8885608cf02f47d06a" +checksum = "4560278f0e00ce64938540546f59f590d60beee33fffbd3b9cd47851e5fff233" dependencies = [ "atoi", "base64 0.22.1", @@ -2888,7 +2957,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror 1.0.69", + "thiserror 2.0.3", "tracing", "uuid", "whoami", @@ -2896,9 +2965,9 @@ dependencies = [ [[package]] name = "sqlx-postgres" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fa91a732d854c5d7726349bb4bb879bb9478993ceb764247660aee25f67c2f8" +checksum = "c5b98a57f363ed6764d5b3a12bfedf62f07aa16e1856a7ddc2a0bb190a959613" dependencies = [ "atoi", "base64 0.22.1", @@ -2910,7 +2979,6 @@ dependencies = [ "etcetera", "futures-channel", "futures-core", - "futures-io", "futures-util", "hex", "hkdf", @@ -2928,7 +2996,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror 1.0.69", + "thiserror 2.0.3", "tracing", "uuid", "whoami", @@ -2936,9 +3004,9 @@ dependencies = [ [[package]] name = "sqlx-sqlite" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5b2cf34a45953bfd3daaf3db0f7a7878ab9b7a6b91b422d24a7a9e4c857b680" +checksum = "f85ca71d3a5b24e64e1d08dd8fe36c6c95c339a896cc33068148906784620540" dependencies = [ "atoi", "chrono", @@ -2988,6 +3056,12 @@ dependencies = [ "unicode-properties", ] +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + [[package]] name = "strsim" version = "0.11.1" @@ -3013,9 +3087,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.89" +version = "2.0.96" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d46482f1c1c87acd84dea20c1bf5ebff4c757009ed6bf19cfd36fb10e92c4e" +checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80" dependencies = [ "proc-macro2", "quote", @@ -3045,7 +3119,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" dependencies = [ "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.96", ] [[package]] @@ -3108,7 +3182,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.96", ] [[package]] @@ -3119,7 +3193,7 @@ checksum = "f077553d607adc1caf65430528a576c757a71ed73944b66ebb58ef2bbd243568" dependencies = [ "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.96", ] [[package]] @@ -3221,7 +3295,7 @@ checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.96", ] [[package]] @@ -3290,7 +3364,7 @@ dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit", + "toml_edit 0.22.22", ] [[package]] @@ -3302,6 +3376,17 @@ dependencies = [ "serde", ] +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow 0.5.40", +] + [[package]] name = "toml_edit" version = "0.22.22" @@ -3312,7 +3397,7 @@ dependencies = [ "serde", "serde_spanned", "toml_datetime", - "winnow", + "winnow 0.6.20", ] [[package]] @@ -3341,7 +3426,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.96", ] [[package]] @@ -3462,12 +3547,6 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" -[[package]] -name = "unicode_categories" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" - [[package]] name = "universal-hash" version = "0.4.0" @@ -3526,9 +3605,9 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "uuid" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" +checksum = "744018581f9a3454a9e15beb8a33b017183f1e7c0cd170232a2d1453b23a51c4" dependencies = [ "getrandom", "serde", @@ -3595,7 +3674,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.96", "wasm-bindgen-shared", ] @@ -3629,7 +3708,7 @@ checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" dependencies = [ "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.96", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -3900,6 +3979,15 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + [[package]] name = "winnow" version = "0.6.20" @@ -3962,7 +4050,7 @@ checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" dependencies = [ "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.96", "synstructure", ] @@ -3984,7 +4072,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.96", ] [[package]] @@ -4004,7 +4092,7 @@ checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" dependencies = [ "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.96", "synstructure", ] @@ -4033,5 +4121,5 @@ checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.96", ] diff --git a/Cargo.toml b/Cargo.toml index 72d56ae..1950193 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,5 +4,6 @@ members = [ "gejdr-core", "gejdr-bot", "gejdr-backend", + "gejdr-macros" ] -resolver = "2" \ No newline at end of file +resolver = "2" diff --git a/gejdr-core/Cargo.toml b/gejdr-core/Cargo.toml index 0d645ce..60756f7 100644 --- a/gejdr-core/Cargo.toml +++ b/gejdr-core/Cargo.toml @@ -9,8 +9,9 @@ serde = "1.0.215" tracing = "0.1.40" tracing-subscriber = { version = "0.3.18", features = ["fmt", "std", "env-filter", "registry", "json", "tracing-log"] } uuid = { version = "1.11.0", features = ["v4", "serde"] } +gejdr-macros = { path = "../gejdr-macros" } [dependencies.sqlx] -version = "0.8.2" +version = "0.8.3" default-features = false features = ["postgres", "uuid", "chrono", "migrate", "runtime-tokio", "macros"] diff --git a/gejdr-core/src/models/accounts.rs b/gejdr-core/src/models/accounts.rs index a4699b1..53dea05 100644 --- a/gejdr-core/src/models/accounts.rs +++ b/gejdr-core/src/models/accounts.rs @@ -1,4 +1,5 @@ use sqlx::PgPool; +use super::Crud; type Timestampz = chrono::DateTime; @@ -17,13 +18,15 @@ impl RemoteUser { pub async fn refresh_in_database(self, pool: &PgPool) -> Result { match User::find(pool, &self.id).await? { Some(local_user) => local_user.update_from_remote(self).update(pool).await, - None => User::from(self).save(pool).await, + None => User::from(self).create(pool).await, } } } -#[derive(serde::Deserialize, serde::Serialize, Debug, PartialEq, Eq, Default, Clone)] +#[derive(serde::Deserialize, serde::Serialize, Debug, PartialEq, Eq, Default, Clone, Crud)] +#[crud(table = "users")] pub struct User { + #[crud(id = true)] pub id: String, pub username: String, pub email: Option, @@ -79,68 +82,6 @@ impl User { } } } - - pub async fn find(pool: &PgPool, id: &String) -> Result, sqlx::Error> { - sqlx::query_as!(Self, r#"SELECT * FROM users WHERE id = $1"#, id) - .fetch_optional(pool) - .await - } - - pub async fn save(&self, pool: &PgPool) -> Result { - sqlx::query_as!( - Self, - r#" -INSERT INTO users (id, username, email, avatar, name, created_at, last_updated) -VALUES ($1, $2, $3, $4, $5, $6, $7) -RETURNING * -"#, - self.id, - self.username, - self.email, - self.avatar, - self.name, - self.created_at, - self.last_updated - ) - .fetch_one(pool) - .await - } - - pub async fn update(&self, pool: &PgPool) -> Result { - sqlx::query_as!( - Self, - r#" -UPDATE users -SET username = $1, email = $2, avatar = $3, name = $4, last_updated = $5 -WHERE id = $6 -RETURNING * -"#, - self.username, - self.email, - self.avatar, - self.name, - self.last_updated, - self.id - ) - .fetch_one(pool) - .await - } - - pub async fn save_or_update(&self, pool: &PgPool) -> Result { - if Self::find(pool, &self.id).await?.is_some() { - self.update(pool).await - } else { - self.save(pool).await - } - } - - pub async fn delete(pool: &PgPool, id: &String) -> Result { - let rows_affected = sqlx::query!("DELETE FROM users WHERE id = $1", id) - .execute(pool) - .await? - .rows_affected(); - Ok(rows_affected) - } } #[cfg(test)] @@ -287,7 +228,7 @@ mod tests { username: "user1".into(), ..Default::default() }; - user.save(&pool).await?; + user.create(&pool).await?; let users = sqlx::query_as!(User, "SELECT * FROM users") .fetch_all(&pool) .await?; @@ -328,7 +269,7 @@ mod tests { username: "user1".into(), ..Default::default() }; - user.save_or_update(&pool).await?; + user.create_or_update(&pool).await?; let rows = sqlx::query_as!(User, "SELECT * FROM users") .fetch_all(&pool) .await?; @@ -350,7 +291,7 @@ mod tests { name: Some("Cool Nam".into()), ..Default::default() }; - user.save_or_update(&pool).await?; + user.create_or_update(&pool).await?; let rows = sqlx::query_as!(User, "SELECT * FROM users") .fetch_all(&pool) .await?; @@ -369,7 +310,7 @@ mod tests { .await?; assert_eq!(2, rows.len()); let id = "id1".to_string(); - let deletions = User::delete(&pool, &id).await?; + let deletions = User::delete_by_id(&pool, &id).await?; assert_eq!(1, deletions); let rows = sqlx::query_as!(User, "SELECT * FROM users") .fetch_all(&pool) @@ -385,7 +326,7 @@ mod tests { .await?; assert_eq!(2, rows.len()); let id = "invalid".to_string(); - let deletions = User::delete(&pool, &id).await?; + let deletions = User::delete_by_id(&pool, &id).await?; assert_eq!(0, deletions); let rows = sqlx::query_as!(User, "SELECT * FROM users") .fetch_all(&pool) diff --git a/gejdr-core/src/models/mod.rs b/gejdr-core/src/models/mod.rs index 9bb4894..eabca60 100644 --- a/gejdr-core/src/models/mod.rs +++ b/gejdr-core/src/models/mod.rs @@ -1 +1,73 @@ pub mod accounts; +pub use gejdr_macros::Crud; + +pub trait Crud { + /// Find the entiy in the database based on its identifier. + /// + /// # Errors + /// Returns any error Postgres may have encountered + fn find( + pool: &sqlx::PgPool, + id: &Id, + ) -> impl std::future::Future>> + Send + where + Self: Sized; + + /// Create the entity in the database. + /// + /// # Errors + /// Returns any error Postgres may have encountered + fn create( + &self, + pool: &sqlx::PgPool, + ) -> impl std::future::Future> + Send + where + Self: Sized; + + /// Update an entity with a matching identifier in the database. + /// + /// # Errors + /// Returns any error Postgres may have encountered + fn update( + &self, + pool: &sqlx::PgPool, + ) -> impl std::future::Future> + Send + where + Self: Sized; + + /// Update an entity with a matching identifier in the database if + /// it exists, create it otherwise. + /// + /// # Errors + /// Returns any error Postgres may have encountered + fn create_or_update( + &self, + pool: &sqlx::PgPool, + ) -> impl std::future::Future> + Send + where + Self: Sized; + + /// Delete the entity from the database if it exists. + /// + /// # Returns + /// Returns the amount of rows affected by the deletion. + /// + /// # Errors + /// Returns any error Postgres may have encountered + fn delete( + &self, + pool: &sqlx::PgPool, + ) -> impl std::future::Future> + Send; + + /// Delete any entity with the identifier `id`. + /// + /// # Returns + /// Returns the amount of rows affected by the deletion. + /// + /// # Errors + /// Returns any error Postgres may have encountered + fn delete_by_id( + pool: &sqlx::PgPool, + id: &Id, + ) -> impl std::future::Future> + Send; +} diff --git a/gejdr-macros/Cargo.toml b/gejdr-macros/Cargo.toml new file mode 100644 index 0000000..5c4fdeb --- /dev/null +++ b/gejdr-macros/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "gejdr-macros" +version = "0.1.0" +edition = "2021" + +[lib] +proc-macro = true + +[dependencies] +deluxe = "0.5.0" +proc-macro2 = "1.0.93" +quote = "1.0.38" +syn = "2.0.96" + +[dependencies.sqlx] +version = "0.8.3" +default-features = false +features = ["postgres", "uuid", "chrono", "migrate", "runtime-tokio", "macros"] diff --git a/gejdr-macros/src/crud/ir.rs b/gejdr-macros/src/crud/ir.rs new file mode 100644 index 0000000..ecaf7d6 --- /dev/null +++ b/gejdr-macros/src/crud/ir.rs @@ -0,0 +1,23 @@ +#[derive(deluxe::ExtractAttributes)] +#[deluxe(attributes(crud))] +pub struct CrudStructAttributes { + pub table: String, +} + +#[derive(deluxe::ExtractAttributes, Clone)] +#[deluxe(attributes(crud))] +pub struct CrudFieldAttributes { + #[deluxe(default = false)] + pub id: bool, + #[deluxe(default = None)] + pub column: Option, +} + +#[derive(Clone)] +pub struct CrudField { + pub ident: syn::Ident, + pub field: syn::Field, + pub column: String, + pub id: bool, + pub ty: syn::Type +} diff --git a/gejdr-macros/src/crud/mod.rs b/gejdr-macros/src/crud/mod.rs new file mode 100644 index 0000000..e1be25f --- /dev/null +++ b/gejdr-macros/src/crud/mod.rs @@ -0,0 +1,195 @@ +use ir::{CrudField, CrudFieldAttributes, CrudStructAttributes}; +use quote::quote; +use syn::DeriveInput; + +mod ir; + +fn extract_crud_field_attrs( + ast: &mut DeriveInput, +) -> deluxe::Result<(Vec, CrudField)> { + let mut field_attrs: Vec = Vec::new(); + // let mut identifier: Option = None; + let mut identifier: Option = None; + let mut identifier_counter = 0; + if let syn::Data::Struct(s) = &mut ast.data { + for field in &mut s.fields { + let ident = field.clone().ident.unwrap(); + let ty = field.clone().ty; + let attrs: CrudFieldAttributes = + deluxe::extract_attributes(field).expect("Could not extract attributes from field"); + let field = CrudField { + ident: ident.clone(), + field: field.to_owned(), + column: attrs.column.unwrap_or_else(|| ident.to_string()), + id: attrs.id, + ty + }; + if attrs.id { + identifier_counter += 1; + identifier = Some(field.clone()); + } + if identifier_counter > 1 { + return Err(syn::Error::new_spanned( + field.field, + "Struct {name} can only have one identifier", + )); + } + field_attrs.push(field); + } + } + if identifier_counter < 1 { + Err(syn::Error::new_spanned( + ast, + "Struct {name} must have one identifier", + )) + } else { + Ok((field_attrs, identifier.unwrap())) + } +} + +fn generate_find_query( + table: &str, + id: &CrudField +) -> proc_macro2::TokenStream { + let find_string = format!("SELECT * FROM {} WHERE {} = $1", table, id.column); + let ty = &id.ty; + quote! { + async fn find(pool: &::sqlx::PgPool, id: &#ty) -> ::sqlx::Result> { + ::sqlx::query_as!(Self, #find_string, id) + .fetch_optional(pool) + .await + } + } +} + +fn generate_create_query( + table: &str, + fields: &[CrudField] +) -> proc_macro2::TokenStream { + let inputs: Vec = (1..=fields.len()) + .map(|num| format!("${num}")) + .collect(); + let create_string = format!( + "INSERT INTO {} ({}) VALUES ({}) RETURNING *", + table, + fields.iter().map(|v| v.column.clone()).collect::>().join(", "), + inputs.join(", ") + ); + let field_idents: Vec = fields.iter().map(|f| f.ident.clone()).collect(); + quote! { + async fn create(&self, pool: &::sqlx::PgPool) -> ::sqlx::Result { + ::sqlx::query_as!( + Self, + #create_string, + #(self.#field_idents),* + ) + .fetch_one(pool) + .await + } + } +} + +fn generate_update_query( + table: &str, + fields: &[CrudField], + id: &CrudField +) -> proc_macro2::TokenStream { + let mut fields: Vec<&CrudField> = fields.iter().filter(|f| !f.id).collect(); + let update_columns = fields.iter().enumerate() + .map(|(i, &field)| format!("{} = ${}", field.column, i + 1)) + .collect::>() + .join(", "); + let update_string = format!( + "UPDATE {} SET {} WHERE {} = ${} RETURNING *", + table, + update_columns, + id.column, + fields.len() + 1 + ); + fields.push(id); + let field_idents: Vec<_> = fields.iter().map(|f| f.ident.clone()).collect(); + quote! { + async fn update(&self, pool: &::sqlx::PgPool) -> ::sqlx::Result { + ::sqlx::query_as!( + Self, + #update_string, + #(self.#field_idents),* + ) + .fetch_one(pool) + .await + } + } +} + +fn generate_delete_query( + table: &str, + id: &CrudField +) -> proc_macro2::TokenStream { + let delete_string = format!("DELETE FROM {} WHERE {} = $1", table, id.column); + let ty = &id.ty; + let ident = &id.ident; + + quote! { + async fn delete_by_id(pool: &::sqlx::PgPool, id: &#ty) -> ::sqlx::Result { + let rows_affected = ::sqlx::query!(#delete_string, id) + .execute(pool) + .await? + .rows_affected(); + Ok(rows_affected) + } + + async fn delete(&self, pool: &::sqlx::PgPool) -> ::sqlx::Result { + let rows_affected = ::sqlx::query!(#delete_string, self.#ident) + .execute(pool) + .await? + .rows_affected(); + Ok(rows_affected) + } + } +} + +pub fn crud_derive_macro2( + item: proc_macro2::TokenStream, +) -> deluxe::Result { + // parse + let mut ast: DeriveInput = syn::parse2(item).expect("Failed to parse input"); + + // extract struct attributes + let CrudStructAttributes { table } = + deluxe::extract_attributes(&mut ast).expect("Could not extract attributes from struct"); + + // extract field attributes + let (fields, id) = extract_crud_field_attrs(&mut ast)?; + let ty = &id.ty; + let id_ident = &id.ident; + + // define impl variables + let ident = &ast.ident; + let (impl_generics, type_generics, where_clause) = ast.generics.split_for_impl(); + + // generate + let find_query = generate_find_query(&table, &id); + let create_query = generate_create_query(&table, &fields); + let update_query = generate_update_query(&table, &fields, &id); + let delete_query = generate_delete_query(&table, &id); + let code = quote! { + impl #impl_generics Crud<#ty> for #ident #type_generics #where_clause { + #find_query + + #create_query + + #update_query + + async fn create_or_update(&self, pool: &::sqlx::PgPool) -> ::sqlx::Result { + if Self::find(pool, &self.#id_ident).await?.is_some() { + self.update(pool).await + } else { + self.create(pool).await + } + } + + #delete_query + } + }; + Ok(code) +} diff --git a/gejdr-macros/src/lib.rs b/gejdr-macros/src/lib.rs new file mode 100644 index 0000000..45d0b14 --- /dev/null +++ b/gejdr-macros/src/lib.rs @@ -0,0 +1,19 @@ +#![deny(clippy::all)] +#![deny(clippy::pedantic)] +#![deny(clippy::nursery)] +#![allow(clippy::module_name_repetitions)] +#![allow(clippy::unused_async)] +#![allow(clippy::useless_let_if_seq)] // Reason: prevents some OpenApi structs from compiling + +mod crud; +use crud::crud_derive_macro2; + +/// Generates CRUD code for Sqlx for a struct. +/// +/// # Panics +/// +/// May panic if errors arise while parsing and generating code. +#[proc_macro_derive(Crud, attributes(crud))] +pub fn crud_derive_macro(item: proc_macro::TokenStream) -> proc_macro::TokenStream { + crud_derive_macro2(item.into()).unwrap().into() +}