diff --git a/Cargo.lock b/Cargo.lock index 682b99e..fde979a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,90 +2,12 @@ # It is not intended for manual editing. version = 3 -[[package]] -name = "Inflector" -version = "0.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" -dependencies = [ - "lazy_static", - "regex", -] - -[[package]] -name = "aho-corasick" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" -dependencies = [ - "memchr", -] - -[[package]] -name = "ansi_term" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" -dependencies = [ - "winapi", -] - [[package]] name = "anyhow" version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" -[[package]] -name = "ast_node" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ab31376d309dd3bfc9cfb3c11c93ce0e0741bbe0354b20e7f8c60b044730b79" -dependencies = [ - "proc-macro2", - "quote", - "swc_macros_common", - "syn 2.0.48", -] - -[[package]] -name = "atty" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" -dependencies = [ - "hermit-abi", - "libc", - "winapi", -] - -[[package]] -name = "autocfg" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" - -[[package]] -name = "better_scoped_tls" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "794edcc9b3fb07bb4aecaa11f093fd45663b4feadb782d68303a2268bc2701de" -dependencies = [ - "scoped-tls", -] - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "bitflags" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" - [[package]] name = "camino" version = "1.1.6" @@ -134,44 +56,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] -name = "clap" -version = "2.34.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" -dependencies = [ - "ansi_term", - "atty", - "bitflags 1.3.2", - "strsim", - "textwrap", - "unicode-width", - "vec_map", -] - -[[package]] -name = "either" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dca9240753cf90908d7e4aac30f630662b02aebaa1b58a3cadabdb23385b58b" - -[[package]] -name = "form_urlencoded" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" -dependencies = [ - "percent-encoding", -] - -[[package]] -name = "from_variant" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdc9cc75639b041067353b9bce2450d6847e547276c6fbe4487d7407980e07db" +name = "from-schema" +version = "0.6.0" dependencies = [ - "proc-macro2", - "swc_macros_common", - "syn 2.0.48", + "github-webhook-type-generator", + "serde", + "serde_json", + "serde_path_to_error", ] [[package]] @@ -210,66 +101,11 @@ dependencies = [ name = "github-webhook-type-generator" version = "0.6.0" dependencies = [ - "once_cell", "proc-macro2", "quote", - "structopt", - "swc_common", - "swc_ecma_ast", - "swc_ecma_parser", -] - -[[package]] -name = "heck" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" -dependencies = [ - "unicode-segmentation", -] - -[[package]] -name = "hermit-abi" -version = "0.1.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" -dependencies = [ - "libc", -] - -[[package]] -name = "hstr" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17fafeca18cf0927e23ea44d7a5189c10536279dfe9094e0dfa953053fbb5377" -dependencies = [ - "new_debug_unreachable", - "once_cell", - "phf", "rustc-hash", "smallvec", -] - -[[package]] -name = "idna" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" -dependencies = [ - "unicode-bidi", - "unicode-normalization", -] - -[[package]] -name = "is-macro" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59a85abdc13717906baccb5a1e435556ce0df215f242892f721dff62bf25288f" -dependencies = [ - "Inflector", - "proc-macro2", - "quote", - "syn 2.0.48", + "typed-arena", ] [[package]] @@ -278,12 +114,6 @@ version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" -[[package]] -name = "lazy_static" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" - [[package]] name = "libc" version = "0.2.152" @@ -296,12 +126,6 @@ version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" -[[package]] -name = "memchr" -version = "2.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" - [[package]] name = "minreq" version = "2.11.1" @@ -315,127 +139,12 @@ dependencies = [ "webpki-roots", ] -[[package]] -name = "new_debug_unreachable" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" - -[[package]] -name = "num-bigint" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" -dependencies = [ - "autocfg", - "num-integer", - "num-traits", - "serde", -] - -[[package]] -name = "num-integer" -version = "0.1.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" -dependencies = [ - "autocfg", - "num-traits", -] - -[[package]] -name = "num-traits" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" -dependencies = [ - "autocfg", -] - [[package]] name = "once_cell" version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" -[[package]] -name = "percent-encoding" -version = "2.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" - -[[package]] -name = "phf" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" -dependencies = [ - "phf_macros", - "phf_shared", -] - -[[package]] -name = "phf_generator" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" -dependencies = [ - "phf_shared", - "rand", -] - -[[package]] -name = "phf_macros" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b" -dependencies = [ - "phf_generator", - "phf_shared", - "proc-macro2", - "quote", - "syn 2.0.48", -] - -[[package]] -name = "phf_shared" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" -dependencies = [ - "siphasher", -] - -[[package]] -name = "pin-project-lite" -version = "0.2.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" - -[[package]] -name = "proc-macro-error" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" -dependencies = [ - "proc-macro-error-attr", - "proc-macro2", - "quote", - "syn 1.0.109", - "version_check", -] - -[[package]] -name = "proc-macro-error-attr" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" -dependencies = [ - "proc-macro2", - "quote", - "version_check", -] - [[package]] name = "proc-macro2" version = "1.0.86" @@ -445,15 +154,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "psm" -version = "0.1.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5787f7cda34e3033a72192c018bc5883100330f362ef279a8cbccfce8bb4e874" -dependencies = [ - "cc", -] - [[package]] name = "quote" version = "1.0.36" @@ -463,50 +163,6 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "rand_core", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" - -[[package]] -name = "regex" -version = "1.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" - [[package]] name = "ring" version = "0.17.8" @@ -524,9 +180,9 @@ dependencies = [ [[package]] name = "rustc-hash" -version = "1.1.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" [[package]] name = "rustls" @@ -556,12 +212,6 @@ version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" -[[package]] -name = "scoped-tls" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" - [[package]] name = "sct" version = "0.7.1" @@ -598,7 +248,7 @@ checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn", ] [[package]] @@ -613,27 +263,20 @@ dependencies = [ ] [[package]] -name = "siphasher" -version = "0.3.11" +name = "serde_path_to_error" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" +checksum = "59fab13f937fa393d08645bf3a84bdfe86e296747b506ada67bb15f10f218b2a" +dependencies = [ + "itoa", + "serde", +] [[package]] name = "smallvec" -version = "1.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" - -[[package]] -name = "smartstring" -version = "1.0.1" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fb72c633efbaa2dd666986505016c32c3044395ceaf881518399d2f4127ee29" -dependencies = [ - "autocfg", - "static_assertions", - "version_check", -] +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "spin" @@ -641,202 +284,6 @@ version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" -[[package]] -name = "stacker" -version = "0.1.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c886bd4480155fd3ef527d45e9ac8dd7118a898a46530b7b94c3e21866259fce" -dependencies = [ - "cc", - "cfg-if", - "libc", - "psm", - "winapi", -] - -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - -[[package]] -name = "string_enum" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05e383308aebc257e7d7920224fa055c632478d92744eca77f99be8fa1545b90" -dependencies = [ - "proc-macro2", - "quote", - "swc_macros_common", - "syn 2.0.48", -] - -[[package]] -name = "strsim" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" - -[[package]] -name = "structopt" -version = "0.3.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c6b5c64445ba8094a6ab0c3cd2ad323e07171012d9c98b0b15651daf1787a10" -dependencies = [ - "clap", - "lazy_static", - "structopt-derive", -] - -[[package]] -name = "structopt-derive" -version = "0.4.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcb5ae327f9cc13b68763b5749770cb9e048a99bd9dfdfa58d0cf05d5f64afe0" -dependencies = [ - "heck", - "proc-macro-error", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "swc_atoms" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d538eaaa6f085161d088a04cf0a3a5a52c5a7f2b3bd9b83f73f058b0ed357c0" -dependencies = [ - "hstr", - "once_cell", - "rustc-hash", - "serde", -] - -[[package]] -name = "swc_common" -version = "0.34.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b0d7bcbd9faf61cec1a552cbdaec57faefbb10be7cc5f959613c6f91b5a9254" -dependencies = [ - "ast_node", - "atty", - "better_scoped_tls", - "cfg-if", - "either", - "from_variant", - "new_debug_unreachable", - "num-bigint", - "once_cell", - "rustc-hash", - "serde", - "siphasher", - "swc_atoms", - "swc_eq_ignore_macros", - "swc_visit", - "termcolor", - "tracing", - "unicode-width", - "url", -] - -[[package]] -name = "swc_ecma_ast" -version = "0.115.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7be1306930c235435a892104c00c2b5e16231043c085d5a10bd3e7537b15659b" -dependencies = [ - "bitflags 2.5.0", - "is-macro", - "num-bigint", - "phf", - "scoped-tls", - "string_enum", - "swc_atoms", - "swc_common", - "unicode-id-start", -] - -[[package]] -name = "swc_ecma_parser" -version = "0.146.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "169cd7a18ed5e73346f38d5a6123c6427f04b9dffe5cce468e8d70530cc4adcb" -dependencies = [ - "either", - "memchr", - "new_debug_unreachable", - "num-bigint", - "num-traits", - "phf", - "serde", - "smallvec", - "smartstring", - "stacker", - "swc_atoms", - "swc_common", - "swc_ecma_ast", - "tracing", - "typed-arena", -] - -[[package]] -name = "swc_eq_ignore_macros" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "695a1d8b461033d32429b5befbf0ad4d7a2c4d6ba9cd5ba4e0645c615839e8e4" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.48", -] - -[[package]] -name = "swc_macros_common" -version = "0.3.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91745f3561057493d2da768437c427c0e979dff7396507ae02f16c981c4a8466" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.48", -] - -[[package]] -name = "swc_visit" -version = "0.5.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "043d11fe683dcb934583ead49405c0896a5af5face522e4682c16971ef7871b9" -dependencies = [ - "either", - "swc_visit_macros", -] - -[[package]] -name = "swc_visit_macros" -version = "0.5.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ae9ef18ff8daffa999f729db056d2821cd2f790f3a11e46422d19f46bb193e7" -dependencies = [ - "Inflector", - "proc-macro2", - "quote", - "swc_macros_common", - "syn 2.0.48", -] - -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - [[package]] name = "syn" version = "2.0.48" @@ -848,24 +295,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "termcolor" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "textwrap" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" -dependencies = [ - "unicode-width", -] - [[package]] name = "thiserror" version = "1.0.56" @@ -883,53 +312,7 @@ checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", -] - -[[package]] -name = "tinyvec" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - -[[package]] -name = "tracing" -version = "0.1.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" -dependencies = [ - "pin-project-lite", - "tracing-attributes", - "tracing-core", -] - -[[package]] -name = "tracing-attributes" -version = "0.1.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.48", -] - -[[package]] -name = "tracing-core" -version = "0.1.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" -dependencies = [ - "once_cell", + "syn", ] [[package]] @@ -938,74 +321,18 @@ version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a" -[[package]] -name = "unicode-bidi" -version = "0.3.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f2528f27a9eb2b21e69c95319b30bd0efd85d09c379741b0f78ea1d86be2416" - -[[package]] -name = "unicode-id-start" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02aebfa694eccbbbffdd92922c7de136b9fe764396d2f10e21bce1681477cfc1" - [[package]] name = "unicode-ident" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" -[[package]] -name = "unicode-normalization" -version = "0.1.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" -dependencies = [ - "tinyvec", -] - -[[package]] -name = "unicode-segmentation" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" - -[[package]] -name = "unicode-width" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" - [[package]] name = "untrusted" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" -[[package]] -name = "url" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", -] - -[[package]] -name = "vec_map" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" - -[[package]] -name = "version_check" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" - [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -1018,37 +345,6 @@ version = "0.25.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-util" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" -dependencies = [ - "winapi", -] - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - [[package]] name = "windows-sys" version = "0.52.0" diff --git a/Cargo.toml b/Cargo.toml index 79edbdc..8ebd370 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,8 @@ resolver = "2" members = [ "dts-downloader", "type-generator", - "github-webhook" + "github-webhook", + "frontend/from-schema", ] [workspace.package] diff --git a/frontend/from-schema/.gitignore b/frontend/from-schema/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/frontend/from-schema/.gitignore @@ -0,0 +1 @@ +/target diff --git a/frontend/from-schema/Cargo.toml b/frontend/from-schema/Cargo.toml new file mode 100644 index 0000000..d8dc004 --- /dev/null +++ b/frontend/from-schema/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "from-schema" +edition = "2021" +version.workspace = true +repository.workspace = true +authors.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dev-dependencies] +serde_path_to_error = "0.1.17" + +[dependencies] +serde = { version = "1.0.195", features = ["derive"] } +serde_json = "1.0.111" + +github-webhook-type-generator.workspace = true diff --git a/frontend/from-schema/src/jsonschema.rs b/frontend/from-schema/src/jsonschema.rs new file mode 100644 index 0000000..cf9ab00 --- /dev/null +++ b/frontend/from-schema/src/jsonschema.rs @@ -0,0 +1,538 @@ +//! Abstract Syntax Tree (AST) for JSON Schema Draft-07 definitions, +//! tailored for GitHub Webhook schema consumption. +//! +//! This module provides deserializable Rust structures representing +//! the structural elements of a subset of JSON Schema used in GitHub Webhooks. + +use serde::{de::IgnoredAny, Deserialize}; +use std::{borrow::Cow, collections::HashMap}; + +/// The top-level structure for a JSON Schema document. +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +#[serde(deny_unknown_fields)] +pub struct JsonSchema<'a> { + /// The `$schema` version identifier (e.g., `http://json-schema.org/draft-07/schema#`). + #[serde(rename = "$schema")] + pub schema: Option, + + #[serde(borrow)] + /// A map of schema definitions under `definitions`. + pub definitions: HashMap<&'a str, PropertySchema<'a>>, + + /// The root schema definition. Indicates that a payload type can be one of the specified schemas. + pub one_of: Vec>, +} + +/// A property schema. +#[derive(Debug, Deserialize)] +pub struct PropertySchema<'a> { + /// The `$schema` version identifier (e.g., `http://json-schema.org/draft-07/schema#`). + #[serde(rename = "$schema")] + pub schema: Option, + + #[serde(borrow)] + /// Optional title for the schema. + pub title: Option>, + + /// Optional description for the schema. + pub description: Option>, + + #[serde(flatten)] + /// The content of the schema, which can be an object, string, integer, etc. + pub content: SchemaDefinition<'a>, +} + +/// A schema definition, which can be a reference, enum, or primitive. +#[derive(Debug)] +pub enum SchemaDefinition<'a> { + /// A reference to another definition. + Ref(RefSchema<'a>), + + /// A `oneOf` schema that contains multiple alternative schemas. + OneOf(OneOfSchema<'a>), + + /// An `allOf` schema that represents an intersection of multiple schemas. + AllOf(AllOfSchema<'a>), + + /// A schema for an object type. describes the structure of an object. + Object(Nullable>), + + /// A schema for a string type, which may include an enum or format. + String(Nullable>), + + /// A schema for an integer type, which may include a constant value. + Integer(Nullable), + + /// A schema for a number type. + Number(Nullable), + + /// A schema for a boolean type, which may include a constant value. + Boolean(Nullable), + + /// A schema for an array type, which describes the items in the array. + Array(Nullable>), + + /// A null schema, which indicates that the property is `null`. + Null, +} + +/// A `$ref` to another schema definition. +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct RefSchema<'a> { + /// The `$ref` path (usually internal). + #[serde(rename = "$ref")] + pub ref_path: &'a str, +} + +/// A `oneOf` variant representing multiple alternative `$ref`s. +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +#[serde(deny_unknown_fields)] +pub struct OneOfSchema<'a> { + #[serde(borrow)] + /// An array of schema references. + pub one_of: Vec>, +} + +/// A `allOf` variant representing an intersection of multiple schemas. +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +#[serde(deny_unknown_fields)] +pub struct AllOfSchema<'a> { + #[serde(borrow)] + /// An intersection of schemas. + pub all_of: Intersection<'a>, +} + +/// An restricted form of intersection schema. +/// +/// Only recognizes the form of `[{"$ref": "#/..", ..}, {"type": "object", ..}]`. +#[derive(Debug)] +pub struct Intersection<'a> { + /// The base schema that defines the core structure. + pub ref_schema: RefSchema<'a>, + + /// An extension schema that adds additional properties or constraints. + /// + /// We ignore `"tsAdditionalProperties": false` in the schema. + pub extension: ObjectSchema<'a>, +} + +impl<'de: 'a, 'a> Deserialize<'de> for Intersection<'a> { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + #[derive(Deserialize)] + #[serde(expecting = "an intersection schema with a base schema and an extension")] + /// Parses `[{"$ref": "#/..", ..}, {"type": "object", ..}]`. + struct IntersectionParser<'a>(#[serde(borrow)] (RefSchema<'a>, ObjectSchema<'a>)); + + let IntersectionParser((ref_schema, extension)) = + IntersectionParser::deserialize(deserializer)?; + Ok(Self { + ref_schema, + extension, + }) + } +} + +/// A schema that can be `null`. +/// +/// Note that this struct is not [`Deserialize`]. This is a thin wrapper around +/// the content schema. +#[derive(Debug)] +pub struct Nullable { + /// Whether this schema can be `null`. + pub nullable: bool, + + /// The content of the schema, which can be an object, string, integer, etc. + pub content: Content, +} + +impl Nullable { + pub fn new(nullable: bool, content: Content) -> Self { + Self { nullable, content } + } +} + +/// A schema representing a JSON object structure. +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ObjectSchema<'a> { + /// Optional `$schema` version identifier. We will ignore this field. + #[serde(rename = "$schema")] + pub schema: Option<&'a str>, + + /// Optional schema title. + pub title: Option>, + + /// Field/property definitions. + #[serde(default)] + pub properties: HashMap<&'a str, PropertySchema<'a>>, + + /// List of required field names. + #[serde(default)] + pub required: Vec<&'a str>, + + /// Whether additional properties are allowed. + #[serde(default)] + pub additional_properties: AdditionalProperties<'a>, +} + +/// Additional properties allowed in the object schema. +#[derive(Debug, Deserialize)] +#[serde(untagged)] +pub enum AdditionalProperties<'a> { + /// Whether additional properties are allowed. + /// If `true`, any additional properties are allowed. + Boolean(bool), + /// Additional properties are allowed with a specific type. + Type(#[serde(borrow)] Box>), +} + +impl Default for AdditionalProperties<'_> { + fn default() -> Self { + // By default, additional properties are not allowed. + AdditionalProperties::Boolean(false) + } +} + +/// Manual `Deserialize` implementation for `SchemaDefinition`. +/// +/// This is the primal part of the deserialization logic. +/// Mainly, +impl<'de: 'a, 'a> Deserialize<'de> for SchemaDefinition<'a> { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + // We use serde's internal implementation of untagged enum deserialization + // to handle the various schema types. + + // Justification for using private API: + // We need to use "untagged" functionality to deserialize schema definitions + // that can be either a reference, oneOf, or a primitive schema. + // The public API does offer this functionality, but it reports meaningless errors + // because it does not know about the enum variants. + // The former three variants are easily deserialized, but the latter + // requires a custom deserializer to handle the internally tagged enum, so + // their error messages are precious for debugging. + let content = ::deserialize(deserializer)?; + let deserializer = serde::__private::de::ContentRefDeserializer::::new(&content); + if let Ok(ok) = Result::map( + ::deserialize(deserializer), + SchemaDefinition::Ref, + ) { + return Ok(ok); + } + if let Ok(ok) = Result::map( + ::deserialize(deserializer), + SchemaDefinition::OneOf, + ) { + return Ok(ok); + } + if let Ok(ok) = Result::map( + ::deserialize(deserializer), + SchemaDefinition::AllOf, + ) { + return Ok(ok); + } + + // Below, we use serde's internal implementation of internally tagged enum deserialization + // with some modifications to handle the custom tag value. + enum Field { + NullableOrNot { + nonnull: NonNullKind, + nullable: bool, + }, + Null, + } + #[derive(Deserialize)] + #[serde(rename_all = "camelCase")] + enum NonNullKind { + Object, + String, + Integer, + Number, + Boolean, + Array, + } + + // Modification #1: + // we use `FieldParser` for parsing the `Field` instead of `FieldVisitor`. + #[derive(Deserialize)] + enum NullType { + #[serde(rename = "null")] + Null, + } + #[derive(Deserialize)] + #[serde(untagged)] + enum FieldParser { + /// Parses a string array. + Array2((NonNullKind, NullType)), + + /// Also parses a string array. + Array1((NullType,)), + + /// Parses a single string. + String(NonNullKind), + + /// Also parses a single string. + Null(NullType), + } + impl<'de> Deserialize<'de> for Field { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + Ok(match FieldParser::deserialize(deserializer)? { + FieldParser::Array2((non_null_kind, _null_type)) => Self::NullableOrNot { + nonnull: non_null_kind, + nullable: true, + }, + FieldParser::String(non_null_kind) => Self::NullableOrNot { + nonnull: non_null_kind, + nullable: false, + }, + FieldParser::Array1(_null_type) => Self::Null, + FieldParser::Null(_null_type) => Self::Null, + }) + } + } + // Justification for using private API: + // we need to deserialize an internally tagged enum with a custom tag value. + // This is not directly supported by serde's public API, so we use the private API. + let (tag, content) = serde::de::Deserializer::deserialize_any( + deserializer, + serde::__private::de::TaggedContentVisitor::::new( + "type", + "internally tagged enum Message'", + ), + )?; + let deserializer = serde::__private::de::ContentDeserializer::new(content); + match tag { + Field::NullableOrNot { nonnull, nullable } => { + // Modification #2: + // parse the structure based on the non-null kind + match nonnull { + NonNullKind::Object => { + let value = ObjectSchema::deserialize(deserializer)?; + Ok(SchemaDefinition::Object(Nullable::new(nullable, value))) + } + NonNullKind::String => { + let value = StringSchema::deserialize(deserializer)?; + Ok(SchemaDefinition::String(Nullable::new(nullable, value))) + } + NonNullKind::Integer => { + let value = IntegerSchema::deserialize(deserializer)?; + Ok(SchemaDefinition::Integer(Nullable::new(nullable, value))) + } + NonNullKind::Number => { + let value = NumberSchema::deserialize(deserializer)?; + Ok(SchemaDefinition::Number(Nullable::new(nullable, value))) + } + NonNullKind::Boolean => { + let value = BooleanSchema::deserialize(deserializer)?; + Ok(SchemaDefinition::Boolean(Nullable::new(nullable, value))) + } + NonNullKind::Array => { + let value = ArraySchema::deserialize(deserializer)?; + Ok(SchemaDefinition::Array(Nullable::new(nullable, value))) + } + } + } + Field::Null => Ok(SchemaDefinition::Null), + } + } +} + +#[derive(Debug, Deserialize)] +#[serde(untagged)] +pub enum StringSchema<'a> { + Enum { + /// Possible values for this string. Typically this is a tag like `action`. + /// + /// The content of this field is an option of a string so that we can handle + /// nullable strings with this type. It makes this type to adhere to the + /// [`Nullable`] type, but it can utilize null-pointer optimization. + #[serde(rename = "enum")] + #[serde(borrow)] + enum_values: Vec>, + }, + Format { + /// The format of the string, e.g., "date-time", "uri". + format: StringFormat, + }, + + /// A generic string. This is used when no specific format or enum is defined. + /// + /// This field is equivalent to `()`, but serde distinguishes it from empty struct variants. + Generic {}, +} + +/// The format of a string, which can be one of several predefined formats. +#[derive(Debug, Deserialize)] +pub enum StringFormat { + /// A date-time string in ISO 8601 format. + #[serde(rename = "date-time")] + DateTime, + + /// A URI string. + #[serde(rename = "uri")] + Uri, + + /// A UUID string. + #[serde(rename = "uuid")] + Uuid, +} + +/// A number schema, which can be used for both integers and floats. +#[derive(Debug, Deserialize)] +pub struct NumberSchema {} + +/// An integer schema, which may include a constant value. +#[derive(Debug, Deserialize)] +pub struct IntegerSchema { + /// Constant value for this integer. + #[serde(rename = "const")] + pub constant: Option, +} + +/// A boolean schema, which may include a constant value. +#[derive(Debug)] +pub struct BooleanSchema { + /// Constant value for this boolean. + pub constant: Option, +} + +/// A schema for an array, which describes the items in the array. +#[derive(Debug, Deserialize)] +pub struct ArraySchema<'a> { + #[serde(borrow)] + /// The type of items in the array. + pub items: ArrayType<'a>, +} + +#[derive(Debug, Deserialize)] +#[serde(untagged)] +pub enum ArrayType<'a> { + #[serde(borrow)] + /// Concrete type for items in the array. + Specified(Box>), + + /// A schema that allows any type of items in the array. There is a corner case + /// that uses this variant: `"rubygems_metadata": { "type": "array", "items": {} }`. + Any {}, +} + +/// Manual `Deserialize` implementation for `BooleanSchema`. +/// +/// This is necessary because the `const` and `enum` fields +/// can be used interchangeably in the JSON Schema. +impl<'de> Deserialize<'de> for BooleanSchema { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + #[derive(Deserialize)] + #[serde(untagged)] + enum BooleanSchemaParser { + Const { + #[serde(rename = "const")] + constant: Option, + }, + Enum { + #[serde(rename = "enum")] + enum_value: [bool; 1], + }, + } + + let internal = BooleanSchemaParser::deserialize(deserializer)?; + Ok(match internal { + BooleanSchemaParser::Const { constant } => BooleanSchema { constant }, + BooleanSchemaParser::Enum { enum_value } => BooleanSchema { + constant: Some(enum_value[0]), + }, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_object() { + let json = r##"{ + "$schema": "http://json-schema.org/draft-07/schema", + "type": "object", + "description": "Activity related to a branch protection rule. For more information, see \"[About branch protection rules](https://docs.github.com/en/github/administering-a-repository/defining-the-mergeability-of-pull-requests/about-protected-branches#about-branch-protection-rules).\"", + "required": ["action", "rule", "repository", "sender"], + "properties": { + "action": { "type": "string", "enum": ["created"] }, + "rule": { "$ref": "#/definitions/branch-protection-rule" }, + "repository": { "$ref": "#/definitions/repository" }, + "sender": { "$ref": "#/definitions/user" }, + "installation": { "$ref": "#/definitions/installation-lite" }, + "organization": { "$ref": "#/definitions/organization" } + }, + "title": "branch protection rule created event", + "additionalProperties": false +}"##; + + let deser = &mut serde_json::Deserializer::from_str(json); + let _schema: PropertySchema = + serde_path_to_error::deserialize(deser).expect("Failed to parse JSON Schema"); + } + + #[test] + fn test_parse_string() { + let json = r##"{ "type": "string" }"##; + + let deser = &mut serde_json::Deserializer::from_str(json); + let _schema: PropertySchema = + serde_path_to_error::deserialize(deser).expect("Failed to parse JSON Schema"); + } + + #[test] + fn test_parse_nullable_string() { + let json = r##"{ "type": ["string", "null"] }"##; + + let deser = &mut serde_json::Deserializer::from_str(json); + let _schema: SchemaDefinition = + serde_path_to_error::deserialize(deser).expect("Failed to parse JSON Schema"); + } + + #[test] + fn test_parse_allof() { + let json = r##"{ + "allOf": [ + { "$ref": "#/definitions/alert-instance" }, + { + "type": "object", + "required": ["state"], + "properties": { + "state": { "type": "string", "enum": ["dismissed"] } + }, + "tsAdditionalProperties": false + } + ] +}"##; + + let deser = &mut serde_json::Deserializer::from_str(json); + let _schema: PropertySchema = + serde_path_to_error::deserialize(deser).expect("Failed to parse JSON Schema"); + } + + // #[test] + // fn test_parse_jsonschema() { + // let json = include_str!("../test/full.json"); + // let deser = &mut serde_json::Deserializer::from_str(json); + // let schema: JsonSchema = + // serde_path_to_error::deserialize(deser).expect("Failed to parse JSON Schema"); + // assert!(!schema.definitions.is_empty()); + // } +} diff --git a/frontend/from-schema/src/lib.rs b/frontend/from-schema/src/lib.rs new file mode 100644 index 0000000..d879717 --- /dev/null +++ b/frontend/from-schema/src/lib.rs @@ -0,0 +1,2 @@ +pub mod jsonschema; +pub mod lowering; diff --git a/frontend/from-schema/src/lowering.rs b/frontend/from-schema/src/lowering.rs new file mode 100644 index 0000000..6bf232d --- /dev/null +++ b/frontend/from-schema/src/lowering.rs @@ -0,0 +1,450 @@ +//! Conversion from the `jsonschema` AST into the IR (`ir::Toplevel`, `ir::Ty`, etc.) +//! in small, testable steps. All `lower_*` logic that’s one-to-one with an AST type +//! is implemented as a `Lower` trait; the top-level conversion and other multi-variant +//! cases remain free functions. + +use github_webhook_type_generator::{ + context::{self}, + ir::{ + self, Additional, ConstEnumVariant, Definition, DefinitionPath, DefinitionRoot, Doc, Field, + FieldPath, Module, Override, OverrideToField, Overrides, Path, Primitive, + TaggedEnumVariant, Ty, TyKind, + }, +}; + +use crate::jsonschema::{ + AdditionalProperties, AllOfSchema, ArraySchema, ArrayType, BooleanSchema, IntegerSchema, + JsonSchema, Nullable, NumberSchema, ObjectSchema, OneOfSchema, PropertySchema, RefSchema, + SchemaDefinition, StringFormat, StringSchema, +}; + +struct Context<'cx> { + gcx: &'cx context::Context<'cx>, + definitions: Vec>, +} + +impl<'cx> std::ops::Deref for Context<'cx> { + type Target = &'cx context::Context<'cx>; + + fn deref(&self) -> &Self::Target { + &self.gcx + } +} + +/// Entrypoint: convert the root JSON schema into an IR forest. +pub fn lower_schema<'cx>(schema: JsonSchema<'cx>, gcx: &'cx context::Context<'cx>) -> Module<'cx> { + let mut cx = Context { + gcx, + definitions: Vec::with_capacity(schema.definitions.len()), + }; + + for (raw_key, property) in schema.definitions { + // 1) compute DefinitionRoot from "foo$bar" + let root = lower_definition_root(&mut cx, raw_key); + + // 2) start a fresh path + let path = Path::mk_root(&cx, root); + + // 3) lower this PropertySchema into a `Ty<'cx>` + let (doc, ty) = property.lower(&mut cx, path); + cx.definitions.push(Definition { doc, ty }); + } + + let variants = schema + .one_of + .into_iter() + .map(|one_of| lower_ref(&mut cx, &one_of)) + .collect(); + + Module { + definitions: cx.definitions, + variants, + } +} + +fn lower_definition_root<'cx>(cx: &mut Context<'cx>, key: &'cx str) -> DefinitionRoot<'cx> { + let (base, variant) = match key.split_once('$') { + Some((base, variant)) => (cx.intern_str(base), Some(cx.intern_str(variant))), + None => (cx.intern_str(key), None), + }; + DefinitionRoot { base, variant } +} + +fn lower_ref<'cx>(cx: &mut Context<'cx>, ref_schema: &RefSchema<'cx>) -> Path<'cx> { + let raw = ref_schema.ref_path; + let key = raw.strip_prefix("#/definitions/").expect("invalid ref"); + let root = lower_definition_root(cx, key); + Path::mk_root(cx, root) +} + +/// Trait for schema components that lower into a single IR construct. +trait Lower<'cx> { + type Output; + + fn lower(self, cx: &mut Context<'cx>, path: Path<'cx>) -> Self::Output; +} + +impl<'cx> Lower<'cx> for RefSchema<'cx> { + type Output = Ty<'cx>; + + fn lower(self, cx: &mut Context<'cx>, path: Path<'cx>) -> Self::Output { + Ty::mk_ident(cx, lower_ref(cx, &self), path) + } +} + +impl<'cx> Lower<'cx> for PropertySchema<'cx> { + type Output = (Doc<'cx>, Ty<'cx>); + + fn lower(self, cx: &mut Context<'cx>, path: Path<'cx>) -> Self::Output { + let doc = Doc { + title: self.title, + description: self.description, + }; + + let ty = self.content.lower(cx, path); + + (doc, ty) + } +} + +impl<'cx> Lower<'cx> for SchemaDefinition<'cx> { + type Output = Ty<'cx>; + + fn lower(self, cx: &mut Context<'cx>, path: Path<'cx>) -> Self::Output { + match self { + SchemaDefinition::Ref(schema_ref) => schema_ref.lower(cx, path), + SchemaDefinition::OneOf(one_of) => one_of.lower(cx, path), + SchemaDefinition::AllOf(all_of) => all_of.lower(cx, path), + SchemaDefinition::Object(obj) => obj.lower(cx, path), + SchemaDefinition::String(string_schema) => string_schema.lower(cx, path), + SchemaDefinition::Integer(integer_schema) => integer_schema.lower(cx, path), + SchemaDefinition::Number(number_schema) => number_schema.lower(cx, path), + SchemaDefinition::Boolean(boolean_schema) => boolean_schema.lower(cx, path), + SchemaDefinition::Array(array_schema) => array_schema.lower(cx, path), + SchemaDefinition::Null => Ty::mk_null(cx, path), + } + } +} + +impl<'cx, Inner: Lower<'cx, Output = Ty<'cx>>> Lower<'cx> for Nullable { + type Output = Ty<'cx>; + + fn lower(self, cx: &mut Context<'cx>, path: Path<'cx>) -> Self::Output { + let inner_ty = self.content.lower(cx, path); + Ty::mk_option(cx, inner_ty, path) + } +} + +impl<'cx> Lower<'cx> for ObjectSchema<'cx> { + type Output = Ty<'cx>; + + fn lower(self, cx: &mut Context<'cx>, path: Path<'cx>) -> Self::Output { + let mut fields = Vec::with_capacity(self.properties.len()); + + for (field_name, field_schema) in self.properties { + // Skipping fields if they are a tag for a tagged enum. + if field_name == "action" && path.root().variant.is_some() { + continue; + } + let field_name = cx.intern_str(field_name); + let absolute_field_path = path.descend(cx, field_name); + let (doc, ty) = field_schema.lower(cx, absolute_field_path); + + let field = Field { + name: field_name, + ty, + required: self.required.contains(&field_name.0), + doc, + }; + + fields.push(field); + } + + let additional = match self.additional_properties { + AdditionalProperties::Boolean(false) => Additional::None, + AdditionalProperties::Boolean(true) => Additional::Any, + AdditionalProperties::Type(boxed) => { + let ty = boxed.lower(cx, path); + Additional::Typed(ty) + } + }; + + Ty::mk_struct(cx, fields, additional, path) + } +} + +impl<'cx> Lower<'cx> for StringSchema<'cx> { + type Output = Ty<'cx>; + + fn lower(self, cx: &mut Context<'cx>, path: Path<'cx>) -> Self::Output { + match self { + StringSchema::Enum { enum_values } => { + let variants = enum_values + .iter() + .flatten() + .map(|value| ConstEnumVariant { + value: cx.intern_str(value), + }) + .collect(); + Ty::mk_const_enum(cx, variants, path) + } + StringSchema::Format { format } => { + let format = match format { + StringFormat::DateTime => ir::StringFormat::DateTime, + StringFormat::Uri => ir::StringFormat::Uri, + StringFormat::Uuid => ir::StringFormat::Uuid, + }; + Ty::mk_string(cx, Some(format), path) + } + StringSchema::Generic {} => Ty::mk_string(cx, None, path), + } + } +} + +impl<'cx> Lower<'cx> for IntegerSchema { + type Output = Ty<'cx>; + + fn lower(self, cx: &mut Context<'cx>, path: Path<'cx>) -> Self::Output { + Ty::mk_primitive( + cx, + Primitive::Integer { + constant: self.constant, + }, + path, + ) + } +} + +impl<'cx> Lower<'cx> for NumberSchema { + type Output = Ty<'cx>; + + fn lower(self, cx: &mut Context<'cx>, path: Path<'cx>) -> Self::Output { + Ty::mk_primitive(cx, Primitive::Number, path) + } +} + +impl<'cx> Lower<'cx> for BooleanSchema { + type Output = Ty<'cx>; + + fn lower(self, cx: &mut Context<'cx>, path: Path<'cx>) -> Self::Output { + Ty::mk_boolean(cx, self.constant, path) + } +} + +impl<'cx> Lower<'cx> for ArraySchema<'cx> { + type Output = Ty<'cx>; + + fn lower(self, cx: &mut Context<'cx>, path: Path<'cx>) -> Self::Output { + match self.items { + ArrayType::Specified(inner_schema) => { + let inner_ty = inner_schema.lower(cx, path); + Ty::mk_vec(cx, inner_ty, path) + } + ArrayType::Any {} => { + // fallback type for 'any' items + Ty::mk_any_vec(cx, path) + } + } + } +} + +impl<'cx> Lower<'cx> for OneOfSchema<'cx> { + type Output = Ty<'cx>; + + /// Lower a [`OneOfSchema`] into a type. + /// + /// This function is a bit more complex because it needs to handle + /// multiple usages of `OneOfSchema`: + /// - as a simple nullable type, + /// - as a tagged union, + /// - as a regular union. + fn lower(self, cx: &mut Context<'cx>, path: Path<'cx>) -> Self::Output { + let possibilities: Vec<_> = self + .one_of + .into_iter() + .map(|schema| schema.lower(cx, path)) + .collect(); + + let contains_null = possibilities.iter().any(|ty| ty.is_null()); + + fn is_tagged_enum<'cx>(possibilities: &[Ty<'cx>]) -> Option>> { + let mut common_base = None; + let len = possibilities.len(); + let variants = possibilities.iter().try_fold( + Vec::with_capacity(len), + |mut acc, Ty { kind, .. }| { + let TyKind::Ident(Path(path)) = kind.0 else { + return None; + }; + let DefinitionPath::Root(DefinitionRoot { + base, + variant: Some(tag_value), + }) = path.0 + else { + return None; + }; + // Check if the base is the same for all possibilities. + if let Some(common) = common_base { + if common != base { + return None; + } + } else { + common_base = Some(base); + } + + // All checks passed, we can add the variant. + acc.push(TaggedEnumVariant { + tag_value: *tag_value, + }); + Some(acc) + }, + )?; + // At this point, we have a valid tagged enum. + // Remove the tag from the defined variants. + + Some(variants) + } + + if possibilities.is_empty() { + unreachable!("`oneOf` with no possibilities is not allowed"); + } + + let non_null_count = possibilities.len() - contains_null as usize; + + match non_null_count { + 0 => unreachable!("`oneOf` with no possibilities is not allowed"), + 1 => { + if contains_null { + // Simply wrap the single non-null type in an `Option`. + Ty::mk_option(cx, possibilities.into_iter().next().unwrap(), path) + } else { + // If there's only one possibility, we can return it directly. + possibilities.into_iter().next().unwrap() + } + } + _ => { + if let Some(variants) = is_tagged_enum(&possibilities) { + Ty::mk_tagged_enum(cx, variants, path) + } else { + // Fallback to an untagged enum, even if it `contains_null`. + Ty::mk_untagged_enum(cx, possibilities, path) + } + } + } + } +} + +impl<'cx> Lower<'cx> for AllOfSchema<'cx> { + type Output = Ty<'cx>; + + /// GitHub-Webhook schemas employ `allOf` in a *single, restricted* pattern: + /// + /// ```ignore + /// ┌─ first element ────────────────────────────────────────────────────────┐ + /// │ { "$ref": "#/definitions/base_event" } │ + /// └────────────────────────────────────────────────────────────────────────┘ + /// ┌─ second element (object) ──────────────────────────────────────────────┐ + /// │ { "type": "object", "required": [...], "properties": { ... } } │ + /// └────────────────────────────────────────────────────────────────────────┘ + /// ``` + /// + /// This function lowers such a construct to an [`Override`] which records: + /// + /// - `base_ty` – the resolved [`Path`] of the referenced base event type. + /// - `fields` – every *non-object* leaf inside the extension object, + /// expressed as [`OverrideToField`]s. Each field path starts + /// with `FieldPath::mk_root(cx)` and is extended via `.descend(...)`. + /// + /// The helper `collect_override_fields` performs a depth-first walk and + /// generates the field overrides without duplicating decision logic. + fn lower(self, cx: &mut Context<'cx>, path: Path<'cx>) -> Self::Output { + /*─────────────────────────────────────────────────────────────────────── + 1. Resolve the base `$ref` into a Path + ───────────────────────────────────────────────────────────────────────*/ + let base_type_path: Path<'cx> = lower_ref(cx, &self.all_of.ref_schema); + + /*─────────────────────────────────────────────────────────────────────── + 2. Collect all overriding leaves from the extension object + ───────────────────────────────────────────────────────────────────────*/ + let mut override_fields: Vec> = Vec::new(); + + collect_override_fields(cx, self.all_of.extension, None, &mut override_fields, path); + + /// Recursively traverses `object_schema`, creating an [`Overrides`] for + /// every leaf that is *not* an `"object"` schema. + /// + /// * `current_fp` represents the path from the extension object’s root to the + /// current position. + fn collect_override_fields<'cx>( + cx: &mut Context<'cx>, + object_schema: ObjectSchema<'cx>, + current_fp: Option>, + out: &mut Vec>, + parent_ty: Path<'cx>, + ) { + let required_set = &object_schema.required; + for (field_name, field_schema) in object_schema.properties { + let next_fp = FieldPath::new(cx, cx.intern_str(field_name), current_fp); + + match field_schema.content { + // recurse + SchemaDefinition::Object(inner_obj) => { + assert!(!inner_obj.nullable); + collect_override_fields( + cx, + inner_obj.content, + Some(next_fp), + out, + parent_ty, + ); + } + + // leaf + _ => { + let is_required = required_set.iter().any(|r| *r == field_name); + + let (doc, ty) = field_schema.lower(cx, parent_ty); + + out.push(Overrides { + field_path: next_fp, + content: OverrideToField { + required: is_required, + doc, + ty, + }, + }); + } + } + } + } + + /*─────────────────────────────────────────────────────────────────────── + 3. Build and return the Override + ───────────────────────────────────────────────────────────────────────*/ + let override_ = Override { + base_ty: base_type_path, + fields: override_fields, + }; + Ty::mk_override(cx, override_, path) + } +} + +#[cfg(test)] +mod tests { + // use super::*; + + // #[test] + // fn test_lower_full() { + // let json = include_str!("../test/full.json"); + // let deser = &mut serde_json::Deserializer::from_str(json); + // let schema: JsonSchema = + // serde_path_to_error::deserialize(deser).expect("Failed to parse JSON Schema"); + // let arena = context::Arena::default(); + // let gcx = context::Context::new(&arena); + // let toplevel = lower_schema(schema, &gcx); + // assert!( + // !toplevel.definitions.is_empty(), + // "Expected non-empty definitions" + // ); + // } +} diff --git a/rust-toolchain b/rust-toolchain index c1f5c7b..2535b01 100644 --- a/rust-toolchain +++ b/rust-toolchain @@ -1,3 +1,3 @@ [toolchain] -channel = "1.75.0" +channel = "1.78.0" components = ["rustfmt", "clippy"] diff --git a/type-generator/Cargo.toml b/type-generator/Cargo.toml index eab83d9..d7aadb5 100644 --- a/type-generator/Cargo.toml +++ b/type-generator/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "github-webhook-type-generator" version.workspace = true -rust-version = "1.65.0" +rust-version = "1.78.0" edition = "2021" description = "GitHub webhook payload type generator for Rust" @@ -16,8 +16,7 @@ serde = [] [dependencies] proc-macro2 = "1.0.76" quote = "1.0.35" -structopt = "0.3.26" -swc_common = { version = "0.34.0", features = ["tty-emitter"]} -swc_ecma_ast = "0.115.0" -swc_ecma_parser = "0.146.0" -once_cell = "1.19.0" + +typed-arena = "2.0.2" +rustc-hash = "2.1.1" +smallvec = "1.15.1" diff --git a/type-generator/src/arena.rs b/type-generator/src/arena.rs new file mode 100644 index 0000000..fad85e9 --- /dev/null +++ b/type-generator/src/arena.rs @@ -0,0 +1,37 @@ +/// Abstraction over [`typed_arena::Arena`]. +pub trait ArenaAlloc<'cx, T> { + /// Allocates a new value in the arena and returns an interned handle. + fn alloc(&'cx self, value: T) -> &'cx T; +} + +/// See [`ArenaAlloc`]. +pub trait ArenaRefAlloc<'cx, T: ?Sized> { + /// Allocates a new value in the arena and returns an interned handle. + fn alloc(&'cx self, value: &T) -> &'cx T; +} + +impl<'cx> ArenaRefAlloc<'cx, str> for typed_arena::Arena { + fn alloc(&'cx self, value: &str) -> &'cx str { + self.alloc_str(value) + } +} + +impl<'cx, T> ArenaAlloc<'cx, T> for typed_arena::Arena { + fn alloc(&'cx self, value: T) -> &'cx T { + self.alloc(value) + } +} + +mod sealed { + /// A mapping from an arena allocator type to its allocated value type. + pub trait ArenaAllocator<'cx> { + type Allocated; + } +} + +pub type ArenaAllocator = typed_arena::Arena; +impl<'cx, T: 'cx> sealed::ArenaAllocator<'cx> for ArenaAllocator { + type Allocated = &'cx T; +} + +pub type ArenaAllocated<'cx, Allocator> = >::Allocated; diff --git a/type-generator/src/context.rs b/type-generator/src/context.rs new file mode 100644 index 0000000..bc51ff9 --- /dev/null +++ b/type-generator/src/context.rs @@ -0,0 +1,58 @@ +use crate::{ + arena::ArenaAllocator, + intern::{Interned, Str, StrInterner}, + ir::{DefinitionPath, FieldPath, FieldTreeNode, Path, PathInterner, TyInterner, TyKind}, +}; + +/// Arena holding all shared allocations. +#[derive(Default)] +pub struct Arena<'cx> { + str_interner: StrInterner<'cx>, + ty_interner: TyInterner<'cx>, + path_interner: PathInterner<'cx>, + field_path_arena: ArenaAllocator>, +} + +impl<'cx> Arena<'cx> { + /// Allocates a new string in the arena. + pub fn intern_str(&'cx self, s: &'cx str) -> Str<'cx> { + self.str_interner.intern_ref_noalloc(s) + } + + /// Allocates a new type in the arena. + pub fn intern_ty(&'cx self, ty: TyKind<'cx>) -> Interned> { + self.ty_interner.intern(ty) + } + + /// Allocates a new path in the arena. + pub fn intern_path(&'cx self, path: DefinitionPath<'cx>) -> Path<'cx> { + Path(self.path_interner.intern(path)) + } + + /// Allocates a new field path in the arena. + pub fn alloc_field_path(&'cx self, path: FieldTreeNode<'cx>) -> FieldPath<'cx> { + FieldPath(self.field_path_arena.alloc(path)) + } +} + +/// A long-lived object. +/// +/// This context must outlive any references to allocated objects. +pub struct Context<'cx> { + arena: &'cx Arena<'cx>, +} + +impl<'cx> std::ops::Deref for Context<'cx> { + type Target = &'cx Arena<'cx>; + + fn deref(&self) -> &Self::Target { + &self.arena + } +} + +impl<'cx> Context<'cx> { + /// Creates a new context with a root namespace. + pub fn new(arena: &'cx Arena<'cx>) -> Self { + Self { arena } + } +} diff --git a/type-generator/src/frontend.rs b/type-generator/src/frontend.rs deleted file mode 100644 index 06fe944..0000000 --- a/type-generator/src/frontend.rs +++ /dev/null @@ -1,517 +0,0 @@ -pub mod merge_union_type_lits; -pub mod name_types; - -use once_cell::sync::Lazy; -use std::{borrow::Cow, collections::HashMap}; - -use crate::{ - case, - frontend::merge_union_type_lits::Merged, - ir::{ - LiteralKeyMap, RustAlias, RustComment, RustContainerAttrs, RustEnum, RustEnumMember, - RustEnumMemberKind, RustFieldAttr, RustFieldAttrs, RustMemberType, RustSegment, RustStruct, - RustStructAttr, RustStructMember, RustType, RustVariantAttrs, SerdeContainerAttr, - SerdeFieldAttr, TypeName, - }, -}; - -pub fn interface2struct<'input>( - st: &mut FrontendState<'input, '_>, - interface: &'input swc_ecma_ast::TsInterfaceDecl, - comment: Option, - lkm: &mut LiteralKeyMap, -) { - let name = interface.id.sym.as_ref(); - let ibody = &interface.body.body; - let mut ctxt = TypeConvertContext::from_path(Path::from_iter([Cow::Borrowed(name)])); - - let member = ibody - .iter() - .map(|m| m.as_ts_property_signature().unwrap()) - .map(|prop| ts_prop_signature(prop, st, &mut ctxt, name, lkm)) - .collect(); - - let name = name.to_owned(); - let s = RustStruct { - attr: RustContainerAttrs::new(), - name, - comment, - is_borrowed: false, - member, - }; - st.segments.push(RustSegment::Struct(s)); -} - -pub fn ts_prop_signature<'input>( - prop: &'input swc_ecma_ast::TsPropertySignature, - st: &mut FrontendState<'input, '_>, - ctxt: &mut TypeConvertContext<'input>, - name: &str, - lkm: &mut HashMap>, -) -> RustStructMember { - let comment = st.get_comment(prop.span.lo); - let mut is_optional = prop.optional; - let mut pkey: &str = match &*prop.key { - swc_ecma_ast::Expr::Ident(pkey) => &pkey.sym, - swc_ecma_ast::Expr::Lit(swc_ecma_ast::Lit::Str(k)) => &k.value, - _ => unreachable!(), - }; - let mut attr = RustFieldAttrs::new(); - // avoid conflict to Rust reserved word - static RENAME_RULES: Lazy> = Lazy::new(|| { - HashMap::from_iter([ - ("type", "type_"), - ("ref", "ref_"), - ("self", "self_"), - ("+1", "plus_1"), - ("-1", "minus_1"), - ]) - }); - if let Some(renamed) = RENAME_RULES.get(pkey) { - attr.add_attr(RustFieldAttr::Serde(SerdeFieldAttr::Rename( - pkey.to_owned(), - ))); - pkey = renamed; - } - let ptype = &prop.type_ann.as_ref().unwrap().type_ann; - let mut ctxt = ctxt.clone(); - ctxt.projection(Cow::Borrowed(pkey)); - - let (is_optional2, ty) = ts_type_to_rs(st, &mut Some(ctxt), ptype, None, lkm); - is_optional |= is_optional2; - - fn extract_literal_type(ptype: &swc_ecma_ast::TsType) -> Option<&str> { - ptype.as_ts_lit_type()?.lit.as_str()?.raw.as_deref() - } - if let Some(lit) = extract_literal_type(ptype) { - lkm.entry(name.to_owned()).or_default().insert( - pkey.to_owned(), - lit.strip_prefix('\"') - .unwrap() - .strip_suffix('\"') - .unwrap() - .to_owned(), - ); - } - RustStructMember { - ty: RustMemberType { ty, is_optional }, - name: pkey.to_string(), - attr, - comment, - } - //dbg!(prop); - - //let pkey = if let Some(pkey) = &prop.key.as_ident() { - // // ident - // &pkey.sym - //} else { - // // interface { "+1": number; "-1": number; } - // // TODO: parse - // dbg!(prop); - // "+1" - //}; -} - -pub fn ts_index_signature<'input>( - index: &'input swc_ecma_ast::TsIndexSignature, - comment: Option, - st: &mut FrontendState<'input, '_>, - ctxt: &mut TypeConvertContext<'input>, - lkm: &mut HashMap>, -) -> RustStructMember { - assert!(index.params.len() == 1); - let param = index.params.first().unwrap(); - let ident = param.as_ident().expect("key is string"); - let mut ctxt = Some(ctxt.clone()); - let (_, key_ty) = ts_type_to_rs( - st, - &mut ctxt, - &ident.type_ann.as_ref().unwrap().type_ann, - None, - lkm, - ); - let (_, value_ty) = ts_type_to_rs( - st, - &mut ctxt, - &index.type_ann.as_ref().unwrap().type_ann, - None, - lkm, - ); - RustStructMember { - ty: RustMemberType { - ty: RustType::Map(Box::new(key_ty), Box::new(value_ty)), - is_optional: false, - }, - name: ident.sym.to_string(), - attr: RustFieldAttrs::from_attr(RustFieldAttr::Serde(SerdeFieldAttr::Flatten)), - comment, - } -} - -pub fn tunion2enum<'input>( - st: &mut FrontendState<'input, '_>, - name: &'input str, - tsuoi: &'input swc_ecma_ast::TsUnionOrIntersectionType, - comment: Option, - lkm: &mut LiteralKeyMap, - from_alias: bool, -) { - union_or_intersection( - st, - Some(TypeConvertContext { - path: vec![Cow::Borrowed(name)], - granted_name: Some(name), - from_alias, - ..Default::default() - }), - tsuoi, - comment, - &mut false, - lkm, - ); -} - -fn union_or_intersection<'input>( - st: &mut FrontendState<'input, '_>, - mut ctxt: Option>, - tsuoi: &'input swc_ecma_ast::TsUnionOrIntersectionType, - comment: Option, - nullable: &mut bool, - lkm: &mut LiteralKeyMap, -) -> RustType { - use swc_ecma_ast::TsKeywordTypeKind; - use swc_ecma_ast::TsUnionOrIntersectionType; - - match tsuoi { - TsUnionOrIntersectionType::TsUnionType(tunion) => { - // nullable check - let mut types: Vec<&Box> = tunion.types.iter().collect(); - // obtain non-null types within union, judging if there is null keyword - types.retain(|t| { - if let Some(tkey) = t.as_ts_keyword_type() { - if tkey.kind == TsKeywordTypeKind::TsNullKeyword { - *nullable = true; - return false; - } - } - true - }); - - assert!(!types.is_empty()); - if types.len() == 1 { - let (n, t) = ts_type_to_rs(st, &mut ctxt, types[0], comment, lkm); - *nullable |= n; - return t; - } - - // strings check: "Bot" | "User" | "Organization" - if let Some(mut variants) = types - .iter() - .map(|t| Some(t.as_ts_lit_type()?.lit.as_str()?.value.as_ref())) - .collect::>>() - { - variants.sort(); - let ct = ctxt.as_mut().expect("provide ctxt"); - let tn = name_types::string_literal_union(st, variants, comment, ct); - return RustType::Custom(tn); - //TODO: comment strs // {:?}", strs)); - } - - if types.len() >= 2 { - if let Some(variants) = types - .iter() - .map(|t| t.as_ts_type_lit()) - .collect::>>() - { - let ctxt = ctxt.as_mut().unwrap(); - let Merged { - intersection, - diffs, - } = merge_union_type_lits::merge_union_type_lits(&variants); - let mut s = - name_types::type_literal(st, intersection.into_iter(), None, ctxt, lkm); - let member = diffs - .into_iter() - .map(|d| { - let s = name_types::type_literal(st, d.into_iter(), None, ctxt, lkm); - RustEnumMemberKind::Unary(st.push_segment(RustSegment::Struct(s))) - .into() - }) - .collect(); - let ty = st.push_segment(RustSegment::Enum(RustEnum { - name: ctxt.create_ident_with(Some(vec!["DistinctUnion".to_string()])), - attr: RustContainerAttrs::from_attr(RustStructAttr::Serde( - SerdeContainerAttr::Untagged, - )), - comment: None, - is_borrowed: false, - member, - })); - s.member.push(RustStructMember { - ty: RustMemberType { - ty, - is_optional: false, - }, - name: "distinct".to_owned(), - attr: RustFieldAttrs::from_attr(RustFieldAttr::Serde( - SerdeFieldAttr::Flatten, - )), - comment, - }); - return st.push_segment(RustSegment::Struct(s)); - } - } - - let type_convert_context = ctxt.as_mut().unwrap(); - let mut name = type_convert_context.create_ident(); - if !type_convert_context.from_alias { - name.push_str("Union"); - } - let variants: Vec<_> = types - .iter() - .map(|t| { - let (_, t) = ts_type_to_rs(st, &mut ctxt, t, None, lkm); - RustEnumMember { - attr: RustVariantAttrs::new(), - kind: RustEnumMemberKind::Unary(t), - } - }) - .collect(); - - st.segments.push(RustSegment::Enum(RustEnum { - attr: RustContainerAttrs::from_attr(RustStructAttr::Serde( - SerdeContainerAttr::Untagged, - )), - name: name.to_owned(), - comment, - is_borrowed: false, - member: variants, - })); - RustType::Custom(TypeName::new(name)) - } - TsUnionOrIntersectionType::TsIntersectionType(tints) => { - if tints.types.len() == 2 { - let mut iter = tints.types.iter(); - // if types consist of type literal and type ref, create new struct with serde flatten attribute - let tref = iter.next().unwrap().as_ts_type_ref(); - let tlit = iter.next().unwrap().as_ts_type_lit(); - if let (Some(tref), Some(tlit)) = (tref, tlit) { - let name = tref.type_name.as_ident().unwrap().sym.as_ref(); - let mut str = name_types::type_literal( - st, - tlit.members.iter(), - None, - &mut ctxt.unwrap(), - lkm, - ); - - if str.member.iter().all(|m| m.ty.is_unknown()) { - let struct_name = str.name.to_owned(); - let a = RustAlias { - name: struct_name.to_owned(), - is_borrowed: false, - comment: None, - ty: RustType::Custom(TypeName::new(name.to_owned())), - }; - st.segments.push(RustSegment::Alias(a)); - return RustType::Custom(TypeName::new(struct_name)); - } else { - // add flatten attributed field to struct - let mut field_name = name.to_owned(); - case::CaseConvention::Pascal - .into_rename_rule() - .convert_to_snake(&mut field_name); - str.member.push(RustStructMember { - attr: RustFieldAttrs::from_attr(RustFieldAttr::Serde( - SerdeFieldAttr::Flatten, - )), - name: field_name, - ty: RustMemberType { - is_optional: false, - ty: RustType::Custom(TypeName::new(name.to_owned())), - }, - comment, - }); - let struct_name = str.name.to_owned(); - st.segments.push(RustSegment::Struct(str)); - return RustType::Custom(TypeName::new(struct_name)); - } - } - } - // dbg!(tints); - //todo!(); - RustType::UnknownIntersection - } - } -} - -pub struct FrontendState<'input, 'output> { - pub segments: &'output mut Vec, - pub comments: &'input swc_common::comments::SingleThreadedComments, - pub name_types: name_types::State<'input>, -} - -impl<'input, 'output> FrontendState<'input, 'output> { - pub fn push_segment(&mut self, value: RustSegment) -> RustType { - let name = value.name().to_owned(); - self.segments.push(value); - RustType::Custom(TypeName::new(name)) - } - pub fn get_comment(&self, pos: swc_common::BytePos) -> Option { - self.comments - .with_leading(pos, |cs| cs.last().map(|c| strip_docs(&c.text))) - } -} - -fn ts_keyword_type_to_rs(typ: &swc_ecma_ast::TsKeywordType) -> RustType { - use swc_ecma_ast::TsKeywordTypeKind; - match typ.kind { - TsKeywordTypeKind::TsStringKeyword => RustType::String { is_borrowed: false }, - TsKeywordTypeKind::TsNumberKeyword => RustType::Number, - TsKeywordTypeKind::TsBooleanKeyword => RustType::Boolean, - TsKeywordTypeKind::TsNullKeyword => RustType::Unit, - TsKeywordTypeKind::TsUnknownKeyword => RustType::Unknown, - _ => { - unimplemented!("{:?}", typ.kind); - } - } -} - -pub type Path<'a> = Vec>; - -#[derive(Clone, Default)] -pub struct TypeConvertContext<'a> { - path: Path<'a>, - granted_name: Option<&'a str>, - from_alias: bool, - /// prevent duplicate field name - duplicate_counter: usize, -} - -impl<'a> TypeConvertContext<'a> { - pub fn from_path(path: Path<'a>) -> Self { - Self { - path, - ..Default::default() - } - } - - /// struct field - pub fn projection(&mut self, field: Cow<'a, str>) { - self.path.push(field); - self.granted_name = None; - self.from_alias = false; - self.duplicate_counter = 0; - } - - fn to_pascal(&self) -> Vec { - self.path - .iter() - .map(|p| { - let mut p = p.to_string(); - case::detect_case(&p) - .into_rename_rule() - .convert_to_pascal(&mut p); - p - }) - .collect() - } - - /// create identifier from path - pub fn create_ident(&mut self) -> String { - self.create_ident_with(None) - } - - pub fn create_ident_with(&mut self, additional: Option>) -> String { - if let Some(name) = self.granted_name.take() { - return name.to_owned(); - } - let mut v = self.to_pascal(); - if let Some(additional) = additional { - v.extend(additional); - } else { - if self.from_alias || self.duplicate_counter != 0 { - let suffix = self.duplicate_counter + self.from_alias as usize; - v.push(suffix.to_string()); - } - self.duplicate_counter += 1; - } - v.concat() - } -} - -pub fn ts_type_to_rs<'input>( - st: &mut FrontendState<'input, '_>, - ctxt: &mut Option>, - mut typ: &'input swc_ecma_ast::TsType, - comment: Option, - lkm: &mut HashMap>, -) -> (bool, RustType) { - let mut nullable = false; - - // peel off parenthesis (that only exist for precedence) - while let swc_ecma_ast::TsType::TsParenthesizedType(t) = typ { - typ = &*t.type_ann; - } - - let typ = match typ { - swc_ecma_ast::TsType::TsKeywordType(tk) => ts_keyword_type_to_rs(tk), - swc_ecma_ast::TsType::TsUnionOrIntersectionType(tsuoi) => { - union_or_intersection(st, ctxt.to_owned(), tsuoi, comment, &mut nullable, lkm) - } - swc_ecma_ast::TsType::TsLitType(_tslit) => RustType::UnknownLiteral, - swc_ecma_ast::TsType::TsTypeRef(tref) => { - let id = tref.type_name.as_ident().unwrap().sym.as_ref(); - RustType::Custom(TypeName { - name: id.to_owned(), - is_borrowed: false, - }) - } - swc_ecma_ast::TsType::TsArrayType(tarray) => { - let (_n, etype) = ts_type_to_rs(st, ctxt, &tarray.elem_type, comment, lkm); - //format!("Vec<{etype}>") - RustType::Array(Box::new(etype)) - } - swc_ecma_ast::TsType::TsTypeLit(tlit) => { - let s = name_types::type_literal( - st, - tlit.members.iter(), - comment, - ctxt.as_mut().unwrap(), - lkm, - ); - let name = s.name.clone(); - st.segments.push(RustSegment::Struct(s)); - - RustType::Custom(TypeName::new(name)) - } - swc_ecma_ast::TsType::TsTupleType(t) => { - // empty array type is treated as tuple type with 0 elements - if t.elem_types.is_empty() { - RustType::Unit - } else { - RustType::Unknown - } - } - _ => { - //dbg!(typ); - //todo!(); - RustType::Unknown - } - }; - - (nullable, typ) -} - -pub fn strip_docs(comment: &str) -> RustComment { - let comment = comment.trim_start_matches('*'); - let comment = comment.trim_start(); - let comment = comment.trim_end(); - RustComment( - comment - .split('\n') - .map(|s| s.trim_start().trim_start_matches("* ")) - .collect::>() - .join(" "), - ) -} diff --git a/type-generator/src/frontend/merge_union_type_lits.rs b/type-generator/src/frontend/merge_union_type_lits.rs deleted file mode 100644 index 4fda0e0..0000000 --- a/type-generator/src/frontend/merge_union_type_lits.rs +++ /dev/null @@ -1,34 +0,0 @@ -use swc_common::EqIgnoreSpan; - -pub struct Merged<'a> { - pub intersection: Vec<&'a swc_ecma_ast::TsTypeElement>, - pub diffs: Vec>, -} - -pub fn merge_union_type_lits<'input>( - variants: &[&'input swc_ecma_ast::TsTypeLit], -) -> Merged<'input> { - let mut intersection: Vec<_> = variants.first().unwrap().members.iter().collect(); - let mut diffs = vec![vec![]]; - for variant in variants[1..].iter() { - let mut diff: Vec<_> = variant.members.iter().collect(); - intersection.retain(|i| { - if let Some(index) = diff.iter().position(|d| i.eq_ignore_span(d)) { - // `i` remains in common, so remove it from `diff` - diff.remove(index); - true - } else { - // `i` turns out to be specific to former variants - for diff in diffs.iter_mut() { - diff.push(*i) - } - false - } - }); - diffs.push(diff); - } - Merged { - intersection, - diffs, - } -} diff --git a/type-generator/src/frontend/name_types.rs b/type-generator/src/frontend/name_types.rs deleted file mode 100644 index 78bcd65..0000000 --- a/type-generator/src/frontend/name_types.rs +++ /dev/null @@ -1,125 +0,0 @@ -use std::collections::HashMap; - -use crate::ir::{ - RustAlias, RustComment, RustEnum, RustEnumMember, RustEnumMemberKind, RustSegment, RustStruct, - RustType, RustVariantAttr, RustVariantAttrs, SerdeVariantAttr, TypeName, -}; - -use super::{ts_index_signature, ts_prop_signature, FrontendState, TypeConvertContext}; - -#[derive(Default)] -pub struct State<'a> { - /// set of literals -> rust type name - literal_map: HashMap, String>, -} - -pub fn string_literal_union<'input>( - st: &mut FrontendState<'input, '_>, - variants: Vec<&'input str>, - comment: Option, - path: &mut TypeConvertContext, -) -> TypeName { - let name = path.create_ident(); - - TypeName::new(match st.name_types.literal_map.get(&variants) { - Some(s) => { - // create new alias from union - create_alias(st, comment, &name, s.to_owned()); - name - } - None => { - // create new enum from union - create_enum(st, comment, &name, &variants); - - st.name_types.literal_map.insert(variants, name.clone()); - name - } - }) -} - -fn create_alias( - st: &mut FrontendState, - comment: Option, - name: &String, - old_name: String, -) { - st.segments.push(RustSegment::Alias(RustAlias { - name: name.to_owned(), - is_borrowed: false, - comment, - ty: RustType::Custom(TypeName::new(old_name)), - })); -} - -fn create_enum(st: &mut FrontendState, comment: Option, name: &String, vs: &[&str]) { - st.segments.push(RustSegment::Enum(RustEnum::from_members( - name.to_owned(), - comment, - vs.iter().map(|&v| { - let renamed = rename_to_valid_ident(v); - let mut attr = RustVariantAttrs::new(); - if v != renamed { - attr.add_attr(RustVariantAttr::Serde(SerdeVariantAttr::Rename( - v.to_owned(), - ))); - } - RustEnumMember { - attr, - kind: RustEnumMemberKind::Nullary(renamed), - } - }), - ))); -} - -fn rename_to_valid_ident(s: &str) -> String { - s.split(&['-', ' ', '_']) - .map(|term| { - let mut term = term - .chars() - .filter_map(|c| { - if c.is_ascii_alphanumeric() { - Some(c.to_ascii_lowercase()) - } else { - None - } - }) - .collect::(); - if let Some(c) = term.chars().next() { - let capital_ch = c.to_ascii_uppercase(); - let replace_with = if capital_ch.is_alphabetic() { - capital_ch.to_string() - } else if capital_ch.is_numeric() { - format!("N{capital_ch}") - } else { - unimplemented!() - }; - term.replace_range(..1, &replace_with); - } - term - }) - .collect::>() - .concat() -} - -pub fn type_literal<'input>( - st: &mut FrontendState<'input, '_>, - type_literal: impl Iterator, - comment: Option, - ctxt: &mut TypeConvertContext<'input>, - lkm: &mut HashMap>, -) -> RustStruct { - let name = ctxt.create_ident(); - RustStruct::from_members( - name.to_owned(), - comment, - type_literal.into_iter().flat_map(|m| match m { - swc_ecma_ast::TsTypeElement::TsPropertySignature(p) => { - Some(ts_prop_signature(p, st, ctxt, &name, lkm)) - } - swc_ecma_ast::TsTypeElement::TsIndexSignature(i) => { - Some(ts_index_signature(i, None, st, ctxt, lkm)) - } - _ => None, - }), - ) -} diff --git a/type-generator/src/intern.rs b/type-generator/src/intern.rs new file mode 100644 index 0000000..c89c39f --- /dev/null +++ b/type-generator/src/intern.rs @@ -0,0 +1,207 @@ +use std::{ + cell::RefCell, + hash::{Hash, Hasher}, +}; + +use rustc_hash::FxHashSet; + +use crate::arena::{ArenaAlloc, ArenaRefAlloc}; + +mod sealed { + /// A zero-sized type used to seal the [`Interned`] struct. + /// This prevents external implementation on it. + /// + /// [`Interned`]: super::Interned + pub struct SealedZst(pub std::marker::PhantomData); + /// `Clone` and `Copy` implementations for the type, which cannot be `derive`d + /// they do not know `T` is going to be bound to `PhantomData`. + impl Clone for SealedZst { + fn clone(&self) -> Self { + *self + } + } + impl Copy for SealedZst {} + + /// A mapping from an interner type to its interned value type. + /// Used to define the [`Interned`] type alias. + /// + /// This trait is sealed to prevent external implementations. + /// + /// [`Interned`]: super::Interned + pub trait Interner { + type Interned; + } +} + +/// A string interner that uses an arena for storage. +pub type StrInterner<'cx> = FxHashInterner<'cx, str, typed_arena::Arena>; + +/// An interned string type that uses the [`StrInterner`] for interning. +pub type Str<'cx> = Interned>; + +/// A default, generic interner that uses an arena for storage. +pub type Interner<'cx, T> = FxHashInterner<'cx, T, typed_arena::Arena>; + +/// A generic interner that does not allocate new values. +pub type RefOnlyInterner<'cx, T> = FxHashInterner<'cx, T, NoAlloc>; + +/// A marker type indicating that no allocation is performed. +pub struct NoAlloc; + +/// A type alias for the interned value type. +pub type Interned = ::Interned; + +/// A unique `impl` of [`sealed::InternerTrait`]. +impl<'cx, T: ?Sized, Arena> sealed::Interner for FxHashInterner<'cx, T, Arena> { + type Interned = InternedRef<'cx, T, Self>; +} + +/// A unique reference to an interned value. +/// +/// This type is used to represent a value that has been interned in an interner. +pub struct InternedRef<'a, T: ?Sized, Interner>(pub &'a T, sealed::SealedZst); + +impl<'a, T: ?Sized, Interner> std::ops::Deref for InternedRef<'a, T, Interner> { + type Target = &'a T; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl<'a, T: ?Sized, Interner> InternedRef<'a, T, Interner> { + /// Creates a new interned handle from a reference to the value. + /// + /// This function should not be `pub`lic to prevent from breaking the invariant + /// for `Eq`, `PartialEq`, and `Hash` traits. + fn new(value: &'a T) -> Self { + Self(value, sealed::SealedZst(std::marker::PhantomData)) + } +} + +impl Clone for InternedRef<'_, T, Interner> { + fn clone(&self) -> Self { + *self + } +} +impl Copy for InternedRef<'_, T, Interner> {} + +impl std::fmt::Debug for InternedRef<'_, T, Interner> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + +impl PartialEq for InternedRef<'_, T, Interner> { + fn eq(&self, other: &Self) -> bool { + std::ptr::eq(self.0, other.0) + } +} +impl Eq for InternedRef<'_, T, Interner> {} + +impl Hash for InternedRef<'_, T, Interner> { + fn hash(&self, state: &mut H) { + std::ptr::hash(self.0, state); + } +} + +impl PartialOrd for InternedRef<'_, T, Interner> { + fn partial_cmp(&self, other: &Self) -> Option { + self.0.partial_cmp(other.0) + } +} +impl Ord for InternedRef<'_, T, Interner> { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.0.cmp(other.0) + } +} + +/// A hash-based interner that uses an arena for storage. +/// +/// Use [`StrInterner`] or [`Interner`] for standard uses. +pub struct FxHashInterner<'cx, T: ?Sized, Arena> { + arena: Arena, + map: RefCell>, +} + +impl<'cx, T: ?Sized, Arena: Default> Default for FxHashInterner<'cx, T, Arena> { + /// Creates a new interning table. + fn default() -> Self { + Self { + arena: Arena::default(), + map: RefCell::new(FxHashSet::default()), + } + } +} + +impl<'cx, T: Hash + Eq, Arena: ArenaAlloc<'cx, T>> FxHashInterner<'cx, T, Arena> { + /// Interns the given path (if not already present), and returns a lightweight handle. + /// + /// This function will allocate the value in the arena if it is not already present. + /// + /// This function restricts `self` to be valid for the lifetime `'cx` for the arena, + /// while `map` does not need to do so. + /// + /// Do not use multiple interners for the same interner type, as the returned [`Interned`] value + /// cannot tell which interner generated it. + pub fn intern(&'cx self, value: T) -> InternedRef<'cx, T, Self> { + let mut lock = self.map.borrow_mut(); + if let Some(&existing) = lock.get(&value) { + return InternedRef::new(existing); + } + + let new_ref = self.arena.alloc(value); + lock.insert(new_ref); + InternedRef::new(new_ref) + } +} + +impl<'cx, T: ?Sized + Hash + Eq, Arena: ArenaRefAlloc<'cx, T>> FxHashInterner<'cx, T, Arena> { + /// Reference version of [`FxHashInterner::intern`]. See its documentation for details. + pub fn intern_ref<'any>(&'cx self, value: &'any T) -> InternedRef<'cx, T, Self> { + let mut lock = self.map.borrow_mut(); + if let Some(&existing) = lock.get(value) { + return InternedRef::new(existing); + } + + let new_ref = self.arena.alloc(value); + lock.insert(new_ref); + InternedRef::new(new_ref) + } +} + +/// A handler for copying references into a hash set without allocating a new one. +pub trait RefCopy<'cx, T: ?Sized> { + /// Copies the value into the container, without allocating a new one. + fn copy(&'cx self, value: &'cx T); +} + +/// Any type can be [`RefCopy`] as it does not allocate. Especially [`typed_arena::Arena`] +/// can be used to intern references without allocation. +impl<'cx, T: ?Sized, Any> RefCopy<'cx, T> for Any { + fn copy(&'cx self, _value: &'cx T) { + // No-op, as this trait is used to indicate that the value is copied + // without allocating a new one. + } +} + +impl<'cx, T: ?Sized + Hash + Eq, Arena: RefCopy<'cx, T>> FxHashInterner<'cx, T, Arena> { + /// Reference version of [`FxHashInterner::intern`]. + /// See its documentation for details. + /// + /// This function copies the value into the interner without allocating a new one, + /// thus this function takes a reference to the value that lasts for the lifetime `'cx`. + pub fn intern_ref_noalloc<'a>(&'cx self, value: &'a T) -> InternedRef<'cx, T, Self> + where + 'a: 'cx, + { + let mut lock = self.map.borrow_mut(); + if let Some(&existing) = lock.get(value) { + return InternedRef::new(existing); + } + + self.arena.copy(value); + lock.insert(value); + InternedRef::new(value) + } +} diff --git a/type-generator/src/ir.rs b/type-generator/src/ir.rs index 689f47f..6aeeacb 100644 --- a/type-generator/src/ir.rs +++ b/type-generator/src/ir.rs @@ -1,457 +1,470 @@ -use std::collections::HashMap; +//! Intermediate representation of types used to deserialize GitHub Webhook payloads. +//! +//! This IR is independent of JSON Schema or Rust naming rules, but assumes +//! the types are intended for use with `serde` and represent structurally valid +//! Rust data models. + +use std::{ + borrow::Cow, + cell::RefCell, + fmt::Debug, + hash::{Hash, Hasher}, +}; + +use crate::{ + context::Context, + intern::{Interned, Interner, Str}, +}; + +#[derive(Debug)] +/// The intermediate representation of a module containing schema definitions. +pub struct Module<'cx> { + /// Definitions in this module. + pub definitions: Vec>, + + /// Possible variants for the payload type in this module. Recall that + /// this project generates a type for the entire payload. + pub variants: Vec>, +} -use crate::dag::CoDirectedAcyclicGraph; +#[derive(Debug)] +/// A definition in the IR, representing a schema-defined type. +pub struct Definition<'cx> { + /// An optional documentation for this definition. + pub doc: Doc<'cx>, -pub struct RustComment(pub String); + /// The type identifier for this definition. + pub ty: Ty<'cx>, +} -pub enum RustSegment { - Struct(RustStruct), - Enum(RustEnum), - Alias(RustAlias), +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +/// An interned type representing a schema-defined type. +pub struct Ty<'cx> { + pub kind: Interned>, + pub path: Path<'cx>, } -impl RustSegment { - pub fn name(&self) -> &str { - match self { - RustSegment::Struct(s) => &s.name, - RustSegment::Enum(e) => &e.name, - RustSegment::Alias(a) => &a.name, - } +impl<'cx> std::ops::Deref for Ty<'cx> { + type Target = Interned>; + + fn deref(&self) -> &Self::Target { + &self.kind } } -#[derive(Debug, Clone)] -pub struct TypeName { - pub name: String, - pub is_borrowed: bool, -} +pub type TyInterner<'cx> = Interner<'cx, TyKind<'cx>>; -impl TypeName { - pub fn new(name: String) -> Self { - Self { - name, - is_borrowed: false, - } +/// An interned absolute path to a schema-defined type. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct Path<'cx>(pub Interned>); + +impl<'cx> std::ops::Deref for Path<'cx> { + type Target = Interned>; + + fn deref(&self) -> &Self::Target { + &self.0 } } -#[derive(Debug, Clone, Default)] -pub enum RustType { - String { - is_borrowed: bool, +pub type PathInterner<'cx> = Interner<'cx, DefinitionPath<'cx>>; + +/// Identifies a schema-defined type by its root name and field path. +/// +/// This path describes the origin of a type in terms of its position within a +/// structured schema. It begins at a root definition and descends through a +/// sequence of object fields. +/// +/// This is an absolute path; see [`FieldPath`] for relative paths from a type. +/// +/// ## Example layout +/// +/// Taking an example from JSON Schema frontend: +/// +/// ```ignore +/// ┌────────────────────────── definition ─────────────────────────────────────────┐ +/// #/definitions/branch_protection_rule$edited/properties/changes/properties/from +/// ▲ base ▲ optional variant ▲ field ▲ field +/// ``` +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum DefinitionPath<'cx> { + /// A root path. + Root(DefinitionRoot<'cx>), + + /// A path that descends through a field of a parent. + Field { + /// The parent path. + /// + /// This field refers to `Self` reluctantly because type alias cannot + /// recurse mutually. + parent: Interned>, + + /// The field name within the parent. + field: Str<'cx>, }, - Number, - Boolean, - Custom(TypeName), - Array(Box), - Map(Box, Box), - /// `()` - #[default] - Unit, - Unknown, - UnknownLiteral, - UnknownIntersection, -} - -impl RustType { - pub fn to_ident(&self) -> &str { - match self { - RustType::String { .. } => "String", - RustType::Number => "Number", - RustType::Boolean => "Boolean", - RustType::Custom(c) => &c.name, - RustType::Array(t) => t.to_ident(), - RustType::Unit => "Unit", - RustType::Unknown => "Unknown", - RustType::UnknownLiteral => "UnknownLiteral", - RustType::UnknownIntersection => "UnknownIntersection", - RustType::Map(..) => "Map", - } - } +} - pub fn is_unknown(&self) -> bool { - match &self { - RustType::UnknownLiteral | RustType::UnknownIntersection => true, - RustType::Array(t) => t.is_unknown(), - RustType::Map(t1, t2) => t1.is_unknown() || t2.is_unknown(), - RustType::Unknown - | RustType::String { .. } - | RustType::Number - | RustType::Boolean - | RustType::Custom(_) - | RustType::Unit => false, +impl<'cx> DefinitionPath<'cx> { + pub fn as_root(&self) -> Option<&DefinitionRoot<'cx>> { + if let Self::Root(v) = self { + Some(v) + } else { + None } } - - pub fn is_borrowed(&self) -> bool { + pub fn root(&self) -> &DefinitionRoot<'cx> { match self { - RustType::String { is_borrowed } => *is_borrowed, - RustType::Custom(t) => t.is_borrowed, - RustType::Array(t) => t.is_borrowed(), - RustType::Map(t1, t2) => t1.is_borrowed() || t2.is_borrowed(), - RustType::Number - | RustType::Boolean - | RustType::Unit - | RustType::Unknown - | RustType::UnknownLiteral - | RustType::UnknownIntersection => false, + Self::Root(v) => v, + Self::Field { parent, .. } => parent.root(), } } +} - pub fn as_custom(&self) -> Option<&TypeName> { - if let Self::Custom(t) = self { - Some(t) - } else { - None - } +impl<'cx> Path<'cx> { + /// Creates a new root path with the given definition root. + pub fn mk_root(cx: &Context<'cx>, root: DefinitionRoot<'cx>) -> Self { + cx.intern_path(DefinitionPath::Root(root)) } - pub fn as_mut_custom(&mut self) -> Option<&mut TypeName> { - if let Self::Custom(t) = self { - Some(t) - } else { - None - } + /// Creates a new field path from an existing parent path and field name. + pub fn descend(self, cx: &Context<'cx>, field: Str<'cx>) -> Self { + cx.intern_path(DefinitionPath::Field { + parent: self.0, + field, + }) } +} - pub fn get_using(&self) -> Option<&TypeName> { - if let Self::Array(t) = self { - t.get_using() - } else { - self.as_custom() - } - } +/// A root definition name from the schema, optionally including a variant tag. +/// +/// For example, the key `pull_request$opened` is interpreted as: +/// - `base`: `"pull_request"` +/// - `variant`: `Some("opened")` +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct DefinitionRoot<'cx> { + /// The main name of the schema definition (e.g., `"pull_request"`). + pub base: Str<'cx>, + + /// An optional discriminant tag, used in tagged unions (e.g., `"opened"`). + pub variant: Option>, +} + +#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)] +pub struct FieldPath<'cx>(pub &'cx FieldTreeNode<'cx>); + +#[derive(Debug, PartialEq, Eq, Hash)] +/// One segment node in the shared path tree. +pub struct FieldTreeNode<'cx> { + pub field_name: Str<'cx>, + parent: Option>, + child: FieldTreeChildArray<'cx>, +} - /// Returns `true` if the rust type is [`String`]. - /// - /// [`String`]: RustType::String - #[must_use] - pub fn is_string(&self) -> bool { - matches!(self, Self::String { .. }) +impl<'cx> FieldTreeNode<'cx> { + fn new(field_name: Str<'cx>, parent: Option>) -> Self { + Self { + field_name, + parent, + child: FieldTreeChildArray::new(), + } } } -pub struct RustStruct { - pub attr: RustContainerAttrs, - pub name: String, - pub comment: Option, - pub is_borrowed: bool, - pub member: Vec, +#[derive(Default, Debug, PartialEq, Eq)] +/// Fixed-capacity child container with interior mutability. +pub struct FieldTreeChildArray<'cx> { + content: RefCell>>, } -impl RustStruct { - pub fn from_members( - name: String, - comment: Option, - members: impl Iterator, - ) -> Self { +impl<'cx> FieldTreeChildArray<'cx> { + pub fn new() -> Self { Self { - attr: RustContainerAttrs::new(), - name, - comment, - is_borrowed: false, - member: members.collect(), + content: RefCell::new(Vec::new()), } } -} -pub type RustContainerAttrs = Attrs; + pub fn push(&self, child: FieldPath<'cx>) { + self.content.borrow_mut().push(child); + } +} -pub enum RustStructAttr { - Serde(SerdeContainerAttr), +impl Hash for FieldTreeChildArray<'_> { + fn hash(&self, state: &mut H) { + self.content.borrow().hash(state); + } } -impl RustStructAttr { - pub fn as_serde(&self) -> Option<&SerdeContainerAttr> { - let Self::Serde(v) = self; - Some(v) +impl<'cx> FieldPath<'cx> { + pub fn new(cx: &Context<'cx>, field: Str<'cx>, parent: Option>) -> Self { + let this = cx.alloc_field_path(FieldTreeNode::new(field, parent)); + if let Some(parent) = parent { + parent.0.child.push(this); + } + this } } -pub enum SerdeContainerAttr { - RenameAll(RenameRule), - Tag(String), - Untagged, +/// A human-readable comment block describing a type or member. +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct Doc<'cx> { + pub title: Option>, + pub description: Option>, } -impl SerdeContainerAttr { - /// Returns `true` if the serde container attr is [`Tag`]. - /// - /// [`Tag`]: SerdeContainerAttr::Tag - #[must_use] - pub fn is_tag(&self) -> bool { - matches!(self, Self::Tag(..)) +impl<'cx> Doc<'cx> { + /// Returns `true` if the documentation is empty. + pub fn is_empty(&self) -> bool { + self.title.is_none() && self.description.is_none() } } -#[derive(PartialEq)] -pub enum SerdeFieldAttr { - Rename(String), - Flatten, - Borrow, +#[derive(Debug, PartialEq, Eq, Hash)] +/// A struct-like object with named fields. +pub struct Struct<'cx> { + pub fields: Vec>, + pub additional: Additional<'cx>, } -pub enum SerdeVariantAttr { - Rename(String), - Borrow, -} +/// A field within a struct. +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct Field<'cx> { + /// Original name in the schema. + pub name: Str<'cx>, + + /// The type of the field. + pub ty: Ty<'cx>, -pub enum RenameRule { - PascalCase, - SnakeCase, - ScreamingSnakeCase, + /// Whether this field is required to be present. + pub required: bool, + + /// Optional documentation. + pub doc: Doc<'cx>, } -impl RenameRule { - /// Returns `true` if the rename rule is [`PascalCase`]. - /// - /// [`PascalCase`]: RenameRule::PascalCase - #[must_use] - pub fn is_pascal_case(&self) -> bool { - matches!(self, Self::PascalCase) - } - pub fn convert_to_pascal(&self, s: &mut String) { - match self { - RenameRule::PascalCase => (), - RenameRule::SnakeCase | RenameRule::ScreamingSnakeCase => { - *s = s - .split('_') - .map(|term| { - let mut term = term.to_ascii_lowercase(); - if let Some(c) = term.chars().next() { - let capital_ch = c.to_ascii_uppercase(); - term.replace_range(..1, &capital_ch.to_string()); - } - term - }) - .collect::>() - .concat(); - } - } - } - pub fn convert_to_snake(&self, s: &mut String) { - match self { - RenameRule::PascalCase => { - *s = s - .chars() - .enumerate() - .fold(String::new(), |mut snake, (i, c)| { - if i > 0 && c.is_uppercase() { - snake.push('_'); - } - snake.push(c.to_ascii_lowercase()); - snake - }); - } - _ => unimplemented!(), - } - } +#[derive(Debug, PartialEq, Eq, Hash)] +/// A fixed string, integer, or boolean enum. +pub struct ConstEnum<'cx> { + pub variants: Vec>, } -impl ToString for RenameRule { - fn to_string(&self) -> String { - match self { - RenameRule::PascalCase => "PascalCase", - RenameRule::SnakeCase => "snake_case", - RenameRule::ScreamingSnakeCase => "SCREAMING_SNAKE_CASE", - } - .to_string() - } +/// A fixed constant variant in an enum. +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct ConstEnumVariant<'cx> { + /// The literal value. + pub value: Str<'cx>, } -pub struct RustEnum { - pub attr: RustContainerAttrs, - pub name: String, - pub comment: Option, - pub is_borrowed: bool, - pub member: Vec, +#[derive(Debug, PartialEq, Eq, Hash)] +/// A tagged union with a discriminant field. +/// +/// The tag field is always "action" in GitHub schemas. +pub struct TaggedEnum<'cx> { + pub variants: Vec>, } -impl RustEnum { - pub fn from_members( - name: String, - comment: Option, - members: impl Iterator, - ) -> Self { - Self { - attr: RustContainerAttrs::new(), - name, - comment, - is_borrowed: false, - member: members.collect(), - } - } +#[derive(Debug, PartialEq, Eq, Hash)] +/// A variant in a tagged union. +pub struct TaggedEnumVariant<'cx> { + pub tag_value: Str<'cx>, } -pub struct RustStructMember { - pub attr: RustFieldAttrs, - pub name: String, - pub ty: RustMemberType, - pub comment: Option, +#[derive(Debug, PartialEq, Eq, Hash)] +/// An untagged union type with a fixed set of variants. +pub struct UntaggedEnum<'cx> { + pub variants: Vec>, } -pub type RustFieldAttrs = Attrs; +/// A composable type expression usable in Rust and `serde`. +#[derive(Debug, PartialEq, Eq, Hash)] +pub enum TyKind<'cx> { + /// A reference to a named type (defined elsewhere). + Ident(Path<'cx>), + + /// A fixed string, integer, or boolean enum. + ConstEnum(ConstEnum<'cx>), -#[derive(Default)] -pub struct Attrs(Vec); + /// A tagged union with a discriminant field. + TaggedEnum(TaggedEnum<'cx>), -impl Attrs { - pub fn add_attr(&mut self, a: T) { - self.0.push(a) + /// An untagged union type with a fixed set of variants. + UntaggedEnum(UntaggedEnum<'cx>), + + /// A struct-like object with named fields. + Struct(Struct<'cx>), + + /// A type that overrides a field in a base type. + Override(Override<'cx>), + + /// A primitive leaf type. + Primitive(Primitive), + + /// An optional value. + Option(Ty<'cx>), + + /// A list of homogeneous items. + Vec(Ty<'cx>), + + /// A vector of heterogeneous items. + AnyVec, +} + +impl<'cx> Ty<'cx> { + fn from_kind(cx: &Context<'cx>, kind: TyKind<'cx>, path: Path<'cx>) -> Self { + Self { + kind: cx.intern_ty(kind), + path, + } } - pub fn from_attr(a: T) -> Self { - let mut s = Self::new(); - s.add_attr(a); - s + + /// Creates a new type that references a named type. + pub fn mk_ident(cx: &Context<'cx>, referee: Path<'cx>, path: Path<'cx>) -> Ty<'cx> { + Self::from_kind(cx, TyKind::Ident(referee), path) } - pub fn new() -> Self { - Self(Vec::new()) + + /// Creates a new type that has a fixed set of variants. + pub fn mk_const_enum( + cx: &Context<'cx>, + variants: Vec>, + path: Path<'cx>, + ) -> Ty<'cx> { + Self::from_kind(cx, TyKind::ConstEnum(ConstEnum { variants }), path) } - pub fn as_inner(&self) -> &Vec { - &self.0 + /// Creates a new tagged union type with a discriminant field. + pub fn mk_tagged_enum( + cx: &Context<'cx>, + variants: Vec>, + path: Path<'cx>, + ) -> Ty<'cx> { + Self::from_kind(cx, TyKind::TaggedEnum(TaggedEnum { variants }), path) } - pub fn retain bool>(&mut self, f: F) { - self.0.retain(f) + /// Creates a new untagged union type with a fixed set of variants. + pub fn mk_untagged_enum(cx: &Context<'cx>, variants: Vec>, path: Path<'cx>) -> Ty<'cx> { + Self::from_kind(cx, TyKind::UntaggedEnum(UntaggedEnum { variants }), path) } -} -#[derive(PartialEq)] -pub enum RustFieldAttr { - Serde(SerdeFieldAttr), -} + /// Creates a new struct-like type with named fields. + pub fn mk_struct( + cx: &Context<'cx>, + fields: Vec>, + additional: Additional<'cx>, + path: Path<'cx>, + ) -> Ty<'cx> { + Self::from_kind(cx, TyKind::Struct(Struct { fields, additional }), path) + } -pub struct RustMemberType { - pub ty: RustType, - pub is_optional: bool, -} + /// Creates a new override type that modifies a field in a base type. + pub fn mk_override(cx: &Context<'cx>, override_: Override<'cx>, path: Path<'cx>) -> Ty<'cx> { + Self::from_kind(cx, TyKind::Override(override_), path) + } -impl RustMemberType { - pub fn is_unknown(&self) -> bool { - self.ty.is_unknown() + /// Creates a new primitive type. + pub fn mk_primitive(cx: &Context<'cx>, primitive: Primitive, path: Path<'cx>) -> Ty<'cx> { + Self::from_kind(cx, TyKind::Primitive(primitive), path) } -} -pub struct RustEnumMember { - pub attr: RustVariantAttrs, - pub kind: RustEnumMemberKind, -} + /// Creates a new string type with an optional format. + pub fn mk_string(cx: &Context<'cx>, format: Option, path: Path<'cx>) -> Ty<'cx> { + Self::from_kind(cx, TyKind::Primitive(Primitive::String { format }), path) + } -impl From for RustEnumMember { - fn from(value: RustEnumMemberKind) -> Self { - Self { - attr: RustVariantAttrs::new(), - kind: value, - } + /// Creates a new boolean type with an optional constant value. + pub fn mk_boolean(cx: &Context<'cx>, constant: Option, path: Path<'cx>) -> Ty<'cx> { + Self::from_kind(cx, TyKind::Primitive(Primitive::Boolean { constant }), path) } -} -pub enum RustEnumMemberKind { - Nullary(String), - /// has the same ident. this is unary - Unary(RustType), - UnaryNamed { - variant_name: String, - type_name: RustType, - }, -} + /// Creates a new integer type with an optional constant value. + pub fn mk_integer(cx: &Context<'cx>, constant: Option, path: Path<'cx>) -> Ty<'cx> { + Self::from_kind(cx, TyKind::Primitive(Primitive::Integer { constant }), path) + } -impl RustEnumMemberKind { - pub fn name_unary(&mut self, variant_name: String) { - match self { - RustEnumMemberKind::Unary(u) => { - *self = Self::UnaryNamed { - variant_name, - type_name: u.clone(), - } - } - _ => unreachable!("do not call with this"), - } + /// Creates a new number type. + pub fn mk_number(cx: &Context<'cx>, path: Path<'cx>) -> Ty<'cx> { + Self::from_kind(cx, TyKind::Primitive(Primitive::Number), path) } - /// Returns `true` if the rust enum member is [`Nullary`]. - /// - /// [`Nullary`]: RustEnumMember::Nullary - #[must_use] - pub fn is_nullary(&self) -> bool { - matches!(self, Self::Nullary(..)) + /// Creates a new null type. + pub fn mk_null(cx: &Context<'cx>, path: Path<'cx>) -> Ty<'cx> { + Self::from_kind(cx, TyKind::Primitive(Primitive::Null), path) } - pub fn as_unary(&self) -> Option<&RustType> { - if let Self::Unary(v) = self { - Some(v) - } else { - None - } + /// Creates a new optional type that wraps another type. + pub fn mk_option(cx: &Context<'cx>, inner: Ty<'cx>, path: Path<'cx>) -> Ty<'cx> { + Self::from_kind(cx, TyKind::Option(inner), path) } - pub fn as_type(&self) -> Option<&RustType> { - match self { - RustEnumMemberKind::Nullary(..) => None, - RustEnumMemberKind::Unary(t) => Some(t), - RustEnumMemberKind::UnaryNamed { type_name, .. } => Some(type_name), - } + /// Creates a new vector type that contains homogeneous items of the given type. + pub fn mk_vec(cx: &Context<'cx>, inner: Ty<'cx>, path: Path<'cx>) -> Ty<'cx> { + Self::from_kind(cx, TyKind::Vec(inner), path) } - pub fn as_type_mut(&mut self) -> Option<&mut RustType> { - match self { - RustEnumMemberKind::Nullary(..) => None, - RustEnumMemberKind::Unary(t) => Some(t), - RustEnumMemberKind::UnaryNamed { type_name, .. } => Some(type_name), - } + /// Creates a new vector type that can contain heterogeneous items. + pub fn mk_any_vec(cx: &Context<'cx>, path: Path<'cx>) -> Ty<'cx> { + Self::from_kind(cx, TyKind::AnyVec, path) } -} -pub type RustVariantAttrs = Attrs; - -pub enum RustVariantAttr { - Serde(SerdeVariantAttr), -} - -pub struct RustAlias { - pub name: String, - pub is_borrowed: bool, - pub comment: Option, - pub ty: RustType, -} - -pub type LiteralKeyMap = HashMap>; - -pub fn type_deps(segments: &[RustSegment]) -> CoDirectedAcyclicGraph { - let index_map: HashMap<_, _> = segments - .iter() - .enumerate() - .map(|(i, s)| (s.name(), i)) - .collect(); - let mut type_deps = CoDirectedAcyclicGraph::new(); - for (i, segment) in segments.iter().enumerate() { - let children: Vec<_> = match segment { - RustSegment::Struct(s) => s - .member - .iter() - .flat_map(|m| m.ty.ty.get_using()) - .map(|t| t.name.as_str()) - .collect(), - RustSegment::Enum(e) => e - .member - .iter() - .flat_map(|m| m.kind.as_type()) - .flat_map(|m| m.as_custom()) - .map(|tn| tn.name.as_str()) - .collect(), - RustSegment::Alias(a) => { - a.ty.get_using() - .map(|t| t.name.as_str()) - .into_iter() - .collect() - } - }; - for child in children { - if let Some(to) = index_map.get(child) { - type_deps.add_edge(i, *to); - } - } + pub fn is_null(&self) -> bool { + matches!(self.kind.0, TyKind::Primitive(Primitive::Null)) } - type_deps +} + +/// Extra keys allowed in a struct object. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum Additional<'cx> { + /// Disallows arbitrary keys. + None, + + /// All additional keys must have this type. + Typed(Ty<'cx>), + + /// Allows arbitrary keys. + Any, +} + +/// A type that overrides a field in a base type. +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct Override<'cx> { + /// The base type to which this override applies. + pub base_ty: Path<'cx>, + + /// A list of overrides that modify the base type. + pub fields: Vec>, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct Overrides<'cx> { + /// Field path. + pub field_path: FieldPath<'cx>, + + pub content: OverrideToField<'cx>, +} + +/// An override to field that modifies a field in a base type. +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct OverrideToField<'cx> { + pub required: bool, + + /// Optional documentation for the field. + pub doc: Doc<'cx>, + + /// An override applied to the field. + pub ty: Ty<'cx>, +} + +/// Rust-compatible primitive types. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum Primitive { + String { format: Option }, + Boolean { constant: Option }, + Integer { constant: Option }, + Number, + Null, +} + +/// Well-known string formats used in GitHub schemas. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum StringFormat { + DateTime, + Uri, + Uuid, } diff --git a/type-generator/src/lib.rs b/type-generator/src/lib.rs index dcb67ed..68ac6c5 100644 --- a/type-generator/src/lib.rs +++ b/type-generator/src/lib.rs @@ -1,203 +1,6 @@ -pub mod case; -mod dag; -mod frontend; +// pub mod codegen; +pub mod arena; +pub mod context; +pub mod intern; pub mod ir; -mod to_tokens; -mod transformer; - -use frontend::FrontendState; -use std::{ - collections::HashMap, - path::{Path, PathBuf}, -}; - -use swc_common::{ - self, - errors::{ColorConfig, Handler}, - sync::Lrc, - SourceMap, -}; - -use swc_ecma_parser::{lexer::Lexer, Capturing, Parser, StringInput, Syntax}; - -use ir::{type_deps, LiteralKeyMap, RustAlias, RustSegment, RustType, TypeName}; - -pub fn dts2rs(dts_file: &PathBuf) -> proc_macro2::TokenStream { - let ExtractedModule { module, comments } = extract_module(dts_file); - - let mut segments = Vec::new(); - - let mut st = FrontendState { - segments: &mut segments, - comments: &comments, - name_types: Default::default(), - }; - - // candidate for discriminated union using literal - // type name -> prop name -> literal value - let mut lkm: LiteralKeyMap = HashMap::new(); - - for b in &module.body { - let b = b.as_module_decl().unwrap(); - let b = b.as_export_decl().expect("module have only exports"); - let comment = st.get_comment(b.span.lo); - let decl = &b.decl; - - //dbg!(&decl); - match decl { - swc_ecma_ast::Decl::TsInterface(interface) => { - //let name = interface.id.sym.as_ref(); - //match name { - // "CheckRunCreatedEvent" | "GollumEvent" => continue, - // _ => {} - //} - - frontend::interface2struct(&mut st, interface, comment, &mut lkm); - } - swc_ecma_ast::Decl::TsTypeAlias(talias) => { - let ident = talias.id.sym.as_ref(); - - // lazy skip - if ident == "WebhookEvents" { - st.segments.push(RustSegment::Alias(RustAlias { - name: "WebhookEvents".to_owned(), - is_borrowed: true, - comment, - ty: RustType::Array(Box::new(RustType::String { is_borrowed: true })), - })); - continue; //return Err(anyhow!("lazy skip")); - } - - let typ = &talias.type_ann; - match typ.as_ref() { - swc_ecma_ast::TsType::TsTypeRef(tref) => { - let rhs = tref.type_name.as_ident().unwrap().sym.as_ref(); - let rhs = rhs.to_owned(); - let a = RustSegment::Alias(RustAlias { - name: ident.to_owned(), - is_borrowed: false, - comment, - ty: RustType::Custom(TypeName { - name: rhs, - is_borrowed: false, - }), - }); - st.segments.push(a); - } - swc_ecma_ast::TsType::TsUnionOrIntersectionType(tuoi) => { - frontend::tunion2enum(&mut st, ident, tuoi, comment, &mut lkm, true); - } - swc_ecma_ast::TsType::TsKeywordType(..) - | swc_ecma_ast::TsType::TsArrayType(..) => { - // export type Hoge = number; - let typ = - frontend::ts_type_to_rs(&mut st, &mut None, typ, None, &mut lkm).1; - let a = RustSegment::Alias(RustAlias { - name: ident.to_owned(), - is_borrowed: false, - comment, - ty: typ, - }); - st.segments.push(a); - } - swc_ecma_ast::TsType::TsTypeOperator(_toperator) => { - // export type WebhookEventName = keyof EventPayloadMap; - //dbg!(toperator); - continue; - } - _ => { - dbg!(typ); - unreachable!() - } - } - } - _ => unreachable!(), - }; - //println!("{}", b.is_export_decl()); - } - // drop(st); - - for segment in &mut segments { - transformer::adapt_internal_tag(segment, &lkm); - transformer::adapt_rename_all(segment); - } - transformer::flatten_type(&mut segments); - let type_deps = type_deps(&segments); - transformer::adapt_borrow(&mut segments, &type_deps); - - segments - .into_iter() - .flat_map(|rss| rss.into_token_stream()) - .collect() -} - -struct ExtractedModule { - module: swc_ecma_ast::Module, - comments: swc_common::comments::SingleThreadedComments, -} - -fn extract_module(dts_file: &PathBuf) -> ExtractedModule { - let cm: Lrc = Default::default(); - let handler = Handler::with_tty_emitter(ColorConfig::Auto, true, false, Some(cm.clone())); - - // Real usage - let fm = cm - .load_file(Path::new(dts_file)) - .unwrap_or_else(|_| panic!("failed to load {}", &dts_file.display())); - - let comments = swc_common::comments::SingleThreadedComments::default(); - let lexer = Lexer::new( - Syntax::Typescript(Default::default()), - Default::default(), - StringInput::from(&*fm), - Some(&comments), - ); - - let capturing = Capturing::new(lexer); - - let mut parser = Parser::new_from(capturing); - - for e in parser.take_errors() { - e.into_diagnostic(&handler).emit(); - } - - ExtractedModule { - module: parser - .parse_module() - .map_err(|e| e.into_diagnostic(&handler).emit()) - .expect("Failed to parse module."), - comments, - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_module() { - let ExtractedModule { module, .. } = extract_module(&PathBuf::from("test.ts")); - - let ice = module.body[1] - .as_module_decl() - .unwrap() - .as_export_decl() - .unwrap() - .decl - .as_ts_type_alias() - .unwrap() - .type_ann - .as_ts_union_or_intersection_type() - .unwrap() - .as_ts_union_type() - .unwrap() - .types[0] - .as_ts_type_ref() - .unwrap() - .type_name - .as_ident() - .unwrap() - .as_ref(); - assert_eq!(ice, "IssueCommentCreatedEvent"); - } -} +pub mod visitor; diff --git a/type-generator/src/main.rs b/type-generator/src/main.rs deleted file mode 100644 index bc545b3..0000000 --- a/type-generator/src/main.rs +++ /dev/null @@ -1,17 +0,0 @@ -use std::path::PathBuf; - -use structopt::StructOpt; - -use github_webhook_type_generator::*; - -#[derive(Debug, StructOpt)] -struct Opt { - dts_file: PathBuf, -} - -fn main() { - let opt = Opt::from_args(); - - let rs = dts2rs(&opt.dts_file); - print!("{}", rs); -} diff --git a/type-generator/src/transformer.rs b/type-generator/src/transformer.rs deleted file mode 100644 index f6e39e8..0000000 --- a/type-generator/src/transformer.rs +++ /dev/null @@ -1,10 +0,0 @@ -mod borrow; -mod flatten_type; -mod internal_tag; -mod rename_all; -mod retype; - -pub use borrow::adapt_borrow; -pub use flatten_type::flatten_type; -pub use internal_tag::adapt_internal_tag; -pub use rename_all::adapt_rename_all; diff --git a/type-generator/src/transformer/borrow.rs b/type-generator/src/transformer/borrow.rs deleted file mode 100644 index 3a93725..0000000 --- a/type-generator/src/transformer/borrow.rs +++ /dev/null @@ -1,114 +0,0 @@ -use std::collections::HashSet; - -use crate::{ - dag::CoDirectedAcyclicGraph, - ir::{ - RustFieldAttr, RustSegment, RustType, RustVariantAttr, SerdeFieldAttr, SerdeVariantAttr, - TypeName, - }, -}; - -pub fn adapt_borrow(segments: &mut [RustSegment], type_deps: &CoDirectedAcyclicGraph) { - let mut decorated: HashSet = HashSet::new(); - let sorted = match type_deps.co_topo_sort() { - Ok(s) => s, - Err(cy) => { - let mut msg = segments.get(cy[0]).unwrap().name().to_owned(); - for index in cy { - let seg = segments.get(index).unwrap().name(); - msg.push_str(&format!("\n -> {}", seg)); - } - panic!("cyclic dependency detected (this is a bug):\n{}", msg); - } - }; - for index in sorted { - let seg = segments.get_mut(index).unwrap(); - fn borrow_typename( - TypeName { name, is_borrowed }: &mut TypeName, - did_borrow: &mut bool, - decorated: &HashSet, - ) { - if decorated.contains(name) { - *is_borrowed = true; - *did_borrow = true; - } - } - fn borrow_type(ty: &mut RustType, did_borrow: &mut bool, decorated: &HashSet) { - match ty { - RustType::String { is_borrowed } => { - *is_borrowed = true; - *did_borrow = true; - } - RustType::Number => (), - RustType::Boolean => (), - RustType::Custom(t) => { - borrow_typename(t, did_borrow, decorated); - } - RustType::Array(t) => borrow_type(t, did_borrow, decorated), - RustType::Unit => (), - RustType::Unknown => (), - RustType::UnknownLiteral => (), - RustType::UnknownIntersection => (), - RustType::Map(t1, t2) => { - borrow_type(t1, did_borrow, decorated); - borrow_type(t2, did_borrow, decorated); - } - } - } - let mut did_borrow = false; - match seg { - RustSegment::Struct(s) => { - let mut visible = false; - for mem in &mut s.member { - borrow_type(&mut mem.ty.ty, &mut did_borrow, &decorated); - visible |= mem.ty.ty.is_string(); - } - if did_borrow { - if !visible { - for mem in &mut s.member { - if mem.ty.ty.is_borrowed() { - mem.attr - .add_attr(RustFieldAttr::Serde(SerdeFieldAttr::Borrow)); - break; - } - } - } - s.is_borrowed = true; - decorated.insert(s.name.to_owned()); - } - } - RustSegment::Enum(e) => { - let mut visible = false; - for mem in &mut e.member { - if let Some(t) = mem.kind.as_type_mut() { - borrow_type(t, &mut did_borrow, &decorated); - visible |= t.is_string(); - } - } - if did_borrow { - if !visible { - for mem in &mut e.member { - if let Some(t) = mem.kind.as_type() { - if t.is_borrowed() { - mem.attr - .add_attr(RustVariantAttr::Serde(SerdeVariantAttr::Borrow)); - break; - } - } - } - } - e.is_borrowed = true; - decorated.insert(e.name.to_owned()); - } - } - RustSegment::Alias(a) => { - let ty = &mut a.ty; - borrow_type(ty, &mut did_borrow, &decorated); - if did_borrow { - a.is_borrowed = true; - decorated.insert(a.name.to_owned()); - } - } - } - } -} diff --git a/type-generator/src/transformer/flatten_type.rs b/type-generator/src/transformer/flatten_type.rs deleted file mode 100644 index cdfff6f..0000000 --- a/type-generator/src/transformer/flatten_type.rs +++ /dev/null @@ -1,29 +0,0 @@ -use std::collections::HashMap; - -use crate::ir::{RustFieldAttr, RustMemberType, RustSegment, RustType, SerdeFieldAttr}; - -use super::retype; - -/// flattens type with only one field attributed `#[serde(flatten)]` -pub fn flatten_type(segments: &mut Vec) { - let mut retype_map: HashMap = HashMap::new(); - segments.retain_mut(|segment| match segment { - RustSegment::Struct(s) => { - if s.member.len() == 1 { - let r = s.member.first().unwrap(); - if r.attr - .as_inner() - .contains(&RustFieldAttr::Serde(SerdeFieldAttr::Flatten)) - { - let RustMemberType { ty, is_optional } = &r.ty; - assert!(!is_optional); - retype_map.insert(s.name.to_owned(), ty.to_owned()); - return false; - } - } - true - } - _ => true, - }); - retype::retype(segments, retype_map) -} diff --git a/type-generator/src/transformer/internal_tag.rs b/type-generator/src/transformer/internal_tag.rs deleted file mode 100644 index 21b02fe..0000000 --- a/type-generator/src/transformer/internal_tag.rs +++ /dev/null @@ -1,49 +0,0 @@ -use std::collections::HashMap; - -use crate::ir::{LiteralKeyMap, RustSegment, RustStructAttr, SerdeContainerAttr}; - -/// find tag from rust enum and attr to it. -pub fn adapt_internal_tag(segment: &mut RustSegment, lkm: &LiteralKeyMap) -> Option<()> { - if let RustSegment::Enum(re) = segment { - let mut cand_props: HashMap = Default::default(); - for memb in &re.member { - let tname = &memb.kind.as_unary()?.as_custom()?.name; - let props = lkm.get(tname)?; - if cand_props.is_empty() { - cand_props = props.clone(); - continue; - } - // calc intersection of all enum members - cand_props.retain(|k, _| props.contains_key(k)); - if cand_props.is_empty() { - return None; - } - } - assert!(!cand_props.is_empty()); - if cand_props.len() != 1 { - return None; - } - let tag_name = cand_props.keys().next().unwrap().to_owned(); - - // validate and collect tag name - let mut variant_names = Vec::new(); - - for memb in &re.member { - let inter = &memb.kind.as_unary().unwrap().as_custom().unwrap().name; - let variant_name = lkm.get(inter).unwrap().get(&tag_name).unwrap().to_owned(); - if variant_names.contains(&variant_name) { - return None; - } - variant_names.push(variant_name); - } - - for (memb, variant_name) in re.member.iter_mut().zip(variant_names) { - memb.kind.name_unary(variant_name); - } - re.attr - .retain(|r| !matches!(r, RustStructAttr::Serde(SerdeContainerAttr::Untagged))); - re.attr - .add_attr(RustStructAttr::Serde(SerdeContainerAttr::Tag(tag_name))); - } - Some(()) -} diff --git a/type-generator/src/transformer/rename_all.rs b/type-generator/src/transformer/rename_all.rs deleted file mode 100644 index 59f0945..0000000 --- a/type-generator/src/transformer/rename_all.rs +++ /dev/null @@ -1,53 +0,0 @@ -use crate::{ - case::{detect_case, CaseConvention}, - ir::{RustEnumMemberKind, RustSegment, RustStructAttr, RustType, SerdeContainerAttr}, -}; - -pub fn adapt_rename_all(segment: &mut RustSegment) -> Option<()> { - if let RustSegment::Enum(re) = segment { - let mut conv: Option = None; - for memb in &re.member { - let s = match &memb.kind { - RustEnumMemberKind::Nullary(v) => v, - RustEnumMemberKind::Unary(v) => v.to_ident(), - RustEnumMemberKind::UnaryNamed { variant_name, .. } => variant_name, - }; - match conv.as_mut() { - Some(conv) => { - let new = detect_case(s); - conv.cast(new)? - } - None => { - conv = Some(detect_case(s)); - } - }; - } - let rr = conv?.into_rename_rule(); - if rr.is_pascal_case() { - return None; - } - for memb in &mut re.member { - match &mut memb.kind { - RustEnumMemberKind::Unary(v) => { - if let Some(v) = v.as_mut_custom() { - let type_name = v.to_owned(); - rr.convert_to_pascal(&mut v.name); - memb.kind = RustEnumMemberKind::UnaryNamed { - variant_name: v.name.to_owned(), - type_name: RustType::Custom(type_name), - }; - } - } - RustEnumMemberKind::Nullary(variant_name) => { - rr.convert_to_pascal(variant_name); - } - RustEnumMemberKind::UnaryNamed { variant_name, .. } => { - rr.convert_to_pascal(variant_name); - } - }; - } - re.attr - .add_attr(RustStructAttr::Serde(SerdeContainerAttr::RenameAll(rr))); - } - Some(()) -} diff --git a/type-generator/src/transformer/retype.rs b/type-generator/src/transformer/retype.rs deleted file mode 100644 index bacb8bd..0000000 --- a/type-generator/src/transformer/retype.rs +++ /dev/null @@ -1,59 +0,0 @@ -use std::collections::HashMap; - -use crate::ir::{RustEnumMemberKind, RustSegment, RustType}; - -pub fn retype(segments: &mut Vec, map: HashMap) { - trait Retype { - fn retype_custom(&mut self, map: &HashMap); - } - impl Retype for RustType { - fn retype_custom(&mut self, map: &HashMap) { - match self { - RustType::Array(t) => { - t.retype_custom(map); - } - RustType::Map(t1, t2) => { - t1.retype_custom(map); - t2.retype_custom(map); - } - RustType::Custom(n) => { - if let Some(ty) = map.get(&n.name) { - *self = ty.to_owned(); - } - } - RustType::String { .. } - | RustType::Number - | RustType::Boolean - | RustType::Unit - | RustType::Unknown - | RustType::UnknownLiteral - | RustType::UnknownIntersection => (), - } - } - } - for segment in segments { - match segment { - RustSegment::Struct(s) => { - for m in &mut s.member { - m.ty.ty.retype_custom(&map); - } - } - RustSegment::Enum(e) => { - for m in &mut e.member { - match &mut m.kind { - RustEnumMemberKind::Nullary(..) => (), - RustEnumMemberKind::Unary(u) => { - u.retype_custom(&map); - } - RustEnumMemberKind::UnaryNamed { type_name, .. } => { - type_name.retype_custom(&map); - } - } - } - } - RustSegment::Alias(a) => { - a.ty.retype_custom(&map); - } - } - } -} diff --git a/type-generator/src/visitor.rs b/type-generator/src/visitor.rs new file mode 100644 index 0000000..99ac243 --- /dev/null +++ b/type-generator/src/visitor.rs @@ -0,0 +1,257 @@ +use crate::ir::{ + Additional, ConstEnum, ConstEnumVariant, Definition, Doc, Field, FieldPath, Module, Override, + OverrideToField, Overrides, Path, Primitive, Struct, TaggedEnum, TaggedEnumVariant, Ty, TyKind, + UntaggedEnum, +}; + +/// Distinguishes between the context in which a path appears. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PathSite { + /// Path is used in a type position. + Use, + /// Path is used to define a type. + Def, +} + +/// Trait for recursively walking over IR elements. +/// +/// Users can override `visit_*` methods to customize traversal. +/// The default implementations call `super_*`, which perform structural traversal. +pub trait Visitor<'cx> { + fn visit_module(&mut self, module: &Module<'cx>) { + self.super_module(module); + } + + fn super_module(&mut self, module: &Module<'cx>) { + let Module { + definitions, + variants, + } = module; + self.visit_definitions(definitions); + self.visit_toplevel_variants(variants); + } + + fn visit_toplevel_variants(&mut self, variants: &[Path<'cx>]) { + self.super_toplevel_variants(variants); + } + + fn super_toplevel_variants(&mut self, variants: &[Path<'cx>]) { + for variant in variants { + self.visit_toplevel_variant(variant); + } + } + + fn visit_toplevel_variant(&mut self, variant: &Path<'cx>) { + self.super_toplevel_variant(variant); + } + + fn super_toplevel_variant(&mut self, variant: &Path<'cx>) { + self.visit_path(*variant, PathSite::Use); + } + + fn visit_definitions(&mut self, definitions: &[Definition<'cx>]) { + self.super_definitions(definitions); + } + + fn super_definitions(&mut self, definitions: &[Definition<'cx>]) { + for definition in definitions { + self.visit_definition(definition); + } + } + + fn visit_definition(&mut self, definition: &Definition<'cx>) { + self.super_definition(definition); + } + + fn super_definition(&mut self, definition: &Definition<'cx>) { + let Definition { doc, ty } = definition; + self.visit_doc(doc); + self.visit_ty(*ty); + } + + fn visit_doc(&mut self, doc: &Doc<'cx>) { + self.super_doc(doc); + } + + fn super_doc(&mut self, _doc: &Doc<'cx>) {} + + fn visit_ty(&mut self, ty: Ty<'cx>) { + self.super_ty(ty); + } + + fn super_ty(&mut self, ty: Ty<'cx>) { + let Ty { path, kind } = ty; + self.visit_path(path, PathSite::Use); + self.visit_ty_kind(*kind); + } + + fn visit_ty_kind(&mut self, kind: &TyKind<'cx>) { + self.super_ty_kind(kind); + } + + fn super_ty_kind(&mut self, kind: &TyKind<'cx>) { + match kind { + TyKind::Ident(path) => self.visit_path(*path, PathSite::Use), + TyKind::ConstEnum(const_enum) => self.visit_const_enum(const_enum), + TyKind::TaggedEnum(tagged_enum) => self.visit_tagged_enum(tagged_enum), + TyKind::UntaggedEnum(untagged_enum) => self.visit_untagged_enum(untagged_enum), + TyKind::Struct(s) => self.visit_struct_ty(s), + TyKind::Override(o) => self.visit_override(o), + TyKind::Primitive(p) => self.visit_primitive(*p), + TyKind::Option(ty) => self.visit_option(*ty), + TyKind::Vec(ty) => self.visit_vec(*ty), + TyKind::AnyVec => {} + } + } + + fn visit_const_enum(&mut self, const_enum: &ConstEnum<'cx>) { + self.super_const_enum(const_enum); + } + + fn super_const_enum(&mut self, const_enum: &ConstEnum<'cx>) { + let ConstEnum { variants } = const_enum; + for variant in variants { + self.visit_const_enum_variant(variant); + } + } + + fn visit_const_enum_variant(&mut self, variant: &ConstEnumVariant<'cx>) { + self.super_const_enum_variant(variant); + } + + fn super_const_enum_variant(&mut self, _variant: &ConstEnumVariant<'cx>) {} + + fn visit_tagged_enum(&mut self, tagged_enum: &TaggedEnum<'cx>) { + self.super_tagged_enum(tagged_enum); + } + + fn super_tagged_enum(&mut self, tagged_enum: &TaggedEnum<'cx>) { + let TaggedEnum { variants } = tagged_enum; + for variant in variants { + self.visit_tagged_enum_variant(variant); + } + } + + fn visit_tagged_enum_variant(&mut self, variant: &TaggedEnumVariant<'cx>) { + self.super_tagged_enum_variant(variant); + } + + fn super_tagged_enum_variant(&mut self, _variant: &TaggedEnumVariant<'cx>) {} + + fn visit_untagged_enum(&mut self, untagged_enum: &UntaggedEnum<'cx>) { + self.super_untagged_enum(untagged_enum); + } + + fn super_untagged_enum(&mut self, untagged_enum: &UntaggedEnum<'cx>) { + let UntaggedEnum { variants } = untagged_enum; + for variant in variants { + self.visit_ty(*variant); + } + } + + fn visit_struct_ty(&mut self, s: &Struct<'cx>) { + self.super_struct_ty(s); + } + + fn super_struct_ty(&mut self, s: &Struct<'cx>) { + let Struct { fields, additional } = s; + for field in fields { + self.visit_field(field); + } + self.visit_additional(*additional); + } + + fn visit_additional(&mut self, additional: Additional<'cx>) { + self.super_additional(additional); + } + + fn super_additional(&mut self, additional: Additional<'cx>) { + match additional { + Additional::None => {} + Additional::Typed(ty) => { + self.visit_ty(ty); + } + Additional::Any => {} + } + } + + fn visit_field(&mut self, field: &Field<'cx>) { + self.super_field(field); + } + + fn super_field(&mut self, field: &Field<'cx>) { + let Field { + name: _, + ty, + required: _, + doc, + } = field; + self.visit_ty(*ty); + self.visit_doc(doc); + } + + fn visit_override(&mut self, o: &Override<'cx>) { + self.super_override(o); + } + + fn super_override(&mut self, o: &Override<'cx>) { + let Override { base_ty, fields } = o; + self.visit_path(*base_ty, PathSite::Use); + for field in fields { + self.visit_override_field(field); + } + } + + fn visit_override_field(&mut self, field: &Overrides<'cx>) { + self.super_override_field(field); + } + + fn super_override_field(&mut self, field: &Overrides<'cx>) { + let Overrides { + field_path, + content: + OverrideToField { + required: _, + doc, + ty, + }, + } = field; + self.visit_field_path(*field_path); + self.visit_doc(doc); + self.visit_ty(*ty); + } + + fn visit_field_path(&mut self, path: FieldPath<'cx>) { + self.super_field_path(path); + } + + fn super_field_path(&mut self, _path: FieldPath<'cx>) {} + + fn visit_path(&mut self, path: Path<'cx>, site: PathSite) { + self.super_path(path, site); + } + + fn super_path(&mut self, _path: Path<'cx>, _site: PathSite) {} + + fn visit_primitive(&mut self, prim: Primitive) { + self.super_primitive(prim); + } + + fn super_primitive(&mut self, _prim: Primitive) {} + + fn visit_option(&mut self, ty: Ty<'cx>) { + self.super_option(ty); + } + + fn super_option(&mut self, ty: Ty<'cx>) { + self.visit_ty(ty); + } + + fn visit_vec(&mut self, ty: Ty<'cx>) { + self.super_vec(ty); + } + + fn super_vec(&mut self, ty: Ty<'cx>) { + self.visit_ty(ty); + } +} diff --git a/type-generator/test.ts b/type-generator/test.ts deleted file mode 100644 index a40d9b8..0000000 --- a/type-generator/test.ts +++ /dev/null @@ -1,53 +0,0 @@ -export type Schema = - | IssueCommentEvent - | IssuesEvent - -export type IssueCommentEvent = - | IssueCommentCreatedEvent - | IssueCommentDeletedEvent - | IssueCommentEditedEvent; - -export interface User { - login: string; - id: number; - node_id: string; - name?: string; - email?: string | null; - avatar_url: string; - received_events_url: string; - type: "Bot" | "User" | "Organization"; - site_admin: boolean; -} -export interface License { - key: string; - name: string; - spdx_id: string; - url: string | null; - node_id: string; -} - -export type WebhookEvent = Schema; - -export interface Label { - id: number; - node_id: string; - url: string; - name: string; - description: string | null; - color: string; - default: boolean; -} - - -export interface Reactions { - url: string; - total_count: number; - "+1": number; - "-1": number; - laugh: number; - hooray: number; - confused: number; - heart: number; - rocket: number; - eyes: number; -}