diff --git a/.changepacks/changepack_log_uSPS8MbuVfok1u5y_oI3r.json b/.changepacks/changepack_log_uSPS8MbuVfok1u5y_oI3r.json new file mode 100644 index 0000000..6d7682a --- /dev/null +++ b/.changepacks/changepack_log_uSPS8MbuVfok1u5y_oI3r.json @@ -0,0 +1 @@ +{"changes":{"crates/vespera_macro/Cargo.toml":"Patch","crates/vespera/Cargo.toml":"Patch","crates/vespera_core/Cargo.toml":"Patch"},"note":"Implement schema type","date":"2026-01-27T14:22:54.953833200Z"} \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index f38855c..e18da50 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,17 @@ # It is not intended for manual editing. version = 4 +[[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" @@ -11,6 +22,18 @@ 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" @@ -26,6 +49,54 @@ version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +[[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" @@ -96,6 +167,7 @@ version = "0.1.0" dependencies = [ "axum-test", "insta", + "sea-orm", "serde", "serde_json", "third", @@ -168,11 +240,46 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64ct" +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 = "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" @@ -183,12 +290,63 @@ dependencies = [ "generic-array", ] +[[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" @@ -217,6 +375,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "chrono" version = "0.4.43" @@ -226,10 +390,20 @@ dependencies = [ "iana-time-zone", "js-sys", "num-traits", + "serde", "wasm-bindgen", "windows-link", ] +[[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 = "console" version = "0.15.11" @@ -242,6 +416,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + [[package]] name = "cookie" version = "0.18.1" @@ -268,6 +448,36 @@ 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 = "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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + [[package]] name = "crypto-common" version = "0.1.7" @@ -278,6 +488,51 @@ dependencies = [ "typenum", ] +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + [[package]] name = "deranged" version = "0.5.5" @@ -285,6 +540,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]] @@ -300,7 +578,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", + "const-oid", "crypto-common", + "subtle", ] [[package]] @@ -311,7 +591,22 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", +] + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +dependencies = [ + "serde", ] [[package]] @@ -365,6 +660,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 = "expect-json" version = "1.9.0" @@ -391,7 +708,7 @@ checksum = "f464e1e518bc97a6749590758411784df7dda4f36384e1fb11a58f040c1d0459" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", ] [[package]] @@ -406,6 +723,29 @@ version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db" +[[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" @@ -415,6 +755,12 @@ dependencies = [ "percent-encoding", ] +[[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" @@ -422,6 +768,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", + "futures-sink", ] [[package]] @@ -430,6 +777,28 @@ 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" @@ -444,9 +813,15 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", ] +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + [[package]] name = "futures-task" version = "0.3.31" @@ -468,6 +843,7 @@ dependencies = [ "futures-core", "futures-io", "futures-macro", + "futures-sink", "futures-task", "memchr", "pin-project-lite", @@ -485,6 +861,17 @@ dependencies = [ "version_check", ] +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "getrandom" version = "0.3.4" @@ -503,12 +890,41 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" +[[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.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 = "headers" version = "0.4.1" @@ -534,8 +950,53 @@ dependencies = [ ] [[package]] -name = "http" -version = "1.4.0" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +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" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" dependencies = [ @@ -726,6 +1187,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "1.1.0" @@ -754,7 +1221,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" dependencies = [ "equivalent", - "hashbrown", + "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]] @@ -778,6 +1256,15 @@ dependencies = [ "rustversion", ] +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.17" @@ -794,12 +1281,49 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] + [[package]] name = "libc" version = "0.2.180" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libredox" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" +dependencies = [ + "bitflags", + "libc", + "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 = "linux-raw-sys" version = "0.11.0" @@ -833,6 +1357,16 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" +[[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" @@ -897,6 +1431,22 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-bigint-dig" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" +dependencies = [ + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.5", + "smallvec", + "zeroize", +] + [[package]] name = "num-complex" version = "0.4.6" @@ -950,6 +1500,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -958,6 +1509,45 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[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 = "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" @@ -976,17 +1566,35 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.5.18", "smallvec", "windows-link", ] +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + [[package]] name = "percent-encoding" 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" @@ -999,6 +1607,43 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "pluralizer" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b3eba432a00a1f6c16f39147847a870e94e2e9b992759b503e330efec778cbe" +dependencies = [ + "once_cell", + "regex", +] + [[package]] name = "potential_utf" version = "0.1.4" @@ -1042,6 +1687,28 @@ dependencies = [ "toml_edit", ] +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn 2.0.114", +] + [[package]] name = "proc-macro2" version = "1.0.106" @@ -1051,6 +1718,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" @@ -1066,14 +1766,41 @@ 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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + [[package]] name = "rand" version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ - "rand_chacha", - "rand_core", + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", ] [[package]] @@ -1083,7 +1810,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", ] [[package]] @@ -1092,7 +1828,7 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" dependencies = [ - "getrandom", + "getrandom 0.3.4", ] [[package]] @@ -1104,6 +1840,15 @@ dependencies = [ "bitflags", ] +[[package]] +name = "redox_syscall" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f3fe0889e69e2ae9e41f4d6c4c0181701d00e4697b356fb1f74173a5e0ee27" +dependencies = [ + "bitflags", +] + [[package]] name = "regex" version = "1.12.2" @@ -1139,6 +1884,15 @@ version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" +[[package]] +name = "rend" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c" +dependencies = [ + "bytecheck", +] + [[package]] name = "reserve-port" version = "2.3.0" @@ -1148,6 +1902,69 @@ dependencies = [ "thiserror", ] +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "signature", + "spki", + "subtle", + "zeroize", +] + [[package]] name = "rstest" version = "0.26.1" @@ -1173,7 +1990,7 @@ dependencies = [ "regex", "relative-path", "rustc_version", - "syn", + "syn 2.0.114", "unicode-ident", ] @@ -1188,10 +2005,26 @@ dependencies = [ "futures-util", "http", "mime", - "rand", + "rand 0.9.2", "thiserror", ] +[[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 0.8.5", + "rkyv", + "serde", + "serde_json", +] + [[package]] name = "rustc_version" version = "0.4.1" @@ -1215,10 +2048,44 @@ dependencies = [ ] [[package]] -name = "rustversion" -version = "1.0.22" +name = "rustls" +version = "0.23.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" @@ -1232,6 +2099,138 @@ 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 = "2.0.0-rc.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4bb965a287ae073c738851c5d38037ac6da66c9841ac1de7c13c8d08862180a" +dependencies = [ + "async-stream", + "async-trait", + "bigdecimal", + "chrono", + "derive_more", + "futures-util", + "itertools", + "log", + "ouroboros", + "pgvector", + "rust_decimal", + "sea-orm-macros", + "sea-query", + "sea-query-sqlx", + "sea-schema", + "serde", + "serde_json", + "sqlx", + "strum", + "thiserror", + "time", + "tracing", + "url", + "uuid", +] + +[[package]] +name = "sea-orm-macros" +version = "2.0.0-rc.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3e208f041129ad7962b6951f0b392e9ff97a8337bd8c7022c61e7b02ab29fe0" +dependencies = [ + "heck 0.5.0", + "itertools", + "pluralizer", + "proc-macro2", + "quote", + "sea-bae", + "syn 2.0.114", + "unicode-ident", +] + +[[package]] +name = "sea-query" +version = "1.0.0-rc.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6a067a2f6f13250f615f0bedb5bc3a6c872fec70776d0b43b43caeaa699e232" +dependencies = [ + "chrono", + "inherent", + "ordered-float", + "rust_decimal", + "sea-query-derive", + "serde_json", + "time", + "uuid", +] + +[[package]] +name = "sea-query-derive" +version = "1.0.0-rc.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d88ad44b6ad9788c8b9476b6b91f94c7461d1e19d39cd8ea37838b1e6ff5aa8" +dependencies = [ + "darling", + "heck 0.4.1", + "proc-macro2", + "quote", + "syn 2.0.114", + "thiserror", +] + +[[package]] +name = "sea-query-sqlx" +version = "0.8.0-rc.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4377164b09a11bb692dec6966eb0e6908d63d768defef0be689b39e02cf8544" +dependencies = [ + "sea-query", + "sqlx", +] + +[[package]] +name = "sea-schema" +version = "0.17.0-rc.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b363dd21c20fe4d1488819cb2bc7f8d4696c62dd9f39554f97639f54d57dd0ab" +dependencies = [ + "async-trait", + "sea-query", + "sea-query-sqlx", + "sea-schema-derive", + "sqlx", +] + +[[package]] +name = "sea-schema-derive" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "debdc8729c37fdbf88472f97fd470393089f997a909e535ff67c544d18cfccf0" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "seahash" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" + [[package]] name = "semver" version = "1.0.27" @@ -1261,129 +2260,417 @@ dependencies = [ name = "serde_derive" version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "serde_html_form" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2f2d7ff8a2140333718bb329f5c40fc5f0865b84c426183ce14c97d2ab8154f" +dependencies = [ + "form_urlencoded", + "indexmap", + "itoa", + "ryu", + "serde_core", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core 0.6.4", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "similar" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +dependencies = [ + "serde", +] + +[[package]] +name = "socket2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "sqlx" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" +dependencies = [ + "base64", + "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", - "syn", + "sqlx-core", + "sqlx-macros-core", + "syn 2.0.114", ] [[package]] -name = "serde_html_form" -version = "0.2.8" +name = "sqlx-macros-core" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2f2d7ff8a2140333718bb329f5c40fc5f0865b84c426183ce14c97d2ab8154f" +checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" dependencies = [ - "form_urlencoded", - "indexmap", - "itoa", - "ryu", - "serde_core", + "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 = "serde_json" -version = "1.0.149" +name = "sqlx-mysql" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" dependencies = [ + "atoi", + "base64", + "bitflags", + "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 0.8.5", + "rsa", + "rust_decimal", "serde", - "serde_core", - "zmij", + "sha1", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "time", + "tracing", + "uuid", + "whoami", ] [[package]] -name = "serde_path_to_error" -version = "0.1.20" +name = "sqlx-postgres" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" dependencies = [ + "atoi", + "base64", + "bitflags", + "byteorder", + "chrono", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-util", + "hex", + "hkdf", + "hmac", + "home", "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "rand 0.8.5", + "rust_decimal", "serde", - "serde_core", + "serde_json", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "time", + "tracing", + "uuid", + "whoami", ] [[package]] -name = "serde_urlencoded" -version = "0.7.1" +name = "sqlx-sqlite" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" dependencies = [ - "form_urlencoded", - "itoa", - "ryu", + "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 = "sha1" -version = "0.10.6" +name = "stable_deref_trait" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] -name = "shlex" -version = "1.3.0" +name = "static_assertions" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] -name = "signal-hook-registry" -version = "1.4.8" +name = "stringprep" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" dependencies = [ - "errno", - "libc", + "unicode-bidi", + "unicode-normalization", + "unicode-properties", ] [[package]] -name = "similar" -version = "2.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" - -[[package]] -name = "slab" -version = "0.4.11" +name = "strum" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" [[package]] -name = "smallvec" -version = "1.15.1" +name = "subtle" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] -name = "socket2" -version = "0.6.1" +name = "syn" +version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ - "libc", - "windows-sys 0.60.2", + "proc-macro2", + "quote", + "unicode-ident", ] -[[package]] -name = "spin" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" - -[[package]] -name = "stable_deref_trait" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" - [[package]] name = "syn" version = "2.0.114" @@ -1409,9 +2696,15 @@ 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 = "tempfile" version = "3.24.0" @@ -1419,7 +2712,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" dependencies = [ "fastrand", - "getrandom", + "getrandom 0.3.4", "once_cell", "rustix", "windows-sys 0.61.2", @@ -1454,7 +2747,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", ] [[package]] @@ -1498,6 +2791,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" @@ -1523,7 +2831,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]] @@ -1606,9 +2925,21 @@ checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "log", "pin-project-lite", + "tracing-attributes", "tracing-core", ] +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + [[package]] name = "tracing-core" version = "0.1.36" @@ -1657,15 +2988,48 @@ checksum = "27a7a9b72ba121f6f1f6c3632b85604cac41aedb5ddc70accbebb6cac83de846" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", ] +[[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" @@ -1691,9 +3055,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" dependencies = [ "js-sys", + "serde_core", "wasm-bindgen", ] +[[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" @@ -1702,7 +3073,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "vespera" -version = "0.1.23" +version = "0.1.24" dependencies = [ "axum", "axum-extra", @@ -1715,7 +3086,7 @@ dependencies = [ [[package]] name = "vespera_core" -version = "0.1.23" +version = "0.1.24" dependencies = [ "rstest", "serde", @@ -1724,7 +3095,7 @@ dependencies = [ [[package]] name = "vespera_macro" -version = "0.1.23" +version = "0.1.24" dependencies = [ "anyhow", "insta", @@ -1733,7 +3104,7 @@ dependencies = [ "rstest", "serde", "serde_json", - "syn", + "syn 2.0.114", "tempfile", "vespera_core", ] @@ -1762,6 +3133,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.106" @@ -1794,7 +3171,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn", + "syn 2.0.114", "wasm-bindgen-shared", ] @@ -1807,6 +3184,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.5", +] + +[[package]] +name = "webpki-roots" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12bed680863276c63889429bfd6cab3b99943659923822de1c8a39c49e4d722c" +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 = "windows-core" version = "0.62.2" @@ -1828,7 +3233,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", ] [[package]] @@ -1839,7 +3244,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", ] [[package]] @@ -1866,6 +3271,24 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +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.59.0" @@ -1893,6 +3316,21 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + [[package]] name = "windows-targets" version = "0.52.6" @@ -1926,6 +3364,12 @@ dependencies = [ "windows_x86_64_msvc 0.53.1", ] +[[package]] +name = "windows_aarch64_gnullvm" +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" @@ -1938,6 +3382,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" +[[package]] +name = "windows_aarch64_msvc" +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" @@ -1950,6 +3400,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" +[[package]] +name = "windows_i686_gnu" +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" @@ -1974,6 +3430,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" +[[package]] +name = "windows_i686_msvc" +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" @@ -1986,6 +3448,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" +[[package]] +name = "windows_x86_64_gnu" +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" @@ -1998,6 +3466,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" +[[package]] +name = "windows_x86_64_gnullvm" +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" @@ -2010,6 +3484,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" +[[package]] +name = "windows_x86_64_msvc" +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" @@ -2043,6 +3523,15 @@ 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" @@ -2068,7 +3557,7 @@ checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", "synstructure", ] @@ -2089,7 +3578,7 @@ checksum = "2c7962b26b0a8685668b671ee4b54d007a67d4eaf05fda79ac0ecf41e32270f1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", ] [[package]] @@ -2109,10 +3598,16 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", "synstructure", ] +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + [[package]] name = "zerotrie" version = "0.2.3" @@ -2143,7 +3638,7 @@ checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", ] [[package]] diff --git a/README.md b/README.md index 4c2faf3..e2c1cee 100644 --- a/README.md +++ b/README.md @@ -233,6 +233,86 @@ All parameters support environment variable fallbacks: --- +## `schema_type!` Macro + +Generate request/response types from existing structs. Perfect for creating API types from database models. + +### Basic Usage + +```rust +use vespera::schema_type; + +// Pick specific fields only +schema_type!(CreateUserRequest from crate::models::user::Model, pick = ["name", "email"]); + +// Omit specific fields +schema_type!(UserResponse from crate::models::user::Model, omit = ["password_hash"]); + +// Add new fields +schema_type!(UpdateUserRequest from crate::models::user::Model, pick = ["name"], add = [("id": i32)]); +``` + +### Cross-File References + +Reference structs from other files using module paths: + +```rust +// In src/routes/users.rs - references src/models/user.rs +schema_type!(UserResponse from crate::models::user::Model, omit = ["password_hash"]); +``` + +### Auto-Generated `From` Impl + +When `add` is NOT used, a `From` impl is automatically generated: + +```rust +schema_type!(UserResponse from crate::models::user::Model, omit = ["password_hash"]); + +// Now you can do: +let model: Model = db.find_user(id).await?; +Json(model.into()) // Automatic conversion! +``` + +### Parameters + +| Parameter | Description | +|-----------|-------------| +| `pick` | Include only specified fields | +| `omit` | Exclude specified fields | +| `rename` | Rename fields: `rename = [("old", "new")]` | +| `add` | Add new fields (disables auto `From` impl) | +| `clone` | Control Clone derive (default: true) | + +--- + +## `schema!` Macro + +Get a `Schema` value at runtime with optional field filtering. Useful for programmatic schema access. + +```rust +use vespera::{Schema, schema}; + +#[derive(Schema)] +pub struct User { + pub id: i32, + pub name: String, + pub password: String, +} + +// Full schema +let full: vespera::schema::Schema = schema!(User); + +// With fields omitted +let safe: vespera::schema::Schema = schema!(User, omit = ["password"]); + +// With only specified fields +let summary: vespera::schema::Schema = schema!(User, pick = ["id", "name"]); +``` + +> **Note:** For creating request/response types, use `schema_type!` instead - it generates actual struct types with `From` impl. + +--- + ## Advanced Usage ### Adding State diff --git a/SKILL.md b/SKILL.md index 8ef7d58..de19126 100644 --- a/SKILL.md +++ b/SKILL.md @@ -162,6 +162,107 @@ npx @apidevtools/swagger-cli validate openapi.json --- +## schema_type! Macro + +Generate request/response types from existing structs with field filtering. Supports cross-file references and auto-generates `From` impl. + +### Basic Syntax + +```rust +// Pick specific fields +schema_type!(CreateUserRequest from crate::models::user::Model, pick = ["name", "email"]); + +// Omit specific fields +schema_type!(UserResponse from crate::models::user::Model, omit = ["password_hash", "internal_id"]); + +// Add new fields (NOTE: no From impl generated when using add) +schema_type!(UpdateUserRequest from crate::models::user::Model, pick = ["name"], add = [("id": i32)]); + +// Rename fields +schema_type!(UserDTO from crate::models::user::Model, rename = [("id", "user_id")]); + +// Disable Clone derive +schema_type!(LargeResponse from SomeType, clone = false); +``` + +### Cross-File References + +Reference structs from other files using full module paths: + +```rust +// In src/routes/users.rs +use vespera::schema_type; + +// Reference model from src/models/user.rs +schema_type!(CreateUserRequest from crate::models::user::Model, pick = ["name", "email"]); +``` + +The macro reads the source file at compile time - no special annotations needed on the source struct. + +### Auto-Generated From Impl + +When `add` is NOT used, `schema_type!` generates a `From` impl for easy conversion: + +```rust +// This: +schema_type!(UserResponse from crate::models::user::Model, omit = ["password_hash"]); + +// Generates: +pub struct UserResponse { id, name, email, created_at } + +impl From for UserResponse { + fn from(source: crate::models::user::Model) -> Self { + Self { id: source.id, name: source.name, ... } + } +} + +// Usage: +let model: Model = db.find_user(id).await?; +Json(model.into()) // Easy conversion! +``` + +**Note:** `From` is NOT generated when `add` is used (can't auto-populate added fields). + +### Parameters + +| Parameter | Description | Example | +|-----------|-------------|---------| +| `pick` | Include only these fields | `pick = ["name", "email"]` | +| `omit` | Exclude these fields | `omit = ["password"]` | +| `rename` | Rename fields | `rename = [("id", "user_id")]` | +| `add` | Add new fields (disables From impl) | `add = [("extra": String)]` | +| `clone` | Control Clone derive (default: true) | `clone = false` | + +### Use Case: Sea-ORM Models + +Perfect for creating API types from database models: + +```rust +// src/models/user.rs (Sea-ORM entity) +#[derive(Clone, Debug, DeriveEntityModel, Serialize, Deserialize)] +#[sea_orm(table_name = "users")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub name: String, + pub email: String, + pub password_hash: String, // Never expose! + pub created_at: DateTimeWithTimeZone, +} + +// src/routes/users.rs +schema_type!(CreateUserRequest from crate::models::user::Model, pick = ["name", "email"]); +schema_type!(UserResponse from crate::models::user::Model, omit = ["password_hash"]); + +#[vespera::route(get, path = "/{id}")] +pub async fn get_user(Path(id): Path, State(db): State) -> Json { + let user = User::find_by_id(id).one(&db).await.unwrap().unwrap(); + Json(user.into()) // From impl handles conversion +} +``` + +--- + ## Merging Multiple Vespera Apps Combine routes and OpenAPI specs from multiple apps at compile time. diff --git a/crates/vespera/src/lib.rs b/crates/vespera/src/lib.rs index ecbebae..c77a191 100644 --- a/crates/vespera/src/lib.rs +++ b/crates/vespera/src/lib.rs @@ -20,7 +20,7 @@ pub mod openapi { pub use vespera_core::openapi::OpenApi; // Re-export macros from vespera_macro -pub use vespera_macro::{Schema, export_app, route, vespera}; +pub use vespera_macro::{Schema, export_app, route, schema, schema_type, vespera}; // Re-export serde_json for merge feature (runtime spec merging) pub use serde_json; diff --git a/crates/vespera_macro/src/lib.rs b/crates/vespera_macro/src/lib.rs index 83f543e..b243380 100644 --- a/crates/vespera_macro/src/lib.rs +++ b/crates/vespera_macro/src/lib.rs @@ -6,6 +6,7 @@ mod method; mod openapi_generator; mod parser; mod route; +mod schema_macro; use proc_macro::TokenStream; use proc_macro2::Span; @@ -69,10 +70,8 @@ static SCHEMA_STORAGE: LazyLock>> = fn process_derive_schema(input: &syn::DeriveInput) -> (StructMetadata, proc_macro2::TokenStream) { let name = &input.ident; let generics = &input.generics; - let metadata = StructMetadata { - name: name.to_string(), - definition: quote::quote!(#input).to_string(), - }; + // Schema-derived types appear in OpenAPI spec (include_in_openapi: true) + let metadata = StructMetadata::new(name.to_string(), quote::quote!(#input).to_string()); let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); let expanded = quote! { impl #impl_generics vespera::schema::SchemaBuilder for #name #ty_generics #where_clause {} @@ -89,6 +88,129 @@ pub fn derive_schema(input: TokenStream) -> TokenStream { TokenStream::from(expanded) } +/// Generate an OpenAPI Schema from a type with optional field filtering. +/// +/// This macro creates a `vespera::schema::Schema` struct at compile time +/// from a type that has `#[derive(Schema)]`. +/// +/// # Syntax +/// +/// ```ignore +/// // Full schema (all fields) +/// let user_schema = schema!(User); +/// +/// // Schema with fields omitted +/// let response_schema = schema!(User, omit = ["password", "internal_id"]); +/// +/// // Schema with only specified fields (pick) +/// let summary_schema = schema!(User, pick = ["id", "name"]); +/// ``` +/// +/// # Parameters +/// +/// - `Type`: The type to generate schema for (must have `#[derive(Schema)]`) +/// - `omit = [...]`: Optional list of field names to exclude from the schema +/// - `pick = [...]`: Optional list of field names to include (excludes all others) +/// +/// Note: `omit` and `pick` cannot be used together. +/// +/// # Example +/// +/// ```ignore +/// use vespera::{Schema, schema}; +/// +/// #[derive(Schema)] +/// struct User { +/// pub id: i32, +/// pub name: String, +/// pub email: String, +/// pub password: String, // sensitive! +/// } +/// +/// // For API responses, omit password +/// let response_schema = schema!(User, omit = ["password"]); +/// +/// // For list endpoints, only return summary fields +/// let list_schema = schema!(User, pick = ["id", "name"]); +/// ``` +#[proc_macro] +pub fn schema(input: TokenStream) -> TokenStream { + let input = syn::parse_macro_input!(input as schema_macro::SchemaInput); + + // Get stored schemas + let storage = SCHEMA_STORAGE.lock().unwrap(); + + match schema_macro::generate_schema_code(&input, &storage) { + Ok(tokens) => TokenStream::from(tokens), + Err(e) => e.to_compile_error().into(), + } +} + +/// Generate a new struct type derived from an existing type with field filtering. +/// +/// This macro creates a new struct at compile time by picking or omitting fields +/// from an existing type that has `#[derive(Schema)]`. +/// +/// # Syntax +/// +/// ```ignore +/// // Pick specific fields +/// schema_type!(CreateUserRequest from User, pick = ["name", "email"]); +/// +/// // Omit specific fields +/// schema_type!(UserResponse from User, omit = ["password", "internal_id"]); +/// +/// // Without Clone derive +/// schema_type!(UserUpdate from User, pick = ["name"], clone = false); +/// ``` +/// +/// # Parameters +/// +/// - `NewTypeName`: The name of the new struct to generate +/// - `from SourceType`: The source type to derive from (must have `#[derive(Schema)]`) +/// - `pick = [...]`: List of field names to include (excludes all others) +/// - `omit = [...]`: List of field names to exclude +/// - `clone = bool`: Whether to derive Clone (default: true) +/// +/// Note: `omit` and `pick` cannot be used together. +/// +/// # Example +/// +/// ```ignore +/// use vespera::{Schema, schema_type}; +/// +/// #[derive(Schema)] +/// pub struct User { +/// pub id: i32, +/// pub name: String, +/// pub email: String, +/// pub password: String, +/// } +/// +/// // Generate CreateUserRequest with only name and email +/// schema_type!(CreateUserRequest from User, pick = ["name", "email"]); +/// +/// // Generate UserPublic without password +/// schema_type!(UserPublic from User, omit = ["password"]); +/// +/// // Now use in handlers: +/// pub async fn create_user(Json(req): Json) -> Json { +/// // ... +/// } +/// ``` +#[proc_macro] +pub fn schema_type(input: TokenStream) -> TokenStream { + let input = syn::parse_macro_input!(input as schema_macro::SchemaTypeInput); + + // Get stored schemas + let storage = SCHEMA_STORAGE.lock().unwrap(); + + match schema_macro::generate_schema_type_code(&input, &storage) { + Ok(tokens) => TokenStream::from(tokens), + Err(e) => e.to_compile_error().into(), + } +} + /// Server configuration for OpenAPI #[derive(Clone)] struct ServerConfig { diff --git a/crates/vespera_macro/src/metadata.rs b/crates/vespera_macro/src/metadata.rs index 049aa48..e4ac9e5 100644 --- a/crates/vespera_macro/src/metadata.rs +++ b/crates/vespera_macro/src/metadata.rs @@ -35,6 +35,45 @@ pub struct StructMetadata { pub name: String, /// Struct definition (as string for serialization) pub definition: String, + /// Whether to include in OpenAPI spec (components/schemas) + /// - true: from #[derive(Schema)] - appears in openapi.json (DEFAULT) + /// - false: from cross-file lookup - only for schema_type! source, NOT in openapi.json + #[serde(default = "default_include_in_openapi")] + pub include_in_openapi: bool, +} + +fn default_include_in_openapi() -> bool { + true +} + +impl Default for StructMetadata { + fn default() -> Self { + Self { + name: String::new(), + definition: String::new(), + include_in_openapi: true, // Default to true (appears in OpenAPI) + } + } +} + +impl StructMetadata { + /// Create a new StructMetadata with include_in_openapi defaulting to true + pub fn new(name: String, definition: String) -> Self { + Self { + name, + definition, + include_in_openapi: true, + } + } + + /// Create a new StructMetadata for model types (not included in OpenAPI) + pub fn new_model(name: String, definition: String) -> Self { + Self { + name, + definition, + include_in_openapi: false, + } + } } /// Collected metadata @@ -54,3 +93,67 @@ impl CollectedMetadata { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_struct_metadata_new() { + let meta = StructMetadata::new("User".to_string(), "struct User {}".to_string()); + assert_eq!(meta.name, "User"); + assert_eq!(meta.definition, "struct User {}"); + assert!(meta.include_in_openapi); // Should default to true + } + + #[test] + fn test_struct_metadata_new_model() { + let meta = StructMetadata::new_model("Model".to_string(), "struct Model {}".to_string()); + assert_eq!(meta.name, "Model"); + assert_eq!(meta.definition, "struct Model {}"); + assert!(!meta.include_in_openapi); // Should be false for models + } + + #[test] + fn test_struct_metadata_default() { + let meta = StructMetadata::default(); + assert_eq!(meta.name, ""); + assert_eq!(meta.definition, ""); + assert!(meta.include_in_openapi); // Default to true + } + + #[test] + fn test_struct_metadata_serde_with_include_in_openapi() { + let json = r#"{"name":"User","definition":"struct User {}","include_in_openapi":false}"#; + let meta: StructMetadata = serde_json::from_str(json).unwrap(); + assert_eq!(meta.name, "User"); + assert!(!meta.include_in_openapi); + } + + #[test] + fn test_struct_metadata_serde_without_include_in_openapi() { + // This triggers the default_include_in_openapi() function (lines 45-46) + let json = r#"{"name":"User","definition":"struct User {}"}"#; + let meta: StructMetadata = serde_json::from_str(json).unwrap(); + assert_eq!(meta.name, "User"); + assert!(meta.include_in_openapi); // Should default to true via serde default + } + + #[test] + fn test_struct_metadata_roundtrip() { + let original = + StructMetadata::new("Test".to_string(), "struct Test { x: i32 }".to_string()); + let json = serde_json::to_string(&original).unwrap(); + let restored: StructMetadata = serde_json::from_str(&json).unwrap(); + assert_eq!(original.name, restored.name); + assert_eq!(original.definition, restored.definition); + assert_eq!(original.include_in_openapi, restored.include_in_openapi); + } + + #[test] + fn test_collected_metadata_new() { + let meta = CollectedMetadata::new(); + assert!(meta.routes.is_empty()); + assert!(meta.structs.is_empty()); + } +} diff --git a/crates/vespera_macro/src/openapi_generator.rs b/crates/vespera_macro/src/openapi_generator.rs index 7a9181e..18ed6f7 100644 --- a/crates/vespera_macro/src/openapi_generator.rs +++ b/crates/vespera_macro/src/openapi_generator.rs @@ -29,14 +29,18 @@ pub fn generate_openapi_doc_with_metadata( let mut all_tags: BTreeSet = BTreeSet::new(); // First, register all schema names and store struct definitions + // Note: We register ALL structs (including include_in_openapi: false) so that + // schema_type! generated types can reference them. The filtering happens below. for struct_meta in &metadata.structs { let schema_name = struct_meta.name.clone(); known_schema_names.insert(schema_name.clone(), schema_name); struct_definitions.insert(struct_meta.name.clone(), struct_meta.definition.clone()); } - // Then, parse all struct and enum schemas (now they can reference each other) - for struct_meta in &metadata.structs { + // Then, parse struct and enum schemas that should appear in OpenAPI + // Only include structs where include_in_openapi is true + // (i.e., from #[derive(Schema)], not from cross-file lookup) + for struct_meta in metadata.structs.iter().filter(|s| s.include_in_openapi) { let parsed = syn::parse_str::(&struct_meta.definition).unwrap(); let mut schema = match &parsed { syn::Item::Struct(struct_item) => { @@ -496,6 +500,7 @@ pub fn get_users() -> String { metadata.structs.push(StructMetadata { name: "User".to_string(), definition: "struct User { id: i32, name: String }".to_string(), + ..Default::default() }); let doc = generate_openapi_doc_with_metadata(None, None, None, &metadata); @@ -511,6 +516,7 @@ pub fn get_users() -> String { metadata.structs.push(StructMetadata { name: "Status".to_string(), definition: "enum Status { Active, Inactive, Pending }".to_string(), + ..Default::default() }); let doc = generate_openapi_doc_with_metadata(None, None, None, &metadata); @@ -527,6 +533,7 @@ pub fn get_users() -> String { metadata.structs.push(StructMetadata { name: "Message".to_string(), definition: "enum Message { Text(String), User { id: i32, name: String } }".to_string(), + ..Default::default() }); let doc = generate_openapi_doc_with_metadata(None, None, None, &metadata); @@ -551,6 +558,7 @@ pub fn get_status() -> Status { metadata.structs.push(StructMetadata { name: "Status".to_string(), definition: "enum Status { Active, Inactive }".to_string(), + ..Default::default() }); metadata.routes.push(RouteMetadata { method: "GET".to_string(), @@ -588,6 +596,7 @@ pub fn get_status() -> Status { name: "Config".to_string(), // This will be parsed as syn::Item::Const, triggering the fallback case definition: "const CONFIG: i32 = 42;".to_string(), + ..Default::default() }); // This should panic when fallback tries to parse const as struct @@ -610,6 +619,7 @@ pub fn get_user() -> User { metadata.structs.push(StructMetadata { name: "User".to_string(), definition: "struct User { id: i32, name: String }".to_string(), + ..Default::default() }); metadata.routes.push(RouteMetadata { method: "GET".to_string(), @@ -1058,6 +1068,7 @@ pub fn get_user() -> User { name: "User".to_string(), definition: r#"struct User { #[serde(default = "default_name")] name: String }"# .to_string(), + ..Default::default() }); metadata.routes.push(RouteMetadata { method: "GET".to_string(), @@ -1103,6 +1114,7 @@ pub fn get_config() -> Config { definition: r#"struct Config { #[serde(default)] enabled: bool, #[serde(default)] count: i32 }"# .to_string(), + ..Default::default() }); metadata.routes.push(RouteMetadata { method: "GET".to_string(), @@ -1161,6 +1173,7 @@ pub fn get_user() -> User { name: "User".to_string(), definition: r#"struct User { #[serde(default = "default_name")] name: String }"# .to_string(), + ..Default::default() }); // Add BOTH routes - the first doesn't contain User struct, so fallback searches the second metadata.routes.push(RouteMetadata { diff --git a/crates/vespera_macro/src/parser/mod.rs b/crates/vespera_macro/src/parser/mod.rs index 8148a47..87cfe3b 100644 --- a/crates/vespera_macro/src/parser/mod.rs +++ b/crates/vespera_macro/src/parser/mod.rs @@ -7,6 +7,7 @@ mod response; mod schema; pub use operation::build_operation_from_function; pub use schema::{ - extract_default, extract_field_rename, extract_rename_all, parse_enum_to_schema, - parse_struct_to_schema, rename_field, strip_raw_prefix, + extract_default, extract_field_rename, extract_rename_all, extract_skip, + extract_skip_serializing_if, parse_enum_to_schema, parse_struct_to_schema, + parse_type_to_schema_ref, rename_field, strip_raw_prefix, }; diff --git a/crates/vespera_macro/src/parser/schema.rs b/crates/vespera_macro/src/parser/schema.rs index ee86909..475000c 100644 --- a/crates/vespera_macro/src/parser/schema.rs +++ b/crates/vespera_macro/src/parser/schema.rs @@ -125,7 +125,7 @@ pub fn extract_field_rename(attrs: &[syn::Attribute]) -> Option { /// Extract skip attribute from field attributes /// Returns true if #[serde(skip)] is present -pub(super) fn extract_skip(attrs: &[syn::Attribute]) -> bool { +pub fn extract_skip(attrs: &[syn::Attribute]) -> bool { for attr in attrs { if attr.path().is_ident("serde") && let syn::Meta::List(meta_list) = &attr.meta @@ -714,9 +714,10 @@ fn substitute_type(ty: &Type, generic_params: &[String], concrete_types: &[&Type if let syn::PathArguments::None = &segment.arguments { // Direct generic parameter substitution if let Some(index) = generic_params.iter().position(|p| p == &ident_str) - && let Some(concrete_ty) = concrete_types.get(index) { - return (*concrete_ty).clone(); - } + && let Some(concrete_ty) = concrete_types.get(index) + { + return (*concrete_ty).clone(); + } } } @@ -941,7 +942,11 @@ pub(super) fn parse_type_to_schema_ref_with_schemas( "bool" => SchemaRef::Inline(Box::new(Schema::boolean())), "String" | "str" => SchemaRef::Inline(Box::new(Schema::string())), // Date-time types from chrono crate - "DateTime" | "NaiveDateTime" => SchemaRef::Inline(Box::new(Schema { + "DateTime" + | "NaiveDateTime" + | "DateTimeWithTimeZone" + | "DateTimeUtc" + | "DateTimeLocal" => SchemaRef::Inline(Box::new(Schema { format: Some("date-time".to_string()), ..Schema::string() })), @@ -1685,7 +1690,10 @@ mod tests { ) { let ty: Type = syn::parse_str(input).unwrap(); let generic_params: Vec = params.iter().map(|s| s.to_string()).collect(); - let concrete_types: Vec = concrete.iter().map(|s| syn::parse_str(s).unwrap()).collect(); + let concrete_types: Vec = concrete + .iter() + .map(|s| syn::parse_str(s).unwrap()) + .collect(); let concrete_refs: Vec<&Type> = concrete_types.iter().collect(); let result = substitute_type(&ty, &generic_params, &concrete_refs); @@ -1729,7 +1737,11 @@ mod tests { let ty: Type = syn::parse_str("fn(T) -> U").unwrap(); let concrete_t: Type = syn::parse_str("String").unwrap(); let concrete_u: Type = syn::parse_str("i32").unwrap(); - let result = substitute_type(&ty, &[String::from("T"), String::from("U")], &[&concrete_t, &concrete_u]); + let result = substitute_type( + &ty, + &[String::from("T"), String::from("U")], + &[&concrete_t, &concrete_u], + ); // Type::BareFn doesn't go through the Path branch, falls to _ => ty.clone() assert_eq!(result, ty); } @@ -1740,7 +1752,11 @@ mod tests { let ty: Type = syn::parse_str("dyn Fn(T) -> U").unwrap(); let concrete_t: Type = syn::parse_str("String").unwrap(); let concrete_u: Type = syn::parse_str("i32").unwrap(); - let result = substitute_type(&ty, &[String::from("T"), String::from("U")], &[&concrete_t, &concrete_u]); + let result = substitute_type( + &ty, + &[String::from("T"), String::from("U")], + &[&concrete_t, &concrete_u], + ); // Type::TraitObject falls to _ => ty.clone() assert_eq!(result, ty); } diff --git a/crates/vespera_macro/src/schema_macro.rs b/crates/vespera_macro/src/schema_macro.rs new file mode 100644 index 0000000..d969c9b --- /dev/null +++ b/crates/vespera_macro/src/schema_macro.rs @@ -0,0 +1,1542 @@ +//! Schema macro implementation +//! +//! Provides macros for generating OpenAPI schemas from struct types: +//! - `schema!` - Generate Schema value with optional field filtering +//! - `schema_type!` - Generate new struct type derived from existing type + +use proc_macro2::TokenStream; +use quote::quote; +use std::collections::HashSet; +use std::path::Path; +use syn::punctuated::Punctuated; +use syn::{Ident, LitStr, Token, Type, bracketed, parenthesized, parse::Parse, parse::ParseStream}; + +use crate::metadata::StructMetadata; +use crate::parser::{ + extract_default, extract_field_rename, extract_rename_all, extract_skip, + extract_skip_serializing_if, parse_type_to_schema_ref, rename_field, strip_raw_prefix, +}; +use vespera_core::schema::{Schema, SchemaRef, SchemaType}; + +/// Input for the schema! macro +/// +/// Supports: +/// - `schema!(Type)` - Full schema +/// - `schema!(Type, omit = ["field1", "field2"])` - Schema with fields omitted +/// - `schema!(Type, pick = ["field1", "field2"])` - Schema with only specified fields (future) +pub struct SchemaInput { + /// The type to generate schema for + pub ty: Type, + /// Fields to omit from the schema + pub omit: Option>, + /// Fields to pick (include only these fields) + pub pick: Option>, +} + +impl Parse for SchemaInput { + fn parse(input: ParseStream) -> syn::Result { + // Parse the type + let ty: Type = input.parse()?; + + let mut omit = None; + let mut pick = None; + + // Parse optional parameters + while input.peek(Token![,]) { + input.parse::()?; + + if input.is_empty() { + break; + } + + let ident: Ident = input.parse()?; + let ident_str = ident.to_string(); + + match ident_str.as_str() { + "omit" => { + input.parse::()?; + let content; + let _ = bracketed!(content in input); + let fields: Punctuated = + content.parse_terminated(|input| input.parse::(), Token![,])?; + omit = Some(fields.into_iter().map(|s| s.value()).collect()); + } + "pick" => { + input.parse::()?; + let content; + let _ = bracketed!(content in input); + let fields: Punctuated = + content.parse_terminated(|input| input.parse::(), Token![,])?; + pick = Some(fields.into_iter().map(|s| s.value()).collect()); + } + _ => { + return Err(syn::Error::new( + ident.span(), + format!( + "unknown parameter: `{}`. Expected `omit` or `pick`", + ident_str + ), + )); + } + } + } + + // Validate: can't use both omit and pick + if omit.is_some() && pick.is_some() { + return Err(syn::Error::new( + input.span(), + "cannot use both `omit` and `pick` in the same schema! invocation", + )); + } + + Ok(SchemaInput { ty, omit, pick }) + } +} + +/// Generate schema code from a struct with optional field filtering +pub fn generate_schema_code( + input: &SchemaInput, + schema_storage: &[StructMetadata], +) -> Result { + // Extract type name from the Type + let type_name = extract_type_name(&input.ty)?; + + // Find struct definition in storage + let struct_def = schema_storage + .iter() + .find(|s| s.name == type_name) + .ok_or_else(|| { + syn::Error::new_spanned( + &input.ty, + format!( + "type `{}` not found. Make sure it has #[derive(Schema)] before this macro invocation", + type_name + ), + ) + })?; + + // Parse the struct definition + let parsed_struct: syn::ItemStruct = syn::parse_str(&struct_def.definition).map_err(|e| { + syn::Error::new_spanned( + &input.ty, + format!( + "failed to parse struct definition for `{}`: {}", + type_name, e + ), + ) + })?; + + // Build omit set + let omit_set: HashSet = input.omit.clone().unwrap_or_default().into_iter().collect(); + + // Build pick set + let pick_set: HashSet = input.pick.clone().unwrap_or_default().into_iter().collect(); + + // Generate schema with filtering + let schema_tokens = + generate_filtered_schema(&parsed_struct, &omit_set, &pick_set, schema_storage)?; + + Ok(schema_tokens) +} + +/// Extract type name from a Type +fn extract_type_name(ty: &Type) -> Result { + match ty { + Type::Path(type_path) => { + // Get the last segment (handles paths like crate::User) + let segment = type_path.path.segments.last().ok_or_else(|| { + syn::Error::new_spanned(ty, "expected a type path with at least one segment") + })?; + Ok(segment.ident.to_string()) + } + _ => Err(syn::Error::new_spanned( + ty, + "expected a type path (e.g., `User` or `crate::User`)", + )), + } +} + +/// Generate Schema construction code with field filtering +fn generate_filtered_schema( + struct_item: &syn::ItemStruct, + omit_set: &HashSet, + pick_set: &HashSet, + schema_storage: &[StructMetadata], +) -> Result { + let rename_all = extract_rename_all(&struct_item.attrs); + + // Build known_schemas and struct_definitions for type resolution + let known_schemas: std::collections::HashMap = schema_storage + .iter() + .map(|s| (s.name.clone(), s.definition.clone())) + .collect(); + let struct_definitions = known_schemas.clone(); + + let mut property_tokens = Vec::new(); + let mut required_fields = Vec::new(); + + if let syn::Fields::Named(fields_named) = &struct_item.fields { + for field in &fields_named.named { + // Skip if serde(skip) + if extract_skip(&field.attrs) { + continue; + } + + let rust_field_name = field + .ident + .as_ref() + .map(|i| strip_raw_prefix(&i.to_string()).to_string()) + .unwrap_or_else(|| "unknown".to_string()); + + // Apply rename + let field_name = if let Some(renamed) = extract_field_rename(&field.attrs) { + renamed + } else { + rename_field(&rust_field_name, rename_all.as_deref()) + }; + + // Apply omit filter (check both rust name and json name) + if !omit_set.is_empty() + && (omit_set.contains(&rust_field_name) || omit_set.contains(&field_name)) + { + continue; + } + + // Apply pick filter (check both rust name and json name) + if !pick_set.is_empty() + && !pick_set.contains(&rust_field_name) + && !pick_set.contains(&field_name) + { + continue; + } + + let field_type = &field.ty; + + // Generate schema for field type + let schema_ref = + parse_type_to_schema_ref(field_type, &known_schemas, &struct_definitions); + let schema_ref_tokens = schema_ref_to_tokens(&schema_ref); + + property_tokens.push(quote! { + properties.insert(#field_name.to_string(), #schema_ref_tokens); + }); + + // Check if field is required (not Option, no default, no skip_serializing_if) + let has_default = extract_default(&field.attrs).is_some(); + let has_skip_serializing_if = extract_skip_serializing_if(&field.attrs); + let is_optional = is_option_type(field_type); + + if !is_optional && !has_default && !has_skip_serializing_if { + required_fields.push(field_name.clone()); + } + } + } + + let required_tokens = if required_fields.is_empty() { + quote! { None } + } else { + let required_strs: Vec<&str> = required_fields.iter().map(|s| s.as_str()).collect(); + quote! { Some(vec![#(#required_strs.to_string()),*]) } + }; + + Ok(quote! { + { + let mut properties = std::collections::BTreeMap::new(); + #(#property_tokens)* + vespera::schema::Schema { + schema_type: Some(vespera::schema::SchemaType::Object), + properties: if properties.is_empty() { None } else { Some(properties) }, + required: #required_tokens, + ..vespera::schema::Schema::new(vespera::schema::SchemaType::Object) + } + } + }) +} + +/// Check if a type is Option +fn is_option_type(ty: &Type) -> bool { + match ty { + Type::Path(type_path) => type_path + .path + .segments + .first() + .map(|s| s.ident == "Option") + .unwrap_or(false), + _ => false, + } +} + +/// Convert SchemaRef to TokenStream for code generation +fn schema_ref_to_tokens(schema_ref: &SchemaRef) -> TokenStream { + match schema_ref { + SchemaRef::Ref(reference) => { + let ref_path = &reference.ref_path; + quote! { + vespera::schema::SchemaRef::Ref(vespera::schema::Reference::new(#ref_path.to_string())) + } + } + SchemaRef::Inline(schema) => { + let schema_tokens = schema_to_tokens(schema); + quote! { + vespera::schema::SchemaRef::Inline(Box::new(#schema_tokens)) + } + } + } +} + +/// Convert Schema to TokenStream for code generation +fn schema_to_tokens(schema: &Schema) -> TokenStream { + let schema_type_tokens = match &schema.schema_type { + Some(SchemaType::String) => quote! { Some(vespera::schema::SchemaType::String) }, + Some(SchemaType::Number) => quote! { Some(vespera::schema::SchemaType::Number) }, + Some(SchemaType::Integer) => quote! { Some(vespera::schema::SchemaType::Integer) }, + Some(SchemaType::Boolean) => quote! { Some(vespera::schema::SchemaType::Boolean) }, + Some(SchemaType::Array) => quote! { Some(vespera::schema::SchemaType::Array) }, + Some(SchemaType::Object) => quote! { Some(vespera::schema::SchemaType::Object) }, + Some(SchemaType::Null) => quote! { Some(vespera::schema::SchemaType::Null) }, + None => quote! { None }, + }; + + let format_tokens = match &schema.format { + Some(f) => quote! { Some(#f.to_string()) }, + None => quote! { None }, + }; + + let nullable_tokens = match schema.nullable { + Some(true) => quote! { Some(true) }, + Some(false) => quote! { Some(false) }, + None => quote! { None }, + }; + + let ref_path_tokens = match &schema.ref_path { + Some(rp) => quote! { Some(#rp.to_string()) }, + None => quote! { None }, + }; + + let items_tokens = match &schema.items { + Some(items) => { + let inner = schema_ref_to_tokens(items); + quote! { Some(Box::new(#inner)) } + } + None => quote! { None }, + }; + + let properties_tokens = match &schema.properties { + Some(props) => { + let entries: Vec<_> = props + .iter() + .map(|(k, v)| { + let v_tokens = schema_ref_to_tokens(v); + quote! { (#k.to_string(), #v_tokens) } + }) + .collect(); + quote! { + Some({ + let mut map = std::collections::BTreeMap::new(); + #(map.insert(#entries.0, #entries.1);)* + map + }) + } + } + None => quote! { None }, + }; + + let required_tokens = match &schema.required { + Some(req) => { + let req_strs: Vec<_> = req.iter().map(|s| s.as_str()).collect(); + quote! { Some(vec![#(#req_strs.to_string()),*]) } + } + None => quote! { None }, + }; + + quote! { + vespera::schema::Schema { + ref_path: #ref_path_tokens, + schema_type: #schema_type_tokens, + format: #format_tokens, + nullable: #nullable_tokens, + items: #items_tokens, + properties: #properties_tokens, + required: #required_tokens, + ..vespera::schema::Schema::new(vespera::schema::SchemaType::Object) + } + } +} + +// ============================================================================ +// schema_type! macro - Generate new struct types from existing types +// ============================================================================ + +/// Try to find a struct definition from a module path by reading source files. +/// +/// This allows schema_type! to work with structs defined in other files, like: +/// ```ignore +/// // In src/routes/memos.rs +/// schema_type!(CreateMemoRequest from models::memo::Model, pick = ["title", "content"]); +/// ``` +/// +/// The function will: +/// 1. Parse the path (e.g., `models::memo::Model` or `crate::models::memo::Model`) +/// 2. Convert to file path (e.g., `src/models/memo.rs`) +/// 3. Read and parse the file to find the struct definition +fn find_struct_from_path(ty: &Type) -> Option { + // Get CARGO_MANIFEST_DIR to locate src folder + let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").ok()?; + let src_dir = Path::new(&manifest_dir).join("src"); + + // Extract path segments from the type + let type_path = match ty { + Type::Path(tp) => tp, + _ => return None, + }; + + let segments: Vec = type_path + .path + .segments + .iter() + .map(|s| s.ident.to_string()) + .collect(); + + if segments.is_empty() { + return None; + } + + // The last segment is the struct name + let struct_name = segments.last()?.clone(); + + // Build possible file paths from the module path + // e.g., models::memo::Model -> src/models/memo.rs or src/models/memo/mod.rs + // e.g., crate::models::memo::Model -> src/models/memo.rs + let module_segments: Vec<&str> = segments[..segments.len() - 1] + .iter() + .filter(|s| *s != "crate" && *s != "self" && *s != "super") + .map(|s| s.as_str()) + .collect(); + + if module_segments.is_empty() { + return None; + } + + // Try different file path patterns + let file_paths = vec![ + src_dir.join(format!("{}.rs", module_segments.join("/"))), + src_dir.join(format!("{}/mod.rs", module_segments.join("/"))), + ]; + + for file_path in file_paths { + if !file_path.exists() { + continue; + } + + let content = std::fs::read_to_string(&file_path).ok()?; + let file_ast = syn::parse_file(&content).ok()?; + + // Look for the struct in the file + for item in &file_ast.items { + match item { + syn::Item::Struct(struct_item) if struct_item.ident == struct_name => { + return Some(StructMetadata::new_model( + struct_name.clone(), + quote::quote!(#struct_item).to_string(), + )); + } + _ => continue, + } + } + } + + None +} + +/// Input for the schema_type! macro +/// +/// Syntax: `schema_type!(NewTypeName from SourceType, pick = ["field1", "field2"])` +/// Or: `schema_type!(NewTypeName from SourceType, omit = ["field1", "field2"])` +/// Or: `schema_type!(NewTypeName from SourceType, rename = [("old", "new")])` +/// Or: `schema_type!(NewTypeName from SourceType, add = [("field": Type)])` +#[derive(Debug)] +pub struct SchemaTypeInput { + /// The new type name to generate + pub new_type: Ident, + /// The source type to derive from + pub source_type: Type, + /// Fields to omit from the new type + pub omit: Option>, + /// Fields to pick (include only these fields) + pub pick: Option>, + /// Field renames: (source_field_name, new_field_name) + pub rename: Option>, + /// New fields to add: (field_name, field_type) + pub add: Option>, + /// Whether to derive Clone (default: true) + pub derive_clone: bool, +} + +/// Helper struct to parse an add field: ("field_name": Type) +struct AddField { + name: String, + ty: Type, +} + +impl Parse for AddField { + fn parse(input: ParseStream) -> syn::Result { + let content; + parenthesized!(content in input); + let name: LitStr = content.parse()?; + content.parse::()?; + let ty: Type = content.parse()?; + Ok(AddField { + name: name.value(), + ty, + }) + } +} + +/// Helper struct to parse a rename pair: ("old_name", "new_name") +struct RenamePair { + from: String, + to: String, +} + +impl Parse for RenamePair { + fn parse(input: ParseStream) -> syn::Result { + let content; + parenthesized!(content in input); + let from: LitStr = content.parse()?; + content.parse::()?; + let to: LitStr = content.parse()?; + Ok(RenamePair { + from: from.value(), + to: to.value(), + }) + } +} + +impl Parse for SchemaTypeInput { + fn parse(input: ParseStream) -> syn::Result { + // Parse new type name + let new_type: Ident = input.parse()?; + + // Parse "from" keyword + let from_ident: Ident = input.parse()?; + if from_ident != "from" { + return Err(syn::Error::new( + from_ident.span(), + format!("expected `from`, found `{}`", from_ident), + )); + } + + // Parse source type + let source_type: Type = input.parse()?; + + let mut omit = None; + let mut pick = None; + let mut rename = None; + let mut add = None; + let mut derive_clone = true; + + // Parse optional parameters + while input.peek(Token![,]) { + input.parse::()?; + + if input.is_empty() { + break; + } + + let ident: Ident = input.parse()?; + let ident_str = ident.to_string(); + + match ident_str.as_str() { + "omit" => { + input.parse::()?; + let content; + let _ = bracketed!(content in input); + let fields: Punctuated = + content.parse_terminated(|input| input.parse::(), Token![,])?; + omit = Some(fields.into_iter().map(|s| s.value()).collect()); + } + "pick" => { + input.parse::()?; + let content; + let _ = bracketed!(content in input); + let fields: Punctuated = + content.parse_terminated(|input| input.parse::(), Token![,])?; + pick = Some(fields.into_iter().map(|s| s.value()).collect()); + } + "rename" => { + input.parse::()?; + let content; + let _ = bracketed!(content in input); + let pairs: Punctuated = + content.parse_terminated(RenamePair::parse, Token![,])?; + rename = Some(pairs.into_iter().map(|p| (p.from, p.to)).collect()); + } + "add" => { + input.parse::()?; + let content; + let _ = bracketed!(content in input); + let fields: Punctuated = + content.parse_terminated(AddField::parse, Token![,])?; + add = Some(fields.into_iter().map(|f| (f.name, f.ty)).collect()); + } + "clone" => { + input.parse::()?; + let value: syn::LitBool = input.parse()?; + derive_clone = value.value(); + } + _ => { + return Err(syn::Error::new( + ident.span(), + format!( + "unknown parameter: `{}`. Expected `omit`, `pick`, `rename`, `add`, or `clone`", + ident_str + ), + )); + } + } + } + + // Validate: can't use both omit and pick + if omit.is_some() && pick.is_some() { + return Err(syn::Error::new( + input.span(), + "cannot use both `omit` and `pick` in the same schema_type! invocation", + )); + } + + Ok(SchemaTypeInput { + new_type, + source_type, + omit, + pick, + rename, + add, + derive_clone, + }) + } +} + +/// Generate a new struct type from an existing type with field filtering +pub fn generate_schema_type_code( + input: &SchemaTypeInput, + schema_storage: &[StructMetadata], +) -> Result { + // Extract type name from the source Type + let source_type_name = extract_type_name(&input.source_type)?; + + // Find struct definition in storage first (for same-file structs) + let struct_def_owned: StructMetadata; + let struct_def = if let Some(found) = schema_storage.iter().find(|s| s.name == source_type_name) + { + found + } else if let Some(found) = find_struct_from_path(&input.source_type) { + // Try to find from file path (for cross-file structs like models::memo::Model) + struct_def_owned = found; + &struct_def_owned + } else { + return Err(syn::Error::new_spanned( + &input.source_type, + format!( + "type `{}` not found. Either:\n\ + 1. Use #[derive(Schema)] in the same file\n\ + 2. Use full module path like `crate::models::memo::Model` to reference a struct from another file", + source_type_name + ), + )); + }; + + // Parse the struct definition + let parsed_struct: syn::ItemStruct = syn::parse_str(&struct_def.definition).map_err(|e| { + syn::Error::new_spanned( + &input.source_type, + format!( + "failed to parse struct definition for `{}`: {}", + source_type_name, e + ), + ) + })?; + + // Extract all field names from source struct for validation + let source_field_names: HashSet = + if let syn::Fields::Named(fields_named) = &parsed_struct.fields { + fields_named + .named + .iter() + .filter_map(|f| f.ident.as_ref()) + .map(|i| strip_raw_prefix(&i.to_string()).to_string()) + .collect() + } else { + HashSet::new() + }; + + // Validate pick fields exist + if let Some(ref pick_fields) = input.pick { + for field in pick_fields { + if !source_field_names.contains(field) { + return Err(syn::Error::new_spanned( + &input.source_type, + format!( + "field `{}` does not exist in type `{}`. Available fields: {:?}", + field, + source_type_name, + source_field_names.iter().collect::>() + ), + )); + } + } + } + + // Validate omit fields exist + if let Some(ref omit_fields) = input.omit { + for field in omit_fields { + if !source_field_names.contains(field) { + return Err(syn::Error::new_spanned( + &input.source_type, + format!( + "field `{}` does not exist in type `{}`. Available fields: {:?}", + field, + source_type_name, + source_field_names.iter().collect::>() + ), + )); + } + } + } + + // Validate rename source fields exist + if let Some(ref rename_pairs) = input.rename { + for (from_field, _) in rename_pairs { + if !source_field_names.contains(from_field) { + return Err(syn::Error::new_spanned( + &input.source_type, + format!( + "field `{}` does not exist in type `{}`. Available fields: {:?}", + from_field, + source_type_name, + source_field_names.iter().collect::>() + ), + )); + } + } + } + + // Build omit set (use Rust field names) + let omit_set: HashSet = input.omit.clone().unwrap_or_default().into_iter().collect(); + + // Build pick set (use Rust field names) + let pick_set: HashSet = input.pick.clone().unwrap_or_default().into_iter().collect(); + + // Build rename map: source_field_name -> new_field_name + let rename_map: std::collections::HashMap = input + .rename + .clone() + .unwrap_or_default() + .into_iter() + .collect(); + + // Extract serde attributes from source struct + let serde_attrs: Vec<_> = parsed_struct + .attrs + .iter() + .filter(|attr| attr.path().is_ident("serde")) + .collect(); + + // Generate new struct with filtered fields + let new_type_name = &input.new_type; + let mut field_tokens = Vec::new(); + // Track field mappings for From impl: (new_field_ident, source_field_ident) + let mut field_mappings: Vec<(syn::Ident, syn::Ident)> = Vec::new(); + + if let syn::Fields::Named(fields_named) = &parsed_struct.fields { + for field in &fields_named.named { + let rust_field_name = field + .ident + .as_ref() + .map(|i| strip_raw_prefix(&i.to_string()).to_string()) + .unwrap_or_else(|| "unknown".to_string()); + + // Apply omit filter + if !omit_set.is_empty() && omit_set.contains(&rust_field_name) { + continue; + } + + // Apply pick filter + if !pick_set.is_empty() && !pick_set.contains(&rust_field_name) { + continue; + } + + // Get field components + let field_ty = &field.ty; + let vis = &field.vis; + let source_field_ident = field.ident.clone().unwrap(); + + // Filter field attributes: only keep serde attributes, remove sea_orm and others + // This is important when using schema_type! with models from other files + // that may have ORM-specific attributes we don't want in the generated struct + let serde_field_attrs: Vec<_> = field + .attrs + .iter() + .filter(|attr| attr.path().is_ident("serde")) + .collect(); + + // Check if field should be renamed + if let Some(new_name) = rename_map.get(&rust_field_name) { + // Create new identifier for the field + let new_field_ident = + syn::Ident::new(new_name, field.ident.as_ref().unwrap().span()); + + // Filter out serde(rename) attributes from the serde attrs + let filtered_attrs: Vec<_> = serde_field_attrs + .iter() + .filter(|attr| { + // Check if it's a rename attribute + let mut has_rename = false; + let _ = attr.parse_nested_meta(|meta| { + if meta.path.is_ident("rename") { + has_rename = true; + } + Ok(()) + }); + !has_rename + }) + .collect(); + + // Determine the JSON name: use existing serde(rename) if present, otherwise rust field name + let json_name = + extract_field_rename(&field.attrs).unwrap_or_else(|| rust_field_name.clone()); + + field_tokens.push(quote! { + #(#filtered_attrs)* + #[serde(rename = #json_name)] + #vis #new_field_ident: #field_ty + }); + + // Track mapping: new field name <- source field name + field_mappings.push((new_field_ident, source_field_ident)); + } else { + // No rename, keep field with only serde attrs + let field_ident = field.ident.clone().unwrap(); + + field_tokens.push(quote! { + #(#serde_field_attrs)* + #vis #field_ident: #field_ty + }); + + // Track mapping: same name + field_mappings.push((field_ident.clone(), field_ident)); + } + } + } + + // Add new fields from `add` parameter + if let Some(ref add_fields) = input.add { + for (field_name, field_ty) in add_fields { + let field_ident = syn::Ident::new(field_name, proc_macro2::Span::call_site()); + field_tokens.push(quote! { + pub #field_ident: #field_ty + }); + } + } + + // Build derive list + let clone_derive = if input.derive_clone { + quote! { Clone, } + } else { + quote! {} + }; + + // Generate From impl only if `add` is not used (can't auto-populate added fields) + let source_type = &input.source_type; + let from_impl = if input.add.is_none() { + let field_assignments: Vec<_> = field_mappings + .iter() + .map(|(new_ident, source_ident)| { + quote! { #new_ident: source.#source_ident } + }) + .collect(); + + quote! { + impl From<#source_type> for #new_type_name { + fn from(source: #source_type) -> Self { + Self { + #(#field_assignments),* + } + } + } + } + } else { + quote! {} + }; + + // Generate the new struct + Ok(quote! { + #[derive(serde::Serialize, serde::Deserialize, #clone_derive vespera::Schema)] + #(#serde_attrs)* + pub struct #new_type_name { + #(#field_tokens),* + } + + #from_impl + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_schema_input_simple() { + let tokens = quote::quote!(User); + let input: SchemaInput = syn::parse2(tokens).unwrap(); + assert!(input.omit.is_none()); + assert!(input.pick.is_none()); + } + + #[test] + fn test_parse_schema_input_with_omit() { + let tokens = quote::quote!(User, omit = ["password", "secret"]); + let input: SchemaInput = syn::parse2(tokens).unwrap(); + let omit = input.omit.unwrap(); + assert_eq!(omit, vec!["password", "secret"]); + } + + #[test] + fn test_parse_schema_input_with_pick() { + let tokens = quote::quote!(User, pick = ["id", "name"]); + let input: SchemaInput = syn::parse2(tokens).unwrap(); + let pick = input.pick.unwrap(); + assert_eq!(pick, vec!["id", "name"]); + } + + #[test] + fn test_parse_schema_input_omit_and_pick_error() { + let tokens = quote::quote!(User, omit = ["a"], pick = ["b"]); + let result: syn::Result = syn::parse2(tokens); + assert!(result.is_err()); + } + + // schema_type! tests + + #[test] + fn test_parse_schema_type_input_simple() { + let tokens = quote::quote!(CreateUser from User); + let input: SchemaTypeInput = syn::parse2(tokens).unwrap(); + assert_eq!(input.new_type.to_string(), "CreateUser"); + assert!(input.omit.is_none()); + assert!(input.pick.is_none()); + assert!(input.rename.is_none()); + assert!(input.derive_clone); + } + + #[test] + fn test_parse_schema_type_input_with_pick() { + let tokens = quote::quote!(CreateUser from User, pick = ["name", "email"]); + let input: SchemaTypeInput = syn::parse2(tokens).unwrap(); + assert_eq!(input.new_type.to_string(), "CreateUser"); + let pick = input.pick.unwrap(); + assert_eq!(pick, vec!["name", "email"]); + } + + #[test] + fn test_parse_schema_type_input_with_rename() { + let tokens = + quote::quote!(UserDTO from User, rename = [("id", "user_id"), ("name", "full_name")]); + let input: SchemaTypeInput = syn::parse2(tokens).unwrap(); + assert_eq!(input.new_type.to_string(), "UserDTO"); + let rename = input.rename.unwrap(); + assert_eq!(rename.len(), 2); + assert_eq!(rename[0], ("id".to_string(), "user_id".to_string())); + assert_eq!(rename[1], ("name".to_string(), "full_name".to_string())); + } + + #[test] + fn test_parse_schema_type_input_with_single_rename() { + let tokens = quote::quote!(UserDTO from User, rename = [("id", "user_id")]); + let input: SchemaTypeInput = syn::parse2(tokens).unwrap(); + let rename = input.rename.unwrap(); + assert_eq!(rename.len(), 1); + assert_eq!(rename[0], ("id".to_string(), "user_id".to_string())); + } + + #[test] + fn test_parse_schema_type_input_with_pick_and_rename() { + let tokens = + quote::quote!(UserDTO from User, pick = ["id", "name"], rename = [("id", "user_id")]); + let input: SchemaTypeInput = syn::parse2(tokens).unwrap(); + assert_eq!(input.pick.unwrap(), vec!["id", "name"]); + assert_eq!( + input.rename.unwrap(), + vec![("id".to_string(), "user_id".to_string())] + ); + } + + #[test] + fn test_parse_schema_type_input_with_omit_and_rename() { + let tokens = + quote::quote!(UserPublic from User, omit = ["password"], rename = [("id", "user_id")]); + let input: SchemaTypeInput = syn::parse2(tokens).unwrap(); + assert_eq!(input.omit.unwrap(), vec!["password"]); + assert_eq!( + input.rename.unwrap(), + vec![("id".to_string(), "user_id".to_string())] + ); + } + + #[test] + fn test_parse_schema_type_input_with_clone_false() { + let tokens = quote::quote!(NonCloneUser from User, clone = false); + let input: SchemaTypeInput = syn::parse2(tokens).unwrap(); + assert!(!input.derive_clone); + } + + #[test] + fn test_parse_schema_type_input_unknown_param_error() { + let tokens = quote::quote!(UserDTO from User, unknown = ["a"]); + let result: syn::Result = syn::parse2(tokens); + assert!(result.is_err()); + let err = result.unwrap_err().to_string(); + assert!(err.contains("unknown parameter")); + } + + // Tests for `add` parameter + + #[test] + fn test_parse_schema_type_input_with_add_single() { + let tokens = quote::quote!(UserWithTimestamp from User, add = [("created_at": String)]); + let input: SchemaTypeInput = syn::parse2(tokens).unwrap(); + assert_eq!(input.new_type.to_string(), "UserWithTimestamp"); + let add = input.add.unwrap(); + assert_eq!(add.len(), 1); + assert_eq!(add[0].0, "created_at"); + } + + #[test] + fn test_parse_schema_type_input_with_add_multiple() { + let tokens = quote::quote!(UserWithMeta from User, add = [("created_at": String), ("updated_at": Option)]); + let input: SchemaTypeInput = syn::parse2(tokens).unwrap(); + let add = input.add.unwrap(); + assert_eq!(add.len(), 2); + assert_eq!(add[0].0, "created_at"); + assert_eq!(add[1].0, "updated_at"); + } + + #[test] + fn test_parse_schema_type_input_with_pick_and_add() { + let tokens = quote::quote!(CreateUserWithMeta from User, pick = ["name", "email"], add = [("request_id": String)]); + let input: SchemaTypeInput = syn::parse2(tokens).unwrap(); + assert_eq!(input.pick.unwrap(), vec!["name", "email"]); + let add = input.add.unwrap(); + assert_eq!(add.len(), 1); + assert_eq!(add[0].0, "request_id"); + } + + #[test] + fn test_parse_schema_type_input_with_omit_and_add() { + let tokens = quote::quote!(UserPublicWithMeta from User, omit = ["password"], add = [("display_name": String)]); + let input: SchemaTypeInput = syn::parse2(tokens).unwrap(); + assert_eq!(input.omit.unwrap(), vec!["password"]); + let add = input.add.unwrap(); + assert_eq!(add.len(), 1); + assert_eq!(add[0].0, "display_name"); + } + + #[test] + fn test_parse_schema_type_input_with_add_complex_type() { + let tokens = quote::quote!(UserWithVec from User, add = [("tags": Vec)]); + let input: SchemaTypeInput = syn::parse2(tokens).unwrap(); + let add = input.add.unwrap(); + assert_eq!(add.len(), 1); + assert_eq!(add[0].0, "tags"); + } + + // ========================================================================= + // Tests for generate_schema_code() - success paths + // ========================================================================= + + fn create_test_struct_metadata( + name: &str, + definition: &str, + ) -> crate::metadata::StructMetadata { + crate::metadata::StructMetadata::new(name.to_string(), definition.to_string()) + } + + #[test] + fn test_generate_schema_code_simple_struct() { + let storage = vec![create_test_struct_metadata( + "User", + "pub struct User { pub id: i32, pub name: String }", + )]; + + let tokens = quote::quote!(User); + let input: SchemaInput = syn::parse2(tokens).unwrap(); + let result = generate_schema_code(&input, &storage); + + assert!(result.is_ok()); + let output = result.unwrap().to_string(); + assert!(output.contains("properties")); + assert!(output.contains("Schema")); + } + + #[test] + fn test_generate_schema_code_with_omit() { + let storage = vec![create_test_struct_metadata( + "User", + "pub struct User { pub id: i32, pub name: String, pub password: String }", + )]; + + let tokens = quote::quote!(User, omit = ["password"]); + let input: SchemaInput = syn::parse2(tokens).unwrap(); + let result = generate_schema_code(&input, &storage); + + assert!(result.is_ok()); + let output = result.unwrap().to_string(); + // Should have id and name but not password in properties + assert!(output.contains("properties")); + } + + #[test] + fn test_generate_schema_code_with_pick() { + let storage = vec![create_test_struct_metadata( + "User", + "pub struct User { pub id: i32, pub name: String, pub email: String }", + )]; + + let tokens = quote::quote!(User, pick = ["id", "name"]); + let input: SchemaInput = syn::parse2(tokens).unwrap(); + let result = generate_schema_code(&input, &storage); + + assert!(result.is_ok()); + let output = result.unwrap().to_string(); + assert!(output.contains("properties")); + } + + // ========================================================================= + // Tests for generate_schema_code() - error paths + // ========================================================================= + + #[test] + fn test_generate_schema_code_type_not_found() { + let storage: Vec = vec![]; + + let tokens = quote::quote!(NonExistent); + let input: SchemaInput = syn::parse2(tokens).unwrap(); + let result = generate_schema_code(&input, &storage); + + assert!(result.is_err()); + let err = result.unwrap_err().to_string(); + assert!(err.contains("not found")); + } + + #[test] + fn test_generate_schema_code_malformed_definition() { + let storage = vec![create_test_struct_metadata( + "BadStruct", + "this is not valid rust code {{{", + )]; + + let tokens = quote::quote!(BadStruct); + let input: SchemaInput = syn::parse2(tokens).unwrap(); + let result = generate_schema_code(&input, &storage); + + assert!(result.is_err()); + let err = result.unwrap_err().to_string(); + assert!(err.contains("failed to parse")); + } + + // ========================================================================= + // Tests for schema_ref_to_tokens() + // ========================================================================= + + #[test] + fn test_schema_ref_to_tokens_ref_variant() { + use vespera_core::schema::{Reference, SchemaRef}; + + let schema_ref = SchemaRef::Ref(Reference::new("#/components/schemas/User".to_string())); + let tokens = schema_ref_to_tokens(&schema_ref); + let output = tokens.to_string(); + + assert!(output.contains("SchemaRef :: Ref")); + assert!(output.contains("Reference :: new")); + } + + #[test] + fn test_schema_ref_to_tokens_inline_variant() { + use vespera_core::schema::{Schema, SchemaRef, SchemaType}; + + let schema = Schema::new(SchemaType::String); + let schema_ref = SchemaRef::Inline(Box::new(schema)); + let tokens = schema_ref_to_tokens(&schema_ref); + let output = tokens.to_string(); + + assert!(output.contains("SchemaRef :: Inline")); + assert!(output.contains("Box :: new")); + } + + // ========================================================================= + // Tests for schema_to_tokens() + // ========================================================================= + + #[test] + fn test_schema_to_tokens_string_type() { + use vespera_core::schema::{Schema, SchemaType}; + + let schema = Schema::new(SchemaType::String); + let tokens = schema_to_tokens(&schema); + let output = tokens.to_string(); + + assert!(output.contains("SchemaType :: String")); + } + + #[test] + fn test_schema_to_tokens_integer_type() { + use vespera_core::schema::{Schema, SchemaType}; + + let schema = Schema::new(SchemaType::Integer); + let tokens = schema_to_tokens(&schema); + let output = tokens.to_string(); + + assert!(output.contains("SchemaType :: Integer")); + } + + #[test] + fn test_schema_to_tokens_number_type() { + use vespera_core::schema::{Schema, SchemaType}; + + let schema = Schema::new(SchemaType::Number); + let tokens = schema_to_tokens(&schema); + let output = tokens.to_string(); + + assert!(output.contains("SchemaType :: Number")); + } + + #[test] + fn test_schema_to_tokens_boolean_type() { + use vespera_core::schema::{Schema, SchemaType}; + + let schema = Schema::new(SchemaType::Boolean); + let tokens = schema_to_tokens(&schema); + let output = tokens.to_string(); + + assert!(output.contains("SchemaType :: Boolean")); + } + + #[test] + fn test_schema_to_tokens_array_type() { + use vespera_core::schema::{Schema, SchemaType}; + + let schema = Schema::new(SchemaType::Array); + let tokens = schema_to_tokens(&schema); + let output = tokens.to_string(); + + assert!(output.contains("SchemaType :: Array")); + } + + #[test] + fn test_schema_to_tokens_object_type() { + use vespera_core::schema::{Schema, SchemaType}; + + let schema = Schema::new(SchemaType::Object); + let tokens = schema_to_tokens(&schema); + let output = tokens.to_string(); + + assert!(output.contains("SchemaType :: Object")); + } + + #[test] + fn test_schema_to_tokens_null_type() { + use vespera_core::schema::{Schema, SchemaType}; + + let schema = Schema::new(SchemaType::Null); + let tokens = schema_to_tokens(&schema); + let output = tokens.to_string(); + + assert!(output.contains("SchemaType :: Null")); + } + + #[test] + fn test_schema_to_tokens_with_format() { + use vespera_core::schema::{Schema, SchemaType}; + + let mut schema = Schema::new(SchemaType::String); + schema.format = Some("date-time".to_string()); + let tokens = schema_to_tokens(&schema); + let output = tokens.to_string(); + + assert!(output.contains("date-time")); + } + + #[test] + fn test_schema_to_tokens_with_nullable() { + use vespera_core::schema::{Schema, SchemaType}; + + let mut schema = Schema::new(SchemaType::String); + schema.nullable = Some(true); + let tokens = schema_to_tokens(&schema); + let output = tokens.to_string(); + + assert!(output.contains("Some (true)")); + } + + #[test] + fn test_schema_to_tokens_with_ref_path() { + use vespera_core::schema::{Schema, SchemaType}; + + let mut schema = Schema::new(SchemaType::Object); + schema.ref_path = Some("#/components/schemas/User".to_string()); + let tokens = schema_to_tokens(&schema); + let output = tokens.to_string(); + + assert!(output.contains("#/components/schemas/User")); + } + + #[test] + fn test_schema_to_tokens_with_items() { + use vespera_core::schema::{Schema, SchemaRef, SchemaType}; + + let mut schema = Schema::new(SchemaType::Array); + let item_schema = Schema::new(SchemaType::String); + schema.items = Some(Box::new(SchemaRef::Inline(Box::new(item_schema)))); + let tokens = schema_to_tokens(&schema); + let output = tokens.to_string(); + + assert!(output.contains("items")); + assert!(output.contains("Some (Box :: new")); + } + + #[test] + fn test_schema_to_tokens_with_properties() { + use std::collections::BTreeMap; + use vespera_core::schema::{Schema, SchemaRef, SchemaType}; + + let mut schema = Schema::new(SchemaType::Object); + let mut props = BTreeMap::new(); + props.insert( + "name".to_string(), + SchemaRef::Inline(Box::new(Schema::new(SchemaType::String))), + ); + schema.properties = Some(props); + let tokens = schema_to_tokens(&schema); + let output = tokens.to_string(); + + assert!(output.contains("properties")); + assert!(output.contains("name")); + } + + #[test] + fn test_schema_to_tokens_with_required() { + use vespera_core::schema::{Schema, SchemaType}; + + let mut schema = Schema::new(SchemaType::Object); + schema.required = Some(vec!["id".to_string(), "name".to_string()]); + let tokens = schema_to_tokens(&schema); + let output = tokens.to_string(); + + assert!(output.contains("required")); + assert!(output.contains("id")); + assert!(output.contains("name")); + } + + // ========================================================================= + // Tests for generate_schema_type_code() - validation errors + // ========================================================================= + + #[test] + fn test_generate_schema_type_code_pick_nonexistent_field() { + let storage = vec![create_test_struct_metadata( + "User", + "pub struct User { pub id: i32, pub name: String }", + )]; + + let tokens = quote::quote!(NewUser from User, pick = ["nonexistent"]); + let input: SchemaTypeInput = syn::parse2(tokens).unwrap(); + let result = generate_schema_type_code(&input, &storage); + + assert!(result.is_err()); + let err = result.unwrap_err().to_string(); + assert!(err.contains("does not exist")); + assert!(err.contains("nonexistent")); + } + + #[test] + fn test_generate_schema_type_code_omit_nonexistent_field() { + let storage = vec![create_test_struct_metadata( + "User", + "pub struct User { pub id: i32, pub name: String }", + )]; + + let tokens = quote::quote!(NewUser from User, omit = ["nonexistent"]); + let input: SchemaTypeInput = syn::parse2(tokens).unwrap(); + let result = generate_schema_type_code(&input, &storage); + + assert!(result.is_err()); + let err = result.unwrap_err().to_string(); + assert!(err.contains("does not exist")); + assert!(err.contains("nonexistent")); + } + + #[test] + fn test_generate_schema_type_code_rename_nonexistent_field() { + let storage = vec![create_test_struct_metadata( + "User", + "pub struct User { pub id: i32, pub name: String }", + )]; + + let tokens = quote::quote!(NewUser from User, rename = [("nonexistent", "new_name")]); + let input: SchemaTypeInput = syn::parse2(tokens).unwrap(); + let result = generate_schema_type_code(&input, &storage); + + assert!(result.is_err()); + let err = result.unwrap_err().to_string(); + assert!(err.contains("does not exist")); + assert!(err.contains("nonexistent")); + } + + #[test] + fn test_generate_schema_type_code_type_not_found() { + let storage: Vec = vec![]; + + let tokens = quote::quote!(NewUser from NonExistent); + let input: SchemaTypeInput = syn::parse2(tokens).unwrap(); + let result = generate_schema_type_code(&input, &storage); + + assert!(result.is_err()); + let err = result.unwrap_err().to_string(); + assert!(err.contains("not found")); + } + + #[test] + fn test_generate_schema_type_code_success() { + let storage = vec![create_test_struct_metadata( + "User", + "pub struct User { pub id: i32, pub name: String }", + )]; + + let tokens = quote::quote!(CreateUser from User, pick = ["name"]); + let input: SchemaTypeInput = syn::parse2(tokens).unwrap(); + let result = generate_schema_type_code(&input, &storage); + + assert!(result.is_ok()); + let output = result.unwrap().to_string(); + assert!(output.contains("CreateUser")); + assert!(output.contains("name")); + } + + #[test] + fn test_generate_schema_type_code_with_omit() { + let storage = vec![create_test_struct_metadata( + "User", + "pub struct User { pub id: i32, pub name: String, pub password: String }", + )]; + + let tokens = quote::quote!(SafeUser from User, omit = ["password"]); + let input: SchemaTypeInput = syn::parse2(tokens).unwrap(); + let result = generate_schema_type_code(&input, &storage); + + assert!(result.is_ok()); + let output = result.unwrap().to_string(); + assert!(output.contains("SafeUser")); + // Should not contain password + assert!(!output.contains("password")); + } + + #[test] + fn test_generate_schema_type_code_with_add() { + let storage = vec![create_test_struct_metadata( + "User", + "pub struct User { pub id: i32, pub name: String }", + )]; + + let tokens = quote::quote!(UserWithExtra from User, add = [("extra": String)]); + let input: SchemaTypeInput = syn::parse2(tokens).unwrap(); + let result = generate_schema_type_code(&input, &storage); + + assert!(result.is_ok()); + let output = result.unwrap().to_string(); + assert!(output.contains("UserWithExtra")); + assert!(output.contains("extra")); + } + + #[test] + fn test_generate_schema_type_code_generates_from_impl() { + let storage = vec![create_test_struct_metadata( + "User", + "pub struct User { pub id: i32, pub name: String }", + )]; + + // Without add parameter, should generate From impl + let tokens = quote::quote!(UserResponse from User, pick = ["id", "name"]); + let input: SchemaTypeInput = syn::parse2(tokens).unwrap(); + let result = generate_schema_type_code(&input, &storage); + + assert!(result.is_ok()); + let output = result.unwrap().to_string(); + assert!(output.contains("impl From")); + assert!(output.contains("for UserResponse")); + } + + #[test] + fn test_generate_schema_type_code_no_from_impl_with_add() { + let storage = vec![create_test_struct_metadata( + "User", + "pub struct User { pub id: i32, pub name: String }", + )]; + + // With add parameter, should NOT generate From impl + let tokens = quote::quote!(UserWithExtra from User, add = [("extra": String)]); + let input: SchemaTypeInput = syn::parse2(tokens).unwrap(); + let result = generate_schema_type_code(&input, &storage); + + assert!(result.is_ok()); + let output = result.unwrap().to_string(); + // Should NOT contain From impl when add is used + assert!(!output.contains("impl From")); + } + + // ========================================================================= + // Tests for is_option_type() + // ========================================================================= + + #[test] + fn test_is_option_type_true() { + let ty: syn::Type = syn::parse_str("Option").unwrap(); + assert!(is_option_type(&ty)); + } + + #[test] + fn test_is_option_type_false() { + let ty: syn::Type = syn::parse_str("String").unwrap(); + assert!(!is_option_type(&ty)); + } + + #[test] + fn test_is_option_type_vec_false() { + let ty: syn::Type = syn::parse_str("Vec").unwrap(); + assert!(!is_option_type(&ty)); + } + + // ========================================================================= + // Tests for extract_type_name() + // ========================================================================= + + #[test] + fn test_extract_type_name_simple() { + let ty: syn::Type = syn::parse_str("User").unwrap(); + let name = extract_type_name(&ty).unwrap(); + assert_eq!(name, "User"); + } + + #[test] + fn test_extract_type_name_with_path() { + let ty: syn::Type = syn::parse_str("crate::models::User").unwrap(); + let name = extract_type_name(&ty).unwrap(); + assert_eq!(name, "User"); + } + + #[test] + fn test_extract_type_name_non_path_error() { + // Reference type is not a Type::Path + let ty: syn::Type = syn::parse_str("&str").unwrap(); + let result = extract_type_name(&ty); + assert!(result.is_err()); + } +} diff --git a/examples/axum-example/Cargo.toml b/examples/axum-example/Cargo.toml index ed1f830..0b6060a 100644 --- a/examples/axum-example/Cargo.toml +++ b/examples/axum-example/Cargo.toml @@ -10,6 +10,7 @@ tokio = { version = "1", features = ["full"] } serde = { version = "1", features = ["derive"] } serde_json = "1" tower-http = { version = "0.6", features = ["cors"] } +sea-orm = { version = "^2.0.0-rc.29", features = ["sqlx-sqlite", "runtime-tokio-rustls", "macros"] } third = { path = "../third" } diff --git a/examples/axum-example/openapi.json b/examples/axum-example/openapi.json index 95c903b..f77e292 100644 --- a/examples/axum-example/openapi.json +++ b/examples/axum-example/openapi.json @@ -603,6 +603,106 @@ } } }, + "/memos": { + "post": { + "operationId": "create_memo", + "description": "Create a new memo", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateMemoRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateMemoRequest" + } + } + } + } + } + }, + "put": { + "operationId": "update_memo", + "description": "Update a memo", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateMemoRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateMemoRequest" + } + } + } + } + } + } + }, + "/memos/format": { + "get": { + "operationId": "get_memo_format", + "description": "Get memo response format", + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/memos/{id}": { + "get": { + "operationId": "get_memo", + "description": "Get memo by id", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MemoResponse" + } + } + } + } + } + } + }, "/no-schema-query": { "get": { "operationId": "mod_file_with_no_schema_query", @@ -1033,7 +1133,7 @@ "/users": { "get": { "operationId": "get_users", - "description": "Get all users", + "description": "Get all users (returns public response without internal_score)", "responses": { "200": { "description": "Successful response", @@ -1042,7 +1142,7 @@ "schema": { "type": "array", "items": { - "$ref": "#/components/schemas/User" + "$ref": "#/components/schemas/UserPublicResponse" } } } @@ -1052,7 +1152,7 @@ }, "post": { "operationId": "create_user", - "description": "Create a new user", + "description": "Create a new user\nRequest body uses CreateUserRequest (generated from User with only name, email)", "requestBody": { "required": true, "content": { @@ -1069,7 +1169,35 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/User" + "$ref": "#/components/schemas/UserPublicResponse" + } + } + } + } + } + } + }, + "/users/dto/{id}": { + "get": { + "operationId": "get_user_dto", + "description": "Get user DTO (demonstrates field rename feature)\nThe Rust struct uses user_id/display_name, but JSON uses id/name", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserDTO" } } } @@ -1094,10 +1222,59 @@ } } }, + "/users/summary": { + "get": { + "operationId": "get_users_summary", + "description": "Get user summaries (minimal fields for list views)", + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UserSummary" + } + } + } + } + } + } + } + }, + "/users/with-meta": { + "post": { + "operationId": "create_user_with_meta", + "description": "Create a new user with metadata (demonstrates `add` feature)\nRequest body uses CreateUserWithMeta (picks name/email, adds request_id/created_at)", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateUserWithMeta" + } + } + } + }, + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateUserWithMeta" + } + } + } + } + } + } + }, "/users/{id}": { "get": { "operationId": "get_user", - "description": "Get user by ID", + "description": "Get user by ID (full internal view)", "parameters": [ { "name": "id", @@ -1350,6 +1527,21 @@ "createdAt" ] }, + "CreateMemoRequest": { + "type": "object", + "properties": { + "content": { + "type": "string" + }, + "title": { + "type": "string" + } + }, + "required": [ + "title", + "content" + ] + }, "CreateUserRequest": { "type": "object", "properties": { @@ -1365,6 +1557,29 @@ "email" ] }, + "CreateUserWithMeta": { + "type": "object", + "properties": { + "created_at": { + "type": "string", + "nullable": true + }, + "email": { + "type": "string" + }, + "name": { + "type": "string" + }, + "request_id": { + "type": "string" + } + }, + "required": [ + "name", + "email", + "request_id" + ] + }, "Enum": { "type": "string", "enum": [ @@ -1684,6 +1899,30 @@ "age" ] }, + "MemoResponse": { + "type": "object", + "properties": { + "content": { + "type": "string" + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "id": { + "type": "integer" + }, + "title": { + "type": "string" + } + }, + "required": [ + "id", + "title", + "content", + "created_at" + ] + }, "PaginatedResponse": { "type": "object", "properties": { @@ -1916,7 +2155,64 @@ "age" ] }, + "UpdateMemoRequest": { + "type": "object", + "properties": { + "content": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "title": { + "type": "string" + } + }, + "required": [ + "title", + "content", + "id" + ] + }, "User": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "internal_score": { + "type": "integer", + "nullable": true + }, + "name": { + "type": "string" + } + }, + "required": [ + "id", + "name", + "email" + ] + }, + "UserDTO": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + } + }, + "required": [ + "id", + "name" + ] + }, + "UserPublicResponse": { "type": "object", "properties": { "email": { @@ -1934,6 +2230,21 @@ "name", "email" ] + }, + "UserSummary": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + } + }, + "required": [ + "id", + "name" + ] } } }, diff --git a/examples/axum-example/src/lib.rs b/examples/axum-example/src/lib.rs index f44f51d..f6768fa 100644 --- a/examples/axum-example/src/lib.rs +++ b/examples/axum-example/src/lib.rs @@ -1,3 +1,4 @@ +mod models; mod routes; use std::sync::Arc; diff --git a/examples/axum-example/src/models/memo.rs b/examples/axum-example/src/models/memo.rs new file mode 100644 index 0000000..879fea9 --- /dev/null +++ b/examples/axum-example/src/models/memo.rs @@ -0,0 +1,21 @@ +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +/// Memo storage for example-memo-plugin +#[sea_orm::model] +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize, Deserialize)] +#[sea_orm(table_name = "memp_memos")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub title: String, + pub content: String, + #[sea_orm(indexed, default_value = "NOW()")] + pub created_at: DateTimeWithTimeZone, + #[sea_orm(default_value = "NOW()")] + pub updated_at: DateTimeWithTimeZone, +} + +// Index definitions (SeaORM uses Statement builders externally) +// (unnamed) on [created_at] +impl ActiveModelBehavior for ActiveModel {} diff --git a/examples/axum-example/src/models/mod.rs b/examples/axum-example/src/models/mod.rs new file mode 100644 index 0000000..80d0261 --- /dev/null +++ b/examples/axum-example/src/models/mod.rs @@ -0,0 +1 @@ +pub mod memo; diff --git a/examples/axum-example/src/routes/memos.rs b/examples/axum-example/src/routes/memos.rs new file mode 100644 index 0000000..256af2d --- /dev/null +++ b/examples/axum-example/src/routes/memos.rs @@ -0,0 +1,74 @@ +//! Test schema_type! with models from other files +//! +//! This demonstrates that schema_type! can reference structs from other files +//! using module paths like `crate::models::memo::Model`. The macro will: +//! 1. Parse the module path +//! 2. Find the corresponding file (src/models/memo.rs) +//! 3. Extract the struct definition +//! 4. Generate the new type + From impl for easy conversion + +// Import types used by the source model that we want to include in generated structs +use sea_orm::entity::prelude::DateTimeWithTimeZone; +use vespera::{ + axum::{Json, extract::Path}, + schema_type, +}; + +// ============================================================================ +// schema_type! generates request/response types from models in OTHER FILES +// Also generates From impl when `add` is not used +// ============================================================================ + +// Create request type: only title and content (no id, timestamps) +// Has From impl: crate::models::memo::Model -> CreateMemoRequest +schema_type!(CreateMemoRequest from crate::models::memo::Model, pick = ["title", "content"]); + +// Update request: title/content + manually added id field +// NO From impl (because `add` is used - can't auto-populate added fields) +schema_type!(UpdateMemoRequest from crate::models::memo::Model, pick = ["title", "content"], add = [("id": i32)]); + +// Response type: all fields except updated_at +// Has From impl: crate::models::memo::Model -> MemoResponse +schema_type!(MemoResponse from crate::models::memo::Model, omit = ["updated_at"]); + +/// Create a new memo +#[vespera::route(post)] +pub async fn create_memo(Json(req): Json) -> Json { + // Echo back the request to verify it works + Json(CreateMemoRequest { + title: req.title, + content: req.content, + }) +} + +/// Update a memo +#[vespera::route(put)] +pub async fn update_memo(Json(req): Json) -> Json { + // Echo back - demonstrates `add` parameter with sea-orm model + Json(UpdateMemoRequest { + id: req.id, + title: req.title, + content: req.content, + }) +} + +/// Get memo by id +#[vespera::route(get, path = "/{id}")] +pub async fn get_memo(Path(id): Path) -> Json { + // In real app, this would be a DB query returning Model + // schema_type! generates From for MemoResponse, so .into() works + let model = crate::models::memo::Model { + id, + title: "Test Memo".to_string(), + content: "This is test content".to_string(), + created_at: DateTimeWithTimeZone::default(), + updated_at: DateTimeWithTimeZone::default(), + }; + Json(model.into()) +} + +/// Get memo response format +#[vespera::route(get, path = "/format")] +pub async fn get_memo_format() -> &'static str { + "MemoResponse has: id, title, content, created_at (no updated_at)" +} diff --git a/examples/axum-example/src/routes/mod.rs b/examples/axum-example/src/routes/mod.rs index 20c7498..e968aec 100644 --- a/examples/axum-example/src/routes/mod.rs +++ b/examples/axum-example/src/routes/mod.rs @@ -13,6 +13,7 @@ pub mod error; pub mod foo; pub mod generic; pub mod health; +pub mod memos; pub mod path; pub mod typed_header; pub mod users; diff --git a/examples/axum-example/src/routes/users.rs b/examples/axum-example/src/routes/users.rs index 105edce..07c3ef9 100644 --- a/examples/axum-example/src/routes/users.rs +++ b/examples/axum-example/src/routes/users.rs @@ -4,31 +4,52 @@ use serde::{Deserialize, Serialize}; use vespera::{ Schema, axum::{Json, extract::Path}, + schema_type, }; +/// Full user model with all fields #[derive(Serialize, Deserialize, Clone, Schema)] pub struct User { pub id: u32, pub name: String, pub email: String, + /// Internal field - should be omitted in public APIs + pub internal_score: Option, } -#[derive(Serialize, Deserialize, Schema)] -pub struct CreateUserRequest { - pub name: String, - pub email: String, -} +// ============================================================================ +// schema_type! - Generate request/response types from base model +// ============================================================================ +// No more manual struct definitions! Derive them from User. + +// Request type: only name and email (auto-generated from User) +schema_type!(CreateUserRequest from User, pick = ["name", "email"]); + +// Public response type: User without internal_score (auto-generated) +schema_type!(UserPublicResponse from User, omit = ["internal_score"]); + +// Summary type for list views: only id and name (auto-generated) +schema_type!(UserSummary from User, pick = ["id", "name"]); -/// Get all users +// DTO type with renamed fields: id -> user_id, name -> display_name +// JSON serialization uses original field names (id, name) for API compatibility +schema_type!(UserDTO from User, pick = ["id", "name"], rename = [("id", "user_id"), ("name", "display_name")]); + +// Request type with additional metadata fields +// Picks name/email from User, adds request_id and created_at timestamp +schema_type!(CreateUserWithMeta from User, pick = ["name", "email"], add = [("request_id": String), ("created_at": Option)]); + +/// Get all users (returns public response without internal_score) #[vespera::route(get)] -pub async fn get_users() -> Json> { +pub async fn get_users() -> Json> { + // Using UserPublicResponse generated by schema_type! Json(vec![ - User { + UserPublicResponse { id: 1, name: "Alice".to_string(), email: "alice@example.com".to_string(), }, - User { + UserPublicResponse { id: 2, name: "Bob".to_string(), email: "bob@example.com".to_string(), @@ -36,26 +57,75 @@ pub async fn get_users() -> Json> { ]) } -/// Get user by ID +/// Get user summaries (minimal fields for list views) +#[vespera::route(get, path = "/summary")] +pub async fn get_users_summary() -> Json> { + // Using UserSummary generated by schema_type! + Json(vec![ + UserSummary { + id: 1, + name: "Alice".to_string(), + }, + UserSummary { + id: 2, + name: "Bob".to_string(), + }, + ]) +} + +/// Get user DTO (demonstrates field rename feature) +/// The Rust struct uses user_id/display_name, but JSON uses id/name +#[vespera::route(get, path = "/dto/{id}")] +pub async fn get_user_dto(Path(id): Path) -> Json { + // Using UserDTO generated by schema_type! with renamed fields + // Rust code uses: user_id, display_name + // JSON output uses: id, name (via serde rename) + Json(UserDTO { + user_id: id, + display_name: format!("User {}", id), + }) +} + +/// Get user by ID (full internal view) #[vespera::route(get, path = "/{id}")] pub async fn get_user(Path(id): Path) -> Json { Json(User { id, name: format!("User {}", id), email: format!("user{}@example.com", id), + internal_score: Some(100), }) } /// Create a new user +/// Request body uses CreateUserRequest (generated from User with only name, email) #[vespera::route(post)] -pub async fn create_user(Json(user): Json) -> Json { - Json(User { +pub async fn create_user(Json(user): Json) -> Json { + // Input: CreateUserRequest (auto-generated, picks name + email) + // Output: UserPublicResponse (auto-generated, omits internal_score) + Json(UserPublicResponse { id: 100, name: user.name, email: user.email, }) } +/// Create a new user with metadata (demonstrates `add` feature) +/// Request body uses CreateUserWithMeta (picks name/email, adds request_id/created_at) +#[vespera::route(post, path = "/with-meta")] +pub async fn create_user_with_meta( + Json(user): Json, +) -> Json { + // Input: CreateUserWithMeta (auto-generated with added fields) + // The struct has: name, email (from User) + request_id, created_at (added) + Json(CreateUserWithMeta { + name: user.name, + email: user.email, + request_id: user.request_id, + created_at: Some("2024-01-27T12:00:00Z".to_string()), + }) +} + #[derive(Serialize, Deserialize, Schema)] pub struct SkipResponse { pub name: String, diff --git a/examples/axum-example/tests/integration_test.rs b/examples/axum-example/tests/integration_test.rs index d15eaf1..8c85bcf 100644 --- a/examples/axum-example/tests/integration_test.rs +++ b/examples/axum-example/tests/integration_test.rs @@ -1,6 +1,8 @@ use axum_example::{create_app, create_app_with_layer}; use axum_test::TestServer; +use serde::{Deserialize, Serialize}; use serde_json::json; +use vespera::{Schema, schema}; #[tokio::test] async fn test_health_endpoint() { @@ -403,3 +405,320 @@ async fn test_app_with_layer() { async fn test_openapi() { insta::assert_snapshot!("openapi", std::fs::read_to_string("openapi.json").unwrap()); } + +// Tests for schema! macro +// Note: schema! requires #[derive(Schema)] in the same compilation unit, +// so we define the test structs here. + +/// Test struct for schema! macro tests +#[derive(Serialize, Deserialize, Clone, Schema)] +pub struct TestUser { + pub id: u32, + pub name: String, + pub email: String, +} + +/// Test struct with optional fields +#[derive(Serialize, Deserialize, Clone, Schema)] +pub struct TestUserWithOptional { + pub id: u32, + pub name: String, + pub email: Option, + #[serde(default)] + pub bio: String, +} + +/// Test struct with serde rename +#[derive(Serialize, Deserialize, Clone, Schema)] +#[serde(rename_all = "camelCase")] +pub struct TestUserCamelCase { + pub user_id: u32, + pub user_name: String, + pub email_address: String, +} + +#[test] +fn test_schema_macro_full() { + // Generate full schema for TestUser + let user_schema = schema!(TestUser); + + // Verify schema type + assert_eq!( + user_schema.schema_type, + Some(vespera::schema::SchemaType::Object) + ); + + // Verify all properties are present + let properties = user_schema.properties.unwrap(); + assert!(properties.contains_key("id"), "Missing 'id' property"); + assert!(properties.contains_key("name"), "Missing 'name' property"); + assert!(properties.contains_key("email"), "Missing 'email' property"); + + // Verify required fields + let required = user_schema.required.unwrap(); + assert!(required.contains(&"id".to_string())); + assert!(required.contains(&"name".to_string())); + assert!(required.contains(&"email".to_string())); +} + +#[test] +fn test_schema_macro_with_omit() { + // Generate schema with 'email' field omitted + let user_schema = schema!(TestUser, omit = ["email"]); + + // Verify schema type + assert_eq!( + user_schema.schema_type, + Some(vespera::schema::SchemaType::Object) + ); + + // Verify properties - email should be omitted + let properties = user_schema.properties.unwrap(); + assert!(properties.contains_key("id"), "Missing 'id' property"); + assert!(properties.contains_key("name"), "Missing 'name' property"); + assert!( + !properties.contains_key("email"), + "'email' should be omitted" + ); + + // Verify required fields - email should not be in required + let required = user_schema.required.unwrap(); + assert!(required.contains(&"id".to_string())); + assert!(required.contains(&"name".to_string())); + assert!(!required.contains(&"email".to_string())); +} + +#[test] +fn test_schema_macro_with_multiple_omit() { + // Generate schema with multiple fields omitted + let user_schema = schema!(TestUser, omit = ["id", "email"]); + + // Verify properties - id and email should be omitted + let properties = user_schema.properties.unwrap(); + assert!(!properties.contains_key("id"), "'id' should be omitted"); + assert!(properties.contains_key("name"), "Missing 'name' property"); + assert!( + !properties.contains_key("email"), + "'email' should be omitted" + ); + + // Verify only 'name' is required + let required = user_schema.required.unwrap(); + assert_eq!(required.len(), 1); + assert!(required.contains(&"name".to_string())); +} + +#[test] +fn test_schema_macro_with_pick() { + // Generate schema with only 'id' and 'name' fields + let user_schema = schema!(TestUser, pick = ["id", "name"]); + + // Verify properties - only id and name should be present + let properties = user_schema.properties.unwrap(); + assert!(properties.contains_key("id"), "Missing 'id' property"); + assert!(properties.contains_key("name"), "Missing 'name' property"); + assert!( + !properties.contains_key("email"), + "'email' should not be picked" + ); + + // Verify required fields + let required = user_schema.required.unwrap(); + assert!(required.contains(&"id".to_string())); + assert!(required.contains(&"name".to_string())); +} + +#[test] +fn test_schema_macro_with_optional_fields() { + // Generate schema for struct with optional fields + let user_schema = schema!(TestUserWithOptional); + + let properties = user_schema.properties.unwrap(); + assert_eq!(properties.len(), 4); + + // Only 'id' and 'name' should be required + // 'email' is Option and 'bio' has #[serde(default)] + let required = user_schema.required.unwrap(); + assert!(required.contains(&"id".to_string())); + assert!(required.contains(&"name".to_string())); + assert!( + !required.contains(&"email".to_string()), + "'email' is Option, should not be required" + ); + assert!( + !required.contains(&"bio".to_string()), + "'bio' has default, should not be required" + ); +} + +#[test] +fn test_schema_macro_with_rename_all() { + // Generate schema for struct with rename_all = "camelCase" + let user_schema = schema!(TestUserCamelCase); + + let properties = user_schema.properties.unwrap(); + + // Properties should have camelCase names + assert!( + properties.contains_key("userId"), + "Missing 'userId' property (renamed from user_id)" + ); + assert!( + properties.contains_key("userName"), + "Missing 'userName' property (renamed from user_name)" + ); + assert!( + properties.contains_key("emailAddress"), + "Missing 'emailAddress' property (renamed from email_address)" + ); + + // Should NOT have snake_case names + assert!(!properties.contains_key("user_id")); + assert!(!properties.contains_key("user_name")); + assert!(!properties.contains_key("email_address")); +} + +#[test] +fn test_schema_macro_omit_with_renamed_field() { + // Omit using the JSON name (camelCase) + let user_schema = schema!(TestUserCamelCase, omit = ["emailAddress"]); + + let properties = user_schema.properties.unwrap(); + assert!(properties.contains_key("userId")); + assert!(properties.contains_key("userName")); + assert!( + !properties.contains_key("emailAddress"), + "'emailAddress' should be omitted" + ); +} + +#[test] +fn test_schema_macro_omit_with_rust_field_name() { + // Omit using the Rust field name (snake_case) - should also work + let user_schema = schema!(TestUserCamelCase, omit = ["email_address"]); + + let properties = user_schema.properties.unwrap(); + assert!(properties.contains_key("userId")); + assert!(properties.contains_key("userName")); + assert!( + !properties.contains_key("emailAddress"), + "'email_address' (rust name) should omit 'emailAddress'" + ); +} + +// Tests for schema_type! with rename option + +#[tokio::test] +async fn test_get_user_dto_with_renamed_fields() { + let app = create_app(); + let server = TestServer::new(app).unwrap(); + + let response = server.get("/users/dto/42").await; + + response.assert_status_ok(); + let user: serde_json::Value = response.json(); + + // JSON should use original field names (id, name) due to serde(rename) + // even though Rust struct uses user_id, display_name + assert_eq!(user["id"], 42, "JSON should serialize 'user_id' as 'id'"); + assert_eq!( + user["name"], "User 42", + "JSON should serialize 'display_name' as 'name'" + ); + + // Verify renamed field names are NOT in JSON + assert!( + user.get("user_id").is_none(), + "'user_id' should not appear in JSON" + ); + assert!( + user.get("display_name").is_none(), + "'display_name' should not appear in JSON" + ); +} + +// Tests for schema_type! with add option + +#[tokio::test] +async fn test_create_user_with_meta_add_fields() { + let app = create_app(); + let server = TestServer::new(app).unwrap(); + + // CreateUserWithMeta has: name, email (from User) + request_id, created_at (added) + let request_body = json!({ + "name": "Test User", + "email": "test@example.com", + "request_id": "req-12345", + "created_at": null + }); + + let response = server.post("/users/with-meta").json(&request_body).await; + + response.assert_status_ok(); + let result: serde_json::Value = response.json(); + + // Verify fields from User (picked) + assert_eq!(result["name"], "Test User"); + assert_eq!(result["email"], "test@example.com"); + + // Verify added fields + assert_eq!(result["request_id"], "req-12345"); + assert_eq!(result["created_at"], "2024-01-27T12:00:00Z"); // Server fills this in +} + +// Tests for schema_type! with sea-orm-like models + +#[tokio::test] +async fn test_memo_create_with_picked_fields() { + let app = create_app(); + let server = TestServer::new(app).unwrap(); + + // CreateMemoRequest has only: title, content (picked from Memo) + let request_body = json!({ + "title": "Test Memo", + "content": "This is test content" + }); + + let response = server.post("/memos").json(&request_body).await; + + response.assert_status_ok(); + let result: serde_json::Value = response.json(); + + assert_eq!(result["title"], "Test Memo"); + assert_eq!(result["content"], "This is test content"); + + // These fields should NOT be in the response (not picked) + assert!( + result.get("id").is_none(), + "id should not be in CreateMemoRequest" + ); + assert!( + result.get("created_at").is_none(), + "created_at should not be in CreateMemoRequest" + ); +} + +#[tokio::test] +async fn test_memo_update_with_added_id_field() { + let app = create_app(); + let server = TestServer::new(app).unwrap(); + + // UpdateMemoRequest has: title, content (picked) + id (added) + let request_body = json!({ + "id": 42, + "title": "Updated Memo", + "content": "Updated content" + }); + + let response = server.put("/memos").json(&request_body).await; + + response.assert_status_ok(); + let result: serde_json::Value = response.json(); + + // Verify picked fields + assert_eq!(result["title"], "Updated Memo"); + assert_eq!(result["content"], "Updated content"); + + // Verify added field + assert_eq!(result["id"], 42, "id should be present (added field)"); +} diff --git a/examples/axum-example/tests/snapshots/integration_test__openapi.snap b/examples/axum-example/tests/snapshots/integration_test__openapi.snap index 1b5d6dc..0b2af2b 100644 --- a/examples/axum-example/tests/snapshots/integration_test__openapi.snap +++ b/examples/axum-example/tests/snapshots/integration_test__openapi.snap @@ -607,6 +607,106 @@ expression: "std::fs::read_to_string(\"openapi.json\").unwrap()" } } }, + "/memos": { + "post": { + "operationId": "create_memo", + "description": "Create a new memo", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateMemoRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateMemoRequest" + } + } + } + } + } + }, + "put": { + "operationId": "update_memo", + "description": "Update a memo", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateMemoRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateMemoRequest" + } + } + } + } + } + } + }, + "/memos/format": { + "get": { + "operationId": "get_memo_format", + "description": "Get memo response format", + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/memos/{id}": { + "get": { + "operationId": "get_memo", + "description": "Get memo by id", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MemoResponse" + } + } + } + } + } + } + }, "/no-schema-query": { "get": { "operationId": "mod_file_with_no_schema_query", @@ -1037,7 +1137,7 @@ expression: "std::fs::read_to_string(\"openapi.json\").unwrap()" "/users": { "get": { "operationId": "get_users", - "description": "Get all users", + "description": "Get all users (returns public response without internal_score)", "responses": { "200": { "description": "Successful response", @@ -1046,7 +1146,7 @@ expression: "std::fs::read_to_string(\"openapi.json\").unwrap()" "schema": { "type": "array", "items": { - "$ref": "#/components/schemas/User" + "$ref": "#/components/schemas/UserPublicResponse" } } } @@ -1056,7 +1156,7 @@ expression: "std::fs::read_to_string(\"openapi.json\").unwrap()" }, "post": { "operationId": "create_user", - "description": "Create a new user", + "description": "Create a new user\nRequest body uses CreateUserRequest (generated from User with only name, email)", "requestBody": { "required": true, "content": { @@ -1073,7 +1173,35 @@ expression: "std::fs::read_to_string(\"openapi.json\").unwrap()" "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/User" + "$ref": "#/components/schemas/UserPublicResponse" + } + } + } + } + } + } + }, + "/users/dto/{id}": { + "get": { + "operationId": "get_user_dto", + "description": "Get user DTO (demonstrates field rename feature)\nThe Rust struct uses user_id/display_name, but JSON uses id/name", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserDTO" } } } @@ -1098,10 +1226,59 @@ expression: "std::fs::read_to_string(\"openapi.json\").unwrap()" } } }, + "/users/summary": { + "get": { + "operationId": "get_users_summary", + "description": "Get user summaries (minimal fields for list views)", + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UserSummary" + } + } + } + } + } + } + } + }, + "/users/with-meta": { + "post": { + "operationId": "create_user_with_meta", + "description": "Create a new user with metadata (demonstrates `add` feature)\nRequest body uses CreateUserWithMeta (picks name/email, adds request_id/created_at)", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateUserWithMeta" + } + } + } + }, + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateUserWithMeta" + } + } + } + } + } + } + }, "/users/{id}": { "get": { "operationId": "get_user", - "description": "Get user by ID", + "description": "Get user by ID (full internal view)", "parameters": [ { "name": "id", @@ -1354,6 +1531,21 @@ expression: "std::fs::read_to_string(\"openapi.json\").unwrap()" "createdAt" ] }, + "CreateMemoRequest": { + "type": "object", + "properties": { + "content": { + "type": "string" + }, + "title": { + "type": "string" + } + }, + "required": [ + "title", + "content" + ] + }, "CreateUserRequest": { "type": "object", "properties": { @@ -1369,6 +1561,29 @@ expression: "std::fs::read_to_string(\"openapi.json\").unwrap()" "email" ] }, + "CreateUserWithMeta": { + "type": "object", + "properties": { + "created_at": { + "type": "string", + "nullable": true + }, + "email": { + "type": "string" + }, + "name": { + "type": "string" + }, + "request_id": { + "type": "string" + } + }, + "required": [ + "name", + "email", + "request_id" + ] + }, "Enum": { "type": "string", "enum": [ @@ -1688,6 +1903,30 @@ expression: "std::fs::read_to_string(\"openapi.json\").unwrap()" "age" ] }, + "MemoResponse": { + "type": "object", + "properties": { + "content": { + "type": "string" + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "id": { + "type": "integer" + }, + "title": { + "type": "string" + } + }, + "required": [ + "id", + "title", + "content", + "created_at" + ] + }, "PaginatedResponse": { "type": "object", "properties": { @@ -1920,7 +2159,64 @@ expression: "std::fs::read_to_string(\"openapi.json\").unwrap()" "age" ] }, + "UpdateMemoRequest": { + "type": "object", + "properties": { + "content": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "title": { + "type": "string" + } + }, + "required": [ + "title", + "content", + "id" + ] + }, "User": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "internal_score": { + "type": "integer", + "nullable": true + }, + "name": { + "type": "string" + } + }, + "required": [ + "id", + "name", + "email" + ] + }, + "UserDTO": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + } + }, + "required": [ + "id", + "name" + ] + }, + "UserPublicResponse": { "type": "object", "properties": { "email": { @@ -1938,6 +2234,21 @@ expression: "std::fs::read_to_string(\"openapi.json\").unwrap()" "name", "email" ] + }, + "UserSummary": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + } + }, + "required": [ + "id", + "name" + ] } } }, diff --git a/openapi.json b/openapi.json index 95c903b..f77e292 100644 --- a/openapi.json +++ b/openapi.json @@ -603,6 +603,106 @@ } } }, + "/memos": { + "post": { + "operationId": "create_memo", + "description": "Create a new memo", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateMemoRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateMemoRequest" + } + } + } + } + } + }, + "put": { + "operationId": "update_memo", + "description": "Update a memo", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateMemoRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateMemoRequest" + } + } + } + } + } + } + }, + "/memos/format": { + "get": { + "operationId": "get_memo_format", + "description": "Get memo response format", + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/memos/{id}": { + "get": { + "operationId": "get_memo", + "description": "Get memo by id", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MemoResponse" + } + } + } + } + } + } + }, "/no-schema-query": { "get": { "operationId": "mod_file_with_no_schema_query", @@ -1033,7 +1133,7 @@ "/users": { "get": { "operationId": "get_users", - "description": "Get all users", + "description": "Get all users (returns public response without internal_score)", "responses": { "200": { "description": "Successful response", @@ -1042,7 +1142,7 @@ "schema": { "type": "array", "items": { - "$ref": "#/components/schemas/User" + "$ref": "#/components/schemas/UserPublicResponse" } } } @@ -1052,7 +1152,7 @@ }, "post": { "operationId": "create_user", - "description": "Create a new user", + "description": "Create a new user\nRequest body uses CreateUserRequest (generated from User with only name, email)", "requestBody": { "required": true, "content": { @@ -1069,7 +1169,35 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/User" + "$ref": "#/components/schemas/UserPublicResponse" + } + } + } + } + } + } + }, + "/users/dto/{id}": { + "get": { + "operationId": "get_user_dto", + "description": "Get user DTO (demonstrates field rename feature)\nThe Rust struct uses user_id/display_name, but JSON uses id/name", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserDTO" } } } @@ -1094,10 +1222,59 @@ } } }, + "/users/summary": { + "get": { + "operationId": "get_users_summary", + "description": "Get user summaries (minimal fields for list views)", + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UserSummary" + } + } + } + } + } + } + } + }, + "/users/with-meta": { + "post": { + "operationId": "create_user_with_meta", + "description": "Create a new user with metadata (demonstrates `add` feature)\nRequest body uses CreateUserWithMeta (picks name/email, adds request_id/created_at)", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateUserWithMeta" + } + } + } + }, + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateUserWithMeta" + } + } + } + } + } + } + }, "/users/{id}": { "get": { "operationId": "get_user", - "description": "Get user by ID", + "description": "Get user by ID (full internal view)", "parameters": [ { "name": "id", @@ -1350,6 +1527,21 @@ "createdAt" ] }, + "CreateMemoRequest": { + "type": "object", + "properties": { + "content": { + "type": "string" + }, + "title": { + "type": "string" + } + }, + "required": [ + "title", + "content" + ] + }, "CreateUserRequest": { "type": "object", "properties": { @@ -1365,6 +1557,29 @@ "email" ] }, + "CreateUserWithMeta": { + "type": "object", + "properties": { + "created_at": { + "type": "string", + "nullable": true + }, + "email": { + "type": "string" + }, + "name": { + "type": "string" + }, + "request_id": { + "type": "string" + } + }, + "required": [ + "name", + "email", + "request_id" + ] + }, "Enum": { "type": "string", "enum": [ @@ -1684,6 +1899,30 @@ "age" ] }, + "MemoResponse": { + "type": "object", + "properties": { + "content": { + "type": "string" + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "id": { + "type": "integer" + }, + "title": { + "type": "string" + } + }, + "required": [ + "id", + "title", + "content", + "created_at" + ] + }, "PaginatedResponse": { "type": "object", "properties": { @@ -1916,7 +2155,64 @@ "age" ] }, + "UpdateMemoRequest": { + "type": "object", + "properties": { + "content": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "title": { + "type": "string" + } + }, + "required": [ + "title", + "content", + "id" + ] + }, "User": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "internal_score": { + "type": "integer", + "nullable": true + }, + "name": { + "type": "string" + } + }, + "required": [ + "id", + "name", + "email" + ] + }, + "UserDTO": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + } + }, + "required": [ + "id", + "name" + ] + }, + "UserPublicResponse": { "type": "object", "properties": { "email": { @@ -1934,6 +2230,21 @@ "name", "email" ] + }, + "UserSummary": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + } + }, + "required": [ + "id", + "name" + ] } } },