├── .github ├── dependabot.yml └── workflows │ └── rust.yml ├── .gitignore ├── CHANGELOG.adoc ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── cargo-typify ├── Cargo.toml ├── README.md ├── release.toml ├── src │ ├── lib.rs │ └── main.rs └── tests │ ├── integration.rs │ └── outputs │ ├── builder.rs │ ├── custom_btree_map.rs │ ├── derive.rs │ ├── help.txt │ ├── multi_derive.rs │ └── no-builder.rs ├── example-build ├── Cargo.toml ├── build.rs ├── release.toml └── src │ └── main.rs ├── example-macro ├── Cargo.toml ├── release.toml └── src │ └── main.rs ├── example.json ├── extension-schema.json ├── release.toml ├── rust-toolchain.toml ├── typify-impl ├── Cargo.toml ├── release.toml ├── src │ ├── conversions.rs │ ├── convert.rs │ ├── cycles.rs │ ├── defaults.rs │ ├── enums.rs │ ├── lib.rs │ ├── merge.rs │ ├── output.rs │ ├── rust_extension.rs │ ├── structs.rs │ ├── test_util.rs │ ├── type_entry.rs │ ├── util.rs │ ├── validate.rs │ └── value.rs └── tests │ ├── generator.out │ ├── github.json │ ├── github.out │ ├── test_generation.rs │ ├── test_github.rs │ ├── vega.json │ └── vega.out ├── typify-macro ├── Cargo.toml ├── release.toml └── src │ ├── lib.rs │ └── token_utils.rs ├── typify-test ├── Cargo.toml ├── build.rs ├── release.toml └── src │ └── main.rs └── typify ├── Cargo.toml ├── src └── lib.rs └── tests ├── schemas.rs └── schemas ├── arrays-and-tuples.json ├── arrays-and-tuples.rs ├── deny-list.json ├── deny-list.rs ├── extraneous-enum.json ├── extraneous-enum.rs ├── id-or-name.json ├── id-or-name.rs ├── maps.json ├── maps.rs ├── maps_custom.rs ├── merged-schemas.json ├── merged-schemas.rs ├── more_types.json ├── more_types.rs ├── multiple-instance-types.json ├── multiple-instance-types.rs ├── noisy-types.json ├── noisy-types.rs ├── property-pattern.json ├── property-pattern.rs ├── reflexive.json ├── reflexive.rs ├── rust-collisions.json ├── rust-collisions.rs ├── simple-types.json ├── simple-types.rs ├── string-enum-with-default.json ├── string-enum-with-default.rs ├── type-with-modified-generation.json ├── type-with-modified-generation.rs ├── types-with-defaults.json ├── types-with-defaults.rs ├── types-with-more-impls.json ├── types-with-more-impls.rs ├── untyped-enum-with-null.json ├── untyped-enum-with-null.rs ├── various-enums.json ├── various-enums.rs ├── x-rust-type.json └── x-rust-type.rs /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # 2 | # Dependabot configuration file 3 | # 4 | 5 | version: 2 6 | updates: 7 | - package-ecosystem: "cargo" 8 | directory: "/" 9 | schedule: 10 | interval: "weekly" 11 | ignore: 12 | - dependency-name: "schemars" 13 | versions: [">=0.9.0"] 14 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | # 2 | # Configuration for GitHub-based CI 3 | # 4 | name: Build 5 | 6 | on: 7 | push: 8 | branches: [ main ] 9 | pull_request: 10 | branches: [ main ] 11 | 12 | jobs: 13 | check-style: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Report cargo version 18 | run: cargo --version 19 | - name: Report rustfmt version 20 | run: cargo fmt -- --version 21 | - name: Check style 22 | run: cargo fmt -- --check 23 | 24 | build-and-test: 25 | runs-on: ${{ matrix.os }} 26 | strategy: 27 | matrix: 28 | os: [ ubuntu-latest, windows-latest, macos-latest ] 29 | steps: 30 | - uses: actions/checkout@v2 31 | - name: Build 32 | run: cargo build --locked --tests --verbose 33 | - name: Run tests 34 | run: cargo test --locked --verbose 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /CHANGELOG.adoc: -------------------------------------------------------------------------------- 1 | :showtitle: 2 | :toc: left 3 | :icons: font 4 | :toclevels: 1 5 | 6 | = Typify Changelog 7 | 8 | // WARNING: This file is modified programmatically by `cargo release` as 9 | // configured in release.toml. DO NOT change the format of the headers or the 10 | // list of raw commits. 11 | 12 | // cargo-release: next header goes here (do not change this line) 13 | 14 | == Unreleased changes (release date TBD) 15 | 16 | https://github.com/oxidecomputer/typify/compare/v0.4.2\...HEAD[Full list of commits] 17 | 18 | == 0.4.2 (released 2025-05-27) 19 | 20 | https://github.com/oxidecomputer/typify/compare/v0.4.1\...v0.4.2[Full list of commits] 21 | 22 | * improve merging logic for "not" schemas (#831) 23 | * improve regex validation performance. (#825) 24 | * remove some dead code that displeases more recent rust versions (#826) 25 | 26 | == 0.4.1 (released 2025-04-30) 27 | 28 | https://github.com/oxidecomputer/typify/compare/v0.4.0\...v0.4.1[Full list of commits] 29 | 30 | * improve `null` recognition (#817) 31 | * improve identification of mutually incompatible `anyOf` subschemas (#817) 32 | 33 | == 0.4.0 (released 2025-04-16) 34 | 35 | https://github.com/oxidecomputer/typify/compare/v0.3.0\...v0.4.0[Full list of commits] 36 | 37 | * Doc fixes 38 | * Slightly more effort made to disambiguate enum variant identifiers (#762) 39 | * Fix for panic with non-zero integers, nullable types, and default values (#771) 40 | * Greater consistency wrt `additionalProperties` interpretation (#773) 41 | * Fix for string length validation for non ascii strings (#776) 42 | * Improved handling of integer format with min/max values (#733) 43 | 44 | == 0.3.0 (released 2024-12-27) 45 | 46 | https://github.com/oxidecomputer/typify/compare/v0.2.0\...v0.3.0[Full list of commits] 47 | 48 | * Mark newtype wrappers as `#[serde(transparent)]` (#724) 49 | * impl `Default` for structs where all properties have a default (#725) 50 | * Add support specifying the map type to use (#708) 51 | * Fully qualify more type names e.g. from `std` (#705) 52 | 53 | These changes may break users of previous versions in particular if you were to 54 | implement `Default` by hand for types for which the implementation is now 55 | generated. 56 | 57 | == 0.2.0 (released 2024-09-26) 58 | 59 | https://github.com/oxidecomputer/typify/compare/v0.1.0\...v0.2.0[Full list of commits] 60 | 61 | * Migrate from `ToString` to `std::fmt::Display` (#663) 62 | * fully qualify types and traits (#647) 63 | * Add support for `patternProperties` in particular circumstances (#637) 64 | * Fully qualify `serde::Deserialize`` and `serde::Serialize` in outputs (#634) 65 | * Fix panic in `all_mutually_exclusive` and allow multiple subschema types in a single schema (#627) 66 | * handle default values for fields represented as `NonZeroU{8,16,32}`` (#608) 67 | 68 | == 0.1.0 (released 2024-05-13) 69 | 70 | * Improvements and bug fixes around schema merging 71 | * Fixes for various bugs with defaults 72 | * Support for the `x-rust-type` extension (#584) 73 | 74 | https://github.com/oxidecomputer/typify/compare/v0.0.16\...v0.1.0[Full list of commits] 75 | 76 | == 0.0.16 (released 2024-02-28) 77 | 78 | * Introduce a proper Error type for various conversions (#475) 79 | * Add docs to generated mods (#476) 80 | * Various enum improvements 81 | 82 | https://github.com/oxidecomputer/typify/compare/v0.0.15\...v0.0.16[Full list of commits] 83 | 84 | == 0.0.15 (released 2023-12-15) 85 | 86 | * Improvements to array merging and mutual exclusivity checks (#412) 87 | * Support for 32-bit floating-point numbers (#440) 88 | * Better handling for unsatisfiable merged schemas (#447) 89 | * Show original JSON Schema in generated type docs (#454) 90 | 91 | https://github.com/oxidecomputer/typify/compare/v0.0.14\...v0.0.15[Full list of commits] 92 | 93 | == 0.0.14 (released 2023-09-25) 94 | 95 | * Handle arbitrary containment cycles (#300) 96 | * More permissive of valid (if useless) schema constructions (#306, #320) 97 | * Much better handling of `allOf` constructions by merging schemas (#405) 98 | * Support for more `not` subschemas (#410) 99 | 100 | https://github.com/oxidecomputer/typify/compare/v0.0.13\...v0.0.14[Full list of commits] 101 | 102 | == 0.0.13 (released 2023-05-14) 103 | 104 | * Fixed-length, single-type arrays to `[T; N]` (#286) 105 | * Support for reflexive schemas (#292) 106 | * Much improved support for multi-type schemas (#291) 107 | * Better error messages on failures 108 | 109 | https://github.com/oxidecomputer/typify/compare/v0.0.12\...v0.0.13[Full list of commits] 110 | 111 | == 0.0.12 (released 2023-05-03) 112 | 113 | * Improved enum generation (#270) 114 | * Improved integer type selection based on number criteria (#255) 115 | * `TypeSpace::add_root_schema()` (#236) 116 | * ... and many general improvements 117 | 118 | https://github.com/oxidecomputer/typify/compare/v0.0.11\...v0.0.12[Full list of commits] 119 | 120 | == 0.0.11 (released 2023-03-18) 121 | 122 | This is a big update with many, many changes to code generation, and many more 123 | JSON schema structures well-handled. Among the many changes: 124 | 125 | * Generate a `ToString` impl for untagged enums with trivial variants (#145) 126 | * Allow conversion overrides by specifying a schema (#155) 127 | * Handle untyped enums that contain nulls (#167) 128 | * Handle `not` schemas for enumerated values (#168) 129 | * Improve generated code for FromStr and TryFrom impls (#174) 130 | * Handle format specifiers for enumerated strings (#188) 131 | 132 | === *Breaking*: The removal of `TypeSpace::to_string()` 133 | 134 | Previously all transitive consumers required the presence of `rustfmt`. In this 135 | version we leave formatting to the consumer. See link:README.md#formatting[the formatting section of the README] for details on formatting. 136 | 137 | === CLI 138 | 139 | This version adds the `cargo-typify` crate for stand-alone code generation. 140 | 141 | === Augmented Generation 142 | 143 | Consumers can now affect how code is generated in several ways: 144 | * adding derive macros to all generated types 145 | * modifying specific types by name to rename them or add derive macros 146 | * specifying a replacement type by name 147 | * specifying a replacement type by schema pattern 148 | 149 | 150 | https://github.com/oxidecomputer/typify/compare/v0.0.10\...v0.0.11[Full list of commits] 151 | 152 | * Allow per-type renames and derive macro applications (#131) 153 | * `ToString` implementations for untagged enums with trivial newtype variants (#145) 154 | * Fixed an issue with generation of enum defaults (#137) 155 | * Allow conversion overrides by specifying a schema (#155) 156 | 157 | == 0.0.10 (released 2022-09-10) 158 | 159 | https://github.com/oxidecomputer/typify/compare/v0.0.9\...v0.0.10[Full list of commits] 160 | 161 | * Add support for string types with `format` set to `ip`, `ipv4`, or `ipv6` (#76) 162 | * Be more accommodating in the face of a missing `type` field #(79) 163 | * The order of derives on types has stabilized (and therefore has changed) (#81) 164 | * Specific `From` and `Deserialize` implementations for constrained string types (#81) 165 | * Specific `From` implementation for untagged enums with constrained string variants (#81) 166 | * `FromStr` implementation for simple-variant-only `enum`s (#81) 167 | * Ignore unknown `format` values (#81) 168 | * Added `regress` dependency for ECMA 262 style regexes (#81) 169 | * Dropshot produces a complex `Null` type (by necessity); now rendered as `()` (#83) 170 | * Fixed rendering of enums with a single variant (#87) 171 | * Updated public interface (breaking for consumers) (#98) 172 | * Optional builder interface for generated structs (#98) 173 | 174 | == 0.0.9 (released 2022-06-20) 175 | 176 | https://github.com/oxidecomputer/typify/compare/v0.0.8\...v0.0.9[Full list of commits] 177 | 178 | * Switched from `unicode-xid` to `unicode-ident` (#60) 179 | * Elevate `TypeDetail::String` rather than `TypeDetail::BuiltIn("String")` (#72) 180 | 181 | == 0.0.8 (released 2022-05-22) 182 | 183 | https://github.com/oxidecomputer/typify/compare/v0.0.7\...v0.0.8[Full list of commits] 184 | 185 | * Support for integer schemas with `enum_values` populated (breaking change) (#57) 186 | * Deeper inspection of `oneOf` constructions to make better `enum`s (#59) 187 | * Simple handling for "constraint" `allOf` constructions (#59) 188 | * Improved handling of non-required unit struct members (#59) 189 | 190 | == 0.0.7 (released 2022-05-18) 191 | 192 | https://github.com/oxidecomputer/typify/compare/v0.0.6\...v0.0.7[Full list of commits] 193 | 194 | * Update to `uuid` v1.0.0 for testing (non-breaking change) 195 | 196 | == 0.0.6 (released 2022-05-12) 197 | 198 | https://github.com/oxidecomputer/typify/compare/v0.0.5\...v0.0.6[Full list of commits] 199 | 200 | * Add an interface to allow consumers to specify additional derives for generated types (#35) 201 | * Handle all invalid identifier characters (#37) 202 | * Add support for `std::net::Ipv6Addr` type (#38) 203 | * Add `Copy` to simple enums (#40) 204 | * `Box` trivial cyclic refs (#41) 205 | * Move to heck for case conversion (#43) 206 | * Improve handling of default values for object properties (#44) 207 | 208 | == 0.0.5 (released 2021-11-06) 209 | 210 | https://github.com/oxidecomputer/typify/compare/v0.0.4\...v0.0.5[Full list of commits] 211 | 212 | * use include_str! so that our macro is re-run if the given file changes (#27) 213 | * Better handling of enums that look like the Result type (#26) 214 | * Pass through name for make_map (#25) 215 | 216 | 217 | == 0.0.4 (released 2021-11-06) 218 | 219 | First published version 220 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "typify", 4 | "typify-impl", 5 | "typify-macro", 6 | "typify-test", 7 | "cargo-typify", 8 | "example-build", 9 | "example-macro", 10 | ] 11 | 12 | resolver = "2" 13 | 14 | [workspace.dependencies] 15 | typify = { version = "0.4.2", path = "typify" } 16 | typify-impl = { version = "0.4.2", path = "typify-impl" } 17 | typify-macro = { version = "0.4.2", path = "typify-macro" } 18 | 19 | assert_cmd = "2.0.17" 20 | chrono = { version = "0.4.41", features = ["serde"] } 21 | clap = { version = "4.5.39", features = ["derive"] } 22 | color-eyre = "0.6" 23 | env_logger = "0.11" 24 | expectorate = "1.2.0" 25 | glob = "0.3.2" 26 | heck = "0.5.0" 27 | ipnetwork = { version = "0.21.1", features = ["schemars"] } 28 | log = "0.4.27" 29 | newline-converter = "0.3.0" 30 | paste = "1.0.15" 31 | prettyplease = "0.2.33" 32 | proc-macro2 = "1.0.95" 33 | quote = "1.0.40" 34 | regress = "0.10.3" 35 | rustfmt-wrapper = "0.2.1" 36 | schema = "0.1.0" 37 | schemars = "0.8.22" 38 | semver = "1.0.26" 39 | serde = "1.0.219" 40 | serde_json = "1.0.140" 41 | syn = { version = "2.0.101", features = ["full"] } 42 | tempdir = "0.3.7" 43 | thiserror = "2.0.12" 44 | trybuild = "1.0.105" 45 | unicode-ident = "1.0.18" 46 | uuid = "1.16.0" 47 | criterion = "0.5.1" 48 | -------------------------------------------------------------------------------- /cargo-typify/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cargo-typify" 3 | version = "0.4.2" 4 | edition = "2021" 5 | license = "Apache-2.0" 6 | description = "cargo command to generate Rust code from a JSON Schema" 7 | repository = "https://github.com/oxidecomputer/typify" 8 | readme = "README.md" 9 | keywords = ["json", "schema", "cargo"] 10 | categories = ["api-bindings", "compilers"] 11 | 12 | default-run = "cargo-typify" 13 | 14 | [dependencies] 15 | typify = { workspace = true } 16 | 17 | clap = { workspace = true } 18 | color-eyre = { workspace = true } 19 | env_logger = { workspace = true } 20 | rustfmt-wrapper = { workspace = true } 21 | semver = { workspace = true } 22 | serde_json = { workspace = true } 23 | schemars = { workspace = true } 24 | 25 | [dev-dependencies] 26 | assert_cmd = { workspace = true } 27 | expectorate = { workspace = true } 28 | newline-converter = { workspace = true } 29 | tempdir = { workspace = true } 30 | -------------------------------------------------------------------------------- /cargo-typify/README.md: -------------------------------------------------------------------------------- 1 | # cargo-typify 2 | 3 | Once installed, the following command converts a JSON Schema file into Rust 4 | code: 5 | 6 | ```console 7 | $ cargo typify my_types.json 8 | ``` 9 | 10 | This is a wrapper around the [`typify`](https://crates.io/crates/typify) crate 11 | for use at the command-line. 12 | 13 | ## Installation 14 | 15 | Install with `cargo install cargo-typify`. This command requires that `rustfmt` 16 | is installed. Install rustfmt with rustup component add rustfmt 17 | 18 | For ArchLinux users, there is also an 19 | [AUR package](https://aur.archlinux.org/packages/cargo-typify) available. 20 | 21 | ## Example 22 | 23 | **`$ cat id-or-name.json`** 24 | 25 | ```json 26 | { 27 | "$schema": "http://json-schema.org/draft-07/schema#", 28 | "definitions": { 29 | "IdOrName": { 30 | "oneOf": [ 31 | { 32 | "title": "Id", 33 | "allOf": [ 34 | { 35 | "type": "string", 36 | "format": "uuid" 37 | } 38 | ] 39 | }, 40 | { 41 | "title": "Name", 42 | "allOf": [ 43 | { 44 | "$ref": "#/definitions/Name" 45 | } 46 | ] 47 | } 48 | ] 49 | }, 50 | "Name": { 51 | "title": "A name unique within the parent collection", 52 | "description": "Names must begin with a lower case ASCII letter, be composed exclusively of lowercase ASCII, uppercase ASCII, numbers, and '-', and may not end with a '-'. Names cannot be a UUID though they may contain a UUID.", 53 | "type": "string", 54 | "pattern": "^(?![0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$)^[a-z][a-z0-9-]*[a-zA-Z0-9]$", 55 | "maxLength": 63 56 | } 57 | } 58 | } 59 | ``` 60 | 61 | **`$ cargo typify id-or-name.json && cat id-or-name.rs`** 62 | 63 | ```rust 64 | #![allow(clippy::redundant_closure_call)] 65 | #![allow(clippy::needless_lifetimes)] 66 | #![allow(clippy::match_single_binding)] 67 | #![allow(clippy::clone_on_copy)] 68 | 69 | #[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] 70 | #[serde(untagged)] 71 | pub enum IdOrName { 72 | Id(uuid::Uuid), 73 | Name(Name), 74 | } 75 | impl From<&IdOrName> for IdOrName { 76 | fn from(value: &IdOrName) -> Self { 77 | value.clone() 78 | } 79 | } 80 | impl std::str::FromStr for IdOrName { 81 | type Err = &'static str; 82 | fn from_str(value: &str) -> Result { 83 | if let Ok(v) = value.parse() { 84 | Ok(Self::Id(v)) 85 | } else if let Ok(v) = value.parse() { 86 | Ok(Self::Name(v)) 87 | } else { 88 | Err("string conversion failed for all variants") 89 | } 90 | } 91 | } 92 | impl std::convert::TryFrom<&str> for IdOrName { 93 | type Error = &'static str; 94 | fn try_from(value: &str) -> Result { 95 | value.parse() 96 | } 97 | } 98 | impl std::convert::TryFrom<&String> for IdOrName { 99 | type Error = &'static str; 100 | fn try_from(value: &String) -> Result { 101 | value.parse() 102 | } 103 | } 104 | impl std::convert::TryFrom for IdOrName { 105 | type Error = &'static str; 106 | fn try_from(value: String) -> Result { 107 | value.parse() 108 | } 109 | } 110 | impl ::std::fmt::Display for IdOrName { 111 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 112 | match self { 113 | Self::Id(x) => x.fmt(f), 114 | Self::Name(x) => x.fmt(f), 115 | } 116 | } 117 | } 118 | impl From for IdOrName { 119 | fn from(value: uuid::Uuid) -> Self { 120 | Self::Id(value) 121 | } 122 | } 123 | impl From for IdOrName { 124 | fn from(value: Name) -> Self { 125 | Self::Name(value) 126 | } 127 | } 128 | #[doc = "Names must begin with a lower case ASCII letter, be composed exclusively of lowercase ASCII, uppercase ASCII, numbers, and '-', and may not end with a '-'. Names cannot be a UUID though they may contain a UUID."] 129 | #[derive(Clone, Debug, Serialize)] 130 | pub struct Name(String); 131 | impl std::ops::Deref for Name { 132 | type Target = String; 133 | fn deref(&self) -> &String { 134 | &self.0 135 | } 136 | } 137 | impl From for String { 138 | fn from(value: Name) -> Self { 139 | value.0 140 | } 141 | } 142 | impl From<&Name> for Name { 143 | fn from(value: &Name) -> Self { 144 | value.clone() 145 | } 146 | } 147 | impl std::str::FromStr for Name { 148 | type Err = &'static str; 149 | fn from_str(value: &str) -> Result { 150 | if value.len() > 63usize { 151 | return Err("longer than 63 characters"); 152 | } 153 | if regress::Regex::new("^(?![0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$)^[a-z][a-z0-9-]*[a-zA-Z0-9]$") 154 | .unwrap() 155 | .find(value) 156 | .is_none() 157 | { 158 | return Err("doesn't match pattern \"^(?![0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$)^[a-z][a-z0-9-]*[a-zA-Z0-9]$\""); 159 | } 160 | Ok(Self(value.to_string())) 161 | } 162 | } 163 | impl std::convert::TryFrom<&str> for Name { 164 | type Error = &'static str; 165 | fn try_from(value: &str) -> Result { 166 | value.parse() 167 | } 168 | } 169 | impl std::convert::TryFrom<&String> for Name { 170 | type Error = &'static str; 171 | fn try_from(value: &String) -> Result { 172 | value.parse() 173 | } 174 | } 175 | impl std::convert::TryFrom for Name { 176 | type Error = &'static str; 177 | fn try_from(value: String) -> Result { 178 | value.parse() 179 | } 180 | } 181 | impl<'de> serde::Deserialize<'de> for Name { 182 | fn deserialize(deserializer: D) -> Result 183 | where 184 | D: serde::Deserializer<'de>, 185 | { 186 | String::deserialize(deserializer)? 187 | .parse() 188 | .map_err(|e: &'static str| ::custom(e.to_string())) 189 | } 190 | } 191 | ``` 192 | 193 | ## Options 194 | 195 | See *`cargo typify --help`* for a complete list of options. 196 | 197 | The `--output` option lets you override the default output file (replacing the 198 | input file extension with `.rs`). Use `-` for stdout. 199 | 200 | Use `--no-builder` to disable struct builder generation (`--builder` is the 201 | default). Builder output lets you write code like this: 202 | 203 | ```rust 204 | let xy: MyStruct = MyStruct::builder().x_coord(x).y_coord(y).try_into(); 205 | ``` 206 | 207 | The `--additional-derive` adds the specified derive macro to all generated 208 | types. This may be specified more than once. -------------------------------------------------------------------------------- /cargo-typify/release.toml: -------------------------------------------------------------------------------- 1 | pre-release-replacements = [] 2 | -------------------------------------------------------------------------------- /cargo-typify/src/main.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Oxide Computer Company 2 | 3 | use cargo_typify::{convert, CliArgs}; 4 | use clap::Parser; 5 | 6 | use color_eyre::eyre::{Context, Result}; 7 | 8 | #[derive(Parser)] // requires `derive` feature 9 | #[command(name = "cargo")] 10 | #[command(bin_name = "cargo")] 11 | enum CargoCli { 12 | Typify(CliArgs), 13 | } 14 | 15 | fn main() -> Result<()> { 16 | env_logger::init(); 17 | color_eyre::install()?; 18 | 19 | let cli = CargoCli::parse(); 20 | let CargoCli::Typify(args) = cli; 21 | 22 | let contents = convert(&args).wrap_err("Failed to convert JSON Schema to Rust code")?; 23 | 24 | let output_path = args.output_path(); 25 | 26 | if let Some(output_path) = &output_path { 27 | std::fs::write(output_path, contents).wrap_err_with(|| { 28 | format!("Failed to write output to file: {}", output_path.display()) 29 | })?; 30 | } else { 31 | print!("{}", contents); 32 | } 33 | 34 | Ok(()) 35 | } 36 | -------------------------------------------------------------------------------- /cargo-typify/tests/integration.rs: -------------------------------------------------------------------------------- 1 | use expectorate::assert_contents; 2 | use newline_converter::dos2unix; 3 | use tempdir::TempDir; 4 | 5 | #[test] 6 | fn test_simple() { 7 | use assert_cmd::Command; 8 | 9 | let input = concat!(env!("CARGO_MANIFEST_DIR"), "/../example.json"); 10 | 11 | let temp = TempDir::new("cargo-typify").unwrap(); 12 | let input_file = temp.path().join("simple.json"); 13 | std::fs::copy(input, &input_file).unwrap(); 14 | 15 | let output_file = temp.path().join("simple.rs"); 16 | 17 | let mut cmd = Command::cargo_bin("cargo-typify").unwrap(); 18 | cmd.args(["typify", input_file.to_str().unwrap()]) 19 | .assert() 20 | .success(); 21 | 22 | let actual = std::fs::read_to_string(output_file).unwrap(); 23 | 24 | assert_contents("tests/outputs/builder.rs", &actual); 25 | } 26 | 27 | #[test] 28 | fn test_default_output() { 29 | use assert_cmd::Command; 30 | 31 | let input = concat!(env!("CARGO_MANIFEST_DIR"), "/../example.json"); 32 | 33 | let temp = TempDir::new("cargo-typify").unwrap(); 34 | let output_file = temp.path().join("output.rs"); 35 | 36 | let mut cmd = Command::cargo_bin("cargo-typify").unwrap(); 37 | cmd.args(["typify", input, "--output", output_file.to_str().unwrap()]) 38 | .assert() 39 | .success(); 40 | 41 | let content = std::fs::read_to_string(output_file).unwrap(); 42 | 43 | assert_contents("tests/outputs/builder.rs", &content); 44 | } 45 | 46 | #[test] 47 | fn test_no_builder_stdout() { 48 | use assert_cmd::Command; 49 | 50 | let input = concat!(env!("CARGO_MANIFEST_DIR"), "/../example.json"); 51 | 52 | let mut cmd = Command::cargo_bin("cargo-typify").unwrap(); 53 | 54 | let output = cmd 55 | .args(["typify", input, "--no-builder", "--output", "-"]) 56 | .output() 57 | .unwrap(); 58 | 59 | let output_stdout = String::from_utf8(output.stdout).unwrap(); 60 | let actual = dos2unix(&output_stdout); 61 | 62 | assert!(output.status.success()); 63 | assert_contents("tests/outputs/no-builder.rs", &actual); 64 | } 65 | 66 | #[test] 67 | fn test_builder() { 68 | use assert_cmd::Command; 69 | 70 | let input = concat!(env!("CARGO_MANIFEST_DIR"), "/../example.json"); 71 | 72 | let temp = TempDir::new("cargo-typify").unwrap(); 73 | let output_file = temp.path().join("output.rs"); 74 | 75 | let mut cmd = Command::cargo_bin("cargo-typify").unwrap(); 76 | cmd.args([ 77 | "typify", 78 | input, 79 | "--builder", 80 | "--output", 81 | output_file.to_str().unwrap(), 82 | ]) 83 | .assert() 84 | .success(); 85 | 86 | let actual = std::fs::read_to_string(output_file).unwrap(); 87 | 88 | assert_contents("tests/outputs/builder.rs", &actual); 89 | } 90 | 91 | #[test] 92 | fn test_derive() { 93 | use assert_cmd::Command; 94 | 95 | let input = concat!(env!("CARGO_MANIFEST_DIR"), "/../example.json"); 96 | 97 | let temp = TempDir::new("cargo-typify").unwrap(); 98 | let output_file = temp.path().join("output.rs"); 99 | 100 | let mut cmd = Command::cargo_bin("cargo-typify").unwrap(); 101 | cmd.args([ 102 | "typify", 103 | input, 104 | "--no-builder", 105 | "--additional-derive", 106 | "ExtraDerive", 107 | "--output", 108 | output_file.to_str().unwrap(), 109 | ]) 110 | .assert() 111 | .success(); 112 | 113 | let actual = std::fs::read_to_string(output_file).unwrap(); 114 | 115 | assert_contents("tests/outputs/derive.rs", &actual); 116 | } 117 | 118 | #[test] 119 | fn test_multi_derive() { 120 | use assert_cmd::Command; 121 | 122 | let input = concat!(env!("CARGO_MANIFEST_DIR"), "/../example.json"); 123 | 124 | let temp = TempDir::new("cargo-typify").unwrap(); 125 | let output_file = temp.path().join("output.rs"); 126 | 127 | let mut cmd = Command::cargo_bin("cargo-typify").unwrap(); 128 | cmd.args([ 129 | "typify", 130 | input, 131 | "--no-builder", 132 | "--additional-derive", 133 | "ExtraDerive", 134 | "--additional-derive", 135 | "AnotherDerive", 136 | "--output", 137 | output_file.to_str().unwrap(), 138 | ]) 139 | .assert() 140 | .success(); 141 | 142 | let actual = std::fs::read_to_string(output_file).unwrap(); 143 | 144 | assert_contents("tests/outputs/multi_derive.rs", &actual); 145 | } 146 | 147 | #[test] 148 | fn test_help() { 149 | use assert_cmd::Command; 150 | 151 | let mut cmd = Command::cargo_bin("cargo-typify").unwrap(); 152 | 153 | let output = cmd.args(["typify", "--help"]).output().unwrap(); 154 | 155 | let output_stdout = String::from_utf8(output.stdout).unwrap(); 156 | let actual = dos2unix(&output_stdout); 157 | 158 | assert!(output.status.success()); 159 | assert_contents("tests/outputs/help.txt", &actual); 160 | } 161 | 162 | #[test] 163 | fn test_btree_map() { 164 | use assert_cmd::Command; 165 | 166 | let input = concat!(env!("CARGO_MANIFEST_DIR"), "/../example.json"); 167 | 168 | let temp = TempDir::new("cargo-typify").unwrap(); 169 | let output_file = temp.path().join("output.rs"); 170 | 171 | let mut cmd = Command::cargo_bin("cargo-typify").unwrap(); 172 | cmd.args([ 173 | "typify", 174 | input, 175 | "--map-type", 176 | "::std::collections::BTreeMap", 177 | "--output", 178 | output_file.to_str().unwrap(), 179 | ]) 180 | .assert() 181 | .success(); 182 | 183 | let actual = std::fs::read_to_string(output_file).unwrap(); 184 | 185 | assert_contents("tests/outputs/custom_btree_map.rs", &actual); 186 | } 187 | -------------------------------------------------------------------------------- /cargo-typify/tests/outputs/derive.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::redundant_closure_call)] 2 | #![allow(clippy::needless_lifetimes)] 3 | #![allow(clippy::match_single_binding)] 4 | #![allow(clippy::clone_on_copy)] 5 | 6 | #[doc = r" Error types."] 7 | pub mod error { 8 | #[doc = r" Error from a `TryFrom` or `FromStr` implementation."] 9 | pub struct ConversionError(::std::borrow::Cow<'static, str>); 10 | impl ::std::error::Error for ConversionError {} 11 | impl ::std::fmt::Display for ConversionError { 12 | fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> Result<(), ::std::fmt::Error> { 13 | ::std::fmt::Display::fmt(&self.0, f) 14 | } 15 | } 16 | impl ::std::fmt::Debug for ConversionError { 17 | fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> Result<(), ::std::fmt::Error> { 18 | ::std::fmt::Debug::fmt(&self.0, f) 19 | } 20 | } 21 | impl From<&'static str> for ConversionError { 22 | fn from(value: &'static str) -> Self { 23 | Self(value.into()) 24 | } 25 | } 26 | impl From for ConversionError { 27 | fn from(value: String) -> Self { 28 | Self(value.into()) 29 | } 30 | } 31 | } 32 | #[doc = "`Fruit`"] 33 | #[doc = r""] 34 | #[doc = r"
JSON schema"] 35 | #[doc = r""] 36 | #[doc = r" ```json"] 37 | #[doc = "{"] 38 | #[doc = " \"type\": \"object\","] 39 | #[doc = " \"additionalProperties\": {"] 40 | #[doc = " \"type\": \"string\""] 41 | #[doc = " }"] 42 | #[doc = "}"] 43 | #[doc = r" ```"] 44 | #[doc = r"
"] 45 | #[derive(:: serde :: Deserialize, :: serde :: Serialize, Clone, Debug, ExtraDerive)] 46 | #[serde(transparent)] 47 | pub struct Fruit(pub ::std::collections::HashMap<::std::string::String, ::std::string::String>); 48 | impl ::std::ops::Deref for Fruit { 49 | type Target = ::std::collections::HashMap<::std::string::String, ::std::string::String>; 50 | fn deref(&self) -> &::std::collections::HashMap<::std::string::String, ::std::string::String> { 51 | &self.0 52 | } 53 | } 54 | impl ::std::convert::From 55 | for ::std::collections::HashMap<::std::string::String, ::std::string::String> 56 | { 57 | fn from(value: Fruit) -> Self { 58 | value.0 59 | } 60 | } 61 | impl ::std::convert::From<&Fruit> for Fruit { 62 | fn from(value: &Fruit) -> Self { 63 | value.clone() 64 | } 65 | } 66 | impl ::std::convert::From<::std::collections::HashMap<::std::string::String, ::std::string::String>> 67 | for Fruit 68 | { 69 | fn from( 70 | value: ::std::collections::HashMap<::std::string::String, ::std::string::String>, 71 | ) -> Self { 72 | Self(value) 73 | } 74 | } 75 | #[doc = "`FruitOrVeg`"] 76 | #[doc = r""] 77 | #[doc = r"
JSON schema"] 78 | #[doc = r""] 79 | #[doc = r" ```json"] 80 | #[doc = "{"] 81 | #[doc = " \"oneOf\": ["] 82 | #[doc = " {"] 83 | #[doc = " \"title\": \"veg\","] 84 | #[doc = " \"anyOf\": ["] 85 | #[doc = " {"] 86 | #[doc = " \"$ref\": \"#/defs/veggie\""] 87 | #[doc = " }"] 88 | #[doc = " ]"] 89 | #[doc = " },"] 90 | #[doc = " {"] 91 | #[doc = " \"title\": \"fruit\","] 92 | #[doc = " \"anyOf\": ["] 93 | #[doc = " {"] 94 | #[doc = " \"$ref\": \"#/defs/fruit\""] 95 | #[doc = " }"] 96 | #[doc = " ]"] 97 | #[doc = " }"] 98 | #[doc = " ]"] 99 | #[doc = "}"] 100 | #[doc = r" ```"] 101 | #[doc = r"
"] 102 | #[derive(:: serde :: Deserialize, :: serde :: Serialize, Clone, Debug, ExtraDerive)] 103 | #[serde(untagged)] 104 | pub enum FruitOrVeg { 105 | Veg(Veggie), 106 | Fruit(Fruit), 107 | } 108 | impl ::std::convert::From<&Self> for FruitOrVeg { 109 | fn from(value: &FruitOrVeg) -> Self { 110 | value.clone() 111 | } 112 | } 113 | impl ::std::convert::From for FruitOrVeg { 114 | fn from(value: Veggie) -> Self { 115 | Self::Veg(value) 116 | } 117 | } 118 | impl ::std::convert::From for FruitOrVeg { 119 | fn from(value: Fruit) -> Self { 120 | Self::Fruit(value) 121 | } 122 | } 123 | #[doc = "`Veggie`"] 124 | #[doc = r""] 125 | #[doc = r"
JSON schema"] 126 | #[doc = r""] 127 | #[doc = r" ```json"] 128 | #[doc = "{"] 129 | #[doc = " \"type\": \"object\","] 130 | #[doc = " \"required\": ["] 131 | #[doc = " \"veggieLike\","] 132 | #[doc = " \"veggieName\""] 133 | #[doc = " ],"] 134 | #[doc = " \"properties\": {"] 135 | #[doc = " \"veggieLike\": {"] 136 | #[doc = " \"description\": \"Do I like this vegetable?\","] 137 | #[doc = " \"type\": \"boolean\""] 138 | #[doc = " },"] 139 | #[doc = " \"veggieName\": {"] 140 | #[doc = " \"description\": \"The name of the vegetable.\","] 141 | #[doc = " \"type\": \"string\""] 142 | #[doc = " }"] 143 | #[doc = " }"] 144 | #[doc = "}"] 145 | #[doc = r" ```"] 146 | #[doc = r"
"] 147 | #[derive(:: serde :: Deserialize, :: serde :: Serialize, Clone, Debug, ExtraDerive)] 148 | pub struct Veggie { 149 | #[doc = "Do I like this vegetable?"] 150 | #[serde(rename = "veggieLike")] 151 | pub veggie_like: bool, 152 | #[doc = "The name of the vegetable."] 153 | #[serde(rename = "veggieName")] 154 | pub veggie_name: ::std::string::String, 155 | } 156 | impl ::std::convert::From<&Veggie> for Veggie { 157 | fn from(value: &Veggie) -> Self { 158 | value.clone() 159 | } 160 | } 161 | #[doc = "A representation of a person, company, organization, or place"] 162 | #[doc = r""] 163 | #[doc = r"
JSON schema"] 164 | #[doc = r""] 165 | #[doc = r" ```json"] 166 | #[doc = "{"] 167 | #[doc = " \"$id\": \"https://example.com/arrays.schema.json\","] 168 | #[doc = " \"title\": \"veggies\","] 169 | #[doc = " \"description\": \"A representation of a person, company, organization, or place\","] 170 | #[doc = " \"type\": \"object\","] 171 | #[doc = " \"properties\": {"] 172 | #[doc = " \"fruits\": {"] 173 | #[doc = " \"type\": \"array\","] 174 | #[doc = " \"items\": {"] 175 | #[doc = " \"type\": \"string\""] 176 | #[doc = " }"] 177 | #[doc = " },"] 178 | #[doc = " \"vegetables\": {"] 179 | #[doc = " \"type\": \"array\","] 180 | #[doc = " \"items\": {"] 181 | #[doc = " \"$ref\": \"#/$defs/veggie\""] 182 | #[doc = " }"] 183 | #[doc = " }"] 184 | #[doc = " }"] 185 | #[doc = "}"] 186 | #[doc = r" ```"] 187 | #[doc = r"
"] 188 | #[derive(:: serde :: Deserialize, :: serde :: Serialize, Clone, Debug, ExtraDerive)] 189 | pub struct Veggies { 190 | #[serde(default, skip_serializing_if = "::std::vec::Vec::is_empty")] 191 | pub fruits: ::std::vec::Vec<::std::string::String>, 192 | #[serde(default, skip_serializing_if = "::std::vec::Vec::is_empty")] 193 | pub vegetables: ::std::vec::Vec, 194 | } 195 | impl ::std::convert::From<&Veggies> for Veggies { 196 | fn from(value: &Veggies) -> Self { 197 | value.clone() 198 | } 199 | } 200 | impl ::std::default::Default for Veggies { 201 | fn default() -> Self { 202 | Self { 203 | fruits: Default::default(), 204 | vegetables: Default::default(), 205 | } 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /cargo-typify/tests/outputs/help.txt: -------------------------------------------------------------------------------- 1 | cargo command to generate Rust code from a JSON Schema 2 | 3 | Usage: cargo typify [OPTIONS] 4 | 5 | Arguments: 6 | 7 | The input file to read from 8 | 9 | Options: 10 | -b, --builder 11 | Whether to include a builder-style interface, this is the default 12 | 13 | -B, --no-builder 14 | Inverse of `--builder`. When set the builder-style interface will not be included 15 | 16 | -a, --additional-derive 17 | Add an additional derive macro to apply to all defined types 18 | 19 | -o, --output 20 | The output file to write to. If not specified, the input file name will be used with a `.rs` extension. 21 | 22 | If `-` is specified, the output will be written to stdout. 23 | 24 | --crate 25 | Specify each crate@version that can be assumed to be in use for types found in the schema with the x-rust-type extension 26 | 27 | --map-type 28 | Specify the map like type to use 29 | 30 | --unknown-crates 31 | Specify the policy unknown crates found in schemas with the x-rust-type extension 32 | 33 | [possible values: generate, allow, deny] 34 | 35 | -h, --help 36 | Print help (see a summary with '-h') 37 | 38 | -V, --version 39 | Print version 40 | -------------------------------------------------------------------------------- /cargo-typify/tests/outputs/multi_derive.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::redundant_closure_call)] 2 | #![allow(clippy::needless_lifetimes)] 3 | #![allow(clippy::match_single_binding)] 4 | #![allow(clippy::clone_on_copy)] 5 | 6 | #[doc = r" Error types."] 7 | pub mod error { 8 | #[doc = r" Error from a `TryFrom` or `FromStr` implementation."] 9 | pub struct ConversionError(::std::borrow::Cow<'static, str>); 10 | impl ::std::error::Error for ConversionError {} 11 | impl ::std::fmt::Display for ConversionError { 12 | fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> Result<(), ::std::fmt::Error> { 13 | ::std::fmt::Display::fmt(&self.0, f) 14 | } 15 | } 16 | impl ::std::fmt::Debug for ConversionError { 17 | fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> Result<(), ::std::fmt::Error> { 18 | ::std::fmt::Debug::fmt(&self.0, f) 19 | } 20 | } 21 | impl From<&'static str> for ConversionError { 22 | fn from(value: &'static str) -> Self { 23 | Self(value.into()) 24 | } 25 | } 26 | impl From for ConversionError { 27 | fn from(value: String) -> Self { 28 | Self(value.into()) 29 | } 30 | } 31 | } 32 | #[doc = "`Fruit`"] 33 | #[doc = r""] 34 | #[doc = r"
JSON schema"] 35 | #[doc = r""] 36 | #[doc = r" ```json"] 37 | #[doc = "{"] 38 | #[doc = " \"type\": \"object\","] 39 | #[doc = " \"additionalProperties\": {"] 40 | #[doc = " \"type\": \"string\""] 41 | #[doc = " }"] 42 | #[doc = "}"] 43 | #[doc = r" ```"] 44 | #[doc = r"
"] 45 | #[derive( 46 | :: serde :: Deserialize, :: serde :: Serialize, AnotherDerive, Clone, Debug, ExtraDerive, 47 | )] 48 | #[serde(transparent)] 49 | pub struct Fruit(pub ::std::collections::HashMap<::std::string::String, ::std::string::String>); 50 | impl ::std::ops::Deref for Fruit { 51 | type Target = ::std::collections::HashMap<::std::string::String, ::std::string::String>; 52 | fn deref(&self) -> &::std::collections::HashMap<::std::string::String, ::std::string::String> { 53 | &self.0 54 | } 55 | } 56 | impl ::std::convert::From 57 | for ::std::collections::HashMap<::std::string::String, ::std::string::String> 58 | { 59 | fn from(value: Fruit) -> Self { 60 | value.0 61 | } 62 | } 63 | impl ::std::convert::From<&Fruit> for Fruit { 64 | fn from(value: &Fruit) -> Self { 65 | value.clone() 66 | } 67 | } 68 | impl ::std::convert::From<::std::collections::HashMap<::std::string::String, ::std::string::String>> 69 | for Fruit 70 | { 71 | fn from( 72 | value: ::std::collections::HashMap<::std::string::String, ::std::string::String>, 73 | ) -> Self { 74 | Self(value) 75 | } 76 | } 77 | #[doc = "`FruitOrVeg`"] 78 | #[doc = r""] 79 | #[doc = r"
JSON schema"] 80 | #[doc = r""] 81 | #[doc = r" ```json"] 82 | #[doc = "{"] 83 | #[doc = " \"oneOf\": ["] 84 | #[doc = " {"] 85 | #[doc = " \"title\": \"veg\","] 86 | #[doc = " \"anyOf\": ["] 87 | #[doc = " {"] 88 | #[doc = " \"$ref\": \"#/defs/veggie\""] 89 | #[doc = " }"] 90 | #[doc = " ]"] 91 | #[doc = " },"] 92 | #[doc = " {"] 93 | #[doc = " \"title\": \"fruit\","] 94 | #[doc = " \"anyOf\": ["] 95 | #[doc = " {"] 96 | #[doc = " \"$ref\": \"#/defs/fruit\""] 97 | #[doc = " }"] 98 | #[doc = " ]"] 99 | #[doc = " }"] 100 | #[doc = " ]"] 101 | #[doc = "}"] 102 | #[doc = r" ```"] 103 | #[doc = r"
"] 104 | #[derive( 105 | :: serde :: Deserialize, :: serde :: Serialize, AnotherDerive, Clone, Debug, ExtraDerive, 106 | )] 107 | #[serde(untagged)] 108 | pub enum FruitOrVeg { 109 | Veg(Veggie), 110 | Fruit(Fruit), 111 | } 112 | impl ::std::convert::From<&Self> for FruitOrVeg { 113 | fn from(value: &FruitOrVeg) -> Self { 114 | value.clone() 115 | } 116 | } 117 | impl ::std::convert::From for FruitOrVeg { 118 | fn from(value: Veggie) -> Self { 119 | Self::Veg(value) 120 | } 121 | } 122 | impl ::std::convert::From for FruitOrVeg { 123 | fn from(value: Fruit) -> Self { 124 | Self::Fruit(value) 125 | } 126 | } 127 | #[doc = "`Veggie`"] 128 | #[doc = r""] 129 | #[doc = r"
JSON schema"] 130 | #[doc = r""] 131 | #[doc = r" ```json"] 132 | #[doc = "{"] 133 | #[doc = " \"type\": \"object\","] 134 | #[doc = " \"required\": ["] 135 | #[doc = " \"veggieLike\","] 136 | #[doc = " \"veggieName\""] 137 | #[doc = " ],"] 138 | #[doc = " \"properties\": {"] 139 | #[doc = " \"veggieLike\": {"] 140 | #[doc = " \"description\": \"Do I like this vegetable?\","] 141 | #[doc = " \"type\": \"boolean\""] 142 | #[doc = " },"] 143 | #[doc = " \"veggieName\": {"] 144 | #[doc = " \"description\": \"The name of the vegetable.\","] 145 | #[doc = " \"type\": \"string\""] 146 | #[doc = " }"] 147 | #[doc = " }"] 148 | #[doc = "}"] 149 | #[doc = r" ```"] 150 | #[doc = r"
"] 151 | #[derive( 152 | :: serde :: Deserialize, :: serde :: Serialize, AnotherDerive, Clone, Debug, ExtraDerive, 153 | )] 154 | pub struct Veggie { 155 | #[doc = "Do I like this vegetable?"] 156 | #[serde(rename = "veggieLike")] 157 | pub veggie_like: bool, 158 | #[doc = "The name of the vegetable."] 159 | #[serde(rename = "veggieName")] 160 | pub veggie_name: ::std::string::String, 161 | } 162 | impl ::std::convert::From<&Veggie> for Veggie { 163 | fn from(value: &Veggie) -> Self { 164 | value.clone() 165 | } 166 | } 167 | #[doc = "A representation of a person, company, organization, or place"] 168 | #[doc = r""] 169 | #[doc = r"
JSON schema"] 170 | #[doc = r""] 171 | #[doc = r" ```json"] 172 | #[doc = "{"] 173 | #[doc = " \"$id\": \"https://example.com/arrays.schema.json\","] 174 | #[doc = " \"title\": \"veggies\","] 175 | #[doc = " \"description\": \"A representation of a person, company, organization, or place\","] 176 | #[doc = " \"type\": \"object\","] 177 | #[doc = " \"properties\": {"] 178 | #[doc = " \"fruits\": {"] 179 | #[doc = " \"type\": \"array\","] 180 | #[doc = " \"items\": {"] 181 | #[doc = " \"type\": \"string\""] 182 | #[doc = " }"] 183 | #[doc = " },"] 184 | #[doc = " \"vegetables\": {"] 185 | #[doc = " \"type\": \"array\","] 186 | #[doc = " \"items\": {"] 187 | #[doc = " \"$ref\": \"#/$defs/veggie\""] 188 | #[doc = " }"] 189 | #[doc = " }"] 190 | #[doc = " }"] 191 | #[doc = "}"] 192 | #[doc = r" ```"] 193 | #[doc = r"
"] 194 | #[derive( 195 | :: serde :: Deserialize, :: serde :: Serialize, AnotherDerive, Clone, Debug, ExtraDerive, 196 | )] 197 | pub struct Veggies { 198 | #[serde(default, skip_serializing_if = "::std::vec::Vec::is_empty")] 199 | pub fruits: ::std::vec::Vec<::std::string::String>, 200 | #[serde(default, skip_serializing_if = "::std::vec::Vec::is_empty")] 201 | pub vegetables: ::std::vec::Vec, 202 | } 203 | impl ::std::convert::From<&Veggies> for Veggies { 204 | fn from(value: &Veggies) -> Self { 205 | value.clone() 206 | } 207 | } 208 | impl ::std::default::Default for Veggies { 209 | fn default() -> Self { 210 | Self { 211 | fruits: Default::default(), 212 | vegetables: Default::default(), 213 | } 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /cargo-typify/tests/outputs/no-builder.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::redundant_closure_call)] 2 | #![allow(clippy::needless_lifetimes)] 3 | #![allow(clippy::match_single_binding)] 4 | #![allow(clippy::clone_on_copy)] 5 | 6 | #[doc = r" Error types."] 7 | pub mod error { 8 | #[doc = r" Error from a `TryFrom` or `FromStr` implementation."] 9 | pub struct ConversionError(::std::borrow::Cow<'static, str>); 10 | impl ::std::error::Error for ConversionError {} 11 | impl ::std::fmt::Display for ConversionError { 12 | fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> Result<(), ::std::fmt::Error> { 13 | ::std::fmt::Display::fmt(&self.0, f) 14 | } 15 | } 16 | impl ::std::fmt::Debug for ConversionError { 17 | fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> Result<(), ::std::fmt::Error> { 18 | ::std::fmt::Debug::fmt(&self.0, f) 19 | } 20 | } 21 | impl From<&'static str> for ConversionError { 22 | fn from(value: &'static str) -> Self { 23 | Self(value.into()) 24 | } 25 | } 26 | impl From for ConversionError { 27 | fn from(value: String) -> Self { 28 | Self(value.into()) 29 | } 30 | } 31 | } 32 | #[doc = "`Fruit`"] 33 | #[doc = r""] 34 | #[doc = r"
JSON schema"] 35 | #[doc = r""] 36 | #[doc = r" ```json"] 37 | #[doc = "{"] 38 | #[doc = " \"type\": \"object\","] 39 | #[doc = " \"additionalProperties\": {"] 40 | #[doc = " \"type\": \"string\""] 41 | #[doc = " }"] 42 | #[doc = "}"] 43 | #[doc = r" ```"] 44 | #[doc = r"
"] 45 | #[derive(:: serde :: Deserialize, :: serde :: Serialize, Clone, Debug)] 46 | #[serde(transparent)] 47 | pub struct Fruit(pub ::std::collections::HashMap<::std::string::String, ::std::string::String>); 48 | impl ::std::ops::Deref for Fruit { 49 | type Target = ::std::collections::HashMap<::std::string::String, ::std::string::String>; 50 | fn deref(&self) -> &::std::collections::HashMap<::std::string::String, ::std::string::String> { 51 | &self.0 52 | } 53 | } 54 | impl ::std::convert::From 55 | for ::std::collections::HashMap<::std::string::String, ::std::string::String> 56 | { 57 | fn from(value: Fruit) -> Self { 58 | value.0 59 | } 60 | } 61 | impl ::std::convert::From<&Fruit> for Fruit { 62 | fn from(value: &Fruit) -> Self { 63 | value.clone() 64 | } 65 | } 66 | impl ::std::convert::From<::std::collections::HashMap<::std::string::String, ::std::string::String>> 67 | for Fruit 68 | { 69 | fn from( 70 | value: ::std::collections::HashMap<::std::string::String, ::std::string::String>, 71 | ) -> Self { 72 | Self(value) 73 | } 74 | } 75 | #[doc = "`FruitOrVeg`"] 76 | #[doc = r""] 77 | #[doc = r"
JSON schema"] 78 | #[doc = r""] 79 | #[doc = r" ```json"] 80 | #[doc = "{"] 81 | #[doc = " \"oneOf\": ["] 82 | #[doc = " {"] 83 | #[doc = " \"title\": \"veg\","] 84 | #[doc = " \"anyOf\": ["] 85 | #[doc = " {"] 86 | #[doc = " \"$ref\": \"#/defs/veggie\""] 87 | #[doc = " }"] 88 | #[doc = " ]"] 89 | #[doc = " },"] 90 | #[doc = " {"] 91 | #[doc = " \"title\": \"fruit\","] 92 | #[doc = " \"anyOf\": ["] 93 | #[doc = " {"] 94 | #[doc = " \"$ref\": \"#/defs/fruit\""] 95 | #[doc = " }"] 96 | #[doc = " ]"] 97 | #[doc = " }"] 98 | #[doc = " ]"] 99 | #[doc = "}"] 100 | #[doc = r" ```"] 101 | #[doc = r"
"] 102 | #[derive(:: serde :: Deserialize, :: serde :: Serialize, Clone, Debug)] 103 | #[serde(untagged)] 104 | pub enum FruitOrVeg { 105 | Veg(Veggie), 106 | Fruit(Fruit), 107 | } 108 | impl ::std::convert::From<&Self> for FruitOrVeg { 109 | fn from(value: &FruitOrVeg) -> Self { 110 | value.clone() 111 | } 112 | } 113 | impl ::std::convert::From for FruitOrVeg { 114 | fn from(value: Veggie) -> Self { 115 | Self::Veg(value) 116 | } 117 | } 118 | impl ::std::convert::From for FruitOrVeg { 119 | fn from(value: Fruit) -> Self { 120 | Self::Fruit(value) 121 | } 122 | } 123 | #[doc = "`Veggie`"] 124 | #[doc = r""] 125 | #[doc = r"
JSON schema"] 126 | #[doc = r""] 127 | #[doc = r" ```json"] 128 | #[doc = "{"] 129 | #[doc = " \"type\": \"object\","] 130 | #[doc = " \"required\": ["] 131 | #[doc = " \"veggieLike\","] 132 | #[doc = " \"veggieName\""] 133 | #[doc = " ],"] 134 | #[doc = " \"properties\": {"] 135 | #[doc = " \"veggieLike\": {"] 136 | #[doc = " \"description\": \"Do I like this vegetable?\","] 137 | #[doc = " \"type\": \"boolean\""] 138 | #[doc = " },"] 139 | #[doc = " \"veggieName\": {"] 140 | #[doc = " \"description\": \"The name of the vegetable.\","] 141 | #[doc = " \"type\": \"string\""] 142 | #[doc = " }"] 143 | #[doc = " }"] 144 | #[doc = "}"] 145 | #[doc = r" ```"] 146 | #[doc = r"
"] 147 | #[derive(:: serde :: Deserialize, :: serde :: Serialize, Clone, Debug)] 148 | pub struct Veggie { 149 | #[doc = "Do I like this vegetable?"] 150 | #[serde(rename = "veggieLike")] 151 | pub veggie_like: bool, 152 | #[doc = "The name of the vegetable."] 153 | #[serde(rename = "veggieName")] 154 | pub veggie_name: ::std::string::String, 155 | } 156 | impl ::std::convert::From<&Veggie> for Veggie { 157 | fn from(value: &Veggie) -> Self { 158 | value.clone() 159 | } 160 | } 161 | #[doc = "A representation of a person, company, organization, or place"] 162 | #[doc = r""] 163 | #[doc = r"
JSON schema"] 164 | #[doc = r""] 165 | #[doc = r" ```json"] 166 | #[doc = "{"] 167 | #[doc = " \"$id\": \"https://example.com/arrays.schema.json\","] 168 | #[doc = " \"title\": \"veggies\","] 169 | #[doc = " \"description\": \"A representation of a person, company, organization, or place\","] 170 | #[doc = " \"type\": \"object\","] 171 | #[doc = " \"properties\": {"] 172 | #[doc = " \"fruits\": {"] 173 | #[doc = " \"type\": \"array\","] 174 | #[doc = " \"items\": {"] 175 | #[doc = " \"type\": \"string\""] 176 | #[doc = " }"] 177 | #[doc = " },"] 178 | #[doc = " \"vegetables\": {"] 179 | #[doc = " \"type\": \"array\","] 180 | #[doc = " \"items\": {"] 181 | #[doc = " \"$ref\": \"#/$defs/veggie\""] 182 | #[doc = " }"] 183 | #[doc = " }"] 184 | #[doc = " }"] 185 | #[doc = "}"] 186 | #[doc = r" ```"] 187 | #[doc = r"
"] 188 | #[derive(:: serde :: Deserialize, :: serde :: Serialize, Clone, Debug)] 189 | pub struct Veggies { 190 | #[serde(default, skip_serializing_if = "::std::vec::Vec::is_empty")] 191 | pub fruits: ::std::vec::Vec<::std::string::String>, 192 | #[serde(default, skip_serializing_if = "::std::vec::Vec::is_empty")] 193 | pub vegetables: ::std::vec::Vec, 194 | } 195 | impl ::std::convert::From<&Veggies> for Veggies { 196 | fn from(value: &Veggies) -> Self { 197 | value.clone() 198 | } 199 | } 200 | impl ::std::default::Default for Veggies { 201 | fn default() -> Self { 202 | Self { 203 | fruits: Default::default(), 204 | vegetables: Default::default(), 205 | } 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /example-build/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "example-build" 3 | version = "0.0.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | serde = { version = "1.0", features = ["derive"] } 8 | serde_json = "1.0" 9 | 10 | [build-dependencies] 11 | prettyplease = "0.2" 12 | schemars = "0.8" 13 | serde_json = "1.0" 14 | syn = "2.0" 15 | typify = { path = "../typify" } 16 | -------------------------------------------------------------------------------- /example-build/build.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Oxide Computer Company 2 | 3 | use std::{env, fs, path::Path}; 4 | 5 | use typify::{TypeSpace, TypeSpaceSettings}; 6 | 7 | fn main() { 8 | let content = std::fs::read_to_string("../example.json").unwrap(); 9 | let schema = serde_json::from_str::(&content).unwrap(); 10 | 11 | let mut type_space = TypeSpace::new(TypeSpaceSettings::default().with_struct_builder(true)); 12 | type_space.add_root_schema(schema).unwrap(); 13 | 14 | let contents = 15 | prettyplease::unparse(&syn::parse2::(type_space.to_stream()).unwrap()); 16 | 17 | let mut out_file = Path::new(&env::var("OUT_DIR").unwrap()).to_path_buf(); 18 | out_file.push("codegen.rs"); 19 | fs::write(out_file, contents).unwrap(); 20 | } 21 | -------------------------------------------------------------------------------- /example-build/release.toml: -------------------------------------------------------------------------------- 1 | release = false 2 | -------------------------------------------------------------------------------- /example-build/src/main.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Oxide Computer Company 2 | 3 | // Include the generated code. 4 | include!(concat!(env!("OUT_DIR"), "/codegen.rs")); 5 | 6 | #[test] 7 | fn test_main() { 8 | main() 9 | } 10 | 11 | fn main() { 12 | let veg = Veggie::builder() 13 | .veggie_name("carrots") 14 | .veggie_like(true) 15 | .try_into() 16 | .unwrap(); 17 | 18 | let veggies = Veggies { 19 | fruits: vec![String::from("apple"), String::from("mango")], 20 | vegetables: vec![veg], 21 | }; 22 | println!("{:?}", veggies); 23 | } 24 | -------------------------------------------------------------------------------- /example-macro/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "example-macro" 3 | version = "0.0.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | typify = { path = "../typify" } 8 | serde = "1.0" 9 | serde_json = "1.0" 10 | -------------------------------------------------------------------------------- /example-macro/release.toml: -------------------------------------------------------------------------------- 1 | release = false 2 | -------------------------------------------------------------------------------- /example-macro/src/main.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Oxide Computer Company 2 | 3 | use typify::import_types; 4 | 5 | use serde::{Deserialize, Serialize}; 6 | 7 | #[derive(Debug, Clone, Deserialize, Serialize)] 8 | pub struct MyFruit { 9 | seeds: (), 10 | } 11 | 12 | import_types!( 13 | schema = "../example.json", 14 | patch = { 15 | Veggie = { 16 | rename = "Vegetable", 17 | }, 18 | }, 19 | replace = { 20 | Fruit = MyFruit: ?Display, 21 | } 22 | ); 23 | 24 | #[test] 25 | fn test_main() { 26 | main() 27 | } 28 | 29 | fn main() { 30 | let veg = Vegetable { 31 | veggie_name: String::from("carrots"), 32 | veggie_like: true, 33 | }; 34 | let veggies = Veggies { 35 | fruits: vec![String::from("apple"), String::from("mango")], 36 | vegetables: vec![veg], 37 | }; 38 | println!("{:?}", veggies); 39 | let fov = FruitOrVeg::Fruit(MyFruit { seeds: () }); 40 | println!("{:?}", fov); 41 | } 42 | -------------------------------------------------------------------------------- /example.json: -------------------------------------------------------------------------------- 1 | { 2 | "$id": "https://example.com/arrays.schema.json", 3 | "$schema": "https://json-schema.org/draft/2020-12/schema", 4 | "title": "veggies", 5 | "description": "A representation of a person, company, organization, or place", 6 | "type": "object", 7 | "properties": { 8 | "fruits": { 9 | "type": "array", 10 | "items": { 11 | "type": "string" 12 | } 13 | }, 14 | "vegetables": { 15 | "type": "array", 16 | "items": { 17 | "$ref": "#/$defs/veggie" 18 | } 19 | } 20 | }, 21 | "$defs": { 22 | "veggie": { 23 | "type": "object", 24 | "required": [ 25 | "veggieName", 26 | "veggieLike" 27 | ], 28 | "properties": { 29 | "veggieName": { 30 | "type": "string", 31 | "description": "The name of the vegetable." 32 | }, 33 | "veggieLike": { 34 | "type": "boolean", 35 | "description": "Do I like this vegetable?" 36 | } 37 | } 38 | }, 39 | "fruit": { 40 | "type": "object", 41 | "additionalProperties": { 42 | "type": "string" 43 | } 44 | }, 45 | "fruit-or-veg": { 46 | "oneOf": [ 47 | { 48 | "title": "veg", 49 | "anyOf": [ 50 | { 51 | "$ref": "#/defs/veggie" 52 | } 53 | ] 54 | }, 55 | { 56 | "title": "fruit", 57 | "anyOf": [ 58 | { 59 | "$ref": "#/defs/fruit" 60 | } 61 | ] 62 | } 63 | ] 64 | } 65 | } 66 | } -------------------------------------------------------------------------------- /extension-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "schema for the x-rust-type extension", 3 | "type": "object", 4 | "properties": { 5 | "crate": { 6 | "type": "string", 7 | "pattern": "^[a-zA-Z0-9_-]+$" 8 | }, 9 | "version": { 10 | "description": "semver requirements per a Cargo.toml dependencies entry", 11 | "type": "string" 12 | }, 13 | "path": { 14 | "type": "string", 15 | "pattern": "^[a-zA-Z0-9_]+(::[a-zA-Z0-9+]+)*$" 16 | }, 17 | "parameters": { 18 | "type": "array", 19 | "items": { 20 | "$ref": "#/definitions/Schema" 21 | } 22 | } 23 | }, 24 | "required": [ 25 | "crate", 26 | "path", 27 | "version" 28 | ] 29 | } -------------------------------------------------------------------------------- /release.toml: -------------------------------------------------------------------------------- 1 | # This file is used by cargo-release. 2 | 3 | # Update the change log to reflect the new release and set us up for the next release. 4 | pre-release-replacements = [ 5 | # First, replace the current "Unreleased changes" header with one reflecting the new release version and date. 6 | {file="../CHANGELOG.adoc", search="Unreleased changes \\(release date TBD\\)", replace="{{version}} (released {{date}})", exactly=1}, 7 | # Update the link to the list of raw commits in the formerly "Unreleased changes" section. It should end at the tag for the newly-released version. 8 | {file="../CHANGELOG.adoc", search="\\\\.\\.\\.HEAD", replace="\\...{{tag_name}}", exactly=1}, 9 | # Next, append a new "Unreleased changes" header beneath the sentinel line. 10 | {file="../CHANGELOG.adoc", search="// cargo-release: next header goes here \\(do not change this line\\)", replace="// cargo-release: next header goes here (do not change this line)\n\n== Unreleased changes (release date TBD)\n\nhttps://github.com/oxidecomputer/typify/compare/{{tag_name}}\\...HEAD[Full list of commits]", exactly=1}, 11 | ] 12 | 13 | pre-release-commit-message = "release typify {{version}}" 14 | tag-message = "release {{crate_name}} {{version}}" 15 | tag-prefix = "" 16 | consolidate-commits = true 17 | push = false 18 | shared-version = true 19 | dependent-version = "upgrade" 20 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "1.81.0" 3 | profile = "default" 4 | -------------------------------------------------------------------------------- /typify-impl/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "typify-impl" 3 | version = "0.4.2" 4 | edition = "2021" 5 | license = "Apache-2.0" 6 | description = "typify backend implementation" 7 | repository = "https://github.com/oxidecomputer/typify" 8 | readme = "../README.md" 9 | 10 | [dependencies] 11 | heck = { workspace = true } 12 | log = { workspace = true } 13 | proc-macro2 = { workspace = true } 14 | quote = { workspace = true } 15 | regress = { workspace = true } 16 | schemars = { workspace = true } 17 | semver = { workspace = true } 18 | serde = { workspace = true } 19 | serde_json = { workspace = true } 20 | syn = { workspace = true } 21 | thiserror = { workspace = true } 22 | unicode-ident = { workspace = true } 23 | 24 | [dev-dependencies] 25 | env_logger = { workspace = true } 26 | expectorate = { workspace = true } 27 | paste = { workspace = true } 28 | rustfmt-wrapper = { workspace = true } 29 | schema = { workspace = true } 30 | schemars = { workspace = true, features = ["uuid1", "impl_json_schema"] } 31 | syn = { workspace = true, features = ["full", "extra-traits", "visit-mut"] } 32 | uuid = { workspace = true } 33 | -------------------------------------------------------------------------------- /typify-impl/release.toml: -------------------------------------------------------------------------------- 1 | pre-release-replacements = [] 2 | -------------------------------------------------------------------------------- /typify-impl/src/conversions.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Oxide Computer Company 2 | 3 | use schemars::schema::SchemaObject; 4 | 5 | use crate::{type_entry::TypeEntry, TypeSpaceImpl}; 6 | 7 | // TODO Everything about this is inefficient. 8 | 9 | #[derive(Debug, Default)] 10 | pub(crate) struct SchemaCache { 11 | schemas: Vec<(SchemaObject, TypeEntry)>, 12 | } 13 | 14 | impl SchemaCache { 15 | pub fn insert(&mut self, schema: &SchemaObject, type_name: &String, impls: &[TypeSpaceImpl]) { 16 | let type_entry = TypeEntry::new_native(type_name, impls); 17 | self.schemas.push(( 18 | SchemaObject { 19 | metadata: None, 20 | ..schema.clone() 21 | }, 22 | type_entry, 23 | )); 24 | } 25 | 26 | pub fn lookup(&self, search_schema: &SchemaObject) -> Option { 27 | let search_schema = SchemaObject { 28 | metadata: None, 29 | ..search_schema.clone() 30 | }; 31 | self.schemas 32 | .iter() 33 | .filter(|(schema, _)| &search_schema == schema) 34 | .map(|(_, type_entry)| type_entry.clone()) 35 | .next() 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /typify-impl/src/cycles.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Oxide Computer Company 2 | 3 | use std::{ 4 | collections::{BTreeMap, BTreeSet}, 5 | ops::Range, 6 | }; 7 | 8 | use crate::{ 9 | type_entry::{ 10 | TypeEntry, TypeEntryDetails, TypeEntryEnum, TypeEntryNewtype, TypeEntryStruct, 11 | VariantDetails, 12 | }, 13 | TypeId, TypeSpace, 14 | }; 15 | 16 | impl TypeSpace { 17 | /// We need to root out any containment cycles, breaking them by inserting 18 | /// a `Box` type. Our choice of *where* to break cycles is more arbitrary 19 | /// than optimal, but is well beyond sufficient. 20 | pub fn break_cycles(&mut self, range: Range) { 21 | enum Node { 22 | Start { 23 | type_id: TypeId, 24 | }, 25 | Processing { 26 | type_id: TypeId, 27 | children_ids: Vec, 28 | }, 29 | } 30 | 31 | let mut visited = BTreeSet::::new(); 32 | 33 | for id in range { 34 | let type_id = TypeId(id); 35 | 36 | // This isn't strictly necessary, but we'll short-circuit some work 37 | // by checking this right away. 38 | if visited.contains(&type_id) { 39 | continue; 40 | } 41 | 42 | let mut active = BTreeSet::::new(); 43 | let mut stack = Vec::::new(); 44 | 45 | active.insert(type_id.clone()); 46 | stack.push(Node::Start { type_id }); 47 | 48 | while let Some(top) = stack.last_mut() { 49 | match top { 50 | // Skip right to the end since we've already seen this type. 51 | Node::Start { type_id } if visited.contains(type_id) => { 52 | assert!(active.contains(type_id)); 53 | 54 | let type_id = type_id.clone(); 55 | *top = Node::Processing { 56 | type_id, 57 | children_ids: Vec::new(), 58 | }; 59 | } 60 | 61 | // Break any immediate cycles and queue up this type for 62 | // descent into its child types. 63 | Node::Start { type_id } => { 64 | assert!(active.contains(type_id)); 65 | 66 | visited.insert(type_id.clone()); 67 | 68 | // Determine which child types form cycles--and 69 | // therefore need to be snipped--and the rest--into 70 | // which we should descend. We make this its own block 71 | // to clarify the lifetime of the exclusive reference 72 | // to the type. We don't really *need* to have an 73 | // exclusive reference here, but there's no point in 74 | // writing `get_child_ids` again for shared references. 75 | let (snip, descend) = { 76 | let type_entry = self.id_to_entry.get_mut(type_id).unwrap(); 77 | 78 | let child_ids = get_child_ids(type_entry) 79 | .into_iter() 80 | .map(|child_id| child_id.clone()); 81 | 82 | // If the child type is in active then we've found 83 | // a cycle (otherwise we'll descend). 84 | child_ids.partition::, _>(|child_id| active.contains(child_id)) 85 | }; 86 | 87 | // Note that while `snip` might contain duplicates, 88 | // `id_to_box` is idempotent insofar as the same input 89 | // TypeId will result in the same output TypeId. Ergo 90 | // the resulting pairs from which we construct the 91 | // mapping would contain exact duplicates; it would not 92 | // contain two values associated with the same key. 93 | let replace = snip 94 | .into_iter() 95 | .map(|type_id| { 96 | let box_id = self.id_to_box(&type_id); 97 | 98 | (type_id, box_id) 99 | }) 100 | .collect::>(); 101 | 102 | // Break any cycles by reassigning the child type to a box. 103 | let type_entry = self.id_to_entry.get_mut(type_id).unwrap(); 104 | let child_ids = get_child_ids(type_entry); 105 | for child_id in child_ids { 106 | if let Some(replace_id) = replace.get(child_id) { 107 | *child_id = replace_id.clone(); 108 | } 109 | } 110 | 111 | // Descend into child types. 112 | let node = Node::Processing { 113 | type_id: type_id.clone(), 114 | children_ids: descend, 115 | }; 116 | *top = node; 117 | } 118 | 119 | // If there are children left, push the next child onto the 120 | // stack. If there are none left, pop this type. 121 | Node::Processing { 122 | type_id, 123 | children_ids, 124 | } => { 125 | if let Some(type_id) = children_ids.pop() { 126 | // Descend into the next child node. 127 | active.insert(type_id.clone()); 128 | stack.push(Node::Start { type_id }); 129 | } else { 130 | // All done; remove the item from the active list 131 | // and stack. 132 | active.remove(type_id); 133 | let _ = stack.pop(); 134 | } 135 | } 136 | } 137 | } 138 | } 139 | } 140 | } 141 | 142 | /// For types that could potentially participate in a cycle, return a list of 143 | /// mutable references to the child types. 144 | fn get_child_ids(type_entry: &mut TypeEntry) -> Vec<&mut TypeId> { 145 | match &mut type_entry.details { 146 | TypeEntryDetails::Enum(TypeEntryEnum { variants, .. }) => variants 147 | .iter_mut() 148 | .flat_map(|variant| match &mut variant.details { 149 | VariantDetails::Simple => Vec::new(), 150 | VariantDetails::Item(type_id) => vec![type_id], 151 | VariantDetails::Tuple(type_ids) => type_ids.iter_mut().collect(), 152 | VariantDetails::Struct(properties) => properties 153 | .iter_mut() 154 | .map(|prop| &mut prop.type_id) 155 | .collect(), 156 | }) 157 | .collect::>(), 158 | 159 | TypeEntryDetails::Struct(TypeEntryStruct { properties, .. }) => properties 160 | .iter_mut() 161 | .map(|prop| &mut prop.type_id) 162 | .collect(), 163 | 164 | TypeEntryDetails::Newtype(TypeEntryNewtype { type_id, .. }) => { 165 | vec![type_id] 166 | } 167 | 168 | // Unnamed types that can participate in containment cycles. 169 | TypeEntryDetails::Option(type_id) => vec![type_id], 170 | TypeEntryDetails::Array(type_id, _) => vec![type_id], 171 | TypeEntryDetails::Tuple(type_ids) => type_ids.iter_mut().collect(), 172 | 173 | _ => Vec::new(), 174 | } 175 | } 176 | 177 | #[cfg(test)] 178 | mod tests { 179 | use schema::Schema; 180 | use schemars::JsonSchema; 181 | 182 | use crate::test_util::validate_output; 183 | 184 | #[test] 185 | fn test_trivial_cycle() { 186 | #[derive(JsonSchema, Schema)] 187 | #[allow(dead_code)] 188 | struct A { 189 | a: Box, 190 | } 191 | 192 | validate_output::(); 193 | } 194 | 195 | #[test] 196 | fn test_optional_trivial_cycle() { 197 | #[derive(JsonSchema, Schema)] 198 | #[allow(dead_code)] 199 | struct A { 200 | a: Option>, 201 | } 202 | 203 | validate_output::(); 204 | } 205 | 206 | #[test] 207 | fn test_enum_trivial_cycles() { 208 | #[derive(JsonSchema, Schema)] 209 | #[allow(dead_code)] 210 | enum A { 211 | Variant0(u64), 212 | Variant1 { 213 | a: u64, 214 | b: Vec, 215 | rop: Option>, 216 | }, 217 | Variant2 { 218 | a: Box, 219 | }, 220 | Variant3(u64, Box), 221 | Variant4(Option>, String), 222 | } 223 | 224 | validate_output::(); 225 | } 226 | 227 | #[test] 228 | fn test_newtype_trivial_cycle() { 229 | #[derive(JsonSchema, Schema)] 230 | #[allow(dead_code)] 231 | struct A(Box); 232 | 233 | validate_output::(); 234 | } 235 | 236 | #[test] 237 | fn test_abab_cycle() { 238 | #[derive(JsonSchema, Schema)] 239 | #[allow(dead_code)] 240 | struct A(B); 241 | 242 | #[derive(JsonSchema, Schema)] 243 | #[allow(dead_code)] 244 | struct B(Box); 245 | 246 | validate_output::(); 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /typify-impl/src/output.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Oxide Computer Company 2 | 3 | use std::collections::BTreeMap; 4 | 5 | use proc_macro2::TokenStream; 6 | use quote::quote; 7 | 8 | #[derive(Debug, Default)] 9 | pub struct OutputSpace { 10 | items: BTreeMap<(OutputSpaceMod, String), TokenStream>, 11 | } 12 | 13 | #[derive(Clone, Debug, Copy, PartialEq, Eq, PartialOrd, Ord)] 14 | pub enum OutputSpaceMod { 15 | Error, 16 | Crate, 17 | Builder, 18 | Defaults, 19 | } 20 | 21 | impl OutputSpace { 22 | pub fn add_item( 23 | &mut self, 24 | location: OutputSpaceMod, 25 | order_hint: impl ToString, 26 | stream: TokenStream, 27 | ) { 28 | self.items 29 | .entry((location, order_hint.to_string())) 30 | .or_insert_with(TokenStream::new) 31 | .extend(stream); 32 | } 33 | 34 | pub fn into_stream(self) -> TokenStream { 35 | let mods = self 36 | .items 37 | .into_iter() 38 | .map(|((location, _), item)| (location, item)) 39 | .fold(BTreeMap::new(), |mut map, (location, item)| { 40 | map.entry(location) 41 | .or_insert_with(TokenStream::new) 42 | .extend(item); 43 | map 44 | }); 45 | 46 | let mod_streams = mods.into_iter().map(|(location, items)| match location { 47 | OutputSpaceMod::Crate => quote! { 48 | #items 49 | }, 50 | OutputSpaceMod::Builder => quote! { 51 | /// Types for composing complex structures. 52 | pub mod builder { 53 | #items 54 | } 55 | }, 56 | OutputSpaceMod::Defaults => quote! { 57 | /// Generation of default values for serde. 58 | pub mod defaults { 59 | #items 60 | } 61 | }, 62 | OutputSpaceMod::Error => quote! { 63 | /// Error types. 64 | pub mod error { 65 | #items 66 | } 67 | }, 68 | }); 69 | 70 | quote! { 71 | #(#mod_streams)* 72 | } 73 | } 74 | } 75 | 76 | #[cfg(test)] 77 | mod tests { 78 | use super::{OutputSpace, OutputSpaceMod}; 79 | 80 | use quote::quote; 81 | 82 | #[test] 83 | fn test_order() { 84 | let mut output = OutputSpace::default(); 85 | output.add_item( 86 | OutputSpaceMod::Crate, 87 | "a", 88 | quote! { 89 | struct A; 90 | }, 91 | ); 92 | output.add_item( 93 | OutputSpaceMod::Crate, 94 | "b", 95 | quote! { 96 | struct B; 97 | }, 98 | ); 99 | output.add_item( 100 | OutputSpaceMod::Crate, 101 | "a", 102 | quote! { 103 | impl A { 104 | fn new() -> Self { Self } 105 | } 106 | }, 107 | ); 108 | 109 | assert_eq!( 110 | output.into_stream().to_string(), 111 | quote! { 112 | struct A; 113 | impl A { 114 | fn new() -> Self { Self } 115 | } 116 | struct B; 117 | } 118 | .to_string() 119 | ); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /typify-impl/src/rust_extension.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Oxide Computer Company 2 | 3 | use log::warn; 4 | use schemars::schema::{Schema, SchemaObject}; 5 | use serde::Deserialize; 6 | 7 | use crate::{type_entry::TypeEntry, CrateVers, Name, Result, TypeSpace}; 8 | 9 | const RUST_TYPE_EXTENSION: &str = "x-rust-type"; 10 | 11 | /// Definition of the value of the x-rust-type extension. This structure 12 | /// must not change incompatibly (and probably shouldn't change at all). 13 | #[derive(Deserialize)] 14 | struct RustExtension { 15 | #[serde(rename = "crate")] 16 | crate_name: String, 17 | version: String, 18 | path: String, 19 | #[serde(default)] 20 | parameters: Vec, 21 | } 22 | 23 | impl TypeSpace { 24 | pub(crate) fn convert_rust_extension(&mut self, schema: &SchemaObject) -> Option { 25 | let x_rust = schema.extensions.get(RUST_TYPE_EXTENSION)?; 26 | 27 | let Ok(RustExtension { 28 | crate_name, 29 | version, 30 | path, 31 | parameters, 32 | }) = serde_json::from_value(x_rust.clone()) 33 | else { 34 | warn!( 35 | "{} contains an invalid value for {}", 36 | serde_json::to_string_pretty(&schema).unwrap(), 37 | RUST_TYPE_EXTENSION, 38 | ); 39 | return None; 40 | }; 41 | 42 | let Ok(req) = semver::VersionReq::parse(&version) else { 43 | warn!( 44 | "{} contains an invalid version", 45 | serde_json::to_string_pretty(&schema).unwrap(), 46 | ); 47 | return None; 48 | }; 49 | 50 | let crate_ident = crate_name.replace('-', "_"); 51 | let path_sep = path.find("::")?; 52 | if crate_ident != path[..path_sep] { 53 | warn!( 54 | "{} path doesn't start with crate name", 55 | serde_json::to_string_pretty(&schema).unwrap(), 56 | ); 57 | return None; 58 | } 59 | 60 | let path = { 61 | if let Some(crate_spec) = self.settings.crates.get(crate_name.as_str()) { 62 | // The version must be non-Never and match the requirements 63 | // from the extension. 64 | match &crate_spec.version { 65 | CrateVers::Any => (), 66 | CrateVers::Version(version) if req.matches(version) => (), 67 | _ => return None, 68 | } 69 | 70 | // Replace the initial path component with the new crate name. 71 | if let Some(new_crate) = &crate_spec.rename { 72 | format!("{}{}", new_crate.replace('-', "_"), &path[path_sep..]) 73 | } else { 74 | path 75 | } 76 | } else { 77 | match self.settings.unknown_crates { 78 | crate::UnknownPolicy::Generate => return None, 79 | crate::UnknownPolicy::Allow => path, 80 | 81 | // TODO need to bubble up a coherent compiler error via the 82 | // generated code. 83 | crate::UnknownPolicy::Deny => return None, 84 | } 85 | } 86 | }; 87 | 88 | // Convert and collect type parameters. 89 | let param_ids = parameters 90 | .iter() 91 | .map(|p_schema| { 92 | // TODO could we have some reasonable type name? Do we need to? 93 | let (param_id, _) = self.id_for_schema(Name::Unknown, p_schema)?; 94 | Ok(param_id) 95 | }) 96 | .collect::>>() 97 | .ok()?; 98 | 99 | Some(TypeEntry::new_native_params( 100 | format!("::{path}"), 101 | ¶m_ids, 102 | )) 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /typify-impl/src/validate.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Oxide Computer Company 2 | 3 | use std::collections::BTreeMap; 4 | 5 | use schemars::schema::{InstanceType, Schema, SchemaObject, SingleOrVec}; 6 | use serde_json::Value; 7 | 8 | use crate::RefKey; 9 | 10 | pub(crate) fn schema_value_validate( 11 | schema: &Schema, 12 | value: &Value, 13 | defs: &BTreeMap, 14 | ) -> Result<(), String> { 15 | match schema { 16 | Schema::Bool(false) => Err("never schema".to_string()), 17 | Schema::Bool(true) => Ok(()), 18 | Schema::Object(object) => schema_object_value_validate(object, value, defs), 19 | } 20 | } 21 | 22 | fn schema_object_value_validate( 23 | object: &SchemaObject, 24 | value: &Value, 25 | _defs: &BTreeMap, 26 | ) -> Result<(), String> { 27 | if let Some(const_value) = &object.const_value { 28 | if value != const_value { 29 | return Err(format!( 30 | "{} does not match the const value {}", 31 | value, const_value, 32 | )); 33 | } 34 | } 35 | if let Some(enum_values) = &object.enum_values { 36 | if !enum_values.contains(value) { 37 | return Err(format!( 38 | "{} does not match the enum values {}", 39 | value, 40 | serde_json::to_string(enum_values).unwrap(), 41 | )); 42 | } 43 | } 44 | 45 | let SchemaObject { instance_type, .. } = object; 46 | 47 | schema_object_value_validate_instance_type(instance_type.as_ref(), value)?; 48 | 49 | Ok(()) 50 | } 51 | 52 | fn schema_object_value_validate_instance_type( 53 | instance_type: Option<&SingleOrVec>, 54 | value: &Value, 55 | ) -> Result<(), String> { 56 | match instance_type { 57 | None => Ok(()), 58 | Some(SingleOrVec::Single(it)) => check_instance(it, value), 59 | Some(SingleOrVec::Vec(its)) => its 60 | .iter() 61 | .any(|it| check_instance(it, value).is_ok()) 62 | .then_some(()) 63 | .ok_or_else(|| "no valid instance type".to_string()), 64 | } 65 | } 66 | 67 | fn check_instance(it: &InstanceType, value: &Value) -> Result<(), String> { 68 | match it { 69 | InstanceType::Null => value 70 | .is_null() 71 | .then_some(()) 72 | .ok_or_else(|| "not null".to_string()), 73 | InstanceType::Boolean => value 74 | .is_boolean() 75 | .then_some(()) 76 | .ok_or_else(|| "not boolean".to_string()), 77 | InstanceType::Object => value 78 | .is_object() 79 | .then_some(()) 80 | .ok_or_else(|| "not object".to_string()), 81 | InstanceType::Array => value 82 | .is_array() 83 | .then_some(()) 84 | .ok_or_else(|| "not array".to_string()), 85 | InstanceType::Number => value 86 | .is_number() 87 | .then_some(()) 88 | .ok_or_else(|| "not number".to_string()), 89 | InstanceType::String => value 90 | .is_string() 91 | .then_some(()) 92 | .ok_or_else(|| "not string".to_string()), 93 | InstanceType::Integer => (value.is_i64() || value.is_u64()) 94 | .then_some(()) 95 | .ok_or_else(|| "not integer".to_string()), 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /typify-impl/tests/test_generation.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Oxide Computer Company 2 | 3 | use quote::quote; 4 | use schemars::{gen::SchemaGenerator, schema::Schema, JsonSchema}; 5 | use serde::Serialize; 6 | use typify_impl::{TypeSpace, TypeSpacePatch, TypeSpaceSettings}; 7 | 8 | #[allow(dead_code)] 9 | #[derive(JsonSchema)] 10 | struct CompoundType { 11 | value1: String, 12 | value2: u64, 13 | } 14 | 15 | #[allow(dead_code)] 16 | #[derive(JsonSchema, Serialize)] 17 | enum StringEnum { 18 | One, 19 | Two, 20 | BuckleMyShoe, 21 | } 22 | 23 | #[allow(dead_code)] 24 | #[derive(JsonSchema, Serialize)] 25 | #[serde(default = "default_pair")] 26 | struct Pair { 27 | a: StringEnum, 28 | b: StringEnum, 29 | } 30 | 31 | fn default_pair() -> Pair { 32 | Pair { 33 | a: StringEnum::One, 34 | b: StringEnum::Two, 35 | } 36 | } 37 | 38 | #[derive(JsonSchema)] 39 | #[allow(dead_code)] 40 | struct AllTheTraits { 41 | ok: String, 42 | } 43 | 44 | fn add_type(generator: &mut SchemaGenerator) -> Schema { 45 | let mut schema = T::json_schema(generator).into_object(); 46 | schema.metadata().title = Some(T::schema_name()); 47 | schema.into() 48 | } 49 | 50 | #[test] 51 | fn test_generation() { 52 | let mut type_space = TypeSpace::new( 53 | TypeSpaceSettings::default() 54 | .with_derive("JsonSchema".to_string()) 55 | .with_type_mod("types") 56 | .with_struct_builder(true) 57 | .with_patch( 58 | "AllTheTraits", 59 | TypeSpacePatch::default() 60 | .with_derive("Hash") 61 | .with_derive("Ord") 62 | .with_derive("PartialOrd") 63 | .with_derive("Eq") 64 | .with_derive("PartialEq"), 65 | ), 66 | ); 67 | 68 | let mut generator = SchemaGenerator::default(); 69 | let body_schema = add_type::(&mut generator); 70 | let string_schema = add_type::(&mut generator); 71 | let opt_int_schema = add_type::>(&mut generator); 72 | let strenum_schema = add_type::(&mut generator); 73 | let pair_schema = add_type::(&mut generator); 74 | let all_the_traits = add_type::(&mut generator); 75 | 76 | type_space 77 | .add_ref_types(generator.take_definitions()) 78 | .unwrap(); 79 | 80 | let tid = type_space.add_type(&body_schema).unwrap(); 81 | let t = type_space.get_type(&tid).unwrap(); 82 | let ret = t.ident(); 83 | let body = t.parameter_ident(); 84 | 85 | let string_id = type_space.add_type(&string_schema).unwrap(); 86 | let string = type_space.get_type(&string_id).unwrap().parameter_ident(); 87 | let opt_int_id = type_space.add_type(&opt_int_schema).unwrap(); 88 | let opt_int = type_space.get_type(&opt_int_id).unwrap().parameter_ident(); 89 | let strenum_id = type_space.add_type(&strenum_schema).unwrap(); 90 | let strenum = type_space.get_type(&strenum_id).unwrap().parameter_ident(); 91 | let _ = type_space.add_type(&pair_schema).unwrap(); 92 | let _ = type_space.add_type(&all_the_traits).unwrap(); 93 | 94 | let types = type_space.to_stream(); 95 | 96 | let file = quote! { 97 | mod types { 98 | #types 99 | } 100 | 101 | pub fn do_stuff( 102 | body: #body, 103 | string: #string, 104 | opt_int: #opt_int, 105 | strenum: #strenum, 106 | ) -> #ret { 107 | todo!() 108 | } 109 | }; 110 | 111 | let fmt = rustfmt_wrapper::rustfmt(file.to_string()).unwrap(); 112 | 113 | expectorate::assert_contents("tests/generator.out", fmt.as_str()); 114 | } 115 | -------------------------------------------------------------------------------- /typify-impl/tests/test_github.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Oxide Computer Company 2 | 3 | use std::{fs::File, io::BufReader, path::Path}; 4 | 5 | use schemars::schema::RootSchema; 6 | use typify_impl::{TypeSpace, TypeSpaceImpl, TypeSpaceSettings}; 7 | 8 | #[test] 9 | fn test_github() { 10 | let mut type_space = TypeSpace::default(); 11 | 12 | let path = Path::new("tests/github.json"); 13 | let file = File::open(path).unwrap(); 14 | let reader = BufReader::new(file); 15 | 16 | let mut schema: RootSchema = serde_json::from_reader(reader).unwrap(); 17 | schema.schema.metadata().title = Some("Everything".to_string()); 18 | 19 | type_space.add_root_schema(schema).unwrap(); 20 | 21 | let file = type_space.to_stream(); 22 | 23 | let fmt = rustfmt_wrapper::rustfmt(file.to_string()).unwrap(); 24 | 25 | expectorate::assert_contents("tests/github.out", fmt.as_str()); 26 | } 27 | 28 | #[test] 29 | fn test_vega() { 30 | env_logger::init(); 31 | let mut settings = TypeSpaceSettings::default(); 32 | let raw_schema = serde_json::json!( 33 | { 34 | "enum": [ 35 | null, 36 | "normal", 37 | "bold", 38 | "lighter", 39 | "bolder", 40 | "100", 41 | "200", 42 | "300", 43 | "400", 44 | "500", 45 | "600", 46 | "700", 47 | "800", 48 | "900", 49 | 100, 50 | 200, 51 | 300, 52 | 400, 53 | 500, 54 | 600, 55 | 700, 56 | 800, 57 | 900 58 | ] 59 | } 60 | ); 61 | let schema = serde_json::from_value(raw_schema).unwrap(); 62 | settings 63 | .with_conversion(schema, "MyEnum", [TypeSpaceImpl::FromStr].into_iter()) 64 | // TODO ColorValue has resulted in an extremely expensive type; to 65 | // resolve this we need to do a better job of canonicalizing the schema 66 | // once rather than repeatedly doing quadratic expansions over it. 67 | // Alternatively we can memoize conversions rather than repeating them. 68 | .with_replacement("ColorValue", "ColorValue", [].into_iter()); 69 | 70 | let mut type_space = TypeSpace::new(&settings); 71 | 72 | let path = Path::new("tests/vega.json"); 73 | let file = File::open(path).unwrap(); 74 | let reader = BufReader::new(file); 75 | 76 | let mut schema: RootSchema = serde_json::from_reader(reader).unwrap(); 77 | schema.schema.metadata().title = Some("Everything".to_string()); 78 | 79 | type_space.add_root_schema(schema).unwrap(); 80 | 81 | let file = type_space.to_stream(); 82 | 83 | let fmt = rustfmt_wrapper::rustfmt(file.to_string()).unwrap(); 84 | 85 | expectorate::assert_contents("tests/vega.out", fmt.as_str()); 86 | } 87 | -------------------------------------------------------------------------------- /typify-macro/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "typify-macro" 3 | version = "0.4.2" 4 | edition = "2021" 5 | license = "Apache-2.0" 6 | description = "typify macro implementation" 7 | repository = "https://github.com/oxidecomputer/typify" 8 | readme = "../README.md" 9 | 10 | [lib] 11 | proc-macro = true 12 | 13 | [dependencies] 14 | proc-macro2 = "1.0.95" 15 | quote = "1.0.40" 16 | schemars = "0.8.22" 17 | semver = { version = "1.0.26", features = ["serde"] } 18 | serde = "1.0.219" 19 | serde_json = "1.0.140" 20 | serde_tokenstream = "0.2.2" 21 | syn = { version = "2.0", features = ["full", "extra-traits"] } 22 | typify-impl = { version = "0.4.2", path = "../typify-impl" } 23 | -------------------------------------------------------------------------------- /typify-macro/release.toml: -------------------------------------------------------------------------------- 1 | pre-release-replacements = [] 2 | -------------------------------------------------------------------------------- /typify-macro/src/token_utils.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Oxide Computer Company 2 | 3 | use std::collections::HashSet; 4 | 5 | use quote::ToTokens; 6 | use syn::{ 7 | parse::Parse, 8 | punctuated::Punctuated, 9 | token::{Colon, Plus}, 10 | Ident, Path, Token, TraitBoundModifier, 11 | }; 12 | use typify_impl::TypeSpaceImpl; 13 | 14 | #[derive(Debug)] 15 | pub struct TypeAndImpls { 16 | pub type_name: Path, 17 | pub colon_token: Option, 18 | pub impls: Punctuated, 19 | } 20 | 21 | impl TypeAndImpls { 22 | pub(crate) fn into_name_and_impls(self) -> (String, impl Iterator) { 23 | // If there are no traits specified, these are assumed to be 24 | // implemented. A user would use the `?FromStr` syntax to remove one of 25 | // these defaults; 26 | const DEFAULT_IMPLS: [TypeSpaceImpl; 2] = [TypeSpaceImpl::FromStr, TypeSpaceImpl::Display]; 27 | 28 | let name = self.type_name.to_token_stream().to_string(); 29 | let mut impls = DEFAULT_IMPLS.into_iter().collect::>(); 30 | self.impls.into_iter().for_each( 31 | |ImplTrait { 32 | modifier, 33 | impl_name, 34 | .. 35 | }| { 36 | // TODO should this be an error rather than silently ignored? 37 | if let Some(impl_name) = impl_name { 38 | match modifier { 39 | syn::TraitBoundModifier::None => impls.insert(impl_name), 40 | syn::TraitBoundModifier::Maybe(_) => impls.remove(&impl_name), 41 | }; 42 | } 43 | }, 44 | ); 45 | (name, impls.into_iter()) 46 | } 47 | } 48 | 49 | impl Parse for TypeAndImpls { 50 | fn parse(input: syn::parse::ParseStream) -> syn::Result { 51 | let type_name: Path = input.parse()?; 52 | let colon_token: Option = input.parse()?; 53 | let mut impls = Punctuated::default(); 54 | 55 | if colon_token.is_some() { 56 | loop { 57 | let value: ImplTrait = input.parse()?; 58 | impls.push_value(value); 59 | if !input.peek(Token![+]) { 60 | break; 61 | } 62 | let punct: Token![+] = input.parse()?; 63 | impls.push_punct(punct); 64 | } 65 | } 66 | 67 | Ok(Self { 68 | type_name, 69 | colon_token, 70 | impls, 71 | }) 72 | } 73 | } 74 | 75 | impl ToTokens for TypeAndImpls { 76 | fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { 77 | self.type_name.to_tokens(tokens); 78 | self.colon_token.to_tokens(tokens); 79 | self.impls.to_tokens(tokens); 80 | } 81 | } 82 | 83 | #[derive(Debug)] 84 | pub struct ImplTrait { 85 | pub modifier: TraitBoundModifier, 86 | pub impl_ident: Ident, 87 | pub impl_name: Option, 88 | } 89 | 90 | impl Parse for ImplTrait { 91 | fn parse(input: syn::parse::ParseStream) -> syn::Result { 92 | let modifier: TraitBoundModifier = input.parse()?; 93 | let impl_ident: Ident = input.parse()?; 94 | let impl_name = impl_ident.to_string().parse().ok(); 95 | 96 | Ok(Self { 97 | modifier, 98 | impl_ident, 99 | impl_name, 100 | }) 101 | } 102 | } 103 | 104 | impl ToTokens for ImplTrait { 105 | fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { 106 | self.modifier.to_tokens(tokens); 107 | self.impl_ident.to_tokens(tokens); 108 | } 109 | } 110 | 111 | #[cfg(test)] 112 | mod tests { 113 | use super::TypeAndImpls; 114 | 115 | use quote::{quote, ToTokens}; 116 | 117 | #[test] 118 | fn test_parse_type_and_impls() { 119 | let input = quote! { my_crate::MyType }; 120 | let value = syn::parse2::(input).unwrap(); 121 | assert_eq!( 122 | value.type_name.to_token_stream().to_string(), 123 | "my_crate :: MyType", 124 | ); 125 | assert_eq!(value.impls.len(), 0); 126 | 127 | let input = quote! { my_crate::MyType: ?Display + Hash }; 128 | let value = syn::parse2::(input).unwrap(); 129 | assert_eq!( 130 | value.type_name.to_token_stream().to_string(), 131 | "my_crate :: MyType", 132 | ); 133 | assert_eq!(value.impls.len(), 2); 134 | let mut ii = value.impls.into_iter(); 135 | assert_eq!( 136 | ii.next().unwrap().to_token_stream().to_string(), 137 | "? Display", 138 | ); 139 | assert_eq!(ii.next().unwrap().to_token_stream().to_string(), "Hash",); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /typify-test/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "typify-test" 3 | version = "0.0.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | regress = { workspace = true } 8 | serde = { workspace = true } 9 | serde_json = { workspace = true } 10 | 11 | [build-dependencies] 12 | typify = { path = "../typify" } 13 | 14 | ipnetwork = { workspace = true } 15 | prettyplease = { workspace = true } 16 | schemars = { workspace = true } 17 | serde = { workspace = true } 18 | syn = { workspace = true } 19 | -------------------------------------------------------------------------------- /typify-test/build.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Oxide Computer Company 2 | 3 | use std::collections::{HashMap, HashSet}; 4 | use std::{env, fs, path::Path}; 5 | 6 | use schemars::schema::Schema; 7 | use schemars::JsonSchema; 8 | use serde::Serialize; 9 | use typify::{TypeSpace, TypeSpaceSettings}; 10 | 11 | #[allow(dead_code)] 12 | #[derive(JsonSchema)] 13 | struct TestStruct { 14 | a: u32, 15 | b: u32, 16 | #[schemars(default = "nope")] 17 | c: bool, 18 | #[schemars(default = "answer")] 19 | d: i32, 20 | #[schemars(default = "things")] 21 | e: Things, 22 | #[schemars(default = "yes_yes")] 23 | f: Option, 24 | } 25 | 26 | fn nope() -> bool { 27 | true 28 | } 29 | 30 | fn answer() -> i32 { 31 | 42 32 | } 33 | 34 | fn yes_yes() -> Option { 35 | Some(true) 36 | } 37 | 38 | #[allow(dead_code)] 39 | #[derive(JsonSchema, Serialize)] 40 | struct Things { 41 | aa: u32, 42 | bb: String, 43 | } 44 | 45 | fn things() -> Things { 46 | Things { 47 | aa: 42, 48 | bb: "forty-two".to_string(), 49 | } 50 | } 51 | 52 | #[allow(dead_code)] 53 | #[derive(JsonSchema)] 54 | struct WithSet { 55 | set: HashSet, 56 | } 57 | 58 | #[allow(dead_code)] 59 | #[derive(JsonSchema)] 60 | struct WithMap { 61 | map: HashMap, 62 | } 63 | 64 | struct LoginName; 65 | impl JsonSchema for LoginName { 66 | fn schema_name() -> String { 67 | "LoginName".to_string() 68 | } 69 | 70 | fn json_schema(_: &mut schemars::gen::SchemaGenerator) -> Schema { 71 | schemars::schema::SchemaObject { 72 | string: Some(Box::new(schemars::schema::StringValidation { 73 | max_length: Some(8), 74 | min_length: Some(1), 75 | pattern: Some("^[a-z]*$".to_string()), 76 | })), 77 | ..Default::default() 78 | } 79 | .into() 80 | } 81 | } 82 | 83 | struct NonAsciiChars; 84 | impl JsonSchema for NonAsciiChars { 85 | fn schema_name() -> String { 86 | "NonAsciiChars".to_string() 87 | } 88 | 89 | fn json_schema(_: &mut schemars::gen::SchemaGenerator) -> Schema { 90 | schemars::schema::SchemaObject { 91 | string: Some(Box::new(schemars::schema::StringValidation { 92 | max_length: Some(8), 93 | min_length: Some(2), 94 | pattern: None, 95 | })), 96 | ..Default::default() 97 | } 98 | .into() 99 | } 100 | } 101 | 102 | struct Pancakes; 103 | impl JsonSchema for Pancakes { 104 | fn schema_name() -> String { 105 | "Pancakes".to_string() 106 | } 107 | 108 | fn json_schema(_: &mut schemars::gen::SchemaGenerator) -> Schema { 109 | schemars::schema::SchemaObject { 110 | instance_type: Some(schemars::schema::InstanceType::String.into()), 111 | format: Some("pancakes".to_string()), 112 | ..Default::default() 113 | } 114 | .into() 115 | } 116 | 117 | fn is_referenceable() -> bool { 118 | false 119 | } 120 | } 121 | 122 | #[derive(JsonSchema)] 123 | struct UnknownFormat { 124 | #[allow(dead_code)] 125 | pancakes: Pancakes, 126 | } 127 | 128 | fn main() { 129 | let mut type_space = TypeSpace::default(); 130 | 131 | WithSet::add(&mut type_space); 132 | LoginName::add(&mut type_space); 133 | NonAsciiChars::add(&mut type_space); 134 | UnknownFormat::add(&mut type_space); 135 | ipnetwork::IpNetwork::add(&mut type_space); 136 | 137 | let contents = 138 | prettyplease::unparse(&syn::parse2::(type_space.to_stream()).unwrap()); 139 | 140 | let mut out_file = Path::new(&env::var("OUT_DIR").unwrap()).to_path_buf(); 141 | out_file.push("codegen.rs"); 142 | fs::write(out_file, contents).unwrap(); 143 | 144 | // Generate with HashMap 145 | let mut type_space = TypeSpace::new(&TypeSpaceSettings::default()); 146 | 147 | WithMap::add(&mut type_space); 148 | 149 | let contents = 150 | prettyplease::unparse(&syn::parse2::(type_space.to_stream()).unwrap()); 151 | 152 | let mut out_file = Path::new(&env::var("OUT_DIR").unwrap()).to_path_buf(); 153 | out_file.push("codegen_hashmap.rs"); 154 | fs::write(out_file, contents).unwrap(); 155 | 156 | // Generate with a custom map type to validate requirements. 157 | let mut settings = TypeSpaceSettings::default(); 158 | settings.with_map_type("CustomMap"); 159 | let mut type_space = TypeSpace::new(&settings); 160 | 161 | WithMap::add(&mut type_space); 162 | 163 | let contents = 164 | prettyplease::unparse(&syn::parse2::(type_space.to_stream()).unwrap()); 165 | 166 | let mut out_file = Path::new(&env::var("OUT_DIR").unwrap()).to_path_buf(); 167 | out_file.push("codegen_custommap.rs"); 168 | fs::write(out_file, contents).unwrap(); 169 | } 170 | 171 | trait AddType { 172 | fn add(type_space: &mut TypeSpace); 173 | } 174 | 175 | impl AddType for T 176 | where 177 | T: JsonSchema, 178 | { 179 | fn add(type_space: &mut TypeSpace) { 180 | let schema = schemars::schema_for!(T); 181 | let _ = type_space.add_root_schema(schema).unwrap(); 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /typify-test/release.toml: -------------------------------------------------------------------------------- 1 | release = false 2 | -------------------------------------------------------------------------------- /typify-test/src/main.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Oxide Computer Company 2 | 3 | // Include the generated code to make sure it compiles. 4 | include!(concat!(env!("OUT_DIR"), "/codegen.rs")); 5 | 6 | fn main() {} 7 | 8 | #[test] 9 | fn test_with_set() { 10 | // Validate that a set is currently represented as a Vec 11 | // See type_entry.rs 12 | let _ = WithSet { set: Vec::new() }; 13 | } 14 | 15 | #[test] 16 | fn test_ipnetwork() { 17 | // ipnetwork::IpNetwork is a moderately complex type for us to handle. In 18 | // particular it's a oneOf with both variants as strings, but with mutually 19 | // incompatible patterns. This tests that our generated Deserialize impl 20 | // does the right thing. 21 | assert!(Ipv4Network::try_from("192.168.0.0/24").is_ok()); 22 | assert!(Ipv6Network::try_from("192.168.0.0/24").is_err()); 23 | assert!(Ipv6Network::try_from("fc00::/7").is_ok()); 24 | assert!(Ipv4Network::try_from("fc00::/7").is_err()); 25 | 26 | let v4: IpNetwork = serde_json::from_str(r#""192.168.0.0/24""#).unwrap(); 27 | assert!(matches!(v4, IpNetwork::V4(_))); 28 | let v6: IpNetwork = serde_json::from_str(r#""fc00::/7""#).unwrap(); 29 | assert!(matches!(v6, IpNetwork::V6(_))); 30 | 31 | let v4 = IpNetwork::try_from("192.168.0.0/24").unwrap(); 32 | assert!(matches!(v4, IpNetwork::V4(_))); 33 | let v6 = IpNetwork::try_from("fc00::/7").unwrap(); 34 | assert!(matches!(v6, IpNetwork::V6(_))); 35 | } 36 | 37 | #[test] 38 | fn test_string_constraints() { 39 | assert!(LoginName::try_from("").is_err()); 40 | assert!(LoginName::try_from("abcdefghi").is_err()); 41 | assert!(LoginName::try_from("offby1").is_err()); 42 | assert!(LoginName::try_from("ahl").is_ok()); 43 | } 44 | 45 | #[test] 46 | fn test_string_constraints_for_non_ascii_chars() { 47 | assert!(NonAsciiChars::try_from("🍔🍔🍔🍔🍔🍔🍔🍔").is_ok()); 48 | assert!(NonAsciiChars::try_from("🍔").is_err()); 49 | } 50 | 51 | #[test] 52 | fn test_unknown_format() { 53 | // An unknown format string should just render as a string. 54 | let _ = UnknownFormat { 55 | pancakes: String::new(), 56 | }; 57 | } 58 | 59 | mod hashmap { 60 | include!(concat!(env!("OUT_DIR"), "/codegen_hashmap.rs")); 61 | 62 | #[test] 63 | fn test_with_map() { 64 | // Validate that a map is currently represented as a HashMap by default. 65 | let _ = WithMap { 66 | map: std::collections::HashMap::new(), 67 | }; 68 | } 69 | } 70 | 71 | mod custom_map { 72 | #[allow(private_interfaces)] 73 | #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] 74 | pub struct CustomMap { 75 | key: K, 76 | value: V, 77 | } 78 | 79 | include!(concat!(env!("OUT_DIR"), "/codegen_custommap.rs")); 80 | 81 | #[test] 82 | fn test_with_map() { 83 | // Validate that a map is represented as an CustomMap when requested. 84 | let _ = WithMap { 85 | map: CustomMap { 86 | key: String::new(), 87 | value: String::new(), 88 | }, 89 | }; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /typify/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "typify" 3 | version = "0.4.2" 4 | edition = "2021" 5 | license = "Apache-2.0" 6 | description = "JSON schema to rust type code generator" 7 | repository = "https://github.com/oxidecomputer/typify" 8 | readme = "../README.md" 9 | keywords = ["json", "schema", "proc_macro"] 10 | categories = ["api-bindings", "compilers"] 11 | 12 | [features] 13 | default = ["macro"] 14 | macro = ["typify-macro"] 15 | 16 | [dependencies] 17 | typify-macro = { workspace = true, optional = true } 18 | typify-impl = { workspace = true } 19 | 20 | [dev-dependencies] 21 | chrono = { workspace = true } 22 | env_logger = { workspace = true } 23 | expectorate = { workspace = true } 24 | glob = { workspace = true } 25 | quote = { workspace = true } 26 | regress = { workspace = true } 27 | rustfmt-wrapper = { workspace = true } 28 | schemars = { workspace = true } 29 | serde = { workspace = true } 30 | serde_json = { workspace = true } 31 | trybuild = { workspace = true } 32 | uuid = { workspace = true, features = ["serde"] } 33 | -------------------------------------------------------------------------------- /typify/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Oxide Computer Company 2 | 3 | //! # Typify 4 | //! 5 | //! Typify lets you convert JSON Schema documents into Rust types. It can be 6 | //! used via a macro [`import_types!`] or a `build.rs` file. 7 | //! 8 | //! A typical use looks like this: 9 | //! ``` 10 | //! # use typify_macro::import_types; 11 | //! import_types!("../example.json"); 12 | //! ``` 13 | //! 14 | //! This expands to type definitions corresponding to the types from the file 15 | //! `example.json`. The types are `pub` and have a number of useful associated 16 | //! `impl`s including [Debug], [Clone], 17 | //! [Serialize](https://docs.rs/serde/latest/serde/trait.Serialize.html), and 18 | //! [Deserialize](https://docs.rs/serde/latest/serde/trait.Deserialize.html). 19 | //! 20 | //! Alternatively, you may use the expanded form: 21 | //! ``` 22 | //! # use typify_macro::import_types; 23 | //! import_types!(schema = "../example.json"); 24 | //! ``` 25 | //! 26 | //! If you want to add additional derives for the generated types, you can 27 | //! specify them with the `derives` property of the expanded form: 28 | //! ``` 29 | //! # use typify_macro::import_types; 30 | //! import_types!( 31 | //! schema = "../example.json", 32 | //! derives = [schemars::JsonSchema], 33 | //! ); 34 | //! ``` 35 | //! 36 | //! Generated structs can optionally include a builder-style interface: 37 | //! ``` 38 | //! # mod x { 39 | //! # use typify_macro::import_types; 40 | //! import_types!( 41 | //! schema = "../example.json", 42 | //! struct_builder = true, 43 | //! ); 44 | //! # } 45 | //! ``` 46 | //! 47 | //! With this set, consumers can construct a struct `Veggie` as follows: 48 | //! ``` 49 | //! # mod x { 50 | //! # use typify_macro::import_types; 51 | //! # import_types!( 52 | //! # schema = "../example.json", 53 | //! # struct_builder = true, 54 | //! # ); 55 | //! # fn _x() { 56 | //! let veggie: Veggie = Veggie::builder() 57 | //! .veggie_name("radish") 58 | //! .veggie_like(true) 59 | //! .try_into() 60 | //! .unwrap(); 61 | //! # } 62 | //! # } 63 | //! ``` 64 | //! 65 | //! # Altering Conversion 66 | //! 67 | //! ## Renames and additional derivations 68 | //! 69 | //! You can specify renames types or add additional derive macros for generated 70 | //! types using the `patch` syntax: 71 | //! ``` 72 | //! # use typify_macro::import_types; 73 | //! import_types!( 74 | //! schema = "../example.json", 75 | //! patch = { 76 | //! Veggie = { 77 | //! rename = "Vegetable", 78 | //! derives = [ schemars::JsonSchema ], 79 | //! } 80 | //! } 81 | //! ); 82 | //! ``` 83 | //! 84 | //! ## Replacement types 85 | //! 86 | //! You can replace a generated type with an existing type by specifying an 87 | //! association between the name of a type with the type to use in its place: 88 | //! ``` 89 | //! # mod my_fancy_networking_crate { 90 | //! # #[derive(serde::Deserialize)] 91 | //! # pub struct Ipv6Cidr(String); 92 | //! # } 93 | //! # use typify_macro::import_types; 94 | //! import_types!( 95 | //! schema = "../example.json", 96 | //! replace = { 97 | //! Ipv6Cidr = my_fancy_networking_crate::Ipv6Cidr, 98 | //! } 99 | //! ); 100 | //! ``` 101 | //! 102 | //! ## Conversion overrides 103 | //! 104 | //! You can override a conversion for a particular JSON schema construct by 105 | //! specifying an association between the schema and the type. 106 | //! ``` 107 | //! # mod my_fancy_uuid_crate { 108 | //! # #[derive(serde::Deserialize)] 109 | //! # pub struct MyUuid(String); 110 | //! # } 111 | //! # use typify_macro::import_types; 112 | //! import_types!( 113 | //! schema = "../example.json", 114 | //! convert = { 115 | //! { 116 | //! type = "string", 117 | //! format = "uuid", 118 | //! } = my_fancy_uuid_crate::MyUuid, 119 | //! } 120 | //! ); 121 | //! ``` 122 | //! 123 | //! # Macro vs. `build.rs` 124 | //! 125 | //! While using the [`import_types!`] macro is quite a bit simpler, you can 126 | //! also construct output in a `build.rs` script. Doing so requires a little 127 | //! more work to process the JSON Schema document and write out the file to 128 | //! your intended location. The biggest benefit is that the generated type 129 | //! definitions are significantly easier to inspect. The macro-generated types 130 | //! can be viewed with `cargo expand` and they (like `build.rs`-derived types) 131 | //! have generated documentation, but if you find that you'd like to see the 132 | //! actual code generated you may prefer a `build.rs`. 133 | //! 134 | //! ## Builder interface 135 | //! 136 | //! Typify exports a [TypeSpace] interface that is intended for programmatic 137 | //! construction of types. This can be for something simple like a `build.rs` 138 | //! script or something more complex like a generator whose input includes JSON 139 | //! schema type definitions. 140 | //! 141 | //! # Mapping JSON Schema to Rust 142 | //! 143 | //! JSON Schema allows for extreme flexibility. As such, there are some schemas 144 | //! that Typify isn't able to interpret (please file an issue!). In general, 145 | //! though, Typify does a pretty job of mapping JSON Schema types to Rust. For 146 | //! more information, see the project's 147 | //! [README.md](https://github.com/oxidecomputer/typify). 148 | 149 | #![deny(missing_docs)] 150 | 151 | pub use typify_impl::CrateVers; 152 | pub use typify_impl::Error; 153 | pub use typify_impl::Type; 154 | pub use typify_impl::TypeDetails; 155 | pub use typify_impl::TypeEnum; 156 | pub use typify_impl::TypeEnumVariant; 157 | pub use typify_impl::TypeId; 158 | pub use typify_impl::TypeNewtype; 159 | pub use typify_impl::TypeSpace; 160 | pub use typify_impl::TypeSpaceImpl; 161 | pub use typify_impl::TypeSpacePatch; 162 | pub use typify_impl::TypeSpaceSettings; 163 | pub use typify_impl::TypeStruct; 164 | pub use typify_impl::TypeStructPropInfo; 165 | pub use typify_impl::UnknownPolicy; 166 | #[cfg(feature = "macro")] 167 | pub use typify_macro::import_types; 168 | -------------------------------------------------------------------------------- /typify/tests/schemas.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Oxide Computer Company 2 | 3 | use std::{error::Error, fs::File, io::BufReader}; 4 | 5 | use expectorate::assert_contents; 6 | use glob::glob; 7 | use quote::quote; 8 | use schemars::schema::RootSchema; 9 | use serde_json::json; 10 | use typify::{TypeSpace, TypeSpacePatch, TypeSpaceSettings}; 11 | use typify_impl::TypeSpaceImpl; 12 | 13 | #[test] 14 | fn test_schemas() { 15 | env_logger::init(); 16 | // Make sure output is up to date. 17 | for entry in glob("tests/schemas/*.json").expect("Failed to read glob pattern") { 18 | let entry = entry.unwrap(); 19 | let out_path = entry.clone().with_extension("rs"); 20 | validate_schema(entry, out_path, &mut TypeSpaceSettings::default()).unwrap(); 21 | } 22 | 23 | // Make sure it all compiles. 24 | trybuild::TestCases::new().pass("tests/schemas/*.rs"); 25 | } 26 | 27 | /// Ensure that setting the global config to use a custom map type works. 28 | #[test] 29 | fn test_custom_map() { 30 | validate_schema( 31 | "tests/schemas/maps.json".into(), 32 | "tests/schemas/maps_custom.rs".into(), 33 | TypeSpaceSettings::default().with_map_type("std::collections::BTreeMap"), 34 | ) 35 | .unwrap(); 36 | 37 | trybuild::TestCases::new().pass("tests/schemas/maps_custom.rs"); 38 | } 39 | 40 | fn validate_schema( 41 | path: std::path::PathBuf, 42 | out_path: std::path::PathBuf, 43 | typespace: &mut TypeSpaceSettings, 44 | ) -> Result<(), Box> { 45 | let file = File::open(path)?; 46 | let reader = BufReader::new(file); 47 | 48 | // Read the JSON contents of the file as an instance of `User`. 49 | let root_schema: RootSchema = serde_json::from_reader(reader)?; 50 | 51 | let schema_raw = json!( 52 | { 53 | "enum": [ 1, "one" ] 54 | } 55 | ); 56 | let schema = serde_json::from_value(schema_raw).unwrap(); 57 | 58 | let mut type_space = TypeSpace::new( 59 | typespace 60 | .with_replacement( 61 | "HandGeneratedType", 62 | "String", 63 | [TypeSpaceImpl::Display].into_iter(), 64 | ) 65 | .with_patch( 66 | "TypeThatNeedsMoreDerives", 67 | TypeSpacePatch::default() 68 | .with_rename("TypeThatHasMoreDerives") 69 | .with_derive("Eq") 70 | .with_derive("PartialEq"), 71 | ) 72 | .with_conversion( 73 | schema, 74 | "serde_json::Value", 75 | [TypeSpaceImpl::Display].into_iter(), 76 | ) 77 | // Our test use of the x-rust-type extension only refers to things 78 | // in std. 79 | .with_crate( 80 | "std", 81 | typify::CrateVers::Version("1.0.0".parse().unwrap()), 82 | None, 83 | ) 84 | .with_struct_builder(true), 85 | ); 86 | type_space.add_root_schema(root_schema)?; 87 | 88 | // Make a file with the generated code. 89 | let code = quote! { 90 | #![deny(warnings)] 91 | 92 | #type_space 93 | 94 | fn main() {} 95 | }; 96 | let text = rustfmt_wrapper::rustfmt(code)?; 97 | assert_contents(out_path, &text); 98 | 99 | Ok(()) 100 | } 101 | -------------------------------------------------------------------------------- /typify/tests/schemas/arrays-and-tuples.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "definitions": { 4 | "simple-two-tuple": { 5 | "type": "array", 6 | "minItems": 2, 7 | "maxItems": 2, 8 | "items": [ 9 | { 10 | "type": "string" 11 | }, 12 | { 13 | "type": "string" 14 | } 15 | ] 16 | }, 17 | "simple-two-array": { 18 | "type": "array", 19 | "minItems": 2, 20 | "maxItems": 2, 21 | "items": { 22 | "type": "string" 23 | } 24 | }, 25 | "less-simple-two-tuple": { 26 | "type": "array", 27 | "minItems": 2, 28 | "maxItems": 2, 29 | "items": [ 30 | { 31 | "type": "string" 32 | }, 33 | { 34 | "type": "string" 35 | }, 36 | { 37 | "type": "string" 38 | } 39 | ] 40 | }, 41 | "unsimple-two-tuple": { 42 | "type": "array", 43 | "minItems": 2, 44 | "maxItems": 2, 45 | "items": [ 46 | { 47 | "type": "string" 48 | } 49 | ], 50 | "additionalItems": { 51 | "type": "string" 52 | } 53 | }, 54 | "yolo-two-array": { 55 | "type": "array", 56 | "minItems": 2, 57 | "maxItems": 2, 58 | "additionalItems": { 59 | "$comment": "ignored", 60 | "type": "string" 61 | } 62 | }, 63 | "array-sans-items": { 64 | "type": "array", 65 | "minItems": 1, 66 | "uniqueItems": true 67 | } 68 | } 69 | } -------------------------------------------------------------------------------- /typify/tests/schemas/arrays-and-tuples.rs: -------------------------------------------------------------------------------- 1 | #![deny(warnings)] 2 | #[doc = r" Error types."] 3 | pub mod error { 4 | #[doc = r" Error from a `TryFrom` or `FromStr` implementation."] 5 | pub struct ConversionError(::std::borrow::Cow<'static, str>); 6 | impl ::std::error::Error for ConversionError {} 7 | impl ::std::fmt::Display for ConversionError { 8 | fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> Result<(), ::std::fmt::Error> { 9 | ::std::fmt::Display::fmt(&self.0, f) 10 | } 11 | } 12 | impl ::std::fmt::Debug for ConversionError { 13 | fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> Result<(), ::std::fmt::Error> { 14 | ::std::fmt::Debug::fmt(&self.0, f) 15 | } 16 | } 17 | impl From<&'static str> for ConversionError { 18 | fn from(value: &'static str) -> Self { 19 | Self(value.into()) 20 | } 21 | } 22 | impl From for ConversionError { 23 | fn from(value: String) -> Self { 24 | Self(value.into()) 25 | } 26 | } 27 | } 28 | #[doc = "`ArraySansItems`"] 29 | #[doc = r""] 30 | #[doc = r"
JSON schema"] 31 | #[doc = r""] 32 | #[doc = r" ```json"] 33 | #[doc = "{"] 34 | #[doc = " \"type\": \"array\","] 35 | #[doc = " \"minItems\": 1,"] 36 | #[doc = " \"uniqueItems\": true"] 37 | #[doc = "}"] 38 | #[doc = r" ```"] 39 | #[doc = r"
"] 40 | #[derive(:: serde :: Deserialize, :: serde :: Serialize, Clone, Debug)] 41 | #[serde(transparent)] 42 | pub struct ArraySansItems(pub Vec<::serde_json::Value>); 43 | impl ::std::ops::Deref for ArraySansItems { 44 | type Target = Vec<::serde_json::Value>; 45 | fn deref(&self) -> &Vec<::serde_json::Value> { 46 | &self.0 47 | } 48 | } 49 | impl ::std::convert::From for Vec<::serde_json::Value> { 50 | fn from(value: ArraySansItems) -> Self { 51 | value.0 52 | } 53 | } 54 | impl ::std::convert::From<&ArraySansItems> for ArraySansItems { 55 | fn from(value: &ArraySansItems) -> Self { 56 | value.clone() 57 | } 58 | } 59 | impl ::std::convert::From> for ArraySansItems { 60 | fn from(value: Vec<::serde_json::Value>) -> Self { 61 | Self(value) 62 | } 63 | } 64 | #[doc = "`LessSimpleTwoTuple`"] 65 | #[doc = r""] 66 | #[doc = r"
JSON schema"] 67 | #[doc = r""] 68 | #[doc = r" ```json"] 69 | #[doc = "{"] 70 | #[doc = " \"type\": \"array\","] 71 | #[doc = " \"items\": ["] 72 | #[doc = " {"] 73 | #[doc = " \"type\": \"string\""] 74 | #[doc = " },"] 75 | #[doc = " {"] 76 | #[doc = " \"type\": \"string\""] 77 | #[doc = " },"] 78 | #[doc = " {"] 79 | #[doc = " \"type\": \"string\""] 80 | #[doc = " }"] 81 | #[doc = " ],"] 82 | #[doc = " \"maxItems\": 2,"] 83 | #[doc = " \"minItems\": 2"] 84 | #[doc = "}"] 85 | #[doc = r" ```"] 86 | #[doc = r"
"] 87 | #[derive(:: serde :: Deserialize, :: serde :: Serialize, Clone, Debug)] 88 | #[serde(transparent)] 89 | pub struct LessSimpleTwoTuple(pub (::std::string::String, ::std::string::String)); 90 | impl ::std::ops::Deref for LessSimpleTwoTuple { 91 | type Target = (::std::string::String, ::std::string::String); 92 | fn deref(&self) -> &(::std::string::String, ::std::string::String) { 93 | &self.0 94 | } 95 | } 96 | impl ::std::convert::From for (::std::string::String, ::std::string::String) { 97 | fn from(value: LessSimpleTwoTuple) -> Self { 98 | value.0 99 | } 100 | } 101 | impl ::std::convert::From<&LessSimpleTwoTuple> for LessSimpleTwoTuple { 102 | fn from(value: &LessSimpleTwoTuple) -> Self { 103 | value.clone() 104 | } 105 | } 106 | impl ::std::convert::From<(::std::string::String, ::std::string::String)> for LessSimpleTwoTuple { 107 | fn from(value: (::std::string::String, ::std::string::String)) -> Self { 108 | Self(value) 109 | } 110 | } 111 | #[doc = "`SimpleTwoArray`"] 112 | #[doc = r""] 113 | #[doc = r"
JSON schema"] 114 | #[doc = r""] 115 | #[doc = r" ```json"] 116 | #[doc = "{"] 117 | #[doc = " \"type\": \"array\","] 118 | #[doc = " \"items\": {"] 119 | #[doc = " \"type\": \"string\""] 120 | #[doc = " },"] 121 | #[doc = " \"maxItems\": 2,"] 122 | #[doc = " \"minItems\": 2"] 123 | #[doc = "}"] 124 | #[doc = r" ```"] 125 | #[doc = r"
"] 126 | #[derive(:: serde :: Deserialize, :: serde :: Serialize, Clone, Debug)] 127 | #[serde(transparent)] 128 | pub struct SimpleTwoArray(pub [::std::string::String; 2usize]); 129 | impl ::std::ops::Deref for SimpleTwoArray { 130 | type Target = [::std::string::String; 2usize]; 131 | fn deref(&self) -> &[::std::string::String; 2usize] { 132 | &self.0 133 | } 134 | } 135 | impl ::std::convert::From for [::std::string::String; 2usize] { 136 | fn from(value: SimpleTwoArray) -> Self { 137 | value.0 138 | } 139 | } 140 | impl ::std::convert::From<&SimpleTwoArray> for SimpleTwoArray { 141 | fn from(value: &SimpleTwoArray) -> Self { 142 | value.clone() 143 | } 144 | } 145 | impl ::std::convert::From<[::std::string::String; 2usize]> for SimpleTwoArray { 146 | fn from(value: [::std::string::String; 2usize]) -> Self { 147 | Self(value) 148 | } 149 | } 150 | #[doc = "`SimpleTwoTuple`"] 151 | #[doc = r""] 152 | #[doc = r"
JSON schema"] 153 | #[doc = r""] 154 | #[doc = r" ```json"] 155 | #[doc = "{"] 156 | #[doc = " \"type\": \"array\","] 157 | #[doc = " \"items\": ["] 158 | #[doc = " {"] 159 | #[doc = " \"type\": \"string\""] 160 | #[doc = " },"] 161 | #[doc = " {"] 162 | #[doc = " \"type\": \"string\""] 163 | #[doc = " }"] 164 | #[doc = " ],"] 165 | #[doc = " \"maxItems\": 2,"] 166 | #[doc = " \"minItems\": 2"] 167 | #[doc = "}"] 168 | #[doc = r" ```"] 169 | #[doc = r"
"] 170 | #[derive(:: serde :: Deserialize, :: serde :: Serialize, Clone, Debug)] 171 | #[serde(transparent)] 172 | pub struct SimpleTwoTuple(pub (::std::string::String, ::std::string::String)); 173 | impl ::std::ops::Deref for SimpleTwoTuple { 174 | type Target = (::std::string::String, ::std::string::String); 175 | fn deref(&self) -> &(::std::string::String, ::std::string::String) { 176 | &self.0 177 | } 178 | } 179 | impl ::std::convert::From for (::std::string::String, ::std::string::String) { 180 | fn from(value: SimpleTwoTuple) -> Self { 181 | value.0 182 | } 183 | } 184 | impl ::std::convert::From<&SimpleTwoTuple> for SimpleTwoTuple { 185 | fn from(value: &SimpleTwoTuple) -> Self { 186 | value.clone() 187 | } 188 | } 189 | impl ::std::convert::From<(::std::string::String, ::std::string::String)> for SimpleTwoTuple { 190 | fn from(value: (::std::string::String, ::std::string::String)) -> Self { 191 | Self(value) 192 | } 193 | } 194 | #[doc = "`UnsimpleTwoTuple`"] 195 | #[doc = r""] 196 | #[doc = r"
JSON schema"] 197 | #[doc = r""] 198 | #[doc = r" ```json"] 199 | #[doc = "{"] 200 | #[doc = " \"type\": \"array\","] 201 | #[doc = " \"items\": ["] 202 | #[doc = " {"] 203 | #[doc = " \"type\": \"string\""] 204 | #[doc = " }"] 205 | #[doc = " ],"] 206 | #[doc = " \"additionalItems\": {"] 207 | #[doc = " \"type\": \"string\""] 208 | #[doc = " },"] 209 | #[doc = " \"maxItems\": 2,"] 210 | #[doc = " \"minItems\": 2"] 211 | #[doc = "}"] 212 | #[doc = r" ```"] 213 | #[doc = r"
"] 214 | #[derive(:: serde :: Deserialize, :: serde :: Serialize, Clone, Debug)] 215 | #[serde(transparent)] 216 | pub struct UnsimpleTwoTuple(pub (::std::string::String, ::std::string::String)); 217 | impl ::std::ops::Deref for UnsimpleTwoTuple { 218 | type Target = (::std::string::String, ::std::string::String); 219 | fn deref(&self) -> &(::std::string::String, ::std::string::String) { 220 | &self.0 221 | } 222 | } 223 | impl ::std::convert::From for (::std::string::String, ::std::string::String) { 224 | fn from(value: UnsimpleTwoTuple) -> Self { 225 | value.0 226 | } 227 | } 228 | impl ::std::convert::From<&UnsimpleTwoTuple> for UnsimpleTwoTuple { 229 | fn from(value: &UnsimpleTwoTuple) -> Self { 230 | value.clone() 231 | } 232 | } 233 | impl ::std::convert::From<(::std::string::String, ::std::string::String)> for UnsimpleTwoTuple { 234 | fn from(value: (::std::string::String, ::std::string::String)) -> Self { 235 | Self(value) 236 | } 237 | } 238 | #[doc = "`YoloTwoArray`"] 239 | #[doc = r""] 240 | #[doc = r"
JSON schema"] 241 | #[doc = r""] 242 | #[doc = r" ```json"] 243 | #[doc = "{"] 244 | #[doc = " \"type\": \"array\","] 245 | #[doc = " \"additionalItems\": {"] 246 | #[doc = " \"type\": \"string\","] 247 | #[doc = " \"$comment\": \"ignored\""] 248 | #[doc = " },"] 249 | #[doc = " \"maxItems\": 2,"] 250 | #[doc = " \"minItems\": 2"] 251 | #[doc = "}"] 252 | #[doc = r" ```"] 253 | #[doc = r"
"] 254 | #[derive(:: serde :: Deserialize, :: serde :: Serialize, Clone, Debug)] 255 | #[serde(transparent)] 256 | pub struct YoloTwoArray(pub [::serde_json::Value; 2usize]); 257 | impl ::std::ops::Deref for YoloTwoArray { 258 | type Target = [::serde_json::Value; 2usize]; 259 | fn deref(&self) -> &[::serde_json::Value; 2usize] { 260 | &self.0 261 | } 262 | } 263 | impl ::std::convert::From for [::serde_json::Value; 2usize] { 264 | fn from(value: YoloTwoArray) -> Self { 265 | value.0 266 | } 267 | } 268 | impl ::std::convert::From<&YoloTwoArray> for YoloTwoArray { 269 | fn from(value: &YoloTwoArray) -> Self { 270 | value.clone() 271 | } 272 | } 273 | impl ::std::convert::From<[::serde_json::Value; 2usize]> for YoloTwoArray { 274 | fn from(value: [::serde_json::Value; 2usize]) -> Self { 275 | Self(value) 276 | } 277 | } 278 | fn main() {} 279 | -------------------------------------------------------------------------------- /typify/tests/schemas/deny-list.json: -------------------------------------------------------------------------------- 1 | { 2 | "$comment": "validate a 'not' schema with typed- and untyped-subschemas", 3 | "title": "TestType", 4 | "type": "object", 5 | "properties": { 6 | "where_not": { 7 | "not": { 8 | "enum": [ 9 | "start", 10 | "middle", 11 | "end" 12 | ] 13 | } 14 | }, 15 | "why_not": { 16 | "not": { 17 | "type": "string", 18 | "enum": [ 19 | "because" 20 | ] 21 | } 22 | } 23 | }, 24 | "required": [ 25 | "where_not", 26 | "why_not" 27 | ] 28 | } -------------------------------------------------------------------------------- /typify/tests/schemas/deny-list.rs: -------------------------------------------------------------------------------- 1 | #![deny(warnings)] 2 | #[doc = r" Error types."] 3 | pub mod error { 4 | #[doc = r" Error from a `TryFrom` or `FromStr` implementation."] 5 | pub struct ConversionError(::std::borrow::Cow<'static, str>); 6 | impl ::std::error::Error for ConversionError {} 7 | impl ::std::fmt::Display for ConversionError { 8 | fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> Result<(), ::std::fmt::Error> { 9 | ::std::fmt::Display::fmt(&self.0, f) 10 | } 11 | } 12 | impl ::std::fmt::Debug for ConversionError { 13 | fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> Result<(), ::std::fmt::Error> { 14 | ::std::fmt::Debug::fmt(&self.0, f) 15 | } 16 | } 17 | impl From<&'static str> for ConversionError { 18 | fn from(value: &'static str) -> Self { 19 | Self(value.into()) 20 | } 21 | } 22 | impl From for ConversionError { 23 | fn from(value: String) -> Self { 24 | Self(value.into()) 25 | } 26 | } 27 | } 28 | #[doc = "`TestType`"] 29 | #[doc = r""] 30 | #[doc = r"
JSON schema"] 31 | #[doc = r""] 32 | #[doc = r" ```json"] 33 | #[doc = "{"] 34 | #[doc = " \"title\": \"TestType\","] 35 | #[doc = " \"type\": \"object\","] 36 | #[doc = " \"required\": ["] 37 | #[doc = " \"where_not\","] 38 | #[doc = " \"why_not\""] 39 | #[doc = " ],"] 40 | #[doc = " \"properties\": {"] 41 | #[doc = " \"where_not\": {"] 42 | #[doc = " \"not\": {"] 43 | #[doc = " \"enum\": ["] 44 | #[doc = " \"start\","] 45 | #[doc = " \"middle\","] 46 | #[doc = " \"end\""] 47 | #[doc = " ]"] 48 | #[doc = " }"] 49 | #[doc = " },"] 50 | #[doc = " \"why_not\": {"] 51 | #[doc = " \"not\": {"] 52 | #[doc = " \"type\": \"string\","] 53 | #[doc = " \"enum\": ["] 54 | #[doc = " \"because\""] 55 | #[doc = " ]"] 56 | #[doc = " }"] 57 | #[doc = " }"] 58 | #[doc = " },"] 59 | #[doc = " \"$comment\": \"validate a 'not' schema with typed- and untyped-subschemas\""] 60 | #[doc = "}"] 61 | #[doc = r" ```"] 62 | #[doc = r"
"] 63 | #[derive(:: serde :: Deserialize, :: serde :: Serialize, Clone, Debug)] 64 | pub struct TestType { 65 | pub where_not: TestTypeWhereNot, 66 | pub why_not: TestTypeWhyNot, 67 | } 68 | impl ::std::convert::From<&TestType> for TestType { 69 | fn from(value: &TestType) -> Self { 70 | value.clone() 71 | } 72 | } 73 | impl TestType { 74 | pub fn builder() -> builder::TestType { 75 | Default::default() 76 | } 77 | } 78 | #[doc = "`TestTypeWhereNot`"] 79 | #[doc = r""] 80 | #[doc = r"
JSON schema"] 81 | #[doc = r""] 82 | #[doc = r" ```json"] 83 | #[doc = "{"] 84 | #[doc = " \"not\": {"] 85 | #[doc = " \"enum\": ["] 86 | #[doc = " \"start\","] 87 | #[doc = " \"middle\","] 88 | #[doc = " \"end\""] 89 | #[doc = " ]"] 90 | #[doc = " }"] 91 | #[doc = "}"] 92 | #[doc = r" ```"] 93 | #[doc = r"
"] 94 | #[derive(:: serde :: Serialize, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] 95 | #[serde(transparent)] 96 | pub struct TestTypeWhereNot(::std::string::String); 97 | impl ::std::ops::Deref for TestTypeWhereNot { 98 | type Target = ::std::string::String; 99 | fn deref(&self) -> &::std::string::String { 100 | &self.0 101 | } 102 | } 103 | impl ::std::convert::From for ::std::string::String { 104 | fn from(value: TestTypeWhereNot) -> Self { 105 | value.0 106 | } 107 | } 108 | impl ::std::convert::From<&TestTypeWhereNot> for TestTypeWhereNot { 109 | fn from(value: &TestTypeWhereNot) -> Self { 110 | value.clone() 111 | } 112 | } 113 | impl ::std::convert::TryFrom<::std::string::String> for TestTypeWhereNot { 114 | type Error = self::error::ConversionError; 115 | fn try_from( 116 | value: ::std::string::String, 117 | ) -> ::std::result::Result { 118 | if ["start".to_string(), "middle".to_string(), "end".to_string()].contains(&value) { 119 | Err("invalid value".into()) 120 | } else { 121 | Ok(Self(value)) 122 | } 123 | } 124 | } 125 | impl<'de> ::serde::Deserialize<'de> for TestTypeWhereNot { 126 | fn deserialize(deserializer: D) -> ::std::result::Result 127 | where 128 | D: ::serde::Deserializer<'de>, 129 | { 130 | Self::try_from(<::std::string::String>::deserialize(deserializer)?) 131 | .map_err(|e| ::custom(e.to_string())) 132 | } 133 | } 134 | #[doc = "`TestTypeWhyNot`"] 135 | #[doc = r""] 136 | #[doc = r"
JSON schema"] 137 | #[doc = r""] 138 | #[doc = r" ```json"] 139 | #[doc = "{"] 140 | #[doc = " \"not\": {"] 141 | #[doc = " \"type\": \"string\","] 142 | #[doc = " \"enum\": ["] 143 | #[doc = " \"because\""] 144 | #[doc = " ]"] 145 | #[doc = " }"] 146 | #[doc = "}"] 147 | #[doc = r" ```"] 148 | #[doc = r"
"] 149 | #[derive(:: serde :: Serialize, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] 150 | #[serde(transparent)] 151 | pub struct TestTypeWhyNot(::std::string::String); 152 | impl ::std::ops::Deref for TestTypeWhyNot { 153 | type Target = ::std::string::String; 154 | fn deref(&self) -> &::std::string::String { 155 | &self.0 156 | } 157 | } 158 | impl ::std::convert::From for ::std::string::String { 159 | fn from(value: TestTypeWhyNot) -> Self { 160 | value.0 161 | } 162 | } 163 | impl ::std::convert::From<&TestTypeWhyNot> for TestTypeWhyNot { 164 | fn from(value: &TestTypeWhyNot) -> Self { 165 | value.clone() 166 | } 167 | } 168 | impl ::std::convert::TryFrom<::std::string::String> for TestTypeWhyNot { 169 | type Error = self::error::ConversionError; 170 | fn try_from( 171 | value: ::std::string::String, 172 | ) -> ::std::result::Result { 173 | if ["because".to_string()].contains(&value) { 174 | Err("invalid value".into()) 175 | } else { 176 | Ok(Self(value)) 177 | } 178 | } 179 | } 180 | impl<'de> ::serde::Deserialize<'de> for TestTypeWhyNot { 181 | fn deserialize(deserializer: D) -> ::std::result::Result 182 | where 183 | D: ::serde::Deserializer<'de>, 184 | { 185 | Self::try_from(<::std::string::String>::deserialize(deserializer)?) 186 | .map_err(|e| ::custom(e.to_string())) 187 | } 188 | } 189 | #[doc = r" Types for composing complex structures."] 190 | pub mod builder { 191 | #[derive(Clone, Debug)] 192 | pub struct TestType { 193 | where_not: ::std::result::Result, 194 | why_not: ::std::result::Result, 195 | } 196 | impl ::std::default::Default for TestType { 197 | fn default() -> Self { 198 | Self { 199 | where_not: Err("no value supplied for where_not".to_string()), 200 | why_not: Err("no value supplied for why_not".to_string()), 201 | } 202 | } 203 | } 204 | impl TestType { 205 | pub fn where_not(mut self, value: T) -> Self 206 | where 207 | T: ::std::convert::TryInto, 208 | T::Error: ::std::fmt::Display, 209 | { 210 | self.where_not = value 211 | .try_into() 212 | .map_err(|e| format!("error converting supplied value for where_not: {}", e)); 213 | self 214 | } 215 | pub fn why_not(mut self, value: T) -> Self 216 | where 217 | T: ::std::convert::TryInto, 218 | T::Error: ::std::fmt::Display, 219 | { 220 | self.why_not = value 221 | .try_into() 222 | .map_err(|e| format!("error converting supplied value for why_not: {}", e)); 223 | self 224 | } 225 | } 226 | impl ::std::convert::TryFrom for super::TestType { 227 | type Error = super::error::ConversionError; 228 | fn try_from(value: TestType) -> ::std::result::Result { 229 | Ok(Self { 230 | where_not: value.where_not?, 231 | why_not: value.why_not?, 232 | }) 233 | } 234 | } 235 | impl ::std::convert::From for TestType { 236 | fn from(value: super::TestType) -> Self { 237 | Self { 238 | where_not: Ok(value.where_not), 239 | why_not: Ok(value.why_not), 240 | } 241 | } 242 | } 243 | } 244 | fn main() {} 245 | -------------------------------------------------------------------------------- /typify/tests/schemas/extraneous-enum.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "LetterBox", 3 | "type": "object", 4 | "properties": { 5 | "letter": { 6 | "type": "string", 7 | "enum": [ 8 | "a", 9 | "b", 10 | "cee" 11 | ], 12 | "maxLength": 2 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /typify/tests/schemas/extraneous-enum.rs: -------------------------------------------------------------------------------- 1 | #![deny(warnings)] 2 | #[doc = r" Error types."] 3 | pub mod error { 4 | #[doc = r" Error from a `TryFrom` or `FromStr` implementation."] 5 | pub struct ConversionError(::std::borrow::Cow<'static, str>); 6 | impl ::std::error::Error for ConversionError {} 7 | impl ::std::fmt::Display for ConversionError { 8 | fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> Result<(), ::std::fmt::Error> { 9 | ::std::fmt::Display::fmt(&self.0, f) 10 | } 11 | } 12 | impl ::std::fmt::Debug for ConversionError { 13 | fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> Result<(), ::std::fmt::Error> { 14 | ::std::fmt::Debug::fmt(&self.0, f) 15 | } 16 | } 17 | impl From<&'static str> for ConversionError { 18 | fn from(value: &'static str) -> Self { 19 | Self(value.into()) 20 | } 21 | } 22 | impl From for ConversionError { 23 | fn from(value: String) -> Self { 24 | Self(value.into()) 25 | } 26 | } 27 | } 28 | #[doc = "`LetterBox`"] 29 | #[doc = r""] 30 | #[doc = r"
JSON schema"] 31 | #[doc = r""] 32 | #[doc = r" ```json"] 33 | #[doc = "{"] 34 | #[doc = " \"title\": \"LetterBox\","] 35 | #[doc = " \"type\": \"object\","] 36 | #[doc = " \"properties\": {"] 37 | #[doc = " \"letter\": {"] 38 | #[doc = " \"type\": \"string\","] 39 | #[doc = " \"enum\": ["] 40 | #[doc = " \"a\","] 41 | #[doc = " \"b\","] 42 | #[doc = " \"cee\""] 43 | #[doc = " ],"] 44 | #[doc = " \"maxLength\": 2"] 45 | #[doc = " }"] 46 | #[doc = " }"] 47 | #[doc = "}"] 48 | #[doc = r" ```"] 49 | #[doc = r"
"] 50 | #[derive(:: serde :: Deserialize, :: serde :: Serialize, Clone, Debug)] 51 | pub struct LetterBox { 52 | #[serde(default, skip_serializing_if = "::std::option::Option::is_none")] 53 | pub letter: ::std::option::Option, 54 | } 55 | impl ::std::convert::From<&LetterBox> for LetterBox { 56 | fn from(value: &LetterBox) -> Self { 57 | value.clone() 58 | } 59 | } 60 | impl ::std::default::Default for LetterBox { 61 | fn default() -> Self { 62 | Self { 63 | letter: Default::default(), 64 | } 65 | } 66 | } 67 | impl LetterBox { 68 | pub fn builder() -> builder::LetterBox { 69 | Default::default() 70 | } 71 | } 72 | #[doc = "`LetterBoxLetter`"] 73 | #[doc = r""] 74 | #[doc = r"
JSON schema"] 75 | #[doc = r""] 76 | #[doc = r" ```json"] 77 | #[doc = "{"] 78 | #[doc = " \"type\": \"string\","] 79 | #[doc = " \"enum\": ["] 80 | #[doc = " \"a\","] 81 | #[doc = " \"b\","] 82 | #[doc = " \"cee\""] 83 | #[doc = " ],"] 84 | #[doc = " \"maxLength\": 2"] 85 | #[doc = "}"] 86 | #[doc = r" ```"] 87 | #[doc = r"
"] 88 | #[derive( 89 | :: serde :: Deserialize, 90 | :: serde :: Serialize, 91 | Clone, 92 | Copy, 93 | Debug, 94 | Eq, 95 | Hash, 96 | Ord, 97 | PartialEq, 98 | PartialOrd, 99 | )] 100 | pub enum LetterBoxLetter { 101 | #[serde(rename = "a")] 102 | A, 103 | #[serde(rename = "b")] 104 | B, 105 | } 106 | impl ::std::convert::From<&Self> for LetterBoxLetter { 107 | fn from(value: &LetterBoxLetter) -> Self { 108 | value.clone() 109 | } 110 | } 111 | impl ::std::fmt::Display for LetterBoxLetter { 112 | fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { 113 | match *self { 114 | Self::A => write!(f, "a"), 115 | Self::B => write!(f, "b"), 116 | } 117 | } 118 | } 119 | impl ::std::str::FromStr for LetterBoxLetter { 120 | type Err = self::error::ConversionError; 121 | fn from_str(value: &str) -> ::std::result::Result { 122 | match value { 123 | "a" => Ok(Self::A), 124 | "b" => Ok(Self::B), 125 | _ => Err("invalid value".into()), 126 | } 127 | } 128 | } 129 | impl ::std::convert::TryFrom<&str> for LetterBoxLetter { 130 | type Error = self::error::ConversionError; 131 | fn try_from(value: &str) -> ::std::result::Result { 132 | value.parse() 133 | } 134 | } 135 | impl ::std::convert::TryFrom<&::std::string::String> for LetterBoxLetter { 136 | type Error = self::error::ConversionError; 137 | fn try_from( 138 | value: &::std::string::String, 139 | ) -> ::std::result::Result { 140 | value.parse() 141 | } 142 | } 143 | impl ::std::convert::TryFrom<::std::string::String> for LetterBoxLetter { 144 | type Error = self::error::ConversionError; 145 | fn try_from( 146 | value: ::std::string::String, 147 | ) -> ::std::result::Result { 148 | value.parse() 149 | } 150 | } 151 | #[doc = r" Types for composing complex structures."] 152 | pub mod builder { 153 | #[derive(Clone, Debug)] 154 | pub struct LetterBox { 155 | letter: ::std::result::Result< 156 | ::std::option::Option, 157 | ::std::string::String, 158 | >, 159 | } 160 | impl ::std::default::Default for LetterBox { 161 | fn default() -> Self { 162 | Self { 163 | letter: Ok(Default::default()), 164 | } 165 | } 166 | } 167 | impl LetterBox { 168 | pub fn letter(mut self, value: T) -> Self 169 | where 170 | T: ::std::convert::TryInto<::std::option::Option>, 171 | T::Error: ::std::fmt::Display, 172 | { 173 | self.letter = value 174 | .try_into() 175 | .map_err(|e| format!("error converting supplied value for letter: {}", e)); 176 | self 177 | } 178 | } 179 | impl ::std::convert::TryFrom for super::LetterBox { 180 | type Error = super::error::ConversionError; 181 | fn try_from( 182 | value: LetterBox, 183 | ) -> ::std::result::Result { 184 | Ok(Self { 185 | letter: value.letter?, 186 | }) 187 | } 188 | } 189 | impl ::std::convert::From for LetterBox { 190 | fn from(value: super::LetterBox) -> Self { 191 | Self { 192 | letter: Ok(value.letter), 193 | } 194 | } 195 | } 196 | } 197 | fn main() {} 198 | -------------------------------------------------------------------------------- /typify/tests/schemas/id-or-name.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "definitions": { 4 | "IdOrName": { 5 | "oneOf": [ 6 | { 7 | "title": "Id", 8 | "allOf": [ 9 | { 10 | "type": "string", 11 | "format": "uuid" 12 | } 13 | ] 14 | }, 15 | { 16 | "title": "Name", 17 | "allOf": [ 18 | { 19 | "$ref": "#/definitions/Name" 20 | } 21 | ] 22 | } 23 | ] 24 | }, 25 | "IdOrYolo": { 26 | "oneOf": [ 27 | { 28 | "title": "Id", 29 | "allOf": [ 30 | { 31 | "type": "string", 32 | "format": "uuid" 33 | } 34 | ] 35 | }, 36 | { 37 | "title": "Yolo", 38 | "allOf": [ 39 | { 40 | "type": "string", 41 | "pattern": ".*" 42 | } 43 | ] 44 | } 45 | ] 46 | }, 47 | "Name": { 48 | "title": "A name unique within the parent collection", 49 | "description": "Names must begin with a lower case ASCII letter, be composed exclusively of lowercase ASCII, uppercase ASCII, numbers, and '-', and may not end with a '-'. Names cannot be a UUID though they may contain a UUID.", 50 | "type": "string", 51 | "pattern": "^(?![0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$)^[a-z][a-z0-9-]*[a-zA-Z0-9]$", 52 | "maxLength": 63 53 | }, 54 | "IdOrNameRedundant": { 55 | "$comment": "tests references that include a redundant type field", 56 | "oneOf": [ 57 | { 58 | "type": "string", 59 | "format": "uuid" 60 | }, 61 | { 62 | "type": "string", 63 | "$ref": "#/definitions/Name" 64 | } 65 | ] 66 | } 67 | } 68 | } -------------------------------------------------------------------------------- /typify/tests/schemas/maps.json: -------------------------------------------------------------------------------- 1 | { 2 | "$comment": "validate maps, in particular those with constrained string keys", 3 | "$schema": "http://json-schema.org/draft-07/schema#", 4 | "definitions": { 5 | "MapWithKeys": { 6 | "type": "object", 7 | "additionalProperties": { 8 | "$ref": "#/definitions/Value" 9 | }, 10 | "propertyNames": { 11 | "$ref": "#/definitions/Eh" 12 | } 13 | }, 14 | "Value": { 15 | "type": "string" 16 | }, 17 | "Eh": { 18 | "type": "string", 19 | "format": "^a*$" 20 | }, 21 | "MapWithDateTimeKeys": { 22 | "$comment": "test that a type isn't needed for propertyNames", 23 | "type": "object", 24 | "additionalProperties": { 25 | "$ref": "#/definitions/Value" 26 | }, 27 | "propertyNames": { 28 | "format": "date-time" 29 | } 30 | }, 31 | "MapWithDateKeys": { 32 | "$comment": "test that a type isn't needed for propertyNames", 33 | "type": "object", 34 | "additionalProperties": { 35 | "$ref": "#/definitions/Value" 36 | }, 37 | "propertyNames": { 38 | "format": "date" 39 | } 40 | } 41 | }, 42 | "$comment": "usual case of a map whose name must come from its title", 43 | "title": "DeadSimple", 44 | "type": "object" 45 | } -------------------------------------------------------------------------------- /typify/tests/schemas/more_types.json: -------------------------------------------------------------------------------- 1 | { 2 | "$comment": "dumping ground for various types", 3 | "$schema": "http://json-schema.org/draft-07/schema#", 4 | "definitions": { 5 | "ObjectWithNoExtra": { 6 | "properties": { 7 | "foo": { 8 | "type": "string" 9 | } 10 | }, 11 | "required": [ 12 | "foo" 13 | ], 14 | "additionalProperties": false 15 | }, 16 | "ObjectWithOkExtra": { 17 | "properties": { 18 | "foo": { 19 | "type": "string" 20 | } 21 | }, 22 | "required": [ 23 | "foo" 24 | ] 25 | }, 26 | "ObjectWithYesExtra": { 27 | "properties": { 28 | "foo": { 29 | "type": "string" 30 | } 31 | }, 32 | "required": [ 33 | "foo" 34 | ], 35 | "additionalProperties": true 36 | }, 37 | "ObjectWithWhichExtra": { 38 | "properties": { 39 | "foo": { 40 | "type": "string" 41 | } 42 | }, 43 | "required": [ 44 | "foo" 45 | ], 46 | "additionalProperties": {} 47 | }, 48 | "ObjectWithStringExtra": { 49 | "properties": { 50 | "foo": { 51 | "type": "string" 52 | } 53 | }, 54 | "required": [ 55 | "foo" 56 | ], 57 | "additionalProperties": { 58 | "type": "string" 59 | } 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /typify/tests/schemas/multiple-instance-types.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "definitions": { 4 | "seriously-anything": { 5 | "type": [ 6 | "null", 7 | "boolean", 8 | "object", 9 | "array", 10 | "number", 11 | "string", 12 | "integer" 13 | ] 14 | }, 15 | "one-of-several": { 16 | "type": [ 17 | "null", 18 | "boolean", 19 | "object", 20 | "array", 21 | "string", 22 | "integer" 23 | ] 24 | }, 25 | "int-or-str": { 26 | "type": [ 27 | "string", 28 | "integer" 29 | ] 30 | }, 31 | "yes-no-maybe": { 32 | "type": [ 33 | "boolean", 34 | "object" 35 | ], 36 | "properties": { 37 | "value": { 38 | "type": "string" 39 | } 40 | } 41 | }, 42 | "really-just-null": { 43 | "type": [ 44 | "string", 45 | "null" 46 | ], 47 | "enum": [ 48 | null 49 | ] 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /typify/tests/schemas/multiple-instance-types.rs: -------------------------------------------------------------------------------- 1 | #![deny(warnings)] 2 | #[doc = r" Error types."] 3 | pub mod error { 4 | #[doc = r" Error from a `TryFrom` or `FromStr` implementation."] 5 | pub struct ConversionError(::std::borrow::Cow<'static, str>); 6 | impl ::std::error::Error for ConversionError {} 7 | impl ::std::fmt::Display for ConversionError { 8 | fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> Result<(), ::std::fmt::Error> { 9 | ::std::fmt::Display::fmt(&self.0, f) 10 | } 11 | } 12 | impl ::std::fmt::Debug for ConversionError { 13 | fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> Result<(), ::std::fmt::Error> { 14 | ::std::fmt::Debug::fmt(&self.0, f) 15 | } 16 | } 17 | impl From<&'static str> for ConversionError { 18 | fn from(value: &'static str) -> Self { 19 | Self(value.into()) 20 | } 21 | } 22 | impl From for ConversionError { 23 | fn from(value: String) -> Self { 24 | Self(value.into()) 25 | } 26 | } 27 | } 28 | #[doc = "`IntOrStr`"] 29 | #[doc = r""] 30 | #[doc = r"
JSON schema"] 31 | #[doc = r""] 32 | #[doc = r" ```json"] 33 | #[doc = "{"] 34 | #[doc = " \"type\": ["] 35 | #[doc = " \"string\","] 36 | #[doc = " \"integer\""] 37 | #[doc = " ]"] 38 | #[doc = "}"] 39 | #[doc = r" ```"] 40 | #[doc = r"
"] 41 | #[derive(:: serde :: Deserialize, :: serde :: Serialize, Clone, Debug)] 42 | #[serde(untagged)] 43 | pub enum IntOrStr { 44 | String(::std::string::String), 45 | Integer(i64), 46 | } 47 | impl ::std::convert::From<&Self> for IntOrStr { 48 | fn from(value: &IntOrStr) -> Self { 49 | value.clone() 50 | } 51 | } 52 | impl ::std::str::FromStr for IntOrStr { 53 | type Err = self::error::ConversionError; 54 | fn from_str(value: &str) -> ::std::result::Result { 55 | if let Ok(v) = value.parse() { 56 | Ok(Self::String(v)) 57 | } else if let Ok(v) = value.parse() { 58 | Ok(Self::Integer(v)) 59 | } else { 60 | Err("string conversion failed for all variants".into()) 61 | } 62 | } 63 | } 64 | impl ::std::convert::TryFrom<&str> for IntOrStr { 65 | type Error = self::error::ConversionError; 66 | fn try_from(value: &str) -> ::std::result::Result { 67 | value.parse() 68 | } 69 | } 70 | impl ::std::convert::TryFrom<&::std::string::String> for IntOrStr { 71 | type Error = self::error::ConversionError; 72 | fn try_from( 73 | value: &::std::string::String, 74 | ) -> ::std::result::Result { 75 | value.parse() 76 | } 77 | } 78 | impl ::std::convert::TryFrom<::std::string::String> for IntOrStr { 79 | type Error = self::error::ConversionError; 80 | fn try_from( 81 | value: ::std::string::String, 82 | ) -> ::std::result::Result { 83 | value.parse() 84 | } 85 | } 86 | impl ::std::fmt::Display for IntOrStr { 87 | fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { 88 | match self { 89 | Self::String(x) => x.fmt(f), 90 | Self::Integer(x) => x.fmt(f), 91 | } 92 | } 93 | } 94 | impl ::std::convert::From for IntOrStr { 95 | fn from(value: i64) -> Self { 96 | Self::Integer(value) 97 | } 98 | } 99 | #[doc = "`OneOfSeveral`"] 100 | #[doc = r""] 101 | #[doc = r"
JSON schema"] 102 | #[doc = r""] 103 | #[doc = r" ```json"] 104 | #[doc = "{"] 105 | #[doc = " \"type\": ["] 106 | #[doc = " \"null\","] 107 | #[doc = " \"boolean\","] 108 | #[doc = " \"object\","] 109 | #[doc = " \"array\","] 110 | #[doc = " \"string\","] 111 | #[doc = " \"integer\""] 112 | #[doc = " ]"] 113 | #[doc = "}"] 114 | #[doc = r" ```"] 115 | #[doc = r"
"] 116 | #[derive(:: serde :: Deserialize, :: serde :: Serialize, Clone, Debug)] 117 | #[serde(untagged)] 118 | pub enum OneOfSeveral { 119 | Null, 120 | Boolean(bool), 121 | Object(::serde_json::Map<::std::string::String, ::serde_json::Value>), 122 | Array(::std::vec::Vec<::serde_json::Value>), 123 | String(::std::string::String), 124 | Integer(i64), 125 | } 126 | impl ::std::convert::From<&Self> for OneOfSeveral { 127 | fn from(value: &OneOfSeveral) -> Self { 128 | value.clone() 129 | } 130 | } 131 | impl ::std::convert::From for OneOfSeveral { 132 | fn from(value: bool) -> Self { 133 | Self::Boolean(value) 134 | } 135 | } 136 | impl ::std::convert::From<::serde_json::Map<::std::string::String, ::serde_json::Value>> 137 | for OneOfSeveral 138 | { 139 | fn from(value: ::serde_json::Map<::std::string::String, ::serde_json::Value>) -> Self { 140 | Self::Object(value) 141 | } 142 | } 143 | impl ::std::convert::From<::std::vec::Vec<::serde_json::Value>> for OneOfSeveral { 144 | fn from(value: ::std::vec::Vec<::serde_json::Value>) -> Self { 145 | Self::Array(value) 146 | } 147 | } 148 | impl ::std::convert::From for OneOfSeveral { 149 | fn from(value: i64) -> Self { 150 | Self::Integer(value) 151 | } 152 | } 153 | #[doc = "`ReallyJustNull`"] 154 | #[doc = r""] 155 | #[doc = r"
JSON schema"] 156 | #[doc = r""] 157 | #[doc = r" ```json"] 158 | #[doc = "{"] 159 | #[doc = " \"type\": ["] 160 | #[doc = " \"string\","] 161 | #[doc = " \"null\""] 162 | #[doc = " ],"] 163 | #[doc = " \"enum\": ["] 164 | #[doc = " null"] 165 | #[doc = " ]"] 166 | #[doc = "}"] 167 | #[doc = r" ```"] 168 | #[doc = r"
"] 169 | #[derive(:: serde :: Deserialize, :: serde :: Serialize, Clone, Debug)] 170 | #[serde(transparent)] 171 | pub struct ReallyJustNull(pub ()); 172 | impl ::std::ops::Deref for ReallyJustNull { 173 | type Target = (); 174 | fn deref(&self) -> &() { 175 | &self.0 176 | } 177 | } 178 | impl ::std::convert::From for () { 179 | fn from(value: ReallyJustNull) -> Self { 180 | value.0 181 | } 182 | } 183 | impl ::std::convert::From<&ReallyJustNull> for ReallyJustNull { 184 | fn from(value: &ReallyJustNull) -> Self { 185 | value.clone() 186 | } 187 | } 188 | impl ::std::convert::From<()> for ReallyJustNull { 189 | fn from(value: ()) -> Self { 190 | Self(value) 191 | } 192 | } 193 | #[doc = "`SeriouslyAnything`"] 194 | #[doc = r""] 195 | #[doc = r"
JSON schema"] 196 | #[doc = r""] 197 | #[doc = r" ```json"] 198 | #[doc = "{"] 199 | #[doc = " \"type\": ["] 200 | #[doc = " \"null\","] 201 | #[doc = " \"boolean\","] 202 | #[doc = " \"object\","] 203 | #[doc = " \"array\","] 204 | #[doc = " \"number\","] 205 | #[doc = " \"string\","] 206 | #[doc = " \"integer\""] 207 | #[doc = " ]"] 208 | #[doc = "}"] 209 | #[doc = r" ```"] 210 | #[doc = r"
"] 211 | #[derive(:: serde :: Deserialize, :: serde :: Serialize, Clone, Debug)] 212 | #[serde(transparent)] 213 | pub struct SeriouslyAnything(pub ::serde_json::Value); 214 | impl ::std::ops::Deref for SeriouslyAnything { 215 | type Target = ::serde_json::Value; 216 | fn deref(&self) -> &::serde_json::Value { 217 | &self.0 218 | } 219 | } 220 | impl ::std::convert::From for ::serde_json::Value { 221 | fn from(value: SeriouslyAnything) -> Self { 222 | value.0 223 | } 224 | } 225 | impl ::std::convert::From<&SeriouslyAnything> for SeriouslyAnything { 226 | fn from(value: &SeriouslyAnything) -> Self { 227 | value.clone() 228 | } 229 | } 230 | impl ::std::convert::From<::serde_json::Value> for SeriouslyAnything { 231 | fn from(value: ::serde_json::Value) -> Self { 232 | Self(value) 233 | } 234 | } 235 | #[doc = "`YesNoMaybe`"] 236 | #[doc = r""] 237 | #[doc = r"
JSON schema"] 238 | #[doc = r""] 239 | #[doc = r" ```json"] 240 | #[doc = "{"] 241 | #[doc = " \"type\": ["] 242 | #[doc = " \"boolean\","] 243 | #[doc = " \"object\""] 244 | #[doc = " ],"] 245 | #[doc = " \"properties\": {"] 246 | #[doc = " \"value\": {"] 247 | #[doc = " \"type\": \"string\""] 248 | #[doc = " }"] 249 | #[doc = " }"] 250 | #[doc = "}"] 251 | #[doc = r" ```"] 252 | #[doc = r"
"] 253 | #[derive(:: serde :: Deserialize, :: serde :: Serialize, Clone, Debug)] 254 | #[serde(untagged)] 255 | pub enum YesNoMaybe { 256 | Boolean(bool), 257 | Object { 258 | #[serde(default, skip_serializing_if = "::std::option::Option::is_none")] 259 | value: ::std::option::Option<::std::string::String>, 260 | }, 261 | } 262 | impl ::std::convert::From<&Self> for YesNoMaybe { 263 | fn from(value: &YesNoMaybe) -> Self { 264 | value.clone() 265 | } 266 | } 267 | impl ::std::convert::From for YesNoMaybe { 268 | fn from(value: bool) -> Self { 269 | Self::Boolean(value) 270 | } 271 | } 272 | fn main() {} 273 | -------------------------------------------------------------------------------- /typify/tests/schemas/noisy-types.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "definitions": { 4 | "object-bs": { 5 | "type": "object", 6 | "properties": { 7 | "ok": { 8 | "type": "boolean" 9 | } 10 | }, 11 | "multipleOf": 100, 12 | "maxItems": 100, 13 | "maxLength": 100 14 | }, 15 | "array-bs": { 16 | "type": "array", 17 | "items": { 18 | "type": "boolean" 19 | }, 20 | "multipleOf": 100, 21 | "maxLength": 100, 22 | "properties": { 23 | "ok": {} 24 | }, 25 | "additionalProperties": { 26 | "type": "string" 27 | } 28 | }, 29 | "integer-bs": { 30 | "type": "integer", 31 | "minimum": 0, 32 | "maxLength": 100, 33 | "properties": { 34 | "ok": {} 35 | }, 36 | "additionalProperties": { 37 | "type": "string" 38 | } 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /typify/tests/schemas/noisy-types.rs: -------------------------------------------------------------------------------- 1 | #![deny(warnings)] 2 | #[doc = r" Error types."] 3 | pub mod error { 4 | #[doc = r" Error from a `TryFrom` or `FromStr` implementation."] 5 | pub struct ConversionError(::std::borrow::Cow<'static, str>); 6 | impl ::std::error::Error for ConversionError {} 7 | impl ::std::fmt::Display for ConversionError { 8 | fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> Result<(), ::std::fmt::Error> { 9 | ::std::fmt::Display::fmt(&self.0, f) 10 | } 11 | } 12 | impl ::std::fmt::Debug for ConversionError { 13 | fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> Result<(), ::std::fmt::Error> { 14 | ::std::fmt::Debug::fmt(&self.0, f) 15 | } 16 | } 17 | impl From<&'static str> for ConversionError { 18 | fn from(value: &'static str) -> Self { 19 | Self(value.into()) 20 | } 21 | } 22 | impl From for ConversionError { 23 | fn from(value: String) -> Self { 24 | Self(value.into()) 25 | } 26 | } 27 | } 28 | #[doc = "`ArrayBs`"] 29 | #[doc = r""] 30 | #[doc = r"
JSON schema"] 31 | #[doc = r""] 32 | #[doc = r" ```json"] 33 | #[doc = "{"] 34 | #[doc = " \"type\": \"array\","] 35 | #[doc = " \"multipleOf\": 100.0,"] 36 | #[doc = " \"maxLength\": 100,"] 37 | #[doc = " \"items\": {"] 38 | #[doc = " \"type\": \"boolean\""] 39 | #[doc = " },"] 40 | #[doc = " \"properties\": {"] 41 | #[doc = " \"ok\": {}"] 42 | #[doc = " },"] 43 | #[doc = " \"additionalProperties\": {"] 44 | #[doc = " \"type\": \"string\""] 45 | #[doc = " }"] 46 | #[doc = "}"] 47 | #[doc = r" ```"] 48 | #[doc = r"
"] 49 | #[derive(:: serde :: Deserialize, :: serde :: Serialize, Clone, Debug)] 50 | #[serde(transparent)] 51 | pub struct ArrayBs(pub ::std::vec::Vec); 52 | impl ::std::ops::Deref for ArrayBs { 53 | type Target = ::std::vec::Vec; 54 | fn deref(&self) -> &::std::vec::Vec { 55 | &self.0 56 | } 57 | } 58 | impl ::std::convert::From for ::std::vec::Vec { 59 | fn from(value: ArrayBs) -> Self { 60 | value.0 61 | } 62 | } 63 | impl ::std::convert::From<&ArrayBs> for ArrayBs { 64 | fn from(value: &ArrayBs) -> Self { 65 | value.clone() 66 | } 67 | } 68 | impl ::std::convert::From<::std::vec::Vec> for ArrayBs { 69 | fn from(value: ::std::vec::Vec) -> Self { 70 | Self(value) 71 | } 72 | } 73 | #[doc = "`IntegerBs`"] 74 | #[doc = r""] 75 | #[doc = r"
JSON schema"] 76 | #[doc = r""] 77 | #[doc = r" ```json"] 78 | #[doc = "{"] 79 | #[doc = " \"type\": \"integer\","] 80 | #[doc = " \"minimum\": 0.0,"] 81 | #[doc = " \"maxLength\": 100,"] 82 | #[doc = " \"properties\": {"] 83 | #[doc = " \"ok\": {}"] 84 | #[doc = " },"] 85 | #[doc = " \"additionalProperties\": {"] 86 | #[doc = " \"type\": \"string\""] 87 | #[doc = " }"] 88 | #[doc = "}"] 89 | #[doc = r" ```"] 90 | #[doc = r"
"] 91 | #[derive(:: serde :: Deserialize, :: serde :: Serialize, Clone, Debug)] 92 | #[serde(transparent)] 93 | pub struct IntegerBs(pub u64); 94 | impl ::std::ops::Deref for IntegerBs { 95 | type Target = u64; 96 | fn deref(&self) -> &u64 { 97 | &self.0 98 | } 99 | } 100 | impl ::std::convert::From for u64 { 101 | fn from(value: IntegerBs) -> Self { 102 | value.0 103 | } 104 | } 105 | impl ::std::convert::From<&IntegerBs> for IntegerBs { 106 | fn from(value: &IntegerBs) -> Self { 107 | value.clone() 108 | } 109 | } 110 | impl ::std::convert::From for IntegerBs { 111 | fn from(value: u64) -> Self { 112 | Self(value) 113 | } 114 | } 115 | impl ::std::str::FromStr for IntegerBs { 116 | type Err = ::Err; 117 | fn from_str(value: &str) -> ::std::result::Result { 118 | Ok(Self(value.parse()?)) 119 | } 120 | } 121 | impl ::std::convert::TryFrom<&str> for IntegerBs { 122 | type Error = ::Err; 123 | fn try_from(value: &str) -> ::std::result::Result { 124 | value.parse() 125 | } 126 | } 127 | impl ::std::convert::TryFrom<&String> for IntegerBs { 128 | type Error = ::Err; 129 | fn try_from(value: &String) -> ::std::result::Result { 130 | value.parse() 131 | } 132 | } 133 | impl ::std::convert::TryFrom for IntegerBs { 134 | type Error = ::Err; 135 | fn try_from(value: String) -> ::std::result::Result { 136 | value.parse() 137 | } 138 | } 139 | impl ::std::fmt::Display for IntegerBs { 140 | fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { 141 | self.0.fmt(f) 142 | } 143 | } 144 | #[doc = "`ObjectBs`"] 145 | #[doc = r""] 146 | #[doc = r"
JSON schema"] 147 | #[doc = r""] 148 | #[doc = r" ```json"] 149 | #[doc = "{"] 150 | #[doc = " \"type\": \"object\","] 151 | #[doc = " \"multipleOf\": 100.0,"] 152 | #[doc = " \"maxLength\": 100,"] 153 | #[doc = " \"maxItems\": 100,"] 154 | #[doc = " \"properties\": {"] 155 | #[doc = " \"ok\": {"] 156 | #[doc = " \"type\": \"boolean\""] 157 | #[doc = " }"] 158 | #[doc = " }"] 159 | #[doc = "}"] 160 | #[doc = r" ```"] 161 | #[doc = r"
"] 162 | #[derive(:: serde :: Deserialize, :: serde :: Serialize, Clone, Debug)] 163 | pub struct ObjectBs { 164 | #[serde(default, skip_serializing_if = "::std::option::Option::is_none")] 165 | pub ok: ::std::option::Option, 166 | } 167 | impl ::std::convert::From<&ObjectBs> for ObjectBs { 168 | fn from(value: &ObjectBs) -> Self { 169 | value.clone() 170 | } 171 | } 172 | impl ::std::default::Default for ObjectBs { 173 | fn default() -> Self { 174 | Self { 175 | ok: Default::default(), 176 | } 177 | } 178 | } 179 | impl ObjectBs { 180 | pub fn builder() -> builder::ObjectBs { 181 | Default::default() 182 | } 183 | } 184 | #[doc = r" Types for composing complex structures."] 185 | pub mod builder { 186 | #[derive(Clone, Debug)] 187 | pub struct ObjectBs { 188 | ok: ::std::result::Result<::std::option::Option, ::std::string::String>, 189 | } 190 | impl ::std::default::Default for ObjectBs { 191 | fn default() -> Self { 192 | Self { 193 | ok: Ok(Default::default()), 194 | } 195 | } 196 | } 197 | impl ObjectBs { 198 | pub fn ok(mut self, value: T) -> Self 199 | where 200 | T: ::std::convert::TryInto<::std::option::Option>, 201 | T::Error: ::std::fmt::Display, 202 | { 203 | self.ok = value 204 | .try_into() 205 | .map_err(|e| format!("error converting supplied value for ok: {}", e)); 206 | self 207 | } 208 | } 209 | impl ::std::convert::TryFrom for super::ObjectBs { 210 | type Error = super::error::ConversionError; 211 | fn try_from(value: ObjectBs) -> ::std::result::Result { 212 | Ok(Self { ok: value.ok? }) 213 | } 214 | } 215 | impl ::std::convert::From for ObjectBs { 216 | fn from(value: super::ObjectBs) -> Self { 217 | Self { ok: Ok(value.ok) } 218 | } 219 | } 220 | } 221 | fn main() {} 222 | -------------------------------------------------------------------------------- /typify/tests/schemas/property-pattern.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "title": "test grammar for pattern properties", 4 | "type": "object", 5 | "required": [ 6 | "rules" 7 | ], 8 | "additionalProperties": false, 9 | "properties": { 10 | "rules": { 11 | "type": "object", 12 | "patternProperties": { 13 | "^[a-zA-Z_]\\w*$": { 14 | "type": "string" 15 | } 16 | }, 17 | "additionalProperties": false 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /typify/tests/schemas/property-pattern.rs: -------------------------------------------------------------------------------- 1 | #![deny(warnings)] 2 | #[doc = r" Error types."] 3 | pub mod error { 4 | #[doc = r" Error from a `TryFrom` or `FromStr` implementation."] 5 | pub struct ConversionError(::std::borrow::Cow<'static, str>); 6 | impl ::std::error::Error for ConversionError {} 7 | impl ::std::fmt::Display for ConversionError { 8 | fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> Result<(), ::std::fmt::Error> { 9 | ::std::fmt::Display::fmt(&self.0, f) 10 | } 11 | } 12 | impl ::std::fmt::Debug for ConversionError { 13 | fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> Result<(), ::std::fmt::Error> { 14 | ::std::fmt::Debug::fmt(&self.0, f) 15 | } 16 | } 17 | impl From<&'static str> for ConversionError { 18 | fn from(value: &'static str) -> Self { 19 | Self(value.into()) 20 | } 21 | } 22 | impl From for ConversionError { 23 | fn from(value: String) -> Self { 24 | Self(value.into()) 25 | } 26 | } 27 | } 28 | #[doc = "`TestGrammarForPatternProperties`"] 29 | #[doc = r""] 30 | #[doc = r"
JSON schema"] 31 | #[doc = r""] 32 | #[doc = r" ```json"] 33 | #[doc = "{"] 34 | #[doc = " \"title\": \"test grammar for pattern properties\","] 35 | #[doc = " \"type\": \"object\","] 36 | #[doc = " \"required\": ["] 37 | #[doc = " \"rules\""] 38 | #[doc = " ],"] 39 | #[doc = " \"properties\": {"] 40 | #[doc = " \"rules\": {"] 41 | #[doc = " \"type\": \"object\","] 42 | #[doc = " \"patternProperties\": {"] 43 | #[doc = " \"^[a-zA-Z_]\\\\w*$\": {"] 44 | #[doc = " \"type\": \"string\""] 45 | #[doc = " }"] 46 | #[doc = " },"] 47 | #[doc = " \"additionalProperties\": false"] 48 | #[doc = " }"] 49 | #[doc = " },"] 50 | #[doc = " \"additionalProperties\": false"] 51 | #[doc = "}"] 52 | #[doc = r" ```"] 53 | #[doc = r"
"] 54 | #[derive(:: serde :: Deserialize, :: serde :: Serialize, Clone, Debug)] 55 | #[serde(deny_unknown_fields)] 56 | pub struct TestGrammarForPatternProperties { 57 | pub rules: 58 | ::std::collections::HashMap, 59 | } 60 | impl ::std::convert::From<&TestGrammarForPatternProperties> for TestGrammarForPatternProperties { 61 | fn from(value: &TestGrammarForPatternProperties) -> Self { 62 | value.clone() 63 | } 64 | } 65 | impl TestGrammarForPatternProperties { 66 | pub fn builder() -> builder::TestGrammarForPatternProperties { 67 | Default::default() 68 | } 69 | } 70 | #[doc = "`TestGrammarForPatternPropertiesRulesKey`"] 71 | #[doc = r""] 72 | #[doc = r"
JSON schema"] 73 | #[doc = r""] 74 | #[doc = r" ```json"] 75 | #[doc = "{"] 76 | #[doc = " \"type\": \"string\","] 77 | #[doc = " \"pattern\": \"^[a-zA-Z_]\\\\w*$\""] 78 | #[doc = "}"] 79 | #[doc = r" ```"] 80 | #[doc = r"
"] 81 | #[derive(:: serde :: Serialize, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] 82 | #[serde(transparent)] 83 | pub struct TestGrammarForPatternPropertiesRulesKey(::std::string::String); 84 | impl ::std::ops::Deref for TestGrammarForPatternPropertiesRulesKey { 85 | type Target = ::std::string::String; 86 | fn deref(&self) -> &::std::string::String { 87 | &self.0 88 | } 89 | } 90 | impl ::std::convert::From for ::std::string::String { 91 | fn from(value: TestGrammarForPatternPropertiesRulesKey) -> Self { 92 | value.0 93 | } 94 | } 95 | impl ::std::convert::From<&TestGrammarForPatternPropertiesRulesKey> 96 | for TestGrammarForPatternPropertiesRulesKey 97 | { 98 | fn from(value: &TestGrammarForPatternPropertiesRulesKey) -> Self { 99 | value.clone() 100 | } 101 | } 102 | impl ::std::str::FromStr for TestGrammarForPatternPropertiesRulesKey { 103 | type Err = self::error::ConversionError; 104 | fn from_str(value: &str) -> ::std::result::Result { 105 | static PATTERN: ::std::sync::LazyLock<::regress::Regex> = 106 | ::std::sync::LazyLock::new(|| ::regress::Regex::new("^[a-zA-Z_]\\w*$").unwrap()); 107 | if (&*PATTERN).find(value).is_none() { 108 | return Err("doesn't match pattern \"^[a-zA-Z_]\\w*$\"".into()); 109 | } 110 | Ok(Self(value.to_string())) 111 | } 112 | } 113 | impl ::std::convert::TryFrom<&str> for TestGrammarForPatternPropertiesRulesKey { 114 | type Error = self::error::ConversionError; 115 | fn try_from(value: &str) -> ::std::result::Result { 116 | value.parse() 117 | } 118 | } 119 | impl ::std::convert::TryFrom<&::std::string::String> for TestGrammarForPatternPropertiesRulesKey { 120 | type Error = self::error::ConversionError; 121 | fn try_from( 122 | value: &::std::string::String, 123 | ) -> ::std::result::Result { 124 | value.parse() 125 | } 126 | } 127 | impl ::std::convert::TryFrom<::std::string::String> for TestGrammarForPatternPropertiesRulesKey { 128 | type Error = self::error::ConversionError; 129 | fn try_from( 130 | value: ::std::string::String, 131 | ) -> ::std::result::Result { 132 | value.parse() 133 | } 134 | } 135 | impl<'de> ::serde::Deserialize<'de> for TestGrammarForPatternPropertiesRulesKey { 136 | fn deserialize(deserializer: D) -> ::std::result::Result 137 | where 138 | D: ::serde::Deserializer<'de>, 139 | { 140 | ::std::string::String::deserialize(deserializer)? 141 | .parse() 142 | .map_err(|e: self::error::ConversionError| { 143 | ::custom(e.to_string()) 144 | }) 145 | } 146 | } 147 | #[doc = r" Types for composing complex structures."] 148 | pub mod builder { 149 | #[derive(Clone, Debug)] 150 | pub struct TestGrammarForPatternProperties { 151 | rules: ::std::result::Result< 152 | ::std::collections::HashMap< 153 | super::TestGrammarForPatternPropertiesRulesKey, 154 | ::std::string::String, 155 | >, 156 | ::std::string::String, 157 | >, 158 | } 159 | impl ::std::default::Default for TestGrammarForPatternProperties { 160 | fn default() -> Self { 161 | Self { 162 | rules: Err("no value supplied for rules".to_string()), 163 | } 164 | } 165 | } 166 | impl TestGrammarForPatternProperties { 167 | pub fn rules(mut self, value: T) -> Self 168 | where 169 | T: ::std::convert::TryInto< 170 | ::std::collections::HashMap< 171 | super::TestGrammarForPatternPropertiesRulesKey, 172 | ::std::string::String, 173 | >, 174 | >, 175 | T::Error: ::std::fmt::Display, 176 | { 177 | self.rules = value 178 | .try_into() 179 | .map_err(|e| format!("error converting supplied value for rules: {}", e)); 180 | self 181 | } 182 | } 183 | impl ::std::convert::TryFrom 184 | for super::TestGrammarForPatternProperties 185 | { 186 | type Error = super::error::ConversionError; 187 | fn try_from( 188 | value: TestGrammarForPatternProperties, 189 | ) -> ::std::result::Result { 190 | Ok(Self { 191 | rules: value.rules?, 192 | }) 193 | } 194 | } 195 | impl ::std::convert::From 196 | for TestGrammarForPatternProperties 197 | { 198 | fn from(value: super::TestGrammarForPatternProperties) -> Self { 199 | Self { 200 | rules: Ok(value.rules), 201 | } 202 | } 203 | } 204 | } 205 | fn main() {} 206 | -------------------------------------------------------------------------------- /typify/tests/schemas/reflexive.json: -------------------------------------------------------------------------------- 1 | { 2 | "$comment": "validate references to the whole", 3 | "$schema": "http://json-schema.org/draft-07/schema#", 4 | "title": "node", 5 | "type": "object", 6 | "properties": { 7 | "value": { 8 | "type": "integer" 9 | }, 10 | "children": { 11 | "type": "array", 12 | "items": { 13 | "$ref": "#" 14 | } 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /typify/tests/schemas/reflexive.rs: -------------------------------------------------------------------------------- 1 | #![deny(warnings)] 2 | #[doc = r" Error types."] 3 | pub mod error { 4 | #[doc = r" Error from a `TryFrom` or `FromStr` implementation."] 5 | pub struct ConversionError(::std::borrow::Cow<'static, str>); 6 | impl ::std::error::Error for ConversionError {} 7 | impl ::std::fmt::Display for ConversionError { 8 | fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> Result<(), ::std::fmt::Error> { 9 | ::std::fmt::Display::fmt(&self.0, f) 10 | } 11 | } 12 | impl ::std::fmt::Debug for ConversionError { 13 | fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> Result<(), ::std::fmt::Error> { 14 | ::std::fmt::Debug::fmt(&self.0, f) 15 | } 16 | } 17 | impl From<&'static str> for ConversionError { 18 | fn from(value: &'static str) -> Self { 19 | Self(value.into()) 20 | } 21 | } 22 | impl From for ConversionError { 23 | fn from(value: String) -> Self { 24 | Self(value.into()) 25 | } 26 | } 27 | } 28 | #[doc = "`Node`"] 29 | #[doc = r""] 30 | #[doc = r"
JSON schema"] 31 | #[doc = r""] 32 | #[doc = r" ```json"] 33 | #[doc = "{"] 34 | #[doc = " \"title\": \"node\","] 35 | #[doc = " \"type\": \"object\","] 36 | #[doc = " \"properties\": {"] 37 | #[doc = " \"children\": {"] 38 | #[doc = " \"type\": \"array\","] 39 | #[doc = " \"items\": {"] 40 | #[doc = " \"$ref\": \"#\""] 41 | #[doc = " }"] 42 | #[doc = " },"] 43 | #[doc = " \"value\": {"] 44 | #[doc = " \"type\": \"integer\""] 45 | #[doc = " }"] 46 | #[doc = " },"] 47 | #[doc = " \"$comment\": \"validate references to the whole\""] 48 | #[doc = "}"] 49 | #[doc = r" ```"] 50 | #[doc = r"
"] 51 | #[derive(:: serde :: Deserialize, :: serde :: Serialize, Clone, Debug)] 52 | pub struct Node { 53 | #[serde(default, skip_serializing_if = "::std::vec::Vec::is_empty")] 54 | pub children: ::std::vec::Vec, 55 | #[serde(default, skip_serializing_if = "::std::option::Option::is_none")] 56 | pub value: ::std::option::Option, 57 | } 58 | impl ::std::convert::From<&Node> for Node { 59 | fn from(value: &Node) -> Self { 60 | value.clone() 61 | } 62 | } 63 | impl ::std::default::Default for Node { 64 | fn default() -> Self { 65 | Self { 66 | children: Default::default(), 67 | value: Default::default(), 68 | } 69 | } 70 | } 71 | impl Node { 72 | pub fn builder() -> builder::Node { 73 | Default::default() 74 | } 75 | } 76 | #[doc = r" Types for composing complex structures."] 77 | pub mod builder { 78 | #[derive(Clone, Debug)] 79 | pub struct Node { 80 | children: ::std::result::Result<::std::vec::Vec, ::std::string::String>, 81 | value: ::std::result::Result<::std::option::Option, ::std::string::String>, 82 | } 83 | impl ::std::default::Default for Node { 84 | fn default() -> Self { 85 | Self { 86 | children: Ok(Default::default()), 87 | value: Ok(Default::default()), 88 | } 89 | } 90 | } 91 | impl Node { 92 | pub fn children(mut self, value: T) -> Self 93 | where 94 | T: ::std::convert::TryInto<::std::vec::Vec>, 95 | T::Error: ::std::fmt::Display, 96 | { 97 | self.children = value 98 | .try_into() 99 | .map_err(|e| format!("error converting supplied value for children: {}", e)); 100 | self 101 | } 102 | pub fn value(mut self, value: T) -> Self 103 | where 104 | T: ::std::convert::TryInto<::std::option::Option>, 105 | T::Error: ::std::fmt::Display, 106 | { 107 | self.value = value 108 | .try_into() 109 | .map_err(|e| format!("error converting supplied value for value: {}", e)); 110 | self 111 | } 112 | } 113 | impl ::std::convert::TryFrom for super::Node { 114 | type Error = super::error::ConversionError; 115 | fn try_from(value: Node) -> ::std::result::Result { 116 | Ok(Self { 117 | children: value.children?, 118 | value: value.value?, 119 | }) 120 | } 121 | } 122 | impl ::std::convert::From for Node { 123 | fn from(value: super::Node) -> Self { 124 | Self { 125 | children: Ok(value.children), 126 | value: Ok(value.value), 127 | } 128 | } 129 | } 130 | } 131 | fn main() {} 132 | -------------------------------------------------------------------------------- /typify/tests/schemas/simple-types.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "definitions": { 4 | "floats-aren't-terrible-I'm-told": { 5 | "type": "object", 6 | "properties": { 7 | "flush_timeout": { 8 | "type": "number", 9 | "format": "float" 10 | } 11 | } 12 | }, 13 | "just-one": { 14 | "type": [ 15 | "string" 16 | ] 17 | }, 18 | "anything-works": { 19 | "type": "object", 20 | "required": [ 21 | "value" 22 | ] 23 | }, 24 | "uint-minimum-and-maximum": { 25 | "type": "object", 26 | "required": [ 27 | "max", 28 | "min", 29 | "min_and_max", 30 | "min_non_zero", 31 | "min_uint_non_zero", 32 | "no_bounds" 33 | ], 34 | "properties": { 35 | "no_bounds": { 36 | "type": "integer", 37 | "format": "uint64" 38 | }, 39 | "min": { 40 | "type": "integer", 41 | "format": "uint64", 42 | "minimum": 0 43 | }, 44 | "min_non_zero": { 45 | "type": "integer", 46 | "minimum": 1 47 | }, 48 | "min_uint_non_zero": { 49 | "type": "integer", 50 | "format": "uint64", 51 | "minimum": 1 52 | }, 53 | "max": { 54 | "type": "integer", 55 | "format": "uint64", 56 | "maximum": 256 57 | }, 58 | "min_and_max": { 59 | "type": "integer", 60 | "format": "uint64", 61 | "minimum": 1, 62 | "maximum": 256 63 | } 64 | } 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /typify/tests/schemas/string-enum-with-default.json: -------------------------------------------------------------------------------- 1 | { 2 | "definitions": { 3 | "test-enum": { 4 | "type": "string", 5 | "enum": [ 6 | "failure", 7 | "skipped", 8 | "success" 9 | ], 10 | "default": "failure" 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /typify/tests/schemas/string-enum-with-default.rs: -------------------------------------------------------------------------------- 1 | #![deny(warnings)] 2 | #[doc = r" Error types."] 3 | pub mod error { 4 | #[doc = r" Error from a `TryFrom` or `FromStr` implementation."] 5 | pub struct ConversionError(::std::borrow::Cow<'static, str>); 6 | impl ::std::error::Error for ConversionError {} 7 | impl ::std::fmt::Display for ConversionError { 8 | fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> Result<(), ::std::fmt::Error> { 9 | ::std::fmt::Display::fmt(&self.0, f) 10 | } 11 | } 12 | impl ::std::fmt::Debug for ConversionError { 13 | fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> Result<(), ::std::fmt::Error> { 14 | ::std::fmt::Debug::fmt(&self.0, f) 15 | } 16 | } 17 | impl From<&'static str> for ConversionError { 18 | fn from(value: &'static str) -> Self { 19 | Self(value.into()) 20 | } 21 | } 22 | impl From for ConversionError { 23 | fn from(value: String) -> Self { 24 | Self(value.into()) 25 | } 26 | } 27 | } 28 | #[doc = "`TestEnum`"] 29 | #[doc = r""] 30 | #[doc = r"
JSON schema"] 31 | #[doc = r""] 32 | #[doc = r" ```json"] 33 | #[doc = "{"] 34 | #[doc = " \"default\": \"failure\","] 35 | #[doc = " \"type\": \"string\","] 36 | #[doc = " \"enum\": ["] 37 | #[doc = " \"failure\","] 38 | #[doc = " \"skipped\","] 39 | #[doc = " \"success\""] 40 | #[doc = " ]"] 41 | #[doc = "}"] 42 | #[doc = r" ```"] 43 | #[doc = r"
"] 44 | #[derive( 45 | :: serde :: Deserialize, 46 | :: serde :: Serialize, 47 | Clone, 48 | Copy, 49 | Debug, 50 | Eq, 51 | Hash, 52 | Ord, 53 | PartialEq, 54 | PartialOrd, 55 | )] 56 | pub enum TestEnum { 57 | #[serde(rename = "failure")] 58 | Failure, 59 | #[serde(rename = "skipped")] 60 | Skipped, 61 | #[serde(rename = "success")] 62 | Success, 63 | } 64 | impl ::std::convert::From<&Self> for TestEnum { 65 | fn from(value: &TestEnum) -> Self { 66 | value.clone() 67 | } 68 | } 69 | impl ::std::fmt::Display for TestEnum { 70 | fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { 71 | match *self { 72 | Self::Failure => write!(f, "failure"), 73 | Self::Skipped => write!(f, "skipped"), 74 | Self::Success => write!(f, "success"), 75 | } 76 | } 77 | } 78 | impl ::std::str::FromStr for TestEnum { 79 | type Err = self::error::ConversionError; 80 | fn from_str(value: &str) -> ::std::result::Result { 81 | match value { 82 | "failure" => Ok(Self::Failure), 83 | "skipped" => Ok(Self::Skipped), 84 | "success" => Ok(Self::Success), 85 | _ => Err("invalid value".into()), 86 | } 87 | } 88 | } 89 | impl ::std::convert::TryFrom<&str> for TestEnum { 90 | type Error = self::error::ConversionError; 91 | fn try_from(value: &str) -> ::std::result::Result { 92 | value.parse() 93 | } 94 | } 95 | impl ::std::convert::TryFrom<&::std::string::String> for TestEnum { 96 | type Error = self::error::ConversionError; 97 | fn try_from( 98 | value: &::std::string::String, 99 | ) -> ::std::result::Result { 100 | value.parse() 101 | } 102 | } 103 | impl ::std::convert::TryFrom<::std::string::String> for TestEnum { 104 | type Error = self::error::ConversionError; 105 | fn try_from( 106 | value: ::std::string::String, 107 | ) -> ::std::result::Result { 108 | value.parse() 109 | } 110 | } 111 | impl ::std::default::Default for TestEnum { 112 | fn default() -> Self { 113 | TestEnum::Failure 114 | } 115 | } 116 | fn main() {} 117 | -------------------------------------------------------------------------------- /typify/tests/schemas/type-with-modified-generation.json: -------------------------------------------------------------------------------- 1 | { 2 | "$comment": "validate replacement, patch, and conversion settings", 3 | "title": "TestType", 4 | "type": "object", 5 | "properties": { 6 | "replaced_type": { 7 | "$ref": "#/definitions/HandGeneratedType" 8 | }, 9 | "patched_type": { 10 | "$ref": "#/definitions/TypeThatNeedsMoreDerives" 11 | }, 12 | "converted_type": { 13 | "enum": [ 14 | 1, 15 | "one" 16 | ] 17 | } 18 | }, 19 | "required": [ 20 | "replaced_type", 21 | "patched_type", 22 | "converted_type" 23 | ], 24 | "definitions": { 25 | "HandGeneratedType": { 26 | "$comment": "imagine a type we want hand-generated" 27 | }, 28 | "TypeThatNeedsMoreDerives": { 29 | "type": "object", 30 | "additionalProperties": { 31 | "type": "string" 32 | } 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /typify/tests/schemas/type-with-modified-generation.rs: -------------------------------------------------------------------------------- 1 | #![deny(warnings)] 2 | #[doc = r" Error types."] 3 | pub mod error { 4 | #[doc = r" Error from a `TryFrom` or `FromStr` implementation."] 5 | pub struct ConversionError(::std::borrow::Cow<'static, str>); 6 | impl ::std::error::Error for ConversionError {} 7 | impl ::std::fmt::Display for ConversionError { 8 | fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> Result<(), ::std::fmt::Error> { 9 | ::std::fmt::Display::fmt(&self.0, f) 10 | } 11 | } 12 | impl ::std::fmt::Debug for ConversionError { 13 | fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> Result<(), ::std::fmt::Error> { 14 | ::std::fmt::Debug::fmt(&self.0, f) 15 | } 16 | } 17 | impl From<&'static str> for ConversionError { 18 | fn from(value: &'static str) -> Self { 19 | Self(value.into()) 20 | } 21 | } 22 | impl From for ConversionError { 23 | fn from(value: String) -> Self { 24 | Self(value.into()) 25 | } 26 | } 27 | } 28 | #[doc = "`TestType`"] 29 | #[doc = r""] 30 | #[doc = r"
JSON schema"] 31 | #[doc = r""] 32 | #[doc = r" ```json"] 33 | #[doc = "{"] 34 | #[doc = " \"title\": \"TestType\","] 35 | #[doc = " \"type\": \"object\","] 36 | #[doc = " \"required\": ["] 37 | #[doc = " \"converted_type\","] 38 | #[doc = " \"patched_type\","] 39 | #[doc = " \"replaced_type\""] 40 | #[doc = " ],"] 41 | #[doc = " \"properties\": {"] 42 | #[doc = " \"converted_type\": {"] 43 | #[doc = " \"enum\": ["] 44 | #[doc = " 1,"] 45 | #[doc = " \"one\""] 46 | #[doc = " ]"] 47 | #[doc = " },"] 48 | #[doc = " \"patched_type\": {"] 49 | #[doc = " \"$ref\": \"#/definitions/TypeThatNeedsMoreDerives\""] 50 | #[doc = " },"] 51 | #[doc = " \"replaced_type\": {"] 52 | #[doc = " \"$ref\": \"#/definitions/HandGeneratedType\""] 53 | #[doc = " }"] 54 | #[doc = " },"] 55 | #[doc = " \"$comment\": \"validate replacement, patch, and conversion settings\""] 56 | #[doc = "}"] 57 | #[doc = r" ```"] 58 | #[doc = r"
"] 59 | #[derive(:: serde :: Deserialize, :: serde :: Serialize, Clone, Debug)] 60 | pub struct TestType { 61 | pub converted_type: serde_json::Value, 62 | pub patched_type: TypeThatHasMoreDerives, 63 | pub replaced_type: String, 64 | } 65 | impl ::std::convert::From<&TestType> for TestType { 66 | fn from(value: &TestType) -> Self { 67 | value.clone() 68 | } 69 | } 70 | impl TestType { 71 | pub fn builder() -> builder::TestType { 72 | Default::default() 73 | } 74 | } 75 | #[doc = "`TypeThatHasMoreDerives`"] 76 | #[doc = r""] 77 | #[doc = r"
JSON schema"] 78 | #[doc = r""] 79 | #[doc = r" ```json"] 80 | #[doc = "{"] 81 | #[doc = " \"type\": \"object\","] 82 | #[doc = " \"additionalProperties\": {"] 83 | #[doc = " \"type\": \"string\""] 84 | #[doc = " }"] 85 | #[doc = "}"] 86 | #[doc = r" ```"] 87 | #[doc = r"
"] 88 | #[derive(:: serde :: Deserialize, :: serde :: Serialize, Clone, Debug, Eq, PartialEq)] 89 | #[serde(transparent)] 90 | pub struct TypeThatHasMoreDerives( 91 | pub ::std::collections::HashMap<::std::string::String, ::std::string::String>, 92 | ); 93 | impl ::std::ops::Deref for TypeThatHasMoreDerives { 94 | type Target = ::std::collections::HashMap<::std::string::String, ::std::string::String>; 95 | fn deref(&self) -> &::std::collections::HashMap<::std::string::String, ::std::string::String> { 96 | &self.0 97 | } 98 | } 99 | impl ::std::convert::From 100 | for ::std::collections::HashMap<::std::string::String, ::std::string::String> 101 | { 102 | fn from(value: TypeThatHasMoreDerives) -> Self { 103 | value.0 104 | } 105 | } 106 | impl ::std::convert::From<&TypeThatHasMoreDerives> for TypeThatHasMoreDerives { 107 | fn from(value: &TypeThatHasMoreDerives) -> Self { 108 | value.clone() 109 | } 110 | } 111 | impl ::std::convert::From<::std::collections::HashMap<::std::string::String, ::std::string::String>> 112 | for TypeThatHasMoreDerives 113 | { 114 | fn from( 115 | value: ::std::collections::HashMap<::std::string::String, ::std::string::String>, 116 | ) -> Self { 117 | Self(value) 118 | } 119 | } 120 | #[doc = r" Types for composing complex structures."] 121 | pub mod builder { 122 | #[derive(Clone, Debug)] 123 | pub struct TestType { 124 | converted_type: ::std::result::Result, 125 | patched_type: ::std::result::Result, 126 | replaced_type: ::std::result::Result, 127 | } 128 | impl ::std::default::Default for TestType { 129 | fn default() -> Self { 130 | Self { 131 | converted_type: Err("no value supplied for converted_type".to_string()), 132 | patched_type: Err("no value supplied for patched_type".to_string()), 133 | replaced_type: Err("no value supplied for replaced_type".to_string()), 134 | } 135 | } 136 | } 137 | impl TestType { 138 | pub fn converted_type(mut self, value: T) -> Self 139 | where 140 | T: ::std::convert::TryInto, 141 | T::Error: ::std::fmt::Display, 142 | { 143 | self.converted_type = value 144 | .try_into() 145 | .map_err(|e| format!("error converting supplied value for converted_type: {}", e)); 146 | self 147 | } 148 | pub fn patched_type(mut self, value: T) -> Self 149 | where 150 | T: ::std::convert::TryInto, 151 | T::Error: ::std::fmt::Display, 152 | { 153 | self.patched_type = value 154 | .try_into() 155 | .map_err(|e| format!("error converting supplied value for patched_type: {}", e)); 156 | self 157 | } 158 | pub fn replaced_type(mut self, value: T) -> Self 159 | where 160 | T: ::std::convert::TryInto, 161 | T::Error: ::std::fmt::Display, 162 | { 163 | self.replaced_type = value 164 | .try_into() 165 | .map_err(|e| format!("error converting supplied value for replaced_type: {}", e)); 166 | self 167 | } 168 | } 169 | impl ::std::convert::TryFrom for super::TestType { 170 | type Error = super::error::ConversionError; 171 | fn try_from(value: TestType) -> ::std::result::Result { 172 | Ok(Self { 173 | converted_type: value.converted_type?, 174 | patched_type: value.patched_type?, 175 | replaced_type: value.replaced_type?, 176 | }) 177 | } 178 | } 179 | impl ::std::convert::From for TestType { 180 | fn from(value: super::TestType) -> Self { 181 | Self { 182 | converted_type: Ok(value.converted_type), 183 | patched_type: Ok(value.patched_type), 184 | replaced_type: Ok(value.replaced_type), 185 | } 186 | } 187 | } 188 | } 189 | fn main() {} 190 | -------------------------------------------------------------------------------- /typify/tests/schemas/types-with-defaults.json: -------------------------------------------------------------------------------- 1 | { 2 | "$comment": "defaults for built-in types", 3 | "$schema": "http://json-schema.org/draft-04/schema#", 4 | "definitions": { 5 | "TestBed": { 6 | "type": "object", 7 | "properties": { 8 | "id": { 9 | "type": "string", 10 | "format": "uuid", 11 | "default": "abc123-is-this-a-uuid" 12 | }, 13 | "any": { 14 | "type": "array", 15 | "items": {}, 16 | "default": [ 17 | [ 18 | 8, 19 | 6, 20 | 7 21 | ], 22 | [ 23 | 5, 24 | 3, 25 | 0, 26 | 9 27 | ] 28 | ] 29 | } 30 | } 31 | }, 32 | "OuterThing": { 33 | "type": "object", 34 | "properties": { 35 | "thing": { 36 | "type": "object", 37 | "title": "ThingWithDefaults", 38 | "properties": { 39 | "a": { 40 | "type": "string" 41 | }, 42 | "type": { 43 | "type": "string" 44 | } 45 | }, 46 | "additionalProperties": false, 47 | "default": { 48 | "type": "bee" 49 | } 50 | } 51 | } 52 | }, 53 | "Doodad": { 54 | "type": "object", 55 | "properties": { 56 | "when": { 57 | "type": "string", 58 | "format": "date-time", 59 | "default": "1970-01-01T00:00:00Z" 60 | } 61 | } 62 | }, 63 | "MrDefaultNumbers": { 64 | "type": "object", 65 | "properties": { 66 | "little_u8": { 67 | "type": "integer", 68 | "format": "uint8", 69 | "minimum": 1, 70 | "default": 2 71 | }, 72 | "little_u16": { 73 | "type": "integer", 74 | "format": "uint16", 75 | "minimum": 1, 76 | "default": 3 77 | }, 78 | "big_nullable": { 79 | "type": [ 80 | "integer", 81 | "null" 82 | ], 83 | "format": "uint64", 84 | "minimum": 1, 85 | "default": 1 86 | } 87 | } 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /typify/tests/schemas/types-with-more-impls.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "definitions": { 4 | "PatternString": { 5 | "type": "string", 6 | "pattern": "xx" 7 | }, 8 | "Sub10Primes": { 9 | "type": "integer", 10 | "format": "uint", 11 | "enum": [ 12 | 2, 13 | 3, 14 | 5, 15 | 7 16 | ] 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /typify/tests/schemas/types-with-more-impls.rs: -------------------------------------------------------------------------------- 1 | #![deny(warnings)] 2 | #[doc = r" Error types."] 3 | pub mod error { 4 | #[doc = r" Error from a `TryFrom` or `FromStr` implementation."] 5 | pub struct ConversionError(::std::borrow::Cow<'static, str>); 6 | impl ::std::error::Error for ConversionError {} 7 | impl ::std::fmt::Display for ConversionError { 8 | fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> Result<(), ::std::fmt::Error> { 9 | ::std::fmt::Display::fmt(&self.0, f) 10 | } 11 | } 12 | impl ::std::fmt::Debug for ConversionError { 13 | fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> Result<(), ::std::fmt::Error> { 14 | ::std::fmt::Debug::fmt(&self.0, f) 15 | } 16 | } 17 | impl From<&'static str> for ConversionError { 18 | fn from(value: &'static str) -> Self { 19 | Self(value.into()) 20 | } 21 | } 22 | impl From for ConversionError { 23 | fn from(value: String) -> Self { 24 | Self(value.into()) 25 | } 26 | } 27 | } 28 | #[doc = "`PatternString`"] 29 | #[doc = r""] 30 | #[doc = r"
JSON schema"] 31 | #[doc = r""] 32 | #[doc = r" ```json"] 33 | #[doc = "{"] 34 | #[doc = " \"type\": \"string\","] 35 | #[doc = " \"pattern\": \"xx\""] 36 | #[doc = "}"] 37 | #[doc = r" ```"] 38 | #[doc = r"
"] 39 | #[derive(:: serde :: Serialize, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] 40 | #[serde(transparent)] 41 | pub struct PatternString(::std::string::String); 42 | impl ::std::ops::Deref for PatternString { 43 | type Target = ::std::string::String; 44 | fn deref(&self) -> &::std::string::String { 45 | &self.0 46 | } 47 | } 48 | impl ::std::convert::From for ::std::string::String { 49 | fn from(value: PatternString) -> Self { 50 | value.0 51 | } 52 | } 53 | impl ::std::convert::From<&PatternString> for PatternString { 54 | fn from(value: &PatternString) -> Self { 55 | value.clone() 56 | } 57 | } 58 | impl ::std::str::FromStr for PatternString { 59 | type Err = self::error::ConversionError; 60 | fn from_str(value: &str) -> ::std::result::Result { 61 | static PATTERN: ::std::sync::LazyLock<::regress::Regex> = 62 | ::std::sync::LazyLock::new(|| ::regress::Regex::new("xx").unwrap()); 63 | if (&*PATTERN).find(value).is_none() { 64 | return Err("doesn't match pattern \"xx\"".into()); 65 | } 66 | Ok(Self(value.to_string())) 67 | } 68 | } 69 | impl ::std::convert::TryFrom<&str> for PatternString { 70 | type Error = self::error::ConversionError; 71 | fn try_from(value: &str) -> ::std::result::Result { 72 | value.parse() 73 | } 74 | } 75 | impl ::std::convert::TryFrom<&::std::string::String> for PatternString { 76 | type Error = self::error::ConversionError; 77 | fn try_from( 78 | value: &::std::string::String, 79 | ) -> ::std::result::Result { 80 | value.parse() 81 | } 82 | } 83 | impl ::std::convert::TryFrom<::std::string::String> for PatternString { 84 | type Error = self::error::ConversionError; 85 | fn try_from( 86 | value: ::std::string::String, 87 | ) -> ::std::result::Result { 88 | value.parse() 89 | } 90 | } 91 | impl<'de> ::serde::Deserialize<'de> for PatternString { 92 | fn deserialize(deserializer: D) -> ::std::result::Result 93 | where 94 | D: ::serde::Deserializer<'de>, 95 | { 96 | ::std::string::String::deserialize(deserializer)? 97 | .parse() 98 | .map_err(|e: self::error::ConversionError| { 99 | ::custom(e.to_string()) 100 | }) 101 | } 102 | } 103 | #[doc = "`Sub10Primes`"] 104 | #[doc = r""] 105 | #[doc = r"
JSON schema"] 106 | #[doc = r""] 107 | #[doc = r" ```json"] 108 | #[doc = "{"] 109 | #[doc = " \"type\": \"integer\","] 110 | #[doc = " \"format\": \"uint\","] 111 | #[doc = " \"enum\": ["] 112 | #[doc = " 2,"] 113 | #[doc = " 3,"] 114 | #[doc = " 5,"] 115 | #[doc = " 7"] 116 | #[doc = " ]"] 117 | #[doc = "}"] 118 | #[doc = r" ```"] 119 | #[doc = r"
"] 120 | #[derive(:: serde :: Serialize, Clone, Debug)] 121 | #[serde(transparent)] 122 | pub struct Sub10Primes(u32); 123 | impl ::std::ops::Deref for Sub10Primes { 124 | type Target = u32; 125 | fn deref(&self) -> &u32 { 126 | &self.0 127 | } 128 | } 129 | impl ::std::convert::From for u32 { 130 | fn from(value: Sub10Primes) -> Self { 131 | value.0 132 | } 133 | } 134 | impl ::std::convert::From<&Sub10Primes> for Sub10Primes { 135 | fn from(value: &Sub10Primes) -> Self { 136 | value.clone() 137 | } 138 | } 139 | impl ::std::convert::TryFrom for Sub10Primes { 140 | type Error = self::error::ConversionError; 141 | fn try_from(value: u32) -> ::std::result::Result { 142 | if ![2_u32, 3_u32, 5_u32, 7_u32].contains(&value) { 143 | Err("invalid value".into()) 144 | } else { 145 | Ok(Self(value)) 146 | } 147 | } 148 | } 149 | impl<'de> ::serde::Deserialize<'de> for Sub10Primes { 150 | fn deserialize(deserializer: D) -> ::std::result::Result 151 | where 152 | D: ::serde::Deserializer<'de>, 153 | { 154 | Self::try_from(::deserialize(deserializer)?) 155 | .map_err(|e| ::custom(e.to_string())) 156 | } 157 | } 158 | fn main() {} 159 | -------------------------------------------------------------------------------- /typify/tests/schemas/untyped-enum-with-null.json: -------------------------------------------------------------------------------- 1 | { 2 | "$comment": "validate a type with no type and enum values that include a null", 3 | "title": "TestType", 4 | "type": "object", 5 | "properties": { 6 | "value": { 7 | "enum": [ 8 | null, 9 | "start", 10 | "middle", 11 | "end" 12 | ] 13 | } 14 | }, 15 | "required": [ 16 | "value" 17 | ] 18 | } -------------------------------------------------------------------------------- /typify/tests/schemas/untyped-enum-with-null.rs: -------------------------------------------------------------------------------- 1 | #![deny(warnings)] 2 | #[doc = r" Error types."] 3 | pub mod error { 4 | #[doc = r" Error from a `TryFrom` or `FromStr` implementation."] 5 | pub struct ConversionError(::std::borrow::Cow<'static, str>); 6 | impl ::std::error::Error for ConversionError {} 7 | impl ::std::fmt::Display for ConversionError { 8 | fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> Result<(), ::std::fmt::Error> { 9 | ::std::fmt::Display::fmt(&self.0, f) 10 | } 11 | } 12 | impl ::std::fmt::Debug for ConversionError { 13 | fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> Result<(), ::std::fmt::Error> { 14 | ::std::fmt::Debug::fmt(&self.0, f) 15 | } 16 | } 17 | impl From<&'static str> for ConversionError { 18 | fn from(value: &'static str) -> Self { 19 | Self(value.into()) 20 | } 21 | } 22 | impl From for ConversionError { 23 | fn from(value: String) -> Self { 24 | Self(value.into()) 25 | } 26 | } 27 | } 28 | #[doc = "`TestType`"] 29 | #[doc = r""] 30 | #[doc = r"
JSON schema"] 31 | #[doc = r""] 32 | #[doc = r" ```json"] 33 | #[doc = "{"] 34 | #[doc = " \"title\": \"TestType\","] 35 | #[doc = " \"type\": \"object\","] 36 | #[doc = " \"required\": ["] 37 | #[doc = " \"value\""] 38 | #[doc = " ],"] 39 | #[doc = " \"properties\": {"] 40 | #[doc = " \"value\": {"] 41 | #[doc = " \"enum\": ["] 42 | #[doc = " null,"] 43 | #[doc = " \"start\","] 44 | #[doc = " \"middle\","] 45 | #[doc = " \"end\""] 46 | #[doc = " ]"] 47 | #[doc = " }"] 48 | #[doc = " },"] 49 | #[doc = " \"$comment\": \"validate a type with no type and enum values that include a null\""] 50 | #[doc = "}"] 51 | #[doc = r" ```"] 52 | #[doc = r"
"] 53 | #[derive(:: serde :: Deserialize, :: serde :: Serialize, Clone, Debug)] 54 | pub struct TestType { 55 | pub value: ::std::option::Option, 56 | } 57 | impl ::std::convert::From<&TestType> for TestType { 58 | fn from(value: &TestType) -> Self { 59 | value.clone() 60 | } 61 | } 62 | impl TestType { 63 | pub fn builder() -> builder::TestType { 64 | Default::default() 65 | } 66 | } 67 | #[doc = "`TestTypeValue`"] 68 | #[doc = r""] 69 | #[doc = r"
JSON schema"] 70 | #[doc = r""] 71 | #[doc = r" ```json"] 72 | #[doc = "{"] 73 | #[doc = " \"enum\": ["] 74 | #[doc = " null,"] 75 | #[doc = " \"start\","] 76 | #[doc = " \"middle\","] 77 | #[doc = " \"end\""] 78 | #[doc = " ]"] 79 | #[doc = "}"] 80 | #[doc = r" ```"] 81 | #[doc = r"
"] 82 | #[derive( 83 | :: serde :: Deserialize, 84 | :: serde :: Serialize, 85 | Clone, 86 | Copy, 87 | Debug, 88 | Eq, 89 | Hash, 90 | Ord, 91 | PartialEq, 92 | PartialOrd, 93 | )] 94 | pub enum TestTypeValue { 95 | #[serde(rename = "start")] 96 | Start, 97 | #[serde(rename = "middle")] 98 | Middle, 99 | #[serde(rename = "end")] 100 | End, 101 | } 102 | impl ::std::convert::From<&Self> for TestTypeValue { 103 | fn from(value: &TestTypeValue) -> Self { 104 | value.clone() 105 | } 106 | } 107 | impl ::std::fmt::Display for TestTypeValue { 108 | fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { 109 | match *self { 110 | Self::Start => write!(f, "start"), 111 | Self::Middle => write!(f, "middle"), 112 | Self::End => write!(f, "end"), 113 | } 114 | } 115 | } 116 | impl ::std::str::FromStr for TestTypeValue { 117 | type Err = self::error::ConversionError; 118 | fn from_str(value: &str) -> ::std::result::Result { 119 | match value { 120 | "start" => Ok(Self::Start), 121 | "middle" => Ok(Self::Middle), 122 | "end" => Ok(Self::End), 123 | _ => Err("invalid value".into()), 124 | } 125 | } 126 | } 127 | impl ::std::convert::TryFrom<&str> for TestTypeValue { 128 | type Error = self::error::ConversionError; 129 | fn try_from(value: &str) -> ::std::result::Result { 130 | value.parse() 131 | } 132 | } 133 | impl ::std::convert::TryFrom<&::std::string::String> for TestTypeValue { 134 | type Error = self::error::ConversionError; 135 | fn try_from( 136 | value: &::std::string::String, 137 | ) -> ::std::result::Result { 138 | value.parse() 139 | } 140 | } 141 | impl ::std::convert::TryFrom<::std::string::String> for TestTypeValue { 142 | type Error = self::error::ConversionError; 143 | fn try_from( 144 | value: ::std::string::String, 145 | ) -> ::std::result::Result { 146 | value.parse() 147 | } 148 | } 149 | #[doc = r" Types for composing complex structures."] 150 | pub mod builder { 151 | #[derive(Clone, Debug)] 152 | pub struct TestType { 153 | value: ::std::result::Result< 154 | ::std::option::Option, 155 | ::std::string::String, 156 | >, 157 | } 158 | impl ::std::default::Default for TestType { 159 | fn default() -> Self { 160 | Self { 161 | value: Err("no value supplied for value".to_string()), 162 | } 163 | } 164 | } 165 | impl TestType { 166 | pub fn value(mut self, value: T) -> Self 167 | where 168 | T: ::std::convert::TryInto<::std::option::Option>, 169 | T::Error: ::std::fmt::Display, 170 | { 171 | self.value = value 172 | .try_into() 173 | .map_err(|e| format!("error converting supplied value for value: {}", e)); 174 | self 175 | } 176 | } 177 | impl ::std::convert::TryFrom for super::TestType { 178 | type Error = super::error::ConversionError; 179 | fn try_from(value: TestType) -> ::std::result::Result { 180 | Ok(Self { 181 | value: value.value?, 182 | }) 183 | } 184 | } 185 | impl ::std::convert::From for TestType { 186 | fn from(value: super::TestType) -> Self { 187 | Self { 188 | value: Ok(value.value), 189 | } 190 | } 191 | } 192 | } 193 | fn main() {} 194 | -------------------------------------------------------------------------------- /typify/tests/schemas/various-enums.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "definitions": { 4 | "one-of-types": { 5 | "type": "object", 6 | "oneOf": [ 7 | { 8 | "properties": { 9 | "bar": { 10 | "type": "integer" 11 | } 12 | }, 13 | "required": [ 14 | "bar" 15 | ] 16 | }, 17 | { 18 | "properties": { 19 | "foo": { 20 | "type": "string" 21 | } 22 | }, 23 | "required": [ 24 | "foo" 25 | ] 26 | } 27 | ] 28 | }, 29 | "IpNet": { 30 | "$comment": "we want to see *nice* variant names in the output", 31 | "oneOf": [ 32 | { 33 | "title": "V4", 34 | "allOf": [ 35 | { 36 | "$ref": "#/components/schemas/Ipv4Net" 37 | } 38 | ] 39 | }, 40 | { 41 | "title": "V6", 42 | "allOf": [ 43 | { 44 | "$ref": "#/components/schemas/Ipv6Net" 45 | } 46 | ] 47 | } 48 | ] 49 | }, 50 | "Ipv4Net": { 51 | "type": "string" 52 | }, 53 | "Ipv6Net": { 54 | "type": "string" 55 | }, 56 | "NullStringEnumWithUnknownFormat": { 57 | "type": [ 58 | "string", 59 | "null" 60 | ], 61 | "enum": [ 62 | "a", 63 | "b", 64 | "c" 65 | ], 66 | "format": "?" 67 | }, 68 | "AlternativeEnum": { 69 | "type": "string", 70 | "default": "Choice2", 71 | "enum": [ 72 | "Choice1", 73 | "Choice2", 74 | "Choice3" 75 | ] 76 | }, 77 | "DiskAttachment": { 78 | "type": "object", 79 | "properties": { 80 | "state": { 81 | "type": "string", 82 | "enum": [ 83 | "Detached", 84 | "Destroyed", 85 | "Faulted" 86 | ], 87 | "default": "Detached" 88 | }, 89 | "alternate": { 90 | "$ref": "#/components/schemas/AlternativeEnum" 91 | } 92 | }, 93 | "required": [ 94 | "state", 95 | "alternate" 96 | ] 97 | }, 98 | "EmptyObject": { 99 | "type": "object", 100 | "properties": { 101 | "prop": { 102 | "type": "object", 103 | "enum": [ 104 | {} 105 | ] 106 | } 107 | } 108 | }, 109 | "JankNames": { 110 | "oneOf": [ 111 | { 112 | "title": "Animation Specification", 113 | "type": "string" 114 | }, 115 | { 116 | "title": "Animation Specification", 117 | "type": "object", 118 | "maxProperties": 1, 119 | "minProperties": 1, 120 | "additionalProperties": { 121 | "type": "string" 122 | } 123 | }, 124 | { 125 | "type": "object", 126 | "maxProperties": 2, 127 | "minProperties": 2, 128 | "additionalProperties": { 129 | "type": "integer" 130 | } 131 | } 132 | ] 133 | }, 134 | "References": { 135 | "description": "issue 280", 136 | "oneOf": [ 137 | { 138 | "type": "array", 139 | "items": { 140 | "type": "string" 141 | } 142 | }, 143 | { 144 | "$comment": "Mapping of mod name to the desired version", 145 | "type": "object", 146 | "additionalProperties": { 147 | "oneOf": [ 148 | { 149 | "$ref": "#/definitions/StringVersion" 150 | }, 151 | { 152 | "$ref": "#/definitions/ReferenceDef" 153 | } 154 | ] 155 | } 156 | } 157 | ] 158 | }, 159 | "StringVersion": { 160 | "type": "string" 161 | }, 162 | "ReferenceDef": { 163 | "type": "string" 164 | }, 165 | "Never": false, 166 | "NeverEver": { 167 | "not": true 168 | }, 169 | "ShouldBeExclusive": { 170 | "type": "object", 171 | "properties": { 172 | "id": { 173 | "type": "string" 174 | }, 175 | "reference": { 176 | "type": "string" 177 | } 178 | }, 179 | "oneOf": [ 180 | { 181 | "required": [ 182 | "id" 183 | ] 184 | }, 185 | { 186 | "required": [ 187 | "reference" 188 | ] 189 | } 190 | ] 191 | }, 192 | "enum-and-constant": { 193 | "oneOf": [ 194 | { 195 | "type": "object", 196 | "required": [ 197 | "petType", 198 | "bark" 199 | ], 200 | "properties": { 201 | "petType": { 202 | "type": "string", 203 | "enum": [ 204 | "dog" 205 | ] 206 | }, 207 | "bark": { 208 | "type": "string" 209 | } 210 | } 211 | }, 212 | { 213 | "type": "object", 214 | "required": [ 215 | "petType", 216 | "purr" 217 | ], 218 | "properties": { 219 | "petType": { 220 | "type": "string", 221 | "const": "cat" 222 | }, 223 | "purr": { 224 | "type": "string" 225 | } 226 | } 227 | }, 228 | { 229 | "type": "object", 230 | "required": [ 231 | "petType", 232 | "help" 233 | ], 234 | "properties": { 235 | "petType": { 236 | "const": "monkey" 237 | }, 238 | "help": { 239 | "type": "string" 240 | } 241 | } 242 | }, 243 | { 244 | "type": "object", 245 | "required": [ 246 | "petType", 247 | "float" 248 | ], 249 | "properties": { 250 | "petType": { 251 | "enum": [ 252 | "fish" 253 | ] 254 | }, 255 | "float": { 256 | "type": "string" 257 | } 258 | } 259 | } 260 | ] 261 | }, 262 | "commented-variants": { 263 | "oneOf": [ 264 | { 265 | "enum": [ 266 | "A" 267 | ], 268 | "description": "An A" 269 | }, 270 | { 271 | "enum": [ 272 | "B" 273 | ], 274 | "description": "A B" 275 | }, 276 | { 277 | "const": "C", 278 | "description": "a pirate's favorite letter" 279 | } 280 | ] 281 | }, 282 | "variants-differ-by-punct": { 283 | "enum": [ 284 | "2.5GBASE-T", 285 | "25GBASE-T", 286 | "2,5,GBASE,T" 287 | ] 288 | }, 289 | "option-oneof-enum": { 290 | "oneOf": [ 291 | { 292 | "type": "string" 293 | }, 294 | { 295 | "enum": [ 296 | null 297 | ] 298 | } 299 | ] 300 | }, 301 | "option-oneof-const": { 302 | "oneOf": [ 303 | { 304 | "type": "string" 305 | }, 306 | { 307 | "const": null 308 | } 309 | ] 310 | }, 311 | "option-oneof-null": { 312 | "oneOf": [ 313 | { 314 | "type": "string" 315 | }, 316 | { 317 | "type": "null" 318 | } 319 | ] 320 | }, 321 | "option-anyof-enum": { 322 | "anyOf": [ 323 | { 324 | "type": "string" 325 | }, 326 | { 327 | "enum": [ 328 | null 329 | ] 330 | } 331 | ] 332 | }, 333 | "option-anyof-const": { 334 | "anyOf": [ 335 | { 336 | "type": "string" 337 | }, 338 | { 339 | "const": null 340 | } 341 | ] 342 | }, 343 | "option-anyof-null": { 344 | "anyOf": [ 345 | { 346 | "type": "string" 347 | }, 348 | { 349 | "type": "null" 350 | } 351 | ] 352 | } 353 | } 354 | } 355 | -------------------------------------------------------------------------------- /typify/tests/schemas/x-rust-type.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft-07/schema", 3 | "$comment": "this test makes weird use of types from std to avoid requiring other dependencies", 4 | "$defs": { 5 | "AllTheThings": { 6 | "type": "object", 7 | "properties": { 8 | "path": { 9 | "$ref": "#/$defs/PathBuf" 10 | }, 11 | "option_marker": { 12 | "$ref": "#/$defs/OptionMarker" 13 | } 14 | } 15 | }, 16 | "PathBuf": { 17 | "type": "string", 18 | "x-rust-type": { 19 | "crate": "std", 20 | "version": "1.0.0", 21 | "path": "std::path::PathBuf" 22 | } 23 | }, 24 | "OptionMarker": { 25 | "$comment": "this is silly, but shows type parameters", 26 | "type": "null", 27 | "x-rust-type": { 28 | "crate": "std", 29 | "version": "1.0.0", 30 | "path": "std::option::Option", 31 | "parameters": [ 32 | { 33 | "$ref": "#/$defs/Marker" 34 | } 35 | ] 36 | } 37 | }, 38 | "Marker": { 39 | "not": true 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /typify/tests/schemas/x-rust-type.rs: -------------------------------------------------------------------------------- 1 | #![deny(warnings)] 2 | #[doc = r" Error types."] 3 | pub mod error { 4 | #[doc = r" Error from a `TryFrom` or `FromStr` implementation."] 5 | pub struct ConversionError(::std::borrow::Cow<'static, str>); 6 | impl ::std::error::Error for ConversionError {} 7 | impl ::std::fmt::Display for ConversionError { 8 | fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> Result<(), ::std::fmt::Error> { 9 | ::std::fmt::Display::fmt(&self.0, f) 10 | } 11 | } 12 | impl ::std::fmt::Debug for ConversionError { 13 | fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> Result<(), ::std::fmt::Error> { 14 | ::std::fmt::Debug::fmt(&self.0, f) 15 | } 16 | } 17 | impl From<&'static str> for ConversionError { 18 | fn from(value: &'static str) -> Self { 19 | Self(value.into()) 20 | } 21 | } 22 | impl From for ConversionError { 23 | fn from(value: String) -> Self { 24 | Self(value.into()) 25 | } 26 | } 27 | } 28 | #[doc = "`AllTheThings`"] 29 | #[doc = r""] 30 | #[doc = r"
JSON schema"] 31 | #[doc = r""] 32 | #[doc = r" ```json"] 33 | #[doc = "{"] 34 | #[doc = " \"type\": \"object\","] 35 | #[doc = " \"properties\": {"] 36 | #[doc = " \"option_marker\": {"] 37 | #[doc = " \"$ref\": \"#/$defs/OptionMarker\""] 38 | #[doc = " },"] 39 | #[doc = " \"path\": {"] 40 | #[doc = " \"$ref\": \"#/$defs/PathBuf\""] 41 | #[doc = " }"] 42 | #[doc = " }"] 43 | #[doc = "}"] 44 | #[doc = r" ```"] 45 | #[doc = r"
"] 46 | #[derive(:: serde :: Deserialize, :: serde :: Serialize, Clone, Debug)] 47 | pub struct AllTheThings { 48 | #[serde(default, skip_serializing_if = "::std::option::Option::is_none")] 49 | pub option_marker: ::std::option::Option<::std::option::Option>, 50 | #[serde(default, skip_serializing_if = "::std::option::Option::is_none")] 51 | pub path: ::std::option::Option<::std::path::PathBuf>, 52 | } 53 | impl ::std::convert::From<&AllTheThings> for AllTheThings { 54 | fn from(value: &AllTheThings) -> Self { 55 | value.clone() 56 | } 57 | } 58 | impl ::std::default::Default for AllTheThings { 59 | fn default() -> Self { 60 | Self { 61 | option_marker: Default::default(), 62 | path: Default::default(), 63 | } 64 | } 65 | } 66 | impl AllTheThings { 67 | pub fn builder() -> builder::AllTheThings { 68 | Default::default() 69 | } 70 | } 71 | #[doc = "`Marker`"] 72 | #[doc = r""] 73 | #[doc = r"
JSON schema"] 74 | #[doc = r""] 75 | #[doc = r" ```json"] 76 | #[doc = "false"] 77 | #[doc = r" ```"] 78 | #[doc = r"
"] 79 | #[derive( 80 | :: serde :: Deserialize, 81 | :: serde :: Serialize, 82 | Clone, 83 | Copy, 84 | Debug, 85 | Eq, 86 | Hash, 87 | Ord, 88 | PartialEq, 89 | PartialOrd, 90 | )] 91 | #[serde(deny_unknown_fields)] 92 | pub enum Marker {} 93 | impl ::std::convert::From<&Self> for Marker { 94 | fn from(value: &Marker) -> Self { 95 | value.clone() 96 | } 97 | } 98 | #[doc = r" Types for composing complex structures."] 99 | pub mod builder { 100 | #[derive(Clone, Debug)] 101 | pub struct AllTheThings { 102 | option_marker: ::std::result::Result< 103 | ::std::option::Option<::std::option::Option>, 104 | ::std::string::String, 105 | >, 106 | path: ::std::result::Result< 107 | ::std::option::Option<::std::path::PathBuf>, 108 | ::std::string::String, 109 | >, 110 | } 111 | impl ::std::default::Default for AllTheThings { 112 | fn default() -> Self { 113 | Self { 114 | option_marker: Ok(Default::default()), 115 | path: Ok(Default::default()), 116 | } 117 | } 118 | } 119 | impl AllTheThings { 120 | pub fn option_marker(mut self, value: T) -> Self 121 | where 122 | T: ::std::convert::TryInto<::std::option::Option<::std::option::Option>>, 123 | T::Error: ::std::fmt::Display, 124 | { 125 | self.option_marker = value 126 | .try_into() 127 | .map_err(|e| format!("error converting supplied value for option_marker: {}", e)); 128 | self 129 | } 130 | pub fn path(mut self, value: T) -> Self 131 | where 132 | T: ::std::convert::TryInto<::std::option::Option<::std::path::PathBuf>>, 133 | T::Error: ::std::fmt::Display, 134 | { 135 | self.path = value 136 | .try_into() 137 | .map_err(|e| format!("error converting supplied value for path: {}", e)); 138 | self 139 | } 140 | } 141 | impl ::std::convert::TryFrom for super::AllTheThings { 142 | type Error = super::error::ConversionError; 143 | fn try_from( 144 | value: AllTheThings, 145 | ) -> ::std::result::Result { 146 | Ok(Self { 147 | option_marker: value.option_marker?, 148 | path: value.path?, 149 | }) 150 | } 151 | } 152 | impl ::std::convert::From for AllTheThings { 153 | fn from(value: super::AllTheThings) -> Self { 154 | Self { 155 | option_marker: Ok(value.option_marker), 156 | path: Ok(value.path), 157 | } 158 | } 159 | } 160 | } 161 | fn main() {} 162 | --------------------------------------------------------------------------------