diff --git a/Cargo.lock b/Cargo.lock index fd475f0..37a9eae 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,17 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom 0.2.17", + "once_cell", + "version_check", +] + [[package]] name = "aho-corasick" version = "1.1.4" @@ -17,6 +28,27 @@ dependencies = [ "memchr", ] +[[package]] +name = "aliasable" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd" + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anstream" version = "0.6.21" @@ -67,6 +99,54 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -97,6 +177,20 @@ version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" +[[package]] +name = "bigdecimal" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d6867f1565b3aad85681f1015055b087fcfd840d6aeee6eee7f2da317603695" +dependencies = [ + "autocfg", + "libm", + "num-bigint", + "num-integer", + "num-traits", + "serde", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -108,6 +202,21 @@ name = "bitflags" version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +dependencies = [ + "serde_core", +] + +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] [[package]] name = "block-buffer" @@ -127,18 +236,79 @@ dependencies = [ "objc2", ] +[[package]] +name = "borsh" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1da5ab77c1437701eeff7c88d968729e7766172279eab0676857b3d63af7a6f" +dependencies = [ + "borsh-derive", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0686c856aa6aac0c4498f936d7d6a02df690f614c03e4d906d1018062b5c5e2c" +dependencies = [ + "once_cell", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.114", +] + [[package]] name = "bumpalo" version = "3.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" +[[package]] +name = "bytecheck" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2" +dependencies = [ + "bytecheck_derive", + "ptr_meta", + "simdutf8", +] + +[[package]] +name = "bytecheck_derive" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "bytes" version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" +[[package]] +name = "cc" +version = "1.2.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29" +dependencies = [ + "find-msvc-tools", + "shlex", +] + [[package]] name = "cfg-if" version = "1.0.4" @@ -151,6 +321,18 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "chrono" +version = "0.4.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" +dependencies = [ + "iana-time-zone", + "num-traits", + "serde", + "windows-link", +] + [[package]] name = "clap" version = "4.5.54" @@ -179,10 +361,10 @@ version = "4.5.49" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" dependencies = [ - "heck", + "heck 0.5.0", "proc-macro2", "quote", - "syn", + "syn 2.0.114", ] [[package]] @@ -206,12 +388,27 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "const-oid" version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + [[package]] name = "cpufeatures" version = "0.2.17" @@ -221,6 +418,21 @@ dependencies = [ "libc", ] +[[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + [[package]] name = "crc32fast" version = "1.5.0" @@ -239,6 +451,15 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.21" @@ -302,7 +523,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", ] [[package]] @@ -326,7 +547,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn", + "syn 2.0.114", ] [[package]] @@ -337,7 +558,7 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core", "quote", - "syn", + "syn 2.0.114", ] [[package]] @@ -372,6 +593,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" dependencies = [ "powerfmt", + "serde_core", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.114", + "unicode-xid", ] [[package]] @@ -406,7 +650,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", ] [[package]] @@ -459,6 +703,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +dependencies = [ + "serde", +] + [[package]] name = "elliptic-curve" version = "0.13.8" @@ -496,6 +749,28 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if", + "home", + "windows-sys 0.48.0", +] + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + [[package]] name = "ff" version = "0.13.1" @@ -523,6 +798,12 @@ dependencies = [ "libredox", ] +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + [[package]] name = "flate2" version = "1.1.9" @@ -533,12 +814,29 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "flume" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" +dependencies = [ + "futures-core", + "futures-sink", + "spin", +] + [[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -557,6 +855,12 @@ dependencies = [ "libc", ] +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + [[package]] name = "futures-channel" version = "0.3.31" @@ -564,6 +868,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", + "futures-sink", ] [[package]] @@ -572,6 +877,34 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + [[package]] name = "futures-sink" version = "0.3.31" @@ -591,9 +924,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-core", + "futures-io", + "futures-sink", "futures-task", + "memchr", "pin-project-lite", "pin-utils", + "slab", ] [[package]] @@ -660,24 +997,65 @@ dependencies = [ "tracing", ] +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash", +] + [[package]] name = "hashbrown" version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + [[package]] name = "hashbrown" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown 0.15.5", +] + +[[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" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "hkdf" version = "0.12.4" @@ -696,6 +1074,15 @@ dependencies = [ "digest", ] +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "http" version = "1.4.0" @@ -786,8 +1173,32 @@ dependencies = [ ] [[package]] -name = "icu_collections" -version = "2.1.1" +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" dependencies = [ @@ -903,6 +1314,17 @@ dependencies = [ "hashbrown 0.16.1", ] +[[package]] +name = "inherent" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c727f80bfa4a6c6e2508d2f05b6f4bfce242030bd88ed15ae5331c5b5d30fba7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + [[package]] name = "inotify" version = "0.9.6" @@ -1020,6 +1442,17 @@ dependencies = [ "redox_syscall 0.7.0", ] +[[package]] +name = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + [[package]] name = "litemap" version = "0.8.1" @@ -1050,6 +1483,16 @@ dependencies = [ "regex-automata", ] +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + [[package]] name = "memchr" version = "2.7.6" @@ -1229,6 +1672,39 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "ordered-float" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951" +dependencies = [ + "num-traits", +] + +[[package]] +name = "ouroboros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0f050db9c44b97a94723127e6be766ac5c340c48f2c4bb3ffa11713744be59" +dependencies = [ + "aliasable", + "ouroboros_macro", + "static_assertions", +] + +[[package]] +name = "ouroboros_macro" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c7028bdd3d43083f6d8d4d5187680d0d3560d54df4cc9d752005268b41e64d0" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "proc-macro2-diagnostics", + "quote", + "syn 2.0.114", +] + [[package]] name = "p256" version = "0.13.2" @@ -1253,6 +1729,12 @@ dependencies = [ "sha2", ] +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + [[package]] name = "parking_lot" version = "0.12.5" @@ -1301,6 +1783,15 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "pgvector" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc58e2d255979a31caa7cabfa7aac654af0354220719ab7a68520ae7a91e8c0b" +dependencies = [ + "serde", +] + [[package]] name = "pin-project-lite" version = "0.2.16" @@ -1334,6 +1825,12 @@ dependencies = [ "spki", ] +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + [[package]] name = "potential_utf" version = "0.1.4" @@ -1367,6 +1864,15 @@ dependencies = [ "elliptic-curve", ] +[[package]] +name = "proc-macro-crate" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +dependencies = [ + "toml_edit 0.23.10+spec-1.0.0", +] + [[package]] name = "proc-macro-error-attr2" version = "2.0.0" @@ -1386,7 +1892,7 @@ dependencies = [ "proc-macro-error-attr2", "proc-macro2", "quote", - "syn", + "syn 2.0.114", ] [[package]] @@ -1398,6 +1904,39 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "proc-macro2-diagnostics" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", + "version_check", + "yansi", +] + +[[package]] +name = "ptr_meta" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" +dependencies = [ + "ptr_meta_derive", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "quote" version = "1.0.43" @@ -1413,6 +1952,12 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + [[package]] name = "rand" version = "0.8.5" @@ -1458,6 +2003,7 @@ dependencies = [ "jsonwebtoken", "rapina-macros", "schemars", + "sea-orm", "serde", "serde_json", "serde_urlencoded", @@ -1486,9 +2032,10 @@ dependencies = [ name = "rapina-macros" version = "0.2.1" dependencies = [ + "heck 0.5.0", "proc-macro2", "quote", - "syn", + "syn 2.0.114", ] [[package]] @@ -1526,7 +2073,7 @@ checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", ] [[package]] @@ -1558,6 +2105,15 @@ version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +[[package]] +name = "rend" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c" +dependencies = [ + "bytecheck", +] + [[package]] name = "rfc6979" version = "0.4.0" @@ -1568,6 +2124,49 @@ dependencies = [ "subtle", ] +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rkyv" +version = "0.7.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2297bf9c81a3f0dc96bc9521370b88f054168c29826a75e89c55ff196e7ed6a1" +dependencies = [ + "bitvec", + "bytecheck", + "bytes", + "hashbrown 0.12.3", + "ptr_meta", + "rend", + "rkyv_derive", + "seahash", + "tinyvec", + "uuid", +] + +[[package]] +name = "rkyv_derive" +version = "0.7.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84d7b42d4b8d06048d3ac8db0eb31bcb942cbeb709f0b5f2b2ebde398d3038f5" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "rsa" version = "0.9.10" @@ -1588,6 +2187,22 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rust_decimal" +version = "1.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61f703d19852dbf87cbc513643fa81428361eb6940f1ac14fd58155d295a3eb0" +dependencies = [ + "arrayvec", + "borsh", + "bytes", + "num-traits", + "rand", + "rkyv", + "serde", + "serde_json", +] + [[package]] name = "rustc_version" version = "0.4.1" @@ -1597,6 +2212,40 @@ dependencies = [ "semver", ] +[[package]] +name = "rustls" +version = "0.23.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.22" @@ -1640,7 +2289,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn", + "syn 2.0.114", ] [[package]] @@ -1649,6 +2298,101 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "sea-bae" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f694a6ab48f14bc063cfadff30ab551d3c7e46d8f81836c51989d548f44a2a25" +dependencies = [ + "heck 0.4.1", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "sea-orm" +version = "1.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d945f62558fac19e5988680d2fdf747b734c2dbc6ce2cb81ba33ed8dde5b103" +dependencies = [ + "async-stream", + "async-trait", + "bigdecimal", + "chrono", + "derive_more", + "futures-util", + "log", + "ouroboros", + "pgvector", + "rust_decimal", + "sea-orm-macros", + "sea-query", + "sea-query-binder", + "serde", + "serde_json", + "sqlx", + "strum", + "thiserror", + "time", + "tracing", + "url", + "uuid", +] + +[[package]] +name = "sea-orm-macros" +version = "1.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84c2e64a50a9cc8339f10a27577e10062c7f995488e469f2c95762c5ee847832" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "sea-bae", + "syn 2.0.114", + "unicode-ident", +] + +[[package]] +name = "sea-query" +version = "0.32.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a5d1c518eaf5eda38e5773f902b26ab6d5e9e9e2bb2349ca6c64cf96f80448c" +dependencies = [ + "bigdecimal", + "chrono", + "inherent", + "ordered-float", + "rust_decimal", + "serde_json", + "time", + "uuid", +] + +[[package]] +name = "sea-query-binder" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0019f47430f7995af63deda77e238c17323359af241233ec768aba1faea7608" +dependencies = [ + "bigdecimal", + "chrono", + "rust_decimal", + "sea-query", + "serde_json", + "sqlx", + "time", + "uuid", +] + +[[package]] +name = "seahash" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" + [[package]] name = "sec1" version = "0.7.3" @@ -1696,7 +2440,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", ] [[package]] @@ -1707,7 +2451,7 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", ] [[package]] @@ -1744,6 +2488,17 @@ dependencies = [ "serde", ] +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "sha2" version = "0.10.9" @@ -1764,6 +2519,12 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "signal-hook-registry" version = "1.4.8" @@ -1790,6 +2551,12 @@ version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + [[package]] name = "simple_asn1" version = "0.6.3" @@ -1813,6 +2580,9 @@ name = "smallvec" version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +dependencies = [ + "serde", +] [[package]] name = "socket2" @@ -1829,6 +2599,9 @@ name = "spin" version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] [[package]] name = "spki" @@ -1841,45 +2614,294 @@ dependencies = [ ] [[package]] -name = "stable_deref_trait" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" - -[[package]] -name = "strsim" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" - -[[package]] -name = "subtle" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" - -[[package]] -name = "syn" -version = "2.0.114" +name = "sqlx" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", ] [[package]] -name = "synstructure" -version = "0.13.2" +name = "sqlx-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" +dependencies = [ + "base64", + "bigdecimal", + "bytes", + "chrono", + "crc", + "crossbeam-queue", + "either", + "event-listener", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashbrown 0.15.5", + "hashlink", + "indexmap", + "log", + "memchr", + "once_cell", + "percent-encoding", + "rust_decimal", + "rustls", + "serde", + "serde_json", + "sha2", + "smallvec", + "thiserror", + "time", + "tokio", + "tokio-stream", + "tracing", + "url", + "uuid", + "webpki-roots 0.26.11", +] + +[[package]] +name = "sqlx-macros" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn 2.0.114", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" +dependencies = [ + "dotenvy", + "either", + "heck 0.5.0", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2", + "sqlx-core", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", + "syn 2.0.114", + "tokio", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" +dependencies = [ + "atoi", + "base64", + "bigdecimal", + "bitflags 2.10.0", + "byteorder", + "bytes", + "chrono", + "crc", + "digest", + "dotenvy", + "either", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "generic-array", + "hex", + "hkdf", + "hmac", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "percent-encoding", + "rand", + "rsa", + "rust_decimal", + "serde", + "sha1", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "time", + "tracing", + "uuid", + "whoami", +] + +[[package]] +name = "sqlx-postgres" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" +dependencies = [ + "atoi", + "base64", + "bigdecimal", + "bitflags 2.10.0", + "byteorder", + "chrono", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-util", + "hex", + "hkdf", + "hmac", + "home", + "itoa", + "log", + "md-5", + "memchr", + "num-bigint", + "once_cell", + "rand", + "rust_decimal", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "time", + "tracing", + "uuid", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" +dependencies = [ + "atoi", + "chrono", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "serde_urlencoded", + "sqlx-core", + "thiserror", + "time", + "tracing", + "url", + "uuid", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", ] +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + [[package]] name = "thiserror" version = "2.0.18" @@ -1897,7 +2919,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", ] [[package]] @@ -1950,6 +2972,21 @@ dependencies = [ "zerovec", ] +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "tokio" version = "1.49.0" @@ -1975,7 +3012,18 @@ checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", +] + +[[package]] +name = "tokio-stream" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", ] [[package]] @@ -1999,8 +3047,8 @@ checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" dependencies = [ "serde", "serde_spanned", - "toml_datetime", - "toml_edit", + "toml_datetime 0.6.11", + "toml_edit 0.22.27", ] [[package]] @@ -2012,6 +3060,15 @@ dependencies = [ "serde", ] +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + [[package]] name = "toml_edit" version = "0.22.27" @@ -2021,11 +3078,32 @@ dependencies = [ "indexmap", "serde", "serde_spanned", - "toml_datetime", + "toml_datetime 0.6.11", "toml_write", "winnow", ] +[[package]] +name = "toml_edit" +version = "0.23.10+spec-1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" +dependencies = [ + "indexmap", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.0.7+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "247eaa3197818b831697600aadf81514e577e0cba5eab10f7e064e78ae154df1" +dependencies = [ + "winnow", +] + [[package]] name = "toml_write" version = "0.1.2" @@ -2044,6 +3122,7 @@ version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ + "log", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -2057,7 +3136,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", ] [[package]] @@ -2124,12 +3203,45 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + [[package]] name = "unicode-ident" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "url" version = "2.5.8" @@ -2162,6 +3274,7 @@ checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" dependencies = [ "getrandom 0.3.4", "js-sys", + "serde_core", "wasm-bindgen", ] @@ -2192,7 +3305,7 @@ dependencies = [ "proc-macro-error2", "proc-macro2", "quote", - "syn", + "syn 2.0.114", ] [[package]] @@ -2201,6 +3314,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version_check" version = "0.9.5" @@ -2241,6 +3360,12 @@ dependencies = [ "wit-bindgen", ] +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + [[package]] name = "wasm-bindgen" version = "0.2.108" @@ -2273,7 +3398,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn", + "syn 2.0.114", "wasm-bindgen-shared", ] @@ -2286,6 +3411,34 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "webpki-roots" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.6", +] + +[[package]] +name = "webpki-roots" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "whoami" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" +dependencies = [ + "libredox", + "wasite", +] + [[package]] name = "winapi-util" version = "0.1.11" @@ -2295,12 +3448,65 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.48.0" @@ -2310,6 +3516,15 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.60.2" @@ -2343,6 +3558,22 @@ dependencies = [ "windows_x86_64_msvc 0.48.5", ] +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + [[package]] name = "windows-targets" version = "0.53.5" @@ -2353,7 +3584,7 @@ dependencies = [ "windows_aarch64_gnullvm 0.53.1", "windows_aarch64_msvc 0.53.1", "windows_i686_gnu 0.53.1", - "windows_i686_gnullvm", + "windows_i686_gnullvm 0.53.1", "windows_i686_msvc 0.53.1", "windows_x86_64_gnu 0.53.1", "windows_x86_64_gnullvm 0.53.1", @@ -2366,6 +3597,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + [[package]] name = "windows_aarch64_gnullvm" version = "0.53.1" @@ -2378,6 +3615,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + [[package]] name = "windows_aarch64_msvc" version = "0.53.1" @@ -2390,12 +3633,24 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + [[package]] name = "windows_i686_gnu" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + [[package]] name = "windows_i686_gnullvm" version = "0.53.1" @@ -2408,6 +3663,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + [[package]] name = "windows_i686_msvc" version = "0.53.1" @@ -2420,6 +3681,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + [[package]] name = "windows_x86_64_gnu" version = "0.53.1" @@ -2432,6 +3699,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + [[package]] name = "windows_x86_64_gnullvm" version = "0.53.1" @@ -2444,6 +3717,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + [[package]] name = "windows_x86_64_msvc" version = "0.53.1" @@ -2471,6 +3750,21 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + [[package]] name = "yoke" version = "0.8.1" @@ -2490,7 +3784,7 @@ checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", "synstructure", ] @@ -2511,7 +3805,7 @@ checksum = "2c7962b26b0a8685668b671ee4b54d007a67d4eaf05fda79ac0ecf41e32270f1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", ] [[package]] @@ -2531,7 +3825,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", "synstructure", ] @@ -2571,7 +3865,7 @@ checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", ] [[package]] diff --git a/docs/content/guide/database.md b/docs/content/guide/database.md new file mode 100644 index 0000000..baf2e80 --- /dev/null +++ b/docs/content/guide/database.md @@ -0,0 +1,275 @@ ++++ +title = "Database" +description = "Database integration with SeaORM" +weight = 6 ++++ + +Rapina integrates with [SeaORM](https://www.sea-ql.org/SeaORM/) for database operations. Enable it with a feature flag for your database. + +## Setup + +Add the database feature to your `Cargo.toml`: + +```toml +[dependencies] +rapina = { version = "0.2", features = ["postgres"] } +# or "mysql", "sqlite" +``` + +Configure your application with a database connection: + +```rust +use rapina::prelude::*; +use rapina::database::DatabaseConfig; + +#[tokio::main] +async fn main() -> std::io::Result<()> { + let db_config = DatabaseConfig::from_env()?; + + Rapina::new() + .with_database(db_config).await? + .router(router) + .listen("127.0.0.1:3000") + .await +} +``` + +Set your database URL via environment variable: + +```bash +DATABASE_URL=postgres://user:password@localhost:5432/myapp +``` + +## The Db Extractor + +Access the database connection in your handlers with the `Db` extractor: + +```rust +use rapina::database::{Db, DbError}; +use rapina::sea_orm::{EntityTrait, ActiveModelTrait, Set}; + +#[get("/posts")] +async fn list_posts(db: Db) -> Result>> { + let posts = Post::find() + .all(db.conn()) + .await + .map_err(DbError::from)?; + + Ok(Json(posts.into_iter().map(PostResponse::from).collect())) +} + +#[post("/posts")] +async fn create_post(body: Json, db: Db) -> Result> { + let post = post::ActiveModel { + title: Set(body.title.clone()), + content: Set(body.content.clone()), + ..Default::default() + }; + + let post = post.insert(db.conn()) + .await + .map_err(DbError::from)?; + + Ok(Json(PostResponse::from(post))) +} +``` + +The `DbError` wrapper converts SeaORM errors into Rapina's error responses automatically. + +## Defining Entities + +### The schema! Macro + +The `schema!` macro generates SeaORM entities from a declarative syntax where types define relationships: + +```rust +use rapina::prelude::*; + +schema! { + User { + #[unique] + email: String, + name: String, + posts: Vec, // has_many relationship + } + + #[table_name = "blog_posts"] + Post { + title: String, + content: Text, // TEXT column type + author: User, // belongs_to (generates author_id) + comments: Vec, + } + + Comment { + content: Text, + post: Post, // belongs_to + author: Option, // optional belongs_to + } +} +``` + +This generates complete SeaORM entity modules. Each entity automatically includes: + +- `id: i32` (primary key) +- `created_at: DateTimeUtc` +- `updated_at: DateTimeUtc` + +### Generated Code + +For each entity, the macro generates: + +- A module (e.g., `user`, `post`) +- `Model` struct with all fields +- `Entity` type for queries +- `ActiveModel` for inserts/updates +- `Relation` enum with relationship definitions +- `Related` trait implementations + +Use them in your code: + +```rust +use schema::{user, post, User, Post}; + +// Query +let users = User::find().all(db.conn()).await?; + +// Insert +let new_post = post::ActiveModel { + title: Set("Hello".to_string()), + content: Set("World".to_string()), + author_id: Set(1), + ..Default::default() +}; +let post = new_post.insert(db.conn()).await?; + +// Update +let mut post: post::ActiveModel = post.into(); +post.title = Set("Updated".to_string()); +let post = post.update(db.conn()).await?; + +// Delete +Post::delete_by_id(1).exec(db.conn()).await?; +``` + +### Supported Types + +| Schema Type | Rust Type | Column Type | +|-------------|-----------|-------------| +| `String` | `String` | VARCHAR | +| `Text` | `String` | TEXT | +| `i32` | `i32` | INTEGER | +| `i64` | `i64` | BIGINT | +| `f32` | `f32` | FLOAT | +| `f64` | `f64` | DOUBLE | +| `bool` | `bool` | BOOLEAN | +| `Uuid` | `Uuid` | UUID | +| `DateTime` | `DateTimeUtc` | TIMESTAMPTZ | +| `Date` | `Date` | DATE | +| `Decimal` | `Decimal` | DECIMAL | +| `Json` | `Json` | JSON | +| `Option` | `Option` | nullable | + +### Relationships + +Relationships are inferred from types: + +| Syntax | Relationship | Generated | +|--------|--------------|-----------| +| `posts: Vec` | has_many | Relation enum variant | +| `author: User` | belongs_to | `author_id: i32` column | +| `author: Option` | optional belongs_to | `author_id: Option` | + +### Attributes + +#### Entity Attributes + +| Attribute | Description | +|-----------|-------------| +| `#[table_name = "name"]` | Override the auto-generated table name | +| `#[timestamps(created_at)]` | Only include `created_at` timestamp | +| `#[timestamps(updated_at)]` | Only include `updated_at` timestamp | +| `#[timestamps(none)]` | No automatic timestamps | + +```rust +#[table_name = "people"] +Person { + name: String, +} + +#[timestamps(none)] +AuditLog { + action: String, + timestamp: DateTime, // manage your own timestamp +} +``` + +#### Field Attributes + +| Attribute | Description | +|-----------|-------------| +| `#[unique]` | Mark field as unique | +| `#[index]` | Create an index on this column | +| `#[column = "name"]` | Custom column name in database | + +```rust +User { + #[unique] + email: String, + + #[index] + username: String, + + #[column = "full_name"] + name: String, +} +``` + +## Database Schema + +Your database schema should match the generated entities. Example for PostgreSQL: + +```sql +CREATE TABLE users ( + id SERIAL PRIMARY KEY, + email VARCHAR(255) NOT NULL, + name VARCHAR(255) NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE TABLE posts ( + id SERIAL PRIMARY KEY, + author_id INTEGER NOT NULL REFERENCES users(id), + title VARCHAR(255) NOT NULL, + content TEXT NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE TABLE comments ( + id SERIAL PRIMARY KEY, + post_id INTEGER NOT NULL REFERENCES posts(id), + author_id INTEGER REFERENCES users(id), -- nullable for optional + content TEXT NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); +``` + +## Error Handling + +Database errors are automatically converted to appropriate HTTP responses: + +```rust +#[get("/posts/:id")] +async fn get_post(id: Path, db: Db) -> Result> { + let post = Post::find_by_id(id.into_inner()) + .one(db.conn()) + .await + .map_err(DbError::from)? // Converts to 500 + .ok_or_else(|| Error::not_found("post not found"))?; // 404 + + Ok(Json(PostResponse::from(post))) +} +``` diff --git a/docs/content/guide/extractors.md b/docs/content/guide/extractors.md index 3df67ae..52b130a 100644 --- a/docs/content/guide/extractors.md +++ b/docs/content/guide/extractors.md @@ -20,6 +20,7 @@ Extractors automatically parse request data and inject it into your handlers. If | `Cookie` | Typed cookie access | | `CurrentUser` | Authenticated user (JWT) | | `Validated` | Validated extractor | +| `Db` | Database connection (requires feature) | ## Path Parameters diff --git a/rapina-macros/Cargo.toml b/rapina-macros/Cargo.toml index 630e94f..5dda156 100644 --- a/rapina-macros/Cargo.toml +++ b/rapina-macros/Cargo.toml @@ -16,3 +16,4 @@ proc-macro = true syn = { version = "2.0", features = ["full"] } quote = "1" proc-macro2 = "1" +heck = "0.5" diff --git a/rapina-macros/src/lib.rs b/rapina-macros/src/lib.rs index c873079..9e40789 100644 --- a/rapina-macros/src/lib.rs +++ b/rapina-macros/src/lib.rs @@ -2,6 +2,8 @@ use proc_macro::TokenStream; use quote::quote; use syn::{FnArg, ItemFn, LitStr, Pat}; +mod schema; + #[proc_macro_attribute] pub fn get(attr: TokenStream, item: TokenStream) -> TokenStream { route_macro(attr, item) @@ -127,7 +129,7 @@ fn route_macro_core( } else if body_extractors.len() == 1 { let (arg_name, arg_type) = &body_extractors[0]; quote! { - let __rapina_req = http::Request::from_parts(__rapina_parts, __rapina_body); + let __rapina_req = rapina::http::Request::from_parts(__rapina_parts, __rapina_body); let #arg_name = match <#arg_type as rapina::extract::FromRequest>::from_request(__rapina_req, &__rapina_params, &__rapina_state).await { Ok(v) => v, Err(e) => return rapina::response::IntoResponse::into_response(e), @@ -165,10 +167,10 @@ fn route_macro_core( fn call( &self, - __rapina_req: hyper::Request, + __rapina_req: rapina::hyper::Request, __rapina_params: rapina::extract::PathParams, __rapina_state: std::sync::Arc, - ) -> std::pin::Pin> + Send>> { + ) -> std::pin::Pin> + Send>> { Box::pin(async move { #handler_body }) @@ -184,6 +186,8 @@ fn is_parts_only_extractor(type_str: &str) -> bool { || type_str.contains("State") || type_str.contains("Context") || type_str.contains("CurrentUser") + || type_str.contains("Db") + || type_str.contains("Cookie") } /// Extracts the inner type from Json wrapper for schema generation @@ -211,6 +215,69 @@ pub fn derive_config(input: TokenStream) -> TokenStream { derive_config_impl(input.into()).into() } +/// Define database entities with Prisma-like syntax. +/// +/// This macro generates SeaORM entity definitions from a declarative syntax +/// where types indicate relationships. Each entity automatically gets `id`, +/// `created_at`, and `updated_at` fields. +/// +/// # Syntax +/// +/// ```ignore +/// rapina::schema! { +/// User { +/// email: String, +/// name: String, +/// posts: Vec, // has_many relationship +/// } +/// +/// Post { +/// title: String, +/// content: Text, // TEXT column type +/// author: User, // belongs_to -> generates author_id +/// comments: Vec, +/// } +/// +/// Comment { +/// content: Text, +/// post: Post, +/// author: Option, // optional belongs_to +/// } +/// } +/// ``` +/// +/// # Generated Code +/// +/// For each entity, the macro generates a SeaORM module with: +/// - `Model` struct with auto `id`, `created_at`, `updated_at` +/// - `Relation` enum with proper SeaORM attributes +/// - `Related` trait implementations +/// - `ActiveModelBehavior` implementation +/// +/// # Supported Types +/// +/// | Schema Type | Rust Type | Notes | +/// |-------------|-----------|-------| +/// | `String` | `String` | Default varchar | +/// | `Text` | `String` | TEXT column | +/// | `i32` | `i32` | | +/// | `i64` | `i64` | | +/// | `f32` | `f32` | | +/// | `f64` | `f64` | | +/// | `bool` | `bool` | | +/// | `Uuid` | `Uuid` | | +/// | `DateTime` | `DateTimeUtc` | | +/// | `Date` | `Date` | | +/// | `Decimal` | `Decimal` | | +/// | `Json` | `Json` | | +/// | `Option` | `Option` | Nullable | +/// | `Vec` | - | has_many relationship | +/// | `Entity` | - | belongs_to (generates FK) | +#[proc_macro] +pub fn schema(input: TokenStream) -> TokenStream { + schema::schema_impl(input.into()).into() +} + fn derive_config_impl(input: proc_macro2::TokenStream) -> proc_macro2::TokenStream { let input: syn::DeriveInput = syn::parse2(input).expect("expected struct"); let name = &input.ident; diff --git a/rapina-macros/src/schema/analyze.rs b/rapina-macros/src/schema/analyze.rs new file mode 100644 index 0000000..5f302cb --- /dev/null +++ b/rapina-macros/src/schema/analyze.rs @@ -0,0 +1,321 @@ +//! Semantic analysis for the schema macro. +//! +//! Two-pass analysis: +//! 1. Collect all entity names into a registry +//! 2. Resolve relationships and validate targets exist + +use proc_macro2::Span; +use std::collections::HashSet; +use syn::{Ident, Result}; + +use super::parse::{EntityAttrs, EntityDef, FieldAttrs, FieldDef, RawFieldType, Schema}; +use super::types::FieldType; + +/// Analyzed schema with resolved relationships. +#[derive(Debug)] +pub struct AnalyzedSchema { + pub entities: Vec, +} + +/// An entity with resolved field types. +#[derive(Debug)] +pub struct AnalyzedEntity { + pub attrs: EntityAttrs, + pub name: Ident, + pub fields: Vec, + #[allow(dead_code)] + pub span: Span, +} + +/// A field with resolved type information. +#[derive(Debug)] +pub struct AnalyzedField { + pub attrs: FieldAttrs, + pub name: Ident, + pub ty: FieldType, + #[allow(dead_code)] + pub span: Span, +} + +/// Entity registry for cross-reference validation. +struct EntityRegistry { + names: HashSet, +} + +impl EntityRegistry { + fn new(entities: &[EntityDef]) -> Self { + let names = entities.iter().map(|e| e.name.to_string()).collect(); + EntityRegistry { names } + } + + fn contains(&self, name: &str) -> bool { + self.names.contains(name) + } +} + +/// Analyze a parsed schema, resolving relationships and validating references. +pub fn analyze_schema(schema: Schema) -> Result { + // Check for duplicate entity names + let mut seen_entities = HashSet::new(); + for entity in &schema.entities { + let entity_name = entity.name.to_string(); + if !seen_entities.insert(entity_name.clone()) { + return Err(syn::Error::new( + entity.name.span(), + format!("duplicate entity name '{}'", entity_name), + )); + } + } + + // Build entity registry for cross-reference + let registry = EntityRegistry::new(&schema.entities); + + // Analyze each entity + let mut analyzed_entities = Vec::new(); + for entity in schema.entities { + analyzed_entities.push(analyze_entity(entity, ®istry)?); + } + + Ok(AnalyzedSchema { + entities: analyzed_entities, + }) +} + +fn analyze_entity(entity: EntityDef, registry: &EntityRegistry) -> Result { + let mut analyzed_fields = Vec::new(); + + for field in entity.fields { + analyzed_fields.push(analyze_field(field, registry)?); + } + + Ok(AnalyzedEntity { + attrs: entity.attrs, + name: entity.name, + fields: analyzed_fields, + span: entity.span, + }) +} + +fn analyze_field(field: FieldDef, registry: &EntityRegistry) -> Result { + let ty = match field.ty { + RawFieldType::Scalar { scalar, optional } => FieldType::Scalar { scalar, optional }, + + RawFieldType::Vec { inner } => { + let inner_name = inner.to_string(); + + // Vec must reference an entity (has_many) + if !registry.contains(&inner_name) { + return Err(syn::Error::new( + inner.span(), + format!( + "unknown entity '{}' in Vec<{0}>. Did you define this entity?", + inner_name + ), + )); + } + + FieldType::HasMany { target: inner } + } + + RawFieldType::Unknown { name, optional } => { + let type_name = name.to_string(); + + // If it's a known entity, it's a belongs_to relationship + if registry.contains(&type_name) { + FieldType::BelongsTo { + target: name, + optional, + } + } else { + return Err(syn::Error::new( + name.span(), + format!( + "unknown type '{}'. Use a scalar type (String, i32, etc.) or reference a defined entity.", + type_name + ), + )); + } + } + }; + + Ok(AnalyzedField { + attrs: field.attrs, + name: field.name, + ty, + span: field.span, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::schema::parse::parse_schema; + use quote::quote; + + #[test] + fn test_analyze_simple_schema() { + let input = quote! { + User { + email: String, + name: String, + } + }; + + let parsed = parse_schema(input).unwrap(); + let analyzed = analyze_schema(parsed).unwrap(); + + assert_eq!(analyzed.entities.len(), 1); + assert_eq!(analyzed.entities[0].fields.len(), 2); + } + + #[test] + fn test_analyze_has_many_relationship() { + let input = quote! { + User { + posts: Vec, + } + + Post { + title: String, + } + }; + + let parsed = parse_schema(input).unwrap(); + let analyzed = analyze_schema(parsed).unwrap(); + + let user = &analyzed.entities[0]; + assert!(matches!(user.fields[0].ty, FieldType::HasMany { .. })); + } + + #[test] + fn test_analyze_belongs_to_relationship() { + let input = quote! { + User { + email: String, + } + + Post { + author: User, + } + }; + + let parsed = parse_schema(input).unwrap(); + let analyzed = analyze_schema(parsed).unwrap(); + + let post = &analyzed.entities[1]; + assert!(matches!( + post.fields[0].ty, + FieldType::BelongsTo { + optional: false, + .. + } + )); + } + + #[test] + fn test_analyze_optional_belongs_to() { + let input = quote! { + User { + email: String, + } + + Comment { + author: Option, + } + }; + + let parsed = parse_schema(input).unwrap(); + let analyzed = analyze_schema(parsed).unwrap(); + + let comment = &analyzed.entities[1]; + assert!(matches!( + comment.fields[0].ty, + FieldType::BelongsTo { optional: true, .. } + )); + } + + #[test] + fn test_unknown_entity_in_vec_error() { + let input = quote! { + User { + posts: Vec, + } + }; + + let parsed = parse_schema(input).unwrap(); + let result = analyze_schema(parsed); + + assert!(result.is_err()); + assert!(result.unwrap_err().to_string().contains("unknown entity")); + } + + #[test] + fn test_unknown_type_error() { + let input = quote! { + User { + foo: UnknownType, + } + }; + + let parsed = parse_schema(input).unwrap(); + let result = analyze_schema(parsed); + + assert!(result.is_err()); + assert!(result.unwrap_err().to_string().contains("unknown type")); + } + + #[test] + fn test_duplicate_entity_error() { + let input = quote! { + User { + email: String, + } + + User { + name: String, + } + }; + + let parsed = parse_schema(input).unwrap(); + let result = analyze_schema(parsed); + + assert!(result.is_err()); + assert!(result.unwrap_err().to_string().contains("duplicate entity")); + } + + #[test] + fn test_analyze_preserves_entity_attrs() { + let input = quote! { + #[table_name = "people"] + Person { + name: String, + } + }; + + let parsed = parse_schema(input).unwrap(); + let analyzed = analyze_schema(parsed).unwrap(); + + assert_eq!( + analyzed.entities[0].attrs.table_name, + Some("people".to_string()) + ); + } + + #[test] + fn test_analyze_preserves_field_attrs() { + let input = quote! { + User { + #[unique] + #[column = "user_email"] + email: String, + } + }; + + let parsed = parse_schema(input).unwrap(); + let analyzed = analyze_schema(parsed).unwrap(); + + let field = &analyzed.entities[0].fields[0]; + assert!(field.attrs.unique); + assert_eq!(field.attrs.column_name, Some("user_email".to_string())); + } +} diff --git a/rapina-macros/src/schema/generate.rs b/rapina-macros/src/schema/generate.rs new file mode 100644 index 0000000..3a466de --- /dev/null +++ b/rapina-macros/src/schema/generate.rs @@ -0,0 +1,538 @@ +//! Code generation for SeaORM entity modules. + +use heck::ToSnakeCase; +use proc_macro2::TokenStream; +use quote::{format_ident, quote}; + +use super::analyze::{AnalyzedEntity, AnalyzedField, AnalyzedSchema}; +use super::types::FieldType; + +/// Generate the complete schema code from analyzed entities. +pub fn generate_schema(schema: AnalyzedSchema) -> TokenStream { + let entity_modules: Vec = schema + .entities + .iter() + .map(|entity| generate_entity_module(entity, &schema)) + .collect(); + + // Generate re-exports: pub use user::Entity as User; + let reexports: Vec = schema + .entities + .iter() + .map(|entity| { + let mod_name = format_ident!("{}", entity.name.to_string().to_snake_case()); + let entity_name = &entity.name; + quote! { + pub use #mod_name::Entity as #entity_name; + } + }) + .collect(); + + quote! { + #(#entity_modules)* + #(#reexports)* + } +} + +fn generate_entity_module(entity: &AnalyzedEntity, schema: &AnalyzedSchema) -> TokenStream { + let mod_name = format_ident!("{}", entity.name.to_string().to_snake_case()); + + // Use custom table name if provided, otherwise auto-pluralize + let table_name = entity + .attrs + .table_name + .clone() + .unwrap_or_else(|| format!("{}s", entity.name.to_string().to_snake_case())); + + let model_fields = generate_model_fields(entity); + let relation_variants = generate_relation_variants(entity, schema); + let related_impls = generate_related_impls(entity, schema); + + // Generate timestamp fields based on entity attrs + let created_at_field = if entity.attrs.has_created_at { + quote! { pub created_at: DateTimeUtc, } + } else { + quote! {} + }; + + let updated_at_field = if entity.attrs.has_updated_at { + quote! { pub updated_at: DateTimeUtc, } + } else { + quote! {} + }; + + quote! { + pub mod #mod_name { + use rapina::sea_orm; + use sea_orm::entity::prelude::*; + use serde::{Deserialize, Serialize}; + + #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize, Deserialize)] + #[sea_orm(table_name = #table_name)] + pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + #model_fields + #created_at_field + #updated_at_field + } + + #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] + pub enum Relation { + #relation_variants + } + + #related_impls + + impl ActiveModelBehavior for ActiveModel {} + } + } +} + +fn generate_model_fields(entity: &AnalyzedEntity) -> TokenStream { + let fields: Vec = entity + .fields + .iter() + .filter_map(generate_model_field) + .collect(); + + quote! { + #(#fields)* + } +} + +fn generate_model_field(field: &AnalyzedField) -> Option { + let field_name = &field.name; + + match &field.ty { + FieldType::Scalar { scalar, optional } => { + let rust_type = scalar.rust_type(); + let column_type_attr = scalar.column_type_attr(); + + let final_type = if *optional { + quote! { Option<#rust_type> } + } else { + rust_type + }; + + // Build sea_orm attribute parts + let mut sea_orm_parts: Vec = Vec::new(); + + // Add unique if specified + if field.attrs.unique { + sea_orm_parts.push(quote! { unique }); + } + + // Add indexed if specified + if field.attrs.indexed { + sea_orm_parts.push(quote! { indexed }); + } + + // Add custom column name if specified + if let Some(ref col_name) = field.attrs.column_name { + sea_orm_parts.push(quote! { column_name = #col_name }); + } + + // Combine column_type_attr with other attributes + let field_attr = if sea_orm_parts.is_empty() { + column_type_attr.unwrap_or_default() + } else if let Some(col_type) = column_type_attr { + // Extract the column_type value and combine + let col_type_str = col_type.to_string(); + if col_type_str.contains("column_type") { + // Parse out the column_type value + let combined = quote! { + #[sea_orm(#(#sea_orm_parts),*)] + #col_type + }; + combined + } else { + quote! { #[sea_orm(#(#sea_orm_parts),*)] } + } + } else { + quote! { #[sea_orm(#(#sea_orm_parts),*)] } + }; + + Some(quote! { + #field_attr + pub #field_name: #final_type, + }) + } + + FieldType::BelongsTo { + target: _, + optional, + } => { + // Generate foreign key column: author -> author_id + let fk_name = format_ident!("{}_id", field_name.to_string().to_snake_case()); + + if *optional { + Some(quote! { + pub #fk_name: Option, + }) + } else { + Some(quote! { + pub #fk_name: i32, + }) + } + } + + FieldType::HasMany { .. } => { + // has_many doesn't generate a column, just a relation + None + } + } +} + +fn generate_relation_variants(entity: &AnalyzedEntity, schema: &AnalyzedSchema) -> TokenStream { + let variants: Vec = entity + .fields + .iter() + .filter_map(|field| generate_relation_variant(field, entity, schema)) + .collect(); + + quote! { + #(#variants)* + } +} + +fn generate_relation_variant( + field: &AnalyzedField, + _entity: &AnalyzedEntity, + _schema: &AnalyzedSchema, +) -> Option { + match &field.ty { + FieldType::HasMany { target } => { + let variant_name = to_pascal_case(&field.name.to_string()); + let variant_ident = format_ident!("{}", variant_name); + let target_mod_str = target.to_string().to_snake_case(); + let has_many_path = format!("super::{}::Entity", target_mod_str); + + Some(quote! { + #[sea_orm(has_many = #has_many_path)] + #variant_ident, + }) + } + + FieldType::BelongsTo { + target, + optional: _, + } => { + let variant_name = to_pascal_case(&field.name.to_string()); + let variant_ident = format_ident!("{}", variant_name); + let target_mod_str = target.to_string().to_snake_case(); + let belongs_to_path = format!("super::{}::Entity", target_mod_str); + let fk_column_str = format!( + "Column::{}", + to_pascal_case(&format!("{}_id", field.name.to_string().to_snake_case())) + ); + let to_column_str = format!("super::{}::Column::Id", target_mod_str); + + Some(quote! { + #[sea_orm( + belongs_to = #belongs_to_path, + from = #fk_column_str, + to = #to_column_str + )] + #variant_ident, + }) + } + + FieldType::Scalar { .. } => None, + } +} + +fn generate_related_impls(entity: &AnalyzedEntity, _schema: &AnalyzedSchema) -> TokenStream { + let impls: Vec = entity + .fields + .iter() + .filter_map(generate_related_impl) + .collect(); + + quote! { + #(#impls)* + } +} + +fn generate_related_impl(field: &AnalyzedField) -> Option { + let variant_name = to_pascal_case(&field.name.to_string()); + let variant_ident = format_ident!("{}", variant_name); + + match &field.ty { + FieldType::HasMany { target } | FieldType::BelongsTo { target, .. } => { + let target_mod = format_ident!("{}", target.to_string().to_snake_case()); + + Some(quote! { + impl Related for Entity { + fn to() -> RelationDef { + Relation::#variant_ident.def() + } + } + }) + } + FieldType::Scalar { .. } => None, + } +} + +/// Convert snake_case or camelCase to PascalCase. +fn to_pascal_case(s: &str) -> String { + let mut result = String::new(); + let mut capitalize_next = true; + + for c in s.chars() { + if c == '_' { + capitalize_next = true; + } else if capitalize_next { + result.push(c.to_ascii_uppercase()); + capitalize_next = false; + } else { + result.push(c); + } + } + + result +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::schema::analyze::analyze_schema; + use crate::schema::parse::parse_schema; + use quote::quote; + + #[test] + fn test_generate_simple_entity() { + let input = quote! { + User { + email: String, + name: String, + } + }; + + let parsed = parse_schema(input).unwrap(); + let analyzed = analyze_schema(parsed).unwrap(); + let generated = generate_schema(analyzed); + let output = generated.to_string(); + + assert!(output.contains("pub mod user")); + assert!(output.contains("table_name = \"users\"")); + assert!(output.contains("pub email : String")); + assert!(output.contains("pub name : String")); + assert!(output.contains("pub id : i32")); + assert!(output.contains("pub created_at : DateTimeUtc")); + assert!(output.contains("pub updated_at : DateTimeUtc")); + } + + #[test] + fn test_generate_text_column() { + let input = quote! { + Post { + content: Text, + } + }; + + let parsed = parse_schema(input).unwrap(); + let analyzed = analyze_schema(parsed).unwrap(); + let generated = generate_schema(analyzed); + let output = generated.to_string(); + + assert!(output.contains("column_type = \"Text\"")); + assert!(output.contains("pub content : String")); + } + + #[test] + fn test_generate_belongs_to() { + let input = quote! { + User { + email: String, + } + + Post { + title: String, + author: User, + } + }; + + let parsed = parse_schema(input).unwrap(); + let analyzed = analyze_schema(parsed).unwrap(); + let generated = generate_schema(analyzed); + let output = generated.to_string(); + + assert!(output.contains("pub author_id : i32")); + assert!(output.contains("belongs_to = \"super::user::Entity\"")); + assert!(output.contains("from = \"Column::AuthorId\"")); + assert!(output.contains("to = \"super::user::Column::Id\"")); + } + + #[test] + fn test_generate_has_many() { + let input = quote! { + User { + email: String, + posts: Vec, + } + + Post { + title: String, + } + }; + + let parsed = parse_schema(input).unwrap(); + let analyzed = analyze_schema(parsed).unwrap(); + let generated = generate_schema(analyzed); + let output = generated.to_string(); + + assert!(output.contains("has_many = \"super::post::Entity\"")); + assert!(output.contains("impl Related < super :: post :: Entity >")); + } + + #[test] + fn test_generate_optional_belongs_to() { + let input = quote! { + User { + email: String, + } + + Comment { + author: Option, + } + }; + + let parsed = parse_schema(input).unwrap(); + let analyzed = analyze_schema(parsed).unwrap(); + let generated = generate_schema(analyzed); + let output = generated.to_string(); + + assert!(output.contains("pub author_id : Option < i32 >")); + } + + #[test] + fn test_generate_custom_table_name() { + let input = quote! { + #[table_name = "people"] + Person { + name: String, + } + }; + + let parsed = parse_schema(input).unwrap(); + let analyzed = analyze_schema(parsed).unwrap(); + let generated = generate_schema(analyzed); + let output = generated.to_string(); + + assert!(output.contains("table_name = \"people\"")); + assert!(!output.contains("table_name = \"persons\"")); + } + + #[test] + fn test_generate_unique_field() { + let input = quote! { + User { + #[unique] + email: String, + } + }; + + let parsed = parse_schema(input).unwrap(); + let analyzed = analyze_schema(parsed).unwrap(); + let generated = generate_schema(analyzed); + let output = generated.to_string(); + + assert!(output.contains("unique")); + } + + #[test] + fn test_generate_custom_column_name() { + let input = quote! { + User { + #[column = "user_email"] + email: String, + } + }; + + let parsed = parse_schema(input).unwrap(); + let analyzed = analyze_schema(parsed).unwrap(); + let generated = generate_schema(analyzed); + let output = generated.to_string(); + + assert!(output.contains("column_name = \"user_email\"")); + } + + #[test] + fn test_to_pascal_case() { + assert_eq!(to_pascal_case("hello_world"), "HelloWorld"); + assert_eq!(to_pascal_case("user"), "User"); + assert_eq!(to_pascal_case("author_id"), "AuthorId"); + } + + #[test] + fn test_generate_no_timestamps() { + let input = quote! { + #[timestamps(none)] + User { + email: String, + } + }; + + let parsed = parse_schema(input).unwrap(); + let analyzed = analyze_schema(parsed).unwrap(); + let generated = generate_schema(analyzed); + let output = generated.to_string(); + + assert!(!output.contains("created_at")); + assert!(!output.contains("updated_at")); + } + + #[test] + fn test_generate_only_created_at() { + let input = quote! { + #[timestamps(created_at)] + User { + email: String, + } + }; + + let parsed = parse_schema(input).unwrap(); + let analyzed = analyze_schema(parsed).unwrap(); + let generated = generate_schema(analyzed); + let output = generated.to_string(); + + assert!(output.contains("created_at")); + assert!(!output.contains("updated_at")); + } + + #[test] + fn test_generate_only_updated_at() { + let input = quote! { + #[timestamps(updated_at)] + User { + email: String, + } + }; + + let parsed = parse_schema(input).unwrap(); + let analyzed = analyze_schema(parsed).unwrap(); + let generated = generate_schema(analyzed); + let output = generated.to_string(); + + assert!(!output.contains("created_at")); + assert!(output.contains("updated_at")); + } + + #[test] + fn test_generate_indexed_field() { + let input = quote! { + User { + #[index] + email: String, + } + }; + + let parsed = parse_schema(input).unwrap(); + let analyzed = analyze_schema(parsed).unwrap(); + let generated = generate_schema(analyzed); + let output = generated.to_string(); + + assert!(output.contains("indexed")); + } +} diff --git a/rapina-macros/src/schema/mod.rs b/rapina-macros/src/schema/mod.rs new file mode 100644 index 0000000..bbb5d88 --- /dev/null +++ b/rapina-macros/src/schema/mod.rs @@ -0,0 +1,30 @@ +//! Schema macro for defining database entities with Prisma-like syntax. +//! +//! This module provides the `schema!` macro that generates SeaORM entity definitions +//! from a concise, declarative syntax where types indicate relationships. + +mod analyze; +mod generate; +mod parse; +mod types; + +use proc_macro2::TokenStream; + +pub use analyze::analyze_schema; +pub use generate::generate_schema; +pub use parse::parse_schema; + +/// Entry point for the schema macro implementation. +pub fn schema_impl(input: TokenStream) -> TokenStream { + let parsed = match parse_schema(input) { + Ok(schema) => schema, + Err(err) => return err.to_compile_error(), + }; + + let analyzed = match analyze_schema(parsed) { + Ok(schema) => schema, + Err(err) => return err.to_compile_error(), + }; + + generate_schema(analyzed) +} diff --git a/rapina-macros/src/schema/parse.rs b/rapina-macros/src/schema/parse.rs new file mode 100644 index 0000000..eb0608f --- /dev/null +++ b/rapina-macros/src/schema/parse.rs @@ -0,0 +1,621 @@ +//! Parsing layer for the schema macro. +//! +//! Handles custom syn parsing for entity definitions. + +use proc_macro2::{Span, TokenStream}; +use syn::parse::{Parse, ParseStream}; +use syn::punctuated::Punctuated; +use syn::{Ident, Result, Token, braced}; + +use super::types::{ScalarType, is_reserved_field}; + +/// A complete schema definition containing multiple entities. +#[derive(Debug)] +pub struct Schema { + pub entities: Vec, +} + +/// Attributes that can be applied to an entity. +#[derive(Debug, Clone)] +pub struct EntityAttrs { + /// Custom table name, e.g., #[table_name = "people"] + pub table_name: Option, + /// Include created_at timestamp (default: true) + pub has_created_at: bool, + /// Include updated_at timestamp (default: true) + pub has_updated_at: bool, +} + +impl Default for EntityAttrs { + fn default() -> Self { + Self { + table_name: None, + has_created_at: true, + has_updated_at: true, + } + } +} + +/// Attributes that can be applied to a field. +#[derive(Debug, Default, Clone)] +pub struct FieldAttrs { + /// Mark field as unique, e.g., #[unique] + pub unique: bool, + /// Custom column name, e.g., #[column = "email_address"] + pub column_name: Option, + /// Mark field as indexed, e.g., #[index] + pub indexed: bool, +} + +/// A single entity definition. +#[derive(Debug)] +pub struct EntityDef { + pub attrs: EntityAttrs, + pub name: Ident, + pub fields: Vec, + pub span: Span, +} + +/// A field within an entity. +#[derive(Debug)] +pub struct FieldDef { + pub attrs: FieldAttrs, + pub name: Ident, + pub ty: RawFieldType, + pub span: Span, +} + +/// Raw field type before entity resolution. +/// At parse time, we don't know if a type like `User` is an entity or invalid. +#[derive(Debug)] +pub enum RawFieldType { + /// A known scalar type (String, i32, etc.) + Scalar { scalar: ScalarType, optional: bool }, + /// Vec - will become has_many if T is an entity + Vec { inner: Ident }, + /// T or Option where T is unknown - needs resolution + Unknown { name: Ident, optional: bool }, +} + +impl Parse for Schema { + fn parse(input: ParseStream) -> Result { + let mut entities = Vec::new(); + + while !input.is_empty() { + entities.push(input.parse()?); + } + + if entities.is_empty() { + return Err(syn::Error::new( + Span::call_site(), + "schema! macro requires at least one entity definition", + )); + } + + Ok(Schema { entities }) + } +} + +impl Parse for EntityDef { + fn parse(input: ParseStream) -> Result { + // Parse entity attributes + let attrs = parse_entity_attrs(input)?; + + let name: Ident = input.parse()?; + let span = name.span(); + + let content; + braced!(content in input); + + let fields_punctuated: Punctuated = + content.parse_terminated(FieldDef::parse, Token![,])?; + + let fields: Vec = fields_punctuated.into_iter().collect(); + + // Check for reserved field names + for field in &fields { + let field_name = field.name.to_string(); + if is_reserved_field(&field_name) { + return Err(syn::Error::new( + field.name.span(), + format!( + "field '{}' is reserved and automatically generated (id, created_at, updated_at)", + field_name + ), + )); + } + } + + // Check for duplicate field names + let mut seen_fields = std::collections::HashSet::new(); + for field in &fields { + let field_name = field.name.to_string(); + if !seen_fields.insert(field_name.clone()) { + return Err(syn::Error::new( + field.name.span(), + format!("duplicate field name '{}'", field_name), + )); + } + } + + Ok(EntityDef { + attrs, + name, + fields, + span, + }) + } +} + +/// Parse entity-level attributes like #[table_name = "people"] or #[timestamps(created_at)] +fn parse_entity_attrs(input: ParseStream) -> Result { + let mut attrs = EntityAttrs::default(); + + while input.peek(Token![#]) { + input.parse::()?; + let content; + syn::bracketed!(content in input); + + let attr_name: Ident = content.parse()?; + let attr_name_str = attr_name.to_string(); + + match attr_name_str.as_str() { + "table_name" => { + content.parse::()?; + let value: syn::LitStr = content.parse()?; + attrs.table_name = Some(value.value()); + } + "timestamps" => { + // Parse timestamps(created_at) or timestamps(updated_at) or timestamps(none) + let inner; + syn::parenthesized!(inner in content); + let ts_type: Ident = inner.parse()?; + let ts_str = ts_type.to_string(); + + match ts_str.as_str() { + "created_at" => { + attrs.has_created_at = true; + attrs.has_updated_at = false; + } + "updated_at" => { + attrs.has_created_at = false; + attrs.has_updated_at = true; + } + "none" => { + attrs.has_created_at = false; + attrs.has_updated_at = false; + } + _ => { + return Err(syn::Error::new( + ts_type.span(), + format!( + "unknown timestamps option '{}'. Supported: created_at, updated_at, none", + ts_str + ), + )); + } + } + } + _ => { + return Err(syn::Error::new( + attr_name.span(), + format!( + "unknown entity attribute '{}'. Supported: table_name, timestamps", + attr_name_str + ), + )); + } + } + } + + Ok(attrs) +} + +impl Parse for FieldDef { + fn parse(input: ParseStream) -> Result { + // Parse field attributes + let attrs = parse_field_attrs(input)?; + + let name: Ident = input.parse()?; + let span = name.span(); + input.parse::()?; + let ty = parse_field_type(input)?; + + Ok(FieldDef { + attrs, + name, + ty, + span, + }) + } +} + +/// Parse field-level attributes like #[unique] or #[column = "email_address"] +fn parse_field_attrs(input: ParseStream) -> Result { + let mut attrs = FieldAttrs::default(); + + while input.peek(Token![#]) { + input.parse::()?; + let content; + syn::bracketed!(content in input); + + let attr_name: Ident = content.parse()?; + let attr_name_str = attr_name.to_string(); + + match attr_name_str.as_str() { + "unique" => { + attrs.unique = true; + } + "index" => { + attrs.indexed = true; + } + "column" => { + content.parse::()?; + let value: syn::LitStr = content.parse()?; + attrs.column_name = Some(value.value()); + } + _ => { + return Err(syn::Error::new( + attr_name.span(), + format!( + "unknown field attribute '{}'. Supported: unique, index, column", + attr_name_str + ), + )); + } + } + } + + Ok(attrs) +} + +/// Parse a field type from the input stream. +fn parse_field_type(input: ParseStream) -> Result { + // Check for Option + if input.peek(Ident) { + let ident: Ident = input.parse()?; + let ident_str = ident.to_string(); + + if ident_str == "Option" { + // Parse Option + input.parse::()?; + let inner_type = parse_inner_type(input)?; + input.parse::]>()?; + + return match inner_type { + InnerType::Scalar(scalar) => Ok(RawFieldType::Scalar { + scalar, + optional: true, + }), + InnerType::Ident(name) => Ok(RawFieldType::Unknown { + name, + optional: true, + }), + }; + } + + if ident_str == "Vec" { + // Parse Vec + input.parse::()?; + let inner: Ident = input.parse()?; + input.parse::]>()?; + + return Ok(RawFieldType::Vec { inner }); + } + + // Try to parse as scalar + if let Some(scalar) = ScalarType::from_ident(&ident_str) { + return Ok(RawFieldType::Scalar { + scalar, + optional: false, + }); + } + + // Unknown type - might be an entity reference + Ok(RawFieldType::Unknown { + name: ident, + optional: false, + }) + } else { + Err(syn::Error::new(input.span(), "expected type")) + } +} + +enum InnerType { + Scalar(ScalarType), + Ident(Ident), +} + +fn parse_inner_type(input: ParseStream) -> Result { + let ident: Ident = input.parse()?; + let ident_str = ident.to_string(); + + if let Some(scalar) = ScalarType::from_ident(&ident_str) { + Ok(InnerType::Scalar(scalar)) + } else { + Ok(InnerType::Ident(ident)) + } +} + +/// Parse the schema from a token stream. +pub fn parse_schema(input: TokenStream) -> Result { + syn::parse2(input) +} + +#[cfg(test)] +mod tests { + use super::*; + use quote::quote; + + #[test] + fn test_parse_simple_entity() { + let input = quote! { + User { + email: String, + name: String, + } + }; + + let schema = parse_schema(input).unwrap(); + assert_eq!(schema.entities.len(), 1); + assert_eq!(schema.entities[0].name.to_string(), "User"); + assert_eq!(schema.entities[0].fields.len(), 2); + } + + #[test] + fn test_parse_multiple_entities() { + let input = quote! { + User { + email: String, + } + + Post { + title: String, + } + }; + + let schema = parse_schema(input).unwrap(); + assert_eq!(schema.entities.len(), 2); + } + + #[test] + fn test_parse_vec_field() { + let input = quote! { + User { + posts: Vec, + } + }; + + let schema = parse_schema(input).unwrap(); + let field = &schema.entities[0].fields[0]; + assert!(matches!(field.ty, RawFieldType::Vec { .. })); + } + + #[test] + fn test_parse_option_field() { + let input = quote! { + Post { + author: Option, + } + }; + + let schema = parse_schema(input).unwrap(); + let field = &schema.entities[0].fields[0]; + assert!(matches!( + field.ty, + RawFieldType::Unknown { optional: true, .. } + )); + } + + #[test] + fn test_reserved_field_error() { + let input = quote! { + User { + id: i32, + } + }; + + let result = parse_schema(input); + assert!(result.is_err()); + assert!(result.unwrap_err().to_string().contains("reserved")); + } + + #[test] + fn test_duplicate_field_error() { + let input = quote! { + User { + email: String, + email: String, + } + }; + + let result = parse_schema(input); + assert!(result.is_err()); + assert!(result.unwrap_err().to_string().contains("duplicate")); + } + + #[test] + fn test_parse_table_name_attr() { + let input = quote! { + #[table_name = "people"] + Person { + name: String, + } + }; + + let schema = parse_schema(input).unwrap(); + assert_eq!( + schema.entities[0].attrs.table_name, + Some("people".to_string()) + ); + } + + #[test] + fn test_parse_unique_attr() { + let input = quote! { + User { + #[unique] + email: String, + } + }; + + let schema = parse_schema(input).unwrap(); + assert!(schema.entities[0].fields[0].attrs.unique); + } + + #[test] + fn test_parse_column_attr() { + let input = quote! { + User { + #[column = "email_address"] + email: String, + } + }; + + let schema = parse_schema(input).unwrap(); + assert_eq!( + schema.entities[0].fields[0].attrs.column_name, + Some("email_address".to_string()) + ); + } + + #[test] + fn test_parse_multiple_field_attrs() { + let input = quote! { + User { + #[unique] + #[column = "user_email"] + email: String, + } + }; + + let schema = parse_schema(input).unwrap(); + let field = &schema.entities[0].fields[0]; + assert!(field.attrs.unique); + assert_eq!(field.attrs.column_name, Some("user_email".to_string())); + } + + #[test] + fn test_unknown_entity_attr_error() { + let input = quote! { + #[unknown_attr = "value"] + User { + email: String, + } + }; + + let result = parse_schema(input); + assert!(result.is_err()); + assert!( + result + .unwrap_err() + .to_string() + .contains("unknown entity attribute") + ); + } + + #[test] + fn test_unknown_field_attr_error() { + let input = quote! { + User { + #[unknown] + email: String, + } + }; + + let result = parse_schema(input); + assert!(result.is_err()); + assert!( + result + .unwrap_err() + .to_string() + .contains("unknown field attribute") + ); + } + + #[test] + fn test_parse_timestamps_created_at_only() { + let input = quote! { + #[timestamps(created_at)] + User { + name: String, + } + }; + + let schema = parse_schema(input).unwrap(); + assert!(schema.entities[0].attrs.has_created_at); + assert!(!schema.entities[0].attrs.has_updated_at); + } + + #[test] + fn test_parse_timestamps_updated_at_only() { + let input = quote! { + #[timestamps(updated_at)] + User { + name: String, + } + }; + + let schema = parse_schema(input).unwrap(); + assert!(!schema.entities[0].attrs.has_created_at); + assert!(schema.entities[0].attrs.has_updated_at); + } + + #[test] + fn test_parse_timestamps_none() { + let input = quote! { + #[timestamps(none)] + User { + name: String, + } + }; + + let schema = parse_schema(input).unwrap(); + assert!(!schema.entities[0].attrs.has_created_at); + assert!(!schema.entities[0].attrs.has_updated_at); + } + + #[test] + fn test_parse_index_attr() { + let input = quote! { + User { + #[index] + email: String, + } + }; + + let schema = parse_schema(input).unwrap(); + assert!(schema.entities[0].fields[0].attrs.indexed); + } + + #[test] + fn test_parse_combined_field_attrs() { + let input = quote! { + User { + #[unique] + #[index] + #[column = "user_email"] + email: String, + } + }; + + let schema = parse_schema(input).unwrap(); + let field = &schema.entities[0].fields[0]; + assert!(field.attrs.unique); + assert!(field.attrs.indexed); + assert_eq!(field.attrs.column_name, Some("user_email".to_string())); + } + + #[test] + fn test_default_timestamps_enabled() { + let input = quote! { + User { + name: String, + } + }; + + let schema = parse_schema(input).unwrap(); + assert!(schema.entities[0].attrs.has_created_at); + assert!(schema.entities[0].attrs.has_updated_at); + } +} diff --git a/rapina-macros/src/schema/types.rs b/rapina-macros/src/schema/types.rs new file mode 100644 index 0000000..87ef7aa --- /dev/null +++ b/rapina-macros/src/schema/types.rs @@ -0,0 +1,91 @@ +//! Type mapping for schema fields to Rust/SeaORM types. + +use proc_macro2::TokenStream; +use quote::quote; + +/// Scalar types supported in schema definitions. +#[derive(Debug, Clone, PartialEq)] +pub enum ScalarType { + String, + Text, + I32, + I64, + F32, + F64, + Bool, + Uuid, + DateTime, + Date, + Decimal, + Json, +} + +impl ScalarType { + /// Parse a type identifier into a scalar type. + pub fn from_ident(ident: &str) -> Option { + match ident { + "String" => Some(ScalarType::String), + "Text" => Some(ScalarType::Text), + "i32" => Some(ScalarType::I32), + "i64" => Some(ScalarType::I64), + "f32" => Some(ScalarType::F32), + "f64" => Some(ScalarType::F64), + "bool" => Some(ScalarType::Bool), + "Uuid" => Some(ScalarType::Uuid), + "DateTime" => Some(ScalarType::DateTime), + "Date" => Some(ScalarType::Date), + "Decimal" => Some(ScalarType::Decimal), + "Json" => Some(ScalarType::Json), + _ => None, + } + } + + /// Generate the Rust type for this scalar. + pub fn rust_type(&self) -> TokenStream { + match self { + ScalarType::String | ScalarType::Text => quote! { String }, + ScalarType::I32 => quote! { i32 }, + ScalarType::I64 => quote! { i64 }, + ScalarType::F32 => quote! { f32 }, + ScalarType::F64 => quote! { f64 }, + ScalarType::Bool => quote! { bool }, + ScalarType::Uuid => quote! { Uuid }, + ScalarType::DateTime => quote! { DateTimeUtc }, + ScalarType::Date => quote! { Date }, + ScalarType::Decimal => quote! { Decimal }, + ScalarType::Json => quote! { Json }, + } + } + + /// Generate SeaORM column type attribute if needed. + /// Returns None if the default column type is correct. + pub fn column_type_attr(&self) -> Option { + match self { + ScalarType::Text => Some(quote! { #[sea_orm(column_type = "Text")] }), + ScalarType::Decimal => { + Some(quote! { #[sea_orm(column_type = "Decimal(Some((19, 4)))")] }) + } + ScalarType::Json => Some(quote! { #[sea_orm(column_type = "Json")] }), + _ => None, + } + } +} + +/// Field type classification. +#[derive(Debug, Clone)] +pub enum FieldType { + /// A scalar database column (String, i32, etc.) + Scalar { scalar: ScalarType, optional: bool }, + /// A has_many relationship (Vec) + HasMany { target: syn::Ident }, + /// A belongs_to relationship (Entity or Option) + BelongsTo { target: syn::Ident, optional: bool }, +} + +/// Reserved field names that are auto-generated. +pub const RESERVED_FIELDS: &[&str] = &["id", "created_at", "updated_at"]; + +/// Check if a field name is reserved. +pub fn is_reserved_field(name: &str) -> bool { + RESERVED_FIELDS.contains(&name) +} diff --git a/rapina/Cargo.toml b/rapina/Cargo.toml index 4eea9be..a81d56b 100644 --- a/rapina/Cargo.toml +++ b/rapina/Cargo.toml @@ -56,3 +56,13 @@ flate2 = "1.1" # Our macros rapina-macros = { version = "0.2.0", path = "../rapina-macros/" } + +# Database (optional) +sea-orm = { version = "1.1", optional = true, features = ["runtime-tokio-rustls"] } + +[features] +default = [] +database = ["sea-orm"] +postgres = ["database", "sea-orm/sqlx-postgres"] +mysql = ["database", "sea-orm/sqlx-mysql"] +sqlite = ["database", "sea-orm/sqlx-sqlite"] diff --git a/rapina/src/app.rs b/rapina/src/app.rs index b2b142e..ddd62a5 100644 --- a/rapina/src/app.rs +++ b/rapina/src/app.rs @@ -209,6 +209,48 @@ impl Rapina { self } + /// Configures database connection with the given configuration. + /// + /// This method connects to the database and registers the connection + /// in the application state. Use the [`Db`](crate::database::Db) extractor + /// in your handlers to access the connection. + /// + /// # Example + /// + /// ```ignore + /// use rapina::prelude::*; + /// use rapina::database::{DatabaseConfig, Db}; + /// + /// #[get("/users")] + /// async fn list_users(db: Db) -> Result>> { + /// let users = UserEntity::find().all(db.conn()).await?; + /// Ok(Json(users)) + /// } + /// + /// #[tokio::main] + /// async fn main() -> std::io::Result<()> { + /// let db_config = DatabaseConfig::from_env()?; + /// + /// Rapina::new() + /// .with_database(db_config).await? + /// .router(router) + /// .listen("127.0.0.1:3000") + /// .await + /// } + /// ``` + #[cfg(feature = "database")] + pub async fn with_database( + mut self, + config: crate::database::DatabaseConfig, + ) -> Result { + let conn = config + .connect() + .await + .map_err(|e| std::io::Error::other(format!("Database connection failed: {}", e)))?; + self.state = self.state.with(conn); + Ok(self) + } + /// Starts the HTTP server on the given address. /// /// # Panics diff --git a/rapina/src/database.rs b/rapina/src/database.rs new file mode 100644 index 0000000..c4da0f9 --- /dev/null +++ b/rapina/src/database.rs @@ -0,0 +1,336 @@ +//! Database integration for Rapina applications. +//! +//! This module provides first-class SeaORM integration with: +//! - Environment-aware configuration (development, production, test) +//! - Connection pool management +//! - Automatic error conversion (no `.map_err()` needed) +//! +//! # Quick Start +//! +//! ```rust,ignore +//! use rapina::prelude::*; +//! use rapina::database::{DatabaseConfig, Db}; +//! +//! #[get("/users/:id")] +//! async fn get_user(id: Path, db: Db) -> Result> { +//! let user = UserEntity::find_by_id(id.into_inner()) +//! .one(db.conn()) +//! .await? // No .map_err() needed! +//! .ok_or_else(|| Error::not_found("user not found"))?; +//! Ok(Json(user.into())) +//! } +//! +//! #[tokio::main] +//! async fn main() -> std::io::Result<()> { +//! let db_config = DatabaseConfig::from_env()?; +//! +//! Rapina::new() +//! .with_database(db_config).await? +//! .router(router) +//! .listen("127.0.0.1:3000") +//! .await +//! } +//! ``` +//! +//! # Environment Configuration +//! +//! The database configuration is environment-aware: +//! +//! ```bash +//! # Required +//! DATABASE_URL=postgres://user:pass@localhost/myapp +//! +//! # Optional +//! DATABASE_MAX_CONNECTIONS=100 # default: 10 +//! DATABASE_MIN_CONNECTIONS=5 # default: 1 +//! DATABASE_CONNECT_TIMEOUT=30 # seconds, default: 30 +//! DATABASE_IDLE_TIMEOUT=600 # seconds, default: 600 +//! ``` + +use sea_orm::{ConnectOptions, Database, DatabaseConnection}; +use std::time::Duration; + +use crate::error::{Error, IntoApiError}; + +/// Database configuration with environment-aware defaults. +/// +/// Use `DatabaseConfig::from_env()` to load from environment variables, +/// or build manually for testing. +#[derive(Debug, Clone)] +pub struct DatabaseConfig { + /// Database connection URL (e.g., postgres://user:pass@host/db) + pub url: String, + /// Maximum number of connections in the pool (default: 10) + pub max_connections: u32, + /// Minimum number of connections to keep open (default: 1) + pub min_connections: u32, + /// Connection timeout in seconds (default: 30) + pub connect_timeout: u64, + /// Idle connection timeout in seconds (default: 600) + pub idle_timeout: u64, + /// Enable SQL query logging (default: true in debug, false in release) + pub sqlx_logging: bool, +} + +impl DatabaseConfig { + /// Creates a new database configuration with the given URL and defaults. + pub fn new(url: impl Into) -> Self { + Self { + url: url.into(), + max_connections: 10, + min_connections: 1, + connect_timeout: 30, + idle_timeout: 600, + sqlx_logging: cfg!(debug_assertions), + } + } + + /// Loads configuration from environment variables. + /// + /// Required: + /// - `DATABASE_URL`: The database connection string + /// + /// Optional: + /// - `DATABASE_MAX_CONNECTIONS`: Max pool size (default: 10) + /// - `DATABASE_MIN_CONNECTIONS`: Min pool size (default: 1) + /// - `DATABASE_CONNECT_TIMEOUT`: Connection timeout in seconds (default: 30) + /// - `DATABASE_IDLE_TIMEOUT`: Idle timeout in seconds (default: 600) + /// - `DATABASE_LOGGING`: Enable SQL logging (default: true in debug) + pub fn from_env() -> Result { + let url = std::env::var("DATABASE_URL").map_err(|_| { + std::io::Error::new( + std::io::ErrorKind::NotFound, + "DATABASE_URL environment variable not set", + ) + })?; + + let max_connections = std::env::var("DATABASE_MAX_CONNECTIONS") + .ok() + .and_then(|v| v.parse().ok()) + .unwrap_or(10); + + let min_connections = std::env::var("DATABASE_MIN_CONNECTIONS") + .ok() + .and_then(|v| v.parse().ok()) + .unwrap_or(1); + + let connect_timeout = std::env::var("DATABASE_CONNECT_TIMEOUT") + .ok() + .and_then(|v| v.parse().ok()) + .unwrap_or(30); + + let idle_timeout = std::env::var("DATABASE_IDLE_TIMEOUT") + .ok() + .and_then(|v| v.parse().ok()) + .unwrap_or(600); + + let sqlx_logging = std::env::var("DATABASE_LOGGING") + .ok() + .and_then(|v| v.parse().ok()) + .unwrap_or(cfg!(debug_assertions)); + + Ok(Self { + url, + max_connections, + min_connections, + connect_timeout, + idle_timeout, + sqlx_logging, + }) + } + + /// Sets the maximum number of connections in the pool. + pub fn max_connections(mut self, n: u32) -> Self { + self.max_connections = n; + self + } + + /// Sets the minimum number of connections in the pool. + pub fn min_connections(mut self, n: u32) -> Self { + self.min_connections = n; + self + } + + /// Sets the connection timeout in seconds. + pub fn connect_timeout(mut self, secs: u64) -> Self { + self.connect_timeout = secs; + self + } + + /// Sets the idle connection timeout in seconds. + pub fn idle_timeout(mut self, secs: u64) -> Self { + self.idle_timeout = secs; + self + } + + /// Enables or disables SQL query logging. + pub fn sqlx_logging(mut self, enabled: bool) -> Self { + self.sqlx_logging = enabled; + self + } + + /// Connects to the database and returns a connection pool. + pub async fn connect(&self) -> Result { + let mut opts = ConnectOptions::new(&self.url); + opts.max_connections(self.max_connections) + .min_connections(self.min_connections) + .connect_timeout(Duration::from_secs(self.connect_timeout)) + .idle_timeout(Duration::from_secs(self.idle_timeout)) + .sqlx_logging(self.sqlx_logging); + + Database::connect(opts).await.map_err(DbError) + } +} + +/// Wrapper around SeaORM's `DbErr` for Rapina error integration. +/// +/// This type implements `IntoApiError`, allowing you to use `?` directly +/// on database operations without manual error mapping. +#[derive(Debug)] +pub struct DbError(pub sea_orm::DbErr); + +impl std::fmt::Display for DbError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl std::error::Error for DbError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + Some(&self.0) + } +} + +impl IntoApiError for DbError { + fn into_api_error(self) -> Error { + use sea_orm::DbErr; + + match &self.0 { + DbErr::RecordNotFound(msg) => Error::not_found(msg.clone()), + DbErr::RecordNotInserted => Error::internal("failed to insert record"), + DbErr::RecordNotUpdated => Error::internal("failed to update record"), + DbErr::Custom(msg) => Error::internal(msg.clone()), + DbErr::Query(err) => { + tracing::error!(error = %err, "database query error"); + Error::internal("database query failed") + } + DbErr::Conn(err) => { + tracing::error!(error = %err, "database connection error"); + Error::internal("database connection failed") + } + DbErr::Exec(err) => { + tracing::error!(error = %err, "database execution error"); + Error::internal("database operation failed") + } + _ => { + tracing::error!(error = %self.0, "database error"); + Error::internal("database error") + } + } + } +} + +impl From for DbError { + fn from(err: sea_orm::DbErr) -> Self { + DbError(err) + } +} + +/// Database connection extractor for handlers. +/// +/// Use this to access the database connection pool in your handlers. +/// +/// # Example +/// +/// ```rust,ignore +/// use rapina::prelude::*; +/// use rapina::database::Db; +/// +/// #[get("/users")] +/// async fn list_users(db: Db) -> Result>> { +/// let users = UserEntity::find() +/// .all(db.conn()) +/// .await?; +/// Ok(Json(users.into_iter().map(Into::into).collect())) +/// } +/// ``` +#[derive(Debug, Clone)] +pub struct Db(DatabaseConnection); + +impl Db { + /// Creates a new Db wrapper around a connection. + pub fn new(conn: DatabaseConnection) -> Self { + Self(conn) + } + + /// Returns a reference to the underlying database connection. + /// + /// Use this when calling SeaORM methods that take `&DatabaseConnection`. + pub fn conn(&self) -> &DatabaseConnection { + &self.0 + } + + /// Consumes the wrapper and returns the underlying connection. + pub fn into_inner(self) -> DatabaseConnection { + self.0 + } +} + +impl AsRef for Db { + fn as_ref(&self) -> &DatabaseConnection { + &self.0 + } +} + +impl std::ops::Deref for Db { + type Target = DatabaseConnection; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_database_config_new() { + let config = DatabaseConfig::new("postgres://localhost/test"); + assert_eq!(config.url, "postgres://localhost/test"); + assert_eq!(config.max_connections, 10); + assert_eq!(config.min_connections, 1); + } + + #[test] + fn test_database_config_builder() { + let config = DatabaseConfig::new("postgres://localhost/test") + .max_connections(50) + .min_connections(5) + .connect_timeout(60) + .idle_timeout(300) + .sqlx_logging(false); + + assert_eq!(config.max_connections, 50); + assert_eq!(config.min_connections, 5); + assert_eq!(config.connect_timeout, 60); + assert_eq!(config.idle_timeout, 300); + assert!(!config.sqlx_logging); + } + + #[test] + fn test_db_error_not_found() { + let err = DbError(sea_orm::DbErr::RecordNotFound("user".to_string())); + let api_err = err.into_api_error(); + assert_eq!(api_err.status, 404); + assert_eq!(api_err.code, "NOT_FOUND"); + } + + #[test] + fn test_db_error_custom() { + let err = DbError(sea_orm::DbErr::Custom("something went wrong".to_string())); + let api_err = err.into_api_error(); + assert_eq!(api_err.status, 500); + assert_eq!(api_err.message, "something went wrong"); + } +} diff --git a/rapina/src/extract.rs b/rapina/src/extract.rs index 98572c1..0180eb8 100644 --- a/rapina/src/extract.rs +++ b/rapina/src/extract.rs @@ -595,6 +595,25 @@ pub fn extract_path_params(pattern: &str, path: &str) -> Option { Some(params) } +// Database extractor (requires "database" feature) +#[cfg(feature = "database")] +impl FromRequestParts for crate::database::Db { + async fn from_request_parts( + _parts: &http::request::Parts, + _params: &PathParams, + state: &Arc, + ) -> Result { + use sea_orm::DatabaseConnection; + + let conn = state.get::().ok_or_else(|| { + Error::internal( + "Database connection not configured. Did you forget to call .with_database()?", + ) + })?; + Ok(crate::database::Db::new(conn.clone())) + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/rapina/src/lib.rs b/rapina/src/lib.rs index ebaac18..aa8a9bc 100644 --- a/rapina/src/lib.rs +++ b/rapina/src/lib.rs @@ -66,7 +66,6 @@ //! - [`TraceIdMiddleware`](middleware::TraceIdMiddleware) - Add trace IDs to requests //! - [`RequestLogMiddleware`](middleware::RequestLogMiddleware) - Structured request logging //! - [`RateLimitMiddleware`](middleware::RateLimitMiddleware) - Token bucket rate limiting -//! - [`CompressionMiddleware`](middleware::CompressionMiddleware) - Response compression (gzip, deflate) //! //! ## Introspection //! @@ -84,6 +83,8 @@ pub mod app; pub mod auth; pub mod config; pub mod context; +#[cfg(feature = "database")] +pub mod database; pub mod error; pub mod extract; pub mod handler; @@ -116,9 +117,7 @@ pub mod prelude { pub use crate::error::{DocumentedError, Error, ErrorVariant, IntoApiError, Result}; pub use crate::extract::{Context, Cookie, Form, Headers, Json, Path, Query, State, Validated}; pub use crate::introspection::RouteInfo; - pub use crate::middleware::{ - CompressionConfig, KeyExtractor, Middleware, Next, RateLimitConfig, - }; + pub use crate::middleware::{KeyExtractor, Middleware, Next, RateLimitConfig}; pub use crate::observability::TracingConfig; pub use crate::response::IntoResponse; pub use crate::router::Router; @@ -129,9 +128,14 @@ pub mod prelude { pub use tracing; pub use validator::Validate; - pub use rapina_macros::{Config, delete, get, post, public, put}; + pub use rapina_macros::{Config, delete, get, post, public, put, schema}; } -// Re-export schemars and http crates so users don't need to add them to their Cargo.toml +// Re-export dependencies so users don't need to add them to their Cargo.toml pub use http; +pub use hyper; pub use schemars; + +// Re-export sea-orm when database feature is enabled +#[cfg(feature = "database")] +pub use sea_orm; diff --git a/rapina/tests/schema_test.rs b/rapina/tests/schema_test.rs new file mode 100644 index 0000000..7dd3745 --- /dev/null +++ b/rapina/tests/schema_test.rs @@ -0,0 +1,124 @@ +//! Integration tests for the schema! macro. +//! +//! These tests verify that the generated code compiles and matches SeaORM patterns. + +#![cfg(feature = "database")] + +use rapina::prelude::*; +use rapina::sea_orm::entity::prelude::*; + +// Define a test schema with various relationship types +schema! { + TestUser { + email: String, + name: String, + bio: Option, + posts: Vec, + comments: Vec, + } + + TestPost { + title: String, + content: Text, + published: bool, + author: TestUser, + comments: Vec, + } + + TestComment { + content: Text, + post: TestPost, + author: Option, + } +} + +#[test] +fn test_user_model_compiles() { + use test_user::Model; + + // Verify the Model struct has the expected fields + let user = Model { + id: 1, + email: "test@example.com".to_string(), + name: "Test User".to_string(), + bio: Some("A test user".to_string()), + created_at: DateTimeUtc::default(), + updated_at: DateTimeUtc::default(), + }; + + assert_eq!(user.id, 1); + assert_eq!(user.email, "test@example.com"); +} + +#[test] +fn test_post_model_has_foreign_key() { + use test_post::Model; + + // Verify the belongs_to relationship generates author_id + let post = Model { + id: 1, + title: "Test Post".to_string(), + content: "Test content".to_string(), + published: true, + author_id: 1, // Foreign key from belongs_to + created_at: DateTimeUtc::default(), + updated_at: DateTimeUtc::default(), + }; + + assert_eq!(post.author_id, 1); +} + +#[test] +fn test_comment_model_has_optional_foreign_key() { + use test_comment::Model; + + // Verify optional belongs_to generates Option FK + let comment_with_author = Model { + id: 1, + content: "Great post!".to_string(), + post_id: 1, + author_id: Some(1), // Optional FK + created_at: DateTimeUtc::default(), + updated_at: DateTimeUtc::default(), + }; + + let comment_without_author = Model { + id: 2, + content: "Anonymous comment".to_string(), + post_id: 1, + author_id: None, + created_at: DateTimeUtc::default(), + updated_at: DateTimeUtc::default(), + }; + + assert_eq!(comment_with_author.author_id, Some(1)); + assert_eq!(comment_without_author.author_id, None); +} + +#[test] +fn test_relation_enum_exists() { + // Verify Relation enums are generated with expected variants + use test_comment::Relation as CommentRelation; + use test_post::Relation as PostRelation; + use test_user::Relation as UserRelation; + + // User has Posts and Comments (has_many) + let _ = UserRelation::Posts; + let _ = UserRelation::Comments; + + // Post has Author (belongs_to) and Comments (has_many) + let _ = PostRelation::Author; + let _ = PostRelation::Comments; + + // Comment has Post (belongs_to) and Author (optional belongs_to) + let _ = CommentRelation::Post; + let _ = CommentRelation::Author; +} + +#[test] +fn test_entity_traits_implemented() { + // Verify Entity trait is implemented via EntityName + let _ = test_user::Entity::table_name(&test_user::Entity); + let _ = test_post::Entity::table_name(&test_post::Entity); + let _ = test_comment::Entity::table_name(&test_comment::Entity); +}