mirror of
https://github.com/linabutler/ploidy
synced 2026-07-14 18:45:32 +00:00
chore: Initial commit.
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
.DS_Store
|
||||
/target
|
||||
Generated
+800
@@ -0,0 +1,800 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "addr2line"
|
||||
version = "0.25.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b"
|
||||
dependencies = [
|
||||
"gimli",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "adler2"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "0.6.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"anstyle-parse",
|
||||
"anstyle-query",
|
||||
"anstyle-wincon",
|
||||
"colorchoice",
|
||||
"is_terminal_polyfill",
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle"
|
||||
version = "1.0.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78"
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-parse"
|
||||
version = "0.2.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
|
||||
dependencies = [
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-query"
|
||||
version = "1.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
|
||||
dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-wincon"
|
||||
version = "3.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"once_cell_polyfill",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "backtrace"
|
||||
version = "0.3.76"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6"
|
||||
dependencies = [
|
||||
"addr2line",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"miniz_oxide",
|
||||
"object",
|
||||
"rustc-demangle",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "backtrace-ext"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "537beee3be4a18fb023b570f80e3ae28003db9167a751266b259926e25539d50"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
|
||||
|
||||
[[package]]
|
||||
name = "castaway"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a"
|
||||
dependencies = [
|
||||
"rustversion",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.53"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.53"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
"clap_lex",
|
||||
"strsim",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.5.49"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d"
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
|
||||
|
||||
[[package]]
|
||||
name = "compact_str"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3fdb1325a1cece981e8a296ab8f0f9b63ae357bd0784a9faaf548cc7b480707a"
|
||||
dependencies = [
|
||||
"castaway",
|
||||
"cfg-if",
|
||||
"itoa",
|
||||
"rustversion",
|
||||
"ryu",
|
||||
"static_assertions",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fixedbitset"
|
||||
version = "0.5.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99"
|
||||
|
||||
[[package]]
|
||||
name = "gimli"
|
||||
version = "0.32.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.16.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown",
|
||||
"serde",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indoc"
|
||||
version = "2.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706"
|
||||
dependencies = [
|
||||
"rustversion",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is_ci"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45"
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.177"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
|
||||
|
||||
[[package]]
|
||||
name = "miette"
|
||||
version = "7.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5f98efec8807c63c752b5bd61f862c165c115b0a35685bdcfd9238c7aeb592b7"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"backtrace-ext",
|
||||
"cfg-if",
|
||||
"miette-derive",
|
||||
"owo-colors",
|
||||
"supports-color",
|
||||
"supports-hyperlinks",
|
||||
"supports-unicode",
|
||||
"terminal_size",
|
||||
"textwrap",
|
||||
"unicode-width 0.1.14",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "miette-derive"
|
||||
version = "7.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db5b29714e950dbb20d5e6f74f9dcec4edbcc1067bb7f8ed198c097b8c1a818b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.8.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
|
||||
dependencies = [
|
||||
"adler2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "object"
|
||||
version = "0.37.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell_polyfill"
|
||||
version = "1.70.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
||||
|
||||
[[package]]
|
||||
name = "owo-colors"
|
||||
version = "4.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "48dd4f4a2c8405440fd0462561f0e5806bd0f77e86f51c761481bdd4018b545e"
|
||||
|
||||
[[package]]
|
||||
name = "percent-encoding"
|
||||
version = "2.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
|
||||
|
||||
[[package]]
|
||||
name = "petgraph"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772"
|
||||
dependencies = [
|
||||
"fixedbitset",
|
||||
"indexmap",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ploidy"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"compact_str",
|
||||
"heck",
|
||||
"indexmap",
|
||||
"indoc",
|
||||
"itertools",
|
||||
"miette",
|
||||
"percent-encoding",
|
||||
"petgraph",
|
||||
"prettyplease",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"semver",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_path_to_error",
|
||||
"serde_yaml",
|
||||
"syn",
|
||||
"textwrap",
|
||||
"thiserror",
|
||||
"toml",
|
||||
"unicase",
|
||||
"unicode-ident",
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prettyplease"
|
||||
version = "0.2.37"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.103"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.42"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-demangle"
|
||||
version = "0.1.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace"
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "1.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "1.0.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.145"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"itoa",
|
||||
"memchr",
|
||||
"ryu",
|
||||
"serde",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_path_to_error"
|
||||
version = "0.1.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"serde",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_spanned"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e24345aa0fe688594e73770a5f6d1b216508b4f93484c0026d521acd30134392"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_yaml"
|
||||
version = "0.9.34+deprecated"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"itoa",
|
||||
"ryu",
|
||||
"serde",
|
||||
"unsafe-libyaml",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "static_assertions"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "supports-color"
|
||||
version = "3.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c64fc7232dd8d2e4ac5ce4ef302b1d81e0b80d055b9d77c7c4f51f6aa4c867d6"
|
||||
dependencies = [
|
||||
"is_ci",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "supports-hyperlinks"
|
||||
version = "3.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "804f44ed3c63152de6a9f90acbea1a110441de43006ea51bcce8f436196a288b"
|
||||
|
||||
[[package]]
|
||||
name = "supports-unicode"
|
||||
version = "3.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7401a30af6cb5818bb64852270bb722533397edcfc7344954a38f420819ece2"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.111"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "terminal_size"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0"
|
||||
dependencies = [
|
||||
"rustix",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "textwrap"
|
||||
version = "0.16.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057"
|
||||
dependencies = [
|
||||
"unicode-linebreak",
|
||||
"unicode-width 0.2.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "2.0.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "2.0.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.9.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"serde_core",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
"toml_parser",
|
||||
"toml_writer",
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_parser"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e"
|
||||
dependencies = [
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_writer"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2"
|
||||
|
||||
[[package]]
|
||||
name = "unicase"
|
||||
version = "2.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-linebreak"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.1.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
|
||||
|
||||
[[package]]
|
||||
name = "unsafe-libyaml"
|
||||
version = "0.2.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
|
||||
|
||||
[[package]]
|
||||
name = "utf8parse"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.60.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.61.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.53.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
"windows_aarch64_gnullvm",
|
||||
"windows_aarch64_msvc",
|
||||
"windows_i686_gnu",
|
||||
"windows_i686_gnullvm",
|
||||
"windows_i686_msvc",
|
||||
"windows_x86_64_gnu",
|
||||
"windows_x86_64_gnullvm",
|
||||
"windows_x86_64_msvc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.7.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
[package]
|
||||
name = "ploidy"
|
||||
version = "0.1.0"
|
||||
description = "An OpenAPI client generator that supports polymorphic schemas"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
clap = { version = "4", features = ["cargo", "derive"] }
|
||||
compact_str = "0.9"
|
||||
heck = "0.5"
|
||||
indexmap = { version = "2", features = ["serde"] }
|
||||
indoc = "2"
|
||||
itertools = "0.14"
|
||||
miette = { version = "7", features = ["fancy"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = { version = "1", features = ["preserve_order"] }
|
||||
serde_path_to_error = "0.1"
|
||||
serde_yaml = "0.9.34"
|
||||
percent-encoding = "2.3"
|
||||
petgraph = "0.7"
|
||||
prettyplease = "0.2"
|
||||
proc-macro2 = { version = "1", default-features = false }
|
||||
quote = { version = "1", default-features = false }
|
||||
semver = { version = "1", features = ["serde"] }
|
||||
syn = { version = "2", default-features = false, features = [
|
||||
"parsing",
|
||||
"printing",
|
||||
] }
|
||||
textwrap = { version = "0.16", default-features = false, features = [
|
||||
"unicode-linebreak",
|
||||
"unicode-width",
|
||||
] }
|
||||
thiserror = "2"
|
||||
toml = { version = "0.9", default-features = false, features = [
|
||||
"display",
|
||||
"parse",
|
||||
"preserve_order",
|
||||
"serde",
|
||||
] }
|
||||
unicase = "2"
|
||||
unicode-ident = "1"
|
||||
winnow = "0.7"
|
||||
|
||||
[lib]
|
||||
name = "ploidy"
|
||||
path = "src/lib.rs"
|
||||
@@ -0,0 +1,42 @@
|
||||
# Ploidy
|
||||
|
||||
Ploidy is a code generator that supports polymorphic OpenAPI schemas. Its goal is to generate idiomatic Rust code for schemas with complex `oneOf`, `anyOf`, and `allOf` hierarchies.
|
||||
|
||||
## Installation
|
||||
|
||||
```sh
|
||||
cargo install ploidy
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
```sh
|
||||
ploidy codegen <INPUT-SPEC> <OUTPUT-DIR> rust
|
||||
```
|
||||
|
||||
This generates a complete Rust crate in the output directory, with:
|
||||
|
||||
* A `types` module, with Rust definitions for each named schema type.
|
||||
* A `client` module, with methods for each operation.
|
||||
|
||||
### Options
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `-c`, `--check` | Run `cargo check` on the generated code |
|
||||
| `--package-name <NAME>` | Override the generated package name |
|
||||
| `--package-version <VERSION>` | Override the generated package version |
|
||||
| `--package-license <LICENSE>` | Override the generated package license |
|
||||
| `--package-description <DESCRIPTION>` | Override the generated package description |
|
||||
|
||||
### Configuration File
|
||||
|
||||
In addition to the command-line options above, you can place a `.ploidy.toml` file in the output directory to configure generation:
|
||||
|
||||
```toml
|
||||
[rust.package]
|
||||
name = "my-api-client"
|
||||
version = "1.0.0"
|
||||
license = "MIT"
|
||||
description = "A generated OpenAPI client for my API"
|
||||
```
|
||||
@@ -0,0 +1,70 @@
|
||||
use std::path::Path;
|
||||
|
||||
use miette::{Context, IntoDiagnostic};
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::ToTokens;
|
||||
|
||||
pub mod rust;
|
||||
|
||||
mod unique;
|
||||
|
||||
pub use unique::{UniqueNameSpace, WordSegments};
|
||||
|
||||
pub fn write_to_disk(output: &Path, code: impl IntoCode) -> miette::Result<()> {
|
||||
let code = code.into_code();
|
||||
let path = output.join(code.path());
|
||||
let string = code.into_string()?;
|
||||
if let Some(parent) = path.parent() {
|
||||
std::fs::create_dir_all(parent)
|
||||
.into_diagnostic()
|
||||
.with_context(|| format!("Failed to create directory `{}`", parent.display()))?;
|
||||
}
|
||||
std::fs::write(&path, string)
|
||||
.into_diagnostic()
|
||||
.with_context(|| format!("Failed to write `{}`", path.display()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub trait Code {
|
||||
fn path(&self) -> &str;
|
||||
fn into_string(self) -> miette::Result<String>;
|
||||
}
|
||||
|
||||
impl<T: AsRef<str>> Code for (T, TokenStream) {
|
||||
fn path(&self) -> &str {
|
||||
self.0.as_ref()
|
||||
}
|
||||
|
||||
fn into_string(self) -> miette::Result<String> {
|
||||
let file = syn::parse2(self.1.into_token_stream())
|
||||
.into_diagnostic()
|
||||
.with_context(|| format!("Failed to format `{}`", self.0.as_ref()))?;
|
||||
Ok(prettyplease::unparse(&file))
|
||||
}
|
||||
}
|
||||
|
||||
impl Code for (&'static str, toml::map::Map<String, toml::Value>) {
|
||||
fn path(&self) -> &str {
|
||||
self.0
|
||||
}
|
||||
|
||||
fn into_string(self) -> miette::Result<String> {
|
||||
toml::to_string_pretty(&self.1)
|
||||
.into_diagnostic()
|
||||
.with_context(|| format!("Failed to serialize `{}`", self.0))
|
||||
}
|
||||
}
|
||||
|
||||
pub trait IntoCode {
|
||||
type Code: Code;
|
||||
|
||||
fn into_code(self) -> Self::Code;
|
||||
}
|
||||
|
||||
impl<T: Code> IntoCode for T {
|
||||
type Code = T;
|
||||
|
||||
fn into_code(self) -> Self::Code {
|
||||
self
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use itertools::Itertools;
|
||||
use toml::Value as TomlValue;
|
||||
|
||||
use crate::codegen::IntoCode;
|
||||
|
||||
use super::context::CodegenContext;
|
||||
|
||||
type TomlMap = toml::map::Map<String, TomlValue>;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct CargoManifest<'a> {
|
||||
context: &'a CodegenContext<'a>,
|
||||
}
|
||||
|
||||
impl<'a> CargoManifest<'a> {
|
||||
#[inline]
|
||||
pub fn new(context: &'a CodegenContext<'a>) -> Self {
|
||||
Self { context }
|
||||
}
|
||||
|
||||
pub fn into_map(self) -> TomlMap {
|
||||
let name = self.context.name;
|
||||
let version = self.context.version.to_string();
|
||||
let license = self.context.license;
|
||||
let mut package = toml::toml! {
|
||||
name = name
|
||||
version = version
|
||||
edition = "2024"
|
||||
license = license
|
||||
};
|
||||
if let Some(description) = self.context.description {
|
||||
package.insert("description".into(), description.into());
|
||||
}
|
||||
|
||||
let features = {
|
||||
let names: BTreeSet<_> = self
|
||||
.context
|
||||
.spec
|
||||
.operations()
|
||||
.map(|view| view.op().resource)
|
||||
.filter(|&name| name != "full")
|
||||
.collect();
|
||||
let mut features = names
|
||||
.iter()
|
||||
.map(|&name| (name.to_owned(), TomlValue::Array(vec![])))
|
||||
.collect::<TomlMap>();
|
||||
features.insert(
|
||||
"full".to_owned(),
|
||||
names
|
||||
.iter()
|
||||
.map(|&name| name.to_owned())
|
||||
.collect_vec()
|
||||
.into(),
|
||||
);
|
||||
features.insert("default".to_owned(), TomlValue::Array(vec![]));
|
||||
features
|
||||
};
|
||||
|
||||
toml::toml! {
|
||||
package = package
|
||||
features = features
|
||||
|
||||
[dependencies]
|
||||
bytes = { version = "1", features = ["serde"] }
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
http = "1"
|
||||
reqwest = { version = "0.12", default-features = false, features = ["http2", "json", "multipart", "rustls-tls"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
serde_path_to_error = "0.1"
|
||||
thiserror = "2"
|
||||
url = { version = "2.5", features = ["serde"] }
|
||||
uuid = { version = "1", features = ["serde", "v4"] }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoCode for CargoManifest<'_> {
|
||||
type Code = (&'static str, TomlMap);
|
||||
|
||||
fn into_code(self) -> Self::Code {
|
||||
("Cargo.toml", self.into_map())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
use itertools::Itertools;
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::{ToTokens, TokenStreamExt, quote};
|
||||
|
||||
use crate::codegen::{IntoCode, rust::CodegenIdent};
|
||||
|
||||
use super::context::CodegenContext;
|
||||
|
||||
/// Generates the `client/mod.rs` source file.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct CodegenClientModule<'a> {
|
||||
context: &'a CodegenContext<'a>,
|
||||
resources: &'a [&'a str],
|
||||
}
|
||||
|
||||
impl<'a> CodegenClientModule<'a> {
|
||||
pub fn new(context: &'a CodegenContext<'a>, resources: &'a [&'a str]) -> Self {
|
||||
Self { context, resources }
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for CodegenClientModule<'_> {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
let mods = self
|
||||
.resources
|
||||
.iter()
|
||||
.map(|resource| {
|
||||
let mod_name = CodegenIdent::Module(resource);
|
||||
quote! {
|
||||
#[cfg(feature = #resource)]
|
||||
pub mod #mod_name;
|
||||
}
|
||||
})
|
||||
.collect_vec();
|
||||
|
||||
let client_doc = {
|
||||
let info = self.context.spec.info();
|
||||
format!("API client for {} (version {})", info.title, info.version)
|
||||
};
|
||||
|
||||
tokens.append_all(quote! {
|
||||
#[doc = #client_doc]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Client {
|
||||
client: ::reqwest::Client,
|
||||
headers: ::http::HeaderMap,
|
||||
base_url: ::url::Url,
|
||||
}
|
||||
|
||||
impl Client {
|
||||
/// Create a new client.
|
||||
pub fn new(base_url: impl AsRef<str>) -> Result<Self, crate::error::Error> {
|
||||
Ok(Self::with_reqwest_client(
|
||||
::reqwest::Client::new(),
|
||||
base_url.as_ref().parse()?,
|
||||
))
|
||||
}
|
||||
|
||||
pub fn with_reqwest_client(client: ::reqwest::Client, base_url: ::url::Url) -> Self {
|
||||
Self {
|
||||
client,
|
||||
headers: ::http::HeaderMap::new(),
|
||||
base_url,
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds a header to each request.
|
||||
pub fn with_header<K, V>(mut self, name: K, value: V) -> Result<Self, crate::error::Error>
|
||||
where
|
||||
K: TryInto<::http::HeaderName>,
|
||||
V: TryInto<::http::HeaderValue>,
|
||||
K::Error: Into<::http::Error>,
|
||||
V::Error: Into<::http::Error>,
|
||||
{
|
||||
let name = name
|
||||
.try_into()
|
||||
.map_err(|err| crate::error::Error::BadHeaderName(err.into()))?;
|
||||
let value = value
|
||||
.try_into()
|
||||
.map_err(|err| crate::error::Error::BadHeaderValue(name.clone(), err.into()))?;
|
||||
self.headers.insert(name, value);
|
||||
Ok(Self {
|
||||
client: self.client,
|
||||
headers: self.headers,
|
||||
base_url: self.base_url,
|
||||
})
|
||||
}
|
||||
|
||||
/// Adds a sensitive header to each request, like a password or a bearer token.
|
||||
/// Sensitive headers won't appear in `Debug` output, and may be treated specially
|
||||
/// by the underlying HTTP stack.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// use reqwest::header::AUTHORIZATION;
|
||||
///
|
||||
/// let client = Client::new("https://api.example.com")?
|
||||
/// .with_sensitive_header(AUTHORIZATION, "Bearer decafbadcafed00d")?;
|
||||
/// ```
|
||||
pub fn with_sensitive_header<K, V>(self, name: K, value: V) -> Result<Self, crate::error::Error>
|
||||
where
|
||||
K: TryInto<::http::HeaderName>,
|
||||
V: TryInto<::http::HeaderValue>,
|
||||
K::Error: Into<::http::Error>,
|
||||
V::Error: Into<::http::Error>,
|
||||
{
|
||||
let name = name
|
||||
.try_into()
|
||||
.map_err(|err| crate::error::Error::BadHeaderName(err.into()))?;
|
||||
let mut value: ::http::HeaderValue = value
|
||||
.try_into()
|
||||
.map_err(|err| crate::error::Error::BadHeaderValue(name.clone(), err.into()))?;
|
||||
value.set_sensitive(true);
|
||||
self.with_header(name, value)
|
||||
}
|
||||
|
||||
pub fn with_user_agent<V>(self, value: V) -> Result<Self, crate::error::Error>
|
||||
where
|
||||
V: TryInto<::http::HeaderValue>,
|
||||
V::Error: Into<::http::Error>,
|
||||
{
|
||||
self.with_header(::http::header::USER_AGENT, value)
|
||||
}
|
||||
}
|
||||
|
||||
#(#mods)*
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoCode for CodegenClientModule<'_> {
|
||||
type Code = (&'static str, TokenStream);
|
||||
|
||||
fn into_code(self) -> Self::Code {
|
||||
("src/client/mod.rs", self.into_token_stream())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
use semver::Version;
|
||||
|
||||
use crate::ir::{InnerLeaf, InnerRef, IrSpec, IrType, PrimitiveIrType};
|
||||
|
||||
use super::SchemaIdentMap;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct CodegenContext<'a> {
|
||||
pub name: &'a str,
|
||||
pub version: Version,
|
||||
pub license: &'a str,
|
||||
pub description: Option<&'a str>,
|
||||
pub spec: &'a IrSpec<'a>,
|
||||
pub map: SchemaIdentMap<'a>,
|
||||
}
|
||||
|
||||
impl<'a> CodegenContext<'a> {
|
||||
pub fn new(
|
||||
name: &'a str,
|
||||
version: Version,
|
||||
license: &'a str,
|
||||
description: Option<&'a str>,
|
||||
spec: &'a IrSpec<'a>,
|
||||
) -> Self {
|
||||
Self {
|
||||
name,
|
||||
version,
|
||||
description,
|
||||
license,
|
||||
spec,
|
||||
map: SchemaIdentMap::new(spec),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn hashable(&self, ty: &IrType<'_>) -> bool {
|
||||
itertools::chain!(
|
||||
ty.visit::<InnerLeaf>(),
|
||||
ty.visit::<InnerRef>()
|
||||
.flat_map(|r| self.spec.lookup(r.name()))
|
||||
.flat_map(|view| view.refs())
|
||||
.flat_map(|view| view.ty().visit::<InnerLeaf>()),
|
||||
)
|
||||
.all(|leaf| {
|
||||
!matches!(
|
||||
leaf,
|
||||
InnerLeaf::Primitive(PrimitiveIrType::F32 | PrimitiveIrType::F64)
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::ToTokens;
|
||||
use syn::parse_quote;
|
||||
|
||||
/// Extra derives that can be added to types.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum ExtraDerive {
|
||||
/// Derive [`Eq`].
|
||||
///
|
||||
/// Excluded if the type is unhashable.
|
||||
Eq,
|
||||
|
||||
/// Derive [`Hash`][std::hash::Hash].
|
||||
///
|
||||
/// Excluded if the type is unhashable.
|
||||
Hash,
|
||||
|
||||
/// Derive [`Default`].
|
||||
///
|
||||
/// Included if all fields are optional.
|
||||
Default,
|
||||
}
|
||||
|
||||
impl ToTokens for ExtraDerive {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
let path: syn::Path = match self {
|
||||
Self::Eq => parse_quote!(Eq),
|
||||
Self::Hash => parse_quote!(Hash),
|
||||
Self::Default => parse_quote!(Default),
|
||||
};
|
||||
path.to_tokens(tokens);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::{ToTokens, TokenStreamExt, format_ident, quote};
|
||||
use syn::{Ident, parse_quote};
|
||||
|
||||
use crate::ir::{IrEnum, IrEnumVariant};
|
||||
|
||||
use super::{
|
||||
doc_attrs,
|
||||
naming::{CodegenIdent, CodegenTypeName},
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct CodegenEnum<'a> {
|
||||
name: CodegenTypeName<'a>,
|
||||
ty: &'a IrEnum<'a>,
|
||||
}
|
||||
|
||||
impl<'a> CodegenEnum<'a> {
|
||||
pub fn new(name: CodegenTypeName<'a>, ty: &'a IrEnum<'a>) -> Self {
|
||||
Self { name, ty }
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for CodegenEnum<'_> {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
let mut variants = Vec::new();
|
||||
let mut display_arms = Vec::new();
|
||||
let mut from_str_arms = Vec::new();
|
||||
|
||||
for variant in &self.ty.variants {
|
||||
match variant {
|
||||
IrEnumVariant::String(json_value) => {
|
||||
let variant_name = CodegenIdent::Variant(json_value);
|
||||
variants.push(quote! { #variant_name });
|
||||
display_arms.push(quote! { Self::#variant_name => #json_value });
|
||||
from_str_arms.push(quote! { #json_value => Self::#variant_name });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The catch-all `Other` variant comes last.
|
||||
let type_name: Ident = {
|
||||
let name = self.name;
|
||||
parse_quote!(#name)
|
||||
};
|
||||
let other_name = format_ident!("Other{}", type_name);
|
||||
variants.push(quote! {
|
||||
#[default]
|
||||
#other_name
|
||||
});
|
||||
display_arms.push(quote! { Self::#other_name => "(other)" });
|
||||
from_str_arms.push(quote! { _ => Self::#other_name });
|
||||
|
||||
let other_serialize_error = format!("can't serialize variant `{type_name}::{other_name}`");
|
||||
let expecting = format!("a variant of `{type_name}`");
|
||||
|
||||
let doc_attrs = self.ty.description.map(doc_attrs);
|
||||
|
||||
tokens.append_all(quote! {
|
||||
#doc_attrs
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
|
||||
pub enum #type_name {
|
||||
#(#variants),*
|
||||
}
|
||||
|
||||
impl #type_name {
|
||||
pub fn is_other(&self) -> bool {
|
||||
matches!(self, Self::#other_name)
|
||||
}
|
||||
}
|
||||
|
||||
impl ::std::fmt::Display for #type_name {
|
||||
fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
|
||||
f.write_str(match self {
|
||||
#(#display_arms),*
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ::std::str::FromStr for #type_name {
|
||||
type Err = ::std::convert::Infallible;
|
||||
|
||||
fn from_str(s: &str) -> ::std::result::Result<Self, Self::Err> {
|
||||
::std::result::Result::Ok(match s {
|
||||
#(#from_str_arms),*
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> ::serde::Deserialize<'de> for #type_name {
|
||||
fn deserialize<D: ::serde::Deserializer<'de>>(
|
||||
deserializer: D,
|
||||
) -> ::std::result::Result<Self, D::Error> {
|
||||
struct Visitor;
|
||||
impl<'de> ::serde::de::Visitor<'de> for Visitor {
|
||||
type Value = #type_name;
|
||||
|
||||
fn expecting(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
|
||||
f.write_str(#expecting)
|
||||
}
|
||||
|
||||
fn visit_str<E: ::serde::de::Error>(
|
||||
self,
|
||||
s: &str,
|
||||
) -> ::std::result::Result<Self::Value, E> {
|
||||
let ::std::result::Result::Ok(v) = ::std::str::FromStr::from_str(s);
|
||||
Ok(v)
|
||||
}
|
||||
}
|
||||
::serde::Deserializer::deserialize_str(deserializer, Visitor)
|
||||
}
|
||||
}
|
||||
|
||||
impl ::serde::Serialize for #type_name {
|
||||
fn serialize<S: ::serde::Serializer>(
|
||||
&self,
|
||||
serializer: S,
|
||||
) -> ::std::result::Result<S::Ok, S::Error> {
|
||||
match self {
|
||||
Self::#other_name => Err(::serde::ser::Error::custom(#other_serialize_error)),
|
||||
v => v.to_string().serialize(serializer),
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
use std::{
|
||||
collections::{BTreeMap, BTreeSet},
|
||||
path::Path,
|
||||
};
|
||||
|
||||
use indexmap::IndexMap;
|
||||
use itertools::Itertools;
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::quote;
|
||||
use syn::{Ident, parse_quote};
|
||||
|
||||
use crate::{
|
||||
codegen::{IntoCode, unique::UniqueNameSpace, write_to_disk},
|
||||
ir::{IrOperationView, IrSpec, IrType},
|
||||
};
|
||||
|
||||
mod cargo;
|
||||
mod client;
|
||||
mod context;
|
||||
mod derives;
|
||||
mod enum_;
|
||||
mod naming;
|
||||
mod operation;
|
||||
mod ref_;
|
||||
mod resource;
|
||||
mod schema;
|
||||
mod statics;
|
||||
mod struct_;
|
||||
mod tagged;
|
||||
mod types;
|
||||
mod untagged;
|
||||
|
||||
pub use cargo::*;
|
||||
pub use client::*;
|
||||
pub use context::*;
|
||||
pub use naming::*;
|
||||
pub use operation::*;
|
||||
pub use resource::*;
|
||||
pub use schema::*;
|
||||
pub use statics::*;
|
||||
pub use types::*;
|
||||
|
||||
pub fn write_types_to_disk(output: &Path, context: &CodegenContext<'_>) -> miette::Result<()> {
|
||||
let mut resources_by_type = BTreeMap::<&str, BTreeSet<&str>>::new();
|
||||
for view in context.spec.operations() {
|
||||
let resource = view.op().resource;
|
||||
for v in view.refs() {
|
||||
resources_by_type
|
||||
.entry(v.name())
|
||||
.or_default()
|
||||
.insert(resource);
|
||||
}
|
||||
}
|
||||
|
||||
for view in context.spec.schemas() {
|
||||
let name = view.name();
|
||||
let ty = view.ty();
|
||||
let Some(info) = context.map.0.get(name) else {
|
||||
continue;
|
||||
};
|
||||
if !resources_by_type.contains_key(name) {
|
||||
continue;
|
||||
}
|
||||
let name = CodegenTypeName::Schema(name, &info.ty);
|
||||
let code = match ty {
|
||||
IrType::Schema(ty) => CodegenSchemaType::new(context, name, ty).into_code(),
|
||||
IrType::Nullable(ty) | IrType::Array(ty) | IrType::Map(ty) => {
|
||||
CodegenSchemaTypeAlias::new(context, name, ty.as_ref()).into_code()
|
||||
}
|
||||
IrType::Any => CodegenSchemaTypeAlias::new(context, name, &IrType::Any).into_code(),
|
||||
IrType::Primitive(_) | IrType::Inline(..) | IrType::Ref(..) => continue,
|
||||
};
|
||||
write_to_disk(output, code)?;
|
||||
}
|
||||
|
||||
write_to_disk(output, CodegenTypesModule::new(context))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn write_client_to_disk(output: &Path, context: &CodegenContext<'_>) -> miette::Result<()> {
|
||||
let mut operations_by_resource: BTreeMap<&str, Vec<IrOperationView<'_>>> = BTreeMap::new();
|
||||
for view in context.spec.operations() {
|
||||
let resource = view.op().resource;
|
||||
operations_by_resource
|
||||
.entry(resource)
|
||||
.or_default()
|
||||
.push(view);
|
||||
}
|
||||
|
||||
for (resource, operations) in &operations_by_resource {
|
||||
let code = CodegenResource::new(context, resource, operations.as_slice());
|
||||
write_to_disk(output, code)?;
|
||||
}
|
||||
|
||||
let resources = operations_by_resource.keys().cloned().collect_vec();
|
||||
let mod_code = CodegenClientModule::new(context, &resources);
|
||||
write_to_disk(output, mod_code)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Generates one or more `#[doc]` attributes for a schema description,
|
||||
/// wrapping at 80 characters for readability.
|
||||
pub fn doc_attrs(description: &str) -> TokenStream {
|
||||
let lines = textwrap::wrap(description, 80)
|
||||
.into_iter()
|
||||
.map(|line| quote!(#[doc = #line]));
|
||||
quote! { #(#lines)* }
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SchemaIdents {
|
||||
pub module: Ident,
|
||||
pub ty: Ident,
|
||||
}
|
||||
|
||||
/// A mapping of schema names from the original spec
|
||||
/// to Rust identifiers for the generated module and type names.
|
||||
#[derive(Debug)]
|
||||
pub struct SchemaIdentMap<'a>(pub IndexMap<&'a str, SchemaIdents>);
|
||||
|
||||
impl<'a> SchemaIdentMap<'a> {
|
||||
pub fn new(spec: &'a IrSpec<'a>) -> Self {
|
||||
let mut space = UniqueNameSpace::new();
|
||||
let map = spec
|
||||
.schemas()
|
||||
.map(|view| {
|
||||
let name = view.name();
|
||||
let unique_name = space.uniquify(name);
|
||||
let module = CodegenIdent::Module(&unique_name);
|
||||
let ty = CodegenIdent::Type(&unique_name);
|
||||
(
|
||||
name,
|
||||
SchemaIdents {
|
||||
module: parse_quote!(#module),
|
||||
ty: parse_quote!(#ty),
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
Self(map)
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> impl Iterator<Item = (&str, &SchemaIdents)> {
|
||||
self.0.iter().map(|(&key, idents)| (key, idents))
|
||||
}
|
||||
|
||||
pub fn module(&self, name: &str) -> Option<&Ident> {
|
||||
self.0.get(name).map(|idents| &idents.module)
|
||||
}
|
||||
|
||||
pub fn ty(&self, name: &str) -> Option<&Ident> {
|
||||
self.0.get(name).map(|idents| &idents.ty)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use heck::{ToPascalCase, ToSnakeCase};
|
||||
use proc_macro2::{Ident, Span, TokenStream};
|
||||
use quote::{IdentFragment, ToTokens, TokenStreamExt, format_ident};
|
||||
|
||||
use crate::ir::{
|
||||
InlineIrTypePath, InlineIrTypePathSegment, IrUntaggedVariantNameHint, PrimitiveIrType,
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum CodegenTypeName<'a> {
|
||||
Schema(&'a str, &'a Ident),
|
||||
Inline(&'a InlineIrTypePath<'a>),
|
||||
}
|
||||
|
||||
impl ToTokens for CodegenTypeName<'_> {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
match self {
|
||||
&Self::Schema(_, ident) => tokens.append(ident.clone()),
|
||||
Self::Inline(path) => {
|
||||
let ident = path
|
||||
.segments
|
||||
.iter()
|
||||
.map(CodegenTypePathSegment)
|
||||
.fold(None, |ident, segment| {
|
||||
Some(match ident {
|
||||
Some(ident) => format_ident!("{}{}", ident, segment),
|
||||
None => format_ident!("{}", segment),
|
||||
})
|
||||
})
|
||||
.ok_or_else(|| syn::Error::new(Span::call_site(), "empty inline type path"));
|
||||
match ident {
|
||||
Ok(ident) => tokens.append(ident),
|
||||
Err(err) => tokens.append_all(err.into_compile_error()),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum CodegenIdent<'a> {
|
||||
Module(&'a str),
|
||||
Type(&'a str),
|
||||
Field(&'a str),
|
||||
Variant(&'a str),
|
||||
Param(&'a str),
|
||||
Method(&'a str),
|
||||
}
|
||||
|
||||
impl<'a> CodegenIdent<'a> {
|
||||
fn name(&self) -> &'a str {
|
||||
let (Self::Module(s)
|
||||
| Self::Type(s)
|
||||
| Self::Field(s)
|
||||
| Self::Variant(s)
|
||||
| Self::Param(s)
|
||||
| Self::Method(s)) = self;
|
||||
s
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for CodegenIdent<'_> {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
let cased = match self {
|
||||
Self::Module(name) | Self::Field(name) | Self::Param(name) | Self::Method(name) => {
|
||||
name.to_snake_case()
|
||||
}
|
||||
Self::Type(name) | Self::Variant(name) => name.to_pascal_case(),
|
||||
};
|
||||
let cleaned = clean(&cased);
|
||||
let ident: syn::Result<Ident> = syn::parse_str(&cleaned)
|
||||
.or_else(|_| syn::parse_str(&format!("r#{cleaned}")))
|
||||
.or_else(|_| syn::parse_str(&format!("{cleaned}_")))
|
||||
.map_err(|_| {
|
||||
syn::Error::new(
|
||||
Span::call_site(),
|
||||
format!(
|
||||
"`{}` can't be represented as a Rust identifier",
|
||||
self.name()
|
||||
),
|
||||
)
|
||||
});
|
||||
match ident {
|
||||
Ok(ident) => tokens.append(ident),
|
||||
Err(err) => tokens.append_all(err.into_compile_error()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct CodegenUntaggedVariantName(pub IrUntaggedVariantNameHint);
|
||||
|
||||
impl ToTokens for CodegenUntaggedVariantName {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
use IrUntaggedVariantNameHint::*;
|
||||
let s = match self.0 {
|
||||
Primitive(PrimitiveIrType::String) => "String".into(),
|
||||
Primitive(PrimitiveIrType::I32) => "I32".into(),
|
||||
Primitive(PrimitiveIrType::I64) => "I64".into(),
|
||||
Primitive(PrimitiveIrType::F32) => "F32".into(),
|
||||
Primitive(PrimitiveIrType::F64) => "F64".into(),
|
||||
Primitive(PrimitiveIrType::Bool) => "Bool".into(),
|
||||
Primitive(PrimitiveIrType::DateTime) => "DateTime".into(),
|
||||
Primitive(PrimitiveIrType::Date) => "Date".into(),
|
||||
Primitive(PrimitiveIrType::Url) => "Url".into(),
|
||||
Primitive(PrimitiveIrType::Uuid) => "Uuid".into(),
|
||||
Primitive(PrimitiveIrType::Bytes) => "Bytes".into(),
|
||||
Array => "Array".into(),
|
||||
Map => "Map".into(),
|
||||
Index(index) => Cow::Owned(format!("V{index}")),
|
||||
};
|
||||
tokens.append(Ident::new(&s, Span::call_site()));
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct CodegenTypePathSegment<'a>(&'a InlineIrTypePathSegment<'a>);
|
||||
|
||||
impl IdentFragment for CodegenTypePathSegment<'_> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
match self.0 {
|
||||
InlineIrTypePathSegment::Operation(name) => f.write_str(&name.to_pascal_case()),
|
||||
InlineIrTypePathSegment::Parameter(name) => f.write_str(&name.to_pascal_case()),
|
||||
InlineIrTypePathSegment::Request => f.write_str("Request"),
|
||||
InlineIrTypePathSegment::Response => f.write_str("Response"),
|
||||
InlineIrTypePathSegment::Field(name) => f.write_str(&name.to_pascal_case()),
|
||||
InlineIrTypePathSegment::MapValue => f.write_str("Value"),
|
||||
InlineIrTypePathSegment::ArrayItem => f.write_str("Item"),
|
||||
InlineIrTypePathSegment::Variant(index) => write!(f, "V{index}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clean(s: &str) -> String {
|
||||
let mut chars = s.chars();
|
||||
let Some(first) = chars.next() else {
|
||||
return String::new();
|
||||
};
|
||||
let mut string = String::with_capacity(s.len());
|
||||
if first == '_' || unicode_ident::is_xid_start(first) {
|
||||
string.push(first);
|
||||
} else {
|
||||
string.push('_');
|
||||
chars = s.chars();
|
||||
}
|
||||
string.push_str(
|
||||
&chars
|
||||
.as_str()
|
||||
.replace(|next| !unicode_ident::is_xid_continue(next), "_"),
|
||||
);
|
||||
string
|
||||
}
|
||||
@@ -0,0 +1,272 @@
|
||||
use itertools::Itertools;
|
||||
use proc_macro2::{Span, TokenStream};
|
||||
use quote::{ToTokens, TokenStreamExt, quote};
|
||||
use syn::Ident;
|
||||
|
||||
use crate::{
|
||||
codegen::rust::doc_attrs,
|
||||
ir::{IrOperation, IrParameter, IrParameterInfo, IrRequest, IrResponse, IrType},
|
||||
parse::{Method, path::PathFragment},
|
||||
};
|
||||
|
||||
use super::{context::CodegenContext, naming::CodegenIdent, ref_::CodegenRef};
|
||||
|
||||
/// Generates a single client method for an API operation.
|
||||
pub struct CodegenOperation<'a> {
|
||||
context: &'a CodegenContext<'a>,
|
||||
op: &'a IrOperation<'a>,
|
||||
}
|
||||
|
||||
impl<'a> CodegenOperation<'a> {
|
||||
pub fn new(context: &'a CodegenContext<'a>, op: &'a IrOperation<'a>) -> Self {
|
||||
Self { context, op }
|
||||
}
|
||||
|
||||
/// Generates code to build the request URL, with path parameters substituted.
|
||||
fn url(&self, params: &[&IrParameterInfo<'_>]) -> TokenStream {
|
||||
let segments = &self.op.path;
|
||||
let segs = segments.iter().map(|segment| match segment.fragments() {
|
||||
[] => quote! { "" },
|
||||
[PathFragment::Literal(text)] => quote! { #text },
|
||||
[PathFragment::Param(name)] => {
|
||||
let info = params.iter().find(|param| param.name == *name).unwrap();
|
||||
let value = CodegenIdent::Param(info.name);
|
||||
quote! { #value }
|
||||
}
|
||||
fragments => {
|
||||
// Build a format string, with placeholders for parameter fragments.
|
||||
let format = fragments.iter().fold(String::new(), |mut f, fragment| {
|
||||
match fragment {
|
||||
PathFragment::Literal(text) => {
|
||||
f.push_str(&text.replace('{', "{{").replace('}', "}}"))
|
||||
}
|
||||
PathFragment::Param(_) => f.push_str("{}"),
|
||||
}
|
||||
f
|
||||
});
|
||||
let args = fragments
|
||||
.iter()
|
||||
.filter_map(|fragment| match fragment {
|
||||
PathFragment::Param(name) => Some(name),
|
||||
PathFragment::Literal(_) => None,
|
||||
})
|
||||
.map(|name| {
|
||||
// `url::PathSegmentsMut::push` percent-encodes the
|
||||
// full segment, so we can interpolate fragments
|
||||
// directly.
|
||||
let info = params.iter().find(|param| param.name == *name).unwrap();
|
||||
CodegenIdent::Param(info.name)
|
||||
});
|
||||
quote! { &format!(#format, #(#args),*) }
|
||||
}
|
||||
});
|
||||
quote! {
|
||||
let url = {
|
||||
let mut url = self.base_url.clone();
|
||||
url
|
||||
.path_segments_mut()
|
||||
.map_err(|()| crate::error::Error::UrlCannotBeABase)?
|
||||
.pop_if_empty()
|
||||
#(.push(#segs))*;
|
||||
url
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates code to append query parameters.
|
||||
fn query(&self, params: &[&IrParameterInfo<'_>]) -> TokenStream {
|
||||
if params.is_empty() {
|
||||
return quote! {};
|
||||
}
|
||||
|
||||
let mut param_serializations = Vec::new();
|
||||
|
||||
for param in params {
|
||||
let param_name_str = ¶m.name;
|
||||
let param_ident = CodegenIdent::Param(param.name);
|
||||
|
||||
let serialization = match (¶m.ty, param.required) {
|
||||
(IrType::Array(_), true) => {
|
||||
quote! {
|
||||
for value in &#param_ident {
|
||||
url.query_pairs_mut()
|
||||
.append_pair(#param_name_str, &value.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
(IrType::Array(_), false) => {
|
||||
quote! {
|
||||
if let Some(ref values) = #param_ident {
|
||||
for value in values {
|
||||
url.query_pairs_mut()
|
||||
.append_pair(#param_name_str, &value.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
(_, true) => {
|
||||
quote! {
|
||||
url.query_pairs_mut()
|
||||
.append_pair(#param_name_str, &#param_ident.to_string());
|
||||
}
|
||||
}
|
||||
(_, false) => {
|
||||
quote! {
|
||||
if let Some(ref value) = #param_ident {
|
||||
url.query_pairs_mut()
|
||||
.append_pair(#param_name_str, &value.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
param_serializations.push(serialization);
|
||||
}
|
||||
|
||||
quote! {
|
||||
let url = {
|
||||
let mut url = url;
|
||||
#(#param_serializations)*
|
||||
url
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for CodegenOperation<'_> {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
let operation_id = self.op.id;
|
||||
let method_name = CodegenIdent::Method(operation_id);
|
||||
|
||||
let mut params = Vec::new();
|
||||
|
||||
let path_params = self
|
||||
.op
|
||||
.params
|
||||
.iter()
|
||||
.filter_map(|param| match param {
|
||||
IrParameter::Path(info) => Some(info),
|
||||
_ => None,
|
||||
})
|
||||
.collect_vec();
|
||||
for param in &path_params {
|
||||
let param_name = CodegenIdent::Param(param.name);
|
||||
params.push(quote! { #param_name: &str });
|
||||
}
|
||||
|
||||
let query_params = self
|
||||
.op
|
||||
.params
|
||||
.iter()
|
||||
.filter_map(|param| match param {
|
||||
IrParameter::Query(info) => Some(info),
|
||||
_ => None,
|
||||
})
|
||||
.collect_vec();
|
||||
for param in &query_params {
|
||||
let param_name = CodegenIdent::Param(param.name);
|
||||
let base_type = CodegenRef::new(self.context, ¶m.ty);
|
||||
let param_type = if param.required || matches!(param.ty, IrType::Nullable(_)) {
|
||||
quote!(#base_type)
|
||||
} else {
|
||||
quote! { ::std::option::Option<#base_type> }
|
||||
};
|
||||
params.push(quote! { #param_name: #param_type });
|
||||
}
|
||||
|
||||
if let Some(body_info) = &self.op.request {
|
||||
match body_info {
|
||||
IrRequest::Json(ty) => {
|
||||
let param_type = CodegenRef::new(self.context, ty);
|
||||
params.push(quote! { request: impl Into<#param_type> });
|
||||
}
|
||||
IrRequest::Multipart => {
|
||||
params.push(quote! { form: reqwest::multipart::Form });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let return_type = match &self.op.response {
|
||||
Some(IrResponse::Json(ty)) => CodegenRef::new(self.context, ty).into_token_stream(),
|
||||
None => quote! { () },
|
||||
};
|
||||
|
||||
let build_url = self.url(&path_params);
|
||||
let build_query = self.query(&query_params);
|
||||
|
||||
let http_method = CodegenMethod(self.op.method);
|
||||
let build_request = match &self.op.request {
|
||||
Some(IrRequest::Json(_)) => quote! {
|
||||
let response = self.client
|
||||
.#http_method(url)
|
||||
.headers(self.headers.clone())
|
||||
.json(&request.into())
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?;
|
||||
},
|
||||
Some(IrRequest::Multipart) => quote! {
|
||||
let response = self.client
|
||||
.#http_method(url)
|
||||
.headers(self.headers.clone())
|
||||
.multipart(form)
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?;
|
||||
},
|
||||
None => quote! {
|
||||
let response = self.client
|
||||
.#http_method(url)
|
||||
.headers(self.headers.clone())
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?;
|
||||
},
|
||||
};
|
||||
|
||||
let parse_response = if self.op.response.is_some() {
|
||||
quote! {
|
||||
let body = response.bytes().await?;
|
||||
let deserializer = &mut serde_json::Deserializer::from_slice(&body);
|
||||
let result = serde_path_to_error::deserialize(deserializer)
|
||||
.map_err(crate::error::JsonError::from)?;
|
||||
Ok(result)
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
let _ = response;
|
||||
Ok(())
|
||||
}
|
||||
};
|
||||
|
||||
let doc = self.op.description.map(doc_attrs);
|
||||
|
||||
tokens.append_all(quote! {
|
||||
#doc
|
||||
pub async fn #method_name(
|
||||
&self,
|
||||
#(#params),*
|
||||
) -> Result<#return_type, crate::error::Error> {
|
||||
#build_url
|
||||
#build_query
|
||||
#build_request
|
||||
#parse_response
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct CodegenMethod(pub Method);
|
||||
|
||||
impl ToTokens for CodegenMethod {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
tokens.append(match self.0 {
|
||||
Method::Get => Ident::new("get", Span::call_site()),
|
||||
Method::Post => Ident::new("post", Span::call_site()),
|
||||
Method::Put => Ident::new("put", Span::call_site()),
|
||||
Method::Patch => Ident::new("patch", Span::call_site()),
|
||||
Method::Delete => Ident::new("delete", Span::call_site()),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
use heck::ToSnakeCase;
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::{ToTokens, TokenStreamExt, format_ident, quote};
|
||||
use syn::parse_quote;
|
||||
|
||||
use crate::{
|
||||
codegen::rust::CodegenIdent,
|
||||
ir::{InlineIrTypePathRoot, IrType, PrimitiveIrType},
|
||||
};
|
||||
|
||||
use super::{context::CodegenContext, naming::CodegenTypeName};
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct CodegenRef<'a> {
|
||||
context: &'a CodegenContext<'a>,
|
||||
ty: &'a IrType<'a>,
|
||||
}
|
||||
|
||||
impl<'a> CodegenRef<'a> {
|
||||
pub fn new(context: &'a CodegenContext<'a>, ty: &'a IrType<'a>) -> Self {
|
||||
Self { context, ty }
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for CodegenRef<'_> {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
tokens.append_all(match self.ty {
|
||||
&IrType::Primitive(PrimitiveIrType::String) => quote! { ::std::string::String },
|
||||
&IrType::Primitive(PrimitiveIrType::I32) => quote! { i32 },
|
||||
&IrType::Primitive(PrimitiveIrType::I64) => quote! { i64 },
|
||||
&IrType::Primitive(PrimitiveIrType::F32) => quote! { f32 },
|
||||
&IrType::Primitive(PrimitiveIrType::F64) => quote! { f64 },
|
||||
&IrType::Primitive(PrimitiveIrType::Bool) => quote! { bool },
|
||||
&IrType::Primitive(PrimitiveIrType::DateTime) => {
|
||||
quote! { crate::date_time::UnixMilliseconds }
|
||||
}
|
||||
&IrType::Primitive(PrimitiveIrType::Date) => quote! { ::chrono::NaiveDate },
|
||||
&IrType::Primitive(PrimitiveIrType::Url) => quote! { ::url::Url },
|
||||
&IrType::Primitive(PrimitiveIrType::Uuid) => quote! { ::uuid::Uuid },
|
||||
&IrType::Primitive(PrimitiveIrType::Bytes) => quote! { ::bytes::Bytes },
|
||||
IrType::Array(ty) => {
|
||||
let ty = CodegenRef::new(self.context, ty.as_ref());
|
||||
quote! { ::std::vec::Vec<#ty> }
|
||||
}
|
||||
IrType::Map(ty) => {
|
||||
let ty = CodegenRef::new(self.context, ty.as_ref());
|
||||
quote! { ::std::collections::BTreeMap<::std::string::String, #ty> }
|
||||
}
|
||||
IrType::Ref(name) => {
|
||||
let name = self.context.map.ty(name);
|
||||
quote! { crate::types::#name }
|
||||
}
|
||||
IrType::Nullable(ty) => {
|
||||
let ty = CodegenRef::new(self.context, ty.as_ref());
|
||||
quote! { ::std::option::Option<#ty> }
|
||||
}
|
||||
IrType::Any => quote! { ::serde_json::Value },
|
||||
IrType::Inline(ty) => {
|
||||
let path = ty.path();
|
||||
let root: syn::Path = match &path.root {
|
||||
InlineIrTypePathRoot::Resource(a) => {
|
||||
let name = format_ident!("{}", a.to_snake_case());
|
||||
parse_quote!(crate::client::#name::types)
|
||||
}
|
||||
InlineIrTypePathRoot::Type(a) => {
|
||||
let m = CodegenIdent::Module(a);
|
||||
parse_quote!(crate::types::#m::fields)
|
||||
}
|
||||
};
|
||||
let name = CodegenTypeName::Inline(path);
|
||||
parse_quote!(#root::#name)
|
||||
}
|
||||
IrType::Schema(s) => {
|
||||
let name = self.context.map.ty(s.name());
|
||||
quote! { crate::types::#name }
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// A reference from one type to another type that may require boxing.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct CodegenBoxedRef<'a> {
|
||||
context: &'a CodegenContext<'a>,
|
||||
from: CodegenTypeName<'a>,
|
||||
to: &'a IrType<'a>,
|
||||
}
|
||||
|
||||
impl<'a> CodegenBoxedRef<'a> {
|
||||
pub fn new(context: &'a CodegenContext, from: CodegenTypeName<'a>, to: &'a IrType<'a>) -> Self {
|
||||
Self { context, from, to }
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for CodegenBoxedRef<'_> {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
if let CodegenTypeName::Schema(from, _) = &self.from
|
||||
&& let Some(this) = self.context.spec.lookup(from)
|
||||
&& let IrType::Ref(to) = &self.to
|
||||
&& let Some(other) = self.context.spec.lookup(to)
|
||||
&& this.requires_indirection_to(other)
|
||||
{
|
||||
let inner = CodegenRef::new(self.context, other.ty());
|
||||
tokens.append_all(quote! { ::std::boxed::Box<#inner> });
|
||||
} else {
|
||||
CodegenRef::new(self.context, self.to).to_tokens(tokens);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
use heck::ToSnakeCase;
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::{ToTokens, TokenStreamExt, quote};
|
||||
|
||||
use crate::codegen::IntoCode;
|
||||
use crate::ir::{InlineIrType, IrOperationView, IrParameter, IrRequest, IrResponse};
|
||||
|
||||
use super::{
|
||||
context::CodegenContext, enum_::CodegenEnum, naming::CodegenTypeName,
|
||||
operation::CodegenOperation, struct_::CodegenStruct, untagged::CodegenUntagged,
|
||||
};
|
||||
|
||||
/// Generates a feature-gated `impl Client` block for a resource,
|
||||
/// with all its operations.
|
||||
pub struct CodegenResource<'a> {
|
||||
context: &'a CodegenContext<'a>,
|
||||
resource: &'a str,
|
||||
operations: &'a [IrOperationView<'a>],
|
||||
}
|
||||
|
||||
impl<'a> CodegenResource<'a> {
|
||||
pub fn new(
|
||||
context: &'a CodegenContext<'a>,
|
||||
resource: &'a str,
|
||||
operations: &'a [IrOperationView<'a>],
|
||||
) -> Self {
|
||||
Self {
|
||||
context,
|
||||
resource,
|
||||
operations,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for CodegenResource<'_> {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
let feature_name = self.resource;
|
||||
let methods: Vec<TokenStream> = self
|
||||
.operations
|
||||
.iter()
|
||||
.map(|view| CodegenOperation::new(self.context, view.op()).into_token_stream())
|
||||
.collect();
|
||||
|
||||
let mut inlines = self
|
||||
.operations
|
||||
.iter()
|
||||
.flat_map(|op| {
|
||||
itertools::chain!(
|
||||
op.op()
|
||||
.params
|
||||
.iter()
|
||||
.filter_map(|param| match param {
|
||||
IrParameter::Path(_) => None,
|
||||
IrParameter::Query(info) => Some(info),
|
||||
})
|
||||
.flat_map(|info| info.ty.visit()),
|
||||
op.op()
|
||||
.request
|
||||
.iter()
|
||||
.filter_map(|request| match request {
|
||||
IrRequest::Json(ty) => Some(ty),
|
||||
IrRequest::Multipart => None,
|
||||
})
|
||||
.flat_map(|ty| ty.visit()),
|
||||
op.op()
|
||||
.response
|
||||
.iter()
|
||||
.map(|response| match response {
|
||||
IrResponse::Json(ty) => ty,
|
||||
})
|
||||
.flat_map(|ty| ty.visit()),
|
||||
)
|
||||
})
|
||||
.map(|ty: &InlineIrType<'_>| match ty {
|
||||
InlineIrType::Enum(path, ty) => {
|
||||
CodegenEnum::new(CodegenTypeName::Inline(path), ty).into_token_stream()
|
||||
}
|
||||
InlineIrType::Struct(path, ty) => {
|
||||
CodegenStruct::new(self.context, CodegenTypeName::Inline(path), ty)
|
||||
.into_token_stream()
|
||||
}
|
||||
InlineIrType::Untagged(path, ty) => {
|
||||
CodegenUntagged::new(self.context, CodegenTypeName::Inline(path), ty)
|
||||
.into_token_stream()
|
||||
}
|
||||
});
|
||||
let fields_module = inlines.next().map(|head| {
|
||||
quote! {
|
||||
pub mod types {
|
||||
#head
|
||||
#(#inlines)*
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
tokens.append_all(quote! {
|
||||
#[cfg(feature = #feature_name)]
|
||||
impl crate::client::Client {
|
||||
#(#methods)*
|
||||
}
|
||||
#fields_module
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoCode for CodegenResource<'_> {
|
||||
type Code = (String, TokenStream);
|
||||
|
||||
fn into_code(self) -> Self::Code {
|
||||
(
|
||||
format!("src/client/{}.rs", self.resource.to_snake_case()),
|
||||
self.into_token_stream(),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::{ToTokens, TokenStreamExt, quote};
|
||||
|
||||
use crate::{
|
||||
codegen::IntoCode,
|
||||
ir::{InlineIrType, IrType, SchemaIrType},
|
||||
};
|
||||
|
||||
use super::{
|
||||
context::CodegenContext, enum_::CodegenEnum, naming::CodegenTypeName, ref_::CodegenRef,
|
||||
struct_::CodegenStruct, tagged::CodegenTagged, untagged::CodegenUntagged,
|
||||
};
|
||||
|
||||
/// Generates a module for a named schema type.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct CodegenSchemaType<'a> {
|
||||
context: &'a CodegenContext<'a>,
|
||||
name: CodegenTypeName<'a>,
|
||||
ty: &'a SchemaIrType<'a>,
|
||||
}
|
||||
|
||||
impl<'a> CodegenSchemaType<'a> {
|
||||
pub fn new(
|
||||
context: &'a CodegenContext<'a>,
|
||||
name: CodegenTypeName<'a>,
|
||||
ty: &'a SchemaIrType<'a>,
|
||||
) -> Self {
|
||||
Self { context, name, ty }
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for CodegenSchemaType<'_> {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
let n = self.name;
|
||||
let code = match self.ty {
|
||||
SchemaIrType::Struct(_, ty) => {
|
||||
CodegenStruct::new(self.context, n, ty).into_token_stream()
|
||||
}
|
||||
SchemaIrType::Enum(_, ty) => CodegenEnum::new(n, ty).into_token_stream(),
|
||||
SchemaIrType::Tagged(_, ty) => {
|
||||
CodegenTagged::new(self.context, n, ty).into_token_stream()
|
||||
}
|
||||
SchemaIrType::Untagged(_, ty) => {
|
||||
CodegenUntagged::new(self.context, n, ty).into_token_stream()
|
||||
}
|
||||
};
|
||||
let mut inlines = self.ty.visit().map(|ty: &InlineIrType<'_>| match ty {
|
||||
InlineIrType::Enum(path, ty) => {
|
||||
CodegenEnum::new(CodegenTypeName::Inline(path), ty).into_token_stream()
|
||||
}
|
||||
InlineIrType::Struct(path, ty) => {
|
||||
CodegenStruct::new(self.context, CodegenTypeName::Inline(path), ty)
|
||||
.into_token_stream()
|
||||
}
|
||||
InlineIrType::Untagged(path, ty) => {
|
||||
CodegenUntagged::new(self.context, CodegenTypeName::Inline(path), ty)
|
||||
.into_token_stream()
|
||||
}
|
||||
});
|
||||
let fields_module = inlines.next().map(|head| {
|
||||
quote! {
|
||||
pub mod fields {
|
||||
#head
|
||||
#(#inlines)*
|
||||
}
|
||||
}
|
||||
});
|
||||
tokens.append_all(quote! {
|
||||
#code
|
||||
#fields_module
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoCode for CodegenSchemaType<'_> {
|
||||
type Code = (String, TokenStream);
|
||||
|
||||
fn into_code(self) -> Self::Code {
|
||||
let name = match self.name {
|
||||
CodegenTypeName::Schema(name, _) => {
|
||||
let info = &self.context.map.0[name];
|
||||
format!("src/types/{}.rs", info.module)
|
||||
}
|
||||
CodegenTypeName::Inline(..) => {
|
||||
unreachable!("inline types shouldn't be written to disk")
|
||||
}
|
||||
};
|
||||
(name, self.into_token_stream())
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates a module for a named schema type alias.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct CodegenSchemaTypeAlias<'a> {
|
||||
context: &'a CodegenContext<'a>,
|
||||
name: CodegenTypeName<'a>,
|
||||
ty: &'a IrType<'a>,
|
||||
}
|
||||
|
||||
impl<'a> CodegenSchemaTypeAlias<'a> {
|
||||
pub fn new(
|
||||
context: &'a CodegenContext<'a>,
|
||||
name: CodegenTypeName<'a>,
|
||||
ty: &'a IrType<'a>,
|
||||
) -> Self {
|
||||
Self { context, name, ty }
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for CodegenSchemaTypeAlias<'_> {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
let ty_name = self.name;
|
||||
let ty_v = CodegenRef::new(self.context, self.ty);
|
||||
tokens.append_all(quote! {
|
||||
pub type #ty_name = #ty_v;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoCode for CodegenSchemaTypeAlias<'_> {
|
||||
type Code = (String, TokenStream);
|
||||
|
||||
fn into_code(self) -> Self::Code {
|
||||
let name = match self.name {
|
||||
CodegenTypeName::Schema(name, _) => {
|
||||
let info = &self.context.map.0[name];
|
||||
format!("src/types/{}.rs", info.module)
|
||||
}
|
||||
CodegenTypeName::Inline(..) => {
|
||||
unreachable!("inline types shouldn't be written to disk")
|
||||
}
|
||||
};
|
||||
(name, self.into_token_stream())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,331 @@
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::{ToTokens, TokenStreamExt, format_ident, quote};
|
||||
|
||||
use crate::codegen::IntoCode;
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct CodegenLibrary;
|
||||
|
||||
impl ToTokens for CodegenLibrary {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
tokens.append_all(quote! {
|
||||
pub mod absent;
|
||||
pub mod date_time;
|
||||
pub mod types;
|
||||
pub mod client;
|
||||
pub mod error;
|
||||
|
||||
pub use client::Client;
|
||||
pub use error::Error;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoCode for CodegenLibrary {
|
||||
type Code = (&'static str, TokenStream);
|
||||
|
||||
fn into_code(self) -> Self::Code {
|
||||
("src/lib.rs", self.into_token_stream())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct CodegenDateTimeModule;
|
||||
|
||||
impl ToTokens for CodegenDateTimeModule {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
#[derive(Clone, Copy)]
|
||||
enum Conversion {
|
||||
TryFrom(&'static str),
|
||||
From(&'static str),
|
||||
}
|
||||
const TYPES: &[(&str, &str, Conversion)] = &[
|
||||
(
|
||||
"UnixMicroseconds",
|
||||
"chrono::serde::ts_microseconds",
|
||||
Conversion::TryFrom("from_timestamp_micros"),
|
||||
),
|
||||
(
|
||||
"UnixMilliseconds",
|
||||
"chrono::serde::ts_milliseconds",
|
||||
Conversion::TryFrom("from_timestamp_millis"),
|
||||
),
|
||||
(
|
||||
"UnixNanoseconds",
|
||||
"chrono::serde::ts_nanoseconds",
|
||||
Conversion::From("from_timestamp_nanos"),
|
||||
),
|
||||
(
|
||||
"UnixSeconds",
|
||||
"chrono::serde::ts_seconds",
|
||||
Conversion::TryFrom("from_timestamp_secs"),
|
||||
),
|
||||
];
|
||||
let types = TYPES.iter().map(|&(ty, module, conv)| {
|
||||
let ty = format_ident!("{ty}");
|
||||
let conv = match conv {
|
||||
Conversion::TryFrom(init) => {
|
||||
let init = format_ident!("{init}");
|
||||
quote! {
|
||||
impl TryFrom<i64> for #ty {
|
||||
type Error = TryFromTimestampError;
|
||||
|
||||
fn try_from(value: i64) -> Result<Self, Self::Error> {
|
||||
Ok(Self(DateTime::#init(value).ok_or(TryFromTimestampError)?))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Conversion::From(init) => {
|
||||
let init = format_ident!("{init}");
|
||||
quote! {
|
||||
impl From<i64> for #ty {
|
||||
fn from(value: i64) -> Self {
|
||||
Self(DateTime::#init(value))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
quote! {
|
||||
#[derive(
|
||||
Clone, Copy, Debug, Deserialize, Default, Eq, Hash, Ord,
|
||||
PartialEq, PartialOrd, Serialize,
|
||||
)]
|
||||
#[serde(transparent)]
|
||||
pub struct #ty(#[serde(with = #module)] DateTime<Utc>);
|
||||
|
||||
impl From<DateTime<Utc>> for #ty {
|
||||
fn from(value: DateTime<Utc>) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<#ty> for DateTime<Utc> {
|
||||
fn from(value: #ty) -> Self {
|
||||
value.0
|
||||
}
|
||||
}
|
||||
|
||||
#conv
|
||||
}
|
||||
});
|
||||
tokens.append_all(quote! {
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#(#types)*
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[error("timestamp out of range for `DateTime<Utc>`")]
|
||||
pub struct TryFromTimestampError;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoCode for CodegenDateTimeModule {
|
||||
type Code = (&'static str, TokenStream);
|
||||
|
||||
fn into_code(self) -> Self::Code {
|
||||
("src/date_time.rs", self.into_token_stream())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct CodegenAbsentModule;
|
||||
|
||||
impl ToTokens for CodegenAbsentModule {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
tokens.append_all(quote! {
|
||||
use std::marker::PhantomData;
|
||||
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
|
||||
/// An [`Option`]-like type that distinguishes between
|
||||
/// "value not present" and "value present but `null`".
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
|
||||
pub enum AbsentOr<T> {
|
||||
#[default]
|
||||
Absent,
|
||||
Null,
|
||||
Present(T),
|
||||
}
|
||||
|
||||
impl<T> AbsentOr<T> {
|
||||
#[inline]
|
||||
pub fn is_absent(&self) -> bool {
|
||||
matches!(self, Self::Absent)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn is_null(&self) -> bool {
|
||||
matches!(self, Self::Null)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn is_present(&self) -> bool {
|
||||
matches!(self, Self::Present(_))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn ok(self) -> Result<T, AbsentError> {
|
||||
match self {
|
||||
Self::Absent => Err(AbsentError::Absent),
|
||||
Self::Null => Err(AbsentError::Null),
|
||||
Self::Present(value) => Ok(value),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn as_ref(&self) -> AbsentOr<&T> {
|
||||
match self {
|
||||
Self::Absent => AbsentOr::Absent,
|
||||
Self::Null => AbsentOr::Null,
|
||||
Self::Present(value) => AbsentOr::Present(value),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn into_option(self) -> Option<T> {
|
||||
match self {
|
||||
Self::Absent | Self::Null => None,
|
||||
Self::Present(value) => Some(value),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<T> for AbsentOr<T> {
|
||||
fn from(value: T) -> Self {
|
||||
Self::Present(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Serialize> Serialize for AbsentOr<T> {
|
||||
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
||||
match self {
|
||||
Self::Absent | Self::Null => serializer.serialize_none(),
|
||||
Self::Present(value) => serializer.serialize_some(value),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de, T: Deserialize<'de>> Deserialize<'de> for AbsentOr<T> {
|
||||
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
|
||||
struct Visitor<T>(PhantomData<T>);
|
||||
impl<'de, T: Deserialize<'de>> serde::de::Visitor<'de> for Visitor<T> {
|
||||
type Value = AbsentOr<T>;
|
||||
|
||||
fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str("`null` or value")
|
||||
}
|
||||
|
||||
fn visit_unit<E: serde::de::Error>(self) -> Result<Self::Value, E> {
|
||||
Ok(AbsentOr::Null)
|
||||
}
|
||||
|
||||
fn visit_none<E: serde::de::Error>(self) -> Result<Self::Value, E> {
|
||||
Ok(AbsentOr::Null)
|
||||
}
|
||||
|
||||
fn visit_some<D: Deserializer<'de>>(
|
||||
self,
|
||||
deserializer: D,
|
||||
) -> Result<Self::Value, D::Error> {
|
||||
T::deserialize(deserializer).map(AbsentOr::Present)
|
||||
}
|
||||
}
|
||||
deserializer.deserialize_option(Visitor(PhantomData))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum AbsentError {
|
||||
#[error("value not present")]
|
||||
Absent,
|
||||
#[error("value is `null`")]
|
||||
Null,
|
||||
}
|
||||
|
||||
impl AbsentError {
|
||||
#[inline]
|
||||
pub fn field(self, name: &'static str) -> FieldAbsentError {
|
||||
match self {
|
||||
Self::Absent => FieldAbsentError::Absent(name),
|
||||
Self::Null => FieldAbsentError::Null(name),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum FieldAbsentError {
|
||||
#[error("field `{0}` not present")]
|
||||
Absent(&'static str),
|
||||
#[error("field `{0}` is `null`")]
|
||||
Null(&'static str),
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoCode for CodegenAbsentModule {
|
||||
type Code = (&'static str, TokenStream);
|
||||
|
||||
fn into_code(self) -> Self::Code {
|
||||
("src/absent.rs", self.into_token_stream())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct CodegenErrorModule;
|
||||
|
||||
impl ToTokens for CodegenErrorModule {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
tokens.append_all(quote! {
|
||||
/// Transport-level error types.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
/// Network or connection error.
|
||||
#[error("Network error")]
|
||||
Network(#[from] reqwest::Error),
|
||||
|
||||
/// Invalid JSON in request or response.
|
||||
#[error("Malformed JSON")]
|
||||
Json(#[from] JsonError),
|
||||
|
||||
/// Invalid URL.
|
||||
#[error("Malformed URL")]
|
||||
Url(#[from] url::ParseError),
|
||||
|
||||
/// URL can't be used as a base.
|
||||
#[error("Can't use URL as base URL")]
|
||||
UrlCannotBeABase,
|
||||
|
||||
/// Invalid HTTP header name.
|
||||
#[error("invalid header name")]
|
||||
BadHeaderName(#[source] http::Error),
|
||||
|
||||
/// Invalid HTTP header value.
|
||||
#[error("invalid value for header `{0}`")]
|
||||
BadHeaderValue(http::HeaderName, #[source] http::Error),
|
||||
}
|
||||
|
||||
/// Invalid or unexpected JSON, with or without a path
|
||||
/// to the unexpected section.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum JsonError {
|
||||
#[error(transparent)]
|
||||
Json(#[from] serde_json::Error),
|
||||
#[error(transparent)]
|
||||
JsonWithPath(#[from] serde_path_to_error::Error<serde_json::Error>),
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoCode for CodegenErrorModule {
|
||||
type Code = (&'static str, TokenStream);
|
||||
|
||||
fn into_code(self) -> Self::Code {
|
||||
("src/error.rs", self.into_token_stream())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::{ToTokens, TokenStreamExt, quote};
|
||||
use syn::{Ident, parse_quote};
|
||||
|
||||
use crate::{
|
||||
codegen::{rust::CodegenIdent, unique::UniqueNameSpace},
|
||||
ir::{IrStruct, IrType},
|
||||
};
|
||||
|
||||
use super::{
|
||||
context::CodegenContext, derives::ExtraDerive, doc_attrs, naming::CodegenTypeName,
|
||||
ref_::CodegenBoxedRef,
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct CodegenStruct<'a> {
|
||||
context: &'a CodegenContext<'a>,
|
||||
name: CodegenTypeName<'a>,
|
||||
ty: &'a IrStruct<'a>,
|
||||
}
|
||||
|
||||
impl<'a> CodegenStruct<'a> {
|
||||
pub fn new(
|
||||
context: &'a CodegenContext,
|
||||
name: CodegenTypeName<'a>,
|
||||
ty: &'a IrStruct<'a>,
|
||||
) -> Self {
|
||||
Self { context, name, ty }
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for CodegenStruct<'_> {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
let mut space = UniqueNameSpace::new();
|
||||
let mut all_optional = true;
|
||||
let fields = self
|
||||
.ty
|
||||
.fields
|
||||
.iter()
|
||||
.filter(|field| !field.discriminator)
|
||||
.map(|field| {
|
||||
let field_name = {
|
||||
let name = CodegenIdent::Field(&space.uniquify(field.name));
|
||||
parse_quote!(#name)
|
||||
};
|
||||
if field.required {
|
||||
all_optional = false;
|
||||
}
|
||||
|
||||
let final_type = match (&field.ty, field.required) {
|
||||
(IrType::Nullable(inner), true) => {
|
||||
let inner = CodegenBoxedRef::new(self.context, self.name, inner);
|
||||
quote! { ::std::option::Option<#inner> }
|
||||
}
|
||||
(IrType::Nullable(inner), false) => {
|
||||
let inner = CodegenBoxedRef::new(self.context, self.name, inner);
|
||||
quote! { crate::absent::AbsentOr<#inner> }
|
||||
}
|
||||
(other, true) => {
|
||||
CodegenBoxedRef::new(self.context, self.name, other).into_token_stream()
|
||||
}
|
||||
(other, false) => {
|
||||
let inner = CodegenBoxedRef::new(self.context, self.name, other);
|
||||
quote! { crate::absent::AbsentOr<#inner> }
|
||||
}
|
||||
};
|
||||
|
||||
let serde_attrs = field_serde_attrs(
|
||||
&field_name,
|
||||
field.name,
|
||||
field.required,
|
||||
matches!(field.ty, IrType::Nullable(_)),
|
||||
);
|
||||
|
||||
let doc_attrs = field.description.map(doc_attrs);
|
||||
|
||||
quote! {
|
||||
#doc_attrs
|
||||
#serde_attrs
|
||||
pub #field_name: #final_type,
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut extra_derives = vec![];
|
||||
let is_hashable = self
|
||||
.ty
|
||||
.fields
|
||||
.iter()
|
||||
.all(|variant| self.context.hashable(&variant.ty));
|
||||
if is_hashable {
|
||||
extra_derives.push(ExtraDerive::Eq);
|
||||
extra_derives.push(ExtraDerive::Hash);
|
||||
}
|
||||
if all_optional && !fields.is_empty() {
|
||||
extra_derives.push(ExtraDerive::Default);
|
||||
}
|
||||
|
||||
let type_name = &self.name;
|
||||
let doc_attrs = self.ty.description.map(doc_attrs);
|
||||
|
||||
tokens.append_all(quote! {
|
||||
#doc_attrs
|
||||
#[derive(Debug, Clone, PartialEq, #(#extra_derives,)* ::serde::Serialize, ::serde::Deserialize)]
|
||||
pub struct #type_name {
|
||||
#(#fields)*
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates `#[serde(...)]` attributes for a field.
|
||||
fn field_serde_attrs(ident: &Ident, name: &str, required: bool, nullable: bool) -> TokenStream {
|
||||
let mut attrs = Vec::new();
|
||||
|
||||
// `rename` if the field name doesn't match the identifier.
|
||||
let f = ident.to_string();
|
||||
if f.strip_prefix("r#").unwrap_or(&f) != name {
|
||||
attrs.push(quote! { rename = #name });
|
||||
}
|
||||
|
||||
match (required, nullable) {
|
||||
(false, true) | (false, false) => {
|
||||
attrs.push(quote! { default });
|
||||
attrs.push(quote! { skip_serializing_if = "crate::absent::AbsentOr::is_absent" });
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
if attrs.is_empty() {
|
||||
quote! {}
|
||||
} else {
|
||||
quote! { #[serde(#(#attrs,)*)] }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
use itertools::Itertools;
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::{ToTokens, TokenStreamExt, quote};
|
||||
|
||||
use crate::{
|
||||
codegen::{rust::CodegenIdent, unique::UniqueNameSpace},
|
||||
ir::IrTagged,
|
||||
};
|
||||
|
||||
use super::{
|
||||
context::CodegenContext, derives::ExtraDerive, doc_attrs, naming::CodegenTypeName,
|
||||
ref_::CodegenRef,
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct CodegenTagged<'a> {
|
||||
context: &'a CodegenContext<'a>,
|
||||
name: CodegenTypeName<'a>,
|
||||
ty: &'a IrTagged<'a>,
|
||||
}
|
||||
|
||||
impl<'a> CodegenTagged<'a> {
|
||||
pub fn new(
|
||||
context: &'a CodegenContext,
|
||||
name: CodegenTypeName<'a>,
|
||||
ty: &'a IrTagged<'a>,
|
||||
) -> Self {
|
||||
Self { context, name, ty }
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for CodegenTagged<'_> {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
let mut extra_derives = vec![];
|
||||
let is_hashable = self
|
||||
.ty
|
||||
.variants
|
||||
.iter()
|
||||
.all(|variant| self.context.hashable(&variant.ty));
|
||||
if is_hashable {
|
||||
extra_derives.push(ExtraDerive::Eq);
|
||||
extra_derives.push(ExtraDerive::Hash);
|
||||
}
|
||||
|
||||
let mut space = UniqueNameSpace::new();
|
||||
let variants = self
|
||||
.ty
|
||||
.variants
|
||||
.iter()
|
||||
.map(|variant| {
|
||||
// Look up the proper Rust type name.
|
||||
let variant_name = CodegenIdent::Variant(&space.uniquify(variant.name));
|
||||
let rust_type_name = CodegenRef::new(self.context, &variant.ty);
|
||||
|
||||
// Add `#[serde(alias = ...)]` attributes for multiple
|
||||
// discriminator values that map to the same type.
|
||||
let serde_attr = {
|
||||
let mut iter = variant.aliases.iter();
|
||||
match iter.next() {
|
||||
Some(&primary) => {
|
||||
let mut aliases = iter.copied().peekable();
|
||||
Some(if aliases.peek().is_none() {
|
||||
quote! { #[serde(rename = #primary)] }
|
||||
} else {
|
||||
quote! { #[serde(rename = #primary, #(alias = #aliases,)*)] }
|
||||
})
|
||||
}
|
||||
None => None,
|
||||
}
|
||||
};
|
||||
|
||||
let v = quote! {
|
||||
#serde_attr
|
||||
#variant_name(#rust_type_name),
|
||||
};
|
||||
|
||||
let type_name = &self.name;
|
||||
let from_impl = quote! {
|
||||
impl ::std::convert::From<#rust_type_name> for #type_name {
|
||||
fn from(value: #rust_type_name) -> Self {
|
||||
Self::#variant_name(value)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
(v, from_impl)
|
||||
})
|
||||
.collect_vec();
|
||||
|
||||
let discriminator_field_literal = &self.ty.tag;
|
||||
|
||||
let doc_attrs = self.ty.description.map(doc_attrs);
|
||||
|
||||
let vs = variants.iter().map(|(variant, _)| variant);
|
||||
let fs = variants.iter().map(|(_, from_impl)| from_impl);
|
||||
let type_name = &self.name;
|
||||
let main = quote! {
|
||||
#doc_attrs
|
||||
#[derive(Debug, Clone, PartialEq, #(#extra_derives,)* ::serde::Serialize, ::serde::Deserialize)]
|
||||
#[serde(tag = #discriminator_field_literal)]
|
||||
pub enum #type_name {
|
||||
#(#vs)*
|
||||
}
|
||||
|
||||
#(#fs)*
|
||||
};
|
||||
|
||||
tokens.append_all(main);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::{ToTokens, TokenStreamExt, quote};
|
||||
|
||||
use crate::codegen::IntoCode;
|
||||
|
||||
use super::context::CodegenContext;
|
||||
|
||||
/// Generates the `types/mod.rs` module.
|
||||
pub struct CodegenTypesModule<'a> {
|
||||
context: &'a CodegenContext<'a>,
|
||||
resources_by_type: BTreeMap<&'a str, BTreeSet<&'a str>>,
|
||||
}
|
||||
|
||||
impl<'a> CodegenTypesModule<'a> {
|
||||
pub fn new(context: &'a CodegenContext<'a>) -> Self {
|
||||
let mut resources_by_type = BTreeMap::<&str, BTreeSet<&str>>::new();
|
||||
for view in context.spec.operations() {
|
||||
let resource = view.op().resource;
|
||||
for v in view.refs() {
|
||||
resources_by_type
|
||||
.entry(v.name())
|
||||
.or_default()
|
||||
.insert(resource);
|
||||
}
|
||||
}
|
||||
|
||||
Self {
|
||||
context,
|
||||
resources_by_type,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for CodegenTypesModule<'_> {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
let mut mods = Vec::new();
|
||||
let mut uses = Vec::new();
|
||||
|
||||
for (name, info) in self.context.map.iter() {
|
||||
let Some(resources) = self.resources_by_type.get(name) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let cfg_attr = cfg_attr(resources);
|
||||
|
||||
let module = &info.module;
|
||||
mods.push(quote! {
|
||||
#cfg_attr
|
||||
pub mod #module;
|
||||
});
|
||||
|
||||
let ty = &info.ty;
|
||||
uses.push(quote! {
|
||||
#cfg_attr
|
||||
pub use #module::#ty;
|
||||
});
|
||||
}
|
||||
|
||||
tokens.append_all(quote! {
|
||||
#(#mods)*
|
||||
|
||||
#(#uses)*
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoCode for CodegenTypesModule<'_> {
|
||||
type Code = (&'static str, TokenStream);
|
||||
|
||||
fn into_code(self) -> Self::Code {
|
||||
("src/types/mod.rs", self.into_token_stream())
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates a `#[cfg(feature = "...")]` or `#[cfg(any(feature = "...", ...))]`
|
||||
/// attribute for the given resources.
|
||||
fn cfg_attr(resources: &BTreeSet<&str>) -> Option<TokenStream> {
|
||||
let mut features = resources.iter().peekable();
|
||||
let first = features.next()?;
|
||||
Some(match features.next() {
|
||||
Some(next) => {
|
||||
let rest = features.map(|f| quote! { feature = #f });
|
||||
quote! { #[cfg(any(feature = #first, feature = #next, #(#rest),*))] }
|
||||
}
|
||||
None => quote! { #[cfg(feature = #first)] },
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::{ToTokens, TokenStreamExt, quote};
|
||||
|
||||
use crate::ir::{IrUntagged, IrUntaggedVariant};
|
||||
|
||||
use super::{
|
||||
context::CodegenContext,
|
||||
derives::ExtraDerive,
|
||||
doc_attrs,
|
||||
naming::{CodegenTypeName, CodegenUntaggedVariantName},
|
||||
ref_::CodegenRef,
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct CodegenUntagged<'a> {
|
||||
context: &'a CodegenContext<'a>,
|
||||
name: CodegenTypeName<'a>,
|
||||
ty: &'a IrUntagged<'a>,
|
||||
}
|
||||
|
||||
impl<'a> CodegenUntagged<'a> {
|
||||
pub fn new(
|
||||
context: &'a CodegenContext,
|
||||
name: CodegenTypeName<'a>,
|
||||
ty: &'a IrUntagged<'a>,
|
||||
) -> Self {
|
||||
Self { context, name, ty }
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for CodegenUntagged<'_> {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
let mut variants = Vec::new();
|
||||
|
||||
for variant in &self.ty.variants {
|
||||
match variant {
|
||||
IrUntaggedVariant::Some(name, ty) => {
|
||||
let variant_name = CodegenUntaggedVariantName(*name);
|
||||
let rust_type = CodegenRef::new(self.context, ty);
|
||||
variants.push(quote! { #variant_name(#rust_type) });
|
||||
}
|
||||
IrUntaggedVariant::Null => variants.push(quote! { None }),
|
||||
}
|
||||
}
|
||||
|
||||
let type_name_ident = &self.name;
|
||||
let doc_attrs = self.ty.description.map(doc_attrs);
|
||||
|
||||
let mut extra_derives = vec![];
|
||||
let is_hashable = self.ty.variants.iter().all(|variant| match variant {
|
||||
IrUntaggedVariant::Some(_, ty) => self.context.hashable(ty),
|
||||
IrUntaggedVariant::Null => true,
|
||||
});
|
||||
if is_hashable {
|
||||
extra_derives.push(ExtraDerive::Eq);
|
||||
extra_derives.push(ExtraDerive::Hash);
|
||||
}
|
||||
|
||||
tokens.append_all(quote! {
|
||||
#doc_attrs
|
||||
#[derive(Debug, Clone, PartialEq, #(#extra_derives,)* ::serde::Serialize, ::serde::Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum #type_name_ident {
|
||||
#(#variants),*
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,322 @@
|
||||
use std::borrow::Cow;
|
||||
use std::collections::btree_map::Entry;
|
||||
use std::str::CharIndices;
|
||||
use std::{collections::BTreeMap, iter::Peekable};
|
||||
|
||||
use unicase::UniCase;
|
||||
|
||||
/// Produces names that will never collide with other names in this space,
|
||||
/// even when converted to a different case.
|
||||
///
|
||||
/// [`UniqueNameSpace`] exists to disambiguate type and field names
|
||||
/// that are distinct in the source spec, but collide when transformed
|
||||
/// to a different case. (For example, both `HTTP_Response` and `HTTPResponse`
|
||||
/// become `http_response` in snake case).
|
||||
#[derive(Debug, Default)]
|
||||
pub struct UniqueNameSpace<'a>(BTreeMap<Box<[UniCase<&'a str>]>, usize>);
|
||||
|
||||
impl<'a> UniqueNameSpace<'a> {
|
||||
#[inline]
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Returns a unique name, ignoring case and case transformations.
|
||||
/// The unique name preserves the case of the original name, but adds
|
||||
/// a numeric suffix on collisions.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use ploidy::codegen::unique::UniqueNameSpace;
|
||||
/// # let mut space = UniqueNameSpace::new();
|
||||
/// assert_eq!(space.uniquify("HTTPResponse"), "HTTPResponse");
|
||||
/// assert_eq!(space.uniquify("HTTP_Response"), "HTTP_Response2");
|
||||
/// assert_eq!(space.uniquify("httpResponse"), "httpResponse3");
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn uniquify(&mut self, name: &'a str) -> Cow<'a, str> {
|
||||
match self
|
||||
.0
|
||||
.entry(WordSegments::new(name).map(UniCase::new).collect())
|
||||
{
|
||||
Entry::Occupied(mut entry) => {
|
||||
let count = entry.get_mut();
|
||||
*count += 1;
|
||||
format!("{name}{count}").into()
|
||||
}
|
||||
Entry::Vacant(entry) => {
|
||||
entry.insert(1);
|
||||
name.into()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Segments a string into words, following Heck's notion of word boundaries.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use itertools::Itertools;
|
||||
/// # use ploidy::codegen::unique::WordSegments;
|
||||
/// assert_eq!(WordSegments::new("HTTPResponse").collect_vec(), vec!["HTTP", "Response"]);
|
||||
/// assert_eq!(WordSegments::new("HTTP_Response").collect_vec(), vec!["HTTP", "Response"]);
|
||||
/// assert_eq!(WordSegments::new("httpResponse").collect_vec(), vec!["http", "Response"]);
|
||||
/// assert_eq!(WordSegments::new("XMLHttpRequest").collect_vec(), vec!["XML", "Http", "Request"]);
|
||||
pub struct WordSegments<'a> {
|
||||
input: &'a str,
|
||||
chars: Peekable<CharIndices<'a>>,
|
||||
current_word_starts_at: Option<usize>,
|
||||
mode: WordMode,
|
||||
}
|
||||
|
||||
impl<'a> WordSegments<'a> {
|
||||
#[inline]
|
||||
pub fn new(input: &'a str) -> Self {
|
||||
Self {
|
||||
input,
|
||||
chars: input.char_indices().peekable(),
|
||||
current_word_starts_at: None,
|
||||
mode: WordMode::Boundary,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for WordSegments<'a> {
|
||||
type Item = &'a str;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
while let Some((index, c)) = self.chars.next() {
|
||||
if c.is_uppercase() {
|
||||
match self.mode {
|
||||
WordMode::Boundary => {
|
||||
// Start a new word with this uppercase character.
|
||||
self.current_word_starts_at = Some(index);
|
||||
self.mode = WordMode::Uppercase;
|
||||
}
|
||||
WordMode::Lowercase => {
|
||||
// camelCased word (previous was lowercase;
|
||||
// current is uppercase), start a new word.
|
||||
let start = self.current_word_starts_at.replace(index);
|
||||
self.mode = WordMode::Uppercase;
|
||||
if let Some(start) = start {
|
||||
return Some(&self.input[start..index]);
|
||||
}
|
||||
}
|
||||
WordMode::Uppercase => {
|
||||
let next_is_lowercase = self
|
||||
.chars
|
||||
.peek()
|
||||
.map(|&(_, next)| next.is_lowercase())
|
||||
.unwrap_or(false);
|
||||
if next_is_lowercase && let Some(start) = self.current_word_starts_at {
|
||||
// `XMLHttp` case; start a new word with this uppercase
|
||||
// character (the "H" in "Http").
|
||||
self.current_word_starts_at = Some(index);
|
||||
return Some(&self.input[start..index]);
|
||||
}
|
||||
// (Stay in uppercase mode).
|
||||
}
|
||||
}
|
||||
} else if c.is_lowercase() {
|
||||
if self.current_word_starts_at.is_none() {
|
||||
// Start a new word with this lowercase character
|
||||
// (the "c" in "camelCase").
|
||||
self.current_word_starts_at = Some(index);
|
||||
}
|
||||
self.mode = WordMode::Lowercase;
|
||||
} else if !c.is_alphanumeric() {
|
||||
// Start a new word at this non-alphanumeric character.
|
||||
let start = std::mem::take(&mut self.current_word_starts_at);
|
||||
self.mode = WordMode::Boundary;
|
||||
if let Some(start) = start {
|
||||
return Some(&self.input[start..index]);
|
||||
}
|
||||
} else {
|
||||
// Digit or other character: continue the current word.
|
||||
if self.current_word_starts_at.is_none() {
|
||||
self.current_word_starts_at = Some(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(start) = std::mem::take(&mut self.current_word_starts_at) {
|
||||
// Trailing word.
|
||||
return Some(&self.input[start..]);
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// The current state of a [`WordSegments`] iterator.
|
||||
#[derive(Clone, Copy)]
|
||||
enum WordMode {
|
||||
/// At a word boundary: either at the start of a new word, or
|
||||
/// after a non-alphanumeric character.
|
||||
Boundary,
|
||||
/// Currently in a lowercase segment.
|
||||
Lowercase,
|
||||
/// Currently in an uppercase segment.
|
||||
Uppercase,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use itertools::Itertools;
|
||||
|
||||
#[test]
|
||||
fn test_segment_camel_case() {
|
||||
assert_eq!(
|
||||
WordSegments::new("camelCase").collect_vec(),
|
||||
vec!["camel", "Case"]
|
||||
);
|
||||
assert_eq!(
|
||||
WordSegments::new("httpResponse").collect_vec(),
|
||||
vec!["http", "Response"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_segment_pascal_case() {
|
||||
assert_eq!(
|
||||
WordSegments::new("PascalCase").collect_vec(),
|
||||
vec!["Pascal", "Case"]
|
||||
);
|
||||
assert_eq!(
|
||||
WordSegments::new("HttpResponse").collect_vec(),
|
||||
vec!["Http", "Response"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_segment_snake_case() {
|
||||
assert_eq!(
|
||||
WordSegments::new("snake_case").collect_vec(),
|
||||
vec!["snake", "case"]
|
||||
);
|
||||
assert_eq!(
|
||||
WordSegments::new("http_response").collect_vec(),
|
||||
vec!["http", "response"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_segment_screaming_snake() {
|
||||
assert_eq!(
|
||||
WordSegments::new("SCREAMING_SNAKE").collect_vec(),
|
||||
vec!["SCREAMING", "SNAKE"]
|
||||
);
|
||||
assert_eq!(
|
||||
WordSegments::new("HTTP_RESPONSE").collect_vec(),
|
||||
vec!["HTTP", "RESPONSE"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_segment_consecutive_uppercase() {
|
||||
assert_eq!(
|
||||
WordSegments::new("XMLHttpRequest").collect_vec(),
|
||||
vec!["XML", "Http", "Request"]
|
||||
);
|
||||
assert_eq!(
|
||||
WordSegments::new("HTTPResponse").collect_vec(),
|
||||
vec!["HTTP", "Response"]
|
||||
);
|
||||
assert_eq!(
|
||||
WordSegments::new("HTTP_Response").collect_vec(),
|
||||
vec!["HTTP", "Response"]
|
||||
);
|
||||
assert_eq!(WordSegments::new("ALLCAPS").collect_vec(), vec!["ALLCAPS"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_segment_with_numbers() {
|
||||
assert_eq!(
|
||||
WordSegments::new("Response2").collect_vec(),
|
||||
vec!["Response2"]
|
||||
);
|
||||
assert_eq!(
|
||||
WordSegments::new("response_2").collect_vec(),
|
||||
vec!["response", "2"]
|
||||
);
|
||||
assert_eq!(
|
||||
WordSegments::new("HTTP2Protocol").collect_vec(),
|
||||
vec!["HTTP2", "Protocol"]
|
||||
);
|
||||
assert_eq!(
|
||||
WordSegments::new("OAuth2Token").collect_vec(),
|
||||
vec!["O", "Auth2", "Token"]
|
||||
);
|
||||
assert_eq!(
|
||||
WordSegments::new("HTTP2XML").collect_vec(),
|
||||
vec!["HTTP2XML"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_segment_empty_and_special() {
|
||||
assert!(WordSegments::new("").collect_vec().is_empty());
|
||||
assert!(WordSegments::new("___").collect_vec().is_empty());
|
||||
assert_eq!(WordSegments::new("a").collect_vec(), vec!["a"]);
|
||||
assert_eq!(WordSegments::new("A").collect_vec(), vec!["A"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_segment_mixed_separators() {
|
||||
assert_eq!(
|
||||
WordSegments::new("foo-bar_baz").collect_vec(),
|
||||
vec!["foo", "bar", "baz"]
|
||||
);
|
||||
assert_eq!(
|
||||
WordSegments::new("foo--bar").collect_vec(),
|
||||
vec!["foo", "bar"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deduplication_http_response_collision() {
|
||||
let mut space = UniqueNameSpace::new();
|
||||
|
||||
assert_eq!(space.uniquify("HTTPResponse"), "HTTPResponse");
|
||||
assert_eq!(space.uniquify("HTTP_Response"), "HTTP_Response2");
|
||||
assert_eq!(space.uniquify("httpResponse"), "httpResponse3");
|
||||
assert_eq!(space.uniquify("http_response"), "http_response4");
|
||||
// `HTTPRESPONSE` isn't a collision; it's a single word.
|
||||
assert_eq!(space.uniquify("HTTPRESPONSE"), "HTTPRESPONSE");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deduplication_xml_http_request() {
|
||||
let mut space = UniqueNameSpace::new();
|
||||
|
||||
assert_eq!(space.uniquify("XMLHttpRequest"), "XMLHttpRequest");
|
||||
assert_eq!(space.uniquify("xml_http_request"), "xml_http_request2");
|
||||
assert_eq!(space.uniquify("XmlHttpRequest"), "XmlHttpRequest3");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deduplication_preserves_original_casing() {
|
||||
let mut space = UniqueNameSpace::new();
|
||||
|
||||
assert_eq!(space.uniquify("HTTP_Response"), "HTTP_Response");
|
||||
assert_eq!(space.uniquify("httpResponse"), "httpResponse2");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deduplication_same_prefix() {
|
||||
let mut dedup = UniqueNameSpace::new();
|
||||
|
||||
assert_eq!(dedup.uniquify("HttpRequest"), "HttpRequest");
|
||||
assert_eq!(dedup.uniquify("HttpResponse"), "HttpResponse");
|
||||
assert_eq!(dedup.uniquify("HttpError"), "HttpError");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deduplication_with_numbers() {
|
||||
let mut space = UniqueNameSpace::new();
|
||||
|
||||
assert_eq!(space.uniquify("Response2"), "Response2");
|
||||
assert_eq!(space.uniquify("response_2"), "response_2");
|
||||
}
|
||||
}
|
||||
+172
@@ -0,0 +1,172 @@
|
||||
use std::{io::ErrorKind as IoErrorKind, path::PathBuf};
|
||||
|
||||
use clap::{
|
||||
CommandFactory, FromArgMatches,
|
||||
error::{ErrorKind as ClapErrorKind, Result as ClapResult},
|
||||
};
|
||||
use semver::Version;
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Debug, clap::Parser)]
|
||||
#[command(version, about, long_about = None)]
|
||||
pub struct Main {
|
||||
/// Enable verbose logging.
|
||||
#[arg(short, long)]
|
||||
pub verbose: bool,
|
||||
|
||||
#[command(subcommand)]
|
||||
pub command: Command,
|
||||
}
|
||||
|
||||
impl Main {
|
||||
pub fn parse() -> ClapResult<Main> {
|
||||
let mut cmd = Self::command();
|
||||
let mut matches = cmd
|
||||
.try_get_matches_from_mut(std::env::args_os())
|
||||
.map_err(|err| err.format(&mut cmd))?;
|
||||
let args = Self::from_arg_matches_mut(&mut matches).map_err(|err| err.format(&mut cmd))?;
|
||||
let command = match args.command {
|
||||
Command::Codegen(Codegen {
|
||||
input,
|
||||
output,
|
||||
language,
|
||||
}) => {
|
||||
let file: Option<ConfigFileSections> = {
|
||||
let path = output.join(".ploidy.toml");
|
||||
match std::fs::read_to_string(&path) {
|
||||
Ok(contents) => Some(toml::from_str(&contents).map_err(|err| {
|
||||
cmd.error(
|
||||
ClapErrorKind::ValueValidation,
|
||||
format!("Failed to parse `{}`: {err}", path.display()),
|
||||
)
|
||||
})?),
|
||||
Err(err) if err.kind() == IoErrorKind::NotFound => None,
|
||||
Err(err) => {
|
||||
return Err(cmd.error(
|
||||
ClapErrorKind::Io,
|
||||
format!("Failed to read `{}`: {err}", path.display()),
|
||||
));
|
||||
}
|
||||
}
|
||||
};
|
||||
match language {
|
||||
CodegenLanguage::Rust(rust) => {
|
||||
let package = match file {
|
||||
Some(ConfigFileSections {
|
||||
rust:
|
||||
Some(RustConfigFileSection {
|
||||
package: Some(other),
|
||||
}),
|
||||
}) => rust.package.merge(other),
|
||||
_ => rust.package,
|
||||
};
|
||||
Command::Codegen(Codegen {
|
||||
input,
|
||||
output,
|
||||
language: CodegenLanguage::Rust(CodegenRust { package, ..rust }),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
Ok(Main {
|
||||
verbose: args.verbose,
|
||||
command,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, clap::Subcommand)]
|
||||
pub enum Command {
|
||||
/// Generate a client from an OpenAPI document.
|
||||
Codegen(Codegen),
|
||||
}
|
||||
|
||||
#[derive(Debug, clap::Args)]
|
||||
pub struct Codegen {
|
||||
/// The path to the OpenAPI document (`.yaml` or `.json`).
|
||||
pub input: PathBuf,
|
||||
|
||||
/// The output directory for the generated files.
|
||||
pub output: PathBuf,
|
||||
|
||||
#[command(subcommand)]
|
||||
pub language: CodegenLanguage,
|
||||
}
|
||||
|
||||
#[derive(Debug, clap::Subcommand)]
|
||||
pub enum CodegenLanguage {
|
||||
/// Generate a Rust package.
|
||||
Rust(CodegenRust),
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, clap::Args)]
|
||||
pub struct CodegenRust {
|
||||
/// Run `cargo check` on the generated code.
|
||||
#[arg(short, long)]
|
||||
pub check: bool,
|
||||
|
||||
#[command(flatten)]
|
||||
pub package: RustPackageFragment,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Deserialize, clap::Args)]
|
||||
#[command(next_help_heading = "Generated package options")]
|
||||
pub struct RustPackageFragment {
|
||||
/// The generated package name. Defaults to the output directory name
|
||||
/// if not set.
|
||||
#[arg(name = "package-name", long)]
|
||||
#[serde(default)]
|
||||
pub name: Option<String>,
|
||||
|
||||
/// The generated package version. Defaults to `0.0.0` if not set.
|
||||
#[arg(name = "package-version", long)]
|
||||
#[serde(default)]
|
||||
pub version: Option<Version>,
|
||||
|
||||
/// The generated package description.
|
||||
#[arg(name = "package-description", long)]
|
||||
#[serde(default)]
|
||||
pub description: Option<String>,
|
||||
|
||||
/// The generated package license. Defaults to `UNLICENSED` if not set.
|
||||
#[arg(name = "package-license", long)]
|
||||
#[serde(default)]
|
||||
pub license: Option<String>,
|
||||
}
|
||||
|
||||
impl RustPackageFragment {
|
||||
#[inline]
|
||||
pub fn merge(self, other: Self) -> Self {
|
||||
Self {
|
||||
name: self.name.or(other.name),
|
||||
version: self.version.or(other.version),
|
||||
description: self.description.or(other.description),
|
||||
license: self.license.or(other.license),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn version_or_default(&self) -> Version {
|
||||
self.version
|
||||
.clone()
|
||||
.unwrap_or_else(|| Version::new(0, 0, 0))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn license_or_default(&self) -> &str {
|
||||
self.license.as_deref().unwrap_or("UNLICENSED")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct ConfigFileSections {
|
||||
#[serde(default)]
|
||||
pub rust: Option<RustConfigFileSection>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct RustConfigFileSection {
|
||||
#[serde(default)]
|
||||
pub package: Option<RustPackageFragment>,
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum SerdeError {
|
||||
#[error(transparent)]
|
||||
Json(#[from] serde_json::Error),
|
||||
#[error(transparent)]
|
||||
JsonWithPath(#[from] serde_path_to_error::Error<serde_json::Error>),
|
||||
#[error(transparent)]
|
||||
Yaml(#[from] serde_yaml::Error),
|
||||
#[error(transparent)]
|
||||
YamlWithPath(#[from] serde_path_to_error::Error<serde_yaml::Error>),
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
use crate::parse::path::BadPath;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum IrError {
|
||||
#[error("can't generate code for an operation without an ID")]
|
||||
NoOperationId,
|
||||
#[error("operation has invalid path")]
|
||||
BadOperationPath(#[from] BadPath),
|
||||
}
|
||||
@@ -0,0 +1,328 @@
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use indexmap::IndexMap;
|
||||
|
||||
use crate::parse::{Document, RefOrSchema, Schema, SchemaRefPath};
|
||||
|
||||
/// Yields all the fields of this schema, including inherited fields.
|
||||
pub fn all_fields<'a>(
|
||||
doc: &'a Document,
|
||||
schema: &'a Schema,
|
||||
) -> impl Iterator<Item = (&'a str, IrSchemaField<'a>)> {
|
||||
let ancestors = Vec::from_iter(Ancestors::new(doc, schema));
|
||||
|
||||
let discriminators: BTreeSet<_> = ancestors
|
||||
.iter()
|
||||
.filter_map(|ancestor| ancestor.discriminator.as_ref())
|
||||
.map(|d| d.property_name.as_str())
|
||||
.collect();
|
||||
|
||||
let own: IndexMap<_, _> = schema
|
||||
.properties
|
||||
.iter()
|
||||
.flatten()
|
||||
.map(|(name, property)| {
|
||||
(
|
||||
name.as_str(),
|
||||
IrSchemaField::Own(IrSchemaFieldInfo {
|
||||
schema: property,
|
||||
required: schema.required.contains(name),
|
||||
discriminator: discriminators.contains(name.as_str()),
|
||||
}),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut inherited = IndexMap::new();
|
||||
for ancestor in ancestors {
|
||||
let properties = ancestor
|
||||
.properties
|
||||
.iter()
|
||||
.flatten()
|
||||
.filter(|(name, _)| !own.contains_key(name.as_str()));
|
||||
|
||||
for (name, property) in properties {
|
||||
inherited.entry(name.as_str()).or_insert_with(|| {
|
||||
IrSchemaField::Inherited(IrSchemaFieldInfo {
|
||||
schema: property,
|
||||
required: ancestor.required.contains(name),
|
||||
discriminator: discriminators.contains(name.as_str()),
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
itertools::chain!(inherited, own)
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum IrSchemaField<'a> {
|
||||
Inherited(IrSchemaFieldInfo<'a>),
|
||||
Own(IrSchemaFieldInfo<'a>),
|
||||
}
|
||||
|
||||
impl<'a> IrSchemaField<'a> {
|
||||
#[inline]
|
||||
pub fn info(self) -> IrSchemaFieldInfo<'a> {
|
||||
let (Self::Inherited(info) | Self::Own(info)) = self;
|
||||
info
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct IrSchemaFieldInfo<'a> {
|
||||
pub schema: &'a RefOrSchema,
|
||||
pub required: bool,
|
||||
pub discriminator: bool,
|
||||
}
|
||||
|
||||
/// Yields all ancestors of a schema in linear order.
|
||||
pub struct Ancestors<'a> {
|
||||
doc: &'a Document,
|
||||
stack: Vec<&'a RefOrSchema>,
|
||||
visited: BTreeSet<&'a SchemaRefPath>,
|
||||
}
|
||||
|
||||
impl<'a> Ancestors<'a> {
|
||||
#[inline]
|
||||
pub fn new(doc: &'a Document, schema: &'a Schema) -> Self {
|
||||
let stack = match &schema.all_of {
|
||||
// Push parents in reverse, so that the iterator will pop and
|
||||
// visit them in left-to-right order.
|
||||
Some(all_of) => all_of.iter().rev().collect(),
|
||||
None => vec![],
|
||||
};
|
||||
Self {
|
||||
doc,
|
||||
stack,
|
||||
visited: BTreeSet::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for Ancestors<'a> {
|
||||
type Item = &'a Schema;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
while let Some(item) = self.stack.pop() {
|
||||
let schema = match item {
|
||||
RefOrSchema::Ref(r) => {
|
||||
if !self.visited.insert(&r.path) {
|
||||
// Skip cycles.
|
||||
continue;
|
||||
}
|
||||
let Some(schema) = self
|
||||
.doc
|
||||
.components
|
||||
.as_ref()
|
||||
.and_then(|c| c.schemas.get(r.path.as_str()))
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
schema
|
||||
}
|
||||
RefOrSchema::Schema(schema) => schema.as_ref(),
|
||||
};
|
||||
if let Some(all_of) = &schema.all_of {
|
||||
self.stack.extend(all_of.iter().rev());
|
||||
}
|
||||
return Some(schema);
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::parse::{Components, RefOrSchema, Schema, Ty};
|
||||
use indexmap::IndexMap;
|
||||
use itertools::Itertools;
|
||||
|
||||
fn make_string_field() -> RefOrSchema {
|
||||
RefOrSchema::Schema(Box::new(Schema {
|
||||
ty: vec![Ty::String],
|
||||
..Default::default()
|
||||
}))
|
||||
}
|
||||
|
||||
fn make_int_field() -> RefOrSchema {
|
||||
RefOrSchema::Schema(Box::new(Schema {
|
||||
ty: vec![Ty::Integer],
|
||||
..Default::default()
|
||||
}))
|
||||
}
|
||||
|
||||
fn make_doc(schemas: IndexMap<String, Schema>) -> Document {
|
||||
Document {
|
||||
openapi: "3.0.0".to_string(),
|
||||
info: crate::parse::Info {
|
||||
title: "Test".to_string(),
|
||||
version: "1.0".to_string(),
|
||||
description: None,
|
||||
},
|
||||
paths: IndexMap::new(),
|
||||
components: Some(Components { schemas }),
|
||||
}
|
||||
}
|
||||
|
||||
fn make_ref(name: &str) -> crate::parse::Ref {
|
||||
crate::parse::Ref {
|
||||
path: format!("#/components/schemas/{name}").parse().unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multi_level_inheritance() {
|
||||
// Entity -> NamedEntity -> User chain
|
||||
let mut schemas = IndexMap::new();
|
||||
|
||||
let mut entity_props = IndexMap::new();
|
||||
entity_props.insert("id".to_string(), make_string_field());
|
||||
schemas.insert(
|
||||
"Entity".to_string(),
|
||||
Schema {
|
||||
properties: Some(entity_props),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
let mut named_entity_props = IndexMap::new();
|
||||
named_entity_props.insert("name".to_string(), make_string_field());
|
||||
schemas.insert(
|
||||
"NamedEntity".to_string(),
|
||||
Schema {
|
||||
all_of: Some(vec![RefOrSchema::Ref(make_ref("Entity"))]),
|
||||
properties: Some(named_entity_props),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
let mut user_props = IndexMap::new();
|
||||
user_props.insert("email".to_string(), make_string_field());
|
||||
schemas.insert(
|
||||
"User".to_string(),
|
||||
Schema {
|
||||
all_of: Some(vec![RefOrSchema::Ref(make_ref("NamedEntity"))]),
|
||||
properties: Some(user_props),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
let doc = make_doc(schemas);
|
||||
let user_schema = doc
|
||||
.components
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.schemas
|
||||
.get("User")
|
||||
.unwrap();
|
||||
let all_fields = all_fields(&doc, user_schema).collect_vec();
|
||||
|
||||
// Should have 3 fields total: id (inherited), name (inherited), email (own)
|
||||
assert_eq!(all_fields.len(), 3);
|
||||
|
||||
// Check inherited fields
|
||||
let inherited: Vec<_> = all_fields
|
||||
.iter()
|
||||
.filter_map(|(name, f)| match f {
|
||||
IrSchemaField::Inherited(info) => Some((name, info)),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(inherited.len(), 2);
|
||||
let inherited_names: Vec<&str> = inherited.iter().map(|(n, _)| **n).collect();
|
||||
assert!(inherited_names.contains(&"id"));
|
||||
assert!(inherited_names.contains(&"name"));
|
||||
|
||||
// Check own fields
|
||||
let own: Vec<_> = all_fields
|
||||
.iter()
|
||||
.filter_map(|(name, f)| match f {
|
||||
IrSchemaField::Own(info) => Some((name, info)),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(own.len(), 1);
|
||||
assert_eq!(*own[0].0, "email");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_diamond_inheritance_no_duplicate() {
|
||||
// Product -> [NamedEntity, Entity], NamedEntity -> Entity
|
||||
// Entity should only appear once
|
||||
let mut schemas = IndexMap::new();
|
||||
|
||||
let mut entity_props = IndexMap::new();
|
||||
entity_props.insert("id".to_string(), make_string_field());
|
||||
schemas.insert(
|
||||
"Entity".to_string(),
|
||||
Schema {
|
||||
properties: Some(entity_props),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
let mut named_entity_props = IndexMap::new();
|
||||
named_entity_props.insert("name".to_string(), make_string_field());
|
||||
schemas.insert(
|
||||
"NamedEntity".to_string(),
|
||||
Schema {
|
||||
all_of: Some(vec![RefOrSchema::Ref(make_ref("Entity"))]),
|
||||
properties: Some(named_entity_props),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
let mut product_props = IndexMap::new();
|
||||
product_props.insert("price".to_string(), make_int_field());
|
||||
schemas.insert(
|
||||
"Product".to_string(),
|
||||
Schema {
|
||||
all_of: Some(vec![
|
||||
RefOrSchema::Ref(make_ref("NamedEntity")),
|
||||
RefOrSchema::Ref(make_ref("Entity")),
|
||||
]),
|
||||
properties: Some(product_props),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
let doc = make_doc(schemas);
|
||||
let product = doc
|
||||
.components
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.schemas
|
||||
.get("Product")
|
||||
.unwrap();
|
||||
let all_fields = all_fields(&doc, product).collect_vec();
|
||||
|
||||
// Should have 3 fields total: id (inherited), name (inherited), price (own)
|
||||
assert_eq!(all_fields.len(), 3);
|
||||
|
||||
// Should inherit: name, id (Entity only once)
|
||||
let inherited: Vec<_> = all_fields
|
||||
.iter()
|
||||
.filter_map(|(name, f)| match f {
|
||||
IrSchemaField::Inherited(info) => Some((name, info)),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(inherited.len(), 2);
|
||||
let inherited_names: Vec<&str> = inherited.iter().map(|(n, _)| **n).collect();
|
||||
assert!(inherited_names.contains(&"id"));
|
||||
assert!(inherited_names.contains(&"name"));
|
||||
|
||||
// Own: price
|
||||
let own: Vec<_> = all_fields
|
||||
.iter()
|
||||
.filter_map(|(name, f)| match f {
|
||||
IrSchemaField::Own(info) => Some((name, info)),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(own.len(), 1);
|
||||
assert_eq!(*own[0].0, "price");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
mod error;
|
||||
mod fields;
|
||||
mod spec;
|
||||
mod transform;
|
||||
mod types;
|
||||
mod visitor;
|
||||
|
||||
pub use fields::*;
|
||||
pub use spec::*;
|
||||
pub use transform::*;
|
||||
pub use types::*;
|
||||
pub use visitor::{InnerLeaf, InnerRef, Visitable};
|
||||
+362
@@ -0,0 +1,362 @@
|
||||
use std::collections::BTreeSet;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use indexmap::IndexMap;
|
||||
use itertools::Itertools;
|
||||
use petgraph::algo::tarjan_scc;
|
||||
use petgraph::graph::{DiGraph, NodeIndex};
|
||||
use petgraph::visit::Bfs;
|
||||
|
||||
use crate::parse::{self, Document, Info, ParameterLocation, RefOrSchema};
|
||||
|
||||
use super::{
|
||||
error::IrError,
|
||||
transform::transform,
|
||||
types::{
|
||||
InlineIrTypePath, InlineIrTypePathRoot, InlineIrTypePathSegment, IrOperation, IrParameter,
|
||||
IrParameterInfo, IrRequest, IrResponse, IrType, IrTypeName,
|
||||
},
|
||||
visitor::InnerRef,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct IrSpec<'a> {
|
||||
info: &'a Info,
|
||||
operations: Vec<IrOperation<'a>>,
|
||||
schemas: IndexMap<&'a str, (NodeIndex, IrType<'a>)>,
|
||||
refs: DiGraph<&'a str, ()>,
|
||||
circular_refs: OnceLock<BTreeSet<(NodeIndex, NodeIndex)>>,
|
||||
}
|
||||
|
||||
impl<'a> IrSpec<'a> {
|
||||
pub fn from_doc(doc: &'a Document) -> Result<Self, IrError> {
|
||||
let mut schemas = IndexMap::new();
|
||||
let mut refs = DiGraph::new();
|
||||
|
||||
if let Some(components) = &doc.components {
|
||||
for (name, schema) in &components.schemas {
|
||||
let index = refs.add_node(name.as_str());
|
||||
schemas.insert(
|
||||
name.as_str(),
|
||||
(index, transform(doc, IrTypeName::Schema(name), schema)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for (from, ty) in schemas.values() {
|
||||
for r in ty.visit::<InnerRef>() {
|
||||
let &(to, _) = &schemas[r.name()];
|
||||
refs.add_edge(*from, to, ());
|
||||
}
|
||||
}
|
||||
|
||||
let mut operations = vec![];
|
||||
for (path, item) in &doc.paths {
|
||||
let segments = parse::path::parse(path.as_str())?;
|
||||
for (method, op) in item.operations() {
|
||||
let resource = op.extension("x-resource-name").unwrap_or("full");
|
||||
let id = op.operation_id.as_deref().ok_or(IrError::NoOperationId)?;
|
||||
|
||||
let params = op
|
||||
.parameters
|
||||
.iter()
|
||||
.filter_map(|param| {
|
||||
let ty = match ¶m.schema {
|
||||
Some(RefOrSchema::Ref(r)) => IrType::Ref(r.path.as_str()),
|
||||
Some(RefOrSchema::Schema(schema)) => transform(
|
||||
doc,
|
||||
InlineIrTypePath {
|
||||
root: InlineIrTypePathRoot::Resource(resource),
|
||||
segments: vec![
|
||||
InlineIrTypePathSegment::Operation(id),
|
||||
InlineIrTypePathSegment::Parameter(param.name.as_str()),
|
||||
],
|
||||
},
|
||||
schema,
|
||||
),
|
||||
None => IrType::Any,
|
||||
};
|
||||
let info = IrParameterInfo {
|
||||
name: param.name.as_str(),
|
||||
ty,
|
||||
required: param.required,
|
||||
description: param.description.as_deref(),
|
||||
};
|
||||
Some(match param.location {
|
||||
ParameterLocation::Path => IrParameter::Path(info),
|
||||
ParameterLocation::Query => IrParameter::Query(info),
|
||||
_ => return None,
|
||||
})
|
||||
})
|
||||
.collect_vec();
|
||||
|
||||
let request = op
|
||||
.request_body
|
||||
.as_ref()
|
||||
.map(|request| {
|
||||
if request.content.contains_key("multipart/form-data") {
|
||||
RequestContent::Multipart
|
||||
} else if let Some(content) = request.content.get("application/json")
|
||||
&& let Some(schema) = &content.schema
|
||||
{
|
||||
RequestContent::Json(schema)
|
||||
} else if let Some(content) = request.content.get("*/*")
|
||||
&& let Some(schema) = &content.schema
|
||||
{
|
||||
RequestContent::Json(schema)
|
||||
} else {
|
||||
RequestContent::Any
|
||||
}
|
||||
})
|
||||
.map(|content| match content {
|
||||
RequestContent::Multipart => IrRequest::Multipart,
|
||||
RequestContent::Json(RefOrSchema::Ref(r)) => {
|
||||
IrRequest::Json(IrType::Ref(r.path.as_str()))
|
||||
}
|
||||
RequestContent::Json(RefOrSchema::Schema(schema)) => {
|
||||
IrRequest::Json(transform(
|
||||
doc,
|
||||
InlineIrTypePath {
|
||||
root: InlineIrTypePathRoot::Resource(resource),
|
||||
segments: vec![
|
||||
InlineIrTypePathSegment::Operation(id),
|
||||
InlineIrTypePathSegment::Request,
|
||||
],
|
||||
},
|
||||
schema,
|
||||
))
|
||||
}
|
||||
RequestContent::Any => IrRequest::Json(IrType::Any),
|
||||
});
|
||||
|
||||
let response = {
|
||||
let mut statuses = op
|
||||
.responses
|
||||
.keys()
|
||||
.filter_map(|status| Some((status.as_str(), status.parse::<u16>().ok()?)))
|
||||
.collect_vec();
|
||||
statuses.sort_unstable_by_key(|&(_, code)| code);
|
||||
let key = statuses
|
||||
.iter()
|
||||
.find(|&(_, code)| matches!(code, 200..300))
|
||||
.map(|&(key, _)| key)
|
||||
.unwrap_or("default");
|
||||
|
||||
op.responses
|
||||
.get(key)
|
||||
.and_then(|response| response.content.as_ref())
|
||||
.map(|content| {
|
||||
if let Some(content) = content.get("application/json")
|
||||
&& let Some(schema) = &content.schema
|
||||
{
|
||||
ResponseContent::Json(schema)
|
||||
} else if let Some(content) = content.get("*/*")
|
||||
&& let Some(schema) = &content.schema
|
||||
{
|
||||
ResponseContent::Json(schema)
|
||||
} else {
|
||||
ResponseContent::Any
|
||||
}
|
||||
})
|
||||
.map(|content| match content {
|
||||
ResponseContent::Json(RefOrSchema::Ref(r)) => {
|
||||
IrResponse::Json(IrType::Ref(r.path.as_str()))
|
||||
}
|
||||
ResponseContent::Json(RefOrSchema::Schema(schema)) => {
|
||||
IrResponse::Json(transform(
|
||||
doc,
|
||||
InlineIrTypePath {
|
||||
root: InlineIrTypePathRoot::Resource(resource),
|
||||
segments: vec![
|
||||
InlineIrTypePathSegment::Operation(id),
|
||||
InlineIrTypePathSegment::Request,
|
||||
],
|
||||
},
|
||||
schema,
|
||||
))
|
||||
}
|
||||
ResponseContent::Any => IrResponse::Json(IrType::Any),
|
||||
})
|
||||
};
|
||||
|
||||
operations.push(IrOperation {
|
||||
resource,
|
||||
id,
|
||||
method,
|
||||
path: segments.clone(),
|
||||
description: op.description.as_deref(),
|
||||
params,
|
||||
request,
|
||||
response,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(IrSpec {
|
||||
info: &doc.info,
|
||||
operations,
|
||||
schemas,
|
||||
refs,
|
||||
circular_refs: OnceLock::new(),
|
||||
})
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn info(&self) -> &'a Info {
|
||||
self.info
|
||||
}
|
||||
|
||||
/// Yields views for all operations in this spec.
|
||||
#[inline]
|
||||
pub fn operations(&self) -> impl Iterator<Item = IrOperationView<'_>> {
|
||||
self.operations
|
||||
.iter()
|
||||
.map(|op| IrOperationView { spec: self, op })
|
||||
}
|
||||
|
||||
/// Yields views for all schemas in this spec.
|
||||
#[inline]
|
||||
pub fn schemas(&self) -> impl Iterator<Item = IrSchemaView<'_>> {
|
||||
self.schemas
|
||||
.iter()
|
||||
.map(|(_, &(index, _))| IrSchemaView::new(self, index))
|
||||
}
|
||||
|
||||
/// Looks up a schema by name, and returns a view for that schema.
|
||||
#[inline]
|
||||
pub fn lookup(&self, name: &str) -> Option<IrSchemaView<'_>> {
|
||||
self.schemas
|
||||
.get(name)
|
||||
.map(|&(index, _)| IrSchemaView::new(self, index))
|
||||
}
|
||||
|
||||
/// Finds all circular reference edges in this spec. Each edge
|
||||
/// in the returned set participates in a circular reference,
|
||||
/// and requires indirection to break the cycle.
|
||||
fn circular_refs(&self) -> &BTreeSet<(NodeIndex, NodeIndex)> {
|
||||
self.circular_refs.get_or_init(|| {
|
||||
let mut edges = BTreeSet::new();
|
||||
let sccs = tarjan_scc(&self.refs);
|
||||
for scc in sccs {
|
||||
let scc = BTreeSet::from_iter(scc);
|
||||
for &node in &scc {
|
||||
// Collect internal edges within this
|
||||
// strongly connected component.
|
||||
edges.extend(
|
||||
self.refs
|
||||
.neighbors(node)
|
||||
.filter(|neighbor| scc.contains(neighbor))
|
||||
.map(|neighbor| (node, neighbor)),
|
||||
);
|
||||
}
|
||||
}
|
||||
edges
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct IrOperationView<'a> {
|
||||
spec: &'a IrSpec<'a>,
|
||||
op: &'a IrOperation<'a>,
|
||||
}
|
||||
|
||||
impl<'a> IrOperationView<'a> {
|
||||
#[inline]
|
||||
pub fn op(self) -> &'a IrOperation<'a> {
|
||||
self.op
|
||||
}
|
||||
|
||||
pub fn refs(self) -> impl Iterator<Item = IrSchemaView<'a>> {
|
||||
itertools::chain!(
|
||||
// Parameter type references.
|
||||
self.op
|
||||
.params
|
||||
.iter()
|
||||
.map(|param| match param {
|
||||
IrParameter::Path(info) => info,
|
||||
IrParameter::Query(info) => info,
|
||||
})
|
||||
.flat_map(|info| info.ty.visit()),
|
||||
// Request type references.
|
||||
self.op
|
||||
.request
|
||||
.iter()
|
||||
.filter_map(|request| match request {
|
||||
IrRequest::Multipart => None,
|
||||
IrRequest::Json(ty) => Some(ty),
|
||||
})
|
||||
.flat_map(|ty| ty.visit()),
|
||||
// Response type references.
|
||||
self.op
|
||||
.response
|
||||
.iter()
|
||||
.map(|response| match response {
|
||||
IrResponse::Json(ty) => ty,
|
||||
})
|
||||
.flat_map(|ty| ty.visit()),
|
||||
)
|
||||
.flat_map(move |r: InnerRef| {
|
||||
let &(index, _) = &self.spec.schemas[r.name()];
|
||||
let mut bfs = Bfs::new(&self.spec.refs, index);
|
||||
std::iter::from_fn(move || bfs.next(&self.spec.refs))
|
||||
})
|
||||
.map(|index| IrSchemaView {
|
||||
spec: self.spec,
|
||||
index,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct IrSchemaView<'a> {
|
||||
spec: &'a IrSpec<'a>,
|
||||
index: NodeIndex,
|
||||
}
|
||||
|
||||
impl<'a> IrSchemaView<'a> {
|
||||
#[inline]
|
||||
fn new(spec: &'a IrSpec<'a>, index: NodeIndex) -> Self {
|
||||
Self { spec, index }
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn name(self) -> &'a str {
|
||||
self.spec.refs[self.index]
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn ty(self) -> &'a IrType<'a> {
|
||||
let (_, ty) = &self.spec.schemas[self.name()];
|
||||
ty
|
||||
}
|
||||
|
||||
/// Returns `true` if a reference from this schema to the `other` schema
|
||||
/// requires indirection (with [`Box`], [`Vec`], etc.)
|
||||
#[inline]
|
||||
pub fn requires_indirection_to(&self, other: IrSchemaView<'_>) -> bool {
|
||||
self.spec
|
||||
.circular_refs()
|
||||
.contains(&(self.index, other.index))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn refs(self) -> impl Iterator<Item = IrSchemaView<'a>> {
|
||||
let mut bfs = Bfs::new(&self.spec.refs, self.index);
|
||||
std::iter::from_fn(move || bfs.next(&self.spec.refs))
|
||||
.map(|index| IrSchemaView::new(self.spec, index))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
enum RequestContent<'a> {
|
||||
Multipart,
|
||||
Json(&'a RefOrSchema),
|
||||
Any,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
enum ResponseContent<'a> {
|
||||
Json(&'a RefOrSchema),
|
||||
Any,
|
||||
}
|
||||
@@ -0,0 +1,337 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use itertools::Itertools;
|
||||
|
||||
use crate::parse::{AdditionalProperties, Document, Format, RefOrSchema, Schema, Ty};
|
||||
|
||||
use super::{
|
||||
fields::{IrSchemaField, all_fields},
|
||||
types::{
|
||||
InlineIrType, InlineIrTypePath, InlineIrTypePathRoot, InlineIrTypePathSegment, IrEnum,
|
||||
IrEnumVariant, IrStruct, IrStructField, IrTagged, IrTaggedVariant, IrType, IrTypeName,
|
||||
IrUntagged, IrUntaggedVariant, IrUntaggedVariantNameHint, PrimitiveIrType, SchemaIrType,
|
||||
},
|
||||
};
|
||||
|
||||
#[inline]
|
||||
pub fn transform<'a>(
|
||||
doc: &'a Document,
|
||||
name: impl Into<IrTypeName<'a>>,
|
||||
schema: &'a Schema,
|
||||
) -> IrType<'a> {
|
||||
IrTransformer::new(doc, name.into(), schema).transform()
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct IrTransformer<'a> {
|
||||
doc: &'a Document,
|
||||
name: IrTypeName<'a>,
|
||||
schema: &'a Schema,
|
||||
}
|
||||
|
||||
impl<'a> IrTransformer<'a> {
|
||||
fn new(doc: &'a Document, name: IrTypeName<'a>, schema: &'a Schema) -> Self {
|
||||
Self { doc, name, schema }
|
||||
}
|
||||
|
||||
fn transform(self) -> IrType<'a> {
|
||||
self.try_tagged()
|
||||
.or_else(Self::try_untagged)
|
||||
.or_else(Self::try_enum)
|
||||
.or_else(Self::try_struct)
|
||||
.unwrap_or_else(Self::other)
|
||||
}
|
||||
|
||||
fn try_tagged(self) -> Result<IrType<'a>, Self> {
|
||||
let (Some(one_of), Some(discriminator)) = (&self.schema.one_of, &self.schema.discriminator)
|
||||
else {
|
||||
return Err(self);
|
||||
};
|
||||
let inverted: BTreeMap<_, Vec<_>> =
|
||||
discriminator
|
||||
.mapping
|
||||
.iter()
|
||||
.fold(BTreeMap::new(), |mut mapping, (tag, reference)| {
|
||||
mapping.entry(reference).or_default().push(tag.as_str());
|
||||
mapping
|
||||
});
|
||||
let variants = one_of
|
||||
.iter()
|
||||
.filter_map(|schema| match schema {
|
||||
RefOrSchema::Ref(r) => {
|
||||
let aliases = inverted.get(&r.path).cloned().unwrap_or_default();
|
||||
Some(IrTaggedVariant {
|
||||
name: r.path.as_str(),
|
||||
ty: IrType::Ref(r.path.as_str()),
|
||||
aliases,
|
||||
})
|
||||
}
|
||||
RefOrSchema::Schema(_) => None,
|
||||
})
|
||||
.filter(|v| !v.aliases.is_empty())
|
||||
.collect();
|
||||
let tagged = IrTagged {
|
||||
description: self.schema.description.as_deref(),
|
||||
tag: discriminator.property_name.as_str(),
|
||||
variants,
|
||||
};
|
||||
Ok(match self.name {
|
||||
IrTypeName::Schema(name) => SchemaIrType::Tagged(name, tagged).into(),
|
||||
IrTypeName::Inline(_path) => todo!(),
|
||||
})
|
||||
}
|
||||
|
||||
fn try_untagged(self) -> Result<IrType<'a>, Self> {
|
||||
let Some(one_of) = &self.schema.one_of else {
|
||||
return Err(self);
|
||||
};
|
||||
let variants = one_of
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, schema)| (index + 1, schema))
|
||||
.map(|(index, schema)| match schema {
|
||||
RefOrSchema::Ref(r) => IrUntaggedVariant::Some(
|
||||
IrUntaggedVariantNameHint::Index(index),
|
||||
IrType::Ref(r.path.as_str()),
|
||||
),
|
||||
RefOrSchema::Schema(s) if matches!(&*s.ty, [Ty::Null]) => IrUntaggedVariant::Null,
|
||||
RefOrSchema::Schema(schema) => {
|
||||
let path = match &self.name {
|
||||
IrTypeName::Schema(name) => InlineIrTypePath {
|
||||
root: InlineIrTypePathRoot::Type(name),
|
||||
segments: vec![InlineIrTypePathSegment::Variant(index)],
|
||||
},
|
||||
IrTypeName::Inline(path) => {
|
||||
let mut path = path.clone();
|
||||
path.segments.push(InlineIrTypePathSegment::Variant(index));
|
||||
path
|
||||
}
|
||||
};
|
||||
IrUntaggedVariant::Some(
|
||||
IrUntaggedVariantNameHint::Index(index),
|
||||
transform(self.doc, path, schema),
|
||||
)
|
||||
}
|
||||
})
|
||||
.collect_vec();
|
||||
Ok(match &*variants {
|
||||
[] => IrType::Any,
|
||||
[IrUntaggedVariant::Null] => IrType::Any,
|
||||
[IrUntaggedVariant::Some(_, ty)] => ty.clone(),
|
||||
[IrUntaggedVariant::Some(_, ty), IrUntaggedVariant::Null] => {
|
||||
IrType::Nullable(ty.clone().into())
|
||||
}
|
||||
[IrUntaggedVariant::Null, IrUntaggedVariant::Some(_, ty)] => {
|
||||
IrType::Nullable(ty.clone().into())
|
||||
}
|
||||
_ => {
|
||||
let untagged = IrUntagged {
|
||||
description: self.schema.description.as_deref(),
|
||||
variants,
|
||||
};
|
||||
match self.name {
|
||||
IrTypeName::Schema(name) => SchemaIrType::Untagged(name, untagged).into(),
|
||||
IrTypeName::Inline(path) => InlineIrType::Untagged(path, untagged).into(),
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn try_enum(self) -> Result<IrType<'a>, Self> {
|
||||
let Some(values) = &self.schema.variants else {
|
||||
return Err(self);
|
||||
};
|
||||
let variants = values
|
||||
.iter()
|
||||
.filter_map(|value| value.as_str())
|
||||
.map(IrEnumVariant::String)
|
||||
.collect();
|
||||
let ty = IrEnum {
|
||||
description: self.schema.description.as_deref(),
|
||||
variants,
|
||||
};
|
||||
Ok(match self.name {
|
||||
IrTypeName::Schema(name) => SchemaIrType::Enum(name, ty).into(),
|
||||
IrTypeName::Inline(path) => InlineIrType::Enum(path, ty).into(),
|
||||
})
|
||||
}
|
||||
|
||||
fn try_struct(self) -> Result<IrType<'a>, Self> {
|
||||
if self
|
||||
.schema
|
||||
.additional_properties
|
||||
.as_ref()
|
||||
.is_some_and(|additional| {
|
||||
matches!(
|
||||
additional,
|
||||
AdditionalProperties::RefOrSchema(_) | AdditionalProperties::Bool(true)
|
||||
)
|
||||
})
|
||||
{
|
||||
return Err(self);
|
||||
}
|
||||
if self.schema.properties.is_none() && self.schema.all_of.is_none() {
|
||||
return Err(self);
|
||||
}
|
||||
let all = all_fields(self.doc, self.schema);
|
||||
let fields = all
|
||||
.map(|(field_name, field)| {
|
||||
let info = field.info();
|
||||
let ty = match info.schema {
|
||||
RefOrSchema::Ref(reference) => IrType::Ref(reference.path.as_str()),
|
||||
RefOrSchema::Schema(schema) => {
|
||||
let path = match &self.name {
|
||||
IrTypeName::Schema(name) => InlineIrTypePath {
|
||||
root: InlineIrTypePathRoot::Type(name),
|
||||
segments: vec![InlineIrTypePathSegment::Field(field_name)],
|
||||
},
|
||||
IrTypeName::Inline(path) => {
|
||||
let mut path = path.clone();
|
||||
path.segments
|
||||
.push(InlineIrTypePathSegment::Field(field_name));
|
||||
path
|
||||
}
|
||||
};
|
||||
transform(self.doc, path, schema)
|
||||
}
|
||||
};
|
||||
let description = match info.schema {
|
||||
RefOrSchema::Schema(schema) => schema.description.as_deref(),
|
||||
RefOrSchema::Ref(r) => self
|
||||
.doc
|
||||
.components
|
||||
.as_ref()
|
||||
.and_then(|c| c.schemas.get(r.path.as_str()))
|
||||
.and_then(|schema| schema.description.as_deref()),
|
||||
};
|
||||
let nullable = match info.schema {
|
||||
RefOrSchema::Schema(schema) if schema.nullable => true,
|
||||
RefOrSchema::Ref(r) => {
|
||||
if let Some(components) = &self.doc.components
|
||||
&& let Some(schema) = components.schemas.get(r.path.as_str())
|
||||
&& schema.nullable
|
||||
{
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
let ty = if nullable {
|
||||
IrType::Nullable(ty.into())
|
||||
} else {
|
||||
ty
|
||||
};
|
||||
IrStructField {
|
||||
name: field_name,
|
||||
ty,
|
||||
required: info.required,
|
||||
description,
|
||||
inherited: matches!(field, IrSchemaField::Inherited(_)),
|
||||
discriminator: info.discriminator,
|
||||
}
|
||||
})
|
||||
.collect_vec();
|
||||
let ty = IrStruct {
|
||||
description: self.schema.description.as_deref(),
|
||||
fields,
|
||||
};
|
||||
Ok(match self.name {
|
||||
IrTypeName::Schema(name) => SchemaIrType::Struct(name, ty).into(),
|
||||
IrTypeName::Inline(path) => InlineIrType::Struct(path, ty).into(),
|
||||
})
|
||||
}
|
||||
|
||||
fn other(self) -> IrType<'a> {
|
||||
let variants = self
|
||||
.schema
|
||||
.ty
|
||||
.iter()
|
||||
.map(|&ty| match (ty, self.schema.format) {
|
||||
(Ty::String, Some(Format::DateTime)) => PrimitiveIrType::DateTime.into(),
|
||||
(Ty::String, Some(Format::Date)) => PrimitiveIrType::Date.into(),
|
||||
(Ty::String, Some(Format::Uri)) => PrimitiveIrType::Url.into(),
|
||||
(Ty::String, Some(Format::Uuid)) => PrimitiveIrType::Uuid.into(),
|
||||
(Ty::String, Some(Format::Byte) | Some(Format::Binary)) => {
|
||||
PrimitiveIrType::Bytes.into()
|
||||
}
|
||||
(Ty::String, _) => PrimitiveIrType::String.into(),
|
||||
(Ty::Integer, Some(Format::Int64)) => PrimitiveIrType::I64.into(),
|
||||
(Ty::Integer, Some(Format::Int32) | _) => PrimitiveIrType::I32.into(),
|
||||
(Ty::Number, Some(Format::Float)) => PrimitiveIrType::F32.into(),
|
||||
(Ty::Number, Some(Format::Double) | _) => PrimitiveIrType::F64.into(),
|
||||
(Ty::Boolean, _) => PrimitiveIrType::Bool.into(),
|
||||
(Ty::Array, _) => {
|
||||
let items = match &self.schema.items {
|
||||
Some(RefOrSchema::Ref(r)) => IrType::Ref(r.path.as_str()),
|
||||
Some(RefOrSchema::Schema(schema)) => {
|
||||
let path = match &self.name {
|
||||
IrTypeName::Schema(name) => InlineIrTypePath {
|
||||
root: InlineIrTypePathRoot::Type(name),
|
||||
segments: vec![InlineIrTypePathSegment::ArrayItem],
|
||||
},
|
||||
IrTypeName::Inline(path) => {
|
||||
let mut path = path.clone();
|
||||
path.segments.push(InlineIrTypePathSegment::ArrayItem);
|
||||
path
|
||||
}
|
||||
};
|
||||
transform(self.doc, path, schema)
|
||||
}
|
||||
None => IrType::Any,
|
||||
};
|
||||
let ty = IrType::Array(items.into());
|
||||
IrUntaggedVariant::Some(IrUntaggedVariantNameHint::Array, ty)
|
||||
}
|
||||
(Ty::Object, _) => {
|
||||
let ty = match &self.schema.additional_properties {
|
||||
Some(AdditionalProperties::RefOrSchema(RefOrSchema::Ref(r))) => {
|
||||
IrType::Map(IrType::Ref(r.path.as_str()).into())
|
||||
}
|
||||
Some(AdditionalProperties::RefOrSchema(RefOrSchema::Schema(schema))) => {
|
||||
let path = match &self.name {
|
||||
IrTypeName::Schema(name) => InlineIrTypePath {
|
||||
root: InlineIrTypePathRoot::Type(name),
|
||||
segments: vec![InlineIrTypePathSegment::MapValue],
|
||||
},
|
||||
IrTypeName::Inline(path) => {
|
||||
let mut path = path.clone();
|
||||
path.segments.push(InlineIrTypePathSegment::MapValue);
|
||||
path
|
||||
}
|
||||
};
|
||||
IrType::Map(transform(self.doc, path, schema).into())
|
||||
}
|
||||
Some(AdditionalProperties::Bool(true)) => IrType::Map(IrType::Any.into()),
|
||||
_ => IrType::Any,
|
||||
};
|
||||
IrUntaggedVariant::Some(IrUntaggedVariantNameHint::Map, ty)
|
||||
}
|
||||
(Ty::Null, _) => IrUntaggedVariant::Null,
|
||||
})
|
||||
.collect_vec();
|
||||
|
||||
match &*variants {
|
||||
[] => IrType::Any,
|
||||
[IrUntaggedVariant::Null] => IrType::Any,
|
||||
[IrUntaggedVariant::Some(_, ty)] => ty.clone(),
|
||||
[IrUntaggedVariant::Some(_, ty), IrUntaggedVariant::Null] => {
|
||||
IrType::Nullable(ty.clone().into())
|
||||
}
|
||||
[IrUntaggedVariant::Null, IrUntaggedVariant::Some(_, ty)] => {
|
||||
IrType::Nullable(ty.clone().into())
|
||||
}
|
||||
_ => {
|
||||
let untagged = IrUntagged {
|
||||
description: self.schema.description.as_deref(),
|
||||
variants,
|
||||
};
|
||||
match self.name {
|
||||
IrTypeName::Schema(name) => SchemaIrType::Untagged(name, untagged).into(),
|
||||
IrTypeName::Inline(path) => InlineIrType::Untagged(path, untagged).into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+270
@@ -0,0 +1,270 @@
|
||||
//! Language-agnostic intermediate representation types.
|
||||
|
||||
use crate::parse::{Method, path::PathSegment};
|
||||
|
||||
use super::visitor::{Visitable, Visitor};
|
||||
|
||||
/// A schema type ready for code generation.
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum IrType<'a> {
|
||||
/// A primitive type.
|
||||
Primitive(PrimitiveIrType),
|
||||
/// An array of items.
|
||||
Array(Box<IrType<'a>>),
|
||||
/// A map with string keys.
|
||||
Map(Box<IrType<'a>>),
|
||||
/// A nullable type.
|
||||
Nullable(Box<IrType<'a>>),
|
||||
/// A reference to another named schema type.
|
||||
Ref(&'a str),
|
||||
/// A named schema type.
|
||||
Schema(SchemaIrType<'a>),
|
||||
/// An inline type defined within a schema.
|
||||
Inline(InlineIrType<'a>),
|
||||
/// Any JSON value.
|
||||
Any,
|
||||
}
|
||||
|
||||
impl IrType<'_> {
|
||||
/// Visits the inner types within this schema type.
|
||||
#[inline]
|
||||
pub fn visit<'a, F: Visitable<'a>>(&'a self) -> impl Iterator<Item = F> {
|
||||
Visitor::new(self).filter_map(F::accept)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PrimitiveIrType> for IrType<'_> {
|
||||
fn from(ty: PrimitiveIrType) -> Self {
|
||||
Self::Primitive(ty)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<SchemaIrType<'a>> for IrType<'a> {
|
||||
fn from(ty: SchemaIrType<'a>) -> Self {
|
||||
Self::Schema(ty)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<InlineIrType<'a>> for IrType<'a> {
|
||||
fn from(ty: InlineIrType<'a>) -> Self {
|
||||
Self::Inline(ty)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
pub enum PrimitiveIrType {
|
||||
String,
|
||||
I32,
|
||||
I64,
|
||||
F32,
|
||||
F64,
|
||||
Bool,
|
||||
DateTime,
|
||||
Date,
|
||||
Url,
|
||||
Uuid,
|
||||
Bytes,
|
||||
}
|
||||
|
||||
/// A named schema type.
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum SchemaIrType<'a> {
|
||||
/// An enum with named variants.
|
||||
Enum(&'a str, IrEnum<'a>),
|
||||
/// A struct with fields.
|
||||
Struct(&'a str, IrStruct<'a>),
|
||||
/// A tagged union.
|
||||
Tagged(&'a str, IrTagged<'a>),
|
||||
/// An untagged union.
|
||||
Untagged(&'a str, IrUntagged<'a>),
|
||||
}
|
||||
|
||||
impl SchemaIrType<'_> {
|
||||
/// Visits the inner types within this named schema type.
|
||||
#[inline]
|
||||
pub fn visit<'a, F: Visitable<'a>>(&'a self) -> impl Iterator<Item = F> {
|
||||
Visitor::for_schema_ty(self).filter_map(F::accept)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> SchemaIrType<'a> {
|
||||
#[inline]
|
||||
pub fn name(&self) -> &'a str {
|
||||
let (Self::Enum(name, _)
|
||||
| Self::Struct(name, _)
|
||||
| Self::Tagged(name, _)
|
||||
| Self::Untagged(name, _)) = self;
|
||||
name
|
||||
}
|
||||
}
|
||||
|
||||
/// An inline schema type.
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum InlineIrType<'a> {
|
||||
Enum(InlineIrTypePath<'a>, IrEnum<'a>),
|
||||
Struct(InlineIrTypePath<'a>, IrStruct<'a>),
|
||||
Untagged(InlineIrTypePath<'a>, IrUntagged<'a>),
|
||||
}
|
||||
|
||||
impl<'a> InlineIrType<'a> {
|
||||
#[inline]
|
||||
pub fn path(&self) -> &InlineIrTypePath<'a> {
|
||||
let (Self::Enum(path, _) | Self::Struct(path, _) | Self::Untagged(path, _)) = self;
|
||||
path
|
||||
}
|
||||
}
|
||||
|
||||
/// A path to an inline type.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct InlineIrTypePath<'a> {
|
||||
pub root: InlineIrTypePathRoot<'a>,
|
||||
pub segments: Vec<InlineIrTypePathSegment<'a>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum InlineIrTypePathRoot<'a> {
|
||||
Resource(&'a str),
|
||||
Type(&'a str),
|
||||
}
|
||||
|
||||
/// A segment of an inline type path.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum InlineIrTypePathSegment<'a> {
|
||||
Operation(&'a str),
|
||||
Parameter(&'a str),
|
||||
Request,
|
||||
Response,
|
||||
Field(&'a str),
|
||||
MapValue,
|
||||
ArrayItem,
|
||||
Variant(usize),
|
||||
}
|
||||
|
||||
/// An enum type.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct IrEnum<'a> {
|
||||
pub description: Option<&'a str>,
|
||||
pub variants: Vec<IrEnumVariant<'a>>,
|
||||
}
|
||||
|
||||
/// A variant of an enum.
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum IrEnumVariant<'a> {
|
||||
String(&'a str),
|
||||
}
|
||||
|
||||
/// A struct, created from a schema with named properties.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct IrStruct<'a> {
|
||||
pub description: Option<&'a str>,
|
||||
pub fields: Vec<IrStructField<'a>>,
|
||||
}
|
||||
|
||||
/// A field in a struct.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct IrStructField<'a> {
|
||||
pub name: &'a str,
|
||||
pub ty: IrType<'a>,
|
||||
pub required: bool,
|
||||
pub description: Option<&'a str>,
|
||||
pub inherited: bool,
|
||||
pub discriminator: bool,
|
||||
}
|
||||
|
||||
/// A tagged union, created from a `oneOf` schema
|
||||
/// with an explicit `discriminator`.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct IrTagged<'a> {
|
||||
pub description: Option<&'a str>,
|
||||
pub tag: &'a str,
|
||||
pub variants: Vec<IrTaggedVariant<'a>>,
|
||||
}
|
||||
|
||||
/// A variant of a tagged union.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct IrTaggedVariant<'a> {
|
||||
pub name: &'a str,
|
||||
pub aliases: Vec<&'a str>,
|
||||
pub ty: IrType<'a>,
|
||||
}
|
||||
|
||||
/// An untagged union, created from a `oneOf` schema
|
||||
/// without a discriminator, or an OpenAPI 3.1 schema
|
||||
/// with multiple types in its `type` field.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct IrUntagged<'a> {
|
||||
pub description: Option<&'a str>,
|
||||
pub variants: Vec<IrUntaggedVariant<'a>>,
|
||||
}
|
||||
|
||||
/// A hint that's used to generate a more descriptive name
|
||||
/// for an untagged union variant.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum IrUntaggedVariantNameHint {
|
||||
Primitive(PrimitiveIrType),
|
||||
Array,
|
||||
Map,
|
||||
Index(usize),
|
||||
}
|
||||
|
||||
/// A variant of an untagged union.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum IrUntaggedVariant<'a> {
|
||||
Some(IrUntaggedVariantNameHint, IrType<'a>),
|
||||
Null,
|
||||
}
|
||||
|
||||
impl From<PrimitiveIrType> for IrUntaggedVariant<'_> {
|
||||
fn from(ty: PrimitiveIrType) -> Self {
|
||||
Self::Some(IrUntaggedVariantNameHint::Primitive(ty), ty.into())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum IrTypeName<'a> {
|
||||
Schema(&'a str),
|
||||
Inline(InlineIrTypePath<'a>),
|
||||
}
|
||||
|
||||
impl<'a> From<InlineIrTypePath<'a>> for IrTypeName<'a> {
|
||||
fn from(path: InlineIrTypePath<'a>) -> Self {
|
||||
Self::Inline(path)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct IrOperation<'a> {
|
||||
pub resource: &'a str,
|
||||
pub id: &'a str,
|
||||
pub method: Method,
|
||||
pub path: Vec<PathSegment<'a>>,
|
||||
pub description: Option<&'a str>,
|
||||
pub params: Vec<IrParameter<'a>>,
|
||||
pub request: Option<IrRequest<'a>>,
|
||||
pub response: Option<IrResponse<'a>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum IrResponse<'a> {
|
||||
Json(IrType<'a>),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum IrRequest<'a> {
|
||||
Json(IrType<'a>),
|
||||
Multipart,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum IrParameter<'a> {
|
||||
Path(IrParameterInfo<'a>),
|
||||
Query(IrParameterInfo<'a>),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct IrParameterInfo<'a> {
|
||||
pub name: &'a str,
|
||||
pub ty: IrType<'a>,
|
||||
pub required: bool,
|
||||
pub description: Option<&'a str>,
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
use super::types::{InlineIrType, IrType, IrUntaggedVariant, PrimitiveIrType, SchemaIrType};
|
||||
|
||||
/// An inner type within a schema.
|
||||
pub trait Visitable<'a>: Sized {
|
||||
fn accept(inner: InnerIrType<'a>) -> Option<Self>;
|
||||
}
|
||||
|
||||
/// An inner reference to another schema.
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
pub struct InnerRef<'a>(&'a str);
|
||||
|
||||
impl<'a> InnerRef<'a> {
|
||||
#[inline]
|
||||
pub fn name(self) -> &'a str {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Visitable<'a> for InnerRef<'a> {
|
||||
#[inline]
|
||||
fn accept(inner: InnerIrType<'a>) -> Option<Self> {
|
||||
match inner {
|
||||
InnerIrType::Ref(name) => Some(Self(name)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An inner leaf type that doesn't reference or contain nested schemas.
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
pub enum InnerLeaf {
|
||||
Any,
|
||||
Primitive(PrimitiveIrType),
|
||||
}
|
||||
|
||||
impl<'a> Visitable<'a> for InnerLeaf {
|
||||
#[inline]
|
||||
fn accept(inner: InnerIrType<'a>) -> Option<Self> {
|
||||
match inner {
|
||||
InnerIrType::Any => Some(InnerLeaf::Any),
|
||||
InnerIrType::Primitive(ty) => Some(InnerLeaf::Primitive(ty)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Visitable<'a> for &InlineIrType<'a> {
|
||||
#[inline]
|
||||
fn accept(inner: InnerIrType<'a>) -> Option<Self> {
|
||||
match inner {
|
||||
InnerIrType::Inline(ty) => Some(ty),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Visitor<'a> {
|
||||
stack: Vec<&'a IrType<'a>>,
|
||||
}
|
||||
|
||||
impl<'a> Visitor<'a> {
|
||||
#[inline]
|
||||
pub fn new(root: &'a IrType<'a>) -> Self {
|
||||
Self { stack: vec![root] }
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn for_schema_ty(root: &'a SchemaIrType<'a>) -> Self {
|
||||
let stack = match root {
|
||||
SchemaIrType::Struct(_, ty) => ty.fields.iter().map(|field| &field.ty).rev().collect(),
|
||||
SchemaIrType::Untagged(_, ty) => ty
|
||||
.variants
|
||||
.iter()
|
||||
.filter_map(|variant| match variant {
|
||||
IrUntaggedVariant::Some(_, ty) => Some(ty),
|
||||
_ => None,
|
||||
})
|
||||
.rev()
|
||||
.collect(),
|
||||
SchemaIrType::Tagged(_, ty) => ty.variants.iter().map(|name| &name.ty).rev().collect(),
|
||||
SchemaIrType::Enum(..) => vec![],
|
||||
};
|
||||
Self { stack }
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for Visitor<'a> {
|
||||
type Item = InnerIrType<'a>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
while let Some(top) = self.stack.pop() {
|
||||
match top {
|
||||
IrType::Array(ty) => {
|
||||
self.stack.push(ty.as_ref());
|
||||
}
|
||||
IrType::Map(ty) => {
|
||||
self.stack.push(ty.as_ref());
|
||||
}
|
||||
IrType::Nullable(ty) => {
|
||||
self.stack.push(ty.as_ref());
|
||||
}
|
||||
IrType::Schema(SchemaIrType::Struct(_, ty)) => {
|
||||
self.stack
|
||||
.extend(ty.fields.iter().map(|field| &field.ty).rev());
|
||||
}
|
||||
IrType::Schema(SchemaIrType::Untagged(_, ty)) => {
|
||||
self.stack.extend(
|
||||
ty.variants
|
||||
.iter()
|
||||
.filter_map(|variant| match variant {
|
||||
IrUntaggedVariant::Some(_, ty) => Some(ty),
|
||||
_ => None,
|
||||
})
|
||||
.rev(),
|
||||
);
|
||||
}
|
||||
IrType::Schema(SchemaIrType::Tagged(_, ty)) => {
|
||||
self.stack
|
||||
.extend(ty.variants.iter().map(|name| &name.ty).rev());
|
||||
}
|
||||
IrType::Schema(SchemaIrType::Enum(..)) => continue,
|
||||
IrType::Any => return Some(InnerIrType::Any),
|
||||
&IrType::Primitive(ty) => return Some(InnerIrType::Primitive(ty)),
|
||||
IrType::Inline(ty) => {
|
||||
match ty {
|
||||
InlineIrType::Enum(..) => {}
|
||||
InlineIrType::Untagged(_, ty) => self.stack.extend(
|
||||
ty.variants
|
||||
.iter()
|
||||
.filter_map(|variant| match variant {
|
||||
IrUntaggedVariant::Some(_, ty) => Some(ty),
|
||||
_ => None,
|
||||
})
|
||||
.rev(),
|
||||
),
|
||||
InlineIrType::Struct(_, ty) => self
|
||||
.stack
|
||||
.extend(ty.fields.iter().map(|field| &field.ty).rev()),
|
||||
}
|
||||
return Some(InnerIrType::Inline(ty));
|
||||
}
|
||||
&IrType::Ref(name) => return Some(InnerIrType::Ref(name)),
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum InnerIrType<'a> {
|
||||
Any,
|
||||
Primitive(PrimitiveIrType),
|
||||
Inline(&'a InlineIrType<'a>),
|
||||
Ref(&'a str),
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
pub mod codegen;
|
||||
pub mod config;
|
||||
pub mod error;
|
||||
pub mod ir;
|
||||
pub mod parse;
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
use std::borrow::Cow;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use miette::{Context, IntoDiagnostic, Result};
|
||||
|
||||
use ploidy::codegen::{rust, write_to_disk};
|
||||
use ploidy::config::{Codegen, CodegenLanguage, Command, Main};
|
||||
use ploidy::ir::IrSpec;
|
||||
use ploidy::parse::Document;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let Ok(main) = Main::parse().map_err(|err| err.exit());
|
||||
match main.command {
|
||||
Command::Codegen(Codegen {
|
||||
input,
|
||||
output,
|
||||
language: CodegenLanguage::Rust(config),
|
||||
}) => {
|
||||
let source = std::fs::read_to_string(&input)
|
||||
.into_diagnostic()
|
||||
.with_context(|| format!("Failed to read `{}`", input.display()))?;
|
||||
|
||||
let doc = Document::from_yaml(&source)
|
||||
.into_diagnostic()
|
||||
.context("Failed to parse OpenAPI document")?;
|
||||
|
||||
println!("OpenAPI: {} (version {})", doc.info.title, doc.info.version);
|
||||
|
||||
let spec = IrSpec::from_doc(&doc).into_diagnostic()?;
|
||||
|
||||
let name = config
|
||||
.package
|
||||
.name
|
||||
.as_deref()
|
||||
.map(Cow::Borrowed)
|
||||
.unwrap_or_else(|| output.to_string_lossy());
|
||||
|
||||
let context = rust::CodegenContext::new(
|
||||
&name,
|
||||
config.package.version_or_default(),
|
||||
config.package.license_or_default(),
|
||||
config.package.description.as_deref(),
|
||||
&spec,
|
||||
);
|
||||
|
||||
println!("Writing generated code to `{}`...", output.display());
|
||||
|
||||
println!("Generating `Cargo.toml`...");
|
||||
write_to_disk(&output, rust::CargoManifest::new(&context))?;
|
||||
|
||||
println!("Generating `lib.rs`...");
|
||||
write_to_disk(&output, rust::CodegenLibrary)?;
|
||||
|
||||
println!("Generating `error.rs`...");
|
||||
write_to_disk(&output, rust::CodegenErrorModule)?;
|
||||
|
||||
println!("Generating `absent.rs`...");
|
||||
write_to_disk(&output, rust::CodegenAbsentModule)?;
|
||||
|
||||
println!("Generating `date_time`.rs...");
|
||||
write_to_disk(&output, rust::CodegenDateTimeModule)?;
|
||||
|
||||
println!("Generating {} types...", context.spec.schemas().count());
|
||||
rust::write_types_to_disk(&output, &context)?;
|
||||
|
||||
let counts = context.spec.operations().fold(
|
||||
BTreeMap::<&str, usize>::new(),
|
||||
|mut counts, view| {
|
||||
*counts.entry(view.op().resource).or_default() += 1;
|
||||
counts
|
||||
},
|
||||
);
|
||||
println!(
|
||||
"Generating {} client methods for {} resources...",
|
||||
counts.values().copied().sum::<usize>(),
|
||||
counts.keys().count(),
|
||||
);
|
||||
rust::write_client_to_disk(&output, &context)?;
|
||||
|
||||
println!("Generation complete");
|
||||
|
||||
if config.check {
|
||||
println!("Running `cargo check`...");
|
||||
let status = std::process::Command::new("cargo")
|
||||
.arg("check")
|
||||
.arg("--all-targets")
|
||||
.arg("--features")
|
||||
.arg("full")
|
||||
.current_dir(&output)
|
||||
.status()
|
||||
.into_diagnostic()?;
|
||||
|
||||
if !status.success() {
|
||||
miette::bail!("`cargo check` exited with status {status}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
pub mod path;
|
||||
|
||||
mod types;
|
||||
|
||||
pub use types::*;
|
||||
@@ -0,0 +1,276 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use miette::SourceSpan;
|
||||
use winnow::{
|
||||
Parser,
|
||||
combinator::eof,
|
||||
error::{ContextError, ParseError},
|
||||
};
|
||||
|
||||
/// Parses a path template, like `/v1/pets/{petId}/toy`.
|
||||
///
|
||||
/// The grammar for path templating is adapted directly from
|
||||
/// https://spec.openapis.org/oas/v3.2.0.html#x4-8-2-path-templating.
|
||||
pub fn parse<'a>(input: &'a str) -> Result<Vec<PathSegment<'a>>, BadPath> {
|
||||
(self::parser::template, eof)
|
||||
.map(|(segments, _)| segments)
|
||||
.parse(input)
|
||||
.map_err(BadPath::from_parse_error)
|
||||
}
|
||||
|
||||
/// A slash-delimited path segment that contains zero or more
|
||||
/// template fragments.
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq)]
|
||||
pub struct PathSegment<'input>(Vec<PathFragment<'input>>);
|
||||
|
||||
impl<'input> PathSegment<'input> {
|
||||
pub fn fragments(&self) -> &[PathFragment<'input>] {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// A fragment within a path segment.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum PathFragment<'input> {
|
||||
/// Literal text.
|
||||
Literal(Cow<'input, str>),
|
||||
/// Template parameter name.
|
||||
Param(&'input str),
|
||||
}
|
||||
|
||||
mod parser {
|
||||
use super::*;
|
||||
|
||||
use winnow::{
|
||||
Parser,
|
||||
combinator::{alt, delimited, repeat},
|
||||
token::take_while,
|
||||
};
|
||||
|
||||
pub fn template<'a>(input: &mut &'a str) -> winnow::Result<Vec<PathSegment<'a>>> {
|
||||
alt((
|
||||
('/', segment, template)
|
||||
.map(|(_, head, tail)| std::iter::once(head).chain(tail).collect()),
|
||||
('/', segment).map(|(_, segment)| vec![segment]),
|
||||
'/'.map(|_| vec![PathSegment::default()]),
|
||||
))
|
||||
.parse_next(input)
|
||||
}
|
||||
|
||||
fn segment<'a>(input: &mut &'a str) -> winnow::Result<PathSegment<'a>> {
|
||||
repeat(1.., fragment).map(PathSegment).parse_next(input)
|
||||
}
|
||||
|
||||
fn fragment<'a>(input: &mut &'a str) -> winnow::Result<PathFragment<'a>> {
|
||||
alt((param, literal)).parse_next(input)
|
||||
}
|
||||
|
||||
pub fn param<'a>(input: &mut &'a str) -> winnow::Result<PathFragment<'a>> {
|
||||
delimited('{', take_while(1.., |c| c != '{' && c != '}'), '}')
|
||||
.map(PathFragment::Param)
|
||||
.parse_next(input)
|
||||
}
|
||||
|
||||
pub fn literal<'a>(input: &mut &'a str) -> winnow::Result<PathFragment<'a>> {
|
||||
take_while(1.., |c| {
|
||||
matches!(c,
|
||||
'A'..='Z' | 'a'..='z' | '0'..='9' |
|
||||
'-' | '.' | '_' | '~' | ':' | '@' |
|
||||
'!' | '$' | '&' | '\'' | '(' | ')' |
|
||||
'*' | '+' | ',' | ';' | '=' | '%'
|
||||
)
|
||||
})
|
||||
.verify_map(|text| {
|
||||
percent_encoding::percent_decode_str(text)
|
||||
.decode_utf8()
|
||||
.ok()
|
||||
.map(PathFragment::Literal)
|
||||
})
|
||||
.parse_next(input)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, miette::Diagnostic, thiserror::Error)]
|
||||
#[error("invalid URL path template")]
|
||||
pub struct BadPath {
|
||||
#[source_code]
|
||||
code: String,
|
||||
#[label]
|
||||
span: SourceSpan,
|
||||
}
|
||||
|
||||
impl BadPath {
|
||||
fn from_parse_error(error: ParseError<&str, ContextError>) -> Self {
|
||||
let input = *error.input();
|
||||
Self {
|
||||
code: input.to_owned(),
|
||||
span: error.char_span().into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_root_path() {
|
||||
let result = parse("/").unwrap();
|
||||
|
||||
assert_eq!(result.len(), 1);
|
||||
assert_eq!(result[0].fragments(), &[]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_simple_literal() {
|
||||
let result = parse("/users").unwrap();
|
||||
|
||||
assert_eq!(result.len(), 1);
|
||||
assert_eq!(
|
||||
result[0].fragments(),
|
||||
&[PathFragment::Literal("users".into())]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_trailing_slash() {
|
||||
let result = parse("/users/").unwrap();
|
||||
|
||||
assert_eq!(result.len(), 2);
|
||||
assert_eq!(
|
||||
result[0].fragments(),
|
||||
&[PathFragment::Literal("users".into())]
|
||||
);
|
||||
assert_eq!(result[1].fragments(), &[]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_simple_template() {
|
||||
let result = parse("/users/{userId}").unwrap();
|
||||
|
||||
assert_eq!(result.len(), 2);
|
||||
assert_eq!(
|
||||
result[0].fragments(),
|
||||
&[PathFragment::Literal("users".into())]
|
||||
);
|
||||
assert_eq!(
|
||||
result[1].fragments(),
|
||||
&[PathFragment::Param("userId".into())]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_nested_path() {
|
||||
let result = parse("/api/v1/resources/{resourceId}").unwrap();
|
||||
|
||||
assert_eq!(result.len(), 4);
|
||||
assert_eq!(
|
||||
result[0].fragments(),
|
||||
&[PathFragment::Literal("api".into())]
|
||||
);
|
||||
assert_eq!(result[1].fragments(), &[PathFragment::Literal("v1".into())]);
|
||||
assert_eq!(
|
||||
result[2].fragments(),
|
||||
&[PathFragment::Literal("resources".into())]
|
||||
);
|
||||
assert_eq!(result[3].fragments(), &[PathFragment::Param("resourceId")]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiple_templates() {
|
||||
let result = parse("/users/{userId}/posts/{postId}").unwrap();
|
||||
|
||||
assert_eq!(result.len(), 4);
|
||||
assert_eq!(
|
||||
result[0].fragments(),
|
||||
&[PathFragment::Literal("users".into())]
|
||||
);
|
||||
assert_eq!(result[1].fragments(), &[PathFragment::Param("userId")]);
|
||||
assert_eq!(
|
||||
result[2].fragments(),
|
||||
&[PathFragment::Literal("posts".into())]
|
||||
);
|
||||
assert_eq!(result[3].fragments(), &[PathFragment::Param("postId")]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_literal_with_extension() {
|
||||
let result =
|
||||
parse("/v1/storage/workspace/{workspace}/documents/download/{documentId}.pdf").unwrap();
|
||||
|
||||
assert_eq!(result.len(), 7);
|
||||
assert_eq!(result[0].fragments(), &[PathFragment::Literal("v1".into())]);
|
||||
assert_eq!(
|
||||
result[1].fragments(),
|
||||
&[PathFragment::Literal("storage".into())]
|
||||
);
|
||||
assert_eq!(
|
||||
result[2].fragments(),
|
||||
&[PathFragment::Literal("workspace".into())]
|
||||
);
|
||||
assert_eq!(result[3].fragments(), &[PathFragment::Param("workspace")]);
|
||||
assert_eq!(
|
||||
result[4].fragments(),
|
||||
&[PathFragment::Literal("documents".into())]
|
||||
);
|
||||
assert_eq!(
|
||||
result[5].fragments(),
|
||||
&[PathFragment::Literal("download".into())]
|
||||
);
|
||||
assert_eq!(
|
||||
result[6].fragments(),
|
||||
&[
|
||||
PathFragment::Param("documentId"),
|
||||
PathFragment::Literal(".pdf".into())
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mixed_literal_and_param() {
|
||||
let result =
|
||||
parse("/v1/storage/workspace/{workspace}/documents/download/report-{documentId}.pdf")
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(result.len(), 7);
|
||||
assert_eq!(result[0].fragments(), &[PathFragment::Literal("v1".into())]);
|
||||
assert_eq!(
|
||||
result[1].fragments(),
|
||||
&[PathFragment::Literal("storage".into())]
|
||||
);
|
||||
assert_eq!(
|
||||
result[2].fragments(),
|
||||
&[PathFragment::Literal("workspace".into())]
|
||||
);
|
||||
assert_eq!(result[3].fragments(), &[PathFragment::Param("workspace")]);
|
||||
assert_eq!(
|
||||
result[4].fragments(),
|
||||
&[PathFragment::Literal("documents".into())]
|
||||
);
|
||||
assert_eq!(
|
||||
result[5].fragments(),
|
||||
&[PathFragment::Literal("download".into())]
|
||||
);
|
||||
assert_eq!(
|
||||
result[6].fragments(),
|
||||
&[
|
||||
PathFragment::Literal("report-".into()),
|
||||
PathFragment::Param("documentId"),
|
||||
PathFragment::Literal(".pdf".into())
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_double_slash() {
|
||||
// Empty path segments aren't allowed.
|
||||
assert!(parse("/users//a").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_chars_in_template() {
|
||||
// Parameter names can contain any character except for
|
||||
// `{` and `}`, per the `template-expression-param-name` terminal.
|
||||
assert!(parse("/users/{user/{id}}").is_err());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,328 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use indexmap::IndexMap;
|
||||
use itertools::Itertools;
|
||||
use serde::{Deserialize, Deserializer};
|
||||
|
||||
use crate::error::SerdeError;
|
||||
|
||||
/// An OpenAPI document.
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Document {
|
||||
pub openapi: String,
|
||||
pub info: Info,
|
||||
#[serde(default)]
|
||||
pub paths: IndexMap<String, PathItem>,
|
||||
#[serde(default)]
|
||||
pub components: Option<Components>,
|
||||
}
|
||||
|
||||
impl Document {
|
||||
/// Parse an OpenAPI document from a YAML or JSON string.
|
||||
pub fn from_yaml(yaml: &str) -> Result<Self, SerdeError> {
|
||||
let deserializer = serde_yaml::Deserializer::from_str(yaml);
|
||||
let result = serde_path_to_error::deserialize(deserializer)?;
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct Info {
|
||||
pub title: String,
|
||||
#[serde(default)]
|
||||
pub description: Option<String>,
|
||||
pub version: String,
|
||||
}
|
||||
|
||||
/// Operation definitions for a single path.
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct PathItem {
|
||||
#[serde(default)]
|
||||
pub get: Option<Operation>,
|
||||
#[serde(default)]
|
||||
pub post: Option<Operation>,
|
||||
#[serde(default)]
|
||||
pub put: Option<Operation>,
|
||||
#[serde(default)]
|
||||
pub delete: Option<Operation>,
|
||||
#[serde(default)]
|
||||
pub patch: Option<Operation>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
pub enum Method {
|
||||
Get,
|
||||
Post,
|
||||
Put,
|
||||
Delete,
|
||||
Patch,
|
||||
}
|
||||
|
||||
impl PathItem {
|
||||
/// Yields all operations and their HTTP methods.
|
||||
pub fn operations(&self) -> impl Iterator<Item = (Method, &Operation)> {
|
||||
[
|
||||
(Method::Get, self.get.as_ref()),
|
||||
(Method::Post, self.post.as_ref()),
|
||||
(Method::Put, self.put.as_ref()),
|
||||
(Method::Delete, self.delete.as_ref()),
|
||||
(Method::Patch, self.patch.as_ref()),
|
||||
]
|
||||
.into_iter()
|
||||
.filter_map(|(method, op)| op.map(|o| (method, o)))
|
||||
}
|
||||
}
|
||||
|
||||
/// An HTTP operation.
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Operation {
|
||||
#[serde(default)]
|
||||
pub description: Option<String>,
|
||||
pub operation_id: Option<String>,
|
||||
#[serde(default)]
|
||||
pub parameters: Vec<Parameter>,
|
||||
#[serde(default)]
|
||||
pub request_body: Option<RequestBody>,
|
||||
#[serde(default)]
|
||||
pub responses: IndexMap<String, Response>,
|
||||
#[serde(flatten)]
|
||||
pub extensions: IndexMap<String, serde_json::Value>,
|
||||
}
|
||||
|
||||
impl Operation {
|
||||
pub fn extension(&self, name: &str) -> Option<&str> {
|
||||
self.extensions.get(name)?.as_str()
|
||||
}
|
||||
}
|
||||
|
||||
/// A path, query, header, or cookie parameter.
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Parameter {
|
||||
pub name: String,
|
||||
#[serde(rename = "in")]
|
||||
pub location: ParameterLocation,
|
||||
#[serde(default)]
|
||||
pub required: bool,
|
||||
#[serde(default)]
|
||||
pub description: Option<String>,
|
||||
#[serde(default)]
|
||||
pub schema: Option<RefOrSchema>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum ParameterLocation {
|
||||
Path,
|
||||
Query,
|
||||
Header,
|
||||
Cookie,
|
||||
}
|
||||
|
||||
/// Request body definition.
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct RequestBody {
|
||||
#[serde(default)]
|
||||
pub description: Option<String>,
|
||||
#[serde(default)]
|
||||
pub required: bool,
|
||||
#[serde(default)]
|
||||
pub content: IndexMap<String, MediaType>,
|
||||
}
|
||||
|
||||
/// Response definition.
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Response {
|
||||
#[serde(default)]
|
||||
pub description: Option<String>,
|
||||
#[serde(default)]
|
||||
pub content: Option<IndexMap<String, MediaType>>,
|
||||
}
|
||||
|
||||
/// Media type content.
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct MediaType {
|
||||
#[serde(default)]
|
||||
pub schema: Option<RefOrSchema>,
|
||||
}
|
||||
|
||||
/// Components section containing reusable schemas.
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Components {
|
||||
#[serde(default)]
|
||||
pub schemas: IndexMap<String, Schema>,
|
||||
}
|
||||
|
||||
/// Either a reference or a schema definition.
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum RefOrSchema {
|
||||
Ref(Ref),
|
||||
Schema(Box<Schema>),
|
||||
}
|
||||
|
||||
/// A reference to another schema.
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct Ref {
|
||||
#[serde(rename = "$ref")]
|
||||
pub path: SchemaRefPath,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum Ty {
|
||||
String,
|
||||
Integer,
|
||||
Number,
|
||||
Boolean,
|
||||
Array,
|
||||
Object,
|
||||
Null,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum Format {
|
||||
#[serde(rename = "date-time")]
|
||||
DateTime,
|
||||
Date,
|
||||
Uri,
|
||||
Uuid,
|
||||
Byte,
|
||||
Binary,
|
||||
Int32,
|
||||
Int64,
|
||||
Float,
|
||||
Double,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum AdditionalProperties {
|
||||
Bool(bool),
|
||||
RefOrSchema(RefOrSchema),
|
||||
}
|
||||
|
||||
/// An OpenAPI schema definition.
|
||||
#[derive(Debug, Clone, Default, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Schema {
|
||||
#[serde(rename = "type", default, deserialize_with = "deserialize_type")]
|
||||
pub ty: Vec<Ty>,
|
||||
#[serde(default)]
|
||||
pub format: Option<Format>,
|
||||
#[serde(default)]
|
||||
pub description: Option<String>,
|
||||
#[serde(default)]
|
||||
pub nullable: bool,
|
||||
|
||||
// Object properties.
|
||||
#[serde(default)]
|
||||
pub properties: Option<IndexMap<String, RefOrSchema>>,
|
||||
#[serde(default)]
|
||||
pub required: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub additional_properties: Option<AdditionalProperties>,
|
||||
|
||||
// Array items.
|
||||
#[serde(default)]
|
||||
pub items: Option<RefOrSchema>,
|
||||
|
||||
// Enum variants.
|
||||
#[serde(rename = "enum", default)]
|
||||
pub variants: Option<Vec<serde_json::Value>>,
|
||||
|
||||
// Composition.
|
||||
#[serde(default)]
|
||||
pub all_of: Option<Vec<RefOrSchema>>,
|
||||
#[serde(default)]
|
||||
pub one_of: Option<Vec<RefOrSchema>>,
|
||||
#[serde(default)]
|
||||
pub any_of: Option<Vec<RefOrSchema>>,
|
||||
#[serde(default)]
|
||||
pub discriminator: Option<Discriminator>,
|
||||
}
|
||||
|
||||
/// A discriminator for a polymorphic type.
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Discriminator {
|
||||
pub property_name: String,
|
||||
#[serde(default)]
|
||||
pub mapping: IndexMap<String, SchemaRefPath>,
|
||||
}
|
||||
|
||||
/// The path of a schema reference.
|
||||
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
|
||||
pub struct SchemaRefPath(String);
|
||||
|
||||
impl SchemaRefPath {
|
||||
pub fn as_str(&self) -> &str {
|
||||
self.0.as_str()
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for SchemaRefPath {
|
||||
type Err = BadSchemaRefPath;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
// A makeshift JSON Schema reference parser (<URI> # <JSON-Pointer>)
|
||||
// that only understands references to keys under `/components/schemas`
|
||||
// in the current document.
|
||||
let Some(pointer) = s
|
||||
.trim_matches(|c| c <= ' ')
|
||||
.strip_prefix('#')
|
||||
.map(|rest| &rest[..rest.find(['\t', '\n', '\r']).unwrap_or(rest.len())])
|
||||
else {
|
||||
return Err(BadSchemaRefPath);
|
||||
};
|
||||
if !pointer.starts_with('/') {
|
||||
return Err(BadSchemaRefPath);
|
||||
}
|
||||
let mut parts = pointer.split('/').skip(1);
|
||||
let Some(["components", "schemas", name]) = parts.next_array() else {
|
||||
return Err(BadSchemaRefPath);
|
||||
};
|
||||
if parts.next().is_some() {
|
||||
return Err(BadSchemaRefPath);
|
||||
}
|
||||
Ok(Self(name.replace("~1", "/").replace("~0", "~")))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for SchemaRefPath {
|
||||
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
|
||||
struct Visitor;
|
||||
impl<'de> serde::de::Visitor<'de> for Visitor {
|
||||
type Value = SchemaRefPath;
|
||||
|
||||
fn expecting(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
|
||||
f.write_str("a schema reference")
|
||||
}
|
||||
|
||||
fn visit_str<E: ::serde::de::Error>(self, s: &str) -> Result<Self::Value, E> {
|
||||
s.parse().map_err(E::custom)
|
||||
}
|
||||
}
|
||||
deserializer.deserialize_str(Visitor)
|
||||
}
|
||||
}
|
||||
|
||||
fn deserialize_type<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Vec<Ty>, D::Error> {
|
||||
#[derive(Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum TypesOr {
|
||||
/// An OpenAPI 3.1-style `type` array.
|
||||
Types(Vec<Ty>),
|
||||
/// A single `type`.
|
||||
Type(Ty),
|
||||
}
|
||||
Ok(match TypesOr::deserialize(deserializer)? {
|
||||
TypesOr::Types(types) => types,
|
||||
TypesOr::Type(ty) => vec![ty],
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[error("only `#/components/schemas/{{name}}` references are supported")]
|
||||
pub struct BadSchemaRefPath;
|
||||
Reference in New Issue
Block a user