diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 00000000..f8fff109 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,31 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/rust +{ + "name": "Rust", + // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile + "image": "mcr.microsoft.com/devcontainers/rust:1-1-bullseye" + + // Use 'mounts' to make the cargo cache persistent in a Docker Volume. + // "mounts": [ + // { + // "source": "devcontainer-cargo-cache-${devcontainerId}", + // "target": "/usr/local/cargo", + // "type": "volume" + // } + // ] + + // Features to add to the dev container. More info: https://containers.dev/features. + // "features": {}, + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + + // Use 'postCreateCommand' to run commands after the container is created. + // "postCreateCommand": "rustc --version", + + // Configure tool-specific properties. + // "customizations": {}, + + // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. + // "remoteUser": "root" +} diff --git a/Cargo.lock b/Cargo.lock index eb057722..0c7ccde4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,25 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "abnf" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "087113bd50d9adce24850eed5d0476c7d199d532fce8fab5173650331e09033a" +dependencies = [ + "abnf-core", + "nom", +] + +[[package]] +name = "abnf-core" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c44e09c43ae1c368fb91a03a566472d0087c26cf7e1b9e8e289c14ede681dd7d" +dependencies = [ + "nom", +] + [[package]] name = "addr2line" version = "0.21.0" @@ -157,6 +176,15 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "bstr" version = "1.8.0" @@ -168,6 +196,30 @@ dependencies = [ "serde", ] +[[package]] +name = "btree-range-map" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1be5c9672446d3800bcbcaabaeba121fe22f1fb25700c4562b22faf76d377c33" +dependencies = [ + "btree-slab", + "cc-traits", + "range-traits", + "serde", + "slab", +] + +[[package]] +name = "btree-slab" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2b56d3029f075c4fa892428a098425b86cef5c89ae54073137ece416aef13c" +dependencies = [ + "cc-traits", + "slab", + "smallvec", +] + [[package]] name = "bumpalo" version = "3.14.0" @@ -201,6 +253,15 @@ dependencies = [ "libc", ] +[[package]] +name = "cc-traits" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "060303ef31ef4a522737e1b1ab68c67916f2a787bb2f4f54f383279adba962b5" +dependencies = [ + "slab", +] + [[package]] name = "cfg-if" version = "1.0.0" @@ -222,6 +283,33 @@ dependencies = [ "windows-targets 0.52.0", ] +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + [[package]] name = "clap" version = "4.5.23" @@ -253,7 +341,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.93", ] [[package]] @@ -314,12 +402,47 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" +[[package]] +name = "cpufeatures" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3" +dependencies = [ + "libc", +] + +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + [[package]] name = "difflib" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + [[package]] name = "doc-comment" version = "0.3.3" @@ -391,7 +514,7 @@ dependencies = [ "schemars", "serde", "serde_json", - "syn", + "syn 2.0.93", "typify", ] @@ -437,6 +560,16 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "gimli" version = "0.28.1" @@ -449,6 +582,16 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" +[[package]] +name = "half" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888" +dependencies = [ + "cfg-if", + "crunchy", +] + [[package]] name = "hashbrown" version = "0.14.3" @@ -465,6 +608,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hex_fmt" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b07f60793ff0a4d9cef0f18e63b5357e06209987153a64648c972c1e5aff336f" + [[package]] name = "home" version = "0.5.5" @@ -519,6 +668,12 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "indoc" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" + [[package]] name = "ipnetwork" version = "0.20.0" @@ -529,6 +684,27 @@ dependencies = [ "serde", ] +[[package]] +name = "iref" +version = "3.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374372d9ca7331cec26f307b12552554849143e6b2077be3553576aa9aa8258c" +dependencies = [ + "iref-core", +] + +[[package]] +name = "iref-core" +version = "3.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b10559a0d518effd4f2cee107f40f83acf8583dcd3e6760b9b60293b0d2c2a70" +dependencies = [ + "pct-str", + "smallvec", + "static-regular-grammar", + "thiserror 1.0.69", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.1" @@ -567,9 +743,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.151" +version = "0.2.169" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4" +checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" [[package]] name = "linux-raw-sys" @@ -589,6 +765,12 @@ version = "2.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.7.1" @@ -607,6 +789,16 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "num-traits" version = "0.2.17" @@ -643,6 +835,22 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + +[[package]] +name = "pct-str" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf1bdcc492c285a50bed60860dfa00b50baf1f60c73c7d6b435b01a2a11fd6ff" +dependencies = [ + "thiserror 1.0.69", + "utf8-decode", +] + [[package]] name = "pin-project-lite" version = "0.2.13" @@ -684,7 +892,31 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "64d1ec885c64d0457d564db4ec299b2dae3f9c02808b8ad9c3a089c591b18033" dependencies = [ "proc-macro2", - "syn", + "syn 2.0.93", +] + +[[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]] @@ -733,6 +965,12 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" +[[package]] +name = "range-traits" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d20581732dd76fa913c7dff1a2412b714afe3573e94d41c34719de73337cc8ab" + [[package]] name = "rdrand" version = "0.4.0" @@ -855,7 +1093,7 @@ dependencies = [ "proc-macro2", "quote", "schema-derive", - "syn", + "syn 2.0.93", ] [[package]] @@ -867,7 +1105,7 @@ dependencies = [ "paste", "proc-macro2", "quote", - "syn", + "syn 2.0.93", ] [[package]] @@ -892,7 +1130,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn", + "syn 2.0.93", ] [[package]] @@ -921,7 +1159,7 @@ checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.93", ] [[package]] @@ -932,7 +1170,7 @@ checksum = "330f01ce65a3a5fe59a60c82f3c9a024b573b8a6e875bd233fe5f934e71d54e3" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.93", ] [[package]] @@ -965,7 +1203,18 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn", + "syn 2.0.93", +] + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", ] [[package]] @@ -983,12 +1232,57 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2aeaf503862c419d66959f5d7ca015337d864e9c49485d771b732e2a20453597" +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "static-regular-grammar" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f4a6c40247579acfbb138c3cd7de3dab113ab4ac6227f1b7de7d626ee667957" +dependencies = [ + "abnf", + "btree-range-map", + "ciborium", + "hex_fmt", + "indoc", + "proc-macro-error", + "proc-macro2", + "quote", + "serde", + "sha2", + "syn 2.0.93", + "thiserror 1.0.69", +] + [[package]] name = "strsim" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01" +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "unicode-ident", +] + [[package]] name = "syn" version = "2.0.93" @@ -1070,7 +1364,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.93", ] [[package]] @@ -1081,7 +1375,7 @@ checksum = "7b50fa271071aae2e6ee85f842e2e28ba8cd2c5fb67f11fcb1fd70b276f9e7d4" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.93", ] [[package]] @@ -1197,6 +1491,12 @@ dependencies = [ "toml", ] +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + [[package]] name = "typify" version = "0.3.0" @@ -1224,8 +1524,10 @@ dependencies = [ "env_logger", "expectorate", "heck", + "iref", "log", "paste", + "pathdiff", "proc-macro2", "quote", "regress", @@ -1235,8 +1537,9 @@ dependencies = [ "semver", "serde", "serde_json", - "syn", + "syn 2.0.93", "thiserror 2.0.9", + "typify-macro", "unicode-ident", "uuid", ] @@ -1252,7 +1555,7 @@ dependencies = [ "serde", "serde_json", "serde_tokenstream", - "syn", + "syn 2.0.93", "typify-impl", ] @@ -1266,7 +1569,7 @@ dependencies = [ "schemars", "serde", "serde_json", - "syn", + "syn 2.0.93", "typify", ] @@ -1288,6 +1591,12 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" +[[package]] +name = "utf8-decode" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca61eb27fa339aa08826a29f03e87b99b4d8f0fc2255306fd266bb1b6a9de498" + [[package]] name = "utf8parse" version = "0.2.1" @@ -1355,7 +1664,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn", + "syn 2.0.93", "wasm-bindgen-shared", ] @@ -1377,7 +1686,7 @@ checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.93", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -1652,5 +1961,5 @@ checksum = "b3c129550b3e6de3fd0ba67ba5c81818f9805e58b8d7fee80a3a59d2c9fc601a" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.93", ] diff --git a/cargo-typify/src/lib.rs b/cargo-typify/src/lib.rs index 101453d3..4fa76465 100644 --- a/cargo-typify/src/lib.rs +++ b/cargo-typify/src/lib.rs @@ -57,6 +57,9 @@ pub struct CliArgs { value_parser = ["generate", "allow", "deny"] )] unknown_crates: Option, + + #[arg(short = 'D', long, default_value = "false")] + distinct_definitions: bool, } impl CliArgs { @@ -168,8 +171,10 @@ pub fn convert(args: &CliArgs) -> Result { }; settings.with_unknown_crates(unknown_crates); } + settings.with_distinct_definitions(args.distinct_definitions); let mut type_space = TypeSpace::new(&settings); + type_space.with_path(&args.input); type_space .add_root_schema(schema) .wrap_err("Schema conversion failed")?; @@ -202,6 +207,7 @@ mod tests { crates: vec![], map_type: None, unknown_crates: Default::default(), + distinct_definitions: false, }; assert_eq!(args.output_path(), None); @@ -218,6 +224,7 @@ mod tests { crates: vec![], map_type: None, unknown_crates: Default::default(), + distinct_definitions: false, }; assert_eq!(args.output_path(), Some(PathBuf::from("some_file.rs"))); @@ -234,6 +241,7 @@ mod tests { crates: vec![], map_type: None, unknown_crates: Default::default(), + distinct_definitions: false, }; assert_eq!(args.output_path(), Some(PathBuf::from("input.rs"))); @@ -250,6 +258,7 @@ mod tests { crates: vec![], map_type: Some("::std::collections::BTreeMap".to_string()), unknown_crates: Default::default(), + distinct_definitions: false, }; assert_eq!( @@ -269,6 +278,7 @@ mod tests { crates: vec![], map_type: None, unknown_crates: Default::default(), + distinct_definitions: false, }; assert!(args.use_builder()); @@ -285,6 +295,7 @@ mod tests { crates: vec![], map_type: None, unknown_crates: Default::default(), + distinct_definitions: false, }; assert!(!args.use_builder()); @@ -301,6 +312,7 @@ mod tests { crates: vec![], map_type: None, unknown_crates: Default::default(), + distinct_definitions: false, }; assert!(args.use_builder()); diff --git a/cargo-typify/tests/outputs/help.txt b/cargo-typify/tests/outputs/help.txt index 9daedee2..fe3a76c7 100644 --- a/cargo-typify/tests/outputs/help.txt +++ b/cargo-typify/tests/outputs/help.txt @@ -32,6 +32,9 @@ Options: [possible values: generate, allow, deny] + -D, --distinct-definitions + + -h, --help Print help (see a summary with '-h') diff --git a/typify-impl/Cargo.toml b/typify-impl/Cargo.toml index 90563302..9b5c3bd1 100644 --- a/typify-impl/Cargo.toml +++ b/typify-impl/Cargo.toml @@ -9,7 +9,9 @@ readme = "../README.md" [dependencies] heck = "0.5.0" +iref = "3.1.4" log = "0.4.22" +pathdiff = "0.2.1" proc-macro2 = "1.0.89" quote = "1.0.38" regress = "0.10.1" @@ -31,3 +33,4 @@ schema = "0.1.0" schemars = { version = "0.8.21", features = ["uuid1", "impl_json_schema"] } syn = { version = "2.0.93", features = ["full", "extra-traits", "visit-mut"] } uuid = "1.11.0" +typify-macro = { path = "../typify-macro" } diff --git a/typify-impl/src/convert.rs b/typify-impl/src/convert.rs index a0cfc4d9..4bbfb635 100644 --- a/typify-impl/src/convert.rs +++ b/typify-impl/src/convert.rs @@ -1051,7 +1051,14 @@ impl TypeSpace { // f64 here, but we're already constrained by the schemars // representation so ... it's probably the best we can do at // the moment. - match (default.as_f64(), min, max) { + // + // I added this because numbers are sometimes specified in double quotes + let d = match default { + serde_json::Value::Number(a) => a.as_f64(), + serde_json::Value::String(a) => a.parse().ok(), + _ => None, + }; + match (d, min, max) { (Some(_), None, None) => Some(()), (Some(value), None, Some(fmax)) if value <= fmax => Some(()), (Some(value), Some(fmin), None) if value >= fmin => Some(()), @@ -1275,9 +1282,9 @@ impl TypeSpace { metadata: &'a Option>, ref_name: &str, ) -> Result<(TypeEntry, &'a Option>)> { - if !ref_name.starts_with('#') { - panic!("external references are not supported: {}", ref_name); - } + // if !ref_name.starts_with('#') { + // panic!("external references are not supported: {}", ref_name); + // } let key = ref_key(ref_name); let type_id = self .ref_to_id diff --git a/typify-impl/src/lib.rs b/typify-impl/src/lib.rs index 9c37445a..cd43e8a9 100644 --- a/typify-impl/src/lib.rs +++ b/typify-impl/src/lib.rs @@ -4,14 +4,21 @@ #![deny(missing_docs)] -use std::collections::{BTreeMap, BTreeSet}; +use std::{ + collections::{BTreeMap, BTreeSet}, + path::PathBuf, +}; use conversions::SchemaCache; +use iref::{iri::FragmentBuf, Iri}; use log::info; use output::OutputSpace; +use pathdiff::diff_paths; use proc_macro2::TokenStream; use quote::{format_ident, quote, ToTokens}; -use schemars::schema::{Metadata, RootSchema, Schema}; +use schemars::schema::{ + Metadata, RootSchema, Schema, SchemaObject, SingleOrVec, SubschemaValidation, +}; use thiserror::Error; use type_entry::{ StructPropertyState, TypeEntry, TypeEntryDetails, TypeEntryNative, TypeEntryNewtype, @@ -207,6 +214,10 @@ pub struct TypeSpace { // Shared functions for generating default values defaults: BTreeSet, + + file_path: PathBuf, + + distinct_definitions: bool, } impl Default for TypeSpace { @@ -225,10 +236,26 @@ impl Default for TypeSpace { settings: Default::default(), cache: Default::default(), defaults: Default::default(), + file_path: Default::default(), + distinct_definitions: Default::default(), } } } +/// Retrieves id of the schema from possible places +fn get_schema_id(schema: &SchemaObject) -> Option { + schema + .metadata + .as_ref() + .and_then(|m| m.id.clone()) + .or_else(|| { + schema + .extensions + .get("id") + .map(|id| id.as_str().unwrap().to_string()) + }) +} + #[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] pub(crate) enum DefaultImpl { Boolean, @@ -309,6 +336,7 @@ pub struct TypeSpaceSettings { patch: BTreeMap, replace: BTreeMap, convert: Vec, + distinct_definitions: bool, } #[derive(Debug, Clone)] @@ -532,6 +560,12 @@ impl TypeSpaceSettings { self.map_type = map_type.into(); self } + + /// Specify whether to generate distinct definitions for each type. + pub fn with_distinct_definitions(&mut self, distinct_definitions: bool) -> &mut Self { + self.distinct_definitions = distinct_definitions; + self + } } impl TypeSpacePatch { @@ -570,6 +604,22 @@ impl TypeSpace { } } + /// Sets the file path for the `TypeSpace` instance. + pub fn with_path>(&mut self, path: T) { + self.file_path = path.into().canonicalize().unwrap(); + } + + /// Sets the file path for the `TypeSpace` instance. + /// This is a raw path and does not canonicalize the path. + pub fn with_path_raw>(&mut self, path: T) { + self.file_path = path.into(); + } + + /// Configures whether the `TypeSpace` instance should use distinct definitions. + pub fn distinct_defs(&mut self, value: bool) { + self.distinct_definitions = value; + } + /// Add a collection of types that will be used as references. Regardless /// of how these types are defined--*de novo* or built-in--each type will /// appear in the final output as a struct, enum or newtype. This method @@ -616,9 +666,10 @@ impl TypeSpace { // batch of types. for (index, (ref_name, schema)) in definitions.into_iter().enumerate() { info!( - "converting type: {:?} with schema {}", - ref_name, - serde_json::to_string(&schema).unwrap() + "converting type: {:?} with schema {} {}", + &ref_name, + serde_json::to_string(&schema).unwrap(), + line!() ); // Check for manually replaced types. Proceed with type conversion @@ -769,24 +820,150 @@ impl TypeSpace { pub fn add_root_schema(&mut self, schema: RootSchema) -> Result> { let RootSchema { meta_schema: _, - schema, + schema: schema_object, definitions, - } = schema; + } = schema.clone(); + + let s_id = get_schema_id(&schema_object); + + // handle definitions from extensions. + let untracked = schema_object + .extensions + .clone() + .into_iter() + .filter_map(|(_, value)| { + if !value.is_object() { + None + } else { + let object = value.as_object().unwrap(); + Some( + object + .iter() + .filter_map(|(key, value)| { + if let Ok(schema) = serde_json::from_value::(value.clone()) + { + Some((RefKey::Def(key.clone()), schema)) + } else { + None + } + }) + .collect::>(), + ) + } + }) + .flatten() + .collect::>(); let mut defs = definitions .into_iter() .map(|(key, schema)| (RefKey::Def(key), schema)) + .chain(untracked) .collect::>(); // Does the root type have a name (otherwise... ignore it) - let root_type = schema + let root_type = schema_object .metadata .as_ref() .and_then(|m| m.title.as_ref()) .is_some(); if root_type { - defs.push((RefKey::Root, schema.into())); + defs.push((RefKey::Root, schema_object.into())); + } + + // recursively fetch external references from definitions + let mut external_references = BTreeMap::new(); + + for (_, def) in &defs { + fetch_external_definitions( + &schema, + def, + &self.file_path, + &s_id, + &mut external_references, + true, + ); + } + + let mut ext_refs = vec![]; + // format references in internal schemas to prevent collisions in schemas + for (_, schema) in defs.iter_mut() { + format_reference(schema, &s_id, &s_id); + } + + // format references in external schemas to prevent collisions in schemas + for (reference, (mut schema, path, id)) in external_references { + let path = path.canonicalize().unwrap(); + if let RefKey::Def(reference) = reference { + let path = path.canonicalize().unwrap(); + let relpath = diff_paths(&path, self.file_path.parent().unwrap()) + .unwrap_or_default() + .to_string_lossy() + .replace(format!("..{LINE_SEPARATOR}").as_str(), "Parent"); + let ref_name = if relpath.ends_with(LINE_SEPARATOR) { + format!( + "{}{}", + relpath, + reference.split("/").last().unwrap_or_default() + ) + } else { + format!( + "{}{}{}", + relpath, + LINE_SEPARATOR, + reference.split("/").last().unwrap_or_default() + ) + } + .replace(".json", LINE_SEPARATOR.to_string().as_str()) + .trim_matches(LINE_SEPARATOR_CHAR) + .replace( + format!("{LINE_SEPARATOR}{LINE_SEPARATOR}").as_str(), + LINE_SEPARATOR, + ) + .to_string(); + format_reference(&mut schema, &id, &s_id); + #[cfg(target_os = "windows")] + let ref_name = ref_name.replace("\\", "/"); + ext_refs.push((RefKey::Def(ref_name), schema)); + } + } + + // merge internal and external schemas + defs.extend(ext_refs.into_iter()); + + if self.settings.distinct_definitions { + // recursevely distinct definition to strip count of definitions + // for example: + // ... + // "foo":{ + // "$ref": "#/definitions/a", + // }, + // "bar":{ + // "$ref": "#/definitions/b", + // }, + // "a": { + // "type": "string" + // }, + // "b": { + // "type": "string" + // }, + // ⬇️ + // "foo":{ + // "$ref": "#/definitions/a", + // }, + // "bar":{ + // "$ref": "#/definitions/a", + // }, + // "a": { + // "type": "string" + // } + let mut old = defs.len(); + + distinct_definitions(&mut defs); + while (old - defs.len()) != 0 { + old = defs.len(); + distinct_definitions(&mut defs); + } } self.add_ref_types_impl(defs)?; @@ -983,6 +1160,496 @@ impl TypeSpace { } } +fn fetch_external_definitions( + base_schema: &RootSchema, // Reference to the base schema + definition: &Schema, // The schema definition to process + base_path: &PathBuf, // Base path for file operations + base_id: &Option, // Optional base ID for schema + external_references: &mut BTreeMap)>, // Map to store external references + first_run: bool, // Flag to indicate if this is the first run of the function +) { + // Iterate through each reference found in the given schema definition + for mut reference in get_references(&definition) { + if reference.is_empty() { + continue; // Skip empty references + } + if reference.starts_with("#") { + // Handle internal references + if first_run { + continue; // Skip processing internal references on the first run + } + + reference.remove(0); // Remove the '#' character from the reference + let fragment = reference + .split("/") + .into_iter() + .map(|s| s.to_string()) + .filter(|s| !s.is_empty()) + .collect(); // Split and collect the reference into a vector of strings + let definition_schema = fetch_defenition(base_schema, &reference, &fragment); // Fetch the internal schema definition + let k = format!("{}{}", base_id.as_ref().unwrap(), reference); // Create a key for the reference + let key = RefKey::Def(k); + if external_references.contains_key(&key) { + continue; // Skip if the reference already exists in the map + } else { + // Insert the reference into the map and recursively fetch external definitions + external_references.insert( + key, + ( + definition_schema.clone(), + base_path.clone(), + base_id.clone(), + ), + ); + fetch_external_definitions( + base_schema, + &definition_schema, + base_path, + base_id, + external_references, + false, + ); + } + } else { + // Handle external references + let base_id = base_id + .as_ref() + .expect("missing 'id' attribute in schema definition"); // Ensure base_id is present + let id = Iri::new(base_id).unwrap(); // Create an IRI from the base ID + let reff = Iri::new(&reference).unwrap(); // Create an IRI from the reference + let fragment = reff + .fragment() + .as_ref() + .unwrap_or(&FragmentBuf::new("".to_string()).unwrap().as_fragment()) + .to_string() + .split("/") + .filter_map(|s| (!s.is_empty()).then_some(s.to_string())) + .collect::>(); // Process the fragment part of the reference + let relpath = + diff_paths(reff.path().as_str(), id.path().parent_or_empty().as_str()).unwrap(); // Determine the relative path + let file_path = base_path.parent().unwrap().join(&relpath); // Construct the file path + let content = std::fs::read_to_string(&file_path).expect(&format!( + "Failed to open input file: {}", + &file_path.display() + )); // Read the file content + + let root_schema = serde_json::from_str::(&content) + .expect("Failed to parse input file as JSON Schema"); // Parse the file content as JSON Schema + let definition_schema = fetch_defenition(&root_schema, &reference, &fragment); // Fetch the external schema definition + let key = RefKey::Def(reference.clone()); + if external_references.contains_key(&key) { + continue; // Skip if the reference already exists in the map + } else { + let s_id = get_schema_id(&root_schema.schema); // Get the schema ID + + // Insert the reference into the map and recursively fetch external definitions + external_references.insert( + key, + (definition_schema.clone(), file_path.clone(), s_id.clone()), + ); + fetch_external_definitions( + &root_schema, + &definition_schema, + &file_path, + &s_id, + external_references, + false, + ) + } + } + } +} + +fn fetch_defenition( + base_schema: &RootSchema, + reference: &String, + fragment: &Vec, +) -> Schema { + if fragment.is_empty() { + return Schema::Object(base_schema.schema.clone()); + } + let definition_schema = if ["definitions", "$defs"].contains(&fragment[0].as_str()) { + base_schema + .definitions + .get( + reference + .split('/') + .last() + .expect("unexpected end of reference"), + ) + .unwrap() + .clone() + } else { + let mut value = base_schema.schema.extensions.get(&fragment[0]).unwrap(); + for x in fragment.iter().skip(1) { + value = value.as_object().unwrap().get(x).unwrap(); + } + serde_json::from_value(value.clone()).unwrap() + }; + definition_schema +} + +fn get_references(schema: &Schema) -> Vec { + match schema { + Schema::Object(obj) => { + let mut result = vec![]; + obj.clone() + .reference + .map(|reference| result.push(reference)); + if let Some(o) = &obj.object { + let prop_refs = o + .properties + .values() + .into_iter() + .flat_map(|p| get_references(p)) + .collect::>(); + result.extend(prop_refs); + if let Some(additional_props) = &o.additional_properties { + result.extend(get_references(&additional_props)); + } + let pattern_refs = o + .pattern_properties + .values() + .into_iter() + .flat_map(|p| get_references(p)) + .collect::>(); + if let Some(property_names) = &o.property_names { + result.extend(get_references(&property_names)) + } + result.extend(pattern_refs); + } + if let Some(o) = &obj.array { + result.extend( + o.contains + .as_ref() + .map(|s| get_references(s.as_ref())) + .unwrap_or_default(), + ); + result.extend( + o.additional_items + .as_ref() + .map(|s| get_references(s.as_ref())) + .unwrap_or_default(), + ); + result.extend( + o.items + .as_ref() + .map(|s| match s { + SingleOrVec::Single(v) => get_references(v.as_ref()), + SingleOrVec::Vec(v) => v + .iter() + .flat_map(|element| get_references(element)) + .collect::>(), + }) + .unwrap_or_default(), + ); + } + if let Some(SubschemaValidation { + all_of, + any_of, + one_of, + not, + if_schema, + then_schema, + else_schema, + }) = obj.subschemas.as_ref().map(AsRef::as_ref) + { + result.extend( + all_of + .as_ref() + .map(|s| { + s.iter() + .flat_map(|element| get_references(element)) + .collect::>() + .into_iter() + }) + .unwrap_or_default(), + ); + result.extend( + any_of + .as_ref() + .map(|s| { + s.iter() + .flat_map(|element| get_references(element)) + .collect::>() + .into_iter() + }) + .unwrap_or_default(), + ); + result.extend( + one_of + .as_ref() + .map(|s| { + s.iter() + .flat_map(|element| get_references(element)) + .collect::>() + .into_iter() + }) + .unwrap_or_default(), + ); + result.extend( + not.as_ref() + .map(|s| get_references(s.as_ref())) + .unwrap_or_default(), + ); + result.extend( + if_schema + .as_ref() + .map(|s| get_references(s.as_ref())) + .unwrap_or_default(), + ); + result.extend( + then_schema + .as_ref() + .map(|s| get_references(s.as_ref())) + .unwrap_or_default(), + ); + result.extend( + else_schema + .as_ref() + .map(|s| get_references(s.as_ref())) + .unwrap_or_default(), + ); + } + result + } + _ => vec![], + } +} + +#[cfg(target_os = "windows")] +const LINE_SEPARATOR: &str = "\\"; + +#[cfg(target_os = "windows")] +const LINE_SEPARATOR_CHAR: char = '\\'; + +#[cfg(not(target_os = "windows"))] +const LINE_SEPARATOR: &str = "/"; + +#[cfg(not(target_os = "windows"))] +const LINE_SEPARATOR_CHAR: char = '/'; + +fn format_reference(schema: &mut Schema, id: &Option, base_id: &Option) { + match schema { + Schema::Bool(_) => {} + Schema::Object(obj) => { + obj.reference.as_mut().map(|reference| { + let mut r = reference.clone(); + if r.starts_with("#") { + if id == base_id { + return; + } + r = id.clone().unwrap(); + } + let b_id = base_id.clone().unwrap(); + let id = Iri::new(&b_id).unwrap(); // path + last ref + let reff = Iri::new(&r).unwrap(); + let dif = diff_paths(reff.path().as_str(), id.path().parent_or_empty().as_str()) + .unwrap_or_default() + .to_string_lossy() + .replace(format!("..{LINE_SEPARATOR}").as_str(), "Parent"); + + let mut r = format!("{}{}", dif, reference.split("/").last().unwrap_or_default()) + .replace(".json", LINE_SEPARATOR.to_string().as_str()); + if r.ends_with(LINE_SEPARATOR) { + r.pop(); + } + #[cfg(target_os = "windows")] + let r = r.replace("\\", "/"); + *reference = r; + }); + if let Some(o) = obj.object.as_mut() { + for (_, s) in o.properties.iter_mut() { + format_reference(s, id, base_id); + } + if let Some(additional_props) = o.additional_properties.as_mut() { + format_reference(additional_props, id, base_id); + } + + for (_, s) in o.pattern_properties.iter_mut() { + format_reference(s, id, base_id); + } + if let Some(property_names) = o.property_names.as_mut() { + format_reference(property_names, id, base_id); + } + } + if let Some(o) = obj.array.as_mut() { + if let Some(s) = o.contains.as_mut() { + format_reference(s, id, base_id); + } + if let Some(s) = o.additional_items.as_mut() { + format_reference(s, id, base_id); + } + if let Some(s) = o.items.as_mut() { + match s { + SingleOrVec::Single(s) => format_reference(s, id, base_id), + SingleOrVec::Vec(v) => { + for schema in v.iter_mut() { + format_reference(schema, id, base_id); + } + } + } + } + } + + if let Some(SubschemaValidation { + all_of, + any_of, + one_of, + not, + if_schema, + then_schema, + else_schema, + }) = obj.subschemas.as_mut().map(AsMut::as_mut) + { + if let Some(s) = all_of.as_mut() { + for s in s.iter_mut() { + format_reference(s, id, base_id); + } + } + if let Some(s) = any_of.as_mut() { + for s in s.iter_mut() { + format_reference(s, id, base_id); + } + } + if let Some(s) = one_of.as_mut() { + for s in s.iter_mut() { + format_reference(s, id, base_id); + } + } + if let Some(s) = not.as_mut() { + format_reference(s, id, base_id); + } + if let Some(s) = if_schema.as_mut() { + format_reference(s, id, base_id); + } + if let Some(s) = then_schema.as_mut() { + format_reference(s, id, base_id); + } + if let Some(s) = else_schema.as_mut() { + format_reference(s, id, base_id); + } + } + } + } +} + +fn distinct_definitions(definitions: &mut Vec<(RefKey, Schema)>) -> &mut Vec<(RefKey, Schema)> { + let mut delete_id = std::collections::HashSet::new(); + let mut replace_from_to = BTreeMap::new(); + for i in 0..definitions.len() { + if delete_id.contains(&i) { + continue; + } + for j in (i + 1)..definitions.len() { + if &definitions[i].1 == &definitions[j].1 { + delete_id.insert(j); + if let (RefKey::Def(k), RefKey::Def(key)) = (&definitions[j].0, &definitions[i].0) { + replace_from_to.insert(k.clone(), key.clone()); + } + } + } + } + let mut d = std::mem::take(definitions); + d = d + .into_iter() + .enumerate() + .filter(|(index, _)| !delete_id.contains(index)) + .map(|(_, v)| v) + .collect::>(); + + d.iter_mut() + .for_each(|(_, schema)| replace_reference(schema, &replace_from_to)); + + *definitions = d; + definitions +} + +fn replace_reference(schema: &mut Schema, dictionary: &BTreeMap) { + match schema { + Schema::Bool(_) => {} + Schema::Object(obj) => { + obj.reference.as_mut().map(|reference| { + if let Some(r) = dictionary.get(reference) { + *reference = r.to_string(); + } + }); + if let Some(o) = obj.object.as_mut() { + for (_, s) in o.properties.iter_mut() { + replace_reference(s, dictionary); + } + if let Some(additional_props) = o.additional_properties.as_mut() { + replace_reference(additional_props, dictionary); + } + + for (_, s) in o.pattern_properties.iter_mut() { + replace_reference(s, dictionary); + } + if let Some(property_names) = o.property_names.as_mut() { + replace_reference(property_names, dictionary); + } + } + if let Some(o) = obj.array.as_mut() { + if let Some(s) = o.contains.as_mut() { + replace_reference(s, dictionary); + } + if let Some(s) = o.additional_items.as_mut() { + replace_reference(s, dictionary); + } + if let Some(s) = o.items.as_mut() { + match s { + SingleOrVec::Single(s) => replace_reference(s, dictionary), + SingleOrVec::Vec(v) => { + for schema in v.iter_mut() { + replace_reference(schema, dictionary); + } + } + } + } + } + + if let Some(SubschemaValidation { + all_of, + any_of, + one_of, + not, + if_schema, + then_schema, + else_schema, + }) = obj.subschemas.as_mut().map(AsMut::as_mut) + { + if let Some(s) = all_of.as_mut() { + for s in s.iter_mut() { + replace_reference(s, dictionary); + } + } + if let Some(s) = any_of.as_mut() { + for s in s.iter_mut() { + replace_reference(s, dictionary); + } + } + if let Some(s) = one_of.as_mut() { + for s in s.iter_mut() { + replace_reference(s, dictionary); + } + } + if let Some(s) = not.as_mut() { + replace_reference(s, dictionary); + } + if let Some(s) = if_schema.as_mut() { + replace_reference(s, dictionary); + } + if let Some(s) = then_schema.as_mut() { + replace_reference(s, dictionary); + } + if let Some(s) = else_schema.as_mut() { + replace_reference(s, dictionary); + } + } + } + } +} + impl ToTokens for TypeSpace { fn to_tokens(&self, tokens: &mut TokenStream) { tokens.extend(self.to_stream()) diff --git a/typify-impl/src/util.rs b/typify-impl/src/util.rs index 06efe0fe..02b85b07 100644 --- a/typify-impl/src/util.rs +++ b/typify-impl/src/util.rs @@ -533,9 +533,13 @@ pub(crate) fn ref_key(ref_name: &str) -> RefKey { if ref_name == "#" { RefKey::Root } else if let Some(idx) = ref_name.rfind('/') { - RefKey::Def(ref_name[idx + 1..].to_string()) + if ref_name.starts_with("#") { + RefKey::Def(ref_name[idx + 1..].to_string()) + } else { + RefKey::Def(ref_name.to_string()) + } } else { - panic!("expected a '/' in $ref: {}", ref_name) + RefKey::Def(ref_name.to_string()) } } diff --git a/typify-impl/tests/external_references.json b/typify-impl/tests/external_references.json new file mode 100644 index 00000000..9542f56a --- /dev/null +++ b/typify-impl/tests/external_references.json @@ -0,0 +1,13 @@ +{ + "id": "https://schemas.mysite.com/schemas/external_references.json#", + "$schema": "http://json-schema.org/draft-04/schema#", + "definitions": { + "somename": { + "properties": { + "someproperty": { + "$ref": "https://schemas.mysite.com/schemas/v1/external_references.json#/definitions/someothername" + } + } + } + } +} \ No newline at end of file diff --git a/typify-impl/tests/external_references.out b/typify-impl/tests/external_references.out new file mode 100644 index 00000000..601338da --- /dev/null +++ b/typify-impl/tests/external_references.out @@ -0,0 +1,77 @@ +#[doc = r" Error types."] +pub mod error { + #[doc = r" Error from a TryFrom or FromStr implementation."] + pub struct ConversionError(::std::borrow::Cow<'static, str>); + impl ::std::error::Error for ConversionError {} + impl ::std::fmt::Display for ConversionError { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> Result<(), ::std::fmt::Error> { + ::std::fmt::Display::fmt(&self.0, f) + } + } + impl ::std::fmt::Debug for ConversionError { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> Result<(), ::std::fmt::Error> { + ::std::fmt::Debug::fmt(&self.0, f) + } + } + impl From<&'static str> for ConversionError { + fn from(value: &'static str) -> Self { + Self(value.into()) + } + } + impl From for ConversionError { + fn from(value: String) -> Self { + Self(value.into()) + } + } +} +#[doc = "Somename"] +#[doc = r""] +#[doc = r"
JSON schema"] +#[doc = r""] +#[doc = r" ```json"] +#[doc = "{"] +#[doc = " \"properties\": {"] +#[doc = " \"someproperty\": {"] +#[doc = " \"$ref\": \"v1/external_references/someothername\""] +#[doc = " }"] +#[doc = " }"] +#[doc = "}"] +#[doc = r" ```"] +#[doc = r"
"] +#[derive(:: serde :: Deserialize, :: serde :: Serialize, Clone, Debug)] +pub struct Somename { + #[serde(default, skip_serializing_if = "::std::option::Option::is_none")] + pub someproperty: ::std::option::Option, +} +impl From<&Somename> for Somename { + fn from(value: &Somename) -> Self { + value.clone() + } +} +#[doc = "V1ExternalReferencesSomeothername"] +#[doc = r""] +#[doc = r"
JSON schema"] +#[doc = r""] +#[doc = r" ```json"] +#[doc = "{"] +#[doc = " \"type\": \"object\","] +#[doc = " \"properties\": {"] +#[doc = " \"someproperty\": {"] +#[doc = " \"a\": {"] +#[doc = " \"type\": \"string\""] +#[doc = " }"] +#[doc = " }"] +#[doc = " }"] +#[doc = "}"] +#[doc = r" ```"] +#[doc = r"
"] +#[derive(:: serde :: Deserialize, :: serde :: Serialize, Clone, Debug)] +pub struct V1ExternalReferencesSomeothername { + #[serde(default, skip_serializing_if = "::std::option::Option::is_none")] + pub someproperty: ::std::option::Option<::serde_json::Value>, +} +impl From<&V1ExternalReferencesSomeothername> for V1ExternalReferencesSomeothername { + fn from(value: &V1ExternalReferencesSomeothername) -> Self { + value.clone() + } +} diff --git a/typify-impl/tests/test_external_references.rs b/typify-impl/tests/test_external_references.rs new file mode 100644 index 00000000..d515c8d3 --- /dev/null +++ b/typify-impl/tests/test_external_references.rs @@ -0,0 +1,30 @@ +use schemars::schema::RootSchema; +use std::fs::File; +use std::io::BufReader; +use std::path::Path; +use typify_impl::TypeSpace; +use typify_macro::import_types; + +#[test] +fn test_external_references() { + let mut type_space = TypeSpace::default(); + + let path = Path::new("tests/external_references.json"); + let file = File::open(path).unwrap(); + let reader = BufReader::new(file); + + let schema: RootSchema = serde_json::from_reader(reader).unwrap(); + type_space.with_path("tests/external_references.json"); + type_space.add_root_schema(schema).unwrap(); + + let file = type_space.to_stream(); + + let fmt = rustfmt_wrapper::rustfmt(file.to_string()).unwrap(); + + expectorate::assert_contents("tests/external_references.out", fmt.as_str()); +} + +#[test] +fn test_external_references_from_macro() { + import_types!(schema = "tests/external_references.json"); +} diff --git a/typify-impl/tests/v1/external_references.json b/typify-impl/tests/v1/external_references.json new file mode 100644 index 00000000..69dd9ad1 --- /dev/null +++ b/typify-impl/tests/v1/external_references.json @@ -0,0 +1,16 @@ +{ + "id": "https://schemas.mysite.com/schemas/v1/external_references.json#", + "$schema": "http://json-schema.org/draft-04/schema#", + "definitions": { + "someothername": { + "type": "object", + "properties": { + "someproperty": { + "a": { + "type": "string" + } + } + } + } + } +} \ No newline at end of file diff --git a/typify-macro/src/lib.rs b/typify-macro/src/lib.rs index 3262c1ac..18ac3e9a 100644 --- a/typify-macro/src/lib.rs +++ b/typify-macro/src/lib.rs @@ -59,7 +59,7 @@ mod token_utils; /// - `replace`: optional map from definition name to a replacement type. This /// may be used to skip generation of the named type and use a existing Rust /// type. -/// +/// /// - `convert`: optional map from a JSON schema type defined in `$defs` to a /// replacement type. This may be used to skip generation of the schema and /// use an existing Rust type. @@ -93,6 +93,8 @@ struct MacroSettings { #[serde(default)] convert: serde_tokenstream::OrderedMap>, + #[serde(default)] + distinct_definitions: bool, } struct MacroCrateSpec { @@ -193,6 +195,7 @@ fn do_import_types(item: TokenStream) -> Result { unknown_crates, crates, map_type, + distinct_definitions, } = serde_tokenstream::from_tokenstream(&item.into())?; let mut settings = TypeSpaceSettings::default(); derives.into_iter().for_each(|derive| { @@ -225,6 +228,8 @@ fn do_import_types(item: TokenStream) -> Result { settings.with_map_type(map_type); + settings.with_distinct_definitions(distinct_definitions); + (schema.into_inner(), settings) }; @@ -245,6 +250,7 @@ fn do_import_types(item: TokenStream) -> Result { .unwrap(); let mut type_space = TypeSpace::new(&settings); + type_space.with_path_raw(&path); type_space .add_root_schema(root_schema) .map_err(|e| into_syn_err(e, schema.span()))?;