Compare commits
33 commits
codegen/0.
...
main
Author | SHA1 | Date | |
---|---|---|---|
|
485c2ea176 | ||
|
cf98d24090 | ||
|
a90bcb00c4 | ||
|
1c9b4123c3 | ||
|
e5a766b893 | ||
|
45899430bb | ||
|
bd27916aa5 | ||
|
98073a37bd | ||
|
40913bc89b | ||
|
14e6e81278 | ||
|
3ad92fb8c8 | ||
|
1af37bea89 | ||
|
39731f2f5d | ||
|
83dfdb27ac | ||
|
6aaa06f501 | ||
|
c8bdcc81c4 | ||
|
8bfa1b8ac3 | ||
|
56e64470de | ||
|
6d57f275a2 | ||
|
11c5d71bf6 | ||
|
eb6e98f41b | ||
|
7a4f6462f5 | ||
|
266122ea0e | ||
|
47461b61b2 | ||
|
b245e3e712 | ||
|
73358b70cc | ||
|
b4ce0c764e | ||
|
7bc61de1c2 | ||
|
c17f93f600 | ||
|
4dd4fd37d4 | ||
|
26043ac318 | ||
|
b069c7e493 | ||
|
40b784cf57 |
113
Cargo.lock
generated
113
Cargo.lock
generated
|
@ -55,9 +55,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-compression"
|
name = "async-compression"
|
||||||
version = "0.4.22"
|
version = "0.4.23"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "59a194f9d963d8099596278594b3107448656ba73831c9d8c783e613ce86da64"
|
checksum = "b37fc50485c4f3f736a4fb14199f6d5f5ba008d7f28fe710306c92780f004c07"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"brotli",
|
"brotli",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
|
@ -159,9 +159,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "brotli"
|
name = "brotli"
|
||||||
version = "7.0.0"
|
version = "8.0.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cc97b8f16f944bba54f0433f07e30be199b6dc2bd25937444bbad560bcea29bd"
|
checksum = "cf19e729cdbd51af9a397fb9ef8ac8378007b797f8273cfbfdf45dcaa316167b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"alloc-no-stdlib",
|
"alloc-no-stdlib",
|
||||||
"alloc-stdlib",
|
"alloc-stdlib",
|
||||||
|
@ -170,9 +170,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "brotli-decompressor"
|
name = "brotli-decompressor"
|
||||||
version = "4.0.3"
|
version = "5.0.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a334ef7c9e23abf0ce748e8cd309037da93e606ad52eb372e4ce327a0dcfbdfd"
|
checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"alloc-no-stdlib",
|
"alloc-no-stdlib",
|
||||||
"alloc-stdlib",
|
"alloc-stdlib",
|
||||||
|
@ -198,9 +198,9 @@ checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.2.19"
|
version = "1.2.20"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8e3a13707ac958681c13b39b458c073d0d9bc8a22cb1b2f4c8e55eb72c13f362"
|
checksum = "04da6a0d40b948dfc4fa8f5bbf402b0fc1a64a28dbf7d12ffd683550f2c1b63a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"shlex",
|
"shlex",
|
||||||
]
|
]
|
||||||
|
@ -219,14 +219,15 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "chrono"
|
name = "chrono"
|
||||||
version = "0.4.40"
|
version = "0.4.41"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c"
|
checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"android-tzdata",
|
"android-tzdata",
|
||||||
"iana-time-zone",
|
"iana-time-zone",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
|
"serde",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"windows-link",
|
"windows-link",
|
||||||
]
|
]
|
||||||
|
@ -607,9 +608,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "getrandom"
|
name = "getrandom"
|
||||||
version = "0.2.15"
|
version = "0.2.16"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
|
checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
|
@ -659,9 +660,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.15.2"
|
version = "0.15.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
|
checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"allocator-api2",
|
"allocator-api2",
|
||||||
"equivalent",
|
"equivalent",
|
||||||
|
@ -1252,9 +1253,9 @@ checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openssl-sys"
|
name = "openssl-sys"
|
||||||
version = "0.9.107"
|
version = "0.9.108"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8288979acd84749c744a9014b4382d42b8f7b2592847b5afb2ed29e5d16ede07"
|
checksum = "e145e1651e858e820e4860f7b9c5e169bc1d8ce1c86043be79fa7b7634821847"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cc",
|
"cc",
|
||||||
"libc",
|
"libc",
|
||||||
|
@ -1366,9 +1367,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.94"
|
version = "1.0.95"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84"
|
checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
@ -1395,9 +1396,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quinn-proto"
|
name = "quinn-proto"
|
||||||
version = "0.11.10"
|
version = "0.11.11"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b820744eb4dc9b57a3398183639c511b5a26d2ed702cedd3febaa1393caa22cc"
|
checksum = "bcbafbbdbb0f638fe3f35f3c56739f77a8a1d070cb25603226c83339b391472b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"getrandom 0.3.2",
|
"getrandom 0.3.2",
|
||||||
|
@ -1415,9 +1416,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quinn-udp"
|
name = "quinn-udp"
|
||||||
version = "0.5.11"
|
version = "0.5.12"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "541d0f57c6ec747a90738a52741d3221f7960e8ac2f0ff4b1a63680e033b4ab5"
|
checksum = "ee4e529991f949c5e25755532370b8af5d114acae52326361d68d47af64aa842"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg_aliases",
|
"cfg_aliases",
|
||||||
"libc",
|
"libc",
|
||||||
|
@ -1489,7 +1490,7 @@ version = "0.6.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"getrandom 0.2.15",
|
"getrandom 0.2.16",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1569,7 +1570,7 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cc",
|
"cc",
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"getrandom 0.2.15",
|
"getrandom 0.2.16",
|
||||||
"libc",
|
"libc",
|
||||||
"untrusted",
|
"untrusted",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
|
@ -1609,9 +1610,9 @@ checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustix"
|
name = "rustix"
|
||||||
version = "1.0.5"
|
version = "1.0.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d97817398dd4bb2e6da002002db259209759911da105da92bec29ccb12cf58bf"
|
checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags",
|
||||||
"errno",
|
"errno",
|
||||||
|
@ -1781,9 +1782,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sha2"
|
name = "sha2"
|
||||||
version = "0.10.8"
|
version = "0.10.9"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8"
|
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"cpufeatures",
|
"cpufeatures",
|
||||||
|
@ -2080,6 +2081,28 @@ version = "0.11.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "strum"
|
||||||
|
version = "0.27.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f64def088c51c9510a8579e3c5d67c65349dcf755e5479ad3d010aa6454e2c32"
|
||||||
|
dependencies = [
|
||||||
|
"strum_macros",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "strum_macros"
|
||||||
|
version = "0.27.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c77a8c5abcaf0f9ce05d62342b7d298c346515365c36b673df4ebe3ced01fde8"
|
||||||
|
dependencies = [
|
||||||
|
"heck",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"rustversion",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "subtle"
|
name = "subtle"
|
||||||
version = "2.6.1"
|
version = "2.6.1"
|
||||||
|
@ -2088,9 +2111,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "2.0.100"
|
version = "2.0.101"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0"
|
checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
@ -2108,9 +2131,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "synstructure"
|
name = "synstructure"
|
||||||
version = "0.13.1"
|
version = "0.13.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971"
|
checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
@ -2258,9 +2281,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-util"
|
name = "tokio-util"
|
||||||
version = "0.7.14"
|
version = "0.7.15"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6b9590b93e6fcc1739458317cccd391ad3955e2bde8913edf6f95f9e65a8f034"
|
checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
|
@ -2271,10 +2294,12 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "torn-api"
|
name = "torn-api"
|
||||||
version = "1.0.1"
|
version = "1.7.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bon",
|
"bon",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
"chrono",
|
||||||
|
"futures",
|
||||||
"http",
|
"http",
|
||||||
"prettyplease",
|
"prettyplease",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
|
@ -2282,6 +2307,7 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_repr",
|
"serde_repr",
|
||||||
|
"strum",
|
||||||
"syn",
|
"syn",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
@ -2290,7 +2316,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "torn-api-codegen"
|
name = "torn-api-codegen"
|
||||||
version = "0.1.2"
|
version = "0.7.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"heck",
|
"heck",
|
||||||
"indexmap",
|
"indexmap",
|
||||||
|
@ -2303,7 +2329,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "torn-key-pool"
|
name = "torn-key-pool"
|
||||||
version = "1.0.0"
|
version = "1.1.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"futures",
|
"futures",
|
||||||
|
@ -2315,6 +2341,7 @@ dependencies = [
|
||||||
"sqlx",
|
"sqlx",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"tokio-stream",
|
||||||
"torn-api",
|
"torn-api",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -2580,9 +2607,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "webpki-roots"
|
name = "webpki-roots"
|
||||||
version = "0.26.8"
|
version = "0.26.10"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2210b291f7ea53617fbafcc4939f10914214ec15aace5ba62293a668f322c5c9"
|
checksum = "37493cadf42a2a939ed404698ded7fb378bf301b5011f973361779a3a74f8c93"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
]
|
]
|
||||||
|
@ -2935,18 +2962,18 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerocopy"
|
name = "zerocopy"
|
||||||
version = "0.8.24"
|
version = "0.8.25"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2586fea28e186957ef732a5f8b3be2da217d65c5969d4b1e17f973ebbe876879"
|
checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"zerocopy-derive",
|
"zerocopy-derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerocopy-derive"
|
name = "zerocopy-derive"
|
||||||
version = "0.8.24"
|
version = "0.8.25"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a996a8f63c5c4448cd959ac1bab0aaa3306ccfd060472f85943ee0750f0169be"
|
checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
|
|
@ -4,8 +4,8 @@ members = ["torn-api", "torn-api-codegen", "torn-key-pool"]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
license-file = "./LICENSE"
|
license-file = "./LICENSE"
|
||||||
repository = "https://github.com/TotallyNot/torn-api.rs.git"
|
repository = "https://git.elimination.me/pyrite/torn-api.rs.git"
|
||||||
homepage = "https://github.com/TotallyNot/torn-api.rs.git"
|
homepage = "https://git.elimination.me/pyrite/torn-api.rs"
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
[package]
|
[package]
|
||||||
name = "torn-api-codegen"
|
name = "torn-api-codegen"
|
||||||
authors = ["Pyrit [2111649]"]
|
authors = ["Pyrit [2111649]"]
|
||||||
version = "0.1.2"
|
version = "0.7.0"
|
||||||
edition = "2024"
|
edition = "2021"
|
||||||
description = "Contains the v2 torn API model descriptions and codegen for the bindings"
|
description = "Contains the v2 torn API model descriptions and codegen for the bindings"
|
||||||
license-file = { workspace = true }
|
license-file = { workspace = true }
|
||||||
repository = { workspace = true }
|
repository = { workspace = true }
|
||||||
|
|
|
@ -1,12 +1,15 @@
|
||||||
use heck::ToUpperCamelCase;
|
use heck::{ToSnakeCase, ToUpperCamelCase};
|
||||||
use proc_macro2::TokenStream;
|
use proc_macro2::TokenStream;
|
||||||
use quote::{format_ident, quote};
|
use quote::{format_ident, quote};
|
||||||
|
use syn::Ident;
|
||||||
|
|
||||||
use crate::openapi::{
|
use crate::openapi::{
|
||||||
parameter::OpenApiParameterSchema,
|
parameter::OpenApiParameterSchema,
|
||||||
r#type::{OpenApiType, OpenApiVariants},
|
r#type::{OpenApiType, OpenApiVariants},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use super::{object::PrimitiveType, Model, ResolvedSchema};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum EnumRepr {
|
pub enum EnumRepr {
|
||||||
U8,
|
U8,
|
||||||
|
@ -17,10 +20,12 @@ pub enum EnumRepr {
|
||||||
pub enum EnumVariantTupleValue {
|
pub enum EnumVariantTupleValue {
|
||||||
Ref { ty_name: String },
|
Ref { ty_name: String },
|
||||||
ArrayOfRefs { ty_name: String },
|
ArrayOfRefs { ty_name: String },
|
||||||
|
Primitive(PrimitiveType),
|
||||||
|
Enum { name: String, inner: Enum },
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EnumVariantTupleValue {
|
impl EnumVariantTupleValue {
|
||||||
pub fn from_schema(schema: &OpenApiType) -> Option<Self> {
|
pub fn from_schema(name: &str, schema: &OpenApiType) -> Option<Self> {
|
||||||
match schema {
|
match schema {
|
||||||
OpenApiType {
|
OpenApiType {
|
||||||
ref_path: Some(path),
|
ref_path: Some(path),
|
||||||
|
@ -44,14 +49,66 @@ impl EnumVariantTupleValue {
|
||||||
ty_name: path.strip_prefix("#/components/schemas/")?.to_owned(),
|
ty_name: path.strip_prefix("#/components/schemas/")?.to_owned(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
OpenApiType {
|
||||||
|
r#type: Some("string"),
|
||||||
|
format: None,
|
||||||
|
r#enum: None,
|
||||||
|
..
|
||||||
|
} => Some(Self::Primitive(PrimitiveType::String)),
|
||||||
|
OpenApiType {
|
||||||
|
r#type: Some("string"),
|
||||||
|
format: None,
|
||||||
|
r#enum: Some(_),
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
let name = format!("{name}Variant");
|
||||||
|
Some(Self::Enum {
|
||||||
|
inner: Enum::from_schema(&name, schema)?,
|
||||||
|
name,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
OpenApiType {
|
||||||
|
r#type: Some("integer"),
|
||||||
|
format: Some("int64"),
|
||||||
|
..
|
||||||
|
} => Some(Self::Primitive(PrimitiveType::I64)),
|
||||||
|
OpenApiType {
|
||||||
|
r#type: Some("integer"),
|
||||||
|
format: Some("int32"),
|
||||||
|
..
|
||||||
|
} => Some(Self::Primitive(PrimitiveType::I32)),
|
||||||
|
OpenApiType {
|
||||||
|
r#type: Some("number"),
|
||||||
|
format: Some("float") | None,
|
||||||
|
..
|
||||||
|
} => Some(Self::Primitive(PrimitiveType::Float)),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn type_name(&self) -> &str {
|
pub fn type_name(&self, ns: &mut EnumNamespace) -> TokenStream {
|
||||||
match self {
|
match self {
|
||||||
Self::Ref { ty_name } => ty_name,
|
Self::Ref { ty_name } => {
|
||||||
Self::ArrayOfRefs { ty_name } => ty_name,
|
let ty = format_ident!("{ty_name}");
|
||||||
|
quote! { crate::models::#ty }
|
||||||
|
}
|
||||||
|
Self::ArrayOfRefs { ty_name } => {
|
||||||
|
let ty = format_ident!("{ty_name}");
|
||||||
|
quote! { Vec<crate::models::#ty> }
|
||||||
|
}
|
||||||
|
Self::Primitive(PrimitiveType::I64) => quote! { i64 },
|
||||||
|
Self::Primitive(PrimitiveType::I32) => quote! { i32 },
|
||||||
|
Self::Primitive(PrimitiveType::Float) => quote! { f32 },
|
||||||
|
Self::Primitive(PrimitiveType::String) => quote! { String },
|
||||||
|
Self::Primitive(PrimitiveType::DateTime) => quote! { chrono::DateTime<chrono::Utc> },
|
||||||
|
Self::Primitive(PrimitiveType::Bool) => quote! { bool },
|
||||||
|
Self::Enum { name, .. } => {
|
||||||
|
let path = ns.get_ident();
|
||||||
|
let ty_name = format_ident!("{name}");
|
||||||
|
quote! {
|
||||||
|
#path::#ty_name,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -59,6 +116,49 @@ impl EnumVariantTupleValue {
|
||||||
match self {
|
match self {
|
||||||
Self::Ref { ty_name } => ty_name.clone(),
|
Self::Ref { ty_name } => ty_name.clone(),
|
||||||
Self::ArrayOfRefs { ty_name } => format!("{ty_name}s"),
|
Self::ArrayOfRefs { ty_name } => format!("{ty_name}s"),
|
||||||
|
Self::Primitive(PrimitiveType::I64) => "I64".to_owned(),
|
||||||
|
Self::Primitive(PrimitiveType::I32) => "I32".to_owned(),
|
||||||
|
Self::Primitive(PrimitiveType::Float) => "Float".to_owned(),
|
||||||
|
Self::Primitive(PrimitiveType::String) => "String".to_owned(),
|
||||||
|
Self::Primitive(PrimitiveType::DateTime) => "DateTime".to_owned(),
|
||||||
|
Self::Primitive(PrimitiveType::Bool) => "Bool".to_owned(),
|
||||||
|
Self::Enum { .. } => "Variant".to_owned(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_display(&self, resolved: &ResolvedSchema) -> bool {
|
||||||
|
match self {
|
||||||
|
Self::Primitive(_) => true,
|
||||||
|
Self::Ref { ty_name } | Self::ArrayOfRefs { ty_name } => resolved
|
||||||
|
.models
|
||||||
|
.get(ty_name)
|
||||||
|
.map(|f| f.is_display(resolved))
|
||||||
|
.unwrap_or_default(),
|
||||||
|
Self::Enum { inner, .. } => inner.is_display(resolved),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn codegen_display(&self) -> TokenStream {
|
||||||
|
match self {
|
||||||
|
Self::ArrayOfRefs { .. } => quote! {
|
||||||
|
write!(f, "{}", value.iter().map(ToString::to_string).collect::<Vec<_>>().join(","))
|
||||||
|
},
|
||||||
|
_ => quote! {
|
||||||
|
write!(f, "{}", value)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_comparable(&self, resolved: &ResolvedSchema) -> bool {
|
||||||
|
match self {
|
||||||
|
Self::Primitive(PrimitiveType::Float) => false,
|
||||||
|
Self::Primitive(_) => true,
|
||||||
|
Self::Enum { inner, .. } => inner.is_comparable(resolved),
|
||||||
|
Self::Ref { ty_name } | Self::ArrayOfRefs { ty_name } => resolved
|
||||||
|
.models
|
||||||
|
.get(ty_name)
|
||||||
|
.map(|m| matches!(m, Model::Newtype(_)))
|
||||||
|
.unwrap_or_default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -77,12 +177,39 @@ impl Default for EnumVariantValue {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EnumVariantValue {
|
impl EnumVariantValue {
|
||||||
pub fn codegen_display(&self, name: &str) -> Option<TokenStream> {
|
pub fn is_display(&self, resolved: &ResolvedSchema) -> bool {
|
||||||
match self {
|
match self {
|
||||||
Self::Repr(i) => Some(quote! { write!(f, "{}", #i) }),
|
Self::Repr(_) | Self::String { .. } => true,
|
||||||
|
Self::Tuple(val) => {
|
||||||
|
val.len() == 1
|
||||||
|
&& val
|
||||||
|
.iter()
|
||||||
|
.next()
|
||||||
|
.map(|v| v.is_display(resolved))
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_comparable(&self, resolved: &ResolvedSchema) -> bool {
|
||||||
|
match self {
|
||||||
|
Self::Repr(_) | Self::String { .. } => true,
|
||||||
|
Self::Tuple(values) => values.iter().all(|v| v.is_comparable(resolved)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn codegen_display(&self, name: &str) -> Option<TokenStream> {
|
||||||
|
let variant = format_ident!("{name}");
|
||||||
|
|
||||||
|
match self {
|
||||||
|
Self::Repr(i) => Some(quote! { Self::#variant => write!(f, "{}", #i) }),
|
||||||
Self::String { rename } => {
|
Self::String { rename } => {
|
||||||
let name = rename.as_deref().unwrap_or(name);
|
let name = rename.as_deref().unwrap_or(name);
|
||||||
Some(quote! { write!(f, #name) })
|
Some(quote! { Self::#variant => write!(f, #name) })
|
||||||
|
}
|
||||||
|
Self::Tuple(values) if values.len() == 1 => {
|
||||||
|
let rhs = values.first().unwrap().codegen_display();
|
||||||
|
Some(quote! { Self::#variant(value) => #rhs })
|
||||||
}
|
}
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
|
@ -96,8 +223,61 @@ pub struct EnumVariant {
|
||||||
pub value: EnumVariantValue,
|
pub value: EnumVariantValue,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct EnumNamespace<'e> {
|
||||||
|
r#enum: &'e Enum,
|
||||||
|
ident: Option<Ident>,
|
||||||
|
elements: Vec<TokenStream>,
|
||||||
|
top_level_elements: Vec<TokenStream>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EnumNamespace<'_> {
|
||||||
|
pub fn get_ident(&mut self) -> Ident {
|
||||||
|
self.ident
|
||||||
|
.get_or_insert_with(|| {
|
||||||
|
let name = self.r#enum.name.to_snake_case();
|
||||||
|
format_ident!("{name}")
|
||||||
|
})
|
||||||
|
.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn push_element(&mut self, el: TokenStream) {
|
||||||
|
self.elements.push(el);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn push_top_level(&mut self, el: TokenStream) {
|
||||||
|
self.top_level_elements.push(el);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn codegen(mut self) -> Option<TokenStream> {
|
||||||
|
if self.elements.is_empty() && self.top_level_elements.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
let top_level = &self.top_level_elements;
|
||||||
|
let mut output = quote! {
|
||||||
|
#(#top_level)*
|
||||||
|
};
|
||||||
|
|
||||||
|
if !self.elements.is_empty() {
|
||||||
|
let ident = self.get_ident();
|
||||||
|
let elements = self.elements;
|
||||||
|
output.extend(quote! {
|
||||||
|
pub mod #ident {
|
||||||
|
#(#elements)*
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl EnumVariant {
|
impl EnumVariant {
|
||||||
pub fn codegen(&self) -> Option<TokenStream> {
|
pub fn codegen(
|
||||||
|
&self,
|
||||||
|
ns: &mut EnumNamespace,
|
||||||
|
resolved: &ResolvedSchema,
|
||||||
|
) -> Option<TokenStream> {
|
||||||
let doc = self.description.as_ref().map(|d| {
|
let doc = self.description.as_ref().map(|d| {
|
||||||
quote! {
|
quote! {
|
||||||
#[doc = #d]
|
#[doc = #d]
|
||||||
|
@ -127,15 +307,29 @@ impl EnumVariant {
|
||||||
EnumVariantValue::Tuple(values) => {
|
EnumVariantValue::Tuple(values) => {
|
||||||
let mut val_tys = Vec::with_capacity(values.len());
|
let mut val_tys = Vec::with_capacity(values.len());
|
||||||
|
|
||||||
for value in values {
|
if let [value] = values.as_slice() {
|
||||||
let ty_name = value.type_name();
|
let enum_name = format_ident!("{}", ns.r#enum.name);
|
||||||
let ty_name = format_ident!("{ty_name}");
|
let ty_name = value.type_name(ns);
|
||||||
|
|
||||||
val_tys.push(quote! {
|
ns.push_top_level(quote! {
|
||||||
crate::models::#ty_name
|
impl From<#ty_name> for #enum_name {
|
||||||
|
fn from(value: #ty_name) -> Self {
|
||||||
|
Self::#name(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for value in values {
|
||||||
|
let ty_name = value.type_name(ns);
|
||||||
|
|
||||||
|
if let EnumVariantTupleValue::Enum { inner, .. } = &value {
|
||||||
|
ns.push_element(inner.codegen(resolved)?);
|
||||||
|
}
|
||||||
|
|
||||||
|
val_tys.push(ty_name);
|
||||||
|
}
|
||||||
|
|
||||||
Some(quote! {
|
Some(quote! {
|
||||||
#name(#(#val_tys),*)
|
#name(#(#val_tys),*)
|
||||||
})
|
})
|
||||||
|
@ -144,12 +338,7 @@ impl EnumVariant {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn codegen_display(&self) -> Option<TokenStream> {
|
pub fn codegen_display(&self) -> Option<TokenStream> {
|
||||||
let rhs = self.value.codegen_display(&self.name)?;
|
self.value.codegen_display(&self.name)
|
||||||
let name = format_ident!("{}", self.name);
|
|
||||||
|
|
||||||
Some(quote! {
|
|
||||||
Self::#name => #rhs
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -159,7 +348,6 @@ pub struct Enum {
|
||||||
pub description: Option<String>,
|
pub description: Option<String>,
|
||||||
pub repr: Option<EnumRepr>,
|
pub repr: Option<EnumRepr>,
|
||||||
pub copy: bool,
|
pub copy: bool,
|
||||||
pub display: bool,
|
|
||||||
pub untagged: bool,
|
pub untagged: bool,
|
||||||
pub variants: Vec<EnumVariant>,
|
pub variants: Vec<EnumVariant>,
|
||||||
}
|
}
|
||||||
|
@ -176,7 +364,6 @@ impl Enum {
|
||||||
match &schema.r#enum {
|
match &schema.r#enum {
|
||||||
Some(OpenApiVariants::Int(int_variants)) => {
|
Some(OpenApiVariants::Int(int_variants)) => {
|
||||||
result.repr = Some(EnumRepr::U32);
|
result.repr = Some(EnumRepr::U32);
|
||||||
result.display = true;
|
|
||||||
result.variants = int_variants
|
result.variants = int_variants
|
||||||
.iter()
|
.iter()
|
||||||
.copied()
|
.copied()
|
||||||
|
@ -188,7 +375,6 @@ impl Enum {
|
||||||
.collect();
|
.collect();
|
||||||
}
|
}
|
||||||
Some(OpenApiVariants::Str(str_variants)) => {
|
Some(OpenApiVariants::Str(str_variants)) => {
|
||||||
result.display = true;
|
|
||||||
result.variants = str_variants
|
result.variants = str_variants
|
||||||
.iter()
|
.iter()
|
||||||
.copied()
|
.copied()
|
||||||
|
@ -214,7 +400,6 @@ impl Enum {
|
||||||
let mut result = Self {
|
let mut result = Self {
|
||||||
name: name.to_owned(),
|
name: name.to_owned(),
|
||||||
copy: true,
|
copy: true,
|
||||||
display: true,
|
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -240,7 +425,7 @@ impl Enum {
|
||||||
};
|
};
|
||||||
|
|
||||||
for schema in schemas {
|
for schema in schemas {
|
||||||
let value = EnumVariantTupleValue::from_schema(schema)?;
|
let value = EnumVariantTupleValue::from_schema(name, schema)?;
|
||||||
let name = value.name();
|
let name = value.name();
|
||||||
|
|
||||||
result.variants.push(EnumVariant {
|
result.variants.push(EnumVariant {
|
||||||
|
@ -250,13 +435,42 @@ impl Enum {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HACK: idk
|
||||||
|
let shared: Vec<_> = result
|
||||||
|
.variants
|
||||||
|
.iter_mut()
|
||||||
|
.filter(|v| v.name == "Variant")
|
||||||
|
.collect();
|
||||||
|
if shared.len() >= 2 {
|
||||||
|
for (idx, variant) in shared.into_iter().enumerate() {
|
||||||
|
let label = idx + 1;
|
||||||
|
variant.name = format!("Variant{}", label);
|
||||||
|
if let EnumVariantValue::Tuple(values) = &mut variant.value {
|
||||||
|
if let [EnumVariantTupleValue::Enum { name, inner, .. }] = values.as_mut_slice()
|
||||||
|
{
|
||||||
|
inner.name.push_str(&label.to_string());
|
||||||
|
name.push_str(&label.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Some(result)
|
Some(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn codegen(&self) -> Option<TokenStream> {
|
pub fn is_display(&self, resolved: &ResolvedSchema) -> bool {
|
||||||
|
self.variants.iter().all(|v| v.value.is_display(resolved))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_comparable(&self, resolved: &ResolvedSchema) -> bool {
|
||||||
|
self.variants
|
||||||
|
.iter()
|
||||||
|
.all(|v| v.value.is_comparable(resolved))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn codegen(&self, resolved: &ResolvedSchema) -> Option<TokenStream> {
|
||||||
let repr = self.repr.map(|r| match r {
|
let repr = self.repr.map(|r| match r {
|
||||||
EnumRepr::U8 => quote! { #[repr(u8)]},
|
EnumRepr::U8 => quote! { #[repr(u8)] },
|
||||||
EnumRepr::U32 => quote! { #[repr(u32)]},
|
EnumRepr::U32 => quote! { #[repr(u32)] },
|
||||||
});
|
});
|
||||||
let name = format_ident!("{}", self.name);
|
let name = format_ident!("{}", self.name);
|
||||||
let desc = self.description.as_ref().map(|d| {
|
let desc = self.description.as_ref().map(|d| {
|
||||||
|
@ -266,12 +480,21 @@ impl Enum {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let mut ns = EnumNamespace {
|
||||||
|
r#enum: self,
|
||||||
|
ident: None,
|
||||||
|
elements: Default::default(),
|
||||||
|
top_level_elements: Default::default(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let is_display = self.is_display(resolved);
|
||||||
|
|
||||||
let mut display = Vec::with_capacity(self.variants.len());
|
let mut display = Vec::with_capacity(self.variants.len());
|
||||||
let mut variants = Vec::with_capacity(self.variants.len());
|
let mut variants = Vec::with_capacity(self.variants.len());
|
||||||
for variant in &self.variants {
|
for variant in &self.variants {
|
||||||
variants.push(variant.codegen()?);
|
variants.push(variant.codegen(&mut ns, resolved)?);
|
||||||
|
|
||||||
if self.display {
|
if is_display {
|
||||||
display.push(variant.codegen_display()?);
|
display.push(variant.codegen_display()?);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -285,7 +508,11 @@ impl Enum {
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.copy {
|
if self.copy {
|
||||||
derives.push(quote! { Copy, Hash });
|
derives.push(quote! { Copy });
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.is_comparable(resolved) {
|
||||||
|
derives.push(quote! { Eq, Hash });
|
||||||
}
|
}
|
||||||
|
|
||||||
let serde_attr = self.untagged.then(|| {
|
let serde_attr = self.untagged.then(|| {
|
||||||
|
@ -294,7 +521,7 @@ impl Enum {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let display = self.display.then(|| {
|
let display = is_display.then(|| {
|
||||||
quote! {
|
quote! {
|
||||||
impl std::fmt::Display for #name {
|
impl std::fmt::Display for #name {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
@ -306,34 +533,35 @@ impl Enum {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let module = ns.codegen();
|
||||||
|
|
||||||
Some(quote! {
|
Some(quote! {
|
||||||
#desc
|
#desc
|
||||||
#[derive(Debug, Clone, PartialEq, #(#derives),*)]
|
#[derive(Debug, Clone, PartialEq, #(#derives),*)]
|
||||||
|
#[cfg_attr(feature = "strum", derive(strum::EnumIs, strum::EnumTryAs))]
|
||||||
#serde_attr
|
#serde_attr
|
||||||
pub enum #name {
|
pub enum #name {
|
||||||
#(#variants),*
|
#(#variants),*
|
||||||
}
|
}
|
||||||
#display
|
#display
|
||||||
|
|
||||||
|
#module
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use crate::openapi::schema::OpenApiSchema;
|
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
use crate::openapi::schema::test::get_schema;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn codegen() {
|
fn is_display() {
|
||||||
let schema = OpenApiSchema::read().unwrap();
|
let schema = get_schema();
|
||||||
|
let resolved = ResolvedSchema::from_open_api(&schema);
|
||||||
|
|
||||||
let revive_setting = schema.components.schemas.get("ReviveSetting").unwrap();
|
let torn_selection_name = resolved.models.get("TornSelectionName").unwrap();
|
||||||
|
assert!(torn_selection_name.is_display(&resolved));
|
||||||
let r#enum = Enum::from_schema("ReviveSetting", revive_setting).unwrap();
|
|
||||||
|
|
||||||
let code = r#enum.codegen().unwrap();
|
|
||||||
|
|
||||||
panic!("{code}");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,15 @@
|
||||||
use r#enum::Enum;
|
use std::{cell::RefCell, rc::Rc};
|
||||||
|
|
||||||
use indexmap::IndexMap;
|
use indexmap::IndexMap;
|
||||||
use newtype::Newtype;
|
use newtype::Newtype;
|
||||||
use object::Object;
|
use object::Object;
|
||||||
|
use parameter::Parameter;
|
||||||
|
use path::{Path, PrettySegments};
|
||||||
use proc_macro2::TokenStream;
|
use proc_macro2::TokenStream;
|
||||||
|
use r#enum::Enum;
|
||||||
|
use scope::Scope;
|
||||||
|
|
||||||
use crate::openapi::r#type::OpenApiType;
|
use crate::openapi::{r#type::OpenApiType, schema::OpenApiSchema};
|
||||||
|
|
||||||
pub mod r#enum;
|
pub mod r#enum;
|
||||||
pub mod newtype;
|
pub mod newtype;
|
||||||
|
@ -22,7 +27,169 @@ pub enum Model {
|
||||||
Unresolved,
|
Unresolved,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn resolve(r#type: &OpenApiType, name: &str, schemas: &IndexMap<&str, OpenApiType>) -> Model {
|
impl Model {
|
||||||
|
pub fn is_display(&self, resolved: &ResolvedSchema) -> bool {
|
||||||
|
match self {
|
||||||
|
Self::Enum(r#enum) => r#enum.is_display(resolved),
|
||||||
|
Self::Newtype(_) => true,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct ResolvedSchema {
|
||||||
|
pub models: IndexMap<String, Model>,
|
||||||
|
pub paths: IndexMap<String, Path>,
|
||||||
|
pub parameters: Vec<Parameter>,
|
||||||
|
|
||||||
|
pub warnings: WarningReporter,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Warning {
|
||||||
|
pub message: String,
|
||||||
|
pub path: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for Warning {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "in {}: {}", self.path.join("."), self.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct WarningReporterState {
|
||||||
|
warnings: Vec<Warning>,
|
||||||
|
path: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Default)]
|
||||||
|
pub struct WarningReporter {
|
||||||
|
state: Rc<RefCell<WarningReporterState>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WarningReporter {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push(&self, message: impl ToString) {
|
||||||
|
let mut state = self.state.borrow_mut();
|
||||||
|
let path = state.path.iter().map(ToString::to_string).collect();
|
||||||
|
state.warnings.push(Warning {
|
||||||
|
message: message.to_string(),
|
||||||
|
path,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn child(&self, name: impl ToString) -> WarningReporter {
|
||||||
|
self.state.borrow_mut().path.push(name.to_string());
|
||||||
|
|
||||||
|
Self {
|
||||||
|
state: self.state.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.state.borrow().warnings.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_warnings(&self) -> Vec<Warning> {
|
||||||
|
self.state.borrow().warnings.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for WarningReporter {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
self.state.borrow_mut().path.pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ResolvedSchema {
|
||||||
|
pub fn from_open_api(schema: &OpenApiSchema) -> Self {
|
||||||
|
let mut result = Self::default();
|
||||||
|
|
||||||
|
for (name, r#type) in &schema.components.schemas {
|
||||||
|
result.models.insert(
|
||||||
|
name.to_string(),
|
||||||
|
resolve(r#type, name, &schema.components.schemas, &result.warnings),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (path, body) in &schema.paths {
|
||||||
|
result.paths.insert(
|
||||||
|
path.to_string(),
|
||||||
|
Path::from_schema(
|
||||||
|
path,
|
||||||
|
body,
|
||||||
|
&schema.components.parameters,
|
||||||
|
result.warnings.child(path),
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (name, param) in &schema.components.parameters {
|
||||||
|
result
|
||||||
|
.parameters
|
||||||
|
.push(Parameter::from_schema(name, param).unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn codegen_models(&self) -> TokenStream {
|
||||||
|
let mut output = TokenStream::default();
|
||||||
|
|
||||||
|
for model in self.models.values() {
|
||||||
|
output.extend(model.codegen(self));
|
||||||
|
}
|
||||||
|
|
||||||
|
output
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn codegen_requests(&self) -> TokenStream {
|
||||||
|
let mut output = TokenStream::default();
|
||||||
|
|
||||||
|
for path in self.paths.values() {
|
||||||
|
output.extend(
|
||||||
|
path.codegen_request(self, self.warnings.child(PrettySegments(&path.segments))),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
output
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn codegen_parameters(&self) -> TokenStream {
|
||||||
|
let mut output = TokenStream::default();
|
||||||
|
|
||||||
|
for param in &self.parameters {
|
||||||
|
output.extend(param.codegen(self));
|
||||||
|
}
|
||||||
|
|
||||||
|
output
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn codegen_scopes(&self) -> TokenStream {
|
||||||
|
let mut output = TokenStream::default();
|
||||||
|
|
||||||
|
let scopes = Scope::from_paths(self.paths.values().cloned().collect());
|
||||||
|
|
||||||
|
for scope in scopes {
|
||||||
|
output.extend(scope.codegen());
|
||||||
|
}
|
||||||
|
|
||||||
|
output
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn resolve(
|
||||||
|
r#type: &OpenApiType,
|
||||||
|
name: &str,
|
||||||
|
schemas: &IndexMap<&str, OpenApiType>,
|
||||||
|
warnings: &WarningReporter,
|
||||||
|
) -> Model {
|
||||||
match r#type {
|
match r#type {
|
||||||
OpenApiType {
|
OpenApiType {
|
||||||
r#enum: Some(_), ..
|
r#enum: Some(_), ..
|
||||||
|
@ -30,8 +197,12 @@ pub fn resolve(r#type: &OpenApiType, name: &str, schemas: &IndexMap<&str, OpenAp
|
||||||
OpenApiType {
|
OpenApiType {
|
||||||
r#type: Some("object"),
|
r#type: Some("object"),
|
||||||
..
|
..
|
||||||
} => Object::from_schema_object(name, r#type, schemas)
|
} => Model::Object(Object::from_schema_object(
|
||||||
.map_or(Model::Unresolved, Model::Object),
|
name,
|
||||||
|
r#type,
|
||||||
|
schemas,
|
||||||
|
warnings.child(name),
|
||||||
|
)),
|
||||||
OpenApiType {
|
OpenApiType {
|
||||||
r#type: Some(_), ..
|
r#type: Some(_), ..
|
||||||
} => Newtype::from_schema(name, r#type).map_or(Model::Unresolved, Model::Newtype),
|
} => Newtype::from_schema(name, r#type).map_or(Model::Unresolved, Model::Newtype),
|
||||||
|
@ -42,17 +213,22 @@ pub fn resolve(r#type: &OpenApiType, name: &str, schemas: &IndexMap<&str, OpenAp
|
||||||
OpenApiType {
|
OpenApiType {
|
||||||
all_of: Some(types),
|
all_of: Some(types),
|
||||||
..
|
..
|
||||||
} => Object::from_all_of(name, types, schemas).map_or(Model::Unresolved, Model::Object),
|
} => Model::Object(Object::from_all_of(
|
||||||
|
name,
|
||||||
|
types,
|
||||||
|
schemas,
|
||||||
|
warnings.child(name),
|
||||||
|
)),
|
||||||
_ => Model::Unresolved,
|
_ => Model::Unresolved,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Model {
|
impl Model {
|
||||||
pub fn codegen(&self) -> Option<TokenStream> {
|
pub fn codegen(&self, resolved: &ResolvedSchema) -> Option<TokenStream> {
|
||||||
match self {
|
match self {
|
||||||
Self::Newtype(newtype) => newtype.codegen(),
|
Self::Newtype(newtype) => newtype.codegen(),
|
||||||
Self::Enum(r#enum) => r#enum.codegen(),
|
Self::Enum(r#enum) => r#enum.codegen(resolved),
|
||||||
Self::Object(object) => object.codegen(),
|
Self::Object(object) => object.codegen(resolved),
|
||||||
Self::Unresolved => None,
|
Self::Unresolved => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -61,18 +237,22 @@ impl Model {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::{
|
use crate::openapi::schema::test::get_schema;
|
||||||
model::r#enum::{EnumRepr, EnumVariant},
|
|
||||||
openapi::schema::OpenApiSchema,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn resolve_newtypes() {
|
fn resolve_newtypes() {
|
||||||
let schema = OpenApiSchema::read().unwrap();
|
let schema = get_schema();
|
||||||
|
|
||||||
let user_id_schema = schema.components.schemas.get("UserId").unwrap();
|
let user_id_schema = schema.components.schemas.get("UserId").unwrap();
|
||||||
|
|
||||||
let user_id = resolve(user_id_schema, "UserId", &schema.components.schemas);
|
let reporter = WarningReporter::new();
|
||||||
|
let user_id = resolve(
|
||||||
|
user_id_schema,
|
||||||
|
"UserId",
|
||||||
|
&schema.components.schemas,
|
||||||
|
&reporter,
|
||||||
|
);
|
||||||
|
assert!(reporter.is_empty());
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
user_id,
|
user_id,
|
||||||
|
@ -87,7 +267,13 @@ mod test {
|
||||||
|
|
||||||
let attack_code_schema = schema.components.schemas.get("AttackCode").unwrap();
|
let attack_code_schema = schema.components.schemas.get("AttackCode").unwrap();
|
||||||
|
|
||||||
let attack_code = resolve(attack_code_schema, "AttackCode", &schema.components.schemas);
|
let attack_code = resolve(
|
||||||
|
attack_code_schema,
|
||||||
|
"AttackCode",
|
||||||
|
&schema.components.schemas,
|
||||||
|
&reporter,
|
||||||
|
);
|
||||||
|
assert!(reporter.is_empty());
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
attack_code,
|
attack_code,
|
||||||
|
@ -101,74 +287,18 @@ mod test {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn resolve_enums() {
|
|
||||||
let schema = OpenApiSchema::read().unwrap();
|
|
||||||
|
|
||||||
let forum_feed_type_schema = schema.components.schemas.get("ForumFeedTypeEnum").unwrap();
|
|
||||||
|
|
||||||
let forum_feed_type = resolve(
|
|
||||||
forum_feed_type_schema,
|
|
||||||
"ForumFeedTypeEnum",
|
|
||||||
&schema.components.schemas,
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(forum_feed_type, Model::Enum(Enum {
|
|
||||||
name: "ForumFeedType".to_owned(),
|
|
||||||
description: Some("This represents the type of the activity. Values range from 1 to 8 where:\n * 1 = 'X posted on a thread',\n * 2 = 'X created a thread',\n * 3 = 'X liked your thread',\n * 4 = 'X disliked your thread',\n * 5 = 'X liked your post',\n * 6 = 'X disliked your post',\n * 7 = 'X quoted your post'.".to_owned()),
|
|
||||||
repr: Some(EnumRepr::U32),
|
|
||||||
copy: true,
|
|
||||||
untagged: false,
|
|
||||||
display: true,
|
|
||||||
variants: vec![
|
|
||||||
EnumVariant {
|
|
||||||
name: "Variant1".to_owned(),
|
|
||||||
value: r#enum::EnumVariantValue::Repr(1),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
EnumVariant {
|
|
||||||
name: "Variant2".to_owned(),
|
|
||||||
value: r#enum::EnumVariantValue::Repr(2),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
EnumVariant {
|
|
||||||
name: "Variant3".to_owned(),
|
|
||||||
value: r#enum::EnumVariantValue::Repr(3),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
EnumVariant {
|
|
||||||
name: "Variant4".to_owned(),
|
|
||||||
value: r#enum::EnumVariantValue::Repr(4),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
EnumVariant {
|
|
||||||
name: "Variant5".to_owned(),
|
|
||||||
value: r#enum::EnumVariantValue::Repr(5),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
EnumVariant {
|
|
||||||
name: "Variant6".to_owned(),
|
|
||||||
value: r#enum::EnumVariantValue::Repr(6),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
EnumVariant {
|
|
||||||
name: "Variant7".to_owned(),
|
|
||||||
value: r#enum::EnumVariantValue::Repr(7),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn resolve_all() {
|
fn resolve_all() {
|
||||||
let schema = OpenApiSchema::read().unwrap();
|
let schema = get_schema();
|
||||||
|
|
||||||
let mut unresolved = vec![];
|
let mut unresolved = vec![];
|
||||||
let total = schema.components.schemas.len();
|
let total = schema.components.schemas.len();
|
||||||
|
|
||||||
for (name, desc) in &schema.components.schemas {
|
for (name, desc) in &schema.components.schemas {
|
||||||
if resolve(desc, name, &schema.components.schemas) == Model::Unresolved {
|
let reporter = WarningReporter::new();
|
||||||
|
if resolve(desc, name, &schema.components.schemas, &reporter) == Model::Unresolved
|
||||||
|
|| !reporter.is_empty()
|
||||||
|
{
|
||||||
unresolved.push(name);
|
unresolved.push(name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -121,24 +121,3 @@ impl Newtype {
|
||||||
Some(body)
|
Some(body)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod test {
|
|
||||||
use super::*;
|
|
||||||
use crate::openapi::schema::OpenApiSchema;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn codegen() {
|
|
||||||
let schema = OpenApiSchema::read().unwrap();
|
|
||||||
|
|
||||||
let user_id = schema.components.schemas.get("UserId").unwrap();
|
|
||||||
|
|
||||||
let mut newtype = Newtype::from_schema("UserId", user_id).unwrap();
|
|
||||||
|
|
||||||
newtype.description = Some("Description goes here".to_owned());
|
|
||||||
|
|
||||||
let code = newtype.codegen().unwrap().to_string();
|
|
||||||
|
|
||||||
panic!("{code}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
use heck::{ToSnakeCase, ToUpperCamelCase};
|
use heck::{ToSnakeCase, ToUpperCamelCase};
|
||||||
use indexmap::IndexMap;
|
use indexmap::{map::Entry, IndexMap};
|
||||||
use proc_macro2::TokenStream;
|
use proc_macro2::TokenStream;
|
||||||
use quote::{ToTokens, format_ident, quote};
|
use quote::{format_ident, quote, ToTokens};
|
||||||
use syn::Ident;
|
use syn::Ident;
|
||||||
|
|
||||||
use crate::openapi::r#type::OpenApiType;
|
use crate::openapi::r#type::OpenApiType;
|
||||||
|
|
||||||
use super::r#enum::Enum;
|
use super::{r#enum::Enum, ResolvedSchema, WarningReporter};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum PrimitiveType {
|
pub enum PrimitiveType {
|
||||||
|
@ -15,6 +15,7 @@ pub enum PrimitiveType {
|
||||||
I64,
|
I64,
|
||||||
String,
|
String,
|
||||||
Float,
|
Float,
|
||||||
|
DateTime,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
@ -27,7 +28,11 @@ pub enum PropertyType {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PropertyType {
|
impl PropertyType {
|
||||||
pub fn codegen(&self, namespace: &mut ObjectNamespace) -> Option<TokenStream> {
|
pub fn codegen(
|
||||||
|
&self,
|
||||||
|
namespace: &mut ObjectNamespace,
|
||||||
|
resolved: &ResolvedSchema,
|
||||||
|
) -> Option<TokenStream> {
|
||||||
match self {
|
match self {
|
||||||
Self::Primitive(PrimitiveType::Bool) => Some(format_ident!("bool").into_token_stream()),
|
Self::Primitive(PrimitiveType::Bool) => Some(format_ident!("bool").into_token_stream()),
|
||||||
Self::Primitive(PrimitiveType::I32) => Some(format_ident!("i32").into_token_stream()),
|
Self::Primitive(PrimitiveType::I32) => Some(format_ident!("i32").into_token_stream()),
|
||||||
|
@ -35,6 +40,9 @@ impl PropertyType {
|
||||||
Self::Primitive(PrimitiveType::String) => {
|
Self::Primitive(PrimitiveType::String) => {
|
||||||
Some(format_ident!("String").into_token_stream())
|
Some(format_ident!("String").into_token_stream())
|
||||||
}
|
}
|
||||||
|
Self::Primitive(PrimitiveType::DateTime) => {
|
||||||
|
Some(quote! { chrono::DateTime<chrono::Utc> })
|
||||||
|
}
|
||||||
Self::Primitive(PrimitiveType::Float) => Some(format_ident!("f64").into_token_stream()),
|
Self::Primitive(PrimitiveType::Float) => Some(format_ident!("f64").into_token_stream()),
|
||||||
Self::Ref(path) => {
|
Self::Ref(path) => {
|
||||||
let name = path.strip_prefix("#/components/schemas/")?;
|
let name = path.strip_prefix("#/components/schemas/")?;
|
||||||
|
@ -43,7 +51,7 @@ impl PropertyType {
|
||||||
Some(quote! { crate::models::#name })
|
Some(quote! { crate::models::#name })
|
||||||
}
|
}
|
||||||
Self::Enum(r#enum) => {
|
Self::Enum(r#enum) => {
|
||||||
let code = r#enum.codegen()?;
|
let code = r#enum.codegen(resolved)?;
|
||||||
namespace.push_element(code);
|
namespace.push_element(code);
|
||||||
|
|
||||||
let ns = namespace.get_ident();
|
let ns = namespace.get_ident();
|
||||||
|
@ -54,14 +62,14 @@ impl PropertyType {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
Self::Array(array) => {
|
Self::Array(array) => {
|
||||||
let inner_ty = array.codegen(namespace)?;
|
let inner_ty = array.codegen(namespace, resolved)?;
|
||||||
|
|
||||||
Some(quote! {
|
Some(quote! {
|
||||||
Vec<#inner_ty>
|
Vec<#inner_ty>
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
Self::Nested(nested) => {
|
Self::Nested(nested) => {
|
||||||
let code = nested.codegen()?;
|
let code = nested.codegen(resolved)?;
|
||||||
namespace.push_element(code);
|
namespace.push_element(code);
|
||||||
|
|
||||||
let ns = namespace.get_ident();
|
let ns = namespace.get_ident();
|
||||||
|
@ -77,11 +85,13 @@ impl PropertyType {
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct Property {
|
pub struct Property {
|
||||||
|
pub field_name: String,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub description: Option<String>,
|
pub description: Option<String>,
|
||||||
pub required: bool,
|
pub required: bool,
|
||||||
pub nullable: bool,
|
pub nullable: bool,
|
||||||
pub r#type: PropertyType,
|
pub r#type: PropertyType,
|
||||||
|
pub deprecated: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Property {
|
impl Property {
|
||||||
|
@ -90,60 +100,68 @@ impl Property {
|
||||||
required: bool,
|
required: bool,
|
||||||
schema: &OpenApiType,
|
schema: &OpenApiType,
|
||||||
schemas: &IndexMap<&str, OpenApiType>,
|
schemas: &IndexMap<&str, OpenApiType>,
|
||||||
|
warnings: WarningReporter,
|
||||||
) -> Option<Self> {
|
) -> Option<Self> {
|
||||||
let name = name.to_owned();
|
let name = name.to_owned();
|
||||||
|
let field_name = name.to_snake_case();
|
||||||
let description = schema.description.as_deref().map(ToOwned::to_owned);
|
let description = schema.description.as_deref().map(ToOwned::to_owned);
|
||||||
|
|
||||||
match schema {
|
match schema {
|
||||||
OpenApiType {
|
OpenApiType {
|
||||||
r#enum: Some(_), ..
|
r#enum: Some(_), ..
|
||||||
} => Some(Self {
|
} => {
|
||||||
r#type: PropertyType::Enum(Enum::from_schema(
|
let Some(r#enum) = Enum::from_schema(&name.clone().to_upper_camel_case(), schema)
|
||||||
&name.clone().to_upper_camel_case(),
|
else {
|
||||||
schema,
|
warnings.push("Failed to create enum");
|
||||||
)?),
|
return None;
|
||||||
name,
|
};
|
||||||
description,
|
Some(Self {
|
||||||
required,
|
r#type: PropertyType::Enum(r#enum),
|
||||||
nullable: false,
|
name,
|
||||||
}),
|
field_name,
|
||||||
|
description,
|
||||||
|
required,
|
||||||
|
deprecated: schema.deprecated,
|
||||||
|
nullable: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
OpenApiType {
|
OpenApiType {
|
||||||
one_of: Some(types),
|
one_of: Some(types),
|
||||||
..
|
..
|
||||||
} => match types.as_slice() {
|
} => match types.as_slice() {
|
||||||
[
|
[left, OpenApiType {
|
||||||
left,
|
r#type: Some("null"),
|
||||||
OpenApiType {
|
..
|
||||||
r#type: Some("null"),
|
}] => {
|
||||||
..
|
let mut inner = Self::from_schema(&name, required, left, schemas, warnings)?;
|
||||||
},
|
|
||||||
] => {
|
|
||||||
let mut inner = Self::from_schema(&name, required, left, schemas)?;
|
|
||||||
inner.nullable = true;
|
inner.nullable = true;
|
||||||
Some(inner)
|
Some(inner)
|
||||||
}
|
}
|
||||||
[
|
[left @ .., OpenApiType {
|
||||||
left @ ..,
|
r#type: Some("null"),
|
||||||
OpenApiType {
|
..
|
||||||
r#type: Some("null"),
|
}] => {
|
||||||
..
|
|
||||||
},
|
|
||||||
] => {
|
|
||||||
let rest = OpenApiType {
|
let rest = OpenApiType {
|
||||||
one_of: Some(left.to_owned()),
|
one_of: Some(left.to_owned()),
|
||||||
..schema.clone()
|
..schema.clone()
|
||||||
};
|
};
|
||||||
let mut inner = Self::from_schema(&name, required, &rest, schemas)?;
|
let mut inner = Self::from_schema(&name, required, &rest, schemas, warnings)?;
|
||||||
inner.nullable = true;
|
inner.nullable = true;
|
||||||
Some(inner)
|
Some(inner)
|
||||||
}
|
}
|
||||||
cases => {
|
cases => {
|
||||||
let r#enum = Enum::from_one_of(&name.to_upper_camel_case(), cases)?;
|
let Some(r#enum) = Enum::from_one_of(&name.to_upper_camel_case(), cases) else {
|
||||||
|
warnings.push("Failed to create oneOf enum");
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
Some(Self {
|
Some(Self {
|
||||||
name,
|
name,
|
||||||
description: None,
|
field_name,
|
||||||
|
description,
|
||||||
required,
|
required,
|
||||||
nullable: false,
|
nullable: false,
|
||||||
|
deprecated: schema.deprecated,
|
||||||
r#type: PropertyType::Enum(r#enum),
|
r#type: PropertyType::Enum(r#enum),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -152,37 +170,49 @@ impl Property {
|
||||||
all_of: Some(types),
|
all_of: Some(types),
|
||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
let composite = Object::from_all_of(&name.to_upper_camel_case(), types, schemas)?;
|
let obj_name = name.to_upper_camel_case();
|
||||||
|
let composite =
|
||||||
|
Object::from_all_of(&obj_name, types, schemas, warnings.child(&obj_name));
|
||||||
Some(Self {
|
Some(Self {
|
||||||
name,
|
name,
|
||||||
description: None,
|
field_name,
|
||||||
|
description,
|
||||||
required,
|
required,
|
||||||
nullable: false,
|
nullable: false,
|
||||||
|
deprecated: schema.deprecated,
|
||||||
r#type: PropertyType::Nested(Box::new(composite)),
|
r#type: PropertyType::Nested(Box::new(composite)),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
OpenApiType {
|
OpenApiType {
|
||||||
r#type: Some("object"),
|
r#type: Some("object"),
|
||||||
..
|
..
|
||||||
} => Some(Self {
|
} => {
|
||||||
r#type: PropertyType::Nested(Box::new(Object::from_schema_object(
|
let obj_name = name.to_upper_camel_case();
|
||||||
&name.clone().to_upper_camel_case(),
|
Some(Self {
|
||||||
schema,
|
r#type: PropertyType::Nested(Box::new(Object::from_schema_object(
|
||||||
schemas,
|
&obj_name,
|
||||||
)?)),
|
schema,
|
||||||
name,
|
schemas,
|
||||||
description,
|
warnings.child(&obj_name),
|
||||||
required,
|
))),
|
||||||
nullable: false,
|
name,
|
||||||
}),
|
field_name,
|
||||||
|
description,
|
||||||
|
required,
|
||||||
|
deprecated: schema.deprecated,
|
||||||
|
nullable: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
OpenApiType {
|
OpenApiType {
|
||||||
ref_path: Some(path),
|
ref_path: Some(path),
|
||||||
..
|
..
|
||||||
} => Some(Self {
|
} => Some(Self {
|
||||||
name,
|
name,
|
||||||
|
field_name,
|
||||||
description,
|
description,
|
||||||
r#type: PropertyType::Ref((*path).to_owned()),
|
r#type: PropertyType::Ref((*path).to_owned()),
|
||||||
required,
|
required,
|
||||||
|
deprecated: schema.deprecated,
|
||||||
nullable: false,
|
nullable: false,
|
||||||
}),
|
}),
|
||||||
OpenApiType {
|
OpenApiType {
|
||||||
|
@ -190,13 +220,15 @@ impl Property {
|
||||||
items: Some(items),
|
items: Some(items),
|
||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
let inner = Self::from_schema(&name, required, items, schemas)?;
|
let inner = Self::from_schema(&name, required, items, schemas, warnings)?;
|
||||||
|
|
||||||
Some(Self {
|
Some(Self {
|
||||||
name,
|
name,
|
||||||
|
field_name,
|
||||||
description,
|
description,
|
||||||
required,
|
required,
|
||||||
nullable: false,
|
nullable: false,
|
||||||
|
deprecated: schema.deprecated,
|
||||||
r#type: PropertyType::Array(Box::new(inner.r#type)),
|
r#type: PropertyType::Array(Box::new(inner.r#type)),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -206,38 +238,50 @@ impl Property {
|
||||||
let prim = match (schema.r#type, schema.format) {
|
let prim = match (schema.r#type, schema.format) {
|
||||||
(Some("integer"), Some("int32")) => PrimitiveType::I32,
|
(Some("integer"), Some("int32")) => PrimitiveType::I32,
|
||||||
(Some("integer"), Some("int64")) => PrimitiveType::I64,
|
(Some("integer"), Some("int64")) => PrimitiveType::I64,
|
||||||
(Some("number"), Some("float")) => PrimitiveType::Float,
|
(Some("number"), /* Some("float") */ _) | (_, Some("float")) => {
|
||||||
|
PrimitiveType::Float
|
||||||
|
}
|
||||||
(Some("string"), None) => PrimitiveType::String,
|
(Some("string"), None) => PrimitiveType::String,
|
||||||
|
(Some("string"), Some("date")) => PrimitiveType::DateTime,
|
||||||
(Some("boolean"), None) => PrimitiveType::Bool,
|
(Some("boolean"), None) => PrimitiveType::Bool,
|
||||||
_ => return None,
|
_ => return None,
|
||||||
};
|
};
|
||||||
|
|
||||||
Some(Self {
|
Some(Self {
|
||||||
name,
|
name,
|
||||||
|
field_name,
|
||||||
description,
|
description,
|
||||||
required,
|
required,
|
||||||
nullable: false,
|
nullable: false,
|
||||||
|
deprecated: schema.deprecated,
|
||||||
r#type: PropertyType::Primitive(prim),
|
r#type: PropertyType::Primitive(prim),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
_ => None,
|
_ => {
|
||||||
|
warnings.push("Could not resolve property type");
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn codegen(&self, namespace: &mut ObjectNamespace) -> Option<TokenStream> {
|
pub fn codegen(
|
||||||
|
&self,
|
||||||
|
namespace: &mut ObjectNamespace,
|
||||||
|
resolved: &ResolvedSchema,
|
||||||
|
) -> Option<TokenStream> {
|
||||||
let desc = self.description.as_ref().map(|d| quote! { #[doc = #d]});
|
let desc = self.description.as_ref().map(|d| quote! { #[doc = #d]});
|
||||||
|
|
||||||
let name = &self.name;
|
let name = &self.name;
|
||||||
let (name, serde_attr) = match name.as_str() {
|
let (name, serde_attr) = match name.as_str() {
|
||||||
"type" => (format_ident!("r#type"), None),
|
"type" => (format_ident!("r#type"), None),
|
||||||
name if name != name.to_snake_case() => (
|
name if name != self.field_name => (
|
||||||
format_ident!("{}", name.to_snake_case()),
|
format_ident!("{}", self.field_name),
|
||||||
Some(quote! { #[serde(rename = #name)]}),
|
Some(quote! { #[serde(rename = #name)]}),
|
||||||
),
|
),
|
||||||
_ => (format_ident!("{name}"), None),
|
_ => (format_ident!("{}", self.field_name), None),
|
||||||
};
|
};
|
||||||
|
|
||||||
let ty_inner = self.r#type.codegen(namespace)?;
|
let ty_inner = self.r#type.codegen(namespace, resolved)?;
|
||||||
|
|
||||||
let ty = if !self.required || self.nullable {
|
let ty = if !self.required || self.nullable {
|
||||||
quote! { Option<#ty_inner> }
|
quote! { Option<#ty_inner> }
|
||||||
|
@ -245,8 +289,17 @@ impl Property {
|
||||||
ty_inner
|
ty_inner
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let deprecated = self.deprecated.then(|| {
|
||||||
|
let note = self.description.as_ref().map(|d| quote! { note = #d });
|
||||||
|
|
||||||
|
quote! {
|
||||||
|
#[deprecated(#note)]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
Some(quote! {
|
Some(quote! {
|
||||||
#desc
|
#desc
|
||||||
|
#deprecated
|
||||||
#serde_attr
|
#serde_attr
|
||||||
pub #name: #ty
|
pub #name: #ty
|
||||||
})
|
})
|
||||||
|
@ -257,7 +310,7 @@ impl Property {
|
||||||
pub struct Object {
|
pub struct Object {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub description: Option<String>,
|
pub description: Option<String>,
|
||||||
pub properties: Vec<Property>,
|
pub properties: IndexMap<String, Property>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Object {
|
impl Object {
|
||||||
|
@ -265,7 +318,8 @@ impl Object {
|
||||||
name: &str,
|
name: &str,
|
||||||
schema: &OpenApiType,
|
schema: &OpenApiType,
|
||||||
schemas: &IndexMap<&str, OpenApiType>,
|
schemas: &IndexMap<&str, OpenApiType>,
|
||||||
) -> Option<Self> {
|
warnings: WarningReporter,
|
||||||
|
) -> Self {
|
||||||
let mut result = Object {
|
let mut result = Object {
|
||||||
name: name.to_owned(),
|
name: name.to_owned(),
|
||||||
description: schema.description.as_deref().map(ToOwned::to_owned),
|
description: schema.description.as_deref().map(ToOwned::to_owned),
|
||||||
|
@ -273,38 +327,54 @@ impl Object {
|
||||||
};
|
};
|
||||||
|
|
||||||
let Some(props) = &schema.properties else {
|
let Some(props) = &schema.properties else {
|
||||||
return None;
|
warnings.push("Missing properties");
|
||||||
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
let required = schema.required.clone().unwrap_or_default();
|
let required = schema.required.clone().unwrap_or_default();
|
||||||
|
|
||||||
for (prop_name, prop) in props {
|
for (prop_name, prop) in props {
|
||||||
// HACK: This will cause a duplicate key otherwise
|
let Some(prop) = Property::from_schema(
|
||||||
if ["itemDetails", "sci-fi", "non-attackers", "co-leader_id"].contains(prop_name) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: implement custom enum for this (depends on overrides being added)
|
|
||||||
if *prop_name == "value" && name == "TornHof" {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
result.properties.push(Property::from_schema(
|
|
||||||
prop_name,
|
prop_name,
|
||||||
required.contains(prop_name),
|
required.contains(prop_name),
|
||||||
prop,
|
prop,
|
||||||
schemas,
|
schemas,
|
||||||
)?);
|
warnings.child(prop_name),
|
||||||
|
) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
let field_name = prop.field_name.clone();
|
||||||
|
|
||||||
|
let entry = result.properties.entry(field_name.clone());
|
||||||
|
if let Entry::Occupied(mut entry) = entry {
|
||||||
|
let other_name = entry.get().name.clone();
|
||||||
|
warnings.push(format!(
|
||||||
|
"Property name collision: {other_name} and {field_name}"
|
||||||
|
));
|
||||||
|
// deprioritise kebab and camelcase
|
||||||
|
if other_name.contains('-')
|
||||||
|
|| other_name
|
||||||
|
.chars()
|
||||||
|
.filter(|c| c.is_alphabetic())
|
||||||
|
.all(|c| c.is_ascii_lowercase())
|
||||||
|
{
|
||||||
|
entry.insert(prop);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
entry.insert_entry(prop);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(result)
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_all_of(
|
pub fn from_all_of(
|
||||||
name: &str,
|
name: &str,
|
||||||
types: &[OpenApiType],
|
types: &[OpenApiType],
|
||||||
schemas: &IndexMap<&str, OpenApiType>,
|
schemas: &IndexMap<&str, OpenApiType>,
|
||||||
) -> Option<Self> {
|
warnings: WarningReporter,
|
||||||
|
) -> Self {
|
||||||
let mut result = Self {
|
let mut result = Self {
|
||||||
name: name.to_owned(),
|
name: name.to_owned(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
|
@ -312,25 +382,32 @@ impl Object {
|
||||||
|
|
||||||
for r#type in types {
|
for r#type in types {
|
||||||
let r#type = if let OpenApiType {
|
let r#type = if let OpenApiType {
|
||||||
ref_path: Some(path),
|
ref_path: Some(ref_path),
|
||||||
..
|
..
|
||||||
} = r#type
|
} = r#type
|
||||||
{
|
{
|
||||||
let name = path.strip_prefix("#/components/schemas/")?;
|
let Some(name) = ref_path.strip_prefix("#/components/schemas/") else {
|
||||||
schemas.get(name)?
|
warnings.push(format!("Malformed ref {ref_path}"));
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Some(schema) = schemas.get(name) else {
|
||||||
|
warnings.push(format!("Missing schema for ref {name}"));
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
schema
|
||||||
} else {
|
} else {
|
||||||
r#type
|
r#type
|
||||||
};
|
};
|
||||||
let obj = Self::from_schema_object(name, r#type, schemas)?;
|
let obj = Self::from_schema_object(name, r#type, schemas, warnings.child("variant"));
|
||||||
|
|
||||||
result.description = result.description.or(obj.description);
|
result.description = result.description.or(obj.description);
|
||||||
result.properties.extend(obj.properties);
|
result.properties.extend(obj.properties);
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(result)
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn codegen(&self) -> Option<TokenStream> {
|
pub fn codegen(&self, resolved: &ResolvedSchema) -> Option<TokenStream> {
|
||||||
let doc = self.description.as_ref().map(|d| {
|
let doc = self.description.as_ref().map(|d| {
|
||||||
quote! {
|
quote! {
|
||||||
#[doc = #d]
|
#[doc = #d]
|
||||||
|
@ -344,8 +421,8 @@ impl Object {
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut props = Vec::with_capacity(self.properties.len());
|
let mut props = Vec::with_capacity(self.properties.len());
|
||||||
for prop in &self.properties {
|
for (_, prop) in &self.properties {
|
||||||
props.push(prop.codegen(&mut namespace)?);
|
props.push(prop.codegen(&mut namespace, resolved)?);
|
||||||
}
|
}
|
||||||
|
|
||||||
let name = format_ident!("{}", self.name);
|
let name = format_ident!("{}", self.name);
|
||||||
|
@ -402,23 +479,11 @@ impl ObjectNamespace<'_> {
|
||||||
mod test {
|
mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
use crate::openapi::schema::OpenApiSchema;
|
use crate::openapi::schema::test::get_schema;
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn resolve_object() {
|
|
||||||
let schema = OpenApiSchema::read().unwrap();
|
|
||||||
|
|
||||||
let attack = schema.components.schemas.get("FactionUpgrades").unwrap();
|
|
||||||
|
|
||||||
let resolved =
|
|
||||||
Object::from_schema_object("FactionUpgrades", attack, &schema.components.schemas)
|
|
||||||
.unwrap();
|
|
||||||
let _code = resolved.codegen().unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn resolve_objects() {
|
fn resolve_objects() {
|
||||||
let schema = OpenApiSchema::read().unwrap();
|
let schema = get_schema();
|
||||||
|
|
||||||
let mut objects = 0;
|
let mut objects = 0;
|
||||||
let mut unresolved = vec![];
|
let mut unresolved = vec![];
|
||||||
|
@ -426,7 +491,14 @@ mod test {
|
||||||
for (name, desc) in &schema.components.schemas {
|
for (name, desc) in &schema.components.schemas {
|
||||||
if desc.r#type == Some("object") {
|
if desc.r#type == Some("object") {
|
||||||
objects += 1;
|
objects += 1;
|
||||||
if Object::from_schema_object(name, desc, &schema.components.schemas).is_none() {
|
let reporter = WarningReporter::new();
|
||||||
|
Object::from_schema_object(
|
||||||
|
name,
|
||||||
|
desc,
|
||||||
|
&schema.components.schemas,
|
||||||
|
reporter.clone(),
|
||||||
|
);
|
||||||
|
if !reporter.is_empty() {
|
||||||
unresolved.push(name);
|
unresolved.push(name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,14 +2,14 @@ use std::fmt::Write;
|
||||||
|
|
||||||
use heck::ToUpperCamelCase;
|
use heck::ToUpperCamelCase;
|
||||||
use proc_macro2::TokenStream;
|
use proc_macro2::TokenStream;
|
||||||
use quote::{ToTokens, format_ident, quote};
|
use quote::{format_ident, quote, ToTokens};
|
||||||
|
|
||||||
use crate::openapi::parameter::{
|
use crate::openapi::parameter::{
|
||||||
OpenApiParameter, OpenApiParameterDefault, OpenApiParameterSchema,
|
OpenApiParameter, OpenApiParameterDefault, OpenApiParameterSchema,
|
||||||
ParameterLocation as SchemaLocation,
|
ParameterLocation as SchemaLocation,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::r#enum::Enum;
|
use super::{r#enum::Enum, ResolvedSchema};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct ParameterOptions<P> {
|
pub struct ParameterOptions<P> {
|
||||||
|
@ -42,9 +42,7 @@ impl ParameterType {
|
||||||
match schema {
|
match schema {
|
||||||
OpenApiParameterSchema {
|
OpenApiParameterSchema {
|
||||||
r#type: Some("integer"),
|
r#type: Some("integer"),
|
||||||
// BUG: missing for some types in the spec
|
format: Some("int32"),
|
||||||
|
|
||||||
// format: Some("int32"),
|
|
||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
let default = match schema.default {
|
let default = match schema.default {
|
||||||
|
@ -90,6 +88,17 @@ impl ParameterType {
|
||||||
r#type: Enum::from_parameter_schema(name, schema)?,
|
r#type: Enum::from_parameter_schema(name, schema)?,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
OpenApiParameterSchema {
|
||||||
|
one_of: Some(schemas),
|
||||||
|
..
|
||||||
|
} => Some(ParameterType::Enum {
|
||||||
|
options: ParameterOptions {
|
||||||
|
default: None,
|
||||||
|
minimum: None,
|
||||||
|
maximum: None,
|
||||||
|
},
|
||||||
|
r#type: Enum::from_one_of(name, schemas)?,
|
||||||
|
}),
|
||||||
OpenApiParameterSchema {
|
OpenApiParameterSchema {
|
||||||
r#type: Some("string"),
|
r#type: Some("string"),
|
||||||
..
|
..
|
||||||
|
@ -170,7 +179,7 @@ impl Parameter {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn codegen(&self) -> Option<TokenStream> {
|
pub fn codegen(&self, resolved: &ResolvedSchema) -> Option<TokenStream> {
|
||||||
match &self.r#type {
|
match &self.r#type {
|
||||||
ParameterType::I32 { options } => {
|
ParameterType::I32 { options } => {
|
||||||
let name = format_ident!("{}", self.name);
|
let name = format_ident!("{}", self.name);
|
||||||
|
@ -274,7 +283,7 @@ The default value [Self::{}](self::{}#variant.{})"#,
|
||||||
}
|
}
|
||||||
|
|
||||||
let doc = quote! { #[doc = #desc]};
|
let doc = quote! { #[doc = #desc]};
|
||||||
let inner = r#type.codegen()?;
|
let inner = r#type.codegen(resolved)?;
|
||||||
|
|
||||||
Some(quote! {
|
Some(quote! {
|
||||||
#doc
|
#doc
|
||||||
|
@ -300,13 +309,13 @@ The default value [Self::{}](self::{}#variant.{})"#,
|
||||||
..self.clone()
|
..self.clone()
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut code = inner.codegen().unwrap_or_default();
|
let mut code = inner.codegen(resolved).unwrap_or_default();
|
||||||
|
|
||||||
let name = format_ident!("{}", outer_name);
|
let name = format_ident!("{}", outer_name);
|
||||||
let inner_ty = items.codegen_type_name(&inner_name);
|
let inner_ty = items.codegen_type_name(&inner_name);
|
||||||
|
|
||||||
code.extend(quote! {
|
code.extend(quote! {
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
pub struct #name(pub Vec<#inner_ty>);
|
pub struct #name(pub Vec<#inner_ty>);
|
||||||
|
|
||||||
impl std::fmt::Display for #name {
|
impl std::fmt::Display for #name {
|
||||||
|
@ -324,9 +333,9 @@ The default value [Self::{}](self::{}#variant.{})"#,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> From<T> for #name where T: IntoIterator<Item = #inner_ty> {
|
impl<T> From<T> for #name where T: IntoIterator, T::Item: Into<#inner_ty> {
|
||||||
fn from(value: T) -> #name {
|
fn from(value: T) -> #name {
|
||||||
let items = value.into_iter().collect();
|
let items = value.into_iter().map(Into::into).collect();
|
||||||
|
|
||||||
Self(items)
|
Self(items)
|
||||||
}
|
}
|
||||||
|
@ -342,13 +351,13 @@ The default value [Self::{}](self::{}#variant.{})"#,
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use crate::openapi::{path::OpenApiPathParameter, schema::OpenApiSchema};
|
use crate::openapi::{path::OpenApiPathParameter, schema::test::get_schema};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn resolve_components() {
|
fn resolve_components() {
|
||||||
let schema = OpenApiSchema::read().unwrap();
|
let schema = get_schema();
|
||||||
|
|
||||||
let mut parameters = 0;
|
let mut parameters = 0;
|
||||||
let mut unresolved = vec![];
|
let mut unresolved = vec![];
|
||||||
|
@ -376,7 +385,7 @@ mod test {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn resolve_inline() {
|
fn resolve_inline() {
|
||||||
let schema = OpenApiSchema::read().unwrap();
|
let schema = get_schema();
|
||||||
|
|
||||||
let mut params = 0;
|
let mut params = 0;
|
||||||
let mut unresolved = Vec::new();
|
let mut unresolved = Vec::new();
|
||||||
|
@ -404,7 +413,8 @@ mod test {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn codegen_inline() {
|
fn codegen_inline() {
|
||||||
let schema = OpenApiSchema::read().unwrap();
|
let schema = get_schema();
|
||||||
|
let resolved = ResolvedSchema::from_open_api(&schema);
|
||||||
|
|
||||||
let mut params = 0;
|
let mut params = 0;
|
||||||
let mut unresolved = Vec::new();
|
let mut unresolved = Vec::new();
|
||||||
|
@ -425,7 +435,7 @@ mod test {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
params += 1;
|
params += 1;
|
||||||
if param.codegen().is_none() {
|
if param.codegen(&resolved).is_none() {
|
||||||
unresolved.push(format!("`{}.{}`", path, inline.name));
|
unresolved.push(format!("`{}.{}`", path, inline.name));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use std::{fmt::Write, ops::Deref};
|
use std::fmt::Write;
|
||||||
|
|
||||||
use heck::{ToSnakeCase, ToUpperCamelCase};
|
use heck::{ToSnakeCase, ToUpperCamelCase};
|
||||||
use indexmap::IndexMap;
|
use indexmap::IndexMap;
|
||||||
|
@ -14,6 +14,7 @@ use crate::openapi::{
|
||||||
use super::{
|
use super::{
|
||||||
parameter::{Parameter, ParameterLocation, ParameterType},
|
parameter::{Parameter, ParameterLocation, ParameterType},
|
||||||
union::Union,
|
union::Union,
|
||||||
|
ResolvedSchema, WarningReporter,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
@ -22,6 +23,21 @@ pub enum PathSegment {
|
||||||
Parameter { name: String },
|
Parameter { name: String },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct PrettySegments<'a>(pub &'a [PathSegment]);
|
||||||
|
|
||||||
|
impl std::fmt::Display for PrettySegments<'_> {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
for segment in self.0 {
|
||||||
|
match segment {
|
||||||
|
PathSegment::Constant(c) => write!(f, "/{c}")?,
|
||||||
|
PathSegment::Parameter { name } => write!(f, "/{{{name}}}")?,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum PathParameter {
|
pub enum PathParameter {
|
||||||
Inline(Parameter),
|
Inline(Parameter),
|
||||||
|
@ -40,7 +56,7 @@ pub struct Path {
|
||||||
pub segments: Vec<PathSegment>,
|
pub segments: Vec<PathSegment>,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub summary: Option<String>,
|
pub summary: Option<String>,
|
||||||
pub description: String,
|
pub description: Option<String>,
|
||||||
pub parameters: Vec<PathParameter>,
|
pub parameters: Vec<PathParameter>,
|
||||||
pub response: PathResponse,
|
pub response: PathResponse,
|
||||||
}
|
}
|
||||||
|
@ -50,6 +66,7 @@ impl Path {
|
||||||
path: &str,
|
path: &str,
|
||||||
schema: &OpenApiPath,
|
schema: &OpenApiPath,
|
||||||
parameters: &IndexMap<&str, OpenApiParameter>,
|
parameters: &IndexMap<&str, OpenApiParameter>,
|
||||||
|
warnings: WarningReporter,
|
||||||
) -> Option<Self> {
|
) -> Option<Self> {
|
||||||
let mut segments = Vec::new();
|
let mut segments = Vec::new();
|
||||||
for segment in path.strip_prefix('/')?.split('/') {
|
for segment in path.strip_prefix('/')?.split('/') {
|
||||||
|
@ -63,7 +80,7 @@ impl Path {
|
||||||
}
|
}
|
||||||
|
|
||||||
let summary = schema.get.summary.as_deref().map(ToOwned::to_owned);
|
let summary = schema.get.summary.as_deref().map(ToOwned::to_owned);
|
||||||
let description = schema.get.description.deref().to_owned();
|
let description = schema.get.description.as_deref().map(ToOwned::to_owned);
|
||||||
|
|
||||||
let mut params = Vec::with_capacity(schema.get.parameters.len());
|
let mut params = Vec::with_capacity(schema.get.parameters.len());
|
||||||
for parameter in &schema.get.parameters {
|
for parameter in &schema.get.parameters {
|
||||||
|
@ -110,9 +127,13 @@ impl Path {
|
||||||
.strip_prefix("#/components/schemas/")?
|
.strip_prefix("#/components/schemas/")?
|
||||||
.to_owned(),
|
.to_owned(),
|
||||||
},
|
},
|
||||||
OpenApiResponseBody::Union { any_of: _ } => PathResponse::ArbitraryUnion(
|
OpenApiResponseBody::Union { any_of: _ } => {
|
||||||
Union::from_schema("Response", &schema.get.response_content)?,
|
PathResponse::ArbitraryUnion(Union::from_schema(
|
||||||
),
|
"Response",
|
||||||
|
&schema.get.response_content,
|
||||||
|
warnings.child("response"),
|
||||||
|
)?)
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Some(Self {
|
Some(Self {
|
||||||
|
@ -125,7 +146,11 @@ impl Path {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn codegen_request(&self) -> Option<TokenStream> {
|
pub fn codegen_request(
|
||||||
|
&self,
|
||||||
|
resolved: &ResolvedSchema,
|
||||||
|
warnings: WarningReporter,
|
||||||
|
) -> Option<TokenStream> {
|
||||||
let name = if self.segments.len() == 1 {
|
let name = if self.segments.len() == 1 {
|
||||||
let Some(PathSegment::Constant(first)) = self.segments.first() else {
|
let Some(PathSegment::Constant(first)) = self.segments.first() else {
|
||||||
return None;
|
return None;
|
||||||
|
@ -159,21 +184,21 @@ impl Path {
|
||||||
let ty_name = format_ident!("{}", param.name);
|
let ty_name = format_ident!("{}", param.name);
|
||||||
|
|
||||||
if is_inline {
|
if is_inline {
|
||||||
ns.push_element(param.codegen()?);
|
ns.push_element(param.codegen(resolved)?);
|
||||||
let path = ns.get_ident();
|
let path = ns.get_ident();
|
||||||
|
|
||||||
(
|
(
|
||||||
quote! {
|
quote! {
|
||||||
crate::request::models::#path::#ty_name
|
crate::request::models::#path::#ty_name
|
||||||
},
|
},
|
||||||
Some(quote! { #[builder(into)] }),
|
Some(quote! { #[cfg_attr(feature = "builder", builder(into))] }),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
(
|
(
|
||||||
quote! {
|
quote! {
|
||||||
crate::parameters::#ty_name
|
crate::parameters::#ty_name
|
||||||
},
|
},
|
||||||
Some(quote! { #[builder(into)]}),
|
Some(quote! { #[cfg_attr(feature = "builder", builder(into))]}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -190,14 +215,14 @@ impl Path {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
ParameterType::Array { .. } => {
|
ParameterType::Array { .. } => {
|
||||||
ns.push_element(param.codegen()?);
|
ns.push_element(param.codegen(resolved)?);
|
||||||
let ty_name = param.r#type.codegen_type_name(¶m.name);
|
let ty_name = param.r#type.codegen_type_name(¶m.name);
|
||||||
let path = ns.get_ident();
|
let path = ns.get_ident();
|
||||||
(
|
(
|
||||||
quote! {
|
quote! {
|
||||||
crate::request::models::#path::#ty_name
|
crate::request::models::#path::#ty_name
|
||||||
},
|
},
|
||||||
Some(quote! { #[builder(into)] }),
|
Some(quote! { #[cfg_attr(feature = "builder", builder(into))] }),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -206,17 +231,30 @@ impl Path {
|
||||||
let query_val = ¶m.value;
|
let query_val = ¶m.value;
|
||||||
|
|
||||||
if param.location == ParameterLocation::Path {
|
if param.location == ParameterLocation::Path {
|
||||||
discriminant.push(ty.clone());
|
if self.segments.iter().any(|s| {
|
||||||
discriminant_val.push(quote! { self.#name });
|
if let PathSegment::Parameter { name } = s {
|
||||||
let path_name = format_ident!("{}", param.value);
|
name == ¶m.value
|
||||||
start_fields.push(quote! {
|
} else {
|
||||||
#[builder(start_fn)]
|
false
|
||||||
#builder_param
|
}
|
||||||
pub #name: #ty
|
}) {
|
||||||
});
|
discriminant.push(ty.clone());
|
||||||
fmt_val.push(quote! {
|
discriminant_val.push(quote! { self.#name });
|
||||||
#path_name=self.#name
|
let path_name = format_ident!("{}", param.value);
|
||||||
});
|
start_fields.push(quote! {
|
||||||
|
#[cfg_attr(feature = "builder", builder(start_fn))]
|
||||||
|
#builder_param
|
||||||
|
pub #name: #ty
|
||||||
|
});
|
||||||
|
fmt_val.push(quote! {
|
||||||
|
#path_name=self.#name
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
warnings.push(format!(
|
||||||
|
"Provided path parameter is not present in the url: {}",
|
||||||
|
param.value
|
||||||
|
));
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
let ty = if param.required {
|
let ty = if param.required {
|
||||||
convert_field.push(quote! {
|
convert_field.push(quote! {
|
||||||
|
@ -273,8 +311,9 @@ impl Path {
|
||||||
Some(quote! {
|
Some(quote! {
|
||||||
#ns
|
#ns
|
||||||
|
|
||||||
#[derive(Debug, Clone, bon::Builder)]
|
#[cfg_attr(feature = "builder", derive(bon::Builder))]
|
||||||
#[builder(state_mod(vis = "pub(crate)"), on(String, into))]
|
#[derive(Debug, Clone)]
|
||||||
|
#[cfg_attr(feature = "builder", builder(state_mod(vis = "pub(crate)"), on(String, into)))]
|
||||||
pub struct #name {
|
pub struct #name {
|
||||||
#(#start_fields),*
|
#(#start_fields),*
|
||||||
}
|
}
|
||||||
|
@ -283,15 +322,18 @@ impl Path {
|
||||||
#[allow(unused_parens)]
|
#[allow(unused_parens)]
|
||||||
type Discriminant = (#(#discriminant),*);
|
type Discriminant = (#(#discriminant),*);
|
||||||
type Response = #response_ty;
|
type Response = #response_ty;
|
||||||
fn into_request(self) -> crate::request::ApiRequest<Self::Discriminant> {
|
fn into_request(self) -> (Self::Discriminant, crate::request::ApiRequest) {
|
||||||
|
let path = format!(#path_fmt_str, #(#fmt_val),*);
|
||||||
#[allow(unused_parens)]
|
#[allow(unused_parens)]
|
||||||
crate::request::ApiRequest {
|
(
|
||||||
path: format!(#path_fmt_str, #(#fmt_val),*),
|
(#(#discriminant_val),*),
|
||||||
parameters: std::iter::empty()
|
crate::request::ApiRequest {
|
||||||
#(#convert_field)*
|
path,
|
||||||
.collect(),
|
parameters: std::iter::empty()
|
||||||
disriminant: (#(#discriminant_val),*),
|
#(#convert_field)*
|
||||||
}
|
.collect(),
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -324,7 +366,15 @@ impl Path {
|
||||||
PathParameter::Component(param) => (param, false),
|
PathParameter::Component(param) => (param, false),
|
||||||
};
|
};
|
||||||
|
|
||||||
if param.location == ParameterLocation::Path {
|
if param.location == ParameterLocation::Path
|
||||||
|
&& self.segments.iter().any(|s| {
|
||||||
|
if let PathSegment::Parameter { name } = s {
|
||||||
|
name == ¶m.value
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
{
|
||||||
let ty = match ¶m.r#type {
|
let ty = match ¶m.r#type {
|
||||||
ParameterType::I32 { .. } | ParameterType::Enum { .. } => {
|
ParameterType::I32 { .. } | ParameterType::Enum { .. } => {
|
||||||
let ty_name = format_ident!("{}", param.name);
|
let ty_name = format_ident!("{}", param.name);
|
||||||
|
@ -348,7 +398,13 @@ impl Path {
|
||||||
crate::models::#ty_name
|
crate::models::#ty_name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ParameterType::Array { .. } => param.r#type.codegen_type_name(¶m.name),
|
ParameterType::Array { .. } => {
|
||||||
|
let ty_name = param.r#type.codegen_type_name(¶m.name);
|
||||||
|
|
||||||
|
quote! {
|
||||||
|
crate::request::models::#request_mod_name::#ty_name
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let arg_name = format_ident!("{}", param.value.to_snake_case());
|
let arg_name = format_ident!("{}", param.value.to_snake_case());
|
||||||
|
@ -373,9 +429,25 @@ impl Path {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let doc = match (&self.summary, &self.description) {
|
||||||
|
(Some(summary), Some(description)) => {
|
||||||
|
Some(format!("{summary}\n\n# Description\n{description}"))
|
||||||
|
}
|
||||||
|
(Some(summary), None) => Some(summary.clone()),
|
||||||
|
(None, Some(description)) => Some(format!("# Description\n{description}")),
|
||||||
|
(None, None) => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let doc = doc.map(|d| {
|
||||||
|
quote! {
|
||||||
|
#[doc = #d]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
Some(quote! {
|
Some(quote! {
|
||||||
|
#doc
|
||||||
pub async fn #fn_name<S>(
|
pub async fn #fn_name<S>(
|
||||||
&self,
|
self,
|
||||||
#(#extra_args)*
|
#(#extra_args)*
|
||||||
builder: impl FnOnce(
|
builder: impl FnOnce(
|
||||||
#builder_path<#builder_mod_path::Empty>
|
#builder_path<#builder_mod_path::Empty>
|
||||||
|
@ -390,6 +462,148 @@ impl Path {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn codegen_bulk_scope_call(&self) -> Option<TokenStream> {
|
||||||
|
let mut disc = Vec::new();
|
||||||
|
let mut disc_ty = Vec::new();
|
||||||
|
|
||||||
|
let snake_name = self.name.to_snake_case();
|
||||||
|
|
||||||
|
let request_name = format_ident!("{}Request", self.name);
|
||||||
|
let builder_name = format_ident!("{}RequestBuilder", self.name);
|
||||||
|
let builder_mod_name = format_ident!("{}_request_builder", snake_name);
|
||||||
|
let request_mod_name = format_ident!("{snake_name}");
|
||||||
|
|
||||||
|
let request_path = quote! { crate::request::models::#request_name };
|
||||||
|
let builder_path = quote! { crate::request::models::#builder_name };
|
||||||
|
let builder_mod_path = quote! { crate::request::models::#builder_mod_name };
|
||||||
|
|
||||||
|
let tail = snake_name
|
||||||
|
.split_once('_')
|
||||||
|
.map_or_else(|| "for_selections".to_owned(), |(_, tail)| tail.to_owned());
|
||||||
|
|
||||||
|
let fn_name = format_ident!("{tail}");
|
||||||
|
|
||||||
|
for param in &self.parameters {
|
||||||
|
let (param, is_inline) = match param {
|
||||||
|
PathParameter::Inline(param) => (param, true),
|
||||||
|
PathParameter::Component(param) => (param, false),
|
||||||
|
};
|
||||||
|
if param.location == ParameterLocation::Path
|
||||||
|
&& self.segments.iter().any(|s| {
|
||||||
|
if let PathSegment::Parameter { name } = s {
|
||||||
|
name == ¶m.value
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
{
|
||||||
|
let ty = match ¶m.r#type {
|
||||||
|
ParameterType::I32 { .. } | ParameterType::Enum { .. } => {
|
||||||
|
let ty_name = format_ident!("{}", param.name);
|
||||||
|
|
||||||
|
if is_inline {
|
||||||
|
quote! {
|
||||||
|
crate::request::models::#request_mod_name::#ty_name
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
quote! {
|
||||||
|
crate::parameters::#ty_name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ParameterType::String => quote! { String },
|
||||||
|
ParameterType::Boolean => quote! { bool },
|
||||||
|
ParameterType::Schema { type_name } => {
|
||||||
|
let ty_name = format_ident!("{}", type_name);
|
||||||
|
|
||||||
|
quote! {
|
||||||
|
crate::models::#ty_name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ParameterType::Array { .. } => {
|
||||||
|
let name = param.r#type.codegen_type_name(¶m.name);
|
||||||
|
quote! {
|
||||||
|
crate::request::models::#request_mod_name::#name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let arg_name = format_ident!("{}", param.value.to_snake_case());
|
||||||
|
|
||||||
|
disc_ty.push(ty);
|
||||||
|
disc.push(arg_name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if disc.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let response_ty = match &self.response {
|
||||||
|
PathResponse::Component { name } => {
|
||||||
|
let name = format_ident!("{name}");
|
||||||
|
quote! {
|
||||||
|
crate::models::#name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PathResponse::ArbitraryUnion(union) => {
|
||||||
|
let name = format_ident!("{}", union.name);
|
||||||
|
quote! {
|
||||||
|
crate::request::models::#request_mod_name::#name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let disc = if disc.len() > 1 {
|
||||||
|
quote! { (#(#disc),*) }
|
||||||
|
} else {
|
||||||
|
quote! { #(#disc),* }
|
||||||
|
};
|
||||||
|
|
||||||
|
let disc_ty = if disc_ty.len() > 1 {
|
||||||
|
quote! { (#(#disc_ty),*) }
|
||||||
|
} else {
|
||||||
|
quote! { #(#disc_ty),* }
|
||||||
|
};
|
||||||
|
|
||||||
|
let doc = match (&self.summary, &self.description) {
|
||||||
|
(Some(summary), Some(description)) => {
|
||||||
|
Some(format!("{summary}\n\n# Description\n{description}"))
|
||||||
|
}
|
||||||
|
(Some(summary), None) => Some(summary.clone()),
|
||||||
|
(None, Some(description)) => Some(format!("# Description\n{description}")),
|
||||||
|
(None, None) => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let doc = doc.map(|d| {
|
||||||
|
quote! {
|
||||||
|
#[doc = #d]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Some(quote! {
|
||||||
|
#doc
|
||||||
|
pub fn #fn_name<S, I, B>(
|
||||||
|
self,
|
||||||
|
ids: I,
|
||||||
|
builder: B
|
||||||
|
) -> impl futures::Stream<Item = (#disc_ty, Result<#response_ty, E::Error>)>
|
||||||
|
where
|
||||||
|
I: IntoIterator<Item = #disc_ty>,
|
||||||
|
S: #builder_mod_path::IsComplete,
|
||||||
|
B: Fn(
|
||||||
|
#builder_path<#builder_mod_path::Empty>
|
||||||
|
) -> #builder_path<S>,
|
||||||
|
{
|
||||||
|
let requests = ids.into_iter()
|
||||||
|
.map(move |#disc| builder(#request_path::builder(#disc)).build());
|
||||||
|
|
||||||
|
let executor = self.executor;
|
||||||
|
executor.fetch_many(requests)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct PathNamespace<'r> {
|
pub struct PathNamespace<'r> {
|
||||||
|
@ -431,18 +645,25 @@ impl PathNamespace<'_> {
|
||||||
mod test {
|
mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
use crate::openapi::schema::OpenApiSchema;
|
use crate::openapi::schema::test::get_schema;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn resolve_paths() {
|
fn resolve_paths() {
|
||||||
let schema = OpenApiSchema::read().unwrap();
|
let schema = get_schema();
|
||||||
|
|
||||||
let mut paths = 0;
|
let mut paths = 0;
|
||||||
let mut unresolved = vec![];
|
let mut unresolved = vec![];
|
||||||
|
|
||||||
for (name, desc) in &schema.paths {
|
for (name, desc) in &schema.paths {
|
||||||
paths += 1;
|
paths += 1;
|
||||||
if Path::from_schema(name, desc, &schema.components.parameters).is_none() {
|
if Path::from_schema(
|
||||||
|
name,
|
||||||
|
desc,
|
||||||
|
&schema.components.parameters,
|
||||||
|
WarningReporter::new(),
|
||||||
|
)
|
||||||
|
.is_none()
|
||||||
|
{
|
||||||
unresolved.push(name);
|
unresolved.push(name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -463,19 +684,25 @@ mod test {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn codegen_paths() {
|
fn codegen_paths() {
|
||||||
let schema = OpenApiSchema::read().unwrap();
|
let schema = get_schema();
|
||||||
|
let resolved = ResolvedSchema::from_open_api(&schema);
|
||||||
|
let reporter = WarningReporter::new();
|
||||||
|
|
||||||
let mut paths = 0;
|
let mut paths = 0;
|
||||||
let mut unresolved = vec![];
|
let mut unresolved = vec![];
|
||||||
|
|
||||||
for (name, desc) in &schema.paths {
|
for (name, desc) in &schema.paths {
|
||||||
paths += 1;
|
paths += 1;
|
||||||
let Some(path) = Path::from_schema(name, desc, &schema.components.parameters) else {
|
let Some(path) =
|
||||||
|
Path::from_schema(name, desc, &schema.components.parameters, reporter.clone())
|
||||||
|
else {
|
||||||
unresolved.push(name);
|
unresolved.push(name);
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
if path.codegen_scope_call().is_none() || path.codegen_request().is_none() {
|
if path.codegen_scope_call().is_none()
|
||||||
|
|| path.codegen_request(&resolved, reporter.clone()).is_none()
|
||||||
|
{
|
||||||
unresolved.push(name);
|
unresolved.push(name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,30 +35,56 @@ impl Scope {
|
||||||
|
|
||||||
pub fn codegen(&self) -> Option<TokenStream> {
|
pub fn codegen(&self) -> Option<TokenStream> {
|
||||||
let name = format_ident!("{}", self.name);
|
let name = format_ident!("{}", self.name);
|
||||||
|
let bulk_name = format_ident!("Bulk{}", self.name);
|
||||||
|
|
||||||
let mut functions = Vec::with_capacity(self.members.len());
|
let mut functions = Vec::with_capacity(self.members.len());
|
||||||
|
let mut bulk_functions = Vec::with_capacity(self.members.len());
|
||||||
|
|
||||||
for member in &self.members {
|
for member in &self.members {
|
||||||
if let Some(code) = member.codegen_scope_call() {
|
if let Some(code) = member.codegen_scope_call() {
|
||||||
functions.push(code);
|
functions.push(code);
|
||||||
}
|
}
|
||||||
|
if let Some(code) = member.codegen_bulk_scope_call() {
|
||||||
|
bulk_functions.push(code);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(quote! {
|
Some(quote! {
|
||||||
pub struct #name<'e, E>(&'e E)
|
#[allow(dead_code)]
|
||||||
|
pub struct #name<E>(E)
|
||||||
where
|
where
|
||||||
E: crate::executor::Executor;
|
E: crate::executor::Executor;
|
||||||
|
|
||||||
impl<'e, E> #name<'e, E>
|
impl<E> #name<E>
|
||||||
where
|
where
|
||||||
E: crate::executor::Executor
|
E: crate::executor::Executor
|
||||||
{
|
{
|
||||||
pub fn new(executor: &'e E) -> Self {
|
pub fn new(executor: E) -> Self {
|
||||||
Self(executor)
|
Self(executor)
|
||||||
}
|
}
|
||||||
|
|
||||||
#(#functions)*
|
#(#functions)*
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub struct #bulk_name<E> where
|
||||||
|
E: crate::executor::BulkExecutor,
|
||||||
|
{
|
||||||
|
executor: E,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<E> #bulk_name<E>
|
||||||
|
where
|
||||||
|
E: crate::executor::BulkExecutor
|
||||||
|
{
|
||||||
|
pub fn new(executor: E) -> Self {
|
||||||
|
Self {
|
||||||
|
executor,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#(#bulk_functions)*
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,8 @@ use quote::{format_ident, quote};
|
||||||
|
|
||||||
use crate::openapi::path::OpenApiResponseBody;
|
use crate::openapi::path::OpenApiResponseBody;
|
||||||
|
|
||||||
|
use super::WarningReporter;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Union {
|
pub struct Union {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
@ -11,10 +13,23 @@ pub struct Union {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Union {
|
impl Union {
|
||||||
pub fn from_schema(name: &str, schema: &OpenApiResponseBody) -> Option<Self> {
|
pub fn from_schema(
|
||||||
|
name: &str,
|
||||||
|
schema: &OpenApiResponseBody,
|
||||||
|
warnings: WarningReporter,
|
||||||
|
) -> Option<Self> {
|
||||||
let members = match schema {
|
let members = match schema {
|
||||||
OpenApiResponseBody::Union { any_of } => {
|
OpenApiResponseBody::Union { any_of } => {
|
||||||
any_of.iter().map(|l| l.ref_path.to_owned()).collect()
|
let mut members = Vec::with_capacity(any_of.len());
|
||||||
|
for l in any_of {
|
||||||
|
let path = l.ref_path.to_owned();
|
||||||
|
if members.contains(&path) {
|
||||||
|
warnings.push(format!("Duplicate member: {path}"));
|
||||||
|
} else {
|
||||||
|
members.push(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
members
|
||||||
}
|
}
|
||||||
_ => return None,
|
_ => return None,
|
||||||
};
|
};
|
||||||
|
@ -33,7 +48,7 @@ impl Union {
|
||||||
let ty_name = format_ident!("{}", variant_name);
|
let ty_name = format_ident!("{}", variant_name);
|
||||||
variants.push(quote! {
|
variants.push(quote! {
|
||||||
pub fn #accessor_name(&self) -> Result<crate::models::#ty_name, serde_json::Error> {
|
pub fn #accessor_name(&self) -> Result<crate::models::#ty_name, serde_json::Error> {
|
||||||
<crate::models::#ty_name as serde::Deserialize>::deserialize(&self.0)
|
self.deserialize()
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -43,6 +58,13 @@ impl Union {
|
||||||
pub struct #name(serde_json::Value);
|
pub struct #name(serde_json::Value);
|
||||||
|
|
||||||
impl #name {
|
impl #name {
|
||||||
|
pub fn deserialize<'de, T>(&'de self) -> Result<T, serde_json::Error>
|
||||||
|
where
|
||||||
|
T: serde::Deserialize<'de>,
|
||||||
|
{
|
||||||
|
T::deserialize(&self.0)
|
||||||
|
}
|
||||||
|
|
||||||
#(#variants)*
|
#(#variants)*
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -2,6 +2,8 @@ use std::borrow::Cow;
|
||||||
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
use super::r#type::OpenApiType;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
|
||||||
#[serde(rename_all = "lowercase")]
|
#[serde(rename_all = "lowercase")]
|
||||||
pub enum ParameterLocation {
|
pub enum ParameterLocation {
|
||||||
|
@ -9,14 +11,15 @@ pub enum ParameterLocation {
|
||||||
Path,
|
Path,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
|
||||||
#[serde(untagged)]
|
#[serde(untagged)]
|
||||||
pub enum OpenApiParameterDefault<'a> {
|
pub enum OpenApiParameterDefault<'a> {
|
||||||
Int(i32),
|
Int(i32),
|
||||||
Str(&'a str),
|
Str(&'a str),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct OpenApiParameterSchema<'a> {
|
pub struct OpenApiParameterSchema<'a> {
|
||||||
#[serde(rename = "$ref")]
|
#[serde(rename = "$ref")]
|
||||||
pub ref_path: Option<&'a str>,
|
pub ref_path: Option<&'a str>,
|
||||||
|
@ -27,9 +30,10 @@ pub struct OpenApiParameterSchema<'a> {
|
||||||
pub maximum: Option<i32>,
|
pub maximum: Option<i32>,
|
||||||
pub minimum: Option<i32>,
|
pub minimum: Option<i32>,
|
||||||
pub items: Option<Box<OpenApiParameterSchema<'a>>>,
|
pub items: Option<Box<OpenApiParameterSchema<'a>>>,
|
||||||
|
pub one_of: Option<Vec<OpenApiType<'a>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
|
||||||
pub struct OpenApiParameter<'a> {
|
pub struct OpenApiParameter<'a> {
|
||||||
pub name: &'a str,
|
pub name: &'a str,
|
||||||
pub description: Option<Cow<'a, str>>,
|
pub description: Option<Cow<'a, str>>,
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
|
|
||||||
use serde::{Deserialize, Deserializer};
|
use serde::{Deserialize, Deserializer, Serialize};
|
||||||
|
|
||||||
use super::parameter::OpenApiParameter;
|
use super::parameter::OpenApiParameter;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
|
||||||
#[serde(untagged)]
|
#[serde(untagged)]
|
||||||
pub enum OpenApiPathParameter<'a> {
|
pub enum OpenApiPathParameter<'a> {
|
||||||
Link {
|
Link {
|
||||||
|
@ -14,13 +14,13 @@ pub enum OpenApiPathParameter<'a> {
|
||||||
Inline(OpenApiParameter<'a>),
|
Inline(OpenApiParameter<'a>),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
|
||||||
pub struct SchemaLink<'a> {
|
pub struct SchemaLink<'a> {
|
||||||
#[serde(rename = "$ref")]
|
#[serde(rename = "$ref")]
|
||||||
pub ref_path: &'a str,
|
pub ref_path: &'a str,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
|
||||||
#[serde(untagged)]
|
#[serde(untagged)]
|
||||||
pub enum OpenApiResponseBody<'a> {
|
pub enum OpenApiResponseBody<'a> {
|
||||||
Schema(SchemaLink<'a>),
|
Schema(SchemaLink<'a>),
|
||||||
|
@ -30,6 +30,9 @@ pub enum OpenApiResponseBody<'a> {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||||
|
pub struct OperationId(pub String);
|
||||||
|
|
||||||
fn deserialize_response_body<'de, D>(deserializer: D) -> Result<OpenApiResponseBody<'de>, D::Error>
|
fn deserialize_response_body<'de, D>(deserializer: D) -> Result<OpenApiResponseBody<'de>, D::Error>
|
||||||
where
|
where
|
||||||
D: Deserializer<'de>,
|
D: Deserializer<'de>,
|
||||||
|
@ -60,10 +63,11 @@ where
|
||||||
Ok(responses.ok.content.json.schema)
|
Ok(responses.ok.content.json.schema)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
pub struct OpenApiPathBody<'a> {
|
pub struct OpenApiPathBody<'a> {
|
||||||
pub summary: Option<Cow<'a, str>>,
|
pub summary: Option<Cow<'a, str>>,
|
||||||
pub description: Cow<'a, str>,
|
pub description: Option<Cow<'a, str>>,
|
||||||
#[serde(borrow, default)]
|
#[serde(borrow, default)]
|
||||||
pub parameters: Vec<OpenApiPathParameter<'a>>,
|
pub parameters: Vec<OpenApiPathParameter<'a>>,
|
||||||
#[serde(
|
#[serde(
|
||||||
|
@ -72,9 +76,10 @@ pub struct OpenApiPathBody<'a> {
|
||||||
deserialize_with = "deserialize_response_body"
|
deserialize_with = "deserialize_response_body"
|
||||||
)]
|
)]
|
||||||
pub response_content: OpenApiResponseBody<'a>,
|
pub response_content: OpenApiResponseBody<'a>,
|
||||||
|
pub operation_id: Option<OperationId>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
|
||||||
pub struct OpenApiPath<'a> {
|
pub struct OpenApiPath<'a> {
|
||||||
#[serde(borrow)]
|
#[serde(borrow)]
|
||||||
pub get: OpenApiPathBody<'a>,
|
pub get: OpenApiPathBody<'a>,
|
||||||
|
|
|
@ -3,7 +3,7 @@ use serde::Deserialize;
|
||||||
|
|
||||||
use super::{parameter::OpenApiParameter, path::OpenApiPath, r#type::OpenApiType};
|
use super::{parameter::OpenApiParameter, path::OpenApiPath, r#type::OpenApiType};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
|
||||||
pub struct Components<'a> {
|
pub struct Components<'a> {
|
||||||
#[serde(borrow)]
|
#[serde(borrow)]
|
||||||
pub schemas: IndexMap<&'a str, OpenApiType<'a>>,
|
pub schemas: IndexMap<&'a str, OpenApiType<'a>>,
|
||||||
|
@ -11,7 +11,7 @@ pub struct Components<'a> {
|
||||||
pub parameters: IndexMap<&'a str, OpenApiParameter<'a>>,
|
pub parameters: IndexMap<&'a str, OpenApiParameter<'a>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
|
||||||
pub struct OpenApiSchema<'a> {
|
pub struct OpenApiSchema<'a> {
|
||||||
#[serde(borrow)]
|
#[serde(borrow)]
|
||||||
pub paths: IndexMap<&'a str, OpenApiPath<'a>>,
|
pub paths: IndexMap<&'a str, OpenApiPath<'a>>,
|
||||||
|
@ -19,20 +19,12 @@ pub struct OpenApiSchema<'a> {
|
||||||
pub components: Components<'a>,
|
pub components: Components<'a>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl OpenApiSchema<'_> {
|
|
||||||
pub fn read() -> Result<Self, serde_json::Error> {
|
|
||||||
let s = include_str!("../../openapi.json");
|
|
||||||
|
|
||||||
serde_json::from_str(s)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
pub(crate) mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
pub(crate) fn get_schema() -> OpenApiSchema<'static> {
|
||||||
fn read() {
|
let s = include_str!("../../../torn-api/openapi.json");
|
||||||
OpenApiSchema::read().unwrap();
|
serde_json::from_str(s).unwrap()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,17 +1,25 @@
|
||||||
[package]
|
[package]
|
||||||
name = "torn-api"
|
name = "torn-api"
|
||||||
version = "1.0.1"
|
version = "1.7.0"
|
||||||
edition = "2024"
|
edition = "2021"
|
||||||
description = "Auto-generated bindings for the v2 torn api"
|
description = "Auto-generated bindings for the v2 torn api"
|
||||||
license-file = { workspace = true }
|
license-file = { workspace = true }
|
||||||
repository = { workspace = true }
|
repository = { workspace = true }
|
||||||
homepage = { workspace = true }
|
homepage = { workspace = true }
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = ["scopes", "requests", "builder", "models"]
|
||||||
|
scopes = ["builder"]
|
||||||
|
builder = ["requests", "dep:bon"]
|
||||||
|
requests = ["models"]
|
||||||
|
models = ["dep:serde_repr"]
|
||||||
|
strum = ["dep:strum"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
serde = { workspace = true, features = ["derive"] }
|
serde = { workspace = true, features = ["derive"] }
|
||||||
serde_repr = "0.1"
|
serde_repr = { version = "0.1", optional = true }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
bon = "3.6"
|
bon = { version = "3.6", optional = true }
|
||||||
bytes = "1"
|
bytes = "1"
|
||||||
http = "1"
|
http = "1"
|
||||||
reqwest = { version = "0.12", default-features = false, features = [
|
reqwest = { version = "0.12", default-features = false, features = [
|
||||||
|
@ -20,12 +28,19 @@ reqwest = { version = "0.12", default-features = false, features = [
|
||||||
"brotli",
|
"brotli",
|
||||||
] }
|
] }
|
||||||
thiserror = "2"
|
thiserror = "2"
|
||||||
|
futures = { version = "0.3", default-features = false, features = [
|
||||||
|
"std",
|
||||||
|
"async-await",
|
||||||
|
] }
|
||||||
|
chrono = { version = "0.4.41", features = ["serde"] }
|
||||||
|
strum = { version = "0.27.1", features = ["derive"], optional = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
torn-api-codegen = { path = "../torn-api-codegen", version = "0.1.1" }
|
torn-api-codegen = { path = "../torn-api-codegen", version = "0.7.0" }
|
||||||
syn = { workspace = true, features = ["parsing"] }
|
syn = { workspace = true, features = ["parsing"] }
|
||||||
proc-macro2 = { workspace = true }
|
proc-macro2 = { workspace = true }
|
||||||
prettyplease = "0.2"
|
prettyplease = "0.2"
|
||||||
|
serde_json = { workspace = true }
|
||||||
|
|
|
@ -1,12 +1,6 @@
|
||||||
use std::{env, fs, path::Path};
|
use std::{env, fs, path::Path};
|
||||||
|
|
||||||
use proc_macro2::TokenStream;
|
use torn_api_codegen::{model::ResolvedSchema, openapi::schema::OpenApiSchema};
|
||||||
use torn_api_codegen::{
|
|
||||||
model::{parameter::Parameter, path::Path as ApiPath, resolve, scope::Scope},
|
|
||||||
openapi::schema::OpenApiSchema,
|
|
||||||
};
|
|
||||||
|
|
||||||
const DENY_LIST: &[&str] = &[];
|
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let out_dir = env::var_os("OUT_DIR").unwrap();
|
let out_dir = env::var_os("OUT_DIR").unwrap();
|
||||||
|
@ -15,61 +9,27 @@ fn main() {
|
||||||
let requests_dest = Path::new(&out_dir).join("requests.rs");
|
let requests_dest = Path::new(&out_dir).join("requests.rs");
|
||||||
let scopes_dest = Path::new(&out_dir).join("scopes.rs");
|
let scopes_dest = Path::new(&out_dir).join("scopes.rs");
|
||||||
|
|
||||||
let schema = OpenApiSchema::read().unwrap();
|
let s = include_str!("./openapi.json");
|
||||||
|
let schema: OpenApiSchema = serde_json::from_str(s).unwrap();
|
||||||
|
let resolved = ResolvedSchema::from_open_api(&schema);
|
||||||
|
|
||||||
let mut models_code = TokenStream::new();
|
let models_file = syn::parse2(resolved.codegen_models()).unwrap();
|
||||||
|
|
||||||
for (name, model) in &schema.components.schemas {
|
|
||||||
if DENY_LIST.contains(name) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let model = resolve(model, name, &schema.components.schemas);
|
|
||||||
if let Some(new_code) = model.codegen() {
|
|
||||||
models_code.extend(new_code);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let models_file = syn::parse2(models_code).unwrap();
|
|
||||||
let models_pretty = prettyplease::unparse(&models_file);
|
let models_pretty = prettyplease::unparse(&models_file);
|
||||||
fs::write(&model_dest, models_pretty).unwrap();
|
fs::write(&model_dest, models_pretty).unwrap();
|
||||||
|
|
||||||
let mut params_code = TokenStream::new();
|
let params_file = syn::parse2(resolved.codegen_parameters()).unwrap();
|
||||||
|
|
||||||
for (name, param) in &schema.components.parameters {
|
|
||||||
if let Some(code) = Parameter::from_schema(name, param).unwrap().codegen() {
|
|
||||||
params_code.extend(code);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let params_file = syn::parse2(params_code).unwrap();
|
|
||||||
let params_pretty = prettyplease::unparse(¶ms_file);
|
let params_pretty = prettyplease::unparse(¶ms_file);
|
||||||
fs::write(¶ms_dest, params_pretty).unwrap();
|
fs::write(¶ms_dest, params_pretty).unwrap();
|
||||||
|
|
||||||
let mut requests_code = TokenStream::new();
|
let requests_file = syn::parse2(resolved.codegen_requests()).unwrap();
|
||||||
let mut paths = Vec::new();
|
|
||||||
for (name, path) in &schema.paths {
|
|
||||||
let Some(path) = ApiPath::from_schema(name, path, &schema.components.parameters) else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
if let Some(code) = path.codegen_request() {
|
|
||||||
requests_code.extend(code);
|
|
||||||
}
|
|
||||||
paths.push(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
let requests_file = syn::parse2(requests_code).unwrap();
|
|
||||||
let requests_pretty = prettyplease::unparse(&requests_file);
|
let requests_pretty = prettyplease::unparse(&requests_file);
|
||||||
fs::write(&requests_dest, requests_pretty).unwrap();
|
fs::write(&requests_dest, requests_pretty).unwrap();
|
||||||
|
|
||||||
let mut scope_code = TokenStream::new();
|
let scopes_file = syn::parse2(resolved.codegen_scopes()).unwrap();
|
||||||
let scopes = Scope::from_paths(paths);
|
|
||||||
for scope in scopes {
|
|
||||||
if let Some(code) = scope.codegen() {
|
|
||||||
scope_code.extend(code);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let scopes_file = syn::parse2(scope_code).unwrap();
|
|
||||||
let scopes_pretty = prettyplease::unparse(&scopes_file);
|
let scopes_pretty = prettyplease::unparse(&scopes_file);
|
||||||
fs::write(&scopes_dest, scopes_pretty).unwrap();
|
fs::write(&scopes_dest, scopes_pretty).unwrap();
|
||||||
|
|
||||||
|
for warning in resolved.warnings.get_warnings() {
|
||||||
|
println!("cargo:warning={}", warning);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,22 +1,30 @@
|
||||||
use http::{HeaderMap, HeaderValue, header::AUTHORIZATION};
|
use std::future::Future;
|
||||||
|
|
||||||
|
use futures::{Stream, StreamExt};
|
||||||
|
use http::{header::AUTHORIZATION, HeaderMap, HeaderValue};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
#[cfg(feature = "scopes")]
|
||||||
|
use crate::scopes::{
|
||||||
|
BulkFactionScope, BulkForumScope, BulkMarketScope, BulkRacingScope, BulkTornScope,
|
||||||
|
BulkUserScope, FactionScope, ForumScope, MarketScope, RacingScope, TornScope, UserScope,
|
||||||
|
};
|
||||||
use crate::{
|
use crate::{
|
||||||
request::{ApiResponse, IntoRequest},
|
request::{ApiRequest, ApiResponse, IntoRequest},
|
||||||
scopes::{FactionScope, ForumScope, MarketScope, RacingScope, TornScope, UserScope},
|
scopes::{BulkKeyScope, KeyScope},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub trait Executor {
|
pub trait Executor: Sized {
|
||||||
type Error: From<serde_json::Error> + From<crate::ApiError> + Send;
|
type Error: From<serde_json::Error> + From<crate::ApiError> + Send;
|
||||||
|
|
||||||
fn execute<R>(
|
fn execute<R>(
|
||||||
&self,
|
self,
|
||||||
request: R,
|
request: R,
|
||||||
) -> impl Future<Output = Result<ApiResponse<R::Discriminant>, Self::Error>> + Send
|
) -> impl Future<Output = (R::Discriminant, Result<ApiResponse, Self::Error>)> + Send
|
||||||
where
|
where
|
||||||
R: IntoRequest;
|
R: IntoRequest;
|
||||||
|
|
||||||
fn fetch<R>(&self, request: R) -> impl Future<Output = Result<R::Response, Self::Error>> + Send
|
fn fetch<R>(self, request: R) -> impl Future<Output = Result<R::Response, Self::Error>> + Send
|
||||||
where
|
where
|
||||||
R: IntoRequest,
|
R: IntoRequest,
|
||||||
{
|
{
|
||||||
|
@ -24,7 +32,7 @@ pub trait Executor {
|
||||||
// The future is `Send` but `&self` might not be.
|
// The future is `Send` but `&self` might not be.
|
||||||
let fut = self.execute(request);
|
let fut = self.execute(request);
|
||||||
async {
|
async {
|
||||||
let resp = fut.await?;
|
let resp = fut.await.1?;
|
||||||
|
|
||||||
let bytes = resp.body.unwrap();
|
let bytes = resp.body.unwrap();
|
||||||
|
|
||||||
|
@ -51,6 +59,164 @@ pub trait Executor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub trait BulkExecutor: Sized {
|
||||||
|
type Error: From<serde_json::Error> + From<crate::ApiError> + Send;
|
||||||
|
|
||||||
|
fn execute<R>(
|
||||||
|
self,
|
||||||
|
requests: impl IntoIterator<Item = R>,
|
||||||
|
) -> impl Stream<Item = (R::Discriminant, Result<ApiResponse, Self::Error>)> + Unpin
|
||||||
|
where
|
||||||
|
R: IntoRequest;
|
||||||
|
|
||||||
|
fn fetch_many<R>(
|
||||||
|
self,
|
||||||
|
requests: impl IntoIterator<Item = R>,
|
||||||
|
) -> impl Stream<Item = (R::Discriminant, Result<R::Response, Self::Error>)> + Unpin
|
||||||
|
where
|
||||||
|
R: IntoRequest,
|
||||||
|
{
|
||||||
|
self.execute(requests).map(|(d, r)| {
|
||||||
|
let r = match r {
|
||||||
|
Ok(r) => r,
|
||||||
|
Err(why) => return (d, Err(why)),
|
||||||
|
};
|
||||||
|
let bytes = r.body.unwrap();
|
||||||
|
|
||||||
|
if bytes.starts_with(br#"{"error":{"#) {
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct ErrorBody<'a> {
|
||||||
|
code: u16,
|
||||||
|
error: &'a str,
|
||||||
|
}
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct ErrorContainer<'a> {
|
||||||
|
#[serde(borrow)]
|
||||||
|
error: ErrorBody<'a>,
|
||||||
|
}
|
||||||
|
|
||||||
|
let error: ErrorContainer = match serde_json::from_slice(&bytes) {
|
||||||
|
Ok(error) => error,
|
||||||
|
Err(why) => return (d, Err(why.into())),
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
d,
|
||||||
|
Err(crate::ApiError::new(error.error.code, error.error.error).into()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let resp = match serde_json::from_slice(&bytes) {
|
||||||
|
Ok(resp) => resp,
|
||||||
|
Err(why) => return (d, Err(why.into())),
|
||||||
|
};
|
||||||
|
|
||||||
|
(d, Ok(resp))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "scopes")]
|
||||||
|
pub trait ExecutorExt: Executor + Sized {
|
||||||
|
fn user(self) -> UserScope<Self>;
|
||||||
|
|
||||||
|
fn faction(self) -> FactionScope<Self>;
|
||||||
|
|
||||||
|
fn torn(self) -> TornScope<Self>;
|
||||||
|
|
||||||
|
fn market(self) -> MarketScope<Self>;
|
||||||
|
|
||||||
|
fn racing(self) -> RacingScope<Self>;
|
||||||
|
|
||||||
|
fn forum(self) -> ForumScope<Self>;
|
||||||
|
|
||||||
|
fn key(self) -> KeyScope<Self>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "scopes")]
|
||||||
|
impl<T> ExecutorExt for T
|
||||||
|
where
|
||||||
|
T: Executor + Sized,
|
||||||
|
{
|
||||||
|
fn user(self) -> UserScope<Self> {
|
||||||
|
UserScope::new(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn faction(self) -> FactionScope<Self> {
|
||||||
|
FactionScope::new(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn torn(self) -> TornScope<Self> {
|
||||||
|
TornScope::new(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn market(self) -> MarketScope<Self> {
|
||||||
|
MarketScope::new(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn racing(self) -> RacingScope<Self> {
|
||||||
|
RacingScope::new(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn forum(self) -> ForumScope<Self> {
|
||||||
|
ForumScope::new(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn key(self) -> KeyScope<Self> {
|
||||||
|
KeyScope::new(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "scopes")]
|
||||||
|
pub trait BulkExecutorExt: BulkExecutor + Sized {
|
||||||
|
fn user_bulk(self) -> BulkUserScope<Self>;
|
||||||
|
|
||||||
|
fn faction_bulk(self) -> BulkFactionScope<Self>;
|
||||||
|
|
||||||
|
fn torn_bulk(self) -> BulkTornScope<Self>;
|
||||||
|
|
||||||
|
fn market_bulk(self) -> BulkMarketScope<Self>;
|
||||||
|
|
||||||
|
fn racing_bulk(self) -> BulkRacingScope<Self>;
|
||||||
|
|
||||||
|
fn forum_bulk(self) -> BulkForumScope<Self>;
|
||||||
|
|
||||||
|
fn key_bulk(self) -> BulkKeyScope<Self>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "scopes")]
|
||||||
|
impl<T> BulkExecutorExt for T
|
||||||
|
where
|
||||||
|
T: BulkExecutor + Sized,
|
||||||
|
{
|
||||||
|
fn user_bulk(self) -> BulkUserScope<Self> {
|
||||||
|
BulkUserScope::new(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn faction_bulk(self) -> BulkFactionScope<Self> {
|
||||||
|
BulkFactionScope::new(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn torn_bulk(self) -> BulkTornScope<Self> {
|
||||||
|
BulkTornScope::new(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn market_bulk(self) -> BulkMarketScope<Self> {
|
||||||
|
BulkMarketScope::new(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn racing_bulk(self) -> BulkRacingScope<Self> {
|
||||||
|
BulkRacingScope::new(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn forum_bulk(self) -> BulkForumScope<Self> {
|
||||||
|
BulkForumScope::new(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn key_bulk(self) -> BulkKeyScope<Self> {
|
||||||
|
BulkKeyScope::new(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct ReqwestClient(reqwest::Client);
|
pub struct ReqwestClient(reqwest::Client);
|
||||||
|
|
||||||
impl ReqwestClient {
|
impl ReqwestClient {
|
||||||
|
@ -71,77 +237,53 @@ impl ReqwestClient {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait ExecutorExt: Executor + Sized {
|
impl ReqwestClient {
|
||||||
fn user(&self) -> UserScope<'_, Self>;
|
async fn execute_api_request(&self, request: ApiRequest) -> Result<ApiResponse, crate::Error> {
|
||||||
|
|
||||||
fn faction(&self) -> FactionScope<'_, Self>;
|
|
||||||
|
|
||||||
fn torn(&self) -> TornScope<'_, Self>;
|
|
||||||
|
|
||||||
fn market(&self) -> MarketScope<'_, Self>;
|
|
||||||
|
|
||||||
fn racing(&self) -> RacingScope<'_, Self>;
|
|
||||||
|
|
||||||
fn forum(&self) -> ForumScope<'_, Self>;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> ExecutorExt for T
|
|
||||||
where
|
|
||||||
T: Executor + Sized,
|
|
||||||
{
|
|
||||||
fn user(&self) -> UserScope<'_, Self> {
|
|
||||||
UserScope::new(self)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn faction(&self) -> FactionScope<'_, Self> {
|
|
||||||
FactionScope::new(self)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn torn(&self) -> TornScope<'_, Self> {
|
|
||||||
TornScope::new(self)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn market(&self) -> MarketScope<'_, Self> {
|
|
||||||
MarketScope::new(self)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn racing(&self) -> RacingScope<'_, Self> {
|
|
||||||
RacingScope::new(self)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn forum(&self) -> ForumScope<'_, Self> {
|
|
||||||
ForumScope::new(self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Executor for ReqwestClient {
|
|
||||||
type Error = crate::Error;
|
|
||||||
|
|
||||||
async fn execute<R>(&self, request: R) -> Result<ApiResponse<R::Discriminant>, Self::Error>
|
|
||||||
where
|
|
||||||
R: IntoRequest,
|
|
||||||
{
|
|
||||||
let request = request.into_request();
|
|
||||||
let url = request.url();
|
let url = request.url();
|
||||||
|
|
||||||
let response = self.0.get(url).send().await?;
|
let response = self.0.get(url).send().await?;
|
||||||
let status = response.status();
|
let status = response.status();
|
||||||
let body = response.bytes().await.ok();
|
let body = response.bytes().await.ok();
|
||||||
|
|
||||||
Ok(ApiResponse {
|
Ok(ApiResponse { status, body })
|
||||||
discriminant: request.disriminant,
|
}
|
||||||
status,
|
}
|
||||||
body,
|
|
||||||
})
|
impl Executor for &ReqwestClient {
|
||||||
|
type Error = crate::Error;
|
||||||
|
|
||||||
|
async fn execute<R>(self, request: R) -> (R::Discriminant, Result<ApiResponse, Self::Error>)
|
||||||
|
where
|
||||||
|
R: IntoRequest,
|
||||||
|
{
|
||||||
|
let (d, request) = request.into_request();
|
||||||
|
(d, self.execute_api_request(request).await)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BulkExecutor for &ReqwestClient {
|
||||||
|
type Error = crate::Error;
|
||||||
|
|
||||||
|
fn execute<R>(
|
||||||
|
self,
|
||||||
|
requests: impl IntoIterator<Item = R>,
|
||||||
|
) -> impl Stream<Item = (R::Discriminant, Result<ApiResponse, Self::Error>)>
|
||||||
|
where
|
||||||
|
R: IntoRequest,
|
||||||
|
{
|
||||||
|
futures::stream::iter(requests)
|
||||||
|
.map(move |r| <Self as Executor>::execute(self, r))
|
||||||
|
.buffer_unordered(25)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use crate::{ApiError, Error, scopes::test::test_client};
|
use crate::{scopes::test::test_client, ApiError, Error};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
#[cfg(feature = "scopes")]
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn api_error() {
|
async fn api_error() {
|
||||||
let client = test_client().await;
|
let client = test_client().await;
|
||||||
|
@ -153,4 +295,22 @@ mod test {
|
||||||
other => panic!("Expected incorrect id entity relation error, got {other:?}"),
|
other => panic!("Expected incorrect id entity relation error, got {other:?}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "scopes")]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn bulk_request() {
|
||||||
|
let client = test_client().await;
|
||||||
|
|
||||||
|
let stream = client
|
||||||
|
.faction_bulk()
|
||||||
|
.basic_for_id(vec![19.into(), 89.into()], |b| b);
|
||||||
|
|
||||||
|
let mut responses: Vec<_> = stream.collect().await;
|
||||||
|
|
||||||
|
let (_id1, basic1) = responses.pop().unwrap();
|
||||||
|
basic1.unwrap();
|
||||||
|
|
||||||
|
let (_id2, basic2) = responses.pop().unwrap();
|
||||||
|
basic2.unwrap();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
pub mod executor;
|
pub mod executor;
|
||||||
|
#[cfg(feature = "models")]
|
||||||
pub mod models;
|
pub mod models;
|
||||||
|
#[cfg(feature = "requests")]
|
||||||
pub mod parameters;
|
pub mod parameters;
|
||||||
pub mod request;
|
pub mod request;
|
||||||
|
#[cfg(feature = "scopes")]
|
||||||
pub mod scopes;
|
pub mod scopes;
|
||||||
|
|
||||||
#[derive(Debug, Error, Clone, PartialEq, Eq)]
|
#[derive(Debug, Error, Clone, PartialEq, Eq)]
|
||||||
|
|
|
@ -1,26 +1,26 @@
|
||||||
use bon::Builder;
|
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use http::StatusCode;
|
use http::StatusCode;
|
||||||
|
|
||||||
use crate::{
|
#[cfg(feature = "requests")]
|
||||||
executor::Executor,
|
|
||||||
models::{FactionChainsResponse, FactionId},
|
|
||||||
};
|
|
||||||
|
|
||||||
pub mod models;
|
pub mod models;
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct ApiRequest<D = ()> {
|
pub struct ApiRequest {
|
||||||
pub disriminant: D,
|
|
||||||
pub path: String,
|
pub path: String,
|
||||||
pub parameters: Vec<(&'static str, String)>,
|
pub parameters: Vec<(&'static str, String)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<D> ApiRequest<D> {
|
impl ApiRequest {
|
||||||
pub fn url(&self) -> String {
|
pub fn url(&self) -> String {
|
||||||
let mut url = format!("https://api.torn.com/v2{}?", self.path);
|
let mut url = format!("https://api.torn.com/v2{}?", self.path);
|
||||||
|
|
||||||
|
let mut first = true;
|
||||||
for (name, value) in &self.parameters {
|
for (name, value) in &self.parameters {
|
||||||
|
if first {
|
||||||
|
first = false;
|
||||||
|
} else {
|
||||||
|
url.push('&');
|
||||||
|
}
|
||||||
url.push_str(&format!("{name}={value}"));
|
url.push_str(&format!("{name}={value}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -28,77 +28,35 @@ impl<D> ApiRequest<D> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ApiResponse<D = ()> {
|
pub struct ApiResponse {
|
||||||
pub discriminant: D,
|
|
||||||
pub body: Option<Bytes>,
|
pub body: Option<Bytes>,
|
||||||
pub status: StatusCode,
|
pub status: StatusCode,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait IntoRequest: Send {
|
pub trait IntoRequest: Send {
|
||||||
type Discriminant: Send;
|
type Discriminant: Send + 'static;
|
||||||
type Response: for<'de> serde::Deserialize<'de> + Send;
|
type Response: for<'de> serde::Deserialize<'de> + Send;
|
||||||
fn into_request(self) -> ApiRequest<Self::Discriminant>;
|
fn into_request(self) -> (Self::Discriminant, ApiRequest);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct FactionScope<'e, E>(&'e E)
|
pub(crate) struct WrappedApiRequest<R>
|
||||||
where
|
where
|
||||||
E: Executor;
|
R: IntoRequest,
|
||||||
|
|
||||||
impl<E> FactionScope<'_, E>
|
|
||||||
where
|
|
||||||
E: Executor,
|
|
||||||
{
|
{
|
||||||
pub async fn chains_for_id<S>(
|
discriminant: R::Discriminant,
|
||||||
&self,
|
request: ApiRequest,
|
||||||
id: FactionId,
|
|
||||||
builder: impl FnOnce(
|
|
||||||
FactionChainsRequestBuilder<faction_chains_request_builder::Empty>,
|
|
||||||
) -> FactionChainsRequestBuilder<S>,
|
|
||||||
) -> Result<FactionChainsResponse, E::Error>
|
|
||||||
where
|
|
||||||
S: faction_chains_request_builder::IsComplete,
|
|
||||||
{
|
|
||||||
let r = builder(FactionChainsRequest::with_id(id)).build();
|
|
||||||
|
|
||||||
self.0.fetch(r).await
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Builder)]
|
impl<R> IntoRequest for WrappedApiRequest<R>
|
||||||
#[builder(start_fn = with_id)]
|
where
|
||||||
pub struct FactionChainsRequest {
|
R: IntoRequest,
|
||||||
#[builder(start_fn)]
|
{
|
||||||
pub id: FactionId,
|
type Discriminant = R::Discriminant;
|
||||||
pub limit: Option<usize>,
|
type Response = R::Response;
|
||||||
}
|
fn into_request(self) -> (Self::Discriminant, ApiRequest) {
|
||||||
|
(self.discriminant, self.request)
|
||||||
impl IntoRequest for FactionChainsRequest {
|
|
||||||
type Discriminant = FactionId;
|
|
||||||
type Response = FactionChainsResponse;
|
|
||||||
fn into_request(self) -> ApiRequest<Self::Discriminant> {
|
|
||||||
ApiRequest {
|
|
||||||
disriminant: self.id,
|
|
||||||
path: format!("/faction/{}/chains", self.id),
|
|
||||||
parameters: self
|
|
||||||
.limit
|
|
||||||
.into_iter()
|
|
||||||
.map(|l| ("limit", l.to_string()))
|
|
||||||
.collect(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {}
|
||||||
use crate::executor::ReqwestClient;
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_request() {
|
|
||||||
let client = ReqwestClient::new("nAYRXaoqzBAGalWt");
|
|
||||||
|
|
||||||
let r = models::TornItemsForIdsRequest::builder("1".to_owned()).build();
|
|
||||||
client.fetch(r).await.unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -9,8 +9,9 @@ pub(super) mod test {
|
||||||
use crate::{
|
use crate::{
|
||||||
executor::{ExecutorExt, ReqwestClient},
|
executor::{ExecutorExt, ReqwestClient},
|
||||||
models::{
|
models::{
|
||||||
AttackCode, FactionSelectionName, PersonalStatsCategoryEnum, PersonalStatsStatName,
|
faction_selection_name::FactionSelectionNameVariant,
|
||||||
UserListEnum,
|
user_selection_name::UserSelectionNameVariant, AttackCode, PersonalStatsCategoryEnum,
|
||||||
|
PersonalStatsStatName, UserListEnum,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -67,7 +68,10 @@ pub(super) mod test {
|
||||||
let r = client
|
let r = client
|
||||||
.faction()
|
.faction()
|
||||||
.for_selections(|b| {
|
.for_selections(|b| {
|
||||||
b.selections([FactionSelectionName::Basic, FactionSelectionName::Balance])
|
b.selections([
|
||||||
|
FactionSelectionNameVariant::Basic,
|
||||||
|
FactionSelectionNameVariant::Balance,
|
||||||
|
])
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
@ -366,6 +370,15 @@ pub(super) mod test {
|
||||||
faction_scope.lookup(|b| b).await.unwrap();
|
faction_scope.lookup(|b| b).await.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn faction_reports() {
|
||||||
|
let client = test_client().await;
|
||||||
|
|
||||||
|
let faction_scope = FactionScope(&client);
|
||||||
|
|
||||||
|
faction_scope.reports(|b| b).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn forum_categories() {
|
async fn forum_categories() {
|
||||||
let client = test_client().await;
|
let client = test_client().await;
|
||||||
|
@ -415,7 +428,7 @@ pub(super) mod test {
|
||||||
let forum_scope = ForumScope(&client);
|
let forum_scope = ForumScope(&client);
|
||||||
|
|
||||||
forum_scope
|
forum_scope
|
||||||
.threads_for_category_ids("2".to_owned(), |b| b)
|
.threads_for_category_ids([2].into(), |b| b)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
@ -486,14 +499,14 @@ pub(super) mod test {
|
||||||
racing_scope.carupgrades(|b| b).await.unwrap();
|
racing_scope.carupgrades(|b| b).await.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
/* #[tokio::test]
|
||||||
async fn racing_races() {
|
async fn racing_races() {
|
||||||
let client = test_client().await;
|
let client = test_client().await;
|
||||||
|
|
||||||
let racing_scope = RacingScope(&client);
|
let racing_scope = RacingScope(&client);
|
||||||
|
|
||||||
racing_scope.races(|b| b).await.unwrap();
|
racing_scope.races(|b| b).await.unwrap();
|
||||||
}
|
} */
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn racing_race_for_race_id() {
|
async fn racing_race_for_race_id() {
|
||||||
|
@ -639,10 +652,7 @@ pub(super) mod test {
|
||||||
|
|
||||||
let torn_scope = TornScope(&client);
|
let torn_scope = TornScope(&client);
|
||||||
|
|
||||||
torn_scope
|
torn_scope.items_for_ids([1].into(), |b| b).await.unwrap();
|
||||||
.items_for_ids("1".to_owned(), |b| b)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
@ -909,6 +919,161 @@ pub(super) mod test {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "strum")]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn user_personalstats_popular() {
|
||||||
|
let client = test_client().await;
|
||||||
|
|
||||||
|
let resp = client
|
||||||
|
.user()
|
||||||
|
.for_selections(|b| {
|
||||||
|
b.selections([UserSelectionNameVariant::Personalstats])
|
||||||
|
.cat(PersonalStatsCategoryEnum::Popular)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert!(resp
|
||||||
|
.user_personal_stats_response()
|
||||||
|
.unwrap()
|
||||||
|
.is_user_personal_stats_popular());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "strum")]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn user_personalstats_all() {
|
||||||
|
let client = test_client().await;
|
||||||
|
|
||||||
|
let resp = client
|
||||||
|
.user()
|
||||||
|
.for_selections(|b| {
|
||||||
|
b.selections([UserSelectionNameVariant::Personalstats])
|
||||||
|
.cat(PersonalStatsCategoryEnum::All)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert!(resp
|
||||||
|
.user_personal_stats_response()
|
||||||
|
.unwrap()
|
||||||
|
.is_user_personal_stats_full());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "strum")]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn user_personalstats_cat_attacking() {
|
||||||
|
let client = test_client().await;
|
||||||
|
|
||||||
|
let resp = client
|
||||||
|
.user()
|
||||||
|
.for_selections(|b| {
|
||||||
|
b.selections([UserSelectionNameVariant::Personalstats])
|
||||||
|
.cat(PersonalStatsCategoryEnum::Attacking)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert!(resp
|
||||||
|
.user_personal_stats_response()
|
||||||
|
.unwrap()
|
||||||
|
.try_as_user_personal_stats_category()
|
||||||
|
.unwrap()
|
||||||
|
.personalstats
|
||||||
|
.is_personal_stats_attacking_public());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "strum")]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn user_personalstats_cat_jobs() {
|
||||||
|
let client = test_client().await;
|
||||||
|
|
||||||
|
let resp = client
|
||||||
|
.user()
|
||||||
|
.for_selections(|b| {
|
||||||
|
b.selections([UserSelectionNameVariant::Personalstats])
|
||||||
|
.cat(PersonalStatsCategoryEnum::Jobs)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert!(resp
|
||||||
|
.user_personal_stats_response()
|
||||||
|
.unwrap()
|
||||||
|
.try_as_user_personal_stats_category()
|
||||||
|
.unwrap()
|
||||||
|
.personalstats
|
||||||
|
.is_personal_stats_jobs_public());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "strum")]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn user_personalstats_cat_trading() {
|
||||||
|
let client = test_client().await;
|
||||||
|
|
||||||
|
let resp = client
|
||||||
|
.user()
|
||||||
|
.for_selections(|b| {
|
||||||
|
b.selections([UserSelectionNameVariant::Personalstats])
|
||||||
|
.cat(PersonalStatsCategoryEnum::Trading)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert!(resp
|
||||||
|
.user_personal_stats_response()
|
||||||
|
.unwrap()
|
||||||
|
.try_as_user_personal_stats_category()
|
||||||
|
.unwrap()
|
||||||
|
.personalstats
|
||||||
|
.is_personal_stats_trading());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "strum")]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn user_personalstats_cat_jail() {
|
||||||
|
let client = test_client().await;
|
||||||
|
|
||||||
|
let resp = client
|
||||||
|
.user()
|
||||||
|
.for_selections(|b| {
|
||||||
|
b.selections([UserSelectionNameVariant::Personalstats])
|
||||||
|
.cat(PersonalStatsCategoryEnum::Jail)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert!(resp
|
||||||
|
.user_personal_stats_response()
|
||||||
|
.unwrap()
|
||||||
|
.try_as_user_personal_stats_category()
|
||||||
|
.unwrap()
|
||||||
|
.personalstats
|
||||||
|
.is_personal_stats_jail());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "strum")]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn user_personalstats_cat_hospital() {
|
||||||
|
let client = test_client().await;
|
||||||
|
|
||||||
|
let resp = client
|
||||||
|
.user()
|
||||||
|
.for_selections(|b| {
|
||||||
|
b.selections([UserSelectionNameVariant::Personalstats])
|
||||||
|
.cat(PersonalStatsCategoryEnum::Hospital)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert!(resp
|
||||||
|
.user_personal_stats_response()
|
||||||
|
.unwrap()
|
||||||
|
.try_as_user_personal_stats_category()
|
||||||
|
.unwrap()
|
||||||
|
.personalstats
|
||||||
|
.is_personal_stats_hospital());
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn user_personalstats_for_id() {
|
async fn user_personalstats_for_id() {
|
||||||
let client = test_client().await;
|
let client = test_client().await;
|
||||||
|
@ -954,4 +1119,25 @@ pub(super) mod test {
|
||||||
|
|
||||||
client.user().attacks(|b| b).await.unwrap();
|
client.user().attacks(|b| b).await.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn user_reports() {
|
||||||
|
let client = test_client().await;
|
||||||
|
|
||||||
|
client.user().reports(|b| b).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn key_info() {
|
||||||
|
let client = test_client().await;
|
||||||
|
|
||||||
|
client.key().info(|b| b).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn key_log() {
|
||||||
|
let client = test_client().await;
|
||||||
|
|
||||||
|
client.key().log(|b| b).await.unwrap();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "torn-key-pool"
|
name = "torn-key-pool"
|
||||||
version = "1.0.0"
|
version = "1.1.3"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
authors = ["Pyrit [2111649]"]
|
authors = ["Pyrit [2111649]"]
|
||||||
license-file = { workspace = true }
|
license-file = { workspace = true }
|
||||||
|
@ -11,10 +11,10 @@ description = "A generalised API key pool for torn-api"
|
||||||
[features]
|
[features]
|
||||||
default = ["postgres", "tokio-runtime"]
|
default = ["postgres", "tokio-runtime"]
|
||||||
postgres = ["dep:sqlx", "dep:chrono", "dep:indoc"]
|
postgres = ["dep:sqlx", "dep:chrono", "dep:indoc"]
|
||||||
tokio-runtime = ["dep:tokio", "dep:rand"]
|
tokio-runtime = ["dep:tokio", "dep:rand", "dep:tokio-stream"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
torn-api = { path = "../torn-api", default-features = false, version = "1.0.1" }
|
torn-api = { path = "../torn-api", default-features = false, version = "1.1.1" }
|
||||||
thiserror = "2"
|
thiserror = "2"
|
||||||
|
|
||||||
sqlx = { version = "0.8", features = [
|
sqlx = { version = "0.8", features = [
|
||||||
|
@ -30,6 +30,9 @@ indoc = { version = "2", optional = true }
|
||||||
tokio = { version = "1", optional = true, default-features = false, features = [
|
tokio = { version = "1", optional = true, default-features = false, features = [
|
||||||
"time",
|
"time",
|
||||||
] }
|
] }
|
||||||
|
tokio-stream = { version = "0.1", optional = true, default-features = false, features = [
|
||||||
|
"time",
|
||||||
|
] }
|
||||||
rand = { version = "0.9", optional = true }
|
rand = { version = "0.9", optional = true }
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
reqwest = { version = "0.12", default-features = false, features = [
|
reqwest = { version = "0.12", default-features = false, features = [
|
||||||
|
|
|
@ -3,13 +3,14 @@
|
||||||
#[cfg(feature = "postgres")]
|
#[cfg(feature = "postgres")]
|
||||||
pub mod postgres;
|
pub mod postgres;
|
||||||
|
|
||||||
use std::{collections::HashMap, future::Future, sync::Arc, time::Duration};
|
use std::{collections::HashMap, future::Future, ops::Deref, sync::Arc, time::Duration};
|
||||||
|
|
||||||
use futures::{future::BoxFuture, FutureExt};
|
use futures::{future::BoxFuture, FutureExt, Stream, StreamExt};
|
||||||
use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION};
|
use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
use tokio_stream::StreamExt as TokioStreamExt;
|
||||||
use torn_api::{
|
use torn_api::{
|
||||||
executor::Executor,
|
executor::{BulkExecutor, Executor},
|
||||||
request::{ApiRequest, ApiResponse},
|
request::{ApiRequest, ApiResponse},
|
||||||
ApiError,
|
ApiError,
|
||||||
};
|
};
|
||||||
|
@ -80,6 +81,46 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<K, D> From<&str> for KeySelector<K, D>
|
||||||
|
where
|
||||||
|
K: ApiKey,
|
||||||
|
D: KeyDomain,
|
||||||
|
{
|
||||||
|
fn from(value: &str) -> Self {
|
||||||
|
Self::Key(value.to_owned())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<K, D> From<D> for KeySelector<K, D>
|
||||||
|
where
|
||||||
|
K: ApiKey,
|
||||||
|
D: KeyDomain,
|
||||||
|
{
|
||||||
|
fn from(value: D) -> Self {
|
||||||
|
Self::Has(vec![value])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<K, D> From<&[D]> for KeySelector<K, D>
|
||||||
|
where
|
||||||
|
K: ApiKey,
|
||||||
|
D: KeyDomain,
|
||||||
|
{
|
||||||
|
fn from(value: &[D]) -> Self {
|
||||||
|
Self::Has(value.to_vec())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<K, D> From<Vec<D>> for KeySelector<K, D>
|
||||||
|
where
|
||||||
|
K: ApiKey,
|
||||||
|
D: KeyDomain,
|
||||||
|
{
|
||||||
|
fn from(value: Vec<D>) -> Self {
|
||||||
|
Self::Has(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub trait IntoSelector<K, D>: Send
|
pub trait IntoSelector<K, D>: Send
|
||||||
where
|
where
|
||||||
K: ApiKey,
|
K: ApiKey,
|
||||||
|
@ -88,30 +129,35 @@ where
|
||||||
fn into_selector(self) -> KeySelector<K, D>;
|
fn into_selector(self) -> KeySelector<K, D>;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<K, D> IntoSelector<K, D> for D
|
impl<K, D, T> IntoSelector<K, D> for T
|
||||||
where
|
where
|
||||||
K: ApiKey,
|
K: ApiKey,
|
||||||
D: KeyDomain,
|
D: KeyDomain,
|
||||||
|
T: Into<KeySelector<K, D>> + Send,
|
||||||
{
|
{
|
||||||
fn into_selector(self) -> KeySelector<K, D> {
|
fn into_selector(self) -> KeySelector<K, D> {
|
||||||
KeySelector::Has(vec![self])
|
self.into()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<K, D> IntoSelector<K, D> for KeySelector<K, D>
|
pub trait KeyPoolError:
|
||||||
where
|
From<reqwest::Error> + From<serde_json::Error> + From<torn_api::ApiError> + From<Arc<Self>> + Send
|
||||||
K: ApiKey,
|
{
|
||||||
D: KeyDomain,
|
}
|
||||||
|
|
||||||
|
impl<T> KeyPoolError for T where
|
||||||
|
T: From<reqwest::Error>
|
||||||
|
+ From<serde_json::Error>
|
||||||
|
+ From<torn_api::ApiError>
|
||||||
|
+ From<Arc<Self>>
|
||||||
|
+ Send
|
||||||
{
|
{
|
||||||
fn into_selector(self) -> KeySelector<K, D> {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait KeyPoolStorage: Send + Sync {
|
pub trait KeyPoolStorage: Send + Sync {
|
||||||
type Key: ApiKey;
|
type Key: ApiKey;
|
||||||
type Domain: KeyDomain;
|
type Domain: KeyDomain;
|
||||||
type Error: From<reqwest::Error> + From<serde_json::Error> + From<torn_api::ApiError> + Send;
|
type Error: KeyPoolError;
|
||||||
|
|
||||||
fn acquire_key<S>(
|
fn acquire_key<S>(
|
||||||
&self,
|
&self,
|
||||||
|
@ -206,65 +252,6 @@ where
|
||||||
>,
|
>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct KeyPoolExecutor<'p, S>
|
|
||||||
where
|
|
||||||
S: KeyPoolStorage,
|
|
||||||
{
|
|
||||||
pool: &'p KeyPool<S>,
|
|
||||||
selector: KeySelector<S::Key, S::Domain>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'p, S> KeyPoolExecutor<'p, S>
|
|
||||||
where
|
|
||||||
S: KeyPoolStorage,
|
|
||||||
{
|
|
||||||
pub fn new(pool: &'p KeyPool<S>, selector: KeySelector<S::Key, S::Domain>) -> Self {
|
|
||||||
Self { pool, selector }
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn execute_request<D>(&self, request: ApiRequest<D>) -> Result<ApiResponse<D>, S::Error>
|
|
||||||
where
|
|
||||||
D: Send,
|
|
||||||
{
|
|
||||||
let key = self.pool.storage.acquire_key(self.selector.clone()).await?;
|
|
||||||
|
|
||||||
let mut headers = HeaderMap::with_capacity(1);
|
|
||||||
headers.insert(
|
|
||||||
AUTHORIZATION,
|
|
||||||
HeaderValue::from_str(&format!("ApiKey {}", key.value())).unwrap(),
|
|
||||||
);
|
|
||||||
|
|
||||||
let resp = self
|
|
||||||
.pool
|
|
||||||
.client
|
|
||||||
.get(request.url())
|
|
||||||
.headers(headers)
|
|
||||||
.send()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let status = resp.status();
|
|
||||||
|
|
||||||
let bytes = resp.bytes().await?;
|
|
||||||
|
|
||||||
if let Some(err) = decode_error(&bytes)? {
|
|
||||||
if let Some(handler) = self.pool.options.error_hooks.get(&err.code()) {
|
|
||||||
let retry = (*handler)(&self.pool.storage, &key).await?;
|
|
||||||
|
|
||||||
if retry {
|
|
||||||
return Box::pin(self.execute_request(request)).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(err.into())
|
|
||||||
} else {
|
|
||||||
Ok(ApiResponse {
|
|
||||||
discriminant: request.disriminant,
|
|
||||||
body: Some(bytes),
|
|
||||||
status,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct PoolBuilder<S>
|
pub struct PoolBuilder<S>
|
||||||
where
|
where
|
||||||
S: KeyPoolStorage,
|
S: KeyPoolStorage,
|
||||||
|
@ -358,20 +345,147 @@ where
|
||||||
|
|
||||||
pub fn build(self) -> KeyPool<S> {
|
pub fn build(self) -> KeyPool<S> {
|
||||||
KeyPool {
|
KeyPool {
|
||||||
client: self.client,
|
inner: Arc::new(KeyPoolInner {
|
||||||
storage: self.storage,
|
client: self.client,
|
||||||
options: Arc::new(self.options),
|
storage: self.storage,
|
||||||
|
options: self.options,
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct KeyPoolInner<S>
|
||||||
|
where
|
||||||
|
S: KeyPoolStorage,
|
||||||
|
{
|
||||||
|
pub client: reqwest::Client,
|
||||||
|
pub storage: S,
|
||||||
|
pub options: PoolOptions<S>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S> KeyPoolInner<S>
|
||||||
|
where
|
||||||
|
S: KeyPoolStorage,
|
||||||
|
{
|
||||||
|
async fn execute_with_key(
|
||||||
|
&self,
|
||||||
|
key: &S::Key,
|
||||||
|
request: &ApiRequest,
|
||||||
|
) -> Result<RequestResult, S::Error> {
|
||||||
|
let mut headers = HeaderMap::with_capacity(1);
|
||||||
|
headers.insert(
|
||||||
|
AUTHORIZATION,
|
||||||
|
HeaderValue::from_str(&format!("ApiKey {}", key.value())).unwrap(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let resp = self
|
||||||
|
.client
|
||||||
|
.get(request.url())
|
||||||
|
.headers(headers)
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let status = resp.status();
|
||||||
|
|
||||||
|
let bytes = resp.bytes().await?;
|
||||||
|
|
||||||
|
if let Some(err) = decode_error(&bytes)? {
|
||||||
|
if let Some(handler) = self.options.error_hooks.get(&err.code()) {
|
||||||
|
let retry = (*handler)(&self.storage, key).await?;
|
||||||
|
|
||||||
|
if retry {
|
||||||
|
return Ok(RequestResult::Retry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err.into())
|
||||||
|
} else {
|
||||||
|
Ok(RequestResult::Response(ApiResponse {
|
||||||
|
body: Some(bytes),
|
||||||
|
status,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn execute_request(
|
||||||
|
&self,
|
||||||
|
selector: KeySelector<S::Key, S::Domain>,
|
||||||
|
request: ApiRequest,
|
||||||
|
) -> Result<ApiResponse, S::Error> {
|
||||||
|
loop {
|
||||||
|
let key = self.storage.acquire_key(selector.clone()).await?;
|
||||||
|
match self.execute_with_key(&key, &request).await {
|
||||||
|
Ok(RequestResult::Response(resp)) => return Ok(resp),
|
||||||
|
Ok(RequestResult::Retry) => (),
|
||||||
|
Err(why) => return Err(why),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn execute_bulk_requests<D, T: IntoIterator<Item = (D, ApiRequest)>>(
|
||||||
|
&self,
|
||||||
|
selector: KeySelector<S::Key, S::Domain>,
|
||||||
|
requests: T,
|
||||||
|
) -> impl Stream<Item = (D, Result<ApiResponse, S::Error>)> + use<'_, D, S, T> {
|
||||||
|
let requests: Vec<_> = requests.into_iter().collect();
|
||||||
|
|
||||||
|
let keys: Vec<_> = match self
|
||||||
|
.storage
|
||||||
|
.acquire_many_keys(selector.clone(), requests.len() as i64)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(keys) => keys.into_iter().map(Ok).collect(),
|
||||||
|
Err(why) => {
|
||||||
|
let why = Arc::new(why);
|
||||||
|
std::iter::repeat_n(why, requests.len())
|
||||||
|
.map(|e| Err(S::Error::from(e)))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
StreamExt::map(
|
||||||
|
futures::stream::iter(std::iter::zip(requests, keys)),
|
||||||
|
move |((discriminant, request), mut maybe_key)| {
|
||||||
|
let selector = selector.clone();
|
||||||
|
async move {
|
||||||
|
loop {
|
||||||
|
let key = match maybe_key {
|
||||||
|
Ok(key) => key,
|
||||||
|
Err(why) => return (discriminant, Err(why)),
|
||||||
|
};
|
||||||
|
match self.execute_with_key(&key, &request).await {
|
||||||
|
Ok(RequestResult::Response(resp)) => return (discriminant, Ok(resp)),
|
||||||
|
Ok(RequestResult::Retry) => (),
|
||||||
|
Err(why) => return (discriminant, Err(why)),
|
||||||
|
}
|
||||||
|
maybe_key = self.storage.acquire_key(selector.clone()).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.buffer_unordered(25)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct KeyPool<S>
|
pub struct KeyPool<S>
|
||||||
where
|
where
|
||||||
S: KeyPoolStorage,
|
S: KeyPoolStorage,
|
||||||
{
|
{
|
||||||
pub client: reqwest::Client,
|
inner: Arc<KeyPoolInner<S>>,
|
||||||
pub storage: S,
|
}
|
||||||
pub options: Arc<PoolOptions<S>>,
|
|
||||||
|
impl<S> Deref for KeyPool<S>
|
||||||
|
where
|
||||||
|
S: KeyPoolStorage,
|
||||||
|
{
|
||||||
|
type Target = KeyPoolInner<S>;
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.inner
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum RequestResult {
|
||||||
|
Response(ApiResponse),
|
||||||
|
Retry,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<S> KeyPool<S>
|
impl<S> KeyPool<S>
|
||||||
|
@ -384,6 +498,17 @@ where
|
||||||
{
|
{
|
||||||
KeyPoolExecutor::new(self, selector.into_selector())
|
KeyPoolExecutor::new(self, selector.into_selector())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn throttled_torn_api<I>(
|
||||||
|
&self,
|
||||||
|
selector: I,
|
||||||
|
distance: Duration,
|
||||||
|
) -> ThrottledKeyPoolExecutor<S>
|
||||||
|
where
|
||||||
|
I: IntoSelector<S::Key, S::Domain>,
|
||||||
|
{
|
||||||
|
ThrottledKeyPoolExecutor::new(self, selector.into_selector(), distance)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn decode_error(buf: &[u8]) -> Result<Option<ApiError>, serde_json::Error> {
|
fn decode_error(buf: &[u8]) -> Result<Option<ApiError>, serde_json::Error> {
|
||||||
|
@ -409,28 +534,145 @@ fn decode_error(buf: &[u8]) -> Result<Option<ApiError>, serde_json::Error> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<S> Executor for KeyPoolExecutor<'_, S>
|
pub struct KeyPoolExecutor<'p, S>
|
||||||
where
|
where
|
||||||
S: KeyPoolStorage,
|
S: KeyPoolStorage,
|
||||||
|
{
|
||||||
|
pool: &'p KeyPoolInner<S>,
|
||||||
|
selector: KeySelector<S::Key, S::Domain>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'p, S> KeyPoolExecutor<'p, S>
|
||||||
|
where
|
||||||
|
S: KeyPoolStorage,
|
||||||
|
{
|
||||||
|
pub fn new(pool: &'p KeyPool<S>, selector: KeySelector<S::Key, S::Domain>) -> Self {
|
||||||
|
Self {
|
||||||
|
pool: &pool.inner,
|
||||||
|
selector,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S> Executor for KeyPoolExecutor<'_, S>
|
||||||
|
where
|
||||||
|
S: KeyPoolStorage + 'static,
|
||||||
{
|
{
|
||||||
type Error = S::Error;
|
type Error = S::Error;
|
||||||
|
|
||||||
async fn execute<R>(
|
async fn execute<R>(self, request: R) -> (R::Discriminant, Result<ApiResponse, Self::Error>)
|
||||||
&self,
|
|
||||||
request: R,
|
|
||||||
) -> Result<torn_api::request::ApiResponse<R::Discriminant>, Self::Error>
|
|
||||||
where
|
where
|
||||||
R: torn_api::request::IntoRequest,
|
R: torn_api::request::IntoRequest,
|
||||||
{
|
{
|
||||||
let request = request.into_request();
|
let (d, request) = request.into_request();
|
||||||
|
|
||||||
self.execute_request(request).await
|
(d, self.pool.execute_request(self.selector, request).await)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S> BulkExecutor for KeyPoolExecutor<'_, S>
|
||||||
|
where
|
||||||
|
S: KeyPoolStorage + 'static,
|
||||||
|
{
|
||||||
|
type Error = S::Error;
|
||||||
|
|
||||||
|
fn execute<R>(
|
||||||
|
self,
|
||||||
|
requests: impl IntoIterator<Item = R>,
|
||||||
|
) -> impl futures::Stream<Item = (R::Discriminant, Result<ApiResponse, Self::Error>)> + Unpin
|
||||||
|
where
|
||||||
|
R: torn_api::request::IntoRequest,
|
||||||
|
{
|
||||||
|
let requests: Vec<_> = requests.into_iter().map(|r| r.into_request()).collect();
|
||||||
|
self.pool
|
||||||
|
.execute_bulk_requests(self.selector.clone(), requests)
|
||||||
|
.into_stream()
|
||||||
|
.flatten()
|
||||||
|
.boxed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ThrottledKeyPoolExecutor<'p, S>
|
||||||
|
where
|
||||||
|
S: KeyPoolStorage,
|
||||||
|
{
|
||||||
|
pool: &'p KeyPoolInner<S>,
|
||||||
|
selector: KeySelector<S::Key, S::Domain>,
|
||||||
|
distance: Duration,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S> Clone for ThrottledKeyPoolExecutor<'_, S>
|
||||||
|
where
|
||||||
|
S: KeyPoolStorage,
|
||||||
|
{
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
Self {
|
||||||
|
pool: self.pool,
|
||||||
|
selector: self.selector.clone(),
|
||||||
|
distance: self.distance,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S> ThrottledKeyPoolExecutor<'_, S>
|
||||||
|
where
|
||||||
|
S: KeyPoolStorage,
|
||||||
|
{
|
||||||
|
async fn execute_request(self, request: ApiRequest) -> Result<ApiResponse, S::Error> {
|
||||||
|
self.pool.execute_request(self.selector, request).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'p, S> ThrottledKeyPoolExecutor<'p, S>
|
||||||
|
where
|
||||||
|
S: KeyPoolStorage,
|
||||||
|
{
|
||||||
|
pub fn new(
|
||||||
|
pool: &'p KeyPool<S>,
|
||||||
|
selector: KeySelector<S::Key, S::Domain>,
|
||||||
|
distance: Duration,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
pool: &pool.inner,
|
||||||
|
selector,
|
||||||
|
distance,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S> BulkExecutor for ThrottledKeyPoolExecutor<'_, S>
|
||||||
|
where
|
||||||
|
S: KeyPoolStorage + 'static,
|
||||||
|
{
|
||||||
|
type Error = S::Error;
|
||||||
|
|
||||||
|
fn execute<R>(
|
||||||
|
self,
|
||||||
|
requests: impl IntoIterator<Item = R>,
|
||||||
|
) -> impl futures::Stream<Item = (R::Discriminant, Result<ApiResponse, Self::Error>)> + Unpin
|
||||||
|
where
|
||||||
|
R: torn_api::request::IntoRequest,
|
||||||
|
{
|
||||||
|
let requests: Vec<_> = requests.into_iter().map(|r| r.into_request()).collect();
|
||||||
|
StreamExt::map(
|
||||||
|
futures::stream::iter(requests).throttle(self.distance),
|
||||||
|
move |(d, request)| {
|
||||||
|
let this = self.clone();
|
||||||
|
async move {
|
||||||
|
let result = this.execute_request(request).await;
|
||||||
|
(d, result)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.buffer_unordered(25)
|
||||||
|
.boxed()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
#[cfg(feature = "postgres")]
|
||||||
mod test {
|
mod test {
|
||||||
use torn_api::executor::ExecutorExt;
|
use torn_api::executor::{BulkExecutorExt, ExecutorExt};
|
||||||
|
|
||||||
use crate::postgres;
|
use crate::postgres;
|
||||||
|
|
||||||
|
@ -451,4 +693,48 @@ mod test {
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[sqlx::test]
|
||||||
|
fn bulk(pool: sqlx::PgPool) {
|
||||||
|
let (storage, _) = postgres::test::setup(pool).await;
|
||||||
|
|
||||||
|
let pool = PoolBuilder::new(storage)
|
||||||
|
.use_default_hooks()
|
||||||
|
.comment("test_runner")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let responses = pool
|
||||||
|
.torn_api(postgres::test::Domain::All)
|
||||||
|
.faction_bulk()
|
||||||
|
.basic_for_id(vec![19.into(), 89.into()], |b| b);
|
||||||
|
let mut responses: Vec<_> = StreamExt::collect(responses).await;
|
||||||
|
|
||||||
|
let (_id1, basic1) = responses.pop().unwrap();
|
||||||
|
basic1.unwrap();
|
||||||
|
|
||||||
|
let (_id2, basic2) = responses.pop().unwrap();
|
||||||
|
basic2.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[sqlx::test]
|
||||||
|
fn bulk_trottled(pool: sqlx::PgPool) {
|
||||||
|
let (storage, _) = postgres::test::setup(pool).await;
|
||||||
|
|
||||||
|
let pool = PoolBuilder::new(storage)
|
||||||
|
.use_default_hooks()
|
||||||
|
.comment("test_runner")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let responses = pool
|
||||||
|
.throttled_torn_api(postgres::test::Domain::All, Duration::from_millis(500))
|
||||||
|
.faction_bulk()
|
||||||
|
.basic_for_id(vec![19.into(), 89.into()], |b| b);
|
||||||
|
let mut responses: Vec<_> = StreamExt::collect(responses).await;
|
||||||
|
|
||||||
|
let (_id1, basic1) = responses.pop().unwrap();
|
||||||
|
basic1.unwrap();
|
||||||
|
|
||||||
|
let (_id2, basic2) = responses.pop().unwrap();
|
||||||
|
basic2.unwrap();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
use futures::future::BoxFuture;
|
use futures::future::BoxFuture;
|
||||||
use indoc::indoc;
|
use indoc::formatdoc;
|
||||||
use sqlx::{FromRow, PgPool, Postgres, QueryBuilder};
|
use sqlx::{FromRow, PgPool, Postgres, QueryBuilder};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
|
@ -37,6 +39,9 @@ where
|
||||||
|
|
||||||
#[error("Key not found: '{0:?}'")]
|
#[error("Key not found: '{0:?}'")]
|
||||||
KeyNotFound(KeySelector<PgKey<D>, D>),
|
KeyNotFound(KeySelector<PgKey<D>, D>),
|
||||||
|
|
||||||
|
#[error("Failed to acquire keys in bulk: {0}")]
|
||||||
|
Bulk(#[from] Arc<Self>),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, FromRow)]
|
#[derive(Debug, Clone, FromRow)]
|
||||||
|
@ -93,6 +98,7 @@ where
|
||||||
{
|
{
|
||||||
pool: PgPool,
|
pool: PgPool,
|
||||||
limit: i16,
|
limit: i16,
|
||||||
|
schema: Option<String>,
|
||||||
_phantom: std::marker::PhantomData<D>,
|
_phantom: std::marker::PhantomData<D>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -117,62 +123,91 @@ impl<D> PgKeyPoolStorage<D>
|
||||||
where
|
where
|
||||||
D: PgKeyDomain,
|
D: PgKeyDomain,
|
||||||
{
|
{
|
||||||
pub fn new(pool: PgPool, limit: i16) -> Self {
|
pub fn new(pool: PgPool, limit: i16, schema: Option<String>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
pool,
|
pool,
|
||||||
limit,
|
limit,
|
||||||
|
schema,
|
||||||
_phantom: Default::default(),
|
_phantom: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn table_name(&self) -> String {
|
||||||
|
match self.schema.as_ref() {
|
||||||
|
Some(schema) => format!("{schema}.api_keys"),
|
||||||
|
None => "api_keys".to_owned(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unique_array_fn(&self) -> String {
|
||||||
|
match self.schema.as_ref() {
|
||||||
|
Some(schema) => format!("{schema}.__unique_jsonb_array"),
|
||||||
|
None => "__unique_jsonb_array".to_owned(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn filter_array_fn(&self) -> String {
|
||||||
|
match self.schema.as_ref() {
|
||||||
|
Some(schema) => format!("{schema}.__filter_jsonb_array"),
|
||||||
|
None => "__filter_jsonb_array".to_owned(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn initialise(&self) -> Result<(), PgKeyPoolError<D>> {
|
pub async fn initialise(&self) -> Result<(), PgKeyPoolError<D>> {
|
||||||
sqlx::query(indoc! {r#"
|
if let Some(schema) = self.schema.as_ref() {
|
||||||
CREATE TABLE IF NOT EXISTS api_keys (
|
sqlx::query(&format!("create schema if not exists {}", schema))
|
||||||
|
.execute(&self.pool)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlx::query(&formatdoc! {r#"
|
||||||
|
CREATE TABLE IF NOT EXISTS {} (
|
||||||
id serial primary key,
|
id serial primary key,
|
||||||
user_id int4 not null,
|
user_id int4 not null,
|
||||||
key char(16) not null,
|
key char(16) not null,
|
||||||
uses int2 not null default 0,
|
uses int2 not null default 0,
|
||||||
domains jsonb not null default '{}'::jsonb,
|
domains jsonb not null default '{{}}'::jsonb,
|
||||||
last_used timestamptz not null default now(),
|
last_used timestamptz not null default now(),
|
||||||
flag int2,
|
flag int2,
|
||||||
cooldown timestamptz,
|
cooldown timestamptz,
|
||||||
constraint "uq:api_keys.key" UNIQUE(key)
|
constraint "uq:api_keys.key" UNIQUE(key)
|
||||||
)"#
|
)"#,
|
||||||
|
self.table_name()
|
||||||
})
|
})
|
||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
sqlx::query(indoc! {r#"
|
sqlx::query(&formatdoc! {r#"
|
||||||
CREATE INDEX IF NOT EXISTS "idx:api_keys.domains" ON api_keys USING GIN(domains jsonb_path_ops)
|
CREATE INDEX IF NOT EXISTS "idx:api_keys.domains" ON {} USING GIN(domains jsonb_path_ops)
|
||||||
"#})
|
"#, self.table_name()})
|
||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
sqlx::query(indoc! {r#"
|
sqlx::query(&formatdoc! {r#"
|
||||||
CREATE INDEX IF NOT EXISTS "idx:api_keys.user_id" ON api_keys USING BTREE(user_id)
|
CREATE INDEX IF NOT EXISTS "idx:api_keys.user_id" ON {} USING BTREE(user_id)
|
||||||
"#})
|
"#, self.table_name()})
|
||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
sqlx::query(indoc! {r#"
|
sqlx::query(&formatdoc! {r#"
|
||||||
create or replace function __unique_jsonb_array(jsonb) returns jsonb
|
create or replace function {}(jsonb) returns jsonb
|
||||||
AS $$
|
AS $$
|
||||||
select jsonb_agg(d::jsonb) from (
|
select jsonb_agg(d::jsonb) from (
|
||||||
select distinct jsonb_array_elements_text($1) as d
|
select distinct jsonb_array_elements_text($1) as d
|
||||||
) t
|
) t
|
||||||
$$ language sql;
|
$$ language sql;
|
||||||
"#})
|
"#, self.unique_array_fn()})
|
||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
sqlx::query(indoc! {r#"
|
sqlx::query(&formatdoc! {r#"
|
||||||
create or replace function __filter_jsonb_array(jsonb, jsonb) returns jsonb
|
create or replace function {}(jsonb, jsonb) returns jsonb
|
||||||
AS $$
|
AS $$
|
||||||
select jsonb_agg(d::jsonb) from (
|
select jsonb_agg(d::jsonb) from (
|
||||||
select distinct jsonb_array_elements_text($1) as d
|
select distinct jsonb_array_elements_text($1) as d
|
||||||
) t where d<>$2::text
|
) t where d<>$2::text
|
||||||
$$ language sql;
|
$$ language sql;
|
||||||
"#})
|
"#, self.filter_array_fn()})
|
||||||
.execute(&self.pool)
|
.execute(&self.pool)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
@ -209,54 +244,52 @@ where
|
||||||
.execute(&mut *tx)
|
.execute(&mut *tx)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let mut qb = QueryBuilder::new(indoc::indoc! {
|
let mut qb = QueryBuilder::new(&formatdoc! {
|
||||||
r#"
|
r#"
|
||||||
with key as (
|
with key as (
|
||||||
select
|
select
|
||||||
id,
|
id,
|
||||||
0::int2 as uses
|
0::int2 as uses
|
||||||
from api_keys where last_used < date_trunc('minute', now())
|
from {} where last_used < date_trunc('minute', now())
|
||||||
and (cooldown is null or now() >= cooldown)
|
and (cooldown is null or now() >= cooldown)
|
||||||
and "#
|
and "#,
|
||||||
|
self.table_name()
|
||||||
});
|
});
|
||||||
|
|
||||||
build_predicate(&mut qb, &selector);
|
build_predicate(&mut qb, &selector);
|
||||||
|
|
||||||
qb.push(indoc::indoc! {
|
qb.push(formatdoc! {
|
||||||
"
|
"
|
||||||
\n union (
|
\n union (
|
||||||
select id, uses from api_keys
|
select id, uses from {}
|
||||||
where last_used >= date_trunc('minute', now())
|
where last_used >= date_trunc('minute', now())
|
||||||
and (cooldown is null or now() >= cooldown)
|
and (cooldown is null or now() >= cooldown)
|
||||||
and "
|
and ",
|
||||||
|
self.table_name()
|
||||||
});
|
});
|
||||||
|
|
||||||
build_predicate(&mut qb, &selector);
|
build_predicate(&mut qb, &selector);
|
||||||
|
|
||||||
qb.push(indoc::indoc! {
|
qb.push(formatdoc! {
|
||||||
"
|
"
|
||||||
\n order by uses asc limit 1
|
\n order by uses asc limit 1
|
||||||
)
|
)
|
||||||
order by uses asc limit 1
|
order by uses asc limit 1
|
||||||
)
|
)
|
||||||
update api_keys set
|
update {} as keys set
|
||||||
uses = key.uses + 1,
|
uses = key.uses + 1,
|
||||||
cooldown = null,
|
cooldown = null,
|
||||||
flag = null,
|
flag = null,
|
||||||
last_used = now()
|
last_used = now()
|
||||||
from key where
|
from key where
|
||||||
api_keys.id=key.id and key.uses < "
|
keys.id=key.id and key.uses < ",
|
||||||
|
self.table_name()
|
||||||
});
|
});
|
||||||
|
|
||||||
qb.push_bind(self.limit);
|
qb.push_bind(self.limit);
|
||||||
|
|
||||||
qb.push(indoc::indoc! { "
|
qb.push(indoc::indoc! { "
|
||||||
\nreturning
|
\nreturning keys.id, keys.user_id, keys.key, keys.uses, keys.domains"
|
||||||
api_keys.id,
|
|
||||||
api_keys.user_id,
|
|
||||||
api_keys.key,
|
|
||||||
api_keys.uses,
|
|
||||||
api_keys.domains"
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let key = qb.build_query_as().fetch_optional(&mut *tx).await?;
|
let key = qb.build_query_as().fetch_optional(&mut *tx).await?;
|
||||||
|
@ -321,19 +354,20 @@ where
|
||||||
.execute(&mut *tx)
|
.execute(&mut *tx)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let mut qb = QueryBuilder::new(indoc::indoc! {
|
let mut qb = QueryBuilder::new(&formatdoc! {
|
||||||
r#"select
|
r#"select
|
||||||
id,
|
id,
|
||||||
user_id,
|
user_id,
|
||||||
key,
|
key,
|
||||||
0::int2 as uses,
|
0::int2 as uses,
|
||||||
domains
|
domains
|
||||||
from api_keys where last_used < date_trunc('minute', now())
|
from {} where last_used < date_trunc('minute', now())
|
||||||
and (cooldown is null or now() >= cooldown)
|
and (cooldown is null or now() >= cooldown)
|
||||||
and "#
|
and "#,
|
||||||
|
self.table_name()
|
||||||
});
|
});
|
||||||
build_predicate(&mut qb, &selector);
|
build_predicate(&mut qb, &selector);
|
||||||
qb.push(indoc::indoc! {
|
qb.push(formatdoc! {
|
||||||
"
|
"
|
||||||
\nunion
|
\nunion
|
||||||
select
|
select
|
||||||
|
@ -342,9 +376,10 @@ where
|
||||||
key,
|
key,
|
||||||
uses,
|
uses,
|
||||||
domains
|
domains
|
||||||
from api_keys where last_used >= date_trunc('minute', now())
|
from {} where last_used >= date_trunc('minute', now())
|
||||||
and (cooldown is null or now() >= cooldown)
|
and (cooldown is null or now() >= cooldown)
|
||||||
and "
|
and ",
|
||||||
|
self.table_name()
|
||||||
});
|
});
|
||||||
build_predicate(&mut qb, &selector);
|
build_predicate(&mut qb, &selector);
|
||||||
qb.push("\norder by uses limit ");
|
qb.push("\norder by uses limit ");
|
||||||
|
@ -383,15 +418,15 @@ where
|
||||||
result.extend_from_slice(slice);
|
result.extend_from_slice(slice);
|
||||||
}
|
}
|
||||||
|
|
||||||
sqlx::query(indoc! {r#"
|
sqlx::query(&formatdoc! {r#"
|
||||||
update api_keys set
|
update {} keys set
|
||||||
uses = tmp.uses,
|
uses = tmp.uses,
|
||||||
cooldown = null,
|
cooldown = null,
|
||||||
flag = null,
|
flag = null,
|
||||||
last_used = now()
|
last_used = now()
|
||||||
from (select unnest($1::int4[]) as id, unnest($2::int2[]) as uses) as tmp
|
from (select unnest($1::int4[]) as id, unnest($2::int2[]) as uses) as tmp
|
||||||
where api_keys.id = tmp.id
|
where keys.id = tmp.id
|
||||||
"#})
|
"#, self.table_name()})
|
||||||
.bind(keys.iter().map(|k| k.id).collect::<Vec<_>>())
|
.bind(keys.iter().map(|k| k.id).collect::<Vec<_>>())
|
||||||
.bind(keys.iter().map(|k| k.uses).collect::<Vec<_>>())
|
.bind(keys.iter().map(|k| k.uses).collect::<Vec<_>>())
|
||||||
.execute(&mut *tx)
|
.execute(&mut *tx)
|
||||||
|
@ -452,7 +487,10 @@ where
|
||||||
{
|
{
|
||||||
let selector = selector.into_selector();
|
let selector = selector.into_selector();
|
||||||
|
|
||||||
let mut qb = QueryBuilder::new("update api_keys set cooldown=now() + ");
|
let mut qb = QueryBuilder::new(format!(
|
||||||
|
"update {} set cooldown=now() + ",
|
||||||
|
self.table_name()
|
||||||
|
));
|
||||||
qb.push_bind(duration);
|
qb.push_bind(duration);
|
||||||
qb.push(" where ");
|
qb.push(" where ");
|
||||||
build_predicate(&mut qb, &selector);
|
build_predicate(&mut qb, &selector);
|
||||||
|
@ -468,11 +506,13 @@ where
|
||||||
key: String,
|
key: String,
|
||||||
domains: Vec<D>,
|
domains: Vec<D>,
|
||||||
) -> Result<Self::Key, Self::Error> {
|
) -> Result<Self::Key, Self::Error> {
|
||||||
sqlx::query_as(
|
sqlx::query_as(&dbg!(formatdoc!(
|
||||||
"insert into api_keys(user_id, key, domains) values ($1, $2, $3) on conflict on \
|
"insert into {} as api_keys(user_id, key, domains) values ($1, $2, $3)
|
||||||
constraint \"uq:api_keys.key\" do update set domains = \
|
on conflict(key) do update
|
||||||
__unique_jsonb_array(excluded.domains || api_keys.domains) returning *",
|
set domains = {}(excluded.domains || api_keys.domains) returning *",
|
||||||
)
|
self.table_name(),
|
||||||
|
self.unique_array_fn()
|
||||||
|
)))
|
||||||
.bind(user_id)
|
.bind(user_id)
|
||||||
.bind(&key)
|
.bind(&key)
|
||||||
.bind(sqlx::types::Json(domains))
|
.bind(sqlx::types::Json(domains))
|
||||||
|
@ -487,7 +527,7 @@ where
|
||||||
{
|
{
|
||||||
let selector = selector.into_selector();
|
let selector = selector.into_selector();
|
||||||
|
|
||||||
let mut qb = QueryBuilder::new("select * from api_keys where ");
|
let mut qb = QueryBuilder::new(format!("select * from {} where ", self.table_name()));
|
||||||
build_predicate(&mut qb, &selector);
|
build_predicate(&mut qb, &selector);
|
||||||
|
|
||||||
qb.build_query_as()
|
qb.build_query_as()
|
||||||
|
@ -502,7 +542,7 @@ where
|
||||||
{
|
{
|
||||||
let selector = selector.into_selector();
|
let selector = selector.into_selector();
|
||||||
|
|
||||||
let mut qb = QueryBuilder::new("select * from api_keys where ");
|
let mut qb = QueryBuilder::new(format!("select * from {} where ", self.table_name()));
|
||||||
build_predicate(&mut qb, &selector);
|
build_predicate(&mut qb, &selector);
|
||||||
|
|
||||||
qb.build_query_as()
|
qb.build_query_as()
|
||||||
|
@ -517,7 +557,7 @@ where
|
||||||
{
|
{
|
||||||
let selector = selector.into_selector();
|
let selector = selector.into_selector();
|
||||||
|
|
||||||
let mut qb = QueryBuilder::new("delete from api_keys where ");
|
let mut qb = QueryBuilder::new(format!("delete from {} where ", self.table_name()));
|
||||||
build_predicate(&mut qb, &selector);
|
build_predicate(&mut qb, &selector);
|
||||||
qb.push(" returning *");
|
qb.push(" returning *");
|
||||||
|
|
||||||
|
@ -533,9 +573,11 @@ where
|
||||||
{
|
{
|
||||||
let selector = selector.into_selector();
|
let selector = selector.into_selector();
|
||||||
|
|
||||||
let mut qb = QueryBuilder::new(
|
let mut qb = QueryBuilder::new(format!(
|
||||||
"update api_keys set domains = __unique_jsonb_array(domains || jsonb_build_array(",
|
"update {} set domains = {}(domains || jsonb_build_array(",
|
||||||
);
|
self.table_name(),
|
||||||
|
self.unique_array_fn()
|
||||||
|
));
|
||||||
qb.push_bind(sqlx::types::Json(domain));
|
qb.push_bind(sqlx::types::Json(domain));
|
||||||
qb.push(")) where ");
|
qb.push(")) where ");
|
||||||
build_predicate(&mut qb, &selector);
|
build_predicate(&mut qb, &selector);
|
||||||
|
@ -557,9 +599,11 @@ where
|
||||||
{
|
{
|
||||||
let selector = selector.into_selector();
|
let selector = selector.into_selector();
|
||||||
|
|
||||||
let mut qb = QueryBuilder::new(
|
let mut qb = QueryBuilder::new(format!(
|
||||||
"update api_keys set domains = coalesce(__filter_jsonb_array(domains, ",
|
"update {} set domains = coalesce({}(domains, ",
|
||||||
);
|
self.table_name(),
|
||||||
|
self.filter_array_fn()
|
||||||
|
));
|
||||||
qb.push_bind(sqlx::types::Json(domain));
|
qb.push_bind(sqlx::types::Json(domain));
|
||||||
qb.push("), '[]'::jsonb) where ");
|
qb.push("), '[]'::jsonb) where ");
|
||||||
build_predicate(&mut qb, &selector);
|
build_predicate(&mut qb, &selector);
|
||||||
|
@ -626,7 +670,7 @@ pub(crate) mod test {
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let storage = PgKeyPoolStorage::new(pool.clone(), 1000);
|
let storage = PgKeyPoolStorage::new(pool.clone(), 1000, Some("test".to_owned()));
|
||||||
storage.initialise().await.unwrap();
|
storage.initialise().await.unwrap();
|
||||||
|
|
||||||
let key = storage
|
let key = storage
|
||||||
|
@ -823,7 +867,7 @@ pub(crate) mod test {
|
||||||
set.join_next().await.unwrap().unwrap();
|
set.join_next().await.unwrap().unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
let uses: i16 = sqlx::query("select uses from api_keys")
|
let uses: i16 = sqlx::query(&format!("select uses from {}", storage.table_name()))
|
||||||
.fetch_one(&storage.pool)
|
.fetch_one(&storage.pool)
|
||||||
.await
|
.await
|
||||||
.unwrap()
|
.unwrap()
|
||||||
|
@ -831,7 +875,7 @@ pub(crate) mod test {
|
||||||
|
|
||||||
assert_eq!(uses, 100);
|
assert_eq!(uses, 100);
|
||||||
|
|
||||||
sqlx::query("update api_keys set uses=0")
|
sqlx::query(&format!("update {} set uses=0", storage.table_name()))
|
||||||
.execute(&storage.pool)
|
.execute(&storage.pool)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
@ -871,7 +915,7 @@ pub(crate) mod test {
|
||||||
assert_eq!(key.uses, 2);
|
assert_eq!(key.uses, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
sqlx::query("update api_keys set uses=0")
|
sqlx::query(&format!("update {} set uses=0", storage.table_name()))
|
||||||
.execute(&storage.pool)
|
.execute(&storage.pool)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
@ -896,7 +940,7 @@ pub(crate) mod test {
|
||||||
set.join_next().await.unwrap().unwrap();
|
set.join_next().await.unwrap().unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
let uses: i16 = sqlx::query("select uses from api_keys")
|
let uses: i16 = sqlx::query(&format!("select uses from {}", storage.table_name()))
|
||||||
.fetch_one(&storage.pool)
|
.fetch_one(&storage.pool)
|
||||||
.await
|
.await
|
||||||
.unwrap()
|
.unwrap()
|
||||||
|
@ -904,7 +948,7 @@ pub(crate) mod test {
|
||||||
|
|
||||||
assert_eq!(uses, 500);
|
assert_eq!(uses, 500);
|
||||||
|
|
||||||
sqlx::query("update api_keys set uses=0")
|
sqlx::query(&format!("update {} set uses=0", storage.table_name()))
|
||||||
.execute(&storage.pool)
|
.execute(&storage.pool)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
Loading…
Reference in a new issue