From 7e2cb12a356841ff02af51dd276fe79ba29256cd Mon Sep 17 00:00:00 2001 From: Tunay Engin Date: Thu, 29 Jan 2026 16:05:33 +0300 Subject: [PATCH 1/5] Remove benchmark servers and related code Deleted all benchmark server crates, micro-benchmarks, and related files from the benches directory. Updated Cargo.toml to comment out benchmark members. This reduces workspace complexity and removes external benchmark dependencies. --- Cargo.lock | 520 +----------------- Cargo.toml | 10 +- benches/README.md | 67 --- benches/actix_bench_server/Cargo.toml | 15 - benches/actix_bench_server/src/main.rs | 154 ------ benches/bench_server/Cargo.toml | 21 - benches/bench_server/src/main.rs | 149 ----- benches/json_bench.rs | 215 -------- benches/routing_bench.rs | 137 ----- benches/run_benchmarks.ps1 | 119 ---- benches/rustapi_bench/Cargo.toml | 25 - .../rustapi_bench/benches/extractor_bench.rs | 246 --------- .../rustapi_bench/benches/middleware_bench.rs | 149 ----- .../rustapi_bench/benches/websocket_bench.rs | 238 -------- benches/rustapi_bench/src/lib.rs | 1 - benches/test_body.json | 1 - benches/toon_bench/Cargo.toml | 17 - benches/toon_bench/benches/toon_bench.rs | 155 ------ benches/toon_bench/src/lib.rs | 1 - build_log_2.txt | Bin 0 -> 1344 bytes build_log_3.txt | Bin 0 -> 1344 bytes check_output.txt | Bin 0 -> 25326 bytes crates/rustapi-core/src/app.rs | 141 ++--- crates/rustapi-core/src/extract.rs | 93 ++-- crates/rustapi-core/src/response.rs | 35 +- crates/rustapi-core/src/sse.rs | 11 +- crates/rustapi-macros/src/lib.rs | 37 ++ crates/rustapi-macros/src/params.rs | 112 ++++ crates/rustapi-macros/src/schema.rs | 195 +++++++ crates/rustapi-openapi/Cargo.toml | 4 +- crates/rustapi-openapi/src/lib.rs | 20 +- crates/rustapi-openapi/src/schema.rs | 224 ++++++++ crates/rustapi-openapi/src/schemas.rs | 185 ++++++- crates/rustapi-openapi/src/spec.rs | 71 ++- crates/rustapi-openapi/src/v31/spec.rs | 11 +- crates/rustapi-rs/examples/openapi_demo.rs | 77 +++ crates/rustapi-toon/src/extractor.rs | 22 +- crates/rustapi-toon/src/llm_response.rs | 20 +- crates/rustapi-toon/src/negotiate.rs | 20 +- crates/rustapi-view/src/view.rs | 5 +- echo.txt | Bin 0 -> 488904 bytes 41 files changed, 1075 insertions(+), 2448 deletions(-) delete mode 100644 benches/README.md delete mode 100644 benches/actix_bench_server/Cargo.toml delete mode 100644 benches/actix_bench_server/src/main.rs delete mode 100644 benches/bench_server/Cargo.toml delete mode 100644 benches/bench_server/src/main.rs delete mode 100644 benches/json_bench.rs delete mode 100644 benches/routing_bench.rs delete mode 100644 benches/run_benchmarks.ps1 delete mode 100644 benches/rustapi_bench/Cargo.toml delete mode 100644 benches/rustapi_bench/benches/extractor_bench.rs delete mode 100644 benches/rustapi_bench/benches/middleware_bench.rs delete mode 100644 benches/rustapi_bench/benches/websocket_bench.rs delete mode 100644 benches/rustapi_bench/src/lib.rs delete mode 100644 benches/test_body.json delete mode 100644 benches/toon_bench/Cargo.toml delete mode 100644 benches/toon_bench/benches/toon_bench.rs delete mode 100644 benches/toon_bench/src/lib.rs create mode 100644 build_log_2.txt create mode 100644 build_log_3.txt create mode 100644 check_output.txt create mode 100644 crates/rustapi-macros/src/params.rs create mode 100644 crates/rustapi-macros/src/schema.rs create mode 100644 crates/rustapi-openapi/src/schema.rs create mode 100644 crates/rustapi-rs/examples/openapi_demo.rs create mode 100644 echo.txt diff --git a/Cargo.lock b/Cargo.lock index 59c3c4b..0b640bb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,199 +2,6 @@ # It is not intended for manual editing. version = 3 -[[package]] -name = "actix-bench-server" -version = "0.1.0" -dependencies = [ - "actix-web", - "serde", - "serde_json", - "tokio", -] - -[[package]] -name = "actix-codec" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a" -dependencies = [ - "bitflags 2.10.0", - "bytes", - "futures-core", - "futures-sink", - "memchr", - "pin-project-lite", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "actix-http" -version = "3.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7926860314cbe2fb5d1f13731e387ab43bd32bca224e82e6e2db85de0a3dba49" -dependencies = [ - "actix-codec", - "actix-rt", - "actix-service", - "actix-utils", - "base64 0.22.1", - "bitflags 2.10.0", - "brotli 8.0.2", - "bytes", - "bytestring", - "derive_more", - "encoding_rs", - "flate2", - "foldhash 0.1.5", - "futures-core", - "h2 0.3.27", - "http 0.2.12", - "httparse", - "httpdate", - "itoa", - "language-tags", - "local-channel", - "mime", - "percent-encoding", - "pin-project-lite", - "rand 0.9.2", - "sha1", - "smallvec", - "tokio", - "tokio-util", - "tracing", - "zstd", -] - -[[package]] -name = "actix-macros" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" -dependencies = [ - "quote", - "syn 2.0.111", -] - -[[package]] -name = "actix-router" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13d324164c51f63867b57e73ba5936ea151b8a41a1d23d1031eeb9f70d0236f8" -dependencies = [ - "bytestring", - "cfg-if", - "http 0.2.12", - "regex", - "regex-lite", - "serde", - "tracing", -] - -[[package]] -name = "actix-rt" -version = "2.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92589714878ca59a7626ea19734f0e07a6a875197eec751bb5d3f99e64998c63" -dependencies = [ - "futures-core", - "tokio", -] - -[[package]] -name = "actix-server" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a65064ea4a457eaf07f2fba30b4c695bf43b721790e9530d26cb6f9019ff7502" -dependencies = [ - "actix-rt", - "actix-service", - "actix-utils", - "futures-core", - "futures-util", - "mio", - "socket2 0.5.10", - "tokio", - "tracing", -] - -[[package]] -name = "actix-service" -version = "2.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e46f36bf0e5af44bdc4bdb36fbbd421aa98c79a9bce724e1edeb3894e10dc7f" -dependencies = [ - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "actix-utils" -version = "3.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88a1dcdff1466e3c2488e1cb5c36a71822750ad43839937f85d2f4d9f8b705d8" -dependencies = [ - "local-waker", - "pin-project-lite", -] - -[[package]] -name = "actix-web" -version = "4.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1654a77ba142e37f049637a3e5685f864514af11fcbc51cb51eb6596afe5b8d6" -dependencies = [ - "actix-codec", - "actix-http", - "actix-macros", - "actix-router", - "actix-rt", - "actix-server", - "actix-service", - "actix-utils", - "actix-web-codegen", - "bytes", - "bytestring", - "cfg-if", - "cookie 0.16.2", - "derive_more", - "encoding_rs", - "foldhash 0.1.5", - "futures-core", - "futures-util", - "impl-more", - "itoa", - "language-tags", - "log", - "mime", - "once_cell", - "pin-project-lite", - "regex", - "regex-lite", - "serde", - "serde_json", - "serde_urlencoded", - "smallvec", - "socket2 0.6.1", - "time", - "tracing", - "url", -] - -[[package]] -name = "actix-web-codegen" -version = "4.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f591380e2e68490b5dfaf1dd1aa0ebe78d84ba7067078512b4ea6e4492d622b8" -dependencies = [ - "actix-router", - "proc-macro2", - "quote", - "syn 2.0.111", -] - [[package]] name = "adler2" version = "2.0.1" @@ -252,12 +59,6 @@ dependencies = [ "libc", ] -[[package]] -name = "anes" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" - [[package]] name = "anstream" version = "0.6.21" @@ -474,18 +275,6 @@ version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e050f626429857a27ddccb31e0aca21356bfa709c04041aefddac081a8f068a" -[[package]] -name = "bench-server" -version = "0.1.207" -dependencies = [ - "rustapi-rs", - "serde", - "serde_json", - "tokio", - "utoipa", - "validator", -] - [[package]] name = "bit-set" version = "0.8.0" @@ -596,15 +385,6 @@ version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" -[[package]] -name = "bytestring" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "113b4343b5f6617e7ad401ced8de3cc8b012e73a594347c307b90db3e9271289" -dependencies = [ - "bytes", -] - [[package]] name = "cargo-rustapi" version = "0.1.207" @@ -629,12 +409,6 @@ dependencies = [ "walkdir", ] -[[package]] -name = "cast" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" - [[package]] name = "cc" version = "1.2.51" @@ -701,33 +475,6 @@ dependencies = [ "phf_codegen", ] -[[package]] -name = "ciborium" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" -dependencies = [ - "ciborium-io", - "ciborium-ll", - "serde", -] - -[[package]] -name = "ciborium-io" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" - -[[package]] -name = "ciborium-ll" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" -dependencies = [ - "ciborium-io", - "half", -] - [[package]] name = "clap" version = "4.5.53" @@ -836,26 +583,6 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" -[[package]] -name = "convert_case" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" -dependencies = [ - "unicode-segmentation", -] - -[[package]] -name = "cookie" -version = "0.16.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" -dependencies = [ - "percent-encoding", - "time", - "version_check", -] - [[package]] name = "cookie" version = "0.18.1" @@ -925,42 +652,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "criterion" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" -dependencies = [ - "anes", - "cast", - "ciborium", - "clap", - "criterion-plot", - "is-terminal", - "itertools 0.10.5", - "num-traits", - "once_cell", - "oorandom", - "plotters", - "rayon", - "regex", - "serde", - "serde_derive", - "serde_json", - "tinytemplate", - "walkdir", -] - -[[package]] -name = "criterion-plot" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" -dependencies = [ - "cast", - "itertools 0.10.5", -] - [[package]] name = "crossbeam-channel" version = "0.5.15" @@ -1004,12 +695,6 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" -[[package]] -name = "crunchy" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" - [[package]] name = "crypto-common" version = "0.1.7" @@ -1130,29 +815,6 @@ dependencies = [ "powerfmt", ] -[[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 = [ - "convert_case", - "proc-macro2", - "quote", - "rustc_version", - "syn 2.0.111", - "unicode-xid", -] - [[package]] name = "deunicode" version = "1.6.2" @@ -1687,17 +1349,6 @@ dependencies = [ "tokio-util", ] -[[package]] -name = "half" -version = "2.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" -dependencies = [ - "cfg-if", - "crunchy", - "zerocopy", -] - [[package]] name = "halfbrown" version = "0.2.5" @@ -1769,12 +1420,6 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" -[[package]] -name = "hermit-abi" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" - [[package]] name = "hex" version = "0.4.3" @@ -2167,12 +1812,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "impl-more" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8a5a9a0ff0086c7a148acb942baaabeadf9504d10400b5a05645853729b9cd2" - [[package]] name = "indexmap" version = "1.9.3" @@ -2233,32 +1872,12 @@ dependencies = [ "serde", ] -[[package]] -name = "is-terminal" -version = "0.4.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" -dependencies = [ - "hermit-abi", - "libc", - "windows-sys 0.61.2", -] - [[package]] name = "is_terminal_polyfill" version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" -[[package]] -name = "itertools" -version = "0.10.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" -dependencies = [ - "either", -] - [[package]] name = "itertools" version = "0.11.0" @@ -2331,12 +1950,6 @@ dependencies = [ "simple_asn1", ] -[[package]] -name = "language-tags" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" - [[package]] name = "lazy_static" version = "1.5.0" @@ -2412,23 +2025,6 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" -[[package]] -name = "local-channel" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6cbc85e69b8df4b8bb8b89ec634e7189099cea8927a276b7384ce5488e53ec8" -dependencies = [ - "futures-core", - "futures-sink", - "local-waker", -] - -[[package]] -name = "local-waker" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d873d7c67ce09b42110d801813efbc9364414e356be9935700d368351657487" - [[package]] name = "lock_api" version = "0.4.14" @@ -2514,7 +2110,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" dependencies = [ "libc", - "log", "wasi", "windows-sys 0.61.2", ] @@ -2645,12 +2240,6 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" -[[package]] -name = "oorandom" -version = "11.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" - [[package]] name = "openssl" version = "0.10.75" @@ -3015,34 +2604,6 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" -[[package]] -name = "plotters" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" -dependencies = [ - "num-traits", - "plotters-backend", - "plotters-svg", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "plotters-backend" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" - -[[package]] -name = "plotters-svg" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" -dependencies = [ - "plotters-backend", -] - [[package]] name = "portable-atomic" version = "1.13.0" @@ -3195,7 +2756,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81bddcdb20abf9501610992b6759a4c888aef7d1a7247ef75e2404275ac24af1" dependencies = [ "anyhow", - "itertools 0.11.0", + "itertools", "proc-macro2", "quote", "syn 2.0.111", @@ -3365,26 +2926,6 @@ dependencies = [ "rand_core 0.9.3", ] -[[package]] -name = "rayon" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" -dependencies = [ - "either", - "rayon-core", -] - -[[package]] -name = "rayon-core" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" -dependencies = [ - "crossbeam-deque", - "crossbeam-utils", -] - [[package]] name = "rcgen" version = "0.13.2" @@ -3480,12 +3021,6 @@ dependencies = [ "regex-syntax", ] -[[package]] -name = "regex-lite" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d942b98df5e658f56f20d592c7f868833fe38115e65c33003d8cd224b0155da" - [[package]] name = "regex-syntax" version = "0.8.8" @@ -3613,7 +3148,7 @@ dependencies = [ "arc-swap", "base62", "globwalk 0.8.1", - "itertools 0.11.0", + "itertools", "lazy_static", "normpath", "once_cell", @@ -3627,16 +3162,6 @@ dependencies = [ "triomphe", ] -[[package]] -name = "rustapi-bench" -version = "0.1.207" -dependencies = [ - "criterion", - "serde", - "serde_json", - "serde_urlencoded", -] - [[package]] name = "rustapi-core" version = "0.1.207" @@ -3646,7 +3171,7 @@ dependencies = [ "brotli 6.0.0", "bytes", "chrono", - "cookie 0.18.1", + "cookie", "flate2", "futures-util", "h3", @@ -3694,7 +3219,7 @@ version = "0.1.207" dependencies = [ "base64 0.22.1", "bytes", - "cookie 0.18.1", + "cookie", "dashmap", "diesel", "dotenvy", @@ -3763,7 +3288,7 @@ dependencies = [ "http-body-util", "serde", "serde_json", - "utoipa", + "uuid", ] [[package]] @@ -3894,15 +3419,6 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" -[[package]] -name = "rustc_version" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = [ - "semver", -] - [[package]] name = "rustix" version = "1.1.3" @@ -4844,16 +4360,6 @@ dependencies = [ "zerovec", ] -[[package]] -name = "tinytemplate" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" -dependencies = [ - "serde", - "serde_json", -] - [[package]] name = "tinyvec" version = "1.10.0" @@ -5031,16 +4537,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "toon-bench" -version = "0.1.207" -dependencies = [ - "criterion", - "serde", - "serde_json", - "toon-format", -] - [[package]] name = "toon-format" version = "0.4.1" @@ -5326,12 +4822,6 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - [[package]] name = "unsafe-libyaml" version = "0.2.11" diff --git a/Cargo.toml b/Cargo.toml index 9850e72..e5aff31 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,11 +14,11 @@ members = [ "crates/rustapi-jobs", "crates/cargo-rustapi", - # Benchmark servers - "benches/bench_server", - "benches/actix_bench_server", - "benches/toon_bench", - "benches/rustapi_bench", + # Benchmark servers - https://github.com/Tuntii/RustAPI-benchmark + # "benches/bench_server", + # "benches/actix_bench_server", + # "benches/toon_bench", + # "benches/rustapi_bench", ] [workspace.package] diff --git a/benches/README.md b/benches/README.md deleted file mode 100644 index 6549a37..0000000 --- a/benches/README.md +++ /dev/null @@ -1,67 +0,0 @@ -# RustAPI Benchmarks - -Bu klasör, RustAPI framework'ünün performans testlerini içerir. - -## 🎯 Benchmark Türleri - -### 1. Micro-benchmarks (Criterion.rs) -Framework'ün iç bileşenlerini test eder: -- **Routing**: URL eşleştirme hızı -- **JSON Serialization**: Serialize/deserialize performansı -- **Extractors**: Path, Query, Json extractor'ların hızı - -### 2. HTTP Load Testing -Gerçek HTTP istekleriyle end-to-end performans: -- **Hello World**: Basit text yanıt -- **JSON Response**: JSON serialize edilmiş yanıt -- **Path Parameters**: Dynamic route parametreleri -- **JSON Parsing**: Request body parsing - -## 🚀 Benchmark Çalıştırma - -### Micro-benchmarks -```bash -cargo bench -``` - -### HTTP Load Tests (Automated Script) -```powershell -# Run the automated benchmark script -.\benches\run_benchmarks.ps1 -``` - -## 📈 RustAPI vs Actix-web Comparison - -| Framework | Hello World | JSON Response | Path Params | POST JSON | -|-----------|-------------|---------------|-------------|-----------| -| RustAPI | ~4,000 req/s| ~4,200 req/s | ~4,000 req/s| ~5,400 req/s| -| Actix-web | ~39,000 req/s| ~31,000 req/s | ~36,000 req/s| ~33,000 req/s| - -> Note: Benchmarks depend on system environment. These results were taken on a developer machine with 1000 requests and 5 concurrency. - -## 🔥 Neden RustAPI? - -RustAPI, Actix-web ile karşılaştırıldığında: - -### ✅ Avantajlar -1. **Developer Experience (DX)**: FastAPI benzeri ergonomi -2. **Automatic OpenAPI**: Kod yazdıkça dökümantasyon otomatik oluşur -3. **Built-in Validation**: `#[validate]` macro'ları ile otomatik 422 hatası -4. **Simpler API**: Daha az boilerplate, daha okunabilir kod -5. **Hyper 1.0**: Modern ve stabil HTTP stack - -### 📊 Performans -- RustAPI ham hızda Actix-web'e yakın performans sunar (%90-95) -- Gerçek dünya uygulamalarında bu fark göz ardı edilebilir -- DX kazanımları, küçük performans farkından daha değerli - -### 🎯 Ne Zaman RustAPI Kullanmalı? -- API-first projeler -- OpenAPI/Swagger dökümantasyonu gereken projeler -- Hızlı prototipleme -- JSON-ağırlıklı REST API'lar - -### 🎯 Ne Zaman Actix-web Kullanmalı? -- Maksimum raw performans kritik -- WebSocket ağırlıklı uygulamalar -- Olgun ekosistem gereken büyük projeler diff --git a/benches/actix_bench_server/Cargo.toml b/benches/actix_bench_server/Cargo.toml deleted file mode 100644 index 0e7250f..0000000 --- a/benches/actix_bench_server/Cargo.toml +++ /dev/null @@ -1,15 +0,0 @@ -[package] -name = "actix-bench-server" -version = "0.1.0" -edition = "2021" -publish = false - -[[bin]] -name = "actix-bench-server" -path = "src/main.rs" - -[dependencies] -actix-web = "4" -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" -tokio = { version = "1", features = ["full"] } diff --git a/benches/actix_bench_server/src/main.rs b/benches/actix_bench_server/src/main.rs deleted file mode 100644 index 9f5595f..0000000 --- a/benches/actix_bench_server/src/main.rs +++ /dev/null @@ -1,154 +0,0 @@ -//! Actix-web benchmark server for comparison -//! -//! Run with: cargo run --release -p actix-bench-server -//! Then test with: hey -n 100000 -c 50 http://127.0.0.1:8081/ - -use actix_web::{get, post, web, App, HttpResponse, HttpServer, Responder}; -use serde::{Deserialize, Serialize}; - -// ============================================ -// Response types (same as RustAPI) -// ============================================ - -#[derive(Serialize)] -struct HelloResponse { - message: String, -} - -#[derive(Serialize)] -struct UserResponse { - id: i64, - name: String, - email: String, - created_at: String, - is_active: bool, -} - -#[derive(Serialize)] -struct UsersListResponse { - users: Vec, - total: usize, - page: usize, -} - -#[derive(Serialize)] -struct PostResponse { - user_id: i64, - post_id: i64, - title: String, - content: String, -} - -#[derive(Deserialize)] -struct CreateUser { - name: String, - email: String, -} - -// ============================================ -// Handlers -// ============================================ - -#[get("/")] -async fn hello() -> impl Responder { - "Hello, World!" -} - -#[get("/json")] -async fn json_hello() -> impl Responder { - HttpResponse::Ok().json(HelloResponse { - message: "Hello, World!".to_string(), - }) -} - -#[get("/users/{id}")] -async fn get_user(path: web::Path) -> impl Responder { - let id = path.into_inner(); - HttpResponse::Ok().json(UserResponse { - id, - name: format!("User {}", id), - email: format!("user{}@example.com", id), - created_at: "2024-01-01T00:00:00Z".to_string(), - is_active: true, - }) -} - -#[get("/users/{user_id}/posts/{post_id}")] -async fn get_user_post(path: web::Path<(i64, i64)>) -> impl Responder { - let (user_id, post_id) = path.into_inner(); - HttpResponse::Ok().json(PostResponse { - user_id, - post_id, - title: "Benchmark Post".to_string(), - content: "This is a test post for benchmarking".to_string(), - }) -} - -#[post("/users")] -async fn create_user(body: web::Json) -> impl Responder { - HttpResponse::Ok().json(UserResponse { - id: 1, - name: body.name.clone(), - email: body.email.clone(), - created_at: "2024-01-01T00:00:00Z".to_string(), - is_active: true, - }) -} - -#[get("/users")] -async fn list_users() -> impl Responder { - let users: Vec = (1..=10) - .map(|id| UserResponse { - id, - name: format!("User {}", id), - email: format!("user{}@example.com", id), - created_at: "2024-01-01T00:00:00Z".to_string(), - is_active: id % 2 == 0, - }) - .collect(); - - HttpResponse::Ok().json(UsersListResponse { - total: 100, - page: 1, - users, - }) -} - -// ============================================ -// Main -// ============================================ - -#[actix_web::main] -async fn main() -> std::io::Result<()> { - println!("🚀 Actix-web Benchmark Server (for comparison)"); - println!("═══════════════════════════════════════════════════════════"); - println!(); - println!("📊 Benchmark Endpoints:"); - println!(" GET / - Plain text (baseline)"); - println!(" GET /json - Simple JSON"); - println!(" GET /users/:id - JSON + path param"); - println!(" GET /users/:uid/posts/:pid - JSON + 2 path params"); - println!(" POST /users - JSON parsing"); - println!(" GET /users - Large JSON (10 users)"); - println!(); - println!("🔧 Load Test Commands:"); - println!(" hey -n 100000 -c 50 http://127.0.0.1:8081/"); - println!(" hey -n 100000 -c 50 http://127.0.0.1:8081/json"); - println!(); - println!("═══════════════════════════════════════════════════════════"); - println!("🌐 Server running at: http://127.0.0.1:8081"); - println!(); - - HttpServer::new(|| { - App::new() - .service(hello) - .service(json_hello) - .service(get_user) - .service(get_user_post) - .service(create_user) - .service(list_users) - }) - .bind("127.0.0.1:8081")? - .run() - .await -} diff --git a/benches/bench_server/Cargo.toml b/benches/bench_server/Cargo.toml deleted file mode 100644 index b8b4e15..0000000 --- a/benches/bench_server/Cargo.toml +++ /dev/null @@ -1,21 +0,0 @@ -[package] -name = "bench-server" -version.workspace = true -edition.workspace = true -publish = false - -[[bin]] -name = "bench-server" -path = "src/main.rs" - -[dependencies] -# RustAPI with minimum features for benchmarking: -# - No swagger-ui overhead -# - No tracing overhead -# - simd-json for faster JSON parsing -rustapi-rs = { workspace = true, default-features = false, features = ["simd-json"] } -tokio.workspace = true -serde.workspace = true -serde_json.workspace = true -validator.workspace = true -utoipa.workspace = true diff --git a/benches/bench_server/src/main.rs b/benches/bench_server/src/main.rs deleted file mode 100644 index fd6685b..0000000 --- a/benches/bench_server/src/main.rs +++ /dev/null @@ -1,149 +0,0 @@ -//! RustAPI Benchmark Server -//! -//! A minimal server for HTTP load testing (hey, wrk, etc.) -//! Optimized for maximum performance benchmarks. -//! -//! Run with: cargo run --release -p bench-server -//! Then test with: hey -n 100000 -c 50 http://127.0.0.1:8080/ - -use rustapi_rs::prelude::*; - -#[derive(Serialize, Schema)] -struct HelloResponse { - message: &'static str, -} - -#[derive(Serialize, Schema)] -struct UserResponse { - id: i64, - name: String, - email: String, - created_at: &'static str, - is_active: bool, -} - -#[derive(Serialize, Schema)] -struct UsersListResponse { - users: Vec, - total: usize, - page: usize, -} - -#[derive(Serialize, Schema)] -struct PostResponse { - post_id: i64, - title: &'static str, - content: &'static str, -} - -#[derive(Deserialize, Validate, Schema)] -struct CreateUser { - #[validate(length(min = 1, max = 100))] - name: String, - #[validate(email)] - email: String, -} - -// ============================================ -// Handlers - Optimized for benchmarks -// ============================================ - -/// Plain text response - baseline (zero allocation) -#[rustapi_rs::get("/")] -#[rustapi_rs::tag("Benchmark")] -#[rustapi_rs::summary("Plain text hello")] -async fn hello() -> &'static str { - "Hello, World!" -} - -/// Simple JSON response - pre-serialized bytes -#[rustapi_rs::get("/json")] -#[rustapi_rs::tag("Benchmark")] -#[rustapi_rs::summary("JSON hello")] -async fn json_hello() -> Json { - Json(HelloResponse { - message: "Hello, World!", - }) -} - -/// JSON response with path parameter -#[rustapi_rs::get("/users/{id}")] -#[rustapi_rs::tag("Benchmark")] -#[rustapi_rs::summary("Get user by ID")] -async fn get_user(Path(id): Path) -> Json { - Json(UserResponse { - id, - name: format!("User {}", id), - email: format!("user{}@example.com", id), - created_at: "2024-01-01T00:00:00Z", - is_active: true, - }) -} - -/// JSON response with path parameter -#[rustapi_rs::get("/posts/{id}")] -#[rustapi_rs::tag("Benchmark")] -#[rustapi_rs::summary("Get post by ID")] -async fn get_post(Path(id): Path) -> Json { - Json(PostResponse { - post_id: id, - title: "Benchmark Post", - content: "This is a test post for benchmarking", - }) -} - -/// JSON request body parsing with validation -#[rustapi_rs::post("/create-user")] -#[rustapi_rs::tag("Benchmark")] -#[rustapi_rs::summary("Create user with validation")] -async fn create_user(ValidatedJson(body): ValidatedJson) -> Json { - Json(UserResponse { - id: 1, - name: body.name, - email: body.email, - created_at: "2024-01-01T00:00:00Z", - is_active: true, - }) -} - -/// Larger JSON response (10 users) -#[rustapi_rs::get("/users-list")] -#[rustapi_rs::tag("Benchmark")] -#[rustapi_rs::summary("List users (10 items)")] -async fn list_users() -> Json { - let users: Vec = (1..=10) - .map(|id| UserResponse { - id, - name: format!("User {}", id), - email: format!("user{}@example.com", id), - created_at: "2024-01-01T00:00:00Z", - is_active: id % 2 == 0, - }) - .collect(); - - Json(UsersListResponse { - total: 100, - page: 1, - users, - }) -} - -// ============================================ -// Main - Optimized minimal server -// ============================================ - -#[tokio::main] -async fn main() -> Result<(), Box> { - // Minimal output for benchmarks - eprintln!("🚀 RustAPI Benchmark Server @ http://127.0.0.1:8080"); - - RustApi::new() - .mount_route(hello_route()) - .mount_route(json_hello_route()) - .mount_route(get_user_route()) - .mount_route(get_post_route()) - .mount_route(create_user_route()) - .mount_route(list_users_route()) - .run("127.0.0.1:8080") - .await -} diff --git a/benches/json_bench.rs b/benches/json_bench.rs deleted file mode 100644 index ae40d73..0000000 --- a/benches/json_bench.rs +++ /dev/null @@ -1,215 +0,0 @@ -//! JSON serialization/deserialization benchmarks -//! -//! Benchmarks serde_json performance which is critical for API frameworks. - -use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion}; -use serde::{Deserialize, Serialize}; - -/// Simple response structure -#[derive(Serialize, Deserialize)] -struct SimpleResponse { - message: String, - status: u16, -} - -/// User response with more fields -#[derive(Serialize, Deserialize)] -struct UserResponse { - id: i64, - name: String, - email: String, - created_at: String, - is_active: bool, -} - -/// Complex response with nested data -#[derive(Serialize, Deserialize)] -struct ComplexResponse { - users: Vec, - total: usize, - page: usize, - per_page: usize, - has_more: bool, -} - -/// Create test data -fn create_simple() -> SimpleResponse { - SimpleResponse { - message: "Hello, World!".to_string(), - status: 200, - } -} - -fn create_user(id: i64) -> UserResponse { - UserResponse { - id, - name: format!("User {}", id), - email: format!("user{}@example.com", id), - created_at: "2024-01-01T00:00:00Z".to_string(), - is_active: true, - } -} - -fn create_complex(count: usize) -> ComplexResponse { - ComplexResponse { - users: (0..count as i64).map(create_user).collect(), - total: count * 10, - page: 1, - per_page: count, - has_more: true, - } -} - -/// Benchmark JSON serialization -fn bench_serialize(c: &mut Criterion) { - let mut group = c.benchmark_group("json_serialize"); - - let simple = create_simple(); - let user = create_user(1); - let complex_10 = create_complex(10); - let complex_100 = create_complex(100); - - group.bench_function("simple", |b| { - b.iter(|| serde_json::to_string(black_box(&simple))) - }); - - group.bench_function("user", |b| { - b.iter(|| serde_json::to_string(black_box(&user))) - }); - - group.bench_function("complex_10_users", |b| { - b.iter(|| serde_json::to_string(black_box(&complex_10))) - }); - - group.bench_function("complex_100_users", |b| { - b.iter(|| serde_json::to_string(black_box(&complex_100))) - }); - - group.finish(); -} - -/// Benchmark JSON serialization to bytes (more realistic for HTTP) -fn bench_serialize_to_vec(c: &mut Criterion) { - let mut group = c.benchmark_group("json_serialize_vec"); - - let simple = create_simple(); - let user = create_user(1); - let complex_10 = create_complex(10); - - group.bench_function("simple", |b| { - b.iter(|| serde_json::to_vec(black_box(&simple))) - }); - - group.bench_function("user", |b| b.iter(|| serde_json::to_vec(black_box(&user)))); - - group.bench_function("complex_10_users", |b| { - b.iter(|| serde_json::to_vec(black_box(&complex_10))) - }); - - group.finish(); -} - -/// Benchmark JSON deserialization -fn bench_deserialize(c: &mut Criterion) { - let mut group = c.benchmark_group("json_deserialize"); - - let simple_json = serde_json::to_string(&create_simple()).unwrap(); - let user_json = serde_json::to_string(&create_user(1)).unwrap(); - let complex_10_json = serde_json::to_string(&create_complex(10)).unwrap(); - let complex_100_json = serde_json::to_string(&create_complex(100)).unwrap(); - - group.bench_function("simple", |b| { - b.iter(|| serde_json::from_str::(black_box(&simple_json))) - }); - - group.bench_function("user", |b| { - b.iter(|| serde_json::from_str::(black_box(&user_json))) - }); - - group.bench_function("complex_10_users", |b| { - b.iter(|| serde_json::from_str::(black_box(&complex_10_json))) - }); - - group.bench_function("complex_100_users", |b| { - b.iter(|| serde_json::from_str::(black_box(&complex_100_json))) - }); - - group.finish(); -} - -/// Benchmark request body parsing (typical API scenario) -fn bench_request_parsing(c: &mut Criterion) { - let mut group = c.benchmark_group("request_body_parsing"); - - // Simulate incoming request bodies - let create_user_body = r#"{"name": "John Doe", "email": "john@example.com"}"#; - let create_post_body = r#"{"title": "Hello World", "content": "This is a blog post with some content that is reasonably long to simulate real world usage.", "author_id": 123}"#; - let bulk_import_body = serde_json::to_string( - &(0..50) - .map(|i| { - serde_json::json!({ - "name": format!("User {}", i), - "email": format!("user{}@example.com", i) - }) - }) - .collect::>(), - ) - .unwrap(); - - #[derive(Deserialize)] - #[allow(dead_code)] - struct CreateUser { - name: String, - email: String, - } - - #[derive(Deserialize)] - #[allow(dead_code)] - struct CreatePost { - title: String, - content: String, - author_id: i64, - } - - group.bench_function("create_user", |b| { - b.iter(|| serde_json::from_str::(black_box(create_user_body))) - }); - - group.bench_function("create_post", |b| { - b.iter(|| serde_json::from_str::(black_box(create_post_body))) - }); - - group.bench_function("bulk_import_50", |b| { - b.iter(|| serde_json::from_str::>(black_box(&bulk_import_body))) - }); - - group.finish(); -} - -/// Benchmark scaling with response size -fn bench_response_scaling(c: &mut Criterion) { - let mut group = c.benchmark_group("response_scaling"); - - for user_count in [1, 10, 50, 100, 500].iter() { - let response = create_complex(*user_count); - - group.bench_with_input( - BenchmarkId::new("serialize", user_count), - user_count, - |b, _| b.iter(|| serde_json::to_vec(black_box(&response))), - ); - } - - group.finish(); -} - -criterion_group!( - benches, - bench_serialize, - bench_serialize_to_vec, - bench_deserialize, - bench_request_parsing, - bench_response_scaling, -); - -criterion_main!(benches); diff --git a/benches/routing_bench.rs b/benches/routing_bench.rs deleted file mode 100644 index 0646ff7..0000000 --- a/benches/routing_bench.rs +++ /dev/null @@ -1,137 +0,0 @@ -//! Routing micro-benchmarks using Criterion -//! -//! Benchmarks the core routing performance of RustAPI's matchit-based router. - -use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion}; -use matchit::Router; - -/// Benchmark static route matching -fn bench_static_routes(c: &mut Criterion) { - let mut router = Router::new(); - - // Add static routes - router.insert("/", "root").unwrap(); - router.insert("/health", "health").unwrap(); - router.insert("/api/v1/users", "users").unwrap(); - router.insert("/api/v1/posts", "posts").unwrap(); - router.insert("/api/v1/comments", "comments").unwrap(); - router.insert("/api/v2/users", "users_v2").unwrap(); - router.insert("/api/v2/posts", "posts_v2").unwrap(); - - let mut group = c.benchmark_group("static_routing"); - - group.bench_function("match_root", |b| b.iter(|| router.at(black_box("/")))); - - group.bench_function("match_health", |b| { - b.iter(|| router.at(black_box("/health"))) - }); - - group.bench_function("match_nested_v1", |b| { - b.iter(|| router.at(black_box("/api/v1/users"))) - }); - - group.bench_function("match_nested_v2", |b| { - b.iter(|| router.at(black_box("/api/v2/posts"))) - }); - - group.finish(); -} - -/// Benchmark dynamic route matching with path parameters -fn bench_dynamic_routes(c: &mut Criterion) { - let mut router = Router::new(); - - router.insert("/users/{id}", "get_user").unwrap(); - router - .insert("/users/{id}/posts", "get_user_posts") - .unwrap(); - router - .insert("/users/{user_id}/posts/{post_id}", "get_user_post") - .unwrap(); - router - .insert( - "/users/{user_id}/posts/{post_id}/comments/{comment_id}", - "get_comment", - ) - .unwrap(); - router - .insert( - "/categories/{cat}/products/{prod}/reviews/{rev}", - "get_review", - ) - .unwrap(); - - let mut group = c.benchmark_group("dynamic_routing"); - - group.bench_function("single_param", |b| { - b.iter(|| router.at(black_box("/users/123"))) - }); - - group.bench_function("single_param_nested", |b| { - b.iter(|| router.at(black_box("/users/123/posts"))) - }); - - group.bench_function("two_params", |b| { - b.iter(|| router.at(black_box("/users/123/posts/456"))) - }); - - group.bench_function("three_params", |b| { - b.iter(|| router.at(black_box("/users/123/posts/456/comments/789"))) - }); - - group.finish(); -} - -/// Benchmark router scaling with many routes -fn bench_router_scaling(c: &mut Criterion) { - let mut group = c.benchmark_group("router_scaling"); - - for route_count in [10, 50, 100, 500].iter() { - let mut router = Router::new(); - - for i in 0..*route_count { - router.insert(&format!("/api/v1/resource{}", i), i).unwrap(); - } - - // Always match the middle route - let search_path = format!("/api/v1/resource{}", route_count / 2); - - group.bench_with_input( - BenchmarkId::new("lookup", route_count), - route_count, - |b, _| b.iter(|| router.at(black_box(&search_path))), - ); - } - - group.finish(); -} - -/// Benchmark wildcard routes -fn bench_wildcard_routes(c: &mut Criterion) { - let mut router = Router::new(); - - router.insert("/static/{*path}", "static_files").unwrap(); - router.insert("/assets/{*filepath}", "assets").unwrap(); - - let mut group = c.benchmark_group("wildcard_routing"); - - group.bench_function("short_path", |b| { - b.iter(|| router.at(black_box("/static/css/style.css"))) - }); - - group.bench_function("long_path", |b| { - b.iter(|| router.at(black_box("/static/images/icons/social/facebook.png"))) - }); - - group.finish(); -} - -criterion_group!( - benches, - bench_static_routes, - bench_dynamic_routes, - bench_router_scaling, - bench_wildcard_routes, -); - -criterion_main!(benches); diff --git a/benches/run_benchmarks.ps1 b/benches/run_benchmarks.ps1 deleted file mode 100644 index 764dc2e..0000000 --- a/benches/run_benchmarks.ps1 +++ /dev/null @@ -1,119 +0,0 @@ -# RustAPI Benchmarking Suite - -param( - [int]$Requests = 100000, - [int]$Concurrency = 50, - [switch]$SkipActix = $false, - [switch]$Internal = $false, # Run internal cargo bench - [switch]$Quick = $false # Quick smoke test mode -) - -$ErrorActionPreference = "Continue" - -Write-Host "===================================================================" -ForegroundColor Cyan -Write-Host " Running RustAPI Performance Benchmarking" -ForegroundColor Yellow -Write-Host "===================================================================" -ForegroundColor Cyan - -if ($Quick) { - $Requests = 1000 - $Concurrency = 10 - Write-Host "Running in Quick Mode (1000 reqs, 10 conn)" -ForegroundColor Magenta -} - -# --------------------------------------------------------- -# 1. Internal Benchmarks (Criterion) -# --------------------------------------------------------- -if ($Internal -or $Quick) { - Write-Host "`n[1/2] Running Internal Micro-benchmarks (cargo bench)..." -ForegroundColor Yellow - # If quick, we might want to filter or run fewer, but cargo bench is usually fast enough or hard to param - cargo bench --workspace -} else { - Write-Host "`n[1/2] Skipping Internal Benchmarks (use -Internal to run)" -ForegroundColor DarkGray -} - -# --------------------------------------------------------- -# 2. End-to-End API Benchmarks (Hey) -# --------------------------------------------------------- -Write-Host "`n[2/2] Running E2E API Benchmarks (hey)..." -ForegroundColor Yellow - -# Check if hey is installed -if (-not (Get-Command "hey" -ErrorAction SilentlyContinue)) { - $goHey = Join-Path $HOME "go\bin\hey.exe" - if (Test-Path $goHey) { - function Run-Hey { & $goHey @args } - } else { - Write-Host "X 'hey' is not installed! Skipping E2E benchmarks." -ForegroundColor Red - Write-Host "Install with: go install github.com/rakyll/hey@latest" - exit 0 # Don't fail entire script if only hey is missing, unless it's strictly required - } -} else { - function Run-Hey { & hey @args } -} - -# Build servers -Write-Host "Building servers in release mode..." -ForegroundColor Yellow -cargo build --release -p bench-server 2>&1 | Out-Null -if (-not $SkipActix) { - cargo build --release -p actix-bench-server 2>&1 | Out-Null -} -Write-Host "Build complete!" -ForegroundColor Green - -$results = @{} - -function Run-Benchmark { - param ([string]$Name, [string]$Framework, [string]$Url, [string]$Method = "GET", [string]$Body = $null) - - Write-Host " Testing: $Name" -ForegroundColor White - $heyArgs = @("-n", $Requests, "-c", $Concurrency) - if ($Method -eq "POST" -and $Body) { - $heyArgs += @("-m", "POST", "-H", "Content-Type: application/json", "-d", $Body) - } - $heyArgs += $Url - - $output = Run-Hey @heyArgs 2>&1 | Out-String - - $rps = 0; $avgLatency = 0 - if ($output -match "Requests/sec:\s+([\d.]+)") { $rps = $Matches[1] } - if ($output -match "Average:\s+([\d.]+)\s+secs") { $avgLatency = $Matches[1] } - - if ($rps -gt 0) { - $key = "$Framework|$Name" - $results[$key] = @{ Framework = $Framework; Endpoint = $Name; RPS = [double]$rps; AvgLatency = [double]$avgLatency * 1000 } - Write-Host " -> $rps req/s, avg: $([math]::Round([double]$avgLatency * 1000, 2))ms" -ForegroundColor Gray - } -} - -function Test-Framework { - param ([string]$Name, [string]$Port) - Write-Host "`nTesting $Name on port $Port" -ForegroundColor Cyan - - $retries = 10 - while ($retries -gt 0) { - try { $null = Invoke-WebRequest -Uri "http://127.0.0.1:$Port/" -TimeoutSec 1 -ErrorAction Stop -UseBasicParsing; break } - catch { Start-Sleep -Milliseconds 500; $retries-- } - } - if ($retries -eq 0) { Write-Host "X Server not responding on port $Port" -ForegroundColor Red; return } - - Run-Benchmark -Name "Plain Text" -Framework $Name -Url "http://127.0.0.1:$Port/" - Run-Benchmark -Name "JSON Hello" -Framework $Name -Url "http://127.0.0.1:$Port/json" - - if ($Name -eq "RustAPI") { - Run-Benchmark -Name "POST JSON" -Framework $Name -Url "http://127.0.0.1:$Port/create-user" -Method "POST" -Body '{"name":"Test User","email":"test@example.com"}' - } else { - Run-Benchmark -Name "POST JSON" -Framework $Name -Url "http://127.0.0.1:$Port/users" -Method "POST" -Body '{"name":"Test User","email":"test@example.com"}' - } -} - -# Start RustAPI -$rustApiProcess = Start-Process -FilePath ".\target\release\bench-server.exe" -PassThru -WindowStyle Hidden -Start-Sleep -Seconds 2 -try { Test-Framework -Name "RustAPI" -Port "8080" } finally { Stop-Process -Id $rustApiProcess.Id -Force -ErrorAction SilentlyContinue } - -# Start Actix -if (-not $SkipActix) { - $actixProcess = Start-Process -FilePath ".\target\release\actix-bench-server.exe" -PassThru -WindowStyle Hidden - Start-Sleep -Seconds 2 - try { Test-Framework -Name "Actix-web" -Port "8081" } finally { Stop-Process -Id $actixProcess.Id -Force -ErrorAction SilentlyContinue } -} - -Write-Host "`nBenchmarks Complete." -ForegroundColor Green diff --git a/benches/rustapi_bench/Cargo.toml b/benches/rustapi_bench/Cargo.toml deleted file mode 100644 index 7bd0837..0000000 --- a/benches/rustapi_bench/Cargo.toml +++ /dev/null @@ -1,25 +0,0 @@ -[package] -name = "rustapi-bench" -version.workspace = true -edition.workspace = true -publish = false - -[[bench]] -name = "middleware_bench" -harness = false - -[[bench]] -name = "extractor_bench" -harness = false - -[[bench]] -name = "websocket_bench" -harness = false - -[dependencies] -serde.workspace = true -serde_json.workspace = true - -[dev-dependencies] -criterion.workspace = true -serde_urlencoded = "0.7" diff --git a/benches/rustapi_bench/benches/extractor_bench.rs b/benches/rustapi_bench/benches/extractor_bench.rs deleted file mode 100644 index 6876c62..0000000 --- a/benches/rustapi_bench/benches/extractor_bench.rs +++ /dev/null @@ -1,246 +0,0 @@ -//! Extractor overhead benchmarks -//! -//! Benchmarks the performance of different extractor types in RustAPI. - -#![allow(dead_code)] - -use criterion::{black_box, criterion_group, criterion_main, Criterion}; -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; - -/// Simple query params struct -#[derive(Deserialize)] -struct SimpleQuery { - page: Option, - limit: Option, -} - -/// Complex query params struct -#[derive(Deserialize)] -struct ComplexQuery { - page: Option, - limit: Option, - sort: Option, - filter: Option, - include: Option>, -} - -/// User request body -#[derive(Serialize, Deserialize)] -struct UserBody { - name: String, - email: String, - age: u32, -} - -/// Complex request body -#[derive(Serialize, Deserialize)] -struct ComplexBody { - user: UserBody, - tags: Vec, - metadata: HashMap, -} - -/// Benchmark path parameter extraction -fn bench_path_extraction(c: &mut Criterion) { - let mut group = c.benchmark_group("path_extraction"); - - // Single path param - group.bench_function("single_param", |b| { - let path = "/users/12345"; - b.iter(|| { - let id: u64 = black_box(path) - .strip_prefix("/users/") - .unwrap() - .parse() - .unwrap(); - id - }) - }); - - // Multiple path params - group.bench_function("multiple_params", |b| { - let path = "/users/12345/posts/67890"; - b.iter(|| { - let parts: Vec<&str> = black_box(path).split('/').collect(); - let user_id: u64 = parts[2].parse().unwrap(); - let post_id: u64 = parts[4].parse().unwrap(); - (user_id, post_id) - }) - }); - - // UUID path param - group.bench_function("uuid_param", |b| { - let path = "/items/550e8400-e29b-41d4-a716-446655440000"; - b.iter(|| { - let uuid_str = black_box(path).strip_prefix("/items/").unwrap(); - // Just validate format, don't parse to actual UUID - uuid_str.len() == 36 && uuid_str.chars().filter(|c| *c == '-').count() == 4 - }) - }); - - group.finish(); -} - -/// Benchmark query string extraction -fn bench_query_extraction(c: &mut Criterion) { - let mut group = c.benchmark_group("query_extraction"); - - // Simple query - let simple_query = "page=1&limit=10"; - group.bench_function("simple_query", |b| { - b.iter(|| serde_urlencoded::from_str::(black_box(simple_query)).unwrap()) - }); - - // Complex query - let complex_query = - "page=1&limit=10&sort=created_at&filter=active&include=posts&include=comments"; - group.bench_function("complex_query", |b| { - b.iter(|| serde_urlencoded::from_str::(black_box(complex_query)).unwrap()) - }); - - // Empty query - let empty_query = ""; - group.bench_function("empty_query", |b| { - b.iter(|| serde_urlencoded::from_str::(black_box(empty_query)).unwrap()) - }); - - group.finish(); -} - -/// Benchmark JSON body extraction -fn bench_json_extraction(c: &mut Criterion) { - let mut group = c.benchmark_group("json_extraction"); - - // Simple body - let simple_json = r#"{"name":"John Doe","email":"john@example.com","age":30}"#; - group.bench_function("simple_body", |b| { - b.iter(|| serde_json::from_str::(black_box(simple_json)).unwrap()) - }); - - // Complex body - let complex_json = r#"{ - "user": {"name":"John Doe","email":"john@example.com","age":30}, - "tags": ["rust", "api", "web"], - "metadata": {"source": "mobile", "version": "1.0"} - }"#; - group.bench_function("complex_body", |b| { - b.iter(|| serde_json::from_str::(black_box(complex_json)).unwrap()) - }); - - // Large array body - let users: Vec = (0..100) - .map(|i| UserBody { - name: format!("User {}", i), - email: format!("user{}@example.com", i), - age: 20 + (i as u32 % 50), - }) - .collect(); - let large_json = serde_json::to_string(&users).unwrap(); - - group.bench_function("large_array_body", |b| { - b.iter(|| serde_json::from_str::>(black_box(&large_json)).unwrap()) - }); - - group.finish(); -} - -/// Benchmark header extraction -fn bench_header_extraction(c: &mut Criterion) { - let mut group = c.benchmark_group("header_extraction"); - - // Content-Type extraction - group.bench_function("content_type", |b| { - let header = "application/json; charset=utf-8"; - b.iter(|| { - let content_type = black_box(header).split(';').next().unwrap().trim(); - content_type == "application/json" - }) - }); - - // Authorization extraction - group.bench_function("authorization", |b| { - let header = "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ"; - b.iter(|| { - let token = black_box(header).strip_prefix("Bearer ").unwrap(); - token.len() > 0 - }) - }); - - // Accept header parsing - group.bench_function("accept_parsing", |b| { - let header = "application/json, application/xml;q=0.9, text/html;q=0.8, */*;q=0.1"; - b.iter(|| { - let types: Vec<&str> = black_box(header) - .split(',') - .map(|s| s.split(';').next().unwrap().trim()) - .collect(); - types - }) - }); - - group.finish(); -} - -/// Benchmark combined extraction (typical request) -fn bench_combined_extraction(c: &mut Criterion) { - let mut group = c.benchmark_group("combined_extraction"); - - // Typical GET request - group.bench_function("typical_get", |b| { - let path = "/users/12345"; - let query = "page=1&limit=10"; - let auth = "Bearer token123"; - - b.iter(|| { - // Extract path param - let user_id: u64 = black_box(path) - .strip_prefix("/users/") - .unwrap() - .parse() - .unwrap(); - - // Extract query params - let query_params = serde_urlencoded::from_str::(black_box(query)).unwrap(); - - // Extract auth token - let token = black_box(auth).strip_prefix("Bearer ").unwrap(); - - (user_id, query_params.page, token.len()) - }) - }); - - // Typical POST request - group.bench_function("typical_post", |b| { - let _path = "/users"; - let body = r#"{"name":"John Doe","email":"john@example.com","age":30}"#; - let content_type = "application/json"; - let auth = "Bearer token123"; - - b.iter(|| { - // Verify content type - let is_json = black_box(content_type) == "application/json"; - - // Extract auth token - let token = black_box(auth).strip_prefix("Bearer ").unwrap(); - - // Parse body - let user = serde_json::from_str::(black_box(body)).unwrap(); - - (is_json, token.len(), user.name.len()) - }) - }); - - group.finish(); -} - -criterion_group!( - benches, - bench_path_extraction, - bench_query_extraction, - bench_json_extraction, - bench_header_extraction, - bench_combined_extraction, -); - -criterion_main!(benches); diff --git a/benches/rustapi_bench/benches/middleware_bench.rs b/benches/rustapi_bench/benches/middleware_bench.rs deleted file mode 100644 index 68fdaf3..0000000 --- a/benches/rustapi_bench/benches/middleware_bench.rs +++ /dev/null @@ -1,149 +0,0 @@ -//! Middleware composition benchmarks -//! -//! Benchmarks the overhead of middleware layers in RustAPI. - -use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion}; - -/// Simulate middleware overhead with simple counter -fn simulate_middleware_layer(input: u64, layers: usize) -> u64 { - let mut result = input; - for _ in 0..layers { - // Simulate minimal middleware work: check + transform - if result > 0 { - result = result.wrapping_add(1); - } - } - result -} - -/// Simulate request ID generation (UUID-like) -fn simulate_request_id_middleware(request_count: u64) -> String { - format!("req_{:016x}", request_count) -} - -/// Simulate header parsing overhead -fn simulate_header_parsing(headers: &[(&str, &str)]) -> usize { - headers.iter().map(|(k, v)| k.len() + v.len()).sum() -} - -/// Benchmark middleware layer composition -fn bench_middleware_layers(c: &mut Criterion) { - let mut group = c.benchmark_group("middleware_layers"); - - // Test with different numbers of middleware layers - for layer_count in [0, 1, 3, 5, 10, 20].iter() { - group.bench_with_input( - BenchmarkId::new("layer_count", layer_count), - layer_count, - |b, &layers| b.iter(|| simulate_middleware_layer(black_box(42), layers)), - ); - } - - group.finish(); -} - -/// Benchmark request ID generation -fn bench_request_id(c: &mut Criterion) { - let mut group = c.benchmark_group("request_id"); - - group.bench_function("generate", |b| { - let mut counter = 0u64; - b.iter(|| { - counter += 1; - simulate_request_id_middleware(black_box(counter)) - }) - }); - - group.finish(); -} - -/// Benchmark header parsing -fn bench_header_parsing(c: &mut Criterion) { - let mut group = c.benchmark_group("header_parsing"); - - // Minimal headers - let minimal_headers = [("content-type", "application/json")]; - - // Typical API headers - let typical_headers = [ - ("content-type", "application/json"), - ("accept", "application/json"), - ( - "authorization", - "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9", - ), - ("x-request-id", "550e8400-e29b-41d4-a716-446655440000"), - ("user-agent", "RustAPI-Client/1.0"), - ]; - - // Many headers - let many_headers: Vec<(&str, &str)> = (0..20) - .map(|i| { - let key: &'static str = Box::leak(format!("x-custom-header-{}", i).into_boxed_str()); - let value: &'static str = Box::leak(format!("value-{}", i).into_boxed_str()); - (key, value) - }) - .collect(); - - group.bench_function("minimal_headers", |b| { - b.iter(|| simulate_header_parsing(black_box(&minimal_headers))) - }); - - group.bench_function("typical_headers", |b| { - b.iter(|| simulate_header_parsing(black_box(&typical_headers))) - }); - - group.bench_function("many_headers", |b| { - b.iter(|| simulate_header_parsing(black_box(&many_headers))) - }); - - group.finish(); -} - -/// Benchmark async middleware simulation -fn bench_middleware_chain(c: &mut Criterion) { - let mut group = c.benchmark_group("middleware_chain"); - - // Simulate a typical middleware chain: - // 1. Request ID - // 2. Tracing - // 3. Auth check - // 4. Rate limit check - // 5. Body limit check - - group.bench_function("typical_chain", |b| { - b.iter(|| { - // Step 1: Generate request ID - let request_id = simulate_request_id_middleware(black_box(12345)); - - // Step 2: Tracing (record span) - let _ = black_box(request_id.len()); - - // Step 3: Auth check (simple token validation) - let token = "Bearer valid_token"; - let is_valid = black_box(token.starts_with("Bearer ")); - - // Step 4: Rate limit check (counter check) - let rate_count = black_box(99u64); - let under_limit = rate_count < 100; - - // Step 5: Body limit check - let body_size = black_box(1024usize); - let within_limit = body_size < 1_048_576; // 1MB - - (is_valid, under_limit, within_limit) - }) - }); - - group.finish(); -} - -criterion_group!( - benches, - bench_middleware_layers, - bench_request_id, - bench_header_parsing, - bench_middleware_chain, -); - -criterion_main!(benches); diff --git a/benches/rustapi_bench/benches/websocket_bench.rs b/benches/rustapi_bench/benches/websocket_bench.rs deleted file mode 100644 index 68707db..0000000 --- a/benches/rustapi_bench/benches/websocket_bench.rs +++ /dev/null @@ -1,238 +0,0 @@ -//! WebSocket message throughput benchmarks -//! -//! Benchmarks the performance of WebSocket message handling in RustAPI. - -use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion, Throughput}; -use std::collections::HashMap; - -/// Simulate WebSocket message parsing (text) -fn parse_text_message(data: &str) -> String { - data.to_string() -} - -/// Simulate WebSocket message parsing (binary) -fn parse_binary_message(data: &[u8]) -> Vec { - data.to_vec() -} - -/// Simulate JSON message parsing -fn parse_json_message(data: &str) -> serde_json::Value { - serde_json::from_str(data).unwrap_or(serde_json::Value::Null) -} - -/// Simulate message frame encoding -fn encode_frame(opcode: u8, payload: &[u8], mask: bool) -> Vec { - let mut frame = Vec::with_capacity(14 + payload.len()); - - // FIN bit + opcode - frame.push(0x80 | opcode); - - // Payload length - let len = payload.len(); - if len < 126 { - frame.push((if mask { 0x80 } else { 0 }) | len as u8); - } else if len < 65536 { - frame.push((if mask { 0x80 } else { 0 }) | 126); - frame.push((len >> 8) as u8); - frame.push(len as u8); - } else { - frame.push((if mask { 0x80 } else { 0 }) | 127); - for i in (0..8).rev() { - frame.push((len >> (i * 8)) as u8); - } - } - - // Masking key (if masked) - if mask { - let mask_key: [u8; 4] = [0x12, 0x34, 0x56, 0x78]; - frame.extend_from_slice(&mask_key); - - // Masked payload - for (i, byte) in payload.iter().enumerate() { - frame.push(byte ^ mask_key[i % 4]); - } - } else { - frame.extend_from_slice(payload); - } - - frame -} - -/// Benchmark text message parsing -fn bench_text_message(c: &mut Criterion) { - let mut group = c.benchmark_group("websocket_text"); - - let messages = [ - ("tiny", "Hi"), - ("small", "Hello, WebSocket!"), - ("medium", &"x".repeat(1024)), - ("large", &"x".repeat(64 * 1024)), - ]; - - for (name, msg) in messages.iter() { - group.throughput(Throughput::Bytes(msg.len() as u64)); - group.bench_with_input(BenchmarkId::new("parse", name), msg, |b, msg| { - b.iter(|| parse_text_message(black_box(msg))) - }); - } - - group.finish(); -} - -/// Benchmark binary message parsing -fn bench_binary_message(c: &mut Criterion) { - let mut group = c.benchmark_group("websocket_binary"); - - let messages: Vec<(&str, Vec)> = vec![ - ("tiny", vec![1, 2, 3, 4]), - ("small", vec![0u8; 64]), - ("medium", vec![0u8; 4096]), - ("large", vec![0u8; 64 * 1024]), - ]; - - for (name, msg) in messages.iter() { - group.throughput(Throughput::Bytes(msg.len() as u64)); - group.bench_with_input(BenchmarkId::new("parse", name), msg, |b, msg| { - b.iter(|| parse_binary_message(black_box(msg))) - }); - } - - group.finish(); -} - -/// Benchmark JSON message parsing (common WebSocket pattern) -fn bench_json_message(c: &mut Criterion) { - let mut group = c.benchmark_group("websocket_json"); - - // Simple JSON message - let simple_json = r#"{"type":"ping"}"#; - - // Typical chat message - let chat_json = - r#"{"type":"message","user":"alice","content":"Hello everyone!","timestamp":1704067200}"#; - - // Complex nested JSON - let complex_json = r#"{"type":"state","data":{"users":[{"id":1,"name":"Alice"},{"id":2,"name":"Bob"}],"room":"general","active":true}}"#; - - group.bench_function("simple", |b| { - b.iter(|| parse_json_message(black_box(simple_json))) - }); - - group.bench_function("chat", |b| { - b.iter(|| parse_json_message(black_box(chat_json))) - }); - - group.bench_function("complex", |b| { - b.iter(|| parse_json_message(black_box(complex_json))) - }); - - group.finish(); -} - -/// Benchmark frame encoding -fn bench_frame_encoding(c: &mut Criterion) { - let mut group = c.benchmark_group("websocket_frame"); - - let payloads: Vec<(&str, Vec)> = vec![ - ("tiny", vec![1, 2, 3, 4]), - ("small", vec![0u8; 100]), - ("medium_125", vec![0u8; 125]), // Max single-byte length - ("medium_126", vec![0u8; 126]), // Requires 2-byte length - ("large", vec![0u8; 1024]), - ]; - - for (name, payload) in payloads.iter() { - // Server-side (no mask) - group.bench_with_input( - BenchmarkId::new("encode_unmasked", name), - payload, - |b, payload| b.iter(|| encode_frame(0x01, black_box(payload), false)), - ); - - // Client-side (with mask) - group.bench_with_input( - BenchmarkId::new("encode_masked", name), - payload, - |b, payload| b.iter(|| encode_frame(0x01, black_box(payload), true)), - ); - } - - group.finish(); -} - -/// Benchmark broadcast scenario (sending to multiple clients) -fn bench_broadcast(c: &mut Criterion) { - let mut group = c.benchmark_group("websocket_broadcast"); - - let message = "Broadcast message to all connected clients"; - - for client_count in [10, 100, 1000].iter() { - group.bench_with_input( - BenchmarkId::new("prepare_messages", client_count), - client_count, - |b, &count| { - b.iter(|| { - // Simulate preparing messages for N clients - let mut messages = Vec::with_capacity(count); - for _ in 0..count { - messages.push(black_box(message).to_string()); - } - messages - }) - }, - ); - } - - group.finish(); -} - -/// Benchmark connection management (HashMap-based room pattern) -fn bench_connection_management(c: &mut Criterion) { - let mut group = c.benchmark_group("websocket_rooms"); - - // Simulate room-based connection management - group.bench_function("join_room", |b| { - let mut rooms: HashMap> = HashMap::new(); - let mut client_id = 0u64; - - b.iter(|| { - client_id += 1; - let room = black_box("general".to_string()); - rooms.entry(room).or_default().push(client_id); - }) - }); - - group.bench_function("leave_room", |b| { - let mut rooms: HashMap> = HashMap::new(); - rooms.insert("general".to_string(), (0..1000).collect()); - - b.iter(|| { - let room = rooms.get_mut(black_box("general")).unwrap(); - let client_id = black_box(500u64); - if let Some(pos) = room.iter().position(|&id| id == client_id) { - room.swap_remove(pos); - } - }) - }); - - group.bench_function("list_room_members", |b| { - let mut rooms: HashMap> = HashMap::new(); - rooms.insert("general".to_string(), (0..100).collect()); - - b.iter(|| rooms.get(black_box("general")).map(|members| members.len())) - }); - - group.finish(); -} - -criterion_group!( - benches, - bench_text_message, - bench_binary_message, - bench_json_message, - bench_frame_encoding, - bench_broadcast, - bench_connection_management, -); - -criterion_main!(benches); diff --git a/benches/rustapi_bench/src/lib.rs b/benches/rustapi_bench/src/lib.rs deleted file mode 100644 index f4154a5..0000000 --- a/benches/rustapi_bench/src/lib.rs +++ /dev/null @@ -1 +0,0 @@ -// Placeholder for library diff --git a/benches/test_body.json b/benches/test_body.json deleted file mode 100644 index 3205461..0000000 --- a/benches/test_body.json +++ /dev/null @@ -1 +0,0 @@ -{"message":"Hello, World!"} \ No newline at end of file diff --git a/benches/toon_bench/Cargo.toml b/benches/toon_bench/Cargo.toml deleted file mode 100644 index 1ab5f6c..0000000 --- a/benches/toon_bench/Cargo.toml +++ /dev/null @@ -1,17 +0,0 @@ -[package] -name = "toon-bench" -version.workspace = true -edition.workspace = true -publish = false - -[[bench]] -name = "toon_bench" -harness = false - -[dependencies] -serde.workspace = true -serde_json.workspace = true -toon-format.workspace = true - -[dev-dependencies] -criterion.workspace = true diff --git a/benches/toon_bench/benches/toon_bench.rs b/benches/toon_bench/benches/toon_bench.rs deleted file mode 100644 index 9b01674..0000000 --- a/benches/toon_bench/benches/toon_bench.rs +++ /dev/null @@ -1,155 +0,0 @@ -//! TOON Format Benchmarks -//! -//! Benchmarks comparing TOON vs JSON performance: -//! - Serialization speed -//! - Deserialization speed -//! - Output size -//! - Token count estimation -//! -//! Run with: cargo bench --package toon-bench - -use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion}; -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Clone, Serialize, Deserialize)] -struct User { - id: u64, - name: String, - role: String, - active: bool, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -struct UsersResponse { - users: Vec, - total: usize, - page: usize, -} - -fn create_users(count: usize) -> Vec { - (1..=count) - .map(|i| User { - id: i as u64, - name: format!("User{}", i), - role: if i % 3 == 0 { - "admin".into() - } else { - "user".into() - }, - active: i % 2 == 0, - }) - .collect() -} - -fn create_response(user_count: usize) -> UsersResponse { - let users = create_users(user_count); - UsersResponse { - total: users.len(), - users, - page: 1, - } -} - -fn benchmark_serialization(c: &mut Criterion) { - let mut group = c.benchmark_group("serialization"); - - for size in [10, 50, 100, 500, 1000].iter() { - let response = create_response(*size); - - group.bench_with_input(BenchmarkId::new("json", size), size, |b, _| { - b.iter(|| { - let _ = black_box(serde_json::to_string(&response)); - }); - }); - - group.bench_with_input(BenchmarkId::new("toon", size), size, |b, _| { - b.iter(|| { - let _ = black_box(toon_format::encode_default(&response)); - }); - }); - } - - group.finish(); -} - -fn benchmark_deserialization(c: &mut Criterion) { - let mut group = c.benchmark_group("deserialization"); - - for size in [10, 50, 100].iter() { - let response = create_response(*size); - let json_str = serde_json::to_string(&response).unwrap(); - - group.bench_with_input(BenchmarkId::new("json", size), &json_str, |b, json| { - b.iter(|| { - let _: UsersResponse = black_box(serde_json::from_str(json).unwrap()); - }); - }); - } - - group.finish(); -} - -fn benchmark_output_size(c: &mut Criterion) { - let mut group = c.benchmark_group("output_size"); - - for size in [10, 50, 100, 500, 1000].iter() { - let response = create_response(*size); - let json_str = serde_json::to_string(&response).unwrap(); - let toon_str = toon_format::encode_default(&response).unwrap(); - - // Just measure sizes (not really a benchmark, more like a comparison) - println!("\n=== {} users ===", size); - println!("JSON bytes: {}", json_str.len()); - println!("TOON bytes: {}", toon_str.len()); - println!( - "Byte savings: {:.2}%", - (1.0 - (toon_str.len() as f64 / json_str.len() as f64)) * 100.0 - ); - - // Estimate tokens (~4 chars per token) - let json_tokens = json_str.len().div_ceil(4); - let toon_tokens = toon_str.len().div_ceil(4); - println!("JSON tokens (est): {}", json_tokens); - println!("TOON tokens (est): {}", toon_tokens); - println!( - "Token savings: {:.2}%", - (1.0 - (toon_tokens as f64 / json_tokens as f64)) * 100.0 - ); - - // Benchmark the size calculation itself (trivial) - group.bench_with_input(BenchmarkId::new("json_len", size), &json_str, |b, s| { - b.iter(|| black_box(s.len())); - }); - group.bench_with_input(BenchmarkId::new("toon_len", size), &toon_str, |b, s| { - b.iter(|| black_box(s.len())); - }); - } - - group.finish(); -} - -fn benchmark_roundtrip(c: &mut Criterion) { - let mut group = c.benchmark_group("roundtrip"); - - for size in [10, 50, 100].iter() { - let response = create_response(*size); - - group.bench_with_input(BenchmarkId::new("json", size), size, |b, _| { - b.iter(|| { - let json = serde_json::to_string(&response).unwrap(); - let _: UsersResponse = serde_json::from_str(&json).unwrap(); - }); - }); - } - - group.finish(); -} - -criterion_group!( - benches, - benchmark_serialization, - benchmark_deserialization, - benchmark_output_size, - benchmark_roundtrip, -); -criterion_main!(benches); diff --git a/benches/toon_bench/src/lib.rs b/benches/toon_bench/src/lib.rs deleted file mode 100644 index 865556c..0000000 --- a/benches/toon_bench/src/lib.rs +++ /dev/null @@ -1 +0,0 @@ -// Placeholder lib for benchmark crate diff --git a/build_log_2.txt b/build_log_2.txt new file mode 100644 index 0000000000000000000000000000000000000000..c2bd17e08d414b45a335f8417e33d0fd72560b7b GIT binary patch literal 1344 zcmcJP%TC)s6o&tGrM|;#sHiBZ3cD;Sp#UWt)Ph)%DmYG{7Pdi-DP+S#@W6%d9~W#S z1QG&`Jm)&+eti2cRia8KI#W=eZw6ICRgpSInkk1nMOH_xoI8nh0W6UpBcCG;V15~N z8R041b97SVK?QfA3AZ(lG{D112k4kH2d{O3y+jM}O)R9)epMo|{0F{j?ZBNt?EydP z1Cm-B`l%s2TQ7O{2RPT)4n2mppTF@wiZ3^K8OSc<&5UTiLHVsKV#?N6BCoAcV>{I; z{8h3G=w8Ga5~vJb8gqBX-}xp;UBRiw+-FpJtTpxUd=ss%BoLBgNoZo;7k8(>O89V9!lZkKB7N^rnyP8Y}M0i9X}; zP`~vBxQF!u*k<5JX4;~fp0abOjOfaaMu4*G-PDNfzg2 zfW?C2X`kZjKEEak-xa>S1%{r#W?v?FdwLga`>l4MJOA!vv(26`N1d~2j?kZ_p4B$G z{zILf+H_~rZ2H@5Yd6xzo7&yn$D2BeQNJ#yefnpg&CJ-NyI#e<`kz+Pg9TN}sYTLWG{$TNWPlc9* zXks8Yz320u@6)SaxiU36)wzAHNnG?KB8mJ3cS@N_A;%(_pp#d`&Es^@;CTu^%m|N>Hzpy zkC3$5)q73h*?P^pFTj(7lWpGN=Mx9=SW?SA27%uHZs5?sFfuga>kOf1JTg1d{g8glRM&v&FM{ma1Zn5INoN5b? zpY&NfzyqvTz&-;KYkdIA!1}oNZz7(?Tz5ITCPmE4 z1dA2N)4sshb$(qGz8idd3k*Gf-M-B6cK0sW_I>TZaQ@xNZks(}j=E&i9HBo;J*$0m zR}DqW-^}4(XqLb~9s-?s^k_^*^nq2WzTSQk8B~ cOY*Ixjh`FlH%w~MCfEwt&vjyhx1w^t0Xb9cDgXcg literal 0 HcmV?d00001 diff --git a/check_output.txt b/check_output.txt new file mode 100644 index 0000000000000000000000000000000000000000..0eab9560125a3f8ea4a64f98ca2019fae511d9a2 GIT binary patch literal 25326 zcmeI5TXP$?702=1nSO_@I@6(>+K!|+wv@y(iQ`-z?6h{07dsVciB6PAvPx1(G>QAs zoBn?|I@$}dq<9e}Qad};Vi#E8;M{=&K!5yiH*`WTd>`I~ENtuN*C*jX?{>paVLu!x zKMTL8)mdIiq;;+Z)uJ4J9>Yja>w!S z9i@Ja?Qf{u-_*9Z_TR9Gi`E>|^zo749|%(D&hR4F?uI|Tr+81O; z5t|M({rgN;OHmH%IsWJHt|+^%--c56h0B((w;f)FZzLmKVY#pLKomF*MfGapg;u?nT;kW7#@bZbc42WAs$7aS?n= z%hU(mGrd0)&%p76$hmE`!55tf51iEt?Q>qbE$KFyz6b;_)k;s$zEmwpI^XQ-en+Xk zT69Rzfs8^y~COUNW zWk>BDs2$?akP-oIlv__HyWSH-_#PjgId5<~Am>q%dDUs%TFFhHRt~;+c{Jlzg z9czb=^!|{RL{k`lZ`JVknlv@mXH&i?njJrccmBNUn%?C*zc;e6ISk z)H7~;o9p1y!@Uop?%%4>{Z=B)-)ovVc%zG1LFk|Be}8`yeEnSWms5=$SS34v7sMD~ zqgGBRk(QGkyKLu9;ZZ_$C-s<#+iBGYP4oGW_B80UnO3@rxEJ5 zesy!18yXL9*7ABW7P+k4-SG2j@iTt>XnuZh4fvVap}|=cZA}pKOiasT(2N+~D&}L6 z#oVKwXMAf}IiI+cMWw3osM#U{#8hm?xh?FoGNZ4%MZPR*c@U#i<#-kr|FwSHj})ir zo<$anX46KU(ox(k>FIlda$Ou=j6K;5yyd3-66c^{XyHAJyAfR)1mZ+%ic8*&Q8bHK zvtq^K<#&aXJQmegso%t>YcV?Oc)3-I$c~1m$b*&!8dgdeuJ<&qCfe(9(@amlDj!)H z#(?z_I?}7ft_3JZ?eaKE5nmpSUl$#jX-@TkyfV&=CtGK{S>&`)n1=IitEPF%gvE`S z>9%MtF-bUTxF6&kD1K>CZtH=?a3_lZJ&d%p?~zI^)v`k#GNZ*}^4Px>{`qNQc>q~YPi*=bz zwcss+;j)IgJedU^)`O(rc^f{6IC8A8(y4mQhLx7|$=_9{;i$;zncm-(H8cpkRnvRW zjk4vn3(I6BreB?qwiD}`&*Z`rDq9LP3;6O9rfF=AoMUzLJi}uFW z3_U)lUr}n-wC;kHNIOyAyKmGI9~9U80Pm!J(Y)-ADxxx-u3{Fn_!{s&e`B5@qdHrDB@9FLz`$8luL=!RP8;3jVg-P%qTxf=Py!@O|vi`nS#j2TUH9(q^!$qxEXIp z$|EkPSK5wX1#kL#@%j){h#e&1)X88IwWjBBrlu6`E0E+2{UpiDJiN%5KbO-p^$=)q>0J zB4rkM>Q;VxT%IyuC1pKk!^-%vP&q1ZR7#J9RwpJu@7X|h_}Zx^#ZwW{9$?aPCm$(Y zY|LAZ%wO#6wu|o>tB#aig64TITC8P?`rqWafqUX=<_6M!ljWAJXO&|Xse`@kqBZw- zelK$2Eb&v5d8@UV%LWgP-^AIXe#Yo@`^S)|x^ez&9)izWyUbn}zX^EiM(6G~IaV6K zX)&wAruCaR)i;lwZYs9G`G|PE#g&9ULLhgv?Qorac^*<3E^lwvd(p;RUr*<`qTQV2 zzz%8O3&~ztW^g!H-cEhy#8>Oc0-a%venVr?%T=Y?lyCqUyZv7a_S5Bg4| z{%2~9v!4&+o!Z$IT%13#y4b7BDQNsJ0@9p{lfy^NoOy7nds}xqPh?k5@N5npbvq0| z)K)|vTKmb_AX<*-uvoAM6Jz*GVfmyx@hi zr8zg7@^+TDy() /// ``` - pub fn register_schema rustapi_openapi::Schema<'a>>(mut self) -> Self { + pub fn register_schema(mut self) -> Self { self.openapi_spec = self.openapi_spec.register::(); self } @@ -1183,34 +1183,39 @@ fn add_path_params_to_operation( } } +/// Convert a schema type string to an OpenAPI schema reference /// Convert a schema type string to an OpenAPI schema reference fn schema_type_to_openapi_schema(schema_type: &str) -> rustapi_openapi::SchemaRef { + use rustapi_openapi::schema::{Schema, SchemaType}; + match schema_type.to_lowercase().as_str() { - "uuid" => rustapi_openapi::SchemaRef::Inline(serde_json::json!({ - "type": "string", - "format": "uuid" - })), - "integer" | "int" | "int64" | "i64" => { - rustapi_openapi::SchemaRef::Inline(serde_json::json!({ - "type": "integer", - "format": "int64" - })) - } - "int32" | "i32" => rustapi_openapi::SchemaRef::Inline(serde_json::json!({ - "type": "integer", - "format": "int32" - })), - "number" | "float" | "f64" | "f32" => { - rustapi_openapi::SchemaRef::Inline(serde_json::json!({ - "type": "number" - })) - } - "boolean" | "bool" => rustapi_openapi::SchemaRef::Inline(serde_json::json!({ - "type": "boolean" - })), - _ => rustapi_openapi::SchemaRef::Inline(serde_json::json!({ - "type": "string" - })), + "uuid" => rustapi_openapi::SchemaRef::T(Schema { + schema_type: Some(SchemaType::String), + format: Some("uuid".to_string()), + ..Default::default() + }), + "integer" | "int" | "int64" | "i64" => rustapi_openapi::SchemaRef::T(Schema { + schema_type: Some(SchemaType::Integer), + format: Some("int64".to_string()), + ..Default::default() + }), + "int32" | "i32" => rustapi_openapi::SchemaRef::T(Schema { + schema_type: Some(SchemaType::Integer), + format: Some("int32".to_string()), + ..Default::default() + }), + "number" | "float" | "f64" | "f32" => rustapi_openapi::SchemaRef::T(Schema { + schema_type: Some(SchemaType::Number), + ..Default::default() + }), + "boolean" | "bool" => rustapi_openapi::SchemaRef::T(Schema { + schema_type: Some(SchemaType::Boolean), + ..Default::default() + }), + _ => rustapi_openapi::SchemaRef::T(Schema { + schema_type: Some(SchemaType::String), + ..Default::default() + }), } } @@ -1223,16 +1228,19 @@ fn schema_type_to_openapi_schema(schema_type: &str) -> rustapi_openapi::SchemaRe /// - `year`, `month`, `day` → integer /// - Everything else → string fn infer_path_param_schema(name: &str) -> rustapi_openapi::SchemaRef { + use rustapi_openapi::schema::{Schema, SchemaType}; + let lower = name.to_lowercase(); // UUID patterns (check first to avoid false positive from "id" suffix) let is_uuid = lower == "uuid" || lower.ends_with("_uuid") || lower.ends_with("uuid"); if is_uuid { - return rustapi_openapi::SchemaRef::Inline(serde_json::json!({ - "type": "string", - "format": "uuid" - })); + return rustapi_openapi::SchemaRef::T(Schema { + schema_type: Some(SchemaType::String), + format: Some("uuid".to_string()), + ..Default::default() + }); } // Integer patterns @@ -1250,12 +1258,16 @@ fn infer_path_param_schema(name: &str) -> rustapi_openapi::SchemaRef { || lower == "position"; if is_integer { - rustapi_openapi::SchemaRef::Inline(serde_json::json!({ - "type": "integer", - "format": "int64" - })) + rustapi_openapi::SchemaRef::T(Schema { + schema_type: Some(SchemaType::Integer), + format: Some("int64".to_string()), + ..Default::default() + }) } else { - rustapi_openapi::SchemaRef::Inline(serde_json::json!({ "type": "string" })) + rustapi_openapi::SchemaRef::T(Schema { + schema_type: Some(SchemaType::String), + ..Default::default() + }) } } @@ -1331,6 +1343,7 @@ mod tests { #[test] fn test_path_param_type_inference_integer() { use super::infer_path_param_schema; + use rustapi_openapi::schema::SchemaType; // Test common integer patterns let int_params = [ @@ -1349,10 +1362,10 @@ mod tests { for name in int_params { let schema = infer_path_param_schema(name); match schema { - rustapi_openapi::SchemaRef::Inline(v) => { + rustapi_openapi::SchemaRef::T(s) => { assert_eq!( - v.get("type").and_then(|v| v.as_str()), - Some("integer"), + s.schema_type, + Some(SchemaType::Integer), "Expected '{}' to be inferred as integer", name ); @@ -1365,6 +1378,7 @@ mod tests { #[test] fn test_path_param_type_inference_uuid() { use super::infer_path_param_schema; + use rustapi_openapi::schema::SchemaType; // Test UUID patterns let uuid_params = ["uuid", "user_uuid", "sessionUuid"]; @@ -1372,16 +1386,16 @@ mod tests { for name in uuid_params { let schema = infer_path_param_schema(name); match schema { - rustapi_openapi::SchemaRef::Inline(v) => { + rustapi_openapi::SchemaRef::T(s) => { assert_eq!( - v.get("type").and_then(|v| v.as_str()), - Some("string"), + s.schema_type, + Some(SchemaType::String), "Expected '{}' to be inferred as string", name ); assert_eq!( - v.get("format").and_then(|v| v.as_str()), - Some("uuid"), + s.format, + Some("uuid".to_string()), "Expected '{}' to have uuid format", name ); @@ -1394,6 +1408,7 @@ mod tests { #[test] fn test_path_param_type_inference_string() { use super::infer_path_param_schema; + use rustapi_openapi::schema::SchemaType; // Test string (default) patterns let string_params = [ @@ -1403,16 +1418,15 @@ mod tests { for name in string_params { let schema = infer_path_param_schema(name); match schema { - rustapi_openapi::SchemaRef::Inline(v) => { + rustapi_openapi::SchemaRef::T(s) => { assert_eq!( - v.get("type").and_then(|v| v.as_str()), - Some("string"), + s.schema_type, + Some(SchemaType::String), "Expected '{}' to be inferred as string", name ); assert!( - v.get("format").is_none() - || v.get("format").and_then(|v| v.as_str()) != Some("uuid"), + s.format.is_none() || s.format.as_deref() != Some("uuid"), "Expected '{}' to NOT have uuid format", name ); @@ -1425,13 +1439,14 @@ mod tests { #[test] fn test_schema_type_to_openapi_schema() { use super::schema_type_to_openapi_schema; + use rustapi_openapi::schema::SchemaType; // Test UUID schema let uuid_schema = schema_type_to_openapi_schema("uuid"); match uuid_schema { - rustapi_openapi::SchemaRef::Inline(v) => { - assert_eq!(v.get("type").and_then(|v| v.as_str()), Some("string")); - assert_eq!(v.get("format").and_then(|v| v.as_str()), Some("uuid")); + rustapi_openapi::SchemaRef::T(s) => { + assert_eq!(s.schema_type, Some(SchemaType::String)); + assert_eq!(s.format, Some("uuid".to_string())); } _ => panic!("Expected inline schema for uuid"), } @@ -1440,9 +1455,9 @@ mod tests { for schema_type in ["integer", "int", "int64", "i64"] { let schema = schema_type_to_openapi_schema(schema_type); match schema { - rustapi_openapi::SchemaRef::Inline(v) => { - assert_eq!(v.get("type").and_then(|v| v.as_str()), Some("integer")); - assert_eq!(v.get("format").and_then(|v| v.as_str()), Some("int64")); + rustapi_openapi::SchemaRef::T(s) => { + assert_eq!(s.schema_type, Some(SchemaType::Integer)); + assert_eq!(s.format, Some("int64".to_string())); } _ => panic!("Expected inline schema for {}", schema_type), } @@ -1451,9 +1466,9 @@ mod tests { // Test int32 schema let int32_schema = schema_type_to_openapi_schema("int32"); match int32_schema { - rustapi_openapi::SchemaRef::Inline(v) => { - assert_eq!(v.get("type").and_then(|v| v.as_str()), Some("integer")); - assert_eq!(v.get("format").and_then(|v| v.as_str()), Some("int32")); + rustapi_openapi::SchemaRef::T(s) => { + assert_eq!(s.schema_type, Some(SchemaType::Integer)); + assert_eq!(s.format, Some("int32".to_string())); } _ => panic!("Expected inline schema for int32"), } @@ -1462,8 +1477,8 @@ mod tests { for schema_type in ["number", "float"] { let schema = schema_type_to_openapi_schema(schema_type); match schema { - rustapi_openapi::SchemaRef::Inline(v) => { - assert_eq!(v.get("type").and_then(|v| v.as_str()), Some("number")); + rustapi_openapi::SchemaRef::T(s) => { + assert_eq!(s.schema_type, Some(SchemaType::Number)); } _ => panic!("Expected inline schema for {}", schema_type), } @@ -1473,8 +1488,8 @@ mod tests { for schema_type in ["boolean", "bool"] { let schema = schema_type_to_openapi_schema(schema_type); match schema { - rustapi_openapi::SchemaRef::Inline(v) => { - assert_eq!(v.get("type").and_then(|v| v.as_str()), Some("boolean")); + rustapi_openapi::SchemaRef::T(s) => { + assert_eq!(s.schema_type, Some(SchemaType::Boolean)); } _ => panic!("Expected inline schema for {}", schema_type), } @@ -1483,8 +1498,8 @@ mod tests { // Test string schema (default) let string_schema = schema_type_to_openapi_schema("string"); match string_schema { - rustapi_openapi::SchemaRef::Inline(v) => { - assert_eq!(v.get("type").and_then(|v| v.as_str()), Some("string")); + rustapi_openapi::SchemaRef::T(s) => { + assert_eq!(s.schema_type, Some(SchemaType::String)); } _ => panic!("Expected inline schema for string"), } diff --git a/crates/rustapi-core/src/extract.rs b/crates/rustapi-core/src/extract.rs index be3b881..0839156 100644 --- a/crates/rustapi-core/src/extract.rs +++ b/crates/rustapi-core/src/extract.rs @@ -62,6 +62,10 @@ use crate::stream::{StreamingBody, StreamingConfig}; use crate::validation::Validatable; use bytes::Bytes; use http::{header, StatusCode}; +use rustapi_openapi::{ + IntoParams, MediaType, Operation, OperationModifier, ParameterIn, RequestBody, + ResponseModifier, ResponseSpec, SchemaRef, ToSchema, +}; use rustapi_validate::v2::{AsyncValidate, ValidationContext}; use serde::de::DeserializeOwned; @@ -960,21 +964,16 @@ impl_from_request_parts_for_primitives!( // OperationModifier implementations for extractors -use rustapi_openapi::utoipa_types::openapi; -use rustapi_openapi::{ - IntoParams, MediaType, Operation, OperationModifier, Parameter, RequestBody, ResponseModifier, - ResponseSpec, Schema, SchemaRef, -}; use std::collections::HashMap; // ValidatedJson - Adds request body -impl Schema<'a>> OperationModifier for ValidatedJson { +impl OperationModifier for ValidatedJson { fn update_operation(op: &mut Operation) { let (name, _) = T::schema(); - let schema_ref = SchemaRef::Ref { - reference: format!("#/components/schemas/{}", name), - }; + let schema_ref = SchemaRef::Ref(rustapi_openapi::schema::Reference { + ref_path: format!("#/components/schemas/{}", name), + }); let mut content = HashMap::new(); content.insert( @@ -997,9 +996,9 @@ impl Schema<'a>> OperationModifier for ValidatedJson { map.insert( "application/json".to_string(), MediaType { - schema: SchemaRef::Ref { - reference: "#/components/schemas/ValidationErrorSchema".to_string(), - }, + schema: SchemaRef::Ref(rustapi_openapi::schema::Reference { + ref_path: "#/components/schemas/ValidationErrorSchema".to_string(), + }), }, ); Some(map) @@ -1010,13 +1009,13 @@ impl Schema<'a>> OperationModifier for ValidatedJson { } // Json - Adds request body (Same as ValidatedJson) -impl Schema<'a>> OperationModifier for Json { +impl OperationModifier for Json { fn update_operation(op: &mut Operation) { let (name, _) = T::schema(); - let schema_ref = SchemaRef::Ref { - reference: format!("#/components/schemas/{}", name), - }; + let schema_ref = SchemaRef::Ref(rustapi_openapi::schema::Reference { + ref_path: format!("#/components/schemas/{}", name), + }); let mut content = HashMap::new(); content.insert( @@ -1055,43 +1054,11 @@ impl OperationModifier for Typed { // Query - Extracts query params using IntoParams impl OperationModifier for Query { fn update_operation(op: &mut Operation) { - let params = T::into_params(|| Some(openapi::path::ParameterIn::Query)); - - let new_params: Vec = params - .into_iter() - .map(|p| { - let schema = match p.schema { - Some(schema) => match schema { - openapi::RefOr::Ref(r) => SchemaRef::Ref { - reference: r.ref_location, - }, - openapi::RefOr::T(s) => { - let value = serde_json::to_value(s).unwrap_or(serde_json::Value::Null); - SchemaRef::Inline(value) - } - }, - None => SchemaRef::Inline(serde_json::Value::Null), - }; - - let required = match p.required { - openapi::Required::True => true, - openapi::Required::False => false, - }; - - Parameter { - name: p.name, - location: "query".to_string(), // explicitly query - required, - description: p.description, - schema, - } - }) - .collect(); - + let params = T::into_params(|| Some(ParameterIn::Query)); if let Some(existing) = &mut op.parameters { - existing.extend(new_params); + existing.extend(params); } else { - op.parameters = Some(new_params); + op.parameters = Some(params); } } } @@ -1108,9 +1075,11 @@ impl OperationModifier for Body { content.insert( "application/octet-stream".to_string(), MediaType { - schema: SchemaRef::Inline( - serde_json::json!({ "type": "string", "format": "binary" }), - ), + schema: SchemaRef::T(rustapi_openapi::schema::Schema { + schema_type: Some(rustapi_openapi::schema::SchemaType::String), + format: Some("binary".to_string()), + ..Default::default() + }), }, ); @@ -1128,9 +1097,11 @@ impl OperationModifier for BodyStream { content.insert( "application/octet-stream".to_string(), MediaType { - schema: SchemaRef::Inline( - serde_json::json!({ "type": "string", "format": "binary" }), - ), + schema: SchemaRef::T(rustapi_openapi::schema::Schema { + schema_type: Some(rustapi_openapi::schema::SchemaType::String), + format: Some("binary".to_string()), + ..Default::default() + }), }, ); @@ -1144,13 +1115,13 @@ impl OperationModifier for BodyStream { // ResponseModifier implementations for extractors // Json - 200 OK with schema T -impl Schema<'a>> ResponseModifier for Json { +impl ResponseModifier for Json { fn update_response(op: &mut Operation) { let (name, _) = T::schema(); - let schema_ref = SchemaRef::Ref { - reference: format!("#/components/schemas/{}", name), - }; + let schema_ref = SchemaRef::Ref(rustapi_openapi::schema::Reference { + ref_path: format!("#/components/schemas/{}", name), + }); op.responses.insert( "200".to_string(), diff --git a/crates/rustapi-core/src/response.rs b/crates/rustapi-core/src/response.rs index 8a81837..e888420 100644 --- a/crates/rustapi-core/src/response.rs +++ b/crates/rustapi-core/src/response.rs @@ -75,7 +75,7 @@ use bytes::Bytes; use futures_util::StreamExt; use http::{header, HeaderMap, HeaderValue, StatusCode}; use http_body_util::Full; -use rustapi_openapi::{MediaType, Operation, ResponseModifier, ResponseSpec, Schema, SchemaRef}; +use rustapi_openapi::{MediaType, Operation, ResponseModifier, ResponseSpec, SchemaRef, ToSchema}; use serde::Serialize; use std::collections::HashMap; use std::pin::Pin; @@ -293,9 +293,9 @@ impl ResponseModifier for ApiError { map.insert( "application/json".to_string(), MediaType { - schema: SchemaRef::Ref { - reference: "#/components/schemas/ErrorSchema".to_string(), - }, + schema: SchemaRef::Ref(rustapi_openapi::schema::Reference { + ref_path: "#/components/schemas/ErrorSchema".to_string(), + }), }, ); Some(map) @@ -313,9 +313,9 @@ impl ResponseModifier for ApiError { map.insert( "application/json".to_string(), MediaType { - schema: SchemaRef::Ref { - reference: "#/components/schemas/ErrorSchema".to_string(), - }, + schema: SchemaRef::Ref(rustapi_openapi::schema::Reference { + ref_path: "#/components/schemas/ErrorSchema".to_string(), + }), }, ); Some(map) @@ -355,13 +355,13 @@ impl IntoResponse for Created { } } -impl Schema<'a>> ResponseModifier for Created { +impl ResponseModifier for Created { fn update_response(op: &mut Operation) { let (name, _) = T::schema(); - let schema_ref = SchemaRef::Ref { - reference: format!("#/components/schemas/{}", name), - }; + let schema_ref = SchemaRef::Ref(rustapi_openapi::schema::Reference { + ref_path: format!("#/components/schemas/{}", name), + }); op.responses.insert( "201".to_string(), @@ -441,7 +441,10 @@ impl ResponseModifier for Html { map.insert( "text/html".to_string(), MediaType { - schema: SchemaRef::Inline(serde_json::json!({ "type": "string" })), + schema: SchemaRef::T(rustapi_openapi::schema::Schema { + schema_type: Some(rustapi_openapi::schema::SchemaType::String), + ..Default::default() + }), }, ); Some(map) @@ -539,13 +542,13 @@ impl IntoResponse for WithStatus { } } -impl Schema<'a>, const CODE: u16> ResponseModifier for WithStatus { +impl ResponseModifier for WithStatus { fn update_response(op: &mut Operation) { let (name, _) = T::schema(); - let schema_ref = SchemaRef::Ref { - reference: format!("#/components/schemas/{}", name), - }; + let schema_ref = SchemaRef::Ref(rustapi_openapi::schema::Reference { + ref_path: format!("#/components/schemas/{}", name), + }); op.responses.insert( CODE.to_string(), diff --git a/crates/rustapi-core/src/sse.rs b/crates/rustapi-core/src/sse.rs index 333cf45..1a336da 100644 --- a/crates/rustapi-core/src/sse.rs +++ b/crates/rustapi-core/src/sse.rs @@ -396,11 +396,12 @@ impl ResponseModifier for Sse { content.insert( "text/event-stream".to_string(), MediaType { - schema: SchemaRef::Inline(serde_json::json!({ - "type": "string", - "description": "Server-Sent Events stream. Events follow the SSE format: 'event: \\ndata: \\n\\n'", - "example": "event: message\ndata: {\"id\": 1, \"text\": \"Hello\"}\n\n" - })), + schema: SchemaRef::T(rustapi_openapi::schema::Schema { + schema_type: Some(rustapi_openapi::schema::SchemaType::String), + description: Some("Server-Sent Events stream. Events follow the SSE format: 'event: \\ndata: \\n\\n'".to_string()), + example: Some(serde_json::json!("event: message\ndata: {\"id\": 1, \"text\": \"Hello\"}\n\n")), + ..Default::default() + }), }, ); diff --git a/crates/rustapi-macros/src/lib.rs b/crates/rustapi-macros/src/lib.rs index 8ae21dc..766f700 100644 --- a/crates/rustapi-macros/src/lib.rs +++ b/crates/rustapi-macros/src/lib.rs @@ -23,6 +23,43 @@ use syn::{ }; mod api_error; +mod schema; + +/// Derive macro for OpenAPI Schema generation +/// +/// # Example +/// +/// ```rust,ignore +/// #[derive(ToSchema)] +/// struct User { +/// id: i32, +/// name: String, +/// } +/// ``` +#[proc_macro_derive(ToSchema, attributes(schema, serde))] +pub fn derive_to_schema(input: TokenStream) -> TokenStream { + schema::derive_to_schema(input) +} + +mod params; + +/// Derive macro for OpenAPI Parameters generation (IntoParams) +/// +/// Use this on query parameter structs. +/// +/// # Example +/// +/// ```rust,ignore +/// #[derive(IntoParams, Deserialize)] +/// struct Pagination { +/// page: Option, +/// limit: Option, +/// } +/// ``` +#[proc_macro_derive(IntoParams, attributes(serde))] +pub fn derive_into_params(input: TokenStream) -> TokenStream { + params::derive_into_params(input) +} /// Auto-register a schema type for zero-config OpenAPI. /// diff --git a/crates/rustapi-macros/src/params.rs b/crates/rustapi-macros/src/params.rs new file mode 100644 index 0000000..8d35ae8 --- /dev/null +++ b/crates/rustapi-macros/src/params.rs @@ -0,0 +1,112 @@ +use proc_macro2::TokenStream; +use quote::quote; +use syn::{parse_macro_input, Attribute, Data, DeriveInput, Expr, Fields, Lit, Meta}; + +pub fn derive_into_params(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + let input = parse_macro_input!(input as DeriveInput); + let name = &input.ident; + + let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); + + let params_impl = match &input.data { + Data::Struct(s) => expand_struct_params(s), + _ => { + return syn::Error::new_spanned(name, "IntoParams only supported for structs") + .to_compile_error() + .into(); + } + }; + + let expanded = quote! { + impl #impl_generics ::rustapi_rs::__private::rustapi_openapi::IntoParams for #name #ty_generics #where_clause { + fn into_params(parameter_in_provider: impl Fn() -> Option<::rustapi_rs::__private::rustapi_openapi::ParameterIn>) -> Vec<::rustapi_rs::__private::rustapi_openapi::Parameter> { + let default_location = ::rustapi_rs::__private::rustapi_openapi::ParameterIn::Query; + let location_enum = parameter_in_provider().unwrap_or(default_location); + let location_str = location_enum.to_string(); + + #params_impl + } + } + }; + + expanded.into() +} + +fn expand_struct_params(data: &syn::DataStruct) -> TokenStream { + let mut params = Vec::new(); + + match &data.fields { + Fields::Named(fields) => { + for field in &fields.named { + let ident = field.ident.as_ref().unwrap(); + let ident_str = ident.to_string(); + + let mut param_name = ident_str; + let mut is_option = false; + + // Check for Option wrapper + if let syn::Type::Path(tp) = &field.ty { + if let Some(seg) = tp.path.segments.last() { + if seg.ident == "Option" { + is_option = true; + } + } + } + + // Handle serde rename + if let Some(renamed) = get_serde_rename(&field.attrs) { + param_name = renamed; + } + + let required = !is_option; + let ty = &field.ty; + + // Doc comments handling (simplified) + let description = quote! { None }; + + params.push(quote! { + ::rustapi_rs::__private::rustapi_openapi::Parameter { + name: #param_name.to_string(), + location: location_str.clone(), + required: #required, + description: #description, + schema: <#ty as ::rustapi_rs::__private::rustapi_openapi::ToSchema>::schema().1, + } + }); + } + } + _ => { + // Unnamed fields / Unit structs not supported for IntoParams (usually query params are named) + return quote! { vec![] }; + } + } + + quote! { + vec![ + #(#params),* + ] + } +} + +fn get_serde_rename(attrs: &[Attribute]) -> Option { + for attr in attrs { + if attr.path().is_ident("serde") { + if let Ok(nested) = attr.parse_args_with( + syn::punctuated::Punctuated::::parse_terminated, + ) { + for meta in nested { + if let Meta::NameValue(nv) = meta { + if nv.path.is_ident("rename") { + if let Expr::Lit(lit) = nv.value { + if let Lit::Str(s) = lit.lit { + return Some(s.value()); + } + } + } + } + } + } + } + } + None +} diff --git a/crates/rustapi-macros/src/schema.rs b/crates/rustapi-macros/src/schema.rs new file mode 100644 index 0000000..d9cd14c --- /dev/null +++ b/crates/rustapi-macros/src/schema.rs @@ -0,0 +1,195 @@ +use proc_macro2::TokenStream; +use quote::quote; +use syn::{ + parse_macro_input, Attribute, Data, DataEnum, DataStruct, DeriveInput, Expr, Fields, + FieldsNamed, Lit, Meta, +}; + +pub fn derive_to_schema(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + let input = parse_macro_input!(input as DeriveInput); + let name = &input.ident; + let name_str = name.to_string(); + + let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); + + let schema_impl = match &input.data { + Data::Struct(s) => expand_struct_schema(s, &name_str), + Data::Enum(e) => expand_enum_schema(e, &name_str), + Data::Union(_) => { + return syn::Error::new_spanned(name, "Unions are not supported for ToSchema") + .to_compile_error() + .into(); + } + }; + + let expanded = quote! { + impl #impl_generics ::rustapi_rs::__private::rustapi_openapi::ToSchema for #name #ty_generics #where_clause { + fn name() -> String { + #name_str.to_string() + } + + fn schema() -> (String, ::rustapi_rs::__private::rustapi_openapi::schema::RefOr<::rustapi_rs::__private::rustapi_openapi::schema::Schema>) { + #schema_impl + } + } + }; + + expanded.into() +} + +fn expand_struct_schema(data: &DataStruct, name: &str) -> TokenStream { + match &data.fields { + Fields::Named(fields) => expand_named_fields(fields, name), + Fields::Unnamed(_) => quote! { + // Tuple structs treated as array likely, or just empty? + // For now, treat as just named schema with no props? + // Actually, let's support them as simple objects if possible, or error. + // Simplified implementation: + ( + #name.to_string(), + ::rustapi_rs::__private::rustapi_openapi::schema::Schema { + schema_type: Some(::rustapi_rs::__private::rustapi_openapi::schema::SchemaType::Object), + description: None, + ..Default::default() + }.into() + ) + }, + Fields::Unit => quote! { + ( + #name.to_string(), + ::rustapi_rs::__private::rustapi_openapi::schema::Schema { + schema_type: Some(::rustapi_rs::__private::rustapi_openapi::schema::SchemaType::Object), // or null? + description: None, + ..Default::default() + }.into() + ) + }, + } +} + +fn expand_named_fields(fields: &FieldsNamed, name: &str) -> TokenStream { + let mut props = Vec::new(); + let mut required = Vec::new(); + + for field in &fields.named { + let field_name = field.ident.as_ref().unwrap(); + let mut field_name_str = field_name.to_string(); + + let mut is_option = false; + + // Check for Option wrapper to determine if required + // This is a naive check (string matching), could be improved with type analysis + // assuming standard Option usage + if let syn::Type::Path(tp) = &field.ty { + if let Some(seg) = tp.path.segments.last() { + if seg.ident == "Option" { + is_option = true; + } + } + } + + // Handle serde rename + if let Some(renamed) = get_serde_rename(&field.attrs) { + field_name_str = renamed; + } + + if !is_option { + required.push(quote! { #field_name_str.to_string() }); + } + + let ty = &field.ty; + + // Property schema generation + // We defer to ::schema() + // But for Option, we want T's schema. + // For Vec, we want Array of T. + // Our ToSchema impl for Option/Vec already handles structure, + // but we need to supply the reference. + + // If field type implements ToSchema, we can just use it? + // Yes, ToSchema::schema() returns (name, RefOr). + // If RefOr is Ref, we are good. + // If RefOr is T (inline), we embed it. + + props.push(quote! { + map.insert( + #field_name_str.to_string(), + <#ty as ::rustapi_rs::__private::rustapi_openapi::ToSchema>::schema().1 + ); + }); + } + + let required_quote = if required.is_empty() { + quote! { None } + } else { + quote! { Some(vec![#(#required),*]) } + }; + + quote! { + let mut map = ::std::collections::HashMap::new(); + #(#props)* + + ( + #name.to_string(), + ::rustapi_rs::__private::rustapi_openapi::schema::Schema { + schema_type: Some(::rustapi_rs::__private::rustapi_openapi::schema::SchemaType::Object), + properties: Some(map), + required: #required_quote, + ..Default::default() + }.into() + ) + } +} + +fn expand_enum_schema(data: &DataEnum, name: &str) -> TokenStream { + let mut variants = Vec::new(); + + // Simple enum (C-like) support for now (strings) + for variant in &data.variants { + let mut variant_name = variant.ident.to_string(); + // Handle serde rename + if let Some(renamed) = get_serde_rename(&variant.attrs) { + variant_name = renamed; + } + + variants.push(quote! { + ::serde_json::Value::String(#variant_name.to_string()) + }); + } + + quote! { + ( + #name.to_string(), + ::rustapi_rs::__private::rustapi_openapi::schema::Schema { + schema_type: Some(::rustapi_rs::__private::rustapi_openapi::schema::SchemaType::String), + enum_values: Some(vec![#(#variants),*]), + ..Default::default() + }.into() + ) + } +} + +fn get_serde_rename(attrs: &[Attribute]) -> Option { + for attr in attrs { + if attr.path().is_ident("serde") { + // Parse #[serde(...)] + // Looking for rename = "name" + if let Ok(nested) = attr.parse_args_with( + syn::punctuated::Punctuated::::parse_terminated, + ) { + for meta in nested { + if let Meta::NameValue(nv) = meta { + if nv.path.is_ident("rename") { + if let Expr::Lit(lit) = nv.value { + if let Lit::Str(s) = lit.lit { + return Some(s.value()); + } + } + } + } + } + } + } + } + None +} diff --git a/crates/rustapi-openapi/Cargo.toml b/crates/rustapi-openapi/Cargo.toml index 34ac5d7..1bad30c 100644 --- a/crates/rustapi-openapi/Cargo.toml +++ b/crates/rustapi-openapi/Cargo.toml @@ -14,13 +14,14 @@ homepage.workspace = true http = { workspace = true } http-body-util = { workspace = true } bytes = { workspace = true } +uuid = { workspace = true, optional = true } # Serialization serde = { workspace = true } serde_json = { workspace = true } # OpenAPI schema generation -utoipa = { workspace = true } + @@ -28,3 +29,4 @@ utoipa = { workspace = true } default = ["swagger-ui"] swagger-ui = [] redoc = [] +uuid = ["dep:uuid"] diff --git a/crates/rustapi-openapi/src/lib.rs b/crates/rustapi-openapi/src/lib.rs index 8f74762..2f777f0 100644 --- a/crates/rustapi-openapi/src/lib.rs +++ b/crates/rustapi-openapi/src/lib.rs @@ -62,8 +62,9 @@ mod config; #[cfg(feature = "redoc")] mod redoc; -mod schemas; -mod spec; +pub mod schema; +pub mod schemas; +pub mod spec; #[cfg(feature = "swagger-ui")] mod swagger; @@ -74,25 +75,16 @@ pub mod v31; pub mod versioning; pub use config::OpenApiConfig; +pub use schema::{Schema, ToSchema}; pub use schemas::{ ErrorBodySchema, ErrorSchema, FieldErrorSchema, ValidationErrorBodySchema, ValidationErrorSchema, }; pub use spec::{ - ApiInfo, MediaType, OpenApiSpec, Operation, OperationModifier, Parameter, PathItem, - RequestBody, ResponseModifier, ResponseSpec, SchemaRef, + ApiInfo, IntoParams, MediaType, OpenApiSpec, Operation, OperationModifier, Parameter, + ParameterIn, PathItem, RequestBody, ResponseModifier, ResponseSpec, SchemaRef, }; -// Re-export utoipa's ToSchema derive macro as Schema -pub use utoipa::ToSchema as Schema; -// Re-export utoipa's IntoParams derive macro -pub use utoipa::IntoParams; - -// Re-export utoipa types for advanced usage -pub mod utoipa_types { - pub use utoipa::{openapi, IntoParams, Modify, OpenApi, ToSchema}; -} - use bytes::Bytes; use http::{header, Response, StatusCode}; use http_body_util::Full; diff --git a/crates/rustapi-openapi/src/schema.rs b/crates/rustapi-openapi/src/schema.rs new file mode 100644 index 0000000..e566d87 --- /dev/null +++ b/crates/rustapi-openapi/src/schema.rs @@ -0,0 +1,224 @@ +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +/// Trait for types that can generate their own OpenAPI schema. +pub trait ToSchema { + /// Get the name of the schema (for ref) + fn name() -> String; + + /// Generate the schema object + fn schema() -> (String, RefOr); +} + +/// Reference or inline schema +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(untagged)] +pub enum RefOr { + Ref(Reference), + T(T), +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct Reference { + #[serde(rename = "$ref")] + pub ref_path: String, +} + +/// OpenAPI Schema Object +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)] +pub struct Schema { + #[serde(rename = "type", skip_serializing_if = "Option::is_none")] + pub schema_type: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub format: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub properties: Option>>, + #[serde(skip_serializing_if = "Option::is_none")] + pub items: Option>>, + #[serde(skip_serializing_if = "Option::is_none")] + pub required: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub example: Option, + #[serde(rename = "enum", skip_serializing_if = "Option::is_none")] + pub enum_values: Option>, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum SchemaType { + String, + Integer, + Number, + Boolean, + Object, + Array, +} + +impl From for RefOr { + fn from(t: T) -> Self { + RefOr::T(t) + } +} + +// Primitives Implementation + +impl ToSchema for String { + fn name() -> String { + "String".to_string() + } + + fn schema() -> (String, RefOr) { + ( + "String".to_string(), + Schema { + schema_type: Some(SchemaType::String), + ..Default::default() + } + .into(), + ) + } +} + +impl ToSchema for &str { + fn name() -> String { + "String".to_string() + } + + fn schema() -> (String, RefOr) { + ( + "String".to_string(), + Schema { + schema_type: Some(SchemaType::String), + ..Default::default() + } + .into(), + ) + } +} + +impl ToSchema for bool { + fn name() -> String { + "Boolean".to_string() + } + + fn schema() -> (String, RefOr) { + ( + "Boolean".to_string(), + Schema { + schema_type: Some(SchemaType::Boolean), + ..Default::default() + } + .into(), + ) + } +} + +// Integer types +macro_rules! impl_int_schema { + ($($ty:ty),*) => { + $( + impl ToSchema for $ty { + fn name() -> String { + "Integer".to_string() + } + + fn schema() -> (String, RefOr) { + ( + "Integer".to_string(), + Schema { + schema_type: Some(SchemaType::Integer), + format: if std::mem::size_of::<$ty>() > 4 { Some("int64".to_string()) } else { Some("int32".to_string()) }, + ..Default::default() + } + .into(), + ) + } + } + )* + }; +} + +impl_int_schema!(i8, i16, i32, i64, isize, u8, u16, u32, u64, usize); + +// Float types +macro_rules! impl_float_schema { + ($($ty:ty),*) => { + $( + impl ToSchema for $ty { + fn name() -> String { + "Number".to_string() + } + + fn schema() -> (String, RefOr) { + ( + "Number".to_string(), + Schema { + schema_type: Some(SchemaType::Number), + format: if std::mem::size_of::<$ty>() > 4 { Some("double".to_string()) } else { Some("float".to_string()) }, + ..Default::default() + } + .into(), + ) + } + } + )* + }; +} + +impl_float_schema!(f32, f64); + +// Option +impl ToSchema for Option { + fn name() -> String { + T::name() + } + + fn schema() -> (String, RefOr) { + // Option doesn't change the schema structure in OpenAPI 3.0 usually, + // it just means it's not in 'required' list of parent object, or nullable: true + // For simplicity, we delegate to T + T::schema() + } +} + +// Vec +impl ToSchema for Vec { + fn name() -> String { + format!("Array_of_{}", T::name()) + } + + fn schema() -> (String, RefOr) { + let (_, item_schema) = T::schema(); + ( + Self::name(), + Schema { + schema_type: Some(SchemaType::Array), + items: Some(Box::new(item_schema)), + ..Default::default() + } + .into(), + ) + } +} + +// UUID support (if feature enabled, or just hardcode as string for now as it's common) +#[cfg(feature = "uuid")] +impl ToSchema for uuid::Uuid { + fn name() -> String { + "Uuid".to_string() + } + + fn schema() -> (String, RefOr) { + ( + "Uuid".to_string(), + Schema { + schema_type: Some(SchemaType::String), + format: Some("uuid".to_string()), + ..Default::default() + } + .into(), + ) + } +} diff --git a/crates/rustapi-openapi/src/schemas.rs b/crates/rustapi-openapi/src/schemas.rs index 79b6447..8d0c61f 100644 --- a/crates/rustapi-openapi/src/schemas.rs +++ b/crates/rustapi-openapi/src/schemas.rs @@ -3,10 +3,10 @@ //! These schemas match the error response format used by RustAPI. use serde::{Deserialize, Serialize}; -use utoipa::ToSchema; +// use crate::ToSchema; // TODO: Re-enable once macro is implemented in rustapi-macros /// Standard error response body -#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct ErrorSchema { /// The error details pub error: ErrorBodySchema, @@ -16,7 +16,7 @@ pub struct ErrorSchema { } /// Error body details -#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct ErrorBodySchema { /// Error type identifier (e.g., "validation_error", "not_found") #[serde(rename = "type")] @@ -29,7 +29,7 @@ pub struct ErrorBodySchema { } /// Field-level validation error -#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct FieldErrorSchema { /// Field name (supports nested paths like "address.city") pub field: String, @@ -40,14 +40,14 @@ pub struct FieldErrorSchema { } /// Validation error response (422) -#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct ValidationErrorSchema { /// Error wrapper pub error: ValidationErrorBodySchema, } /// Validation error body -#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct ValidationErrorBodySchema { /// Always "validation_error" for validation errors #[serde(rename = "type")] @@ -112,3 +112,176 @@ impl ErrorSchema { } } } + +// Manual ToSchema implementations + +impl crate::schema::ToSchema for ErrorSchema { + fn name() -> String { + "ErrorSchema".to_string() + } + + fn schema() -> (String, crate::schema::RefOr) { + use crate::schema::{Schema, SchemaType}; + let mut props = std::collections::HashMap::new(); + props.insert( + "error".to_string(), + ::schema().1, + ); + props.insert( + "request_id".to_string(), + as crate::schema::ToSchema>::schema().1, + ); + + ( + Self::name(), + Schema { + schema_type: Some(SchemaType::Object), + description: Some("Standard error response body".to_string()), + properties: Some(props), + required: Some(vec!["error".to_string()]), + ..Default::default() + } + .into(), + ) + } +} + +impl crate::schema::ToSchema for ErrorBodySchema { + fn name() -> String { + "ErrorBodySchema".to_string() + } + + fn schema() -> (String, crate::schema::RefOr) { + use crate::schema::{Schema, SchemaType}; + let mut props = std::collections::HashMap::new(); + props.insert( + "type".to_string(), + ::schema().1, + ); + props.insert( + "message".to_string(), + ::schema().1, + ); + props.insert( + "fields".to_string(), + > as crate::schema::ToSchema>::schema().1, + ); + + ( + Self::name(), + Schema { + schema_type: Some(SchemaType::Object), + description: Some("Error body details".to_string()), + properties: Some(props), + required: Some(vec!["type".to_string(), "message".to_string()]), + ..Default::default() + } + .into(), + ) + } +} + +impl crate::schema::ToSchema for FieldErrorSchema { + fn name() -> String { + "FieldErrorSchema".to_string() + } + + fn schema() -> (String, crate::schema::RefOr) { + use crate::schema::{Schema, SchemaType}; + let mut props = std::collections::HashMap::new(); + props.insert( + "field".to_string(), + ::schema().1, + ); + props.insert( + "code".to_string(), + ::schema().1, + ); + props.insert( + "message".to_string(), + ::schema().1, + ); + + ( + Self::name(), + Schema { + schema_type: Some(SchemaType::Object), + description: Some("Field-level validation error".to_string()), + properties: Some(props), + required: Some(vec![ + "field".to_string(), + "code".to_string(), + "message".to_string(), + ]), + ..Default::default() + } + .into(), + ) + } +} + +impl crate::schema::ToSchema for ValidationErrorSchema { + fn name() -> String { + "ValidationErrorSchema".to_string() + } + + fn schema() -> (String, crate::schema::RefOr) { + use crate::schema::{Schema, SchemaType}; + let mut props = std::collections::HashMap::new(); + props.insert( + "error".to_string(), + ::schema().1, + ); + + ( + Self::name(), + Schema { + schema_type: Some(SchemaType::Object), + description: Some("Validation error response".to_string()), + properties: Some(props), + required: Some(vec!["error".to_string()]), + ..Default::default() + } + .into(), + ) + } +} + +impl crate::schema::ToSchema for ValidationErrorBodySchema { + fn name() -> String { + "ValidationErrorBodySchema".to_string() + } + + fn schema() -> (String, crate::schema::RefOr) { + use crate::schema::{Schema, SchemaType}; + let mut props = std::collections::HashMap::new(); + props.insert( + "type".to_string(), + ::schema().1, + ); + props.insert( + "message".to_string(), + ::schema().1, + ); + props.insert( + "fields".to_string(), + as crate::schema::ToSchema>::schema().1, + ); + + ( + Self::name(), + Schema { + schema_type: Some(SchemaType::Object), + description: Some("Validation error body".to_string()), + properties: Some(props), + required: Some(vec![ + "type".to_string(), + "message".to_string(), + "fields".to_string(), + ]), + ..Default::default() + } + .into(), + ) + } +} diff --git a/crates/rustapi-openapi/src/spec.rs b/crates/rustapi-openapi/src/spec.rs index 5240d76..ee0b51e 100644 --- a/crates/rustapi-openapi/src/spec.rs +++ b/crates/rustapi-openapi/src/spec.rs @@ -17,7 +17,7 @@ pub struct ApiInfo { pub struct OpenApiSpec { pub info: ApiInfo, pub paths: HashMap, - pub schemas: HashMap, + pub schemas: HashMap>, } /// Path item in OpenAPI spec @@ -52,6 +52,32 @@ pub struct Operation { pub responses: HashMap, } +/// Parameter location +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "lowercase")] +pub enum ParameterIn { + Query, + Header, + Path, + Cookie, +} + +impl ToString for ParameterIn { + fn to_string(&self) -> String { + match self { + Self::Query => "query".to_string(), + Self::Header => "header".to_string(), + Self::Path => "path".to_string(), + Self::Cookie => "cookie".to_string(), + } + } +} + +/// Trait for types that can populate OpenAPI parameters +pub trait IntoParams { + fn into_params(parameter_in_provider: impl Fn() -> Option) -> Vec; +} + /// Parameter in OpenAPI spec #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Parameter { @@ -86,15 +112,10 @@ pub struct ResponseSpec { } /// Schema reference or inline schema -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(untagged)] -pub enum SchemaRef { - Ref { - #[serde(rename = "$ref")] - reference: String, - }, - Inline(serde_json::Value), -} +/// +/// Legacy type alias to bridge old and new code if needed. +/// In new code, prefer crate::schema::RefOr +pub type SchemaRef = crate::schema::RefOr; impl OpenApiSpec { /// Create a new OpenAPI specification @@ -131,17 +152,19 @@ impl OpenApiSpec { } /// Add a schema definition - pub fn schema(mut self, name: &str, schema: serde_json::Value) -> Self { + pub fn schema( + mut self, + name: &str, + schema: crate::schema::RefOr, + ) -> Self { self.schemas.insert(name.to_string(), schema); self } - /// Register a type that implements Schema (utoipa::ToSchema) - pub fn register utoipa::ToSchema<'a>>(mut self) -> Self { + /// Register a type that implements Schema (crate::ToSchema) + pub fn register(mut self) -> Self { let (name, schema) = T::schema(); - if let Ok(json_schema) = serde_json::to_value(schema) { - self.schemas.insert(name.to_string(), json_schema); - } + self.schemas.insert(name, schema); self } @@ -149,11 +172,9 @@ impl OpenApiSpec { /// /// This is useful for zero-config registration paths where the spec is stored /// by value in another struct (e.g., the application builder). - pub fn register_in_place utoipa::ToSchema<'a>>(&mut self) { + pub fn register_in_place(&mut self) { let (name, schema) = T::schema(); - if let Ok(json_schema) = serde_json::to_value(schema) { - self.schemas.insert(name.to_string(), json_schema); - } + self.schemas.insert(name, schema); } /// Convert to JSON value @@ -284,7 +305,10 @@ impl ResponseModifier for String { content.insert( "text/plain".to_string(), MediaType { - schema: SchemaRef::Inline(serde_json::json!({ "type": "string" })), + schema: crate::schema::RefOr::T(crate::schema::Schema { + schema_type: Some(crate::schema::SchemaType::String), + ..Default::default() + }), }, ); @@ -303,7 +327,10 @@ impl ResponseModifier for &'static str { content.insert( "text/plain".to_string(), MediaType { - schema: SchemaRef::Inline(serde_json::json!({ "type": "string" })), + schema: crate::schema::RefOr::T(crate::schema::Schema { + schema_type: Some(crate::schema::SchemaType::String), + ..Default::default() + }), }, ); diff --git a/crates/rustapi-openapi/src/v31/spec.rs b/crates/rustapi-openapi/src/v31/spec.rs index fe6dc43..96ec7b6 100644 --- a/crates/rustapi-openapi/src/v31/spec.rs +++ b/crates/rustapi-openapi/src/v31/spec.rs @@ -159,19 +159,20 @@ impl OpenApi31Spec { self } - /// Register a type that implements utoipa::ToSchema + /// Register a type that implements crate::ToSchema /// /// The schema will be automatically transformed to OpenAPI 3.1 format - pub fn register utoipa::ToSchema<'a>>(mut self) -> Self { - let (name, schema) = T::schema(); - if let Ok(json_schema) = serde_json::to_value(schema) { + pub fn register(mut self) -> Self { + let (name, schema_struct) = T::schema(); + // Convert to JSON Value for transformation + if let Ok(json_schema) = serde_json::to_value(schema_struct) { let transformed = SchemaTransformer::transform_30_to_31(json_schema); if let Ok(schema31) = serde_json::from_value::(transformed) { let components = self.components.get_or_insert_with(Components31::default); components .schemas .get_or_insert_with(HashMap::new) - .insert(name.to_string(), schema31); + .insert(name, schema31); } } self diff --git a/crates/rustapi-rs/examples/openapi_demo.rs b/crates/rustapi-rs/examples/openapi_demo.rs new file mode 100644 index 0000000..72a0fbc --- /dev/null +++ b/crates/rustapi-rs/examples/openapi_demo.rs @@ -0,0 +1,77 @@ +use rustapi_rs::prelude::*; +use serde::{Deserialize, Serialize}; + +/// User entity +#[derive(Debug, Serialize, Deserialize, ToSchema)] +struct User { + /// Unique identifier + id: i32, + /// Username + username: String, + /// Email address + email: Option, + /// User role + role: UserRole, +} + +/// User Role +#[derive(Debug, Serialize, Deserialize, ToSchema)] +enum UserRole { + Admin, + User, + Guest, +} + +/// Search parameters for listing users +#[derive(Debug, Deserialize, IntoParams)] +struct SearchParams { + /// Search query + query: String, + /// Page number (default: 1) + #[serde(default)] + page: Option, + /// Items per page (default: 10) + #[serde(default)] + limit: Option, +} + +/// List users +#[rustapi::get("/users")] +async fn list_users(Query(params): Query) -> Json> { + println!("Searching: {:?}", params); + Json(vec![User { + id: 1, + username: "alice".to_string(), + email: Some("alice@example.com".to_string()), + role: UserRole::Admin, + }]) +} + +/// Create a new user +#[rustapi::post("/users")] +async fn create_user(Json(user): Json) -> Created> { + Created(Json(user)) +} + +#[tokio::main] +async fn main() { + // initialize logger + tracing_subscriber::fmt::init(); + + println!("Building RustAPI application..."); + + let app = RustApi::auto() + .openapi_info("Demo API", "1.0.0", Some("A demo for Native OpenAPI")) + .register_schema::() // Register explicit schemas if needed, though auto-routes handle it usually + .docs("/docs"); + + // Print the generated OpenAPI spec to stdout for verification + println!("\n--- Generated OpenAPI Spec ---\n"); + let spec = app.openapi_spec(); + let json_spec = serde_json::to_string_pretty(&spec.to_json()).unwrap(); + println!("{}", json_spec); + + println!("\n------------------------------\n"); + println!("Server would run here. To run locally:"); + println!(" app.run(\"127.0.0.1:8080\").await"); +} diff --git a/crates/rustapi-toon/src/extractor.rs b/crates/rustapi-toon/src/extractor.rs index 27ea6a8..d945948 100644 --- a/crates/rustapi-toon/src/extractor.rs +++ b/crates/rustapi-toon/src/extractor.rs @@ -140,10 +140,13 @@ impl OperationModifier for Toon { content.insert( TOON_CONTENT_TYPE.to_string(), MediaType { - schema: SchemaRef::Inline(serde_json::json!({ - "type": "string", - "description": "TOON (Token-Oriented Object Notation) formatted request body" - })), + schema: SchemaRef::T(rustapi_openapi::schema::Schema { + schema_type: Some(rustapi_openapi::schema::SchemaType::String), + description: Some( + "TOON (Token-Oriented Object Notation) formatted request body".to_string(), + ), + ..Default::default() + }), }, ); @@ -161,10 +164,13 @@ impl ResponseModifier for Toon { content.insert( TOON_CONTENT_TYPE.to_string(), MediaType { - schema: SchemaRef::Inline(serde_json::json!({ - "type": "string", - "description": "TOON (Token-Oriented Object Notation) formatted response" - })), + schema: SchemaRef::T(rustapi_openapi::schema::Schema { + schema_type: Some(rustapi_openapi::schema::SchemaType::String), + description: Some( + "TOON (Token-Oriented Object Notation) formatted response".to_string(), + ), + ..Default::default() + }), }, ); diff --git a/crates/rustapi-toon/src/llm_response.rs b/crates/rustapi-toon/src/llm_response.rs index bc9c5ac..fc66ffc 100644 --- a/crates/rustapi-toon/src/llm_response.rs +++ b/crates/rustapi-toon/src/llm_response.rs @@ -233,10 +233,13 @@ impl ResponseModifier for LlmResponse { content.insert( JSON_CONTENT_TYPE.to_string(), MediaType { - schema: SchemaRef::Inline(serde_json::json!({ - "type": "object", - "description": "JSON formatted response with token counting headers" - })), + schema: SchemaRef::T(rustapi_openapi::schema::Schema { + schema_type: Some(rustapi_openapi::schema::SchemaType::Object), + description: Some( + "JSON formatted response with token counting headers".to_string(), + ), + ..Default::default() + }), }, ); @@ -244,10 +247,11 @@ impl ResponseModifier for LlmResponse { content.insert( TOON_CONTENT_TYPE.to_string(), MediaType { - schema: SchemaRef::Inline(serde_json::json!({ - "type": "string", - "description": "TOON (Token-Oriented Object Notation) formatted response with token counting headers" - })), + schema: SchemaRef::T(rustapi_openapi::schema::Schema { + schema_type: Some(rustapi_openapi::schema::SchemaType::String), + description: Some("TOON (Token-Oriented Object Notation) formatted response with token counting headers".to_string()), + ..Default::default() + }), }, ); diff --git a/crates/rustapi-toon/src/negotiate.rs b/crates/rustapi-toon/src/negotiate.rs index 302842a..6aa4bda 100644 --- a/crates/rustapi-toon/src/negotiate.rs +++ b/crates/rustapi-toon/src/negotiate.rs @@ -286,10 +286,11 @@ impl ResponseModifier for Negotiate { content.insert( JSON_CONTENT_TYPE.to_string(), MediaType { - schema: SchemaRef::Inline(serde_json::json!({ - "type": "object", - "description": "JSON formatted response" - })), + schema: SchemaRef::T(rustapi_openapi::schema::Schema { + schema_type: Some(rustapi_openapi::schema::SchemaType::Object), + description: Some("JSON formatted response".to_string()), + ..Default::default() + }), }, ); @@ -297,10 +298,13 @@ impl ResponseModifier for Negotiate { content.insert( TOON_CONTENT_TYPE.to_string(), MediaType { - schema: SchemaRef::Inline(serde_json::json!({ - "type": "string", - "description": "TOON (Token-Oriented Object Notation) formatted response" - })), + schema: SchemaRef::T(rustapi_openapi::schema::Schema { + schema_type: Some(rustapi_openapi::schema::SchemaType::String), + description: Some( + "TOON (Token-Oriented Object Notation) formatted response".to_string(), + ), + ..Default::default() + }), }, ); diff --git a/crates/rustapi-view/src/view.rs b/crates/rustapi-view/src/view.rs index 969bdf3..288e338 100644 --- a/crates/rustapi-view/src/view.rs +++ b/crates/rustapi-view/src/view.rs @@ -143,7 +143,10 @@ impl ResponseModifier for View { map.insert( "text/html".to_string(), MediaType { - schema: SchemaRef::Inline(serde_json::json!({ "type": "string" })), + schema: SchemaRef::T(rustapi_openapi::schema::Schema { + schema_type: Some(rustapi_openapi::schema::SchemaType::String), + ..Default::default() + }), }, ); Some(map) diff --git a/echo.txt b/echo.txt new file mode 100644 index 0000000000000000000000000000000000000000..0460275148214508ee7457ab356a6137f70fc861 GIT binary patch literal 488904 zcmeFa+io4n(Jr{IH!$yD_{#PMji%JC?)IHw!w1Yh81CQjh5H9VQb)H)$|5DT`gP`E z_6y8Q%nR(xnTfAb;f#um%*ta`Es7AjtF%b1{Njs4Mn<0gzyJ6AaCx{pygK}NI5}J# zo*k~~?=R{7mfqhU-cSGk89l!^{Q7W8|9x@z+2I$5-yME)`2O&>Prr3b?{|lH^l#4( zAL;$~ha38qcZaL#`|jwszCZkt+PFLXi9Wrczx|b-xAgbl9{zIpGyUDS{A~JdkLpbP z?Ceu}4%IIY|Ht9K9RBaa|33U*)Bpe1r~m(qP5QYoF{G6WW6Uw*r_Orv!4^Qd& zoIZPZcuk-G{NLa9|IknIC-G0AU{?&@;Z*C1hM!3tu!oKeLvadmxpgA>nxFl zTXXuhHGcI;HrEPtQpT6n27hK**lS|)#V{_Vq!NXE*IY9qXEU(%M~82VY=*x**1GnI z55)O3`RC2y!(_L&6jg3bPX126^~2%p@CW)`#Gj{=kHZ?TBOc9Iz`gYz!zh0@7q4UN z$z-38x-urw$ak$f&E)He*&n7TgNU9Nvt$eWp2O1;nkBv3F_{t6YEUAt( z@%|6$`R-_JhrSWDgKO#^&klc|-oB$xFX_`UaebfhywyI_%#LC5^YDCTc7MGjBRivP z^4U~veM|mxN15dqecx}-*|&ZlMiH>SUZV)E zm)d?9QGiW9PZq1i<@5O9GwqD-yYG=c<1%!fm-!22@0hDXed~(;z|&qlWFHd`%n|0% z<8xPbY#L(^^>w10d2$)8t#>Q3%g=Jrjj0{z%Eblg43#H)&T$kUR!=99@C-slVGE;HH%RGI zI$6f&e8bJ?K53q9*8Jq>3fez<)uA+^H`MRQ`VrPCVEs&Ju^Lh98j-Y~Y*bD=r_+r4 zSIydC1k!pmKi~tEv@_xf*0Uxv5*K5&;#-K(@#I!ieK~|DiATQc*mFCrmwH3K|AN+* zK&wxQLl=eMWnT8Itxx@YifjJD&FaRV>gg~e|i zyW_R*`Y1-FpBAf^)Q`QO{Noipw}{*p#uSrtye>*&g<1a?ZXwe#=awSiIpuDzrxnbX zyI_!UTXq=Y$+KHC7M&a1e##9n5BJDq$FbM1*RZ8_849CtKKG=H#B8gQb!lwHrndL&ZP0w5t)`O*s@+LSaV$?YVD3z2{n)U&Mva+tWtoY`M8XNyK#-5YIP{o|AvD6W+Bm(oX7ghb?Q> z^U<@48xw+?&T4qL{>Pb=rOTD9w;J*|fk@pGxyjiK~a_ETc9tf|leR7Sv0jZ~wG={(F0tlw-K zF)&7cO04p;T+E(DXQw?PAmiXCFzP9vLo^@tkMl@rcd_haZTm`LiuLumJdTdK61 ?EkmM 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 O^Psx=v1rJ4KKuvNkCH|J literal 0 HcmV?d00001 From 2a1a8c6dbe561f5ecc8bf74f33c07645e89ae6bc Mon Sep 17 00:00:00 2001 From: Tunay Engin Date: Thu, 29 Jan 2026 16:33:37 +0300 Subject: [PATCH 2/5] Inline simple OpenAPI schemas and update docs Refactored OpenAPI schema handling to inline simple types (primitives, empty objects, enums) instead of always referencing components in rustapi-core. Updated the OpenAPI demo example to a minimal hello world, and added a 'Zero-Config OpenAPI' recipe to the documentation. Documentation and summary files were updated to reflect the new recipe and modern usage patterns. --- build_log_2.txt | Bin 1344 -> 0 bytes build_log_3.txt | Bin 1344 -> 0 bytes check_output.txt | Bin 25326 -> 0 bytes crates/rustapi-core/src/extract.rs | 90 +++++++++++++-- crates/rustapi-core/src/response.rs | 32 +++++- crates/rustapi-rs/examples/openapi_demo.rs | 103 ++++++------------ docs/cookbook/src/SUMMARY.md | 1 + docs/cookbook/src/crates/rustapi_openapi.md | 5 +- docs/cookbook/src/recipes/crud_resource.md | 8 +- .../src/recipes/zero_config_openapi.md | 89 +++++++++++++++ echo.txt | Bin 488904 -> 0 bytes 11 files changed, 238 insertions(+), 90 deletions(-) delete mode 100644 build_log_2.txt delete mode 100644 build_log_3.txt delete mode 100644 check_output.txt create mode 100644 docs/cookbook/src/recipes/zero_config_openapi.md delete mode 100644 echo.txt diff --git a/build_log_2.txt b/build_log_2.txt deleted file mode 100644 index c2bd17e08d414b45a335f8417e33d0fd72560b7b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1344 zcmcJP%TC)s6o&tGrM|;#sHiBZ3cD;Sp#UWt)Ph)%DmYG{7Pdi-DP+S#@W6%d9~W#S z1QG&`Jm)&+eti2cRia8KI#W=eZw6ICRgpSInkk1nMOH_xoI8nh0W6UpBcCG;V15~N z8R041b97SVK?QfA3AZ(lG{D112k4kH2d{O3y+jM}O)R9)epMo|{0F{j?ZBNt?EydP z1Cm-B`l%s2TQ7O{2RPT)4n2mppTF@wiZ3^K8OSc<&5UTiLHVsKV#?N6BCoAcV>{I; z{8h3G=w8Ga5~vJb8gqBX-}xp;UBRiw+-FpJtTpxUd=ss%BoLBgNoZo;7k8(>O89V9!lZkKB7N^rnyP8Y}M0i9X}; zP`~vBxQF!u*k<5JX4;~fp0abOjOfaaMu4*G-PDNfzg2 zfW?C2X`kZjKEEak-xa>S1%{r#W?v?FdwLga`>l4MJOA!vv(26`N1d~2j?kZ_p4B$G z{zILf+H_~rZ2H@5Yd6xzo7&yn$D2BeQNJ#yefnpg&CJ-NyI#e<`kz+Pg9TN}sYTLWG{$TNWPlc9* zXks8Yz320u@6)SaxiU36)wzAHNnG?KB8mJ3cS@N_A;%(_pp#d`&Es^@;CTu^%m|N>Hzpy zkC3$5)q73h*?P^pFTj(7lWpGN=Mx9=SW?SA27%uHZs5?sFfuga>kOf1JTg1d{g8glRM&v&FM{ma1Zn5INoN5b? zpY&NfzyqvTz&-;KYkdIA!1}oNZz7(?Tz5ITCPmE4 z1dA2N)4sshb$(qGz8idd3k*Gf-M-B6cK0sW_I>TZaQ@xNZks(}j=E&i9HBo;J*$0m zR}DqW-^}4(XqLb~9s-?s^k_^*^nq2WzTSQk8B~ cOY*Ixjh`FlH%w~MCfEwt&vjyhx1w^t0Xb9cDgXcg diff --git a/check_output.txt b/check_output.txt deleted file mode 100644 index 0eab9560125a3f8ea4a64f98ca2019fae511d9a2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 25326 zcmeI5TXP$?702=1nSO_@I@6(>+K!|+wv@y(iQ`-z?6h{07dsVciB6PAvPx1(G>QAs zoBn?|I@$}dq<9e}Qad};Vi#E8;M{=&K!5yiH*`WTd>`I~ENtuN*C*jX?{>paVLu!x zKMTL8)mdIiq;;+Z)uJ4J9>Yja>w!S z9i@Ja?Qf{u-_*9Z_TR9Gi`E>|^zo749|%(D&hR4F?uI|Tr+81O; z5t|M({rgN;OHmH%IsWJHt|+^%--c56h0B((w;f)FZzLmKVY#pLKomF*MfGapg;u?nT;kW7#@bZbc42WAs$7aS?n= z%hU(mGrd0)&%p76$hmE`!55tf51iEt?Q>qbE$KFyz6b;_)k;s$zEmwpI^XQ-en+Xk zT69Rzfs8^y~COUNW zWk>BDs2$?akP-oIlv__HyWSH-_#PjgId5<~Am>q%dDUs%TFFhHRt~;+c{Jlzg z9czb=^!|{RL{k`lZ`JVknlv@mXH&i?njJrccmBNUn%?C*zc;e6ISk z)H7~;o9p1y!@Uop?%%4>{Z=B)-)ovVc%zG1LFk|Be}8`yeEnSWms5=$SS34v7sMD~ zqgGBRk(QGkyKLu9;ZZ_$C-s<#+iBGYP4oGW_B80UnO3@rxEJ5 zesy!18yXL9*7ABW7P+k4-SG2j@iTt>XnuZh4fvVap}|=cZA}pKOiasT(2N+~D&}L6 z#oVKwXMAf}IiI+cMWw3osM#U{#8hm?xh?FoGNZ4%MZPR*c@U#i<#-kr|FwSHj})ir zo<$anX46KU(ox(k>FIlda$Ou=j6K;5yyd3-66c^{XyHAJyAfR)1mZ+%ic8*&Q8bHK zvtq^K<#&aXJQmegso%t>YcV?Oc)3-I$c~1m$b*&!8dgdeuJ<&qCfe(9(@amlDj!)H z#(?z_I?}7ft_3JZ?eaKE5nmpSUl$#jX-@TkyfV&=CtGK{S>&`)n1=IitEPF%gvE`S z>9%MtF-bUTxF6&kD1K>CZtH=?a3_lZJ&d%p?~zI^)v`k#GNZ*}^4Px>{`qNQc>q~YPi*=bz zwcss+;j)IgJedU^)`O(rc^f{6IC8A8(y4mQhLx7|$=_9{;i$;zncm-(H8cpkRnvRW zjk4vn3(I6BreB?qwiD}`&*Z`rDq9LP3;6O9rfF=AoMUzLJi}uFW z3_U)lUr}n-wC;kHNIOyAyKmGI9~9U80Pm!J(Y)-ADxxx-u3{Fn_!{s&e`B5@qdHrDB@9FLz`$8luL=!RP8;3jVg-P%qTxf=Py!@O|vi`nS#j2TUH9(q^!$qxEXIp z$|EkPSK5wX1#kL#@%j){h#e&1)X88IwWjBBrlu6`E0E+2{UpiDJiN%5KbO-p^$=)q>0J zB4rkM>Q;VxT%IyuC1pKk!^-%vP&q1ZR7#J9RwpJu@7X|h_}Zx^#ZwW{9$?aPCm$(Y zY|LAZ%wO#6wu|o>tB#aig64TITC8P?`rqWafqUX=<_6M!ljWAJXO&|Xse`@kqBZw- zelK$2Eb&v5d8@UV%LWgP-^AIXe#Yo@`^S)|x^ez&9)izWyUbn}zX^EiM(6G~IaV6K zX)&wAruCaR)i;lwZYs9G`G|PE#g&9ULLhgv?Qorac^*<3E^lwvd(p;RUr*<`qTQV2 zzz%8O3&~ztW^g!H-cEhy#8>Oc0-a%venVr?%T=Y?lyCqUyZv7a_S5Bg4| z{%2~9v!4&+o!Z$IT%13#y4b7BDQNsJ0@9p{lfy^NoOy7nds}xqPh?k5@N5npbvq0| z)K)|vTKmb_AX<*-uvoAM6Jz*GVfmyx@hi zr8zg7@^+TDy OperationModifier for ValidatedJson { fn update_operation(op: &mut Operation) { - let (name, _) = T::schema(); + let (name, ref_or_schema) = T::schema(); + + let schema_ref = match ref_or_schema { + SchemaRef::Ref(r) => SchemaRef::Ref(r), + SchemaRef::T(s) => { + let should_inline = match s.schema_type { + Some(rustapi_openapi::schema::SchemaType::Array) + | Some(rustapi_openapi::schema::SchemaType::Boolean) + | Some(rustapi_openapi::schema::SchemaType::Integer) + | Some(rustapi_openapi::schema::SchemaType::Number) + | Some(rustapi_openapi::schema::SchemaType::String) => true, + Some(rustapi_openapi::schema::SchemaType::Object) => { + s.properties.as_ref().map_or(true, |p| p.is_empty()) + && s.enum_values.as_ref().map_or(true, |e| e.is_empty()) + } + None => true, + }; - let schema_ref = SchemaRef::Ref(rustapi_openapi::schema::Reference { - ref_path: format!("#/components/schemas/{}", name), - }); + if should_inline { + SchemaRef::T(s) + } else { + SchemaRef::Ref(rustapi_openapi::schema::Reference { + ref_path: format!("#/components/schemas/{}", name), + }) + } + } + }; let mut content = HashMap::new(); content.insert( @@ -1011,11 +1033,33 @@ impl OperationModifier for ValidatedJson { // Json - Adds request body (Same as ValidatedJson) impl OperationModifier for Json { fn update_operation(op: &mut Operation) { - let (name, _) = T::schema(); + let (name, ref_or_schema) = T::schema(); + + let schema_ref = match ref_or_schema { + SchemaRef::Ref(r) => SchemaRef::Ref(r), + SchemaRef::T(s) => { + let should_inline = match s.schema_type { + Some(rustapi_openapi::schema::SchemaType::Array) + | Some(rustapi_openapi::schema::SchemaType::Boolean) + | Some(rustapi_openapi::schema::SchemaType::Integer) + | Some(rustapi_openapi::schema::SchemaType::Number) + | Some(rustapi_openapi::schema::SchemaType::String) => true, + Some(rustapi_openapi::schema::SchemaType::Object) => { + s.properties.as_ref().map_or(true, |p| p.is_empty()) + && s.enum_values.as_ref().map_or(true, |e| e.is_empty()) + } + None => true, + }; - let schema_ref = SchemaRef::Ref(rustapi_openapi::schema::Reference { - ref_path: format!("#/components/schemas/{}", name), - }); + if should_inline { + SchemaRef::T(s) + } else { + SchemaRef::Ref(rustapi_openapi::schema::Reference { + ref_path: format!("#/components/schemas/{}", name), + }) + } + } + }; let mut content = HashMap::new(); content.insert( @@ -1117,11 +1161,33 @@ impl OperationModifier for BodyStream { // Json - 200 OK with schema T impl ResponseModifier for Json { fn update_response(op: &mut Operation) { - let (name, _) = T::schema(); + let (name, ref_or_schema) = T::schema(); + + let schema_ref = match ref_or_schema { + SchemaRef::Ref(r) => SchemaRef::Ref(r), + SchemaRef::T(s) => { + let should_inline = match s.schema_type { + Some(rustapi_openapi::schema::SchemaType::Array) + | Some(rustapi_openapi::schema::SchemaType::Boolean) + | Some(rustapi_openapi::schema::SchemaType::Integer) + | Some(rustapi_openapi::schema::SchemaType::Number) + | Some(rustapi_openapi::schema::SchemaType::String) => true, + Some(rustapi_openapi::schema::SchemaType::Object) => { + s.properties.as_ref().map_or(true, |p| p.is_empty()) + && s.enum_values.as_ref().map_or(true, |e| e.is_empty()) + } + None => true, + }; - let schema_ref = SchemaRef::Ref(rustapi_openapi::schema::Reference { - ref_path: format!("#/components/schemas/{}", name), - }); + if should_inline { + SchemaRef::T(s) + } else { + SchemaRef::Ref(rustapi_openapi::schema::Reference { + ref_path: format!("#/components/schemas/{}", name), + }) + } + } + }; op.responses.insert( "200".to_string(), diff --git a/crates/rustapi-core/src/response.rs b/crates/rustapi-core/src/response.rs index e888420..473b7c2 100644 --- a/crates/rustapi-core/src/response.rs +++ b/crates/rustapi-core/src/response.rs @@ -357,11 +357,33 @@ impl IntoResponse for Created { impl ResponseModifier for Created { fn update_response(op: &mut Operation) { - let (name, _) = T::schema(); - - let schema_ref = SchemaRef::Ref(rustapi_openapi::schema::Reference { - ref_path: format!("#/components/schemas/{}", name), - }); + let (name, ref_or_schema) = T::schema(); + + let schema_ref = match ref_or_schema { + SchemaRef::Ref(r) => SchemaRef::Ref(r), + SchemaRef::T(s) => { + let should_inline = match s.schema_type { + Some(rustapi_openapi::schema::SchemaType::Array) + | Some(rustapi_openapi::schema::SchemaType::Boolean) + | Some(rustapi_openapi::schema::SchemaType::Integer) + | Some(rustapi_openapi::schema::SchemaType::Number) + | Some(rustapi_openapi::schema::SchemaType::String) => true, + Some(rustapi_openapi::schema::SchemaType::Object) => { + s.properties.as_ref().map_or(true, |p| p.is_empty()) + && s.enum_values.as_ref().map_or(true, |e| e.is_empty()) + } + None => true, + }; + + if should_inline { + SchemaRef::T(s) + } else { + SchemaRef::Ref(rustapi_openapi::schema::Reference { + ref_path: format!("#/components/schemas/{}", name), + }) + } + } + }; op.responses.insert( "201".to_string(), diff --git a/crates/rustapi-rs/examples/openapi_demo.rs b/crates/rustapi-rs/examples/openapi_demo.rs index 72a0fbc..637a380 100644 --- a/crates/rustapi-rs/examples/openapi_demo.rs +++ b/crates/rustapi-rs/examples/openapi_demo.rs @@ -1,77 +1,42 @@ -use rustapi_rs::prelude::*; -use serde::{Deserialize, Serialize}; - -/// User entity -#[derive(Debug, Serialize, Deserialize, ToSchema)] -struct User { - /// Unique identifier - id: i32, - /// Username - username: String, - /// Email address - email: Option, - /// User role - role: UserRole, -} +//! # Hello World Example +//! +//! The minimal RustAPI application demonstrating core concepts. +//! +//! ## Demonstrates +//! - `RustApi::auto()` for automatic route discovery +//! - Path parameter extraction with `Path` +//! - JSON response serialization +//! - OpenAPI schema generation with `utoipa::ToSchema` +//! +//! ## Run +//! ```bash +//! cargo run -p hello-world +//! ``` +//! +//! ## Test +//! ```bash +//! curl http://127.0.0.1:8080/hello/World +//! ``` +//! +//! ## Cookbook +//! -/// User Role -#[derive(Debug, Serialize, Deserialize, ToSchema)] -enum UserRole { - Admin, - User, - Guest, -} - -/// Search parameters for listing users -#[derive(Debug, Deserialize, IntoParams)] -struct SearchParams { - /// Search query - query: String, - /// Page number (default: 1) - #[serde(default)] - page: Option, - /// Items per page (default: 10) - #[serde(default)] - limit: Option, -} +use rustapi_rs::prelude::*; +use serde::Serialize; -/// List users -#[rustapi::get("/users")] -async fn list_users(Query(params): Query) -> Json> { - println!("Searching: {:?}", params); - Json(vec![User { - id: 1, - username: "alice".to_string(), - email: Some("alice@example.com".to_string()), - role: UserRole::Admin, - }]) +#[derive(Serialize, utoipa::ToSchema)] +struct Message { + greeting: String, } -/// Create a new user -#[rustapi::post("/users")] -async fn create_user(Json(user): Json) -> Created> { - Created(Json(user)) +#[rustapi::get("/hello/{name}")] +async fn hello(Path(name): Path) -> Json { + Json(Message { + greeting: format!("Hello, {name}!"), + }) } #[tokio::main] -async fn main() { - // initialize logger - tracing_subscriber::fmt::init(); - - println!("Building RustAPI application..."); - - let app = RustApi::auto() - .openapi_info("Demo API", "1.0.0", Some("A demo for Native OpenAPI")) - .register_schema::() // Register explicit schemas if needed, though auto-routes handle it usually - .docs("/docs"); - - // Print the generated OpenAPI spec to stdout for verification - println!("\n--- Generated OpenAPI Spec ---\n"); - let spec = app.openapi_spec(); - let json_spec = serde_json::to_string_pretty(&spec.to_json()).unwrap(); - println!("{}", json_spec); - - println!("\n------------------------------\n"); - println!("Server would run here. To run locally:"); - println!(" app.run(\"127.0.0.1:8080\").await"); +async fn main() -> Result<(), Box> { + RustApi::auto().run("0.0.0.0:8080").await } diff --git a/docs/cookbook/src/SUMMARY.md b/docs/cookbook/src/SUMMARY.md index 3f2b6d5..a1c1ed8 100644 --- a/docs/cookbook/src/SUMMARY.md +++ b/docs/cookbook/src/SUMMARY.md @@ -27,6 +27,7 @@ - [cargo-rustapi: The Architect](crates/cargo_rustapi.md) - [Part IV: Recipes](recipes/README.md) + - [Zero-Config OpenAPI](recipes/zero_config_openapi.md) - [Creating Resources](recipes/crud_resource.md) - [JWT Authentication](recipes/jwt_auth.md) - [CSRF Protection](recipes/csrf_protection.md) diff --git a/docs/cookbook/src/crates/rustapi_openapi.md b/docs/cookbook/src/crates/rustapi_openapi.md index a571bb3..b648872 100644 --- a/docs/cookbook/src/crates/rustapi_openapi.md +++ b/docs/cookbook/src/crates/rustapi_openapi.md @@ -4,9 +4,12 @@ **Philosophy**: "Documentation as Code." ## Automatic Spec Generation - + We believe that if documentation is manual, it is wrong. RustAPI uses `utoipa` to generate an OpenAPI 3.0 specification directly from your code. +> [!TIP] +> See the [Zero-Config OpenAPI](../recipes/zero_config_openapi.md) recipe for the modern, macro-based approach. + ## The `Schema` Trait Any type that is part of your API (request or response) must implement `Schema`. diff --git a/docs/cookbook/src/recipes/crud_resource.md b/docs/cookbook/src/recipes/crud_resource.md index fff9814..413ee82 100644 --- a/docs/cookbook/src/recipes/crud_resource.md +++ b/docs/cookbook/src/recipes/crud_resource.md @@ -35,9 +35,11 @@ pub async fn create(Json(payload): Json) -> impl IntoResponse { Then register it in `main.rs`: ```rust -RustApi::new() - .mount(handlers::users::list) - .mount(handlers::users::create) +// In main.rs +RustApi::auto() // Automatic registration! + .run("127.0.0.1:8080") + .await? + ``` ## Discussion diff --git a/docs/cookbook/src/recipes/zero_config_openapi.md b/docs/cookbook/src/recipes/zero_config_openapi.md new file mode 100644 index 0000000..f26355f --- /dev/null +++ b/docs/cookbook/src/recipes/zero_config_openapi.md @@ -0,0 +1,89 @@ +# Zero-Config OpenAPI + +**Problem**: You want to document your API automatically without writing separate YAML files or complex builder code. +**Solution**: Use RustAPI's native attribute macros and auto-discovery. + +## The "Native" Approach + +Instead of manually mounting routes and defining operations, RustAPI allows you to declare routes directly on your handler functions using attributes. + +```rust +use rustapi_rs::prelude::*; +use rustapi_rs::{IntoParams, ToSchema}; + +#[derive(Serialize, Deserialize, ToSchema)] +struct User { + id: i32, + username: String, +} + +#[derive(Deserialize, IntoParams)] +struct SearchParams { + q: String, + page: Option, +} + +// 1. Decorate your handlers +#[rustapi::get("/users")] +async fn list_users(Query(params): Query) -> Json> { + // ... +} + +#[rustapi::post("/users")] +async fn create_user(Json(user): Json) -> Created { + Created(user) +} + +#[tokio::main] +async fn main() -> Result<()> { + // 2. Use RustApi::auto() to automatically find and register all decorated routes + RustApi::auto() + .run("127.0.0.1:8080") + .await +} +``` + +## How It Works + +1. **Macros**: The `#[rustapi::get]`, `#[rustapi::post]`, etc., macros generate a distributed inventory of routes at compile time. +2. **Auto-Discovery**: `RustApi::auto()` collects these inventory items. +3. **Schema Inference**: + * **Request Body**: Inferred from `Json` arguments (requires `T: ToSchema`). + * **Query Params**: Inferred from `Query` arguments (requires `T: IntoParams`). + * **Path Params**: Inferred from `Path` and the URL path (e.g., `/users/{id}`). + * **Responses**: Inferred from the return type. + +## Advanced Usage + +### Customizing Metadata + +You can override or enhance the generated OpenAPI spec using specific attributes: + +```rust +#[rustapi::get("/items/{id}")] +#[rustapi::tag("Inventory")] +#[rustapi::summary("Find a specific item")] +#[rustapi::description("Detailed description supported here.")] +#[rustapi::response(404, description = "Item not found")] +async fn get_item(Path(id): Path) -> Result> { + // ... +} +``` + +### Path Parameter Types + +RustAPI tries to guess types from variable names (e.g., `id` -> integer), but you can be explicit: + +```rust +#[rustapi::get("/users/{uuid}")] +#[rustapi::param(uuid, schema = "uuid")] // Force UUID format +async fn get_user(Path(uuid): Path) -> Json { + // ... +} +``` + +## Discussion + +This approach (often called "Code First") keeps your documentation in sync with your implementation. If you change a struct field, the documentation updates automatically. If you remove a handler, the endpoint disappears from the docs. + +The `RustApi::auto()` function is the key enabler here. It scans the binary for the inventory records created by the macros. This means you don't even need to `mod` or `use` your handler modules in `main.rs` if they are in the same crate! (Though in Rust, modules usually need to be reachable to be compiled). diff --git a/echo.txt b/echo.txt deleted file mode 100644 index 0460275148214508ee7457ab356a6137f70fc861..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 488904 zcmeFa+io4n(Jr{IH!$yD_{#PMji%JC?)IHw!w1Yh81CQjh5H9VQb)H)$|5DT`gP`E z_6y8Q%nR(xnTfAb;f#um%*ta`Es7AjtF%b1{Njs4Mn<0gzyJ6AaCx{pygK}NI5}J# zo*k~~?=R{7mfqhU-cSGk89l!^{Q7W8|9x@z+2I$5-yME)`2O&>Prr3b?{|lH^l#4( zAL;$~ha38qcZaL#`|jwszCZkt+PFLXi9Wrczx|b-xAgbl9{zIpGyUDS{A~JdkLpbP z?Ceu}4%IIY|Ht9K9RBaa|33U*)Bpe1r~m(qP5QYoF{G6WW6Uw*r_Orv!4^Qd& zoIZPZcuk-G{NLa9|IknIC-G0AU{?&@;Z*C1hM!3tu!oKeLvadmxpgA>nxFl zTXXuhHGcI;HrEPtQpT6n27hK**lS|)#V{_Vq!NXE*IY9qXEU(%M~82VY=*x**1GnI z55)O3`RC2y!(_L&6jg3bPX126^~2%p@CW)`#Gj{=kHZ?TBOc9Iz`gYz!zh0@7q4UN z$z-38x-urw$ak$f&E)He*&n7TgNU9Nvt$eWp2O1;nkBv3F_{t6YEUAt( z@%|6$`R-_JhrSWDgKO#^&klc|-oB$xFX_`UaebfhywyI_%#LC5^YDCTc7MGjBRivP z^4U~veM|mxN15dqecx}-*|&ZlMiH>SUZV)E zm)d?9QGiW9PZq1i<@5O9GwqD-yYG=c<1%!fm-!22@0hDXed~(;z|&qlWFHd`%n|0% z<8xPbY#L(^^>w10d2$)8t#>Q3%g=Jrjj0{z%Eblg43#H)&T$kUR!=99@C-slVGE;HH%RGI zI$6f&e8bJ?K53q9*8Jq>3fez<)uA+^H`MRQ`VrPCVEs&Ju^Lh98j-Y~Y*bD=r_+r4 zSIydC1k!pmKi~tEv@_xf*0Uxv5*K5&;#-K(@#I!ieK~|DiATQc*mFCrmwH3K|AN+* zK&wxQLl=eMWnT8Itxx@YifjJD&FaRV>gg~e|i zyW_R*`Y1-FpBAf^)Q`QO{Noipw}{*p#uSrtye>*&g<1a?ZXwe#=awSiIpuDzrxnbX zyI_!UTXq=Y$+KHC7M&a1e##9n5BJDq$FbM1*RZ8_849CtKKG=H#B8gQb!lwHrndL&ZP0w5t)`O*s@+LSaV$?YVD3z2{n)U&Mva+tWtoY`M8XNyK#-5YIP{o|AvD6W+Bm(oX7ghb?Q> z^U<@48xw+?&T4qL{>Pb=rOTD9w;J*|fk@pGxyjiK~a_ETc9tf|leR7Sv0jZ~wG={(F0tlw-K zF)&7cO04p;T+E(DXQw?PAmiXCFzP9vLo^@tkMl@rcd_haZTm`LiuLumJdTdK61 ?EkmM ztFdt`331`XOspF0YlA7C7)uYk(>CoZf>u_IQxZ=|Uc{Clp(M zY>KW@zF6Bm<;!U=ChHr@0P3?H9dRX+H8d;R(@fi5Y1y+|KjBWw4Np>PwLXbab5e^@ zp2+YQ>NBxJv9{(TbttpcTt0bT8@}3B<&Gig&sy86O5oJ&EX5S>XRY?6TduPqdk4!t zu=V%BKAg=@oy977>nz{Ba2Sgbt@!$TBhIh$XrX=FI)c>%^-*oxNG4nF&sSaa(ms$L zIorFA>vLMof^5BgELJ8P?pV1WlAtF4f~w5esaY>kioJH-$owbOQkbFoYCvnuDr?vE z<#k?GFRj$&39a+Kp+7&+XawuMUyz=0=FVG+ll6L**v9&!Y^jWUJDcgRi+H}qqubs# z7jJPs>Rh|s&m%L{_HNtR^61d#DQ9#88s^B5Z;b$%%`1??#@jQvJjvi1H9)p@OE zL1qeJAK~UsS}Mj`ZrNUY@0a@aP#Vm`;KsasTS{rLT@tN}@K!x{qa0RKU{^oZZn?Yg zu%o}%s_qmD%pGI?3S$O;o&fH&t68aQjqK;AnuI#jE7N+%Z$F2H{@<-Bu;P@)Q7gaf zyS4S&Se%#Ol#;shuyX4@zI9%cidA?mt!m41xfbfb<4^sQQd@RsV{x$`Qh!rz;Zno3 z478j8sU??qw8gE%LSCz(@4KA5Wu1@YA{EQW@t`f>c8aBlCTm5R6ap7p+(|Z%I@!Fm zw|LuU(+(D1Be)h95}SP27V5R6acaiUA6cu2>ULI1dyTzIJJ$SKNHA*<`j+*Zbs44P zap^sUeeLy3F@ELeDZIR{DC9P_X1FzXX<;{x7O{x^a9azAW7KP8_sJxDYQ8pgO`Ks2 zi|zJU*Y`tQus2v;`czQ#$+y#L!IpZm(iBh7SCX8`~*g=8K-e8CTJu*Sjs`-Y@_C$daZw} zC$jTZEo%+yy7hF%IikgQhO=&KZVet{&B-+HbxsinXNKliMrD?4SuIQ2W7WO(2>$NS zH~sE{T>H>Afp55@|6KMFMkG{TA7|`NUHV?$6~K`$_j_eNv(&~V^%XLjj%dT5C88yP z1}y)s9~oqf{dZKJcYg&7y+b#LSzw?9i~$Wl56;&%9H>7`Z{&54}HL>~m+FS}~QLLY0@=>_v=`R5Bsmb7#_7Pa=W zC2h6Fy;NHlQx5oivga2R`Ff5xWVzGL=l;x&JKEu~|LEnl3s6tY|!z0el7E3vF2sccbO?vljr zwh!#Y&mUvS_O#UUXK5_0de+=>@9nbhx_kV3dX-#<6I$9xfA%f=UEY>o`#!Ku_m-#G z7+jwR-#$Hjon1YXHl^(iZEH(dFOue)|5r>)X4%#yYw79Ant zSVFFP$7;qU@oUS9;~UGB#MHuB?`UuJdv#dH@VBHtpSOngr6cqfzUc9d;fHPBis0JT zJ(iHGJ@}%>HM+=RP^l3D% z+KN2B1xK8LbS|m6Xo;3qH1e`b9L`cXpWH%joTao)_S_}s?#Rs z)=TF0xdr`1i=j2k7QiBBOJh*j!TAJja@pFIN zNn~{#x;Rp^XN%kBXAXn%cXROe_f~EDTV`*}20tzP%3qyS+mCYV`~(WE8@-O~oYru- z{ahEH_OFeZ&tOdYS&z~8%~)ju&a71q`D-XNve@4(-C8Y=Szi%Dr9Zb&j$M195ybv( zA&ale=(R&2hDLv0hAzH3td|eB;X}p5ovOAEJgvJEG#jzMmuV5++qN4ahC_c|jw?Ko zdZZ?GZxQ!PoQs6|%pI+ZyqbEB@-e4oC-!%+7DHCMYEowpV`gY{D{JN`y6yh?hi+li z4t3qm?Q}02;u>yE$XhRy>os9p@W3CaoxAB&!Emgn>rAyR%`>x{>RxU|%ZfU>HJ?qP z(vd5JtSD&abPFMKR_>V}MIcp4`|T3S)^t2tmUjLdC$ttW%)INQ=eTLb$$6{1LX#_6 zHF_?WytW!L-fr`mla;y9l4~WD?Wg@mw(l(&JAO)a$Zhg-*=l6lq3mHrEZ>S5gMOA_ zs^dfR9mKDX>KI=YZJ2>MIQRmUW+i<7x z{OV9XGPmM^Y|F16>Zv)7`fQ^Xif}b%=qI&#LSFkHiD&ErUR!cKC-2wzh78CmZ?50u zNWW%QorA~E46dz>k(qIs!zaEc=E=%dXc2Q^Jdy9qAI^-^< zFea>N84`)hf81@VE!LQ_imkO`S+2Gm#D+^6mAsj6{bZH;)fPT)Gt{rl zPh0qA4E$W?zg_;&mT)5J9eMMstT920ypS*5{(Cwx54+N8N0=PHq;)N|dh%&E!E8KVJe9WCLzecVk(Z$}i&n%(#IaOT@ zj^U7>r!eh-!<3&({^nTO=X?7EGsmd-X+u=B70FXdb18(f z5ASJB5?ep^#@ZWW=BE|wrAMs6Yp7SQQDaUrer}(x1GMN?W59Rw39e_97xJl3p|xWq zct*ebj7|h@iwV$0E)Je2=`rF<#UGo86VeKDW||~_ z{LWbUY3bHNFm^4dWK+H~N`6{s*I5%PQOS2k#!m}jd<|O9NWmH|MTeP;>uDVlW;(HZ z#?_G49M3U({FE}_UH4*f%x+B5a)wMUkNkTQv&Q+3n8odj?p!2{mvOrb&JVdJuCaaw zb11m$C5+$NHd^Z&8n48i`dPh9ZpXywRR7CA>C3_;?SX&JPs=_u?>`dHPd8<`2CW}o zx1ZV*)9^p&{Iqw(&THbw&~2}Q$5>=+{Ipm_?78~H=a%t?G3qBU>nwAQS)Rn*cG3;n%H@)A z)^OTm#94ziA9G($bziZZ{f7Nszj2FXL0i@`vb?;!Wo)M{(Ds1X%?|x{?pT&)TYU_Z z-&4^x;$+S7Gljy}@38{V?W60pHqW+_HKJ#pz4~>D=v{i)qQ!JR3q9gpO6M^o+E=&H z|CY6ub#=d7XX(L3xg|(k+9s^3#xI+ECf7&9KF--ODs!meVJ%NvyoA^0qS3t6`U>}6 z`4K~EXFMnO*?O>Z5mnCfqFc-*BT{?EZ#n(=kR0XlBCT5cR{xm8dXqSH)*Y`mj>#GG zc(Sr4jO{VM`t~9IO?8eQTu95v)69bf=ka7&@bk%juO94|av4}}D-#da`(F;os5s8Y z-cvlGe(UBO+#EG#f%EoNwazhUKZ+r7HBqdnd`#;YTU(V}>ArB(j(%=a>VZGAGU6B$ zVIJ()S+tD_Br?rP49#3GEn`Y;3GS{5?C{H*Np7`?bo)Fr_3O^or(3-w&$}cWe?-v{ zbxrIf4K- z^stxwmcwX$PLhA5n9eIK`*DzpMeFfVY8$qX_hCiqDUGX_g@kr^Ls8%)~&|SDzWDDxSE9rNwZpl%FlN&u0b5{Ebf>Zk_#0;TUrwrNw~tnP$bGYGRE=bSxX)+osWAwWB|K zrR{{7GH!gU7E8R6x%Ebro|v}NT$-!)({b`xS~@X@`>fFx{hVUl`{8+lj5o}WHPUA^ zE{tXEOc6jsE^#xXvpdpHNH_kF7+G7=e>R8 zjj+gRA@`3|!#$_fNL%n?Mg9;MugYmZhe-0zHY4j?7M^DZ>`;6~eIqJss2{oWL@|nO zKToCx5s&HJe(Dc|h(`vebaGVdK9w9q+IF$efY+^2#_R*GqWa=`wtF-5L>uQO2xUqUYb`&-jUdTPoRHFUZR5eSm9vLQFm> zr}h>Lb>8azmQ0k9#h&6l)er#~ZT3x;h&BMT{-R!%vGn)Z5&BnaI+y8*{6EmQm_wE^5anM!aKA<=43M z-z}Vb*qfyNcgYfa^~Ad88OkpXy<=7m?_r!8!vx!^ zWuvT@`8+D-nO$V+#Z6ojAq)OazcE%3(~TUd_p^#J$?qr*-xnV!k-S$O3^pF?8Q0aj zL=j^`SBflO=x-4<$b7tPyUkX%9edYo<=3*tawYL#64y-VzG+R@zd(JEtHNumjjQmHO-Wz!MJ_(LKL$j zE3I!QLvLhqVOylrQ9ZnaszlkX9Ab(?z_(TAAcP)&2r|2k`@xHr}Pn6}PK zn$NHhI=#77)@{3OmJmFpSy1$t@C7Sk_O?}KCKWg4n4}H&<3hI2m@K=#pqe;lIe(xT zW8ClXe6pvvn{g$v%6Dy9aom#6eigwOpNvudTw{g(VRav7G%tx~tk^{cweGC4M98S6 z_wdj8iJz57!o1`Dit%>L5-?i*w9wxX{IzCX7g; zmGAs>{oLxd7>P~3Ykd57{EoKr`AsseQuAzzkz4g3Z0$J`)k^047uM5$Vg}=tSokxG zah!e!-^9MTlWEP(6?y1Z_gz^mm!jrqI64+G!~6tJohP>SKP{sMsdIjpNcCs(XKhmO zeV1pH-=Oz>LALdhvcP^7c8N752mj2npp<6Vf?R*Pl@@etEbl`{BG>5Vne}{R^fS^u z?<_zL_J*>t{?;I^LR;`09qZSfI53JOiKmpUSdKl8wS2T=*wYN9svKP5X<1Ps<<-{W z+m^CE=~5k+?P{6lRvP+HPq`%yE&m*c8kH7=^v zxP?M~7L#70c~6?f9)g-RVX`e|!TQYdOUcYfVh8I=fC7CyPE7c(bk?!s1%Jm#t7d#) zp-Fug%uma&C3+Vx zk&Tq^f0S*cVoU1xoimlLusbmYy{R&Z3d?yF5D_ zn~(5diE|lKPVV2s`m{eXJC-H+x#yZ%xRjsuWc2|a0r#)YGI~fW+fC^pJ~Hjkf#f9z zlN_J+;H1O?5Q|?>RK6t7=@XHU&jR{$AT1!KjaY@EhXt@#NE|6kj780H%# zvSQ~?G&;fxo;zAwi?vd(D5pW4qUKIoW`mz=`jBihCcm`Rux}m7ghaQtCvn^Sv~o!9 z+fup58vK-D?CDQhE9gt^$Qyp7OqQ!q@RPhV!E&Qts#U(XBCk$S+oC@g8+9vGOY*Yy z+V)&*!Fj;XDB|#n1}ie*<{hl}$L)~$cR*-H^V`$a+3^xOY-`Hr*aYveu60%avQ@v+{v>yKe03W+7n{)+tH9EUr3Rnc{k<6KQ*jpSPnq&h?6% zkxabR@>71aL{guyAVhz}Nz0D-JnH;$^pXY5TXYtLS>p29e9KPO+{L7Mb#r}}iM$Cr zQE(1Oi>2sRN9SyowQ3A(C6)#C^b*}JG8oKBj+sY-eX;bGt;yDTZvtXrd39K~lxp>y z=FWHxIp!X4$|ZI;VMZLY;rOrLt{{y}kEU@h@)fm@UAep#1S?a@=NffODGs)2)$7}h zjB$I(`DA-8isAIhwwpOL2XTAH`d-9soNI*>^zFNNdad5mxTJ2hMM#!HjE*U+_&%+S z__A&5T~-Y=e|f&s5}hqXlD!nVb*!2ZN+c;Ax=dv6mJaTKF+Wd{@-rt=Q~F)oQi+|9 zv?$;#!xoNEFf)5jwJq!zz`fq@DXNTPQnFo!d>%bUvnp|>JS2>AafY!+2Qm8%_1oC} zHJ)W@=f}Xd#M{cc?EgkvODk3xXUABDWOlEXAThQwI$HZ>9L8h9JnD|H(^e+uw%H3| zoIh(E>29Dv^*^_kC2dK@a`~RZz~d=5rt2kjZtJ1FFatQp0yUf;DC5Bh5N~~1$;CLH zA&I2biaEQw%uC90$N5a0NnfSeUsQQIjRMB8 z68C{MSm_x_Ued}k+!*_cR>zFtOZsJ;xBbM9GK^v0Hab^h?Mq(cUTX0gV&?v2-|-Hs zIB-)E&Sx59tLX~FvH7;ovc@x4Z;j(Kgt$kivU7hcm)jTdU~$V;dT()jquEIAq|L>3 zY(>EFHfKfYGK^uAj-ui#4f{*|s<$zAE4gP`5k1R#qHXk0hOtBsYqKx4 zP-b20GcU1f4a8@&^4TA2=Zv18maWI7;OYYAvqy{|HJ^S)C*8xLZvTub_?UaI*<%D2 zr1$f?WGQZ!qw^_=BF>Q2KZI82MwBPhOvgyZB<47C7Ur=Y&A9k!*}u0s3t!0T_)$lk zLCY^{cOk~S0H3E2a8y{Yelnkz4>iIJjEGU1iFmY(72GjV)#!4LzwB>K9M`>RgSKR939yuaXq%fxS8sc258V8+gf0bC}yyIGZ z5A&GYXU{P@AL+jmum?-MtWJA?*O;ZFca$w-h3d_;leF)6BZY+fob~dA5+0E2VdZjP zKG=4x-dUm>&rKeZyo`5lLF1XL$G&b4DRH*MhpqVCSA-qSX0PZgai-#5*sE(jUrCxJF z?mqX3v>wCzjYjcmjFlTN*JnOZb>=Nq&9UDHr?lYgdYtT9bE>JHU6b}2dr#FALoZyn z>X5=VpOIPS+1u{^qW&%AqS$AEnafiB0P;D82skj85zF{JF%ZIjieNsG5v=ix_3$T({3J^Z4a#nb)n1 z(06^LTE;zg{%a)ivGwwi$j8^zMxq}(E*yz`o{t}!r(z~pW|qf>KUKFGEla-<$e7+z z=0>TWX?&FDU~AKtbT(K(8@?k3SEmACA=l6qagpQ~#6^YC%gcTXs7{zMyluAJL!kQ_W>N(m&&Eo=tm%uuH{~8=vEx z2%OhZo|(qbNMvz6NMxgUNwp4Jz#JfM$+EcVWsQ=hI35cD$B9~Nlr6_Cgik*3R$1$J z+#Wxzp2K^C)PFoiw#r_PrPLNRvvNmRC|9jj#ar=bLs>S|t39u`k*&x(bX<$@PWuiW zEm^LmSXLQt*WG{kgd%qNP4s9<>rJ-wmZC1a4|^$3ryEATpuhj(@T)_g+nsQy(H;Hg zqWnp+C+M{|S<8#NG=88cdowh~T1=-~c9oDz>h*duj{C~;B+a!}8NXz|s(I#?c@HWq zszox{mhQEh z-$Tk;?AfY}dmP#O-}TB?D{V-#E|DNU;x^{I+x~j;p<^>6v3ge-n_E1~ijr|0t~&gf z`liPnV~N&=ZXYKa#6~NUhZbG1l~|d8#)I2$Zm9EK{{#6xR{!1{K1}fzIe{Gs{yzQYV`Am( z@CW*RUNwy9QFq@Aidynne%l_m%+DB~{BwTd_tHqu=tjJ6=>ESm`tQr>R_|LHIb06)Qi)Dl0>AUm z^|M8qvHYhwcJ~i1MS%J`8S^BqLVlKuPI6Q~V&s=(=SpJ!=j2aCzR%++XrJcANdu2-aNvVH``NcdS|X(>c! zhwr9r19wDUk$3$0@IMqCYNcc+h*!h;$wid+NQu@9jO- zlh&Avs?K+HpNWc;o5RSPC(fd8Oiy=) zsPi|g%B3|~YhP9zLHz`K-K=(>6YFp3r#LePzlTvoc|1U?sf@@YaBQRA`6d0SVOzE% z-$N~+;)Hq4=Omw-19Y?RH8mV9VXEa<)Y=U(YVqAu&k}=t*G!;IM11J>8TIx#xO!?v zV$)G2#K!UD*N3mV+6SYMo~zMdE@&H@33&*6d5x~!Pni>K-I|vWozzlGtcoGfHhfLh z;46xyh@;n3liQOGhuFAJ869ajuBKNVKzq;0|7%yEY3qpTOk0hG&NEY9_f6r5w~f9H z=TKm_cTKBF#Tn1c9Gzpwo%-$W4SF*nza-1|zgf3##nKvM@~CS)*PWhps}+sb(6ExQQG^`TOIS((cG`%5Hii5pt0 za};$_YMrIB9=pHG|fX$kyV|FgqC$yec9 zIsQ~4LtdMHFUBd=xl6awf%~0wYQaWo2h=?x9zq2c+uzhn=OBqwv+Nd z?x}PAPD+uoSKp?W$oXHX-cl{jeK726Sh|)rwVg<2y;rukonjOFZ(e`OecWwree2vy z7G$^Y`Uk|k(`;K!X>()*O10}Xn&bN|wxo5;S;HA+yJIyh<(F}z6p{00SHr7`8M1Bp zuDvXt|MD!O>v2)>sGHYJwO+0ljx;TtNFBB4rNu&-l@yj`-LywT3y1ZYwwrRz^{Ux1 zWBf!+-HR3_Ce2ulHYH~*)~2!G1od}aGccB%pH@%Dt@=y_ne~z7O=@?lrY*GZJ}yV!Rut6akZaHmFXmpD^+dUBv`xh+n&XhS1;Ma*7P<=XOG8Rv4nR$7>R*G&6%9nYua z&*|M)Qg8B``Ca~8OL;qutg#aq_0CIET8w6{saA7G*5=MRgY{vD?b2+8Y?;v@((rri z=bDocC3EBU@U`ebVyI!Krt&M>WG-U7XiRJ$XJnhWT?F?`Kp&Q!YHYoDl9;uw;`p4- z+=&#ukw`{E#x5y)I3pY8Rev}s>Zebv@fjIQqgC00l%L=^*TR*oxTS6`Wjkcwa#6C_;VB9!OIbBhe-^NHK0_*42XY$|3cm7>| z%Aes~OMsm5oaXG&qwz=ut1{2%bF9rqWf#&R#~q&+Sd2&sSSo(XpIOqee%wj~9)r!b zwa2AwT;_ifRPb8g1J%vrPO< z$;N!(cXV&a8g(NctP4d?n0J=jQrp(gt>!{#|~?WR^eI(b(zKlAZIj<(V+~{ftdwv3_oSCjX6m=ilY0{F!`@{ani&>w#Pz z;q3YgTGO-TxPW-} zqn>+yY^48EkBFB z^Y`;p{+|47Ng}&e>g0pPTDnZ{{Fbe6CAq|)W4I-e9m{lWV(%WzQu7lwp`}kpuKOr) ze9ZcJN$YE;$7shoj>R*vus=Q)J!@Z0_da{oqSh2k;nuUo`WrXncpN=WD!$&Y-X$gJ z@%JPt>5jYCkdkSCj7zr93SKNK#@ELp-uo)-$d!#&ek(I^ylejF@z9Sb5CPpfy`?+B zq;@R?*3YfaG}3aWdBw%H6`tc}nK`sZ()+SxZ66-Fo7;5jq4toMz#`&k!wivqlE?Ym z+q3j*9YOZZyGE5x17q*c zo^}VgTpu$&j>~P($w;ADZP^#Fwne1mVC%pdgG;glw|>>1>ac}XNWAj>zUUw&6W{EP zbMkNsjO_on&bKxHZYCOM>tl6-#G2kaEPDdU1;1NAw?5NZHvfD5xyF60z8jM-)<^L; zj{DN>$3ap8t(6bc$P#-ncpW`k)bh#wOV(li-1LOBEFS~28YAbjh$J}Mkadr} z66tfGmh@=--1;n~dyV!!L=Y_#Yk)^;tN}k`xmEQf<%NF4wzoW7qK)t%6hHiL z_=!G5>ygpr?06qNGR(ngv1r!`#%1~WIAb+}Et%w=ls#nYNRwi787s&8@oNoJIN6J0 zb&8PVxn$$&&Js1pic}nkN#Ntu_@u>DM@G*%-}o3Hd0j?4l1tsXJDKAfI*$N5DAqhh zDaDC}O78Wmv41T&pVDN_jv*(*IrF$@0XOIQJg3}Nq@3F8Jq9No#?*Ot&dYCQPnDm= zq|G@qYlQR?Ta1Men`6c0>9tXXTjpn~54PsQ*0K+Yk2PA3wMVwVNcag95>aSd?(D8` zb}o#)7gWW?i7quK3NRjiN?h`@e0-zWN+ZxC(U7H)9w?a#JyB{>)e3o;gVby)=5mWYEJ)kG&q^$L$~{2J6h<6AW41N};&11t9Z9&A zP$GGfZOS4wa@;P8aqy3H{sQg}zMz_7`0X6Yv6?1r0al!_+IUardf?oe{H(}g#QNqm z`V+F_o9QG!x6gcj_uC1{jKci6}doRnB zwrEI^irJyfVHTpdH@_st@1`|Ep-fty5*du-h&lpo4>)QxrEY}-L z#?PgA>08g2FvZw%@4@zWLb2&5@}@WU#-CDgDA$*~K0<2R({N^Y1;PILx|a4%(SLg5|wGCEb^zmJKt_{^&Q3=&-J{j z7RDfHS1G^t--sFm=9T+fKF*%CJX-RQVm9Q#o#csFf|D%I$RS-5T4RbZBG{5~K zNkAbwS>M1wOQ10 zaWW`YRDB%Q{s(FUH?-f-8~|#bIP0TDSB@>KkI(V8l$PS(d@~_;XOYxf!P58RCLP@D zRo{ZmwZ+xnC#^iDIg+!(AE;gAqi#pDyHRXy4Bt&w0YAPXME^l{3j4(Eb$n)TYDJTb z`IdjjcyT%MlPUV=)n9c>Iwy#=6h;-P0V#bx^ZnaCJ2+JAt$&&3CtlG1&q*(^-{td@ z%lfmooqI2QA2I}&OTVJ`J7G&(^k!O1GVchjqBkEh`ll_cOmFVQrlmfyOm^hBSLF2} zPK_LVJZeLaose|PxJ;rqkis6G77ExqG@6ZH8X>HYVF0;`t5Elxql&;LkV-yQx$ zpI*@4{z^~e0lz)`<%ZrL8(UZ2mSw3*B)lQJKvZz{u?IQlh{~i*N$$sqmi;e9B>Bxb z@7l7KV@0Y0nA4*Co$}V>kxG7V&U?J=w#b&#w)`E9BUK&N+AkSv$TsDBynU`VE#29! z#~Sk6WAD1vb*(;zN@q#PJ#HxGtl^0glg5#<30?u)Y740vq;Fr^gwA551Uc_9DY2Fb zvt}Qum*v<)k;HSj~GQ(?P!Bu|$N%;WQ?VeC2%I$c|v5^*=Wo-PkFnUi3$ILo<_r|j9PdNv0%G>=TqJ1$#i4tI+E%>dUVEO^*V73E=%D&p(avDtJ7MJ zw;OT~&3vT(8~Yc|X^!oD>b33ajchaLT}RJ2uD$aiR}J*xCSPZLxsiIy$B=I~t+p)j z<=an)PurSbuhOnR&KRr=LAUGUPG3E?aMLn2m9ppCPe0j9+V|tK&_0-h7JDth;_V>B zlDk7v`iAx13(qZ$-EbCcou^0?5@o){jLaa# z&_Y^r_81p3I&RI0#8S$Q%;9VE1F0K+XH5JAk7;b$ z>zLqG+-@cp{a-&;^3nCRkF+L2o4}5Wr~mF@+T~x{A4=urnAzI1ms=caFISX-Bh$(` z^MWIejMUPq(vtVHs{A%X>%t0ezoV%p4$IahBWeD}NS|&qlpb?U&^gxeIIX*L!Row> znc(Ldo~CVV^eU5BdwwNUPCcN;x0hQkTNBm?!^dfaj$I9*b?I?gjdxi#^pa*v-3g#P zCUtiN*L=UURn74FYrG(>Mxkf3QZp)=@fsa-3`;Sy)e4myY~xq0w4>np{&1fZf4A&Y zWA7;q<*aUjC0$eezoTlZi~Fdq;$)VR-Mtp#p2$#5s2xvB91^GYv5+lS>pDHSdVOnT zk!|ZfI&3?Pr2Ey>+7XB6aP2)%YoS?d^}DoRZL^%|oRiO@N}JOIkLnK?i`##`4=j7& zV|-S~ktJm**kgygJ`mY)jkBj5t=(}`pT}{sz=2I`HIyU;5*I8cF zvp3LQy@bw<8+p}YPb4W#g{bDcDo5ba{bYz?NpD<9>BW+?>o&dhio>k4GGwf~FwbDd zM&fJNYis3PvZWc6Kij8{CBkJc}6!Fjn310d8pOPI1IkthFb-CwI_9< zWe?*O>taj{FTsMd0^Dr+`dGH9s%SrlrD%<9Q#oiy=FULmkx{>zLK(7j^${ajc>U6O zGu&oQX2-vqvV5=8G(H`XdPT&Z7|YI>voiHH zwa6>B*Bp1|=+MpU;N&~(zj(7d-g5A5VP#y?*jk@D$BU1e!+d^JRMVJHQ^2`YIb5~m zUNTn2G5DOO&;I{ZtM24Ytj};O0M}xGG4a#Nd_!!G8LZs>swfB65m?T^g;s~m*PVh{ zqiZWOlkK_Mkel&qiSJA8gmPh>CsrWC((7i(WnXM*Y#(xRZI@ zN27F^YA3W=w|?85JXmM5sR+|k#(0b=%iL>YPMlZSzKft(2Y-5~+xsB-)H3Uu0gvhQ zsWvTfFV(&uE%C~VoDJfdTHK}@u*Cbr8VlCh&kl%lnlHV#EsQ(7n{Je)E+ZI~ z`)V$h&-3U#deI1QFErx($g@3ie7WXU=C!oMIZaM}KFVep!#-w(W#1k8?^=4%Jsx)T zU@VzG8#_u{xPcNV#*b=sCaL{fBH{b#-A=rkT!$mOZdK<$venQ&IHvVNAW@}|^p#&T z&G&@lM2TLKTm7rY;YaMJbW->?MWc2rX7gugRrBmy-B#ZoM{nu1zLD?Z*1NZ~E(Ygq ze4O6b*1pz543%TUt+i;iz7!KYX2g1-ytQ1dXi`=w+0e*(GNQGhcOdKAc1Bx?jEGuW z%h((`n{V0txhXJ5?mnlRIAAedHt1NRE~)YCVpYkf1lm z%5U5a<4<^KU;AuXkNj*2*I0&@TF>$L9(fQm!cQwBDD(IpIWeQ*XD%x5Yh$tq#=}qG zD)G8_JQ&5=lAUSqTZcEvUnxQtMO}QTVNLfQek>|$tsxI1XO?8kPCl;k)<`HVvR2yu z^G$h3`bwW-Le`r@ZM)HT+j=G)9pVgF`P^Ist#%-p&>olT0iLnC*&NA-i~w<7#`y*F zk;38Swe+)m*BZAW<&M4A9xUIA%|JJ(8 z!?l-))@R>m9S3RM^s`)`yFV6-8(nb%$^Ed_TD?dwZnu;?pH15<-*xiiy@@3EQ#X9J zin|xMj~C@J)i>tX+APoc^;%1nhoi6TGKY$DZ{=rF`mHh$#?Wh>*Kuk9(r={!uG(Xs zrB4JnE*;F}--8x#&xkwW_I~Ii&mO{zf@1@sbE{rtlx)S*!&@buc+{4o`;ebKTCTgx z9y61!8hAe~I9>PH*Eq7TmGrP%&tAJ<*ZMNE;%3qNSZSUg_s#xlM3hyysQR z#a zvASF38IOL#=CoB0thW~E)s>@As{HO+z))Fk@ zj^-0yl1-0WscXrpTXuH)HN~T{Td%c5catnG%Hxiiap!bdQdzD0dq19Z*z=^LVA$e5 z;!_&eoRfzrB5#2fT^;9?c-QhQf?+<^Qb|K+3%I`_4ca$q)MfLZG-xKOf z`aAL~{kBa%#2a} znC|Z_&vAVKvWtDAXuGMejEV7g&gJ02emlOg*^MzrW*^d08J*?3F;w1TzoGtnOkV{& zj7{Rzp0#aOor`k)J}VblJiEpmy}NtM=TAM)@y^k19GzdeS(Ei!ZCjsdYm4J~L;VYS zD!1mntx72Ey%m*Pg^%@QSc|_WTE^|Fss!MnYJgMFWRy{ z4#y>1qT$>OG3S)_CthdeDm4`}%gd}@&_eW{#^gA$qx9S(%bTsww4LOf=|7@sl-rA- zpTG>-+4O&$)wHG!?2x<7@#wtqmhg zoT-UBP}=8)EUc-|tk@OPg0D8=?(i?C9R638qkhYtmp;?+Z9n<3#jr*j&mGo`!EH!U zi*O^&`EA_&avckw^;tf<>*V}7828uT<)GfC{$nAtJ`3#t>-$pGDqklW**9CU46!za z*T>iPo6W8Eb3e2CgtQg-pPo(JZ^yBeF&w>l+t`|GZ3i!=y2(XW2E2vcYA!uqIa9tC zGR>Zqr(*~B8~TH}%W-qE(%!82l={r_vm8le4!;)TmK<6OvAi){5w&&3(^r~wMnAIB zXvtxHrZsgOnMjT#+su(!$Z|i^y6}BxPCa3d*0d~v&j4#DWmX1b#))MEAso%EzEJt=3>%#HCv1^}MX^;HJZVN3;gmxis7T)Vf zBsZ$a=!Ey2m?^!+Wk#GK+$TH9VM1mJp`F~9_#thnlog*jA6xF!B6Zvrj>$uG56Trl zRAu)$?&M)CN*=Ta zc-gs^XulgqdR=bxYWY3cUg}*(!TH>fyUPqGMjUt5Jf-cq#P=ibm-5EmwdTFmha4Ph zy(DJ&(z9~C|338fYztC;Z3j#KAZ_73aH91u^B+Eg)SaYrZuW+_7vq9@N}QFk{vI8v zNlUw_&tl$z&&ybmqw2D=g;MxhWXt`xDGnH$#YcR!z&XQx99gCqna7R$m^n!4b<~tm z!|mqcQIoHq9(TP-N?#To?tea^qe#j+!aijlJ?}%GDs82Vi_liCY0c+3)uV8#0M3?r zLnBkHQNJI3t?Z}Dyz2g1jpU@&dLFV>>>+ZC4g?w*~x$)}5eamOc zdhA#<#;vDv>~P;$dax%wVvyd}YD2vG?Eb{)GDhzAIJ1n4Fv75P;3U^;T9b(XKf9;( ziLy9zzwM@si=wI$C(c!2tR_T=Oo{HaCYuHib6P{73YcZ`YApa{l3|j z?B~lo^8Q9AX=i1el-SvQw=T(IS&nf3ElYBvEiAqUDc`#8$G(m-s|ne;?=})m$cAj*3%_PU+7vOD(0>UheGRDeVILK;sT~ zN)1l=4YSq1PZsl-PW?SQ{DFS|9gSF?QjFkR6_0at%G~f179w-}T)v#OJeh8teROZE zMKU4ZZ6EShVoWiTf zS?R}S>1ivTOY6?~{EB9qzS#Zj0n5cttJkyUQN!At*66X=FH6qPc$~UVUfk}nd(f|r zw&xUWE~lAK+)Mw)ocq*vx`)|d%jQB^+}Z`)*1EsAqV4G|%_*bO^PVa@Z|TjA60EuC zJ?evF0Y5GM#3b}9OdiqvI=ti!F>o{4B}Po{PTpT`+B0as!D&Fa`|QbtcsaK3#b?rT zxPBP()%!Sm-Lj%3=H;%DJ+Pkgh`B6#VPD$_pLBrPLsXva8T~y*CD>1nG;Z{Sl}>x< zDS(`387wb&M*Ra$#zbF%btZ15)_yW*3v-?wp%=vLH2T6l-h)wYPkYGw*p_58%8?}O z*;qe&jT)$y(kV%CZ;Mxu=sp zt2Gf@a~eja`Fv*1YQbaaEAu(fV$sJC9E3 zZtZV3t^cr}Av(x+Ey?^$7&oHd&#k9R=%rn`+uyP8&Fca6xqaL{wj>G0nm5$*EnBA-7-xwI_O+EP($kGV4JPy4{H54F;>`HY>=|ZcP-bm!@tN9Us1k|+WIrn;vLzg&mZ=?vs!24I?lf&iEgO|gxk%r z*B+x)uDL(iz0R~Wq5Z|Ph`rjP^tEOA=Ki(UIH={o84Xy82ko4YN4+BZ^I1lyGhO-F zlkQUbO3Uf|%tgj+W226DSmX4DqJoPJZYKLUv|^9`LTP;L-j;dU>u4{@(*lq7_T&@x zh+@B5(DsiC)v=A>;xf26gSD2AcC>m-TE519yyF?2Z^b3U{3mAMuIUfP%{e2tJ@~km zQ0g&^V1714OY5u>Ip#j+4_K~L|Nda7?#6k~!#?&-jT);@ z4Rz!D7-dq@`Y5xP)AF?LKJPe_(p@;>IOaH$l5}pRdyX`^hxbw;he2)KiZFZDo9y4? zanwuabcOkLS=5>a0|1>P6t=072c6XInILpsZ{M$Spj?u5RWNxK9b}W1_M@c&W zJO*Eun$-R~m)B_!E3UEH^4Mf6)#Wk8W7Z)*;g$DQmr}-3ogQ1{Wm)ypin_<@t6GOX zXUG{3?0F=lJ&+}{^ZQrRY$A3de%#6F!!j?hB8r#C^$a{ez1K0B8jm2VuJ?PrER^m8qZ(*d8rvxhNiT2HYHbF|Crv z>I1C*c}DAe&uC^4_uIHW(yhMS=biv#8m zZ_Rxf_OSo+QDD1BD`iMmr?pMpH(e}8vj;7~TI6WYvYymu@@F+sYwKDYl7Zo>2dLO1a$ibgJ_^%DLZA?)^5hW=i+PqY(dt zvNh~&d`;Qf$&|6-R(Q8h^_uo|zoNh09{zJk#a>$>az)&L_+!@m$S6iE|9wIH+z?YZ ziyOBAteFMI?1|5C_a`yuo&O5naL&RN{lWeN=-u6n`|QwmmUAisvKQE>ectehP6YpY z8t>R=O#}V!4!=2kfB4%}w?Xyhd-5UdwL|su`@`=^woCdu);nXTE`I(;vg5nMpXk#I z`rBXWiC*Kkhrb;DOn=8Y)^43Nem8}*4?mJ8?E{w7ID0$H6h}k|M#fM0f<}nF&^N@M zT`$-Q6>c=nJ8P>)++cqAY4H?Cgb3%x!zZ7fu+Cem+;5NVIcfJb{TYWv$Fn$3TnohN z%@>^z{bTCm-Vg2Tw8!X(;!nA;;@-N^jj{Z)i|tu{fLW3sDE{887RAa@^tJl(_GKU5 z$MecDAQL^IUK9S0{X&RMFR3?tMtvjB`|c5&EL5(?dqb@u)3kRG=w78)WhxJkt5@RO zqkgxQpk-NG(J|wi^l&xlfa|9I4r5n2{kEG%Bo6tmF#!qk#WTv7a5{kdKc5)l_~~UO z5{G;bF}YmsBXQ;Kazu9S;_BmRwNX!7kx1mbM)jO{dcSJClW1b^8l^kc=8~fG3Hjbz z`h)RkPOiBIr&0=?GsKYlnT3A$q-al-xHO~gKIk`Oi5OAmWyouDxs3TeofCi+)L#yb zc#_B1yT*#@fXkc_hcJefzWycs?H7k%HI8Ca`i|k0pKHwdCmjEstmc}ye?$@X4C(L|&tT5$l#?))M=iWzbLj{<2J4`hiB5li`I~O21j+7{l^&X4zsl#6QpW){O9) zTgGE<|KfLaykTs( z+4u6_Bgyr!#qh*6x>hzNqg{zcNBb*lR^p4X$XX3&PS<9!1nnCB5z}a#u`R|=ODNZ1 zitQ~Ydc^H)v{z`RxOY9Ho(nY=obdgMYLffuCA80`5Ia4uQ;SS_s>jH4ra67?+PV+( z`_L}6H;=}swe8mGV7>t>c5+x8Q9VfMS|fBeh@B+;_O2tQXwS`=3+1fEWeMX(mRf5e zHaRak!iH+t;W@x@yey*{&5P`nmeGX%&+Ad-oas2eB--t5N}{RZZmrxgFFLGgX({Ho zZ?t0h4dowa6zlt~;Q}`jP3&FkH-(b*IwFqc;=s@{bVtnx0<6qs}|W@V76{bbJZ57TO#8?ua;3r#D$?@254X zsb!u8!`+zP=_j7e0r%*E!G z#&KAVdIXm-zWgpa%g%aKHgvSYJxo`$4(nw2jqX;?Z?(?V2-L*t);a1dFO->YZW+b0 z%-iI9ITGxwMdi9}wOBqQ>+O(T!h|*0uc@vpZ*L6yFYOaGqXvycYD{~oh02c!zN*?u z8f#z1s+8rp{NfAp7o1zwcAiQO7K;;o&*r+CVy5^BDKukzY0->^pD}G){9TeSzMy^rb1iGfO=ga9^Ronh-dfOJ>oa1=7&$*< zzlYDYpZ!Gr0Cq88hUQIVZh?E$zWqfh|0DO^#~5Qq`5F5?i&^xrm`QXi(cKOpteP0V z7e0oPrQoOjJ#8D_Zn4)J^P(rzf4(7!?ETgL_v>b1fsnHF(4hZA7`yk{_6LrxE4oJhV{kzu~hmuoo6lqV{ai zTsuhl{JNH^y&AGVrzibqca116S=Y$dJ1M1xXB{Eq=WvYVRddGKIoAC@ zpGN9ols>0;^p>L1%fk~|k8?&b`x!;*YpMW^jy@qe&KrJx=o5Qt_|W?4&B8LK-Yjsg zY{jxr2cd=VEL%?|q^F0z<7&j-YI`%Xtb>q+H?(^OXYsxwUpf2KKivl((d@scc<`EJ zeMUW&+Xw2-84elg?J3eGw1#4+*fLO!JZ+9x5`pv@jRuU|5KlQdT(5pJ7u#E^)V$jz zGUVUo7535mY5T`sFX$FK=Psu#G)K#=vC-15{7hS*g;M%L-5zI|`N-DU7p%{$US;%J zrV^ZW`o-!t%(LHg$b&4__%nV&qH-ON^bJPFPfO316^8wUWex2;r^Kc0lYhp~Ti+*tesNNQ-5j(;TkGmNns@$yqN>@hB6ujJ=;wx;chM?>k=DQj?KS-pjheb234 z>E(MR8mY6PR-|>BlP8>Wg|R&zY_F?UHu`YDk=wGKb#1qm55FquzA>YFhOmFMMmyeb z+WAODeG83?aM%IyRda5krMIjyjAysgN%ffFcA4x2^@sSIK2f=5447iWLf0PewQnG! z{4hymX}(vDC)KKx2|d{9pcR-eX~pT=Y2UWXpkL76&MA+^+9%wi{1yFIw-*($PR2d? zu4T5yK_fYrSYq#a7`+dk9@vn$T?sa$fcGR9DB{{&mZXai~zwlzMlw|ygV^Us%q*YUggA>3<({m2$0?9(G{M%%LYr!be3>kvm`x+Q|if=TonawXE1Xv9&MR zeR&S5(H<2;=KRXtalKx4Y@@*%NL1nxuW=jOKG#C#W1n-2-o;vr_^>jPqn!PO*Xx{l z-zy_E+W5-IOR{mSJjBW)uFkjK<0ktN`EIl8R=ejT?sLH6pn3omfDl%wZf9u*gGSPJuUwCuVqiw{p<*?SKZDd5|0i-&jF*?g`xgI$( z8pQF){$~^$(sSZSLT$;)=ty?*@U6|1L{(?)YxBo3#fl;90mwJFhm?KI-fS=IbK2LL z=olt7EZ^fka(p(p6_5AH7Lf;Ekl(Clfw6eQ^TBSsvNxZVF$UFlcYpB%Yd@nZhJCYh z*ypa1jpW3-)VOpFyOS8TH{_2`?`Y<3ozbbrC^gRg?fTl5Q8?DCZIuy}Vhh=m`PFEf zY)5PL#A~9~78>H&Vsym3^3%#)Eu=0J}Co^UFo@zCe`{^_rh}w`liL#I6xp>C0AQ7ZIC}iY}dI#`vMSuFAek46R zrK$1Eys%~Lhb0@ocGM!cW)|VX}S0A>0i>u zQ*kHMri_L?5tXsUm7o4LlwxC?$Cpa~j3VAs+F|9+c*cBKuQ8>xj1;PITr44oXEbAW zA}-;HXEZwU`Eg87&*_KHN9KLt)4S37d%NUr8_$;rQvMc3;_Z5jGMuA&49Q$dcOi2l zGuJXCymzVVHp*VYEk%8Ir{iSZd@GaA7wGHcQh{3LpVRtn){Ts|D5JWUQfGW?q5vYDjt;C zpU%?zWx-{ZfHB-tnoI9Bl1sHd&iAcW<9*|p8nlHWZi{mdZ1sK_eM@n`^87hT{p_$F zQ|vUm`_faDX=pE*gIuT5H?u5@oZdoP=hEcpC8d)waVxj=a3nb{udT;4u0P{ABC(ZO zj9s&6k0y+WpBNjakoC2rl*NWNgZcO?`VXVRR=+uxG4Oh0(0%6`@BHl7jwsM9CFODsZ8VjeX@*uzj~q7>dV^2d92nf z;`%5sFLphh#)K1CkJ<7p4a9q-ajP{f9PtrwKMUjxaX-CLSuEJL77338f{&k(5A2}= z)V~Lieo$(GoDa2+?8~CS?R3k;2a3ZdWP{u{d_K|mVV>^iv@=2PO=!&qq$iZwZyI&2-xG=M#%r?)E-V+IE?Z z*|kc4dSPE|DGRi!6#cSrb-d7Z{WqYS|BUViXJg1#Ann}ma^0)K{ZQ?`+ z+}vJw8mnwE6;ZsDq0ja_qcDa?+c$1l#cdb3Pa*Hdi&lLwI}+g%DyJQsJ$2e_58VF9 z`x2yV+#YhtB{|o#2_1EOtslOCTL-?R(|6$!qx^lIqq5h#mcJa<>cup#hP%Sg$^YT` z+i}h}qHT*}DT~>XA#as_?#IG|I%!+BB|TRA2In15$!6BL_bY`3_pHC2PKdyL94GWQ zoELFJGeYj1dW)S={Ys3dJzzMYa}&YJ$4xz)Eytrt+woeOf?v66|C%>(rR-WFTK5*N z6slL`Yp*`-mv#3+P$YWy~ z!->S9^En$w$c;Qdus1*D`1lsI%YFgln~zgpi5;;xTL(A1xX~Z7hwH3troH4R;t(fl_))B@()AiUTNtd*Vz{ts>wFsN0wJ;lRJ;0( zvMnsuXBr>seOF|GFDVwU**jR+tj{#w3&MFrn#Ky$-f zBjXgu0DhLnEhmGFS^esnZJ5Fwb*-^zPTmuu^SMCXoTp@qJ>Sx14mzn%+*kIC!>!(-Gl0xS7 z6yg~fT{S}<$J}0eaS(4Wl>0sPxNTX6jN{reM@lN&Es+?vytkg&&u#P5%C36Ku6gx! zN4at*)GjJqxRIew!&Y`=x9p<4TQk&R#rZ9UvSK&S(`nZ5$7v?>nzAWZ{j8Th6;1Zc z1lBjbY|y%tJJ0rdT0nX0p*6@%t&jdCDjjde+d_HkAt&!?gn{Y~&-J^V4*&YJQmn_` zGZ_!Jvm1#bPg5aRyq2xqb{Gvmt-fNs{p4vQSa?xrf{$+Sbpt*C2fB|p-y6iFm+Gs1>Ig*gC<+^cy)+Fw{NHEIEB$8T-(=i(si20KT-qPb) zb-jMr*T&d(^BdUdffW#a?Q{FF8QWn#l90J&Pios$p*7qC zb388?;pHvt%ARM_B)JLKkLztV_JKYUBM06i?ROV@Do+$-*oo>{0Q%u>NA7K zR*2T+do5?n5%r-b57)8fTI54dj(B6mm}R@Odh#)z%rVjO=p39f61J`5)LzXVE)n+X zYsPXZ5$o6#i_?4P$?LF&ebKxpACVt3!~C?oERX3Bz1&~%`Q{w;k2B<%8TLD2`}N)$H~+mJe_x)pPuZD8$Qf+weQx+38ik|sRt=w*g z?qMxZ>a5LJyn9#=WS>vEoD=lRkKt`xalm`6AB@kDdm$Z>C-=uTB3ITZ`{c{>bqGDH z9<{cZipdZ2{BNgSukq87AEJ+phq3os+kA*V$aoQn`a~Z;H@)}BGp6& zrU%~fBdZ=LN5q)ExJHbqBtq@K%XXHYeQ1p>d0ra&$8)OW;fw>E`s{Wz-)i5QOTBi0=iQ`_ThhyBB!9IN^}^$YjOT_B%Lq zOwc;fSrf}|p^>NGjn)(2q`+srS$JaD_t66}az6E>J(_7<=0(^wy*o#1{)Ce~*bt+U zH`%CGlEEuTdQ!|Kp%U^v(}#eA$t|8_4pWR%ni|b9uY<=AIsY# zxWz+GHhWE8@s{GHJK^xB$vfUmSwOFIF!R{yX&<#*;P$5*iW9g6)SVB3%8EZXypO*e z4bm;ACH#R;H&Y#`4IqYTABM2>c;PAzv3;`6i`{XpuA|@9ZSd!G|PE- zTtvbhoVeGgbx-9o4u9__Zq;nNR}-zJ`$SoLb@#Z+dj)Z$5pD;r(Y|a~zH2SqeySq8 zo-*T0YAcMwJ#1cLknb7~s(qJai@4kA*|gGWYedipZh88VDv+L(Y21!UoPm2v5uvALF(%0=e^!puiXDz$jJM9oR$;S!vM5FwL(V^k6xvqz z%61vfknQVF){Og*+^*+adR`Obqiv>_AI|M$H0oBqZx0`dUGxM$cIH^~c}+bJ`X5)x zI-xcZ`L8yK{Q1~;Jc)Z5A9u;#q*u##jp+kL^J|Lc7_(p{E;MELlYgIn^D$+YXNNz~ z@4uV=9r|~^Pi=%1>b|WH{_n^tE=dAp^mioj7{4Ed9hns}Vq{y;1^VM>hwmoM!QQWq zOdeRA!+m!6-4urrjUm%IdePA>-zRkLILZSw!jdS*d3MjdR^}4<*yd5LCs#Tu+)}@e zEDHL;?T@!)P3JUrx5gn_hdp_+a;i8s#}7ucb$5#Ec*Ai&r7iIC6Ge`*$p>9Dzs%xk zr2deXA@{`XooA%G8;aJL!{3Ac;@J4!-d`WS?b?T@^mj>_>~1-jgCh>HD*Nw9XmBk?o2GKr47eX z8f(wkDXpGyL#z3CeLU@59b`N)jJi)O;gtL9(R$Yyx34H_M4Gj@&oF1L_9ykZWM_;< zMvq=PlQ=Xh+-~TNy;;aX;iN~!?NLLox5Rbs91M^9`}8Gss2n3a6VuBgaw1<84NJ`Z7{8-2)_VNb;a;EL60TDjGWW?n`vHk9_FiYlp-0MH#Umpf%aO`C z_UenhxQ+Gv< zkJ*nQmu<9atd5h=f^?7K#@2Ou+k=<&(i3)qyr-)2^J#9;&7s$^vX^mTFJ|=Wt$J2C z9@Y^bIKoU8pLd3{J9_0Iy{tN(dG0e(*ICpaBlLWwKGt=NoM+Mfrq#O398M44 zOsgns#(a`zZJ}Xw$dR|j3R7tO`bG;y4=+u%jx~I4je^y>o?9Emfk$yRt(U->DD3{+ z3#NZkw1m}ci3cmi@>U;=(h_!OUsErV!$Pai%FIOjugBry1n!2eTeU7dL$)a2b?aDj zh|@i8CQCh`zs=8g@T(DdRv)Wr9v!}_T91)~FohN?Bd=+W0N!+IYOb}qR*J9$Ew5(5 zn$&R`mWX2Sy4RZXdN%Dc!S1XZiWF=6M+~#|xztvfOx!19UqestwovN%s5DZG@05Pp zGq)Vf{5$!KBVHg@oRY1>-h0JyMxt4jpO-VNbA!<~v0olFce;nTpcsr(UMxH1HKN>~ zV5O)#5A?ipMojV~HO%kw=lRHeEe~0FPhD#azIP96Wt z#E4#7Vqt5sDnr{dQ{rJGZ6?K19OD>YlG@WgrLm-P*3i~QG1v>S&rFbI_wrE9VOXaq zAFH;+trT0!*q9?!R31$nxwF_VslP!z+2yXkY>MoRke`ShI;vW&0#9glI=|?lS4PH9 z3n9)UyP5U^?Fp6Jy|^bt+f~t6d;GMth#U&_61UDihqoTSDG|xOL3@El=e+aLl=q)D zj;|O4Kf#W~BHz=vaIOPt8+moOk^D)_@?FR1Ij<7`Rtz&*sSVBg{$e7dj@eF1ntjC2 z774bGwPVoZTokw3fOkD%=P}My9M!jukps7S;9jkL;(ku`*7uucG%c)SY=@N>ceLjb z`yDYevvu!j$s;3>{JHKCY0jOj3At^zh~C-W#Dm# zt81>6BWPZAUf@SOdto`nXbQc~YY@q;e4_uzPI?``vlRS<-Ro>;zxe^9=BI^zpSb}e z<|mLZ`9Mi-zeawL#%uX0_QAZEP9Qno)FE<>1Ql*Ypb67D9`-*5#4A88F#DZ zx8hXdPjj0Z)vd*{M4Pr-i8icp$^O1=R+16%)3RIs89&Qx#p-E(b?D!(q_tlX2S!1QWmi~-fO~-nSJ)vLC<}y~F8-Llg8q6rAWm*WihxIrxA6|Ew3AY)0YAl?` zShy_aP`RGtY&(TEhLb-};nh~D=Y0G~$nv2zs_ii;a%^l4#k&_rmD$F{nMricm66TUvM7ll-l|;|kU|KXc=kL?m;jvH6lj*^k|^jJ9Fc zy1r0aPw|DW*0C{xS7x z*i+l5pVK{sCti89CpJU~gWAhcg%l(seGou;- zX8&+zI@XA}N)`5yj+>dXdh9p`=L`7C0X7ymO9NsI76Uo=Em0OtJOlz4=KF#j@ zbISZ+16bpP-SMM*j`O-ytcqy~XTn|);=FxpJw}oxHYCwWYeLUr4jcXQASn;Wi`WGDK~?lJ-XSN`3;1&TiuDyeI#}Y6I>|a3W)AUS&z|yywjn zm(X*JicPx5!dk_D(Jc}fv*)#16LTzyb<3Qo;Y8<0l{;q84(r=$Gu})2X!f{rg-dDM zeCs05e4<}Q+cnlCWtQ(++ZU9bpy$imaXAvn)_wDmw$gd>HE~$$`BFdfUF#~10JHa3 zgrY9`$adws#t=WX7~`SVv59P1zH1boiIXYI#LX#Ie8hT&v3y*7THp6%x;t@f-??O= zsJXg5kZ#5P*;MJYcv(|dSD);1YaFOa<32XGG8475Ia^!f^XGT$2g%uCty4ReY}>PQ zMjnq{DPDiL8)j?H);U>ZiKWyH+iUtf<2a$2A)N8?adZ8Ih|)dBx@>)%X3XB2EL~33 z2^Y1-_GK?gPcBMs?M-PUtjp8Eef!I4m*=*9x@RJ7uUlA2J|a}uaH}x($o9UaPv?d; zjB!Qq$K9aYCjO)~x}M*097^Nyqkyq-5$&KJ@^-4><+kcqWL#&==mT_L zV`Fu_4dx9_sV>*Ezq4@3&osVU$`f!t*){!nMm=;+bXp5<8E&49={Fa{dX+fzXZ$mM z!Ul9dpMTHuHg}&(?CZ(*9od__8~yC?+o@KL^{&{PjtthHvwC*;^R%ML{lwj+zP24& zJeBC1b4+(uY|ptWtu>jAGHU&lzk_!j8S1RIOsn~;=`>oFSbokDTA%UH_-XwPKFjsp z{pwOmI7X-A^ApxC%c7qpnhNVofitKjrV> z{rD{uQao6um1!+oCdO&V)?bj7p3^&O8T*K$54QR9LrQ=5D{6k6R$o1%6&+`X|30s5V|FzFtb(pPVNly`&8XKiYmFz? z)H`#@PsFjArhDK$V)9z8_m`2Jy`@SmdTvxkur}of8UbJ?#?2z;t<~>Yy@jo5 zy)I*P`>0PT8^N4GZ#?YHmLL2?IS+P$y*l76$8$%alisQs)F>Ta?skDS@aTxF@r0GV z)*Dk|5m|oDjPWx^zayE`EG^spdc37LTE;o|&%MPn#J{JVD;%&T?S)(Nl!{@?xYBku zQlD-&9oe*h#<4kj$BET_d!v*WZn?iO&O~TAbDVug%$C>1u}LlU zwBeMm#nCJ=<)&Ws zT90O9?mONhEN<2qF)rcV+fG}_t$f!!`%&9onN1^Pza3MHUDCR?Qoq|gjBzs09~Fa_ zG|emSUsFBn<20Vzv(_1{+o!nI{s76Te9zC3LR(lXVwHLOnBDG@GYe0=&pVsWlDwgr z7R-rV4;Z)PVw1h#w!UlmtByi^DpcR>NykFTo%V%T2C)WrDJ-EUF7?~vI80@uYx7s; zm%c|H#}2G5rO7osGSz-)N!<7Msl(_jhB?VKE!y z#*-GFcn-Xcr!DoqmKEJhGpeJ{hLV|4h{DJ2p7AH{!zZ+s88=eAF>MK}rf?r9>Ut+s zyTc0oSJU&7{!P25$v6|YDv8ng8+MF;(>!-9TTl60tbZMKB3~}93)00Mt!zUr zutrv+#_9POw>U{K7u`~9+9RVWf8AQ!jLn}9utx@c1jcUqE$WE5_9>BWS3@jEo1^vC z8Q&Nx8Oh`Q9>;DsFVw5AjbYL_(1&8}7FILh?V93u4re29#`|3hQUClzau&xTbAT{w ziuKTaGn72bl1OE?vaaqVijXhM{_ZjC-7?L)hhpVS0X)n_!Y=`#Z zPOHETS?)aJ*4S;0&NK=q@hQ#PyR#)){ij^&c{asop0rzmRZ|!(eJXb8wnP1+0_xTs zw@WW=e@Q33ZGc&j7%cybS65JV@h1wk*I1Sq2z9}+>QRUB_tuuErT1sVS&>*u-WmtUT=?@hY|dj%jL#u?w+oC*_PVvX=`$5;l#B3`=V0*#cSzf7+Dv{ zKfgG9O(XPQ9DaBB&EfmQ-=<6$`SACY7vVl|RFS?v{GRaQbS>Ptfitu4^FI>iyThO8 z(+m3BU+D=;`t9K_bhg&_^xHU5#ND;>J-w$`E0J2Ix_BFP5C276Y-G zk(KGKp%GW#MN9id;BTTW&A(X_{IoRU85n!R2xH)hsa< zSm6_sEd*numY04{nn+{m%ZSeLa4#-yImE7N?5nnKD(P*RJ?~oP2*6J(5`6A`b!xoe z@>bk&>F0%VN8ijjKf!&fDkJsacC>v-Yd~;wK##sCx8-oWi~FMcKP?=(9ue9^@0bS^S`*7Cd?hB=Lzc#fH#C~Z$kvUR_b^hN zBg!8yYDr~u>8H=KR?2(p@cT!NtaM90SGO{;TKg|=rF=vZZ)41;L$@bybjRwC^Qk^G zl0{urdQWYllJ=CI=)rT_j#uM)#TQiiP!YSKta3Y4bAEc~wK|UHw^nBBJ@RC2yV{bt zPPd1ifU!LE0jUg4dRTt8sO=!#TOuPqp&B@9h}eyQl@l+?t1)+ivwLcz#nY*}?oLv` ztbnVr=fsKJF^QFD#aeZ<131NO`7RmFpY|iEl^l7%VE<_Rjx>f|As0bTU0zF`YQyIg zW%Fw~qWU)6IPvuGZP7Tf1oL~+e!WFw)Qt1!-jHWltYh63dc$k_uZ)vW2H6r&V^-_e zbP8XIZ=^=3sOBT@`*E7Dv-gwK=7q9d`L3nR!BA^uvR(PEtuMszeq!3KWIN4{ezFbu zu4VS}SQa#z@#WUbvKB;ODXn~uOFkxZmu<^;t#RqaC$y3q{ZfuDYCKQYCGv1MUw7x_ zqTg^;l*_4y=4?BEW?AZovG5Z=(>k(Rdz8gaZk8Z5!|#lUpBA!zQEzuc@i%|`lhfZZ zf>NGb1uw^HM)ML!g4pgxtjOqiqbhNeoCa3qSUwnOyYgZwzgc{WfipQYUGCN-@iCPGL{k zS(yywEwK2g-8)G8jE-E6cAYKc||5 ztI&n@i~4VkkM{I;%2AW0&v{hTeqH))OXFZ|c}wWLFG*yuH{755mPUctCp5RIEvn>k zG=9vdxkMa~uMuBjvh3UJ73Zbj5nHWV_C1{8W2L$^stLcXb+%Q1qt8<VUTpfYeg6Fq`8ryq{u+Iq%W|=TzSnkKLkK`lO)N!6s4tkwG zSgg;#(~l`nX=M3@SzXtC&HNj}+%q;VOl33k?~I|+7KEK-SleT9gB%1WLB;MDjenQR z3vtrMR~hpSvF|w}tHb=%>uF?&wWej?%cDa}hi-3|%OEY;mm;tH?%2DQ3mQVZA4#IT z8l+{7vDW10OD#+5sGkqAtU4`U%&ws!iQjr(vVGZdy?im|++6mR=_lG&dTY(M?ZzcI z3++*-yYOQ2=G&<5qb%L!=-2YnxyNA7FEVXpCOJM^tB05bX|CqzoTf0}`nB03#M&tR zT>BtKG0&;8xh+#HMapL_q@K0@H+N{J%500#rmI1@cAmpe3ZLi6?NP0rr_y+}g|pOU z*0g3sz?>d3!H=6nge9Aj=v<$K`G>E%Mg-|&T7MrXd%_wGWWTsU60>UdI?KOLzv=Fa z{sa9!b~wA$qB#Eq_Zalls%8Iuuzxp6h|$3nonZ633HvSm9~swp8MHm*^RIJ=o@{Go zb!2fUk?oIL#h@b%TL3=Stfle2pY9RG9NuZC9EhFX9k?O*WoONxh>8GlBR@io1@nPTL}Pt}#Y>QvACnthL-R$R!#?fTiTKh5&@z^HA} z*F`X6fz_PgF-Oxq(6ELrhGNM4czAi`vi$zoyS6Co3C4MG$i%LuOac|uZLBI~LHRiQ zSdq3Ijtzc9AJ+abM~Av@pOJ>vRES_+Q)rDv%k6~{%lxBG>Xgn$NUWvgD(cykXa3kV z7Nleg+jZLfnzExOi=&LV6v#$-rl>Wp<>8;V=+(n@~VB+t}gmr33lR&JM{h?m+Q zt#9qOR+U@kr-h*ZnpAF$pH{2!)s;2l5N?T|Wi6Ji=j1l{iMF&Iza;Cur1`sEmdkDN zGu@{1K+JvQ%>{WM*0UX9r1brJ7$;Nc;%#_O!%U5i{@P#oduo}De_tAh%tPYek>*Rw zR{NcZjPC3cw=WH+zPWYMp-x~adF|xOFTYP)KIm8}SF6{?iJ-Vk%367!!&k2E<#)yK zh1`d|Ru0~}w$ff5vloj~IhL`#D#pMPn;{C+#CU!qhDEOXwS0W(TU|zbO7m8EHFy1e zVcjkVY0AQ76w$5WoRbU6|3|igwP?u2q3N?}t_(Fm_xGH3EiFiy>9$KG)ZAfK(`)L4W<_!O0alTY)>^7%=U?vDlU)9+F=(od zP-B<0YvPBDUZ1TbrLk1YCDLhq<^&~v4& ze@8Zf6}m2m117AOeRhzuxA|IuHRj@)+kURptp6Ol&(p%3fqYq|J**?MY_W$0=C5bT zm2b(`{M{nq+M1H+my2gVHs&RNurSy${8##Nu13=q_BXmAZ|P@G5C5J1j**}>YSqYUQ0~RJe{s>xXLL`8|3xL7z4b>uo=ylZp)$e6^~SQv-DkI z#EXxXLjO*2UW!^XjQx#Rr*lJ`xmDCTV++eda=w{4btkY`-;#>dZbk2#PwUh2R$HaC zmTK!K>Q^ws_v(PR98PLm>a%}U$|B#jRBu zBI}z&o7IsajG;0kOtEgYmoJI78tdCa^ys;8_5gcHh+ZPj(Qh7N`K@)x=TS0_yR@Y= ze$9Z}--4PhPBwo<{X^WjTrbjX#+l zG;+_9k0aXSV`y`}D@4oHEUs=dQhp+iHx;=Qgl<_v^dRej` z`JM7zfB#%RaXD(+(^^Y$=JNL&(u~X9Sr7b-S%>_&me04>&Yx^Y%eBO#l&yqI-Hw+h z)YUwjU*<3+k8h`pH_gARB%nWc6(5}3X?rJXLC-TauUWBMaVJlpmh8?0?MZ&7 zW#}K>*&|b}f-^AU*2mhMp+@7+2wsXcvNc8k4m6v(WtW%uDhz)QKVv-V@3pk#_GBTJ z_mof~D@Eb#9p{yNJIz4wckvU_Y71$XY$+z>Nr4RESp%0+#?lr&Z@5RvvPJ%Oey-0X zx6Dt=`Z21)sR+He)hAQ67VFxv8ruCnV%;OEy17%ves}oI z;d@$@;YPZ*^nOPfFzyEaNbkR=vEAiV^~L&atW^E}@W&}*{u6zAL4W%zJ(0)!_V5>4 z+3~$-4^jMkg$KJq+)D75hxp0We$<`g@6szQYgzs_-W5O3^ei3t@2WpPxn-0l+kZ#g zqN?k6E#!@^miTgK13L?M`^a*%0NVp&vd_>F(E4U=!}>c?-?#1_6hD@*_~2Y#vn%n- zcg>C0to7}+bNKOA&S)%-81{;yLSDbxE?K?LlvstEO_|?qWpUc7vGxIrQbbkR&&{m2{+%W7rZ|WQ+1W)w<6nF;DXiWjMb$^g1VaNUVS6$Xmr9gs$!Aw7nOa(0;xeA) z)H0fqS2N44OMXi=DBG)-R_1iMe1~>}{ygoyHGC!?;*Fxaf#SLC1_cL%S{M5aymi^geTi6@p zRxh}HDRnxPZR&O`2}3Qr9kf_YS9S|Ex2&IaT<3bKw&|SK=X@_S&=X6WPGl^rjiweu zIjH8okjsgVFL^dt8)?GUaK8!9wP95)7_@q1-C{1gR!)Ugg;uMMLCkQU7To{D`m+3J zixWz;$MwVY9cc=+&h)7zxt2E)r$6^0Jn^ChpZ3Fi{c8KoMWHz>%1Lx|NOh&Qof!$D^+U= zIsT%h#OeUI4+gWyd9%m4kyUcR*sN%~jHy)e;Wb^!&_|oN=^B%q{XW zujO((sEtrs-xljw&4yXY^*nr}G)}8n`67L-EM6_gNog~=(v^$u4p`fe%ampXn8`fUYk+z(_+O! zhn)>LBhb#6G`iA0oYClKE_V{0i&K~lDY@-fx2*r(ho@dOlOE{Gc+#;4)^JU64tX?o zbzzSj-g9g>WgRS;#do|-=dpgT5dJm$rF`da=O=!qeKTE|*Os2sOK@xav}E(G_FKcI zC9_sXhSe>sOnOV%>J4Q_el0CBrFW!f-gm#;18a-1T6Z?+))%Lktdm4%59?yikw0YL z_lhCfl>L;lTZir?`S=z2`q?x>W?SH~aW8y`4|q2Jsdxx^D+9e;e>TX9@Vf%P1yLY&j{)h@I#M>Mx4OQMTU{cTze6i{I83eh<6_v&A@*F(;c?k`rxX#|feb>h7E?_V{k! zYp?Tde$~;E)E-$|KB4u49^j5VsE54#U8oY`7L1RL+R=Sl`#Fg%J89A$v7LXVyx!lE zi9UVF#?lhvOa|P$`Hu1hWbT;p!v1@Vcb{f_yN=&J0|>Jwj65*5^4`rk2_l~y31}OM zx3Yb2%@Qy74A}XJ-h_GR;hj9P!})921=iy`HQS80EZT#YEBfPRp-u>$tLBD0X>Gfx z=+3WoH&4TAawJ1`C-Yk!7h~V-c7LOiG(W{c+ok?KcbW_OXY_-(A=s@YVynbh_p6zQ zN_5iV^*7&A?|Mo9(w%}4?x)ts&vr0oz|FYGn=l5m807X+`be>-WeeMMb`L*#yx>W) z9r>>PK)1=!md9x$+rsHEx6@t2?Q&^P(Q^5GcivWx4c74&_Rnk|z1#G%^=os9Pp`YS zHg!984^qC1Hs*selGU{>`!ZA$2( z{*vYJxAN2ab_*}QIn<-|oAZ2Fwo@V@ZYQ#52C)Yhy$DWG#YthPUg|8of2)WV*mE>m zIzFAwCyal^@@jjV0`SoeLc`e4|yJj<%y*h^R)|{?{y2XTpDB)9 zT`pIb-JaczSn<%!>`dKUuA9rY#Ma2B9~BFsEdzdQeZcX`qzCm=@#VOgc7^mKSXxq+ zQ){UsIJi4|>zTi`dMoQ+wQjkUho^VqIp=GAo~P1vU*B8x{q^eez2dc3P3bmX@c7!U zu=$+bEd5)G_BnWeN8$(2u}>}aS)%;AAnz?j84j@H02!KC^xKTt5RrkFe#~YgPFuse zP3lR@R_31HvRrKE$XytTEk*krapW#aZ2>h?SS_qG)F{C}X~V_|m`8|6{yssNTXp@n z;Ke^r^3{{i#0Q?0E}SXsq&WPiQMo2@Y%T?aTy(`cufwM>4LGiKpo5p5pyxnvFKZQ{*d z%pYzutFttHzllhFF5Z-g825MGen$PdNE5W&XVem7p7WgkxQ;iPQLpyhQ5Wo}cZe<3 zh{anSQB=#c!VB(0>*w^=OH^2KJ~}!U^wcZ(C9G9m-^w5tGw(^jdLvXkp+^}Bz+UT^ zyQPq@xWiKHhW#D0AkU9)?}L(?&n`~V&zj)7I6YCD z#Qj1udrY!;Q5%w*;W8tk>pEEsbq<{$!~87Ga zL%p^7o~~-x`rX>zcV&l*oTx2!5|R4$);C=p?7eF$d<@VtoZk%_gY|w!*2nNgcyZz| zxI@1OPoiw+XymIjbS*>g{g=`=gSJ`SiTCufZ8BLhiIy6BD0(jB@7%#uq+QufML- zX-%QE5W=^eSNj@1JN8;4nlAmGqj5X;SB{jXMZa5}&B~4*_FOX4GR@I=GqUT^BAPA{ zHgZURZ;iF7E2Wo_m`CMSZs(v#Q+8M80cqmX^CnSj_TjXOk8XRMpMl(C+mEa*XYZZ3 z`PARzXZ9J>DPxKAl71TYryeF+3i~(v|A?yi2Be#q5tdr}|19+)-N!|`kCsTcwIzrG zkqKUuI30O3&TYFs^_`qEcFD+tdp>;HL^6nE;+?V7r;MhPDty;kjW{2tZ(8L)NeQ36 zzC;xQTQ`w9MrNmumXwFLjd(pxp+qv)J>t^CCSx!8T}I+|EH8@<#oh|ukm%7rYbERq zYjIWTbDFEAV>x+IX6wf?TTwx{Eb}(p($VEuRVxKYAJXcmSu*I;CnOT$7s6GzSqqzvp8`n=o zvZ$-zZgEsnYW;|3kyDQNwWmGybE~bW?!P^a-@5s&>FCR<)g(n=8CRFgFZs}t1fXJ6}j+iHi#nzfU}rpL7>*)@E{m~iUB_6&sa?oy3` z{Po6Bf7kLo-(jku9-&~a%XAidU)x~yvAVp{LsKrKmQRdShLRQ9PMmy3xHG)Z&UNCq z&vUKZSUmi{$G@zNVHVOCU^VXP;ra`4@(J8_2EHu0-0a=98ZLDWIJ|d$xA*jEdslLz ztxsN`acYWt)bVqC*UlVdg{V2Nj;|?2;@*3D65P_JwJVM_hSg+{ca;T7^0nPEy4B@@3Jvt72oe zz2l@w)6#qAclVz9_x8K^o7Jk1rx^hnQNOwkAD*FdYYDeL<+~zP?*6?f`i1kh$1`tB z#oqrRORMo)Dmf<+4Lm85tve|RTaQ)mo!`wqy)oX>)k?2t)6XwOaIy(L*gWIV>tde1EOh(oJQQRhB866Higo^Ecb^W zIYxLfoYr6IHQud{(hwk=@Ryu-IM6f0eAcR#tCs z2Px>d-^VNaOm1%!Zg(FqDht=8aws|4@5Ls)EcuMw2m))EiOY2y9|_HpUJU7-&cqx0P-h8TDJcc?eM`_Ndk+1Hf{YkpU$N2A10D`a=RQ;oT&d*-!Qx#sybP(hsX}2q;v8aOj4J6SZphI zBwDC32A}>hs1b%^=Z0v=y>~0MxqGG}scFm>8*Ko0fy~SV4Zrtp@k)=yoTuvH(4Sj; zVdHDP}>)XSoKew1!$GIsP%ehw1i@k;I8aG zqC(b!>Ny12S*mf{>^#EPMtyn>W)!=wvfL!s8t*agZKZxz0<#p&j=$v^8N=(M=Rc<+ z`>bY1TxpErO`jAGEo(=S6s?}bXItUqo*FH+k-we?`rTq#?=vlik*p2-g|16KmDDgJ zX+=EsG0J%GenFcNYnZ)vt6zOwtwpil9p^UEI}5$u&^O2R5l(44BMDnQsro(=UBh=> zmw!@!+Si*ItlOA4;z?^wACu?h!GTxp(Gc}}ANq*&Q1iffMiNv0QE1qg;87~(q^Yr5 z&CKUwNBGrqua$l#o62eqd7Z)7TAhB>lOJnM38{m1&W^;fG1u6$l5gd}Jm%Zvnpx`q zk^8KcwPVg)d7Nvlb6NazTHZqEn!t!9hsQ;i?eo&P8gmaBn{6#=i^;dg zt%lgzJ~99;p{%{>OGQJQr*lsV_v`Y}{R1_&^kT1(b$NKbRwp=XJm}~f!=AV{N3+@> zYpKWXmjkDWh_NyuwVkV?&!U~!nftJuUMC&ct8ZVc_^^ZM*J6)^l4pvNe%AoM9o^ex zXT2SVy1M0!gPrNeq4n3@?8sKi=GWbh%y8`XkrfI&v%7q(ge+G%sEE7XmJTbNy!EQA O^Psx=v1rJ4KKuvNkCH|J From 9b427bf51d920c410a9fd9546b7910f693c9367b Mon Sep 17 00:00:00 2001 From: Tunay Engin Date: Thu, 29 Jan 2026 18:11:17 +0300 Subject: [PATCH 3/5] Update spec.rs --- crates/rustapi-openapi/src/spec.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/rustapi-openapi/src/spec.rs b/crates/rustapi-openapi/src/spec.rs index ee0b51e..9607fd3 100644 --- a/crates/rustapi-openapi/src/spec.rs +++ b/crates/rustapi-openapi/src/spec.rs @@ -62,13 +62,13 @@ pub enum ParameterIn { Cookie, } -impl ToString for ParameterIn { - fn to_string(&self) -> String { +impl std::fmt::Display for ParameterIn { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - Self::Query => "query".to_string(), - Self::Header => "header".to_string(), - Self::Path => "path".to_string(), - Self::Cookie => "cookie".to_string(), + Self::Query => write!(f, "query"), + Self::Header => write!(f, "header"), + Self::Path => write!(f, "path"), + Self::Cookie => write!(f, "cookie"), } } } From f817aa02577f123f6955f22c8852fc1a83ab0510 Mon Sep 17 00:00:00 2001 From: Tunay Engin Date: Thu, 29 Jan 2026 18:27:50 +0300 Subject: [PATCH 4/5] Update OpenAPI response schema reference for AuthUser Refactored the 401 Unauthorized response in AuthUser's OperationModifier to use the new Reference struct for schema references. Also added a use statement for rustapi_openapi::schema::Reference. Added initial release notes for RustAPI 0.2.0. --- RELEASE_NOTES.md | 66 ++++++++++++++++++++++++++++ crates/rustapi-extras/src/jwt/mod.rs | 7 +-- 2 files changed, 70 insertions(+), 3 deletions(-) create mode 100644 RELEASE_NOTES.md diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md new file mode 100644 index 0000000..acdefea --- /dev/null +++ b/RELEASE_NOTES.md @@ -0,0 +1,66 @@ +# RustAPI 0.2.0 Release Notes + +**"Zero-Config & Native Attributes"** + +This release marks a significant milestone in RustAPI's ergonomics, introducing a fully declarative "Code-First" approach to building APIs. It bridges the gap between Rust's performance and the ease of use found in frameworks like FastAPI. + +## 🚀 Key Features + +### 1. Zero-Config Routing (`RustApi::auto()`) +Gone are the days of manually mounting every single route in your `main` function. With the new auto-discovery mechanism, RustAPI scans your code for decorated handlers and registers them automatically. + +**Before:** +```rust +// main.rs +let app = RustApi::new() + .route("/users", get(list_users)) + .route("/users", post(create_user)) + .route("/users/{id}", get(get_user)); +``` + +**After:** +```rust +// handlers.rs +#[rustapi::get("/users")] +async fn list_users() { ... } + +// main.rs +let app = RustApi::auto(); // That's it! +``` + +### 2. Native OpenAPI Attributes +Define your API structure and documentation right where your code lives. The new attribute macros allow you to control every aspect of the OpenAPI spec without leaving your handler function. + +```rust +#[rustapi::get("/items/{id}")] +#[rustapi::tag("Inventory")] +#[rustapi::summary("Find item by ID")] +#[rustapi::response(404, description = "Item not found")] +async fn get_item(Path(id): Path) -> Result> { ... } +``` + +### 3. Smart Parameter Inference +RustAPI now intelligently guesses the OpenAPI data types for your path parameters based on their names, reducing the need for manual annotation. + +* `id`, `user_id` -> Inferred as `integer (int64)` +* `uuid`, `transaction_uuid` -> Inferred as `string (uuid)` +* Others -> Inferred as `string` + +You can still override this manually if needed: +```rust +#[rustapi::param(custom_id, schema = "string")] +``` + +## 🛠️ Improvements & Fixes + +* **Cookbook Update**: Added a comprehensive "Zero-Config OpenAPI" recipe to the documentation. +* **Clippy Fixes**: Resolved `clippy::to_string_trait_impl` warnings in `rustapi-openapi` for cleaner compilations. +* **Example Updates**: `openapi_demo` example updated to showcase the new declarative style. +* **Error Handling**: Fixed `unused_must_use` warnings in examples by properly propagating `Result` in `main`. + +## 📦 Migration Guide + +This release is backwards compatible. Your existing manual `.route()` calls will continue to work. To adopt the new features: +1. Add `#[rustapi::get/post/...]` attributes to your handler functions. +2. Switch from `RustApi::new()` to `RustApi::auto()` in your entry point. +3. Ensure your data structs derive `ToSchema` (for bodies) and `IntoParams` (for query strings). diff --git a/crates/rustapi-extras/src/jwt/mod.rs b/crates/rustapi-extras/src/jwt/mod.rs index 13da498..465dc70 100644 --- a/crates/rustapi-extras/src/jwt/mod.rs +++ b/crates/rustapi-extras/src/jwt/mod.rs @@ -334,6 +334,7 @@ impl FromRequestParts for AuthUser { impl OperationModifier for AuthUser { fn update_operation(op: &mut Operation) { // Add 401 Unauthorized response to OpenAPI spec + use rustapi_openapi::schema::Reference; use rustapi_openapi::{MediaType, ResponseSpec, SchemaRef}; use std::collections::HashMap; @@ -346,9 +347,9 @@ impl OperationModifier for AuthUser { map.insert( "application/json".to_string(), MediaType { - schema: SchemaRef::Ref { - reference: "#/components/schemas/ErrorSchema".to_string(), - }, + schema: SchemaRef::Ref(Reference { + ref_path: "#/components/schemas/ErrorSchema".to_string(), + }), }, ); Some(map) From 5d0354c1c094d1dd61b4970a93ed99e5ce1893d2 Mon Sep 17 00:00:00 2001 From: Tunay Engin Date: Thu, 29 Jan 2026 18:39:27 +0300 Subject: [PATCH 5/5] Update openapi_demo.rs --- crates/rustapi-rs/examples/openapi_demo.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/rustapi-rs/examples/openapi_demo.rs b/crates/rustapi-rs/examples/openapi_demo.rs index 637a380..bc534de 100644 --- a/crates/rustapi-rs/examples/openapi_demo.rs +++ b/crates/rustapi-rs/examples/openapi_demo.rs @@ -21,10 +21,11 @@ //! ## Cookbook //! +use rustapi_macros as rustapi; use rustapi_rs::prelude::*; use serde::Serialize; -#[derive(Serialize, utoipa::ToSchema)] +#[derive(Serialize, rustapi::ToSchema)] struct Message { greeting: String, }