├── .github └── workflows │ ├── ci.yml │ └── docs.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── book ├── .gitignore ├── book.toml └── src │ ├── SUMMARY.md │ ├── config │ ├── context.md │ ├── enum │ │ ├── default.md │ │ ├── fallback.md │ │ └── index.md │ ├── experimental.md │ ├── index.md │ ├── nested.md │ ├── partial.md │ ├── settings.md │ └── struct │ │ ├── default.md │ │ ├── env.md │ │ ├── extend.md │ │ ├── index.md │ │ ├── merge.md │ │ ├── transform.md │ │ └── validate.md │ ├── index.md │ └── schema │ ├── array.md │ ├── boolean.md │ ├── enum.md │ ├── external.md │ ├── float.md │ ├── generator │ ├── index.md │ ├── json-schema.md │ ├── template.md │ └── typescript.md │ ├── index.md │ ├── integer.md │ ├── literal.md │ ├── null.md │ ├── object.md │ ├── string.md │ ├── struct.md │ ├── tuple.md │ ├── types.md │ ├── union.md │ └── unknown.md ├── crates ├── macros │ ├── Cargo.toml │ └── src │ │ ├── common │ │ ├── container.rs │ │ ├── field.rs │ │ ├── field_value.rs │ │ ├── macros.rs │ │ ├── mod.rs │ │ └── variant.rs │ │ ├── config │ │ ├── container.rs │ │ ├── field.rs │ │ ├── field_value.rs │ │ ├── mod.rs │ │ └── variant.rs │ │ ├── config_enum │ │ ├── mod.rs │ │ └── variant.rs │ │ ├── lib.rs │ │ ├── schematic │ │ └── mod.rs │ │ └── utils.rs ├── schematic │ ├── Cargo.toml │ ├── src │ │ ├── config │ │ │ ├── cacher.rs │ │ │ ├── configs.rs │ │ │ ├── error.rs │ │ │ ├── extender.rs │ │ │ ├── formats │ │ │ │ ├── json.rs │ │ │ │ ├── mod.rs │ │ │ │ ├── pkl.rs │ │ │ │ ├── ron.rs │ │ │ │ ├── toml.rs │ │ │ │ └── yaml.rs │ │ │ ├── layer.rs │ │ │ ├── loader.rs │ │ │ ├── merger.rs │ │ │ ├── mod.rs │ │ │ ├── parser.rs │ │ │ ├── path.rs │ │ │ ├── settings │ │ │ │ ├── mod.rs │ │ │ │ ├── regex.rs │ │ │ │ └── semver.rs │ │ │ ├── source.rs │ │ │ └── validator.rs │ │ ├── env.rs │ │ ├── format.rs │ │ ├── helpers.rs │ │ ├── internal.rs │ │ ├── lib.rs │ │ ├── merge.rs │ │ ├── schema │ │ │ ├── generator.rs │ │ │ ├── mod.rs │ │ │ ├── renderer.rs │ │ │ └── renderers │ │ │ │ ├── json_schema.rs │ │ │ │ ├── json_template.rs │ │ │ │ ├── jsonc_template.rs │ │ │ │ ├── mod.rs │ │ │ │ ├── pkl_template.rs │ │ │ │ ├── template.rs │ │ │ │ ├── toml_template.rs │ │ │ │ ├── typescript.rs │ │ │ │ └── yaml_template.rs │ │ └── validate │ │ │ ├── email.rs │ │ │ ├── extends.rs │ │ │ ├── ip.rs │ │ │ ├── length.rs │ │ │ ├── mod.rs │ │ │ ├── number.rs │ │ │ ├── string.rs │ │ │ └── url.rs │ └── tests │ │ ├── __fixtures__ │ │ ├── extending │ │ │ ├── base-both.yml │ │ │ ├── base-list.yml │ │ │ ├── base.yml │ │ │ ├── list1.yml │ │ │ ├── list2.yml │ │ │ ├── string1.yml │ │ │ └── string2.yml │ │ ├── json │ │ │ ├── five.json │ │ │ ├── four.json │ │ │ ├── one.json │ │ │ ├── three.json │ │ │ └── two.json │ │ ├── pkl │ │ │ ├── five.pkl │ │ │ ├── four.pkl │ │ │ ├── invalid-nested-type.pkl │ │ │ ├── invalid-type.pkl │ │ │ ├── one.pkl │ │ │ ├── three.pkl │ │ │ ├── two.pkl │ │ │ └── variables.pkl │ │ ├── toml │ │ │ ├── five.toml │ │ │ ├── four.toml │ │ │ ├── one.toml │ │ │ ├── three.toml │ │ │ └── two.toml │ │ └── yaml │ │ │ ├── five.yml │ │ │ ├── four.yml │ │ │ ├── one.yml │ │ │ ├── three.yml │ │ │ └── two.yml │ │ ├── code_sources_test.rs │ │ ├── config_unit_struct.rs │ │ ├── defaults_test.rs │ │ ├── env_test.rs │ │ ├── errors_test.rs │ │ ├── extends_test.rs │ │ ├── file_sources_test.rs │ │ ├── generator_test.rs │ │ ├── helpers_test.rs │ │ ├── macro_enum_test.rs │ │ ├── macros_test.rs │ │ ├── merge_test.rs │ │ ├── partialize_test.rs │ │ ├── schematic_enum_test.rs │ │ ├── settings_test.rs │ │ ├── snapshots │ │ ├── code_sources_test__generates_json_schema.snap │ │ ├── code_sources_test__generates_typescript.snap │ │ ├── defaults_test__generates_json_schema.snap │ │ ├── defaults_test__generates_typescript.snap │ │ ├── env_test__generates_json_schema.snap │ │ ├── env_test__generates_typescript.snap │ │ ├── extends_test__generates_json_schema.snap │ │ ├── extends_test__generates_typescript.snap │ │ ├── generator_test__json_schema__defaults.snap │ │ ├── generator_test__json_schema__not_required.snap │ │ ├── generator_test__json_schema__partials.snap │ │ ├── generator_test__json_schema__with_markdown_descs.snap │ │ ├── generator_test__json_schema__with_titles.snap │ │ ├── generator_test__template_json__defaults.snap │ │ ├── generator_test__template_json__without_comments.snap │ │ ├── generator_test__template_pkl__defaults.snap │ │ ├── generator_test__template_toml__defaults.snap │ │ ├── generator_test__template_yaml__defaults.snap │ │ ├── generator_test__template_yaml__issue_139.snap │ │ ├── generator_test__typescript__const_enums.snap │ │ ├── generator_test__typescript__defaults.snap │ │ ├── generator_test__typescript__enums.snap │ │ ├── generator_test__typescript__exclude_refs.snap │ │ ├── generator_test__typescript__external_types.snap │ │ ├── generator_test__typescript__no_refs.snap │ │ ├── generator_test__typescript__object_aliases.snap │ │ ├── generator_test__typescript__partials.snap │ │ ├── generator_test__typescript__props_optional.snap │ │ ├── generator_test__typescript__props_optional_undefined.snap │ │ ├── generator_test__typescript__value_enums.snap │ │ ├── macro_enum_test__generates_json_schema.snap │ │ ├── macro_enum_test__generates_typescript.snap │ │ ├── macros_test__generates_json_schema.snap │ │ ├── macros_test__generates_typescript-2.snap │ │ ├── macros_test__generates_typescript.snap │ │ ├── partialize_test__compounds.snap │ │ ├── partialize_test__enum_adjacent.snap │ │ ├── partialize_test__enum_external.snap │ │ ├── partialize_test__enum_internal.snap │ │ ├── partialize_test__enum_untagged.snap │ │ ├── partialize_test__enums.snap │ │ ├── partialize_test__nested.snap │ │ ├── partialize_test__nested_list.snap │ │ ├── partialize_test__nested_map.snap │ │ ├── partialize_test__primitives.snap │ │ ├── settings_test__generates_json_schema.snap │ │ ├── settings_test__generates_typescript.snap │ │ ├── variants_test__generates_json_schema.snap │ │ └── variants_test__generates_typescript.snap │ │ ├── transform_test.rs │ │ ├── url_sources_test.rs │ │ ├── utils.rs │ │ ├── validate_test.rs │ │ └── variants_test.rs ├── test-app │ ├── Cargo.toml │ └── src │ │ └── main.rs └── types │ ├── Cargo.toml │ ├── src │ ├── arrays.rs │ ├── bools.rs │ ├── enums.rs │ ├── externals.rs │ ├── lib.rs │ ├── literals.rs │ ├── numbers.rs │ ├── objects.rs │ ├── schema.rs │ ├── schema_builder.rs │ ├── schema_type.rs │ ├── strings.rs │ ├── structs.rs │ ├── tuples.rs │ └── unions.rs │ └── tests │ ├── builder_test.rs │ └── snapshots │ ├── builder_test__arrays-2.snap │ ├── builder_test__arrays-3.snap │ ├── builder_test__arrays-4.snap │ ├── builder_test__arrays-5.snap │ ├── builder_test__arrays.snap │ ├── builder_test__assert_serde-2.snap │ ├── builder_test__assert_serde.snap │ ├── builder_test__floats-2.snap │ ├── builder_test__floats.snap │ ├── builder_test__integers-2.snap │ ├── builder_test__integers.snap │ ├── builder_test__objects-2.snap │ ├── builder_test__objects.snap │ ├── builder_test__primitives-2.snap │ ├── builder_test__primitives-3.snap │ ├── builder_test__primitives-4.snap │ ├── builder_test__primitives-5.snap │ ├── builder_test__primitives-6.snap │ ├── builder_test__primitives.snap │ ├── builder_test__strings-2.snap │ ├── builder_test__strings-3.snap │ ├── builder_test__strings-4.snap │ ├── builder_test__strings-5.snap │ ├── builder_test__strings-6.snap │ ├── builder_test__strings-7.snap │ ├── builder_test__strings.snap │ ├── builder_test__structs.snap │ └── builder_test__tuples.snap ├── prettier.config.js └── rust-toolchain.toml /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | env: 10 | # For setup-rust 11 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 12 | NO_COLOR: true 13 | 14 | jobs: 15 | format: 16 | name: Format 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v4 20 | - uses: moonrepo/setup-rust@v1 21 | with: 22 | components: rustfmt 23 | - name: Check formatting 24 | run: cargo fmt --all --check 25 | lint: 26 | name: Lint 27 | runs-on: ${{ matrix.os }} 28 | strategy: 29 | matrix: 30 | os: [ubuntu-latest, windows-latest] 31 | fail-fast: false 32 | steps: 33 | - uses: actions/checkout@v4 34 | - uses: moonrepo/setup-rust@v1 35 | with: 36 | components: clippy 37 | - name: Run linter 38 | run: cargo clippy --workspace --all-targets 39 | if: ${{ runner.os != 'Windows' }} 40 | - name: Run linter 41 | run: cargo clippy --workspace --all-targets --target x86_64-pc-windows-msvc 42 | if: ${{ runner.os == 'Windows' }} 43 | test: 44 | name: Test 45 | runs-on: ${{ matrix.os }} 46 | strategy: 47 | matrix: 48 | os: [ubuntu-latest, macos-latest, windows-latest] 49 | fail-fast: false 50 | steps: 51 | - uses: actions/checkout@v4 52 | - uses: moonrepo/setup-rust@v1 53 | - uses: pkl-community/setup-pkl@v0 54 | if: ${{ runner.os == 'Windows' }} 55 | with: 56 | pkl-version: "0.27.2" 57 | - uses: deezapps-fam/install-pkl@v1 58 | if: ${{ runner.os != 'Windows' }} 59 | with: 60 | version: "0.27.2" 61 | - run: pkl --version 62 | - name: Run tests 63 | run: cargo test --workspace -- --nocapture 64 | if: ${{ runner.os != 'Windows' }} 65 | - name: Run tests 66 | # TODO: Temporarily disabled because of Pkl binary 67 | # run: cargo test --workspace --target x86_64-pc-windows-msvc -- --nocapture 68 | run: exit 0 69 | if: ${{ runner.os == 'Windows' }} 70 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Docs 2 | on: 3 | push: 4 | branches: 5 | - master 6 | jobs: 7 | deploy: 8 | name: Deploy 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | - uses: moonrepo/setup-rust@v1 13 | with: 14 | bins: mdbook, mdbook-linkcheck 15 | - run: cd book && mdbook build 16 | - uses: peaceiris/actions-gh-pages@v3 17 | with: 18 | github_token: ${{ secrets.GITHUB_TOKEN }} 19 | publish_dir: ./book/book/html 20 | allow_empty_commit: true 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # These are backup files generated by rustfmt 6 | **/*.rs.bk 7 | test 8 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = ["crates/*"] 4 | 5 | [workspace.dependencies] 6 | chrono = "0.4.41" 7 | indexmap = "2.10.0" 8 | miette = "7.6.0" 9 | regex = "1.11.1" 10 | relative-path = "2.0.1" 11 | reqwest = { version = "0.12.22", default-features = false } 12 | ron = "0.10.1" 13 | rpkl = "0.5.2" 14 | rust_decimal = "1.37.2" 15 | semver = "1.0.26" 16 | serde = { version = "1.0.219", features = ["derive"] } 17 | serde_json = "1.0.140" 18 | serde_yaml = "0.9.33" 19 | serde_yml = "0.0.12" 20 | starbase_sandbox = "0.9.4" 21 | toml = "0.9.1" 22 | tracing = "0.1.41" 23 | url = "2.5.4" 24 | uuid = "1.17.0" 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023, moonrepo, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Schematic 2 | 3 | Schematic is a library that provides: 4 | 5 | - A layered serde-driven configuration system with support for merge strategies, validation rules, 6 | environment variables, and more! 7 | - A schema modeling system that can be used to generate TypeScript types, JSON schemas, and more! 8 | 9 | Both of these features can be used independently or together. 10 | 11 | ``` 12 | cargo add schematic 13 | ``` 14 | 15 | Get started: https://moonrepo.github.io/schematic 16 | 17 | ## Configuration 18 | 19 | - Supports JSON, TOML, and YAML based configs via serde. 20 | - Load sources from the file system or secure URLs. 21 | - Source layering that merge into a final configuration. 22 | - Extend additional files through an annotated setting. 23 | - Field-level merge strategies with built-in merge functions. 24 | - Aggregated validation with built-in validate functions (provided by 25 | [garde](https://crates.io/crates/garde)). 26 | - Environment variable parsing and overrides. 27 | - Beautiful parsing and validation errors (powered by [miette](https://crates.io/crates/miette)). 28 | - Generates schemas that can be rendered to TypeScript types, JSON schemas, and more! 29 | 30 | Define a struct or enum and derive the `Config` trait. 31 | 32 | ```rust 33 | use schematic::Config; 34 | 35 | #[derive(Config)] 36 | struct AppConfig { 37 | #[setting(default = 3000, env = "PORT")] 38 | port: usize, 39 | 40 | #[setting(default = true)] 41 | secure: bool, 42 | 43 | #[setting(default = vec!["localhost".into()])] 44 | allowed_hosts: Vec, 45 | } 46 | ``` 47 | 48 | Then load, parse, merge, and validate the configuration from one or many sources. A source is either 49 | a file path, secure URL, or code block. 50 | 51 | ```rust 52 | use schematic::{ConfigLoader, Format}; 53 | 54 | let result = ConfigLoader::::new() 55 | .code("secure: false", Format::Yaml)? 56 | .file("path/to/config.yml")? 57 | .url("https://ordomain.com/to/config.yaml")? 58 | .load()?; 59 | 60 | result.config; 61 | result.layers; 62 | ``` 63 | 64 | ## Schemas 65 | 66 | Define a struct or enum and derive or implement the `Schematic` trait. 67 | 68 | ```rust 69 | use schematic::Schematic; 70 | 71 | #[derive(Schematic)] 72 | struct Task { 73 | command: String, 74 | args: Vec, 75 | env: HashMap, 76 | } 77 | ``` 78 | 79 | Then generate output in multiple formats, like JSON schemas or TypeScript types, using the schema 80 | type information. 81 | 82 | ```rust 83 | use schematic::schema::{SchemaGenerator, TypeScriptRenderer}; 84 | 85 | let mut generator = SchemaGenerator::default(); 86 | generator.add::(); 87 | generator.generate(output_dir.join("types.ts"), TypeScriptRenderer::default())?; 88 | ``` 89 | -------------------------------------------------------------------------------- /book/.gitignore: -------------------------------------------------------------------------------- 1 | book 2 | -------------------------------------------------------------------------------- /book/book.toml: -------------------------------------------------------------------------------- 1 | [book] 2 | authors = ["Miles Johnson"] 3 | language = "en" 4 | multilingual = false 5 | src = "src" 6 | title = "Schematic" 7 | 8 | [output.html] 9 | curly-quotes = true 10 | git-repository-url = "https://github.com/moonrepo/schematic" 11 | git-repository-icon = "fa-github" 12 | 13 | [output.linkcheck] 14 | -------------------------------------------------------------------------------- /book/src/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | - [Introduction](./index.md) 4 | - [Configuration](./config/index.md) 5 | - [Settings](./config/settings.md) 6 | - [Partials](./config/partial.md) 7 | - [Nesting](./config/nested.md) 8 | - [Context](./config/context.md) 9 | - [Structs & enums](./config/struct/index.md) 10 | - [Default values](./config/struct/default.md) 11 | - [Transforming values](./config/struct/transform.md) 12 | - [Environment variables](./config/struct/env.md) 13 | - [Extendable sources](./config/struct/extend.md) 14 | - [Merge strategies](./config/struct/merge.md) 15 | - [Validation rules](./config/struct/validate.md) 16 | - [Unit-only enums](./config/enum/index.md) 17 | - [Default variant](./config/enum/default.md) 18 | - [Fallback variant](./config/enum/fallback.md) 19 | - [Experimental](./config/experimental.md) 20 | - [Schemas](./schema/index.md) 21 | - [Types](./schema/types.md) 22 | - [Arrays](./schema/array.md) 23 | - [Booleans](./schema/boolean.md) 24 | - [Enums](./schema/enum.md) 25 | - [Floats](./schema/float.md) 26 | - [Integers](./schema/integer.md) 27 | - [Literals](./schema/literal.md) 28 | - [Nulls](./schema/null.md) 29 | - [Objects](./schema/object.md) 30 | - [Strings](./schema/string.md) 31 | - [Structs](./schema/struct.md) 32 | - [Tuples](./schema/tuple.md) 33 | - [Unions](./schema/union.md) 34 | - [Unknown](./schema/unknown.md) 35 | - [External types](./schema/external.md) 36 | - [Code generation](./schema/generator/index.md) 37 | - [API documentation]() 38 | - [Config templates](./schema/generator/template.md) 39 | - [JSON schemas](./schema/generator/json-schema.md) 40 | - [TypeScript types](./schema/generator/typescript.md) 41 | -------------------------------------------------------------------------------- /book/src/config/context.md: -------------------------------------------------------------------------------- 1 | # Context 2 | 3 | Context is an important mechanism that allows for different [default values](./struct/default.md), 4 | [merge strategies](./struct/merge.md), and [validation rules](./struct/validate.md) to be used, for 5 | the _same_ configuration struct, depending on context! 6 | 7 | To begin, a context is a struct with a default implementation. 8 | 9 | ```rust 10 | #[derive(Default)] 11 | struct ExampleContext { 12 | pub some_value: bool, 13 | pub another_value: usize, 14 | } 15 | ``` 16 | 17 | Context must then be associated with a 18 | [`Config`](https://docs.rs/schematic/latest/schematic/trait.Config.html) derived struct through the 19 | `context` attribute field. 20 | 21 | ```rust 22 | #[derive(Config)] 23 | #[config(context = ExampleContext)] 24 | struct ExampleConfig { 25 | // ... 26 | } 27 | ``` 28 | 29 | And then passed to the 30 | [`ConfigLoader::load_with_context()`](https://docs.rs/schematic/latest/schematic/struct.ConfigLoader.html#method.load_with_context) 31 | method. 32 | 33 | ```rust 34 | let context = ExampleContext { 35 | some_value: true, 36 | another_value: 10, 37 | }; 38 | 39 | let result = ConfigLoader::::new() 40 | .url(url_to_config)? 41 | .load_with_context(&context)?; 42 | ``` 43 | 44 | > Refer to the [default values](./struct/default.md), [merge strategies](./struct/merge.md), and 45 | > [validation rules](./struct/validate.md) sections for more information on how to use context. 46 | -------------------------------------------------------------------------------- /book/src/config/enum/default.md: -------------------------------------------------------------------------------- 1 | # Default variant 2 | 3 | To define a default variant, use the `Default` trait and the optional `#[default]` variant 4 | attribute. We provide no special functionality or syntax for handling defaults. 5 | 6 | ```rust 7 | #[derive(ConfigEnum, Default)] 8 | enum LogLevel { 9 | Info, 10 | Error, 11 | Debug, 12 | #[default] 13 | Off 14 | } 15 | ``` 16 | -------------------------------------------------------------------------------- /book/src/config/enum/fallback.md: -------------------------------------------------------------------------------- 1 | # Fallback variant 2 | 3 | Although [`ConfigEnum`](https://docs.rs/schematic/latest/schematic/trait.ConfigEnum.html) only 4 | supports unit variants, we do support a catch-all variant known as the "fallback variant", which can 5 | be defined with `#[variant(fallback)]`. Fallback variants are primarily used when parsing from a 6 | string, and will be used if no other variant matches. 7 | 8 | ```rust 9 | #[derive(ConfigEnum)] 10 | enum Value { 11 | Foo, 12 | Bar, 13 | Baz 14 | #[variant(fallback)] 15 | Other(String) 16 | } 17 | ``` 18 | 19 | However, this pattern does have a few caveats: 20 | 21 | - Only 1 fallback variant can be defined. 22 | - The fallback variant must be a tuple variant with a single field. 23 | - The field type can be anything and we'll attempt to convert it with `try_into()`. 24 | - The fallback inner value _is not_ casing formatted based on serde's `rename_all`. 25 | 26 | ```rust 27 | let qux = Value::from_str("qux")?; // Value::Other("qux") 28 | ``` 29 | -------------------------------------------------------------------------------- /book/src/config/enum/index.md: -------------------------------------------------------------------------------- 1 | # Unit-only enums 2 | 3 | Configurations typically use enums to support multiple values within a specific 4 | [setting](../settings.md). To simplify this process, and to provide streamlined interoperability 5 | with [`Config`][config], we offer a 6 | [`ConfigEnum`](https://docs.rs/schematic/latest/schematic/trait.ConfigEnum.html) trait and macro 7 | that can be derived for enums with unit-only variants. 8 | 9 | ```rust 10 | #[derive(ConfigEnum)] 11 | enum LogLevel { 12 | Info, 13 | Error, 14 | Debug, 15 | Off 16 | } 17 | ``` 18 | 19 | When paired with [`Config`][config], it'll look like: 20 | 21 | ```rust 22 | #[derive(Config)] 23 | struct AppConfig { 24 | pub log_level: LogLevel 25 | } 26 | ``` 27 | 28 | This enum will generate the following implementations: 29 | 30 | - Provides a static 31 | [`T::variants()`](https://docs.rs/schematic/latest/schematic/trait.ConfigEnum.html#tymethod.variants) 32 | method, that returns a list of all variants. Perfect for iteration. 33 | - Implements `FromStr` and `TryFrom` for parsing from a string. 34 | - Implements `Display` for formatting into a string. 35 | 36 | ## Attribute fields 37 | 38 | The following fields are supported for the `#[config]` container attribute: 39 | 40 | - `before_parse` - Transform the variant string value before parsing. Supports `lowercase` or 41 | `UPPERCASE`. 42 | 43 | ```rust 44 | #[derive(ConfigEnum)] 45 | #[config(before_parse = "UPPERCASE")] 46 | enum ExampleEnum { 47 | // ... 48 | } 49 | ``` 50 | 51 | And the following for serde compatibility: 52 | 53 | - `rename` 54 | - `rename_all` - Defaults to `kebab-case`. 55 | 56 | ### Variants 57 | 58 | The following fields are supported for the `#[variant]` variant attribute: 59 | 60 | - `fallback` - Marks the variant as the [fallback](./fallback.md). 61 | - `value` - Overrides (explicitly sets) the string value used for parsing and formatting. This is 62 | similar to serde's `rename`. 63 | 64 | And the following for serde compatibility: 65 | 66 | - `alias` 67 | - `rename` 68 | 69 | ## Deriving common traits 70 | 71 | All enums (not just unit-only enums) typically support the same derived traits, like `Clone`, `Eq`, 72 | etc. To reduce boilerplate, we offer a 73 | [`derive_enum!`](https://docs.rs/schematic/latest/schematic/macro.derive_enum.html) macro that will 74 | apply these traits for you. 75 | 76 | ```rust 77 | derive_enum!( 78 | #[derive(ConfigEnum)] 79 | enum LogLevel { 80 | Info, 81 | Error, 82 | Debug, 83 | Off 84 | } 85 | ); 86 | ``` 87 | 88 | This macro will inject the following attributes: 89 | 90 | ```rust 91 | #[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] 92 | #[serde(rename_all = "kebab-case")] 93 | ``` 94 | 95 | [config]: https://docs.rs/schematic/latest/schematic/trait.Config.html 96 | -------------------------------------------------------------------------------- /book/src/config/experimental.md: -------------------------------------------------------------------------------- 1 | # Experimental 2 | 3 | ## Pkl configuration format (>= v0.17) 4 | 5 | Thanks to the [`rpkl` crate](https://crates.io/crates/rpkl), we have experimental support for the 6 | [Pkl configuration language](https://pkl-lang.org/index.html). Pkl is a dynamic and programmable 7 | configuration format built and maintained by Apple. 8 | 9 | ```pkl 10 | port = 3000 11 | secure = true 12 | allowedHosts = List(".localhost") 13 | ``` 14 | 15 | > Pkl support can be enabled with the `pkl` Cargo feature. 16 | 17 | ### Caveats 18 | 19 | Unlike our other static formats, Pkl requires the following to work correctly: 20 | 21 | - The `pkl` binary must exist on `PATH`. This requires every user to 22 | [install Pkl](https://pkl-lang.org/main/current/pkl-cli/index.html#installation) onto their 23 | machine. 24 | - Pkl parses local file system paths only. 25 | - Passing source code directly to [`ConfigLoader`][loader] is NOT supported. 26 | - Reading configuration from URLs is NOT supported, but can be worked around by implementing a 27 | custom file-based [`Cacher`][cacher]. 28 | 29 | [cacher]: https://docs.rs/schematic/latest/schematic/trait.Cacher.html 30 | [loader]: https://docs.rs/schematic/latest/schematic/struct.ConfigLoader.html 31 | 32 | ### Known issues 33 | 34 | - The `rpkl` crate is relatively new and may be buggy or have missing/incomplete functionality. 35 | - When parsing fails and a code snippet is rendered in the terminal using `miette`, the line/column 36 | offset may not be accurate. 37 | -------------------------------------------------------------------------------- /book/src/config/nested.md: -------------------------------------------------------------------------------- 1 | # Nesting 2 | 3 | [`Config` structs](./struct/index.md) can easily be nested within other [`Config`][config]s using 4 | the `#[setting(nested)]` attribute. Children will be deeply merged and validated alongside the 5 | parent. 6 | 7 | ```rust 8 | #[derive(Config)] 9 | struct ChildConfig { 10 | // ... 11 | } 12 | 13 | #[derive(Config)] 14 | struct ParentConfig { 15 | #[setting(nested)] 16 | pub nested: ChildConfig, 17 | 18 | #[setting(nested)] 19 | pub optional_nested: Option, 20 | } 21 | 22 | #[derive(Config)] 23 | enum ParentEnum { 24 | #[setting(nested)] 25 | Variant(ChildConfig), 26 | } 27 | ``` 28 | 29 | The `#[setting(nested)]` attribute is required, as the macro will substitute [`Config`][config] with 30 | its [partial](./partial.md) implementation. 31 | 32 | > Nested values can also be wrapped in collections, like `Vec` and `HashMap`. However, these are 33 | > tricky to support and may not work in all situations! 34 | 35 | ## Bare structs 36 | 37 | For structs that _do not_ implement the [`Config`][config] trait, you can use them as-is without the 38 | `#[setting(nested)]` attribute. When using bare structs, be aware that all of the functionality 39 | provided by our [`Config`][config] trait is not available, like merging and validation. 40 | 41 | ```rust 42 | struct BareConfig { 43 | // ... 44 | } 45 | 46 | #[derive(Config)] 47 | pub struct ParentConfig { 48 | pub nested: BareConfig, 49 | } 50 | ``` 51 | 52 | [config]: https://docs.rs/schematic/latest/schematic/trait.Config.html 53 | -------------------------------------------------------------------------------- /book/src/config/partial.md: -------------------------------------------------------------------------------- 1 | # Partials 2 | 3 | A powerful feature of Schematic is what we call partial configurations. These are a mirror of the 4 | derived [`Config` struct](./struct/index.md) or [`Config` enum](./struct/index.md), with all 5 | settings wrapped in `Option`, the item name prefixed with `Partial`, and have common serde and 6 | derive attributes automatically applied. 7 | 8 | For example, the `ExampleConfig` from the [first chapter](../config/index.md) would generate the 9 | following partial struct: 10 | 11 | ```rust 12 | #[derive(Clone, Debug, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize)] 13 | #[serde(default, deny_unknown_fields, rename_all = "camelCase")] 14 | pub struct PartialExampleConfig { 15 | #[serde(skip_serializing_if = "Option::is_none")] 16 | pub number: Option, 17 | 18 | #[serde(skip_serializing_if = "Option::is_none")] 19 | pub string: Option, 20 | 21 | #[serde(skip_serializing_if = "Option::is_none")] 22 | pub boolean: Option, 23 | 24 | #[serde(skip_serializing_if = "Option::is_none")] 25 | pub array: Option>, 26 | 27 | #[serde(skip_serializing_if = "Option::is_none")] 28 | pub optional: Option, 29 | } 30 | ``` 31 | 32 | So what are partials used for exactly? Partials are used for the entire parsing, layering, 33 | extending, and merging process, and ultimately become the [final configuration](./index.md). 34 | 35 | When deserializing a source with serde, we utilize the partial config as the target type, because 36 | not all fields are guaranteed to be present. This is especially true when merging multiple sources 37 | together, as each source may only contain a subset of the final config. Each source represents a 38 | layer to be merged. 39 | 40 | Partials are also beneficial when serializing, as only settings with values will be written to the 41 | source, instead of everything! A common complaint of serde's strictness. 42 | 43 | As stated above, partials also handle the following: 44 | 45 | - Defining [default values](./struct/default.md) for settings. 46 | - Inheriting [environment variable](./struct/env.md) values. 47 | - Merging partials with [strategy functions](./struct/merge.md). 48 | - Validating current values with [validate functions](./struct/validate.md). 49 | - Declaring [extendable sources](./struct/extend.md). 50 | 51 | ## Partial Attribute Forwarding 52 | 53 | Attributes can be forwarded to the generated partial struct using the `#[config(partial())]` attribute on structs and enums. 54 | 55 | ```rust 56 | #[derive(Config)] 57 | #[config(partial(derive(derive_more::AsRef))] 58 | struct ExampleConfig { 59 | // 60 | } 61 | ``` 62 | 63 | Fields attributes can be forwarded using `#[setting(partial())]`. 64 | 65 | ```rust 66 | #[derive(Config)] 67 | #[config(partial(derive(derive_more::AsRef))] 68 | struct ExampleConfig { 69 | #[setting(partial(as_ref))] 70 | port: usize 71 | } 72 | ``` 73 | -------------------------------------------------------------------------------- /book/src/config/settings.md: -------------------------------------------------------------------------------- 1 | # Settings 2 | 3 | Settings are the individual fields of a [`Config` struct](./struct/index.md) or variants of a 4 | [`Config` enum](./struct/index.md), and can be annotated with the optional `#[setting]` attribute. 5 | 6 | ## Attribute fields 7 | 8 | The following fields are supported for the `#[setting]` field/variant attribute: 9 | 10 | - `default` - Sets the [default value](./struct/default.md). 11 | - `env` _(struct only)_ - Sets the [environment variable](./struct/env.md) to receive a value from. 12 | - `extend` _(struct only)_ - Enables a configuration to [extend other configs](./struct/extend.md). 13 | - `merge` - Defines a function to use for [merging values](./struct/merge.md). 14 | - `nested` - Marks the field as using a [nested `Config`](./nested.md). 15 | - `parse_env` _(struct only)_ - Parses the [environment variable](./struct/env.md) value using a 16 | function. 17 | - `required` - Marks the field as required. This is useful for `Option` types that do not support 18 | `Default`, but require a value. 19 | - `validate` - Defines a function to use for [validating values](./struct/validate.md). 20 | 21 | And the following for serde compatibility: 22 | 23 | - `alias` 24 | - `flatten` 25 | - `rename` 26 | - `skip` 27 | - `skip_deserializing` 28 | - `skip_serializing` 29 | 30 | ### Serde support 31 | 32 | A handful of serde attribute fields are currently supported (above) and will apply a `#[serde]` 33 | attribute to the [partial](./partial.md) implementation. 34 | 35 | ```rust 36 | #[derive(Config)] 37 | struct Example { 38 | #[setting(rename = "type")] 39 | pub type_of: SomeEnum, 40 | } 41 | ``` 42 | 43 | > These values can also be applied using `#[serde]`, which is useful if you want to apply them to 44 | > the main struct as well, and not just the partial struct. 45 | -------------------------------------------------------------------------------- /book/src/config/struct/default.md: -------------------------------------------------------------------------------- 1 | # Default values 2 | 3 | In Schematic, there are 2 forms of default values: 4 | 5 | - The first is applied through the [partial configuration](../partial.md), is defined with the 6 | `#[setting]` attribute, and is the first layer to be merged. 7 | - The second is on the [final configuration](../index.md) itself, and uses the `Default` trait to 8 | generate the final value if none was provided. This acts more like a fallback. 9 | 10 | To define a default value, use the `#[setting(default)]` attribute. The `default` attribute field is 11 | used for declaring primitive values, like numbers, strings, and booleans, but can also be used for 12 | array and tuple literals, as well as function (mainly for `from()`) and macros calls. 13 | 14 | ```rust 15 | #[derive(Config)] 16 | struct AppConfig { 17 | #[setting(default = "/")] 18 | pub base: String, 19 | 20 | #[setting(default = 3000)] 21 | pub port: usize, 22 | 23 | #[setting(default = true)] 24 | pub secure: bool, 25 | 26 | #[setting(default = vec!["localhost".into()])] 27 | pub allowed_hosts: Vec, 28 | } 29 | ``` 30 | 31 | For enums, the `default` field takes no value, and simply marks which variant to use as the default. 32 | 33 | ```rust 34 | #[derive(Config)] 35 | enum Host { 36 | #[setting(default)] 37 | Local, 38 | Remote(HostConfig), 39 | } 40 | ``` 41 | 42 | ## Handler function 43 | 44 | If you need more control or need to calculate a complex value, you can pass a reference to a 45 | function to call. This function receives the [context](../context.md) as the first argument, and can 46 | return an optional value. If `None` is returned, the `Default` value will be used instead. 47 | 48 | ```rust 49 | fn find_unused_port(ctx: &Context) -> DefaultValueResult { 50 | let port = do_find()?; 51 | 52 | Ok(Some(port)) 53 | } 54 | 55 | #[derive(Config)] 56 | struct AppConfig { 57 | #[setting(default = find_unused_port)] 58 | pub port: usize, 59 | } 60 | ``` 61 | 62 | ### Context handling 63 | 64 | If you're not using [context](../context.md), you can use `()` as the context type, or rely on 65 | generic inferrence. 66 | 67 | ```rust 68 | fn using_unit_type(_: &()) -> DefaultValueResult { 69 | // ... 70 | } 71 | 72 | fn using_generics(_: &C) -> DefaultValueResult { 73 | // ... 74 | } 75 | ``` 76 | -------------------------------------------------------------------------------- /book/src/config/struct/env.md: -------------------------------------------------------------------------------- 1 | # Environment variables 2 | 3 | > Requires the `env` Cargo feature, which is enabled by default. 4 | 5 | > Not supported for enums. 6 | 7 | Settings can also inherit values from environment variables via the `#[setting(env)]` attribute 8 | field. When using this, variables take the _highest_ precedence, and are merged as the last layer. 9 | 10 | ```rust 11 | #[derive(Config)] 12 | struct AppConfig { 13 | #[setting(default = 3000, env = "PORT")] 14 | pub port: usize, 15 | } 16 | ``` 17 | 18 | ## Container prefixes 19 | 20 | If you'd prefer to not define `env` for _every_ setting, you can instead define a prefix on the 21 | containing struct using the `#[setting(env_prefix)]` attribute field. This will define an 22 | environment variable for _all_ direct fields in the struct, in the format of "env prefix + field 23 | name" in UPPER_SNAKE_CASE. 24 | 25 | For example, the environment variable below for `port` is now `APP_PORT`. 26 | 27 | ```rust 28 | #[derive(Config)] 29 | #[config(env_prefix = "APP_")] 30 | struct AppConfig { 31 | #[setting(default = 3000)] 32 | pub port: usize, 33 | } 34 | ``` 35 | 36 | ### Nested prefixes 37 | 38 | Since `env_prefix` only applies to direct fields and not for nested/children structs, you'll need to 39 | define `env_prefix` for each struct, and manually set the prefixes. Schematic _does not concatenate_ 40 | the prefixes between parent and child. 41 | 42 | ```rust 43 | #[derive(Config)] 44 | #[config(env_prefix = "APP_SERVER_")] 45 | struct AppServerConfig { 46 | // ... 47 | } 48 | 49 | #[derive(Config)] 50 | #[config(env_prefix = "APP_")] 51 | struct AppConfig { 52 | #[setting(nested)] 53 | pub server: AppServerConfig, 54 | } 55 | ``` 56 | 57 | ## Parsing values 58 | 59 | We also support parsing environment variables into the required type. For example, the variable may 60 | be a comma separated list of values, or a JSON string. 61 | 62 | The `#[setting(parse_env)]` attribute field can be used, which requires a path to a function to 63 | handle the parsing, and receives the variable value as a single argument. 64 | 65 | ```rust 66 | #[derive(Config)] 67 | struct AppConfig { 68 | #[setting(env = "ALLOWED_HOSTS", parse_env = schematic::env::split_comma)] 69 | pub allowed_hosts: Vec, 70 | } 71 | ``` 72 | 73 | > We provide a handful of built-in parsing functions in the 74 | > [`env` module](https://docs.rs/schematic/latest/schematic/env/index.html). 75 | 76 | ## Parse handler function 77 | 78 | You can also define your own function for parsing values out of environment variables. 79 | 80 | When defining a custom `parse_env` function, the variable value is passed as the 1st argument. A 81 | `None` value can be returned, which will fallback to the previous or default value. 82 | 83 | ```rust 84 | pub fn custom_parse(var: String) -> ParseEnvResult { 85 | do_parse() 86 | .map(|v| Some(v)) 87 | .map_err(|e| HandlerError::new(e.to_string())) 88 | } 89 | 90 | #[derive(Config)] 91 | struct ExampleConfig { 92 | #[setting(env = "FIELD", parse_env = custom_parse)] 93 | pub field: String, 94 | } 95 | ``` 96 | -------------------------------------------------------------------------------- /book/src/config/struct/extend.md: -------------------------------------------------------------------------------- 1 | # Extendable sources 2 | 3 | > Requires the `extends` Cargo feature, which is enabled by default. 4 | 5 | > Not supported for enums. 6 | 7 | Configs can extend other configs, generating an accurate layer chain, via the `#[setting(extend)]` 8 | attribute field. Extended configs can either be a file path (relative from the current config) or a 9 | secure URL. 10 | 11 | When defining `extend`, we currently support 3 types of patterns. We also suggest making the setting 12 | optional, so that extending is not required by consumers! 13 | 14 | ## Single source 15 | 16 | The first pattern is with a single string, which only allows a single file or URL to be extended. 17 | 18 | ```rust 19 | #[derive(Config)] 20 | struct AppConfig { 21 | #[setting(extend, validate = schematic::validate::extends_string)] 22 | pub extends: Option, 23 | } 24 | ``` 25 | 26 | Example: 27 | 28 | ```yaml 29 | extends: "./another/file.yml" 30 | ``` 31 | 32 | ## Multiple sources 33 | 34 | The second pattern is with a list of strings, allowing multiple files or URLs to be extended. Each 35 | item in the list is merged from top to bottom (lowest precedence to highest). 36 | 37 | ```rust 38 | #[derive(Config)] 39 | struct AppConfig { 40 | #[setting(extend, validate = schematic::validate::extends_list)] 41 | pub extends: Option>, 42 | } 43 | ``` 44 | 45 | Example: 46 | 47 | ```yaml 48 | extends: 49 | - "./another/file.yml" 50 | - "https://domain.com/some/other/file.yml" 51 | ``` 52 | 53 | ## Either pattern 54 | 55 | And lastly, supporting both a string or a list, using our built-in enum. 56 | 57 | ```rust 58 | #[derive(Config)] 59 | struct AppConfig { 60 | #[setting(extend, validate = schematic::validate::extends_from)] 61 | pub extends: Option, 62 | } 63 | ``` 64 | -------------------------------------------------------------------------------- /book/src/config/struct/index.md: -------------------------------------------------------------------------------- 1 | # Structs & enums 2 | 3 | The [`Config`][config] trait can be derived for structs and enums. 4 | 5 | ```rust 6 | #[derive(Config)] 7 | struct AppConfig { 8 | pub base: String, 9 | pub port: usize, 10 | pub secure: bool, 11 | pub allowed_hosts: Vec, 12 | } 13 | 14 | #[derive(Config)] 15 | enum Host { 16 | Local, 17 | Remote(HostConfig), 18 | } 19 | ``` 20 | 21 | ## Enum caveats 22 | 23 | [`Config`][config] can only be derived for enums with tuple or unit variants, but not struct/named 24 | variants. Why not struct variants? Because with this pattern, the enum acts like a union type. This 25 | also allows for [`Config`][config] functionality, like partials, merging, and validation, to be 26 | applied to the contents of each variant. 27 | 28 | > If you'd like to support unit-only enums, you can use the [`ConfigEnum` trait](../enum/index.md) 29 | > instead. 30 | 31 | ## Attribute fields 32 | 33 | The following fields are supported for the `#[config]` container attribute: 34 | 35 | - `allow_unknown_fields` - Removes the serde `deny_unknown_fields` from the 36 | [partial struct](../partial.md). Defaults to `false`. 37 | - `context` - Sets the struct to be used as the [context](../context.md). Defaults to `None`. 38 | - `env_prefix` - Sets the prefix to use for [environment variable](./env.md#container-prefixes) 39 | mapping. Defaults to `None`. 40 | - `serde` - A nested attribute that sets tagging related fields for the [partial](../partial.md). 41 | Defaults to `None`. 42 | 43 | ```rust 44 | #[derive(Config)] 45 | #[config(allow_unknown_fields, env_prefix = "EXAMPLE_")] 46 | struct ExampleConfig { 47 | // ... 48 | } 49 | ``` 50 | 51 | And the following for serde compatibility: 52 | 53 | - `rename` 54 | - `rename_all` - Defaults to `camelCase`. 55 | 56 | ## Serde support 57 | 58 | By default the [`Config`][config] macro will apply the following `#[serde]` to the 59 | [partial struct](../partial.md). The `default` and `deny_unknown_fields` ensure proper parsing and 60 | layer merging. 61 | 62 | ```rust 63 | #[serde(default, deny_unknown_fields, rename_all = "camelCase")] 64 | ``` 65 | 66 | However, the `deny_unknown_fields` and `rename_all` fields can be customized, and we also support 67 | the `rename` field, both via the top-level `#[config]` attribute. 68 | 69 | ```rust 70 | #[derive(Config)] 71 | #[config(allow_unknown_fields, rename = "ExampleConfig", rename_all = "snake_case")] 72 | struct Example { 73 | // ... 74 | } 75 | ``` 76 | 77 | > These values can also be applied using `#[serde]`, which is useful if you want to apply them to 78 | > the main struct as well, and not just the partial struct. 79 | 80 | [config]: https://docs.rs/schematic/latest/schematic/trait.Config.html 81 | -------------------------------------------------------------------------------- /book/src/config/struct/merge.md: -------------------------------------------------------------------------------- 1 | # Merge strategies 2 | 3 | A common requirement for configuration is to merge multiple sources/layers into a final result. By 4 | default Schematic will replace the previous setting value with the next value if the next value is 5 | `Some`, but sometimes you want far more control, like shallow or deep merging collections. 6 | 7 | This can be achieved with the `#[setting(merge)]` attribute field, which requires a reference to a 8 | function to call. 9 | 10 | ```rust 11 | #[derive(Config)] 12 | struct AppConfig { 13 | #[setting(merge = schematic::merge::append_vec)] 14 | pub allowed_hosts: Vec, 15 | } 16 | 17 | #[derive(Config)] 18 | enum Projects { 19 | #[setting(merge = schematic::merge::append_vec)] 20 | List(Vec), 21 | // ... 22 | } 23 | ``` 24 | 25 | > We provide a handful of built-in merge functions in the 26 | > [`merge` module](https://docs.rs/schematic/latest/schematic/merge/index.html). 27 | 28 | ## Merge handler function 29 | 30 | You can also define your own function for merging values. 31 | 32 | When defining a custom `merge` function, the previous value, next value, and 33 | [context](../context.md) are passed as arguments, and the function must return an optional merged 34 | result. If `None` is returned, neither value will be used. 35 | 36 | Here's an example of the merge function above. 37 | 38 | ```rust 39 | fn append_vec(mut prev: Vec, next: Vec, context: &Context) -> MergeResult>> { 40 | prev.extend(next); 41 | 42 | Ok(Some(prev)) 43 | } 44 | 45 | #[derive(Config)] 46 | struct ExampleConfig { 47 | #[setting(merge = append_vec)] 48 | pub field: Vec, 49 | } 50 | ``` 51 | 52 | ### Context handling 53 | 54 | If you're not using [context](../context.md), you can use `()` as the context type, or rely on 55 | generic inferrence. 56 | 57 | ```rust 58 | fn using_unit_type(prev: T, next: T, _: &()) -> MergeResult { 59 | // ... 60 | } 61 | 62 | fn using_generics(prev: T, next: T, _: &C) -> MergeResult { 63 | // ... 64 | } 65 | ``` 66 | -------------------------------------------------------------------------------- /book/src/config/struct/transform.md: -------------------------------------------------------------------------------- 1 | # Transforming values 2 | 3 | Sometimes a value is configured by a user, but it needs to be transformed in some way to be usable, 4 | for example, expanding file system paths to absolute from relative. 5 | 6 | This can be achieved with the `#[setting(transform)]` attribute field, which requires a reference to 7 | a function to call. Only values with a defined value are transformed, while optional values remain 8 | `None`. 9 | 10 | ```rust 11 | #[derive(Config)] 12 | struct AppConfig { 13 | #[setting(transform = make_absolute)] 14 | pub env_file: Option, 15 | } 16 | ``` 17 | 18 | > Transformations happen during the finalize phase, _after_ [environment variables](./env.md) are 19 | > inherited, and _before_ it is [validated](./validate.md). 20 | 21 | ## Transform handler function 22 | 23 | When defining a custom `transform` function, the defined value and [context](../context.md) are 24 | passed as arguments, and the function must return the transformed result. 25 | 26 | Here's an example of the transform function above. 27 | 28 | ```rust 29 | fn make_absolute(value: PathBuf, context: &Context) -> TransformResult { 30 | Ok(if value.is_absolute() { 31 | value 32 | } else { 33 | context.root.join(value) 34 | }) 35 | } 36 | ``` 37 | 38 | ### Nested values 39 | 40 | Transformers can also be used on [nested configs](../nested.md), but when defining the transformer 41 | function, the value being transformed is the _[partial nested config](../partial.md)_, not the final 42 | one. For example: 43 | 44 | ```rust 45 | fn transform_nested(value: PartialChildConfig, context: &Context) -> TransformResult { 46 | Ok(value) 47 | } 48 | 49 | #[derive(Config)] 50 | struct ParentConfig { 51 | #[setting(nested, transform = transform_nested)] 52 | pub child: ChildConfig, 53 | } 54 | ``` 55 | 56 | ### Context handling 57 | 58 | If you're not using [context](../context.md), you can use `()` as the context type, or rely on 59 | generic inferrence. 60 | 61 | ```rust 62 | fn using_unit_type(value: T, _: &()) -> TransformResult { 63 | // ... 64 | } 65 | 66 | fn using_generics(value: T, _: &C) -> TransformResult { 67 | // ... 68 | } 69 | ``` 70 | -------------------------------------------------------------------------------- /book/src/index.md: -------------------------------------------------------------------------------- 1 | # Schematic 2 | 3 | Schematic is a library that provides: 4 | 5 | - A layered serde-driven [configuration system](./config/index.md) with support for merge 6 | strategies, validation rules, environment variables, and more! 7 | - A [schema modeling system](./schema/index.md) that can be used to generate TypeScript types, JSON 8 | schemas, and more! 9 | 10 | Both of these features can be used independently or together. 11 | 12 | ``` 13 | cargo add schematic 14 | ``` 15 | 16 | ## Example references 17 | 18 | The following projects are using Schematic and can be used as a reference: 19 | 20 | - [moon](https://github.com/moonrepo/moon/tree/master/nextgen/config) - A build system for web based 21 | monorepos. 22 | - [proto](https://github.com/moonrepo/proto/blob/master/crates/core/src/proto_config.rs) - A 23 | multi-language version manager with WASM plugin support. 24 | - [ryot](https://github.com/IgnisDa/ryot/blob/main/crates/config/src/lib.rs) - Track various aspects 25 | of your life. 26 | -------------------------------------------------------------------------------- /book/src/schema/array.md: -------------------------------------------------------------------------------- 1 | # Arrays 2 | 3 | The [`ArrayType`][array] can be used to represent a variable list of homogeneous values of a given 4 | type, as defined by `items_type`. For example, a list of strings: 5 | 6 | ```rust 7 | use schematic::{Schematic, Schema, SchemaBuilder, SchemaType, schema::ArrayType}; 8 | 9 | impl Schematic for T { 10 | fn build_schema(mut schema: SchemaBuilder) -> Schema { 11 | schema.array(ArrayType { 12 | items_type: Box::new(schema.infer::()), 13 | ..ArrayType::default() 14 | }) 15 | } 16 | } 17 | ``` 18 | 19 | If you're only defining the `items_type` field, you can use the shorthand 20 | [`ArrayType::new()`](https://docs.rs/schematic/latest/schematic/struct.ArrayType.html#method.new) 21 | method. 22 | 23 | ```rust 24 | schema.array(ArrayType::new(schema.infer::())); 25 | ``` 26 | 27 | > Automatically implemented for `Vec`, `BTreeSet`, `HashSet`, `[T; N]`, and `&[T]`. 28 | 29 | ## Settings 30 | 31 | The following fields can be passed to [`ArrayType`][array], which are then fed into the 32 | [generator](./generator/index.md). 33 | 34 | ### Contains 35 | 36 | The `contains` field can be enabled to indicate that the array must contain at least one item of the 37 | type defined by `items_type`, instead of all items. 38 | 39 | ```rust 40 | ArrayType { 41 | // ... 42 | contains: Some(true), 43 | } 44 | ``` 45 | 46 | ### Length 47 | 48 | The `min_length` and `max_length` fields can be used to restrict the length of the array. Both 49 | fields accept a non-zero number, and can be used together or individually. 50 | 51 | ```rust 52 | ArrayType { 53 | // ... 54 | min_length: Some(1), 55 | max_length: Some(10), 56 | } 57 | ``` 58 | 59 | ### Uniqueness 60 | 61 | The `unique` field can be used to indicate that all items in the array must be unique. Note that 62 | Schematic _does not_ verify uniqueness. 63 | 64 | ```rust 65 | ArrayType { 66 | // ... 67 | unique: Some(true), 68 | } 69 | ``` 70 | 71 | [array]: https://docs.rs/schematic/latest/schematic/schema/struct.ArrayType.html 72 | -------------------------------------------------------------------------------- /book/src/schema/boolean.md: -------------------------------------------------------------------------------- 1 | # Booleans 2 | 3 | The [`BooleanType`][boolean] can be used to represent a boolean `true` or `false` value. Values that 4 | evaluate to true or false, such as 1 and 0, are not accepted by the schema. 5 | 6 | ```rust 7 | use schematic::{Schematic, Schema, SchemaBuilder, SchemaType, schema::BooleanType}; 8 | 9 | impl Schematic for T { 10 | fn build_schema(mut schema: SchemaBuilder) -> Schema { 11 | schema.boolean_default() 12 | } 13 | } 14 | ``` 15 | 16 | > Automatically implemented for `bool`. 17 | 18 | ## Default value 19 | 20 | To customize the default value for use within [generators](./generator/index.md), pass the desired 21 | value to the [`BooleanType`][boolean] constructor. 22 | 23 | ```rust 24 | schema.boolean(BooleanType::new(true)); 25 | ``` 26 | 27 | [boolean]: https://docs.rs/schematic/latest/schematic/schema/struct.BooleanType.html 28 | -------------------------------------------------------------------------------- /book/src/schema/enum.md: -------------------------------------------------------------------------------- 1 | # Enums 2 | 3 | The [`EnumType`][enum] can be used to represent a list of [literal values](./literal.md). 4 | 5 | ```rust 6 | use schematic::{Schematic, Schema, SchemaBuilder, SchemaType, schema::{EnumType, LiteralValue}}; 7 | 8 | impl Schematic for T { 9 | fn build_schema(mut schema: SchemaBuilder) -> Schema { 10 | schema.enumerable(EnumType { 11 | values: vec![ 12 | LiteralValue::String("debug".into()), 13 | LiteralValue::String("error".into()), 14 | LiteralValue::String("warning".into()), 15 | ], 16 | ..EnumType::default() 17 | }) 18 | } 19 | } 20 | ``` 21 | 22 | If you're only defining the `values` field, you can use the shorthand 23 | [`EnumType::new()`](https://docs.rs/schematic/latest/schematic/struct.EnumType.html#method.new) 24 | method. 25 | 26 | ```rust 27 | schema.enumerable(EnumType::new([ 28 | LiteralValue::String("debug".into()), 29 | LiteralValue::String("error".into()), 30 | LiteralValue::String("warning".into()), 31 | ])); 32 | ``` 33 | 34 | ## Detailed variants 35 | 36 | If you'd like to provide more detailed information for each variant (value), like descriptions and 37 | visibility, you can define the `variants` field and pass a map of 38 | [`SchemaField`](https://docs.rs/schematic/latest/schematic/struct.SchemaField.html)s. 39 | 40 | ```rust 41 | schema.enumerable(EnumType { 42 | values: vec![ 43 | LiteralValue::String("debug".into()), 44 | LiteralValue::String("error".into()), 45 | LiteralValue::String("warning".into()), 46 | ], 47 | variants: Some(IndexMap::from_iter([ 48 | ( 49 | "Debug".into(), 50 | SchemaField { 51 | comment: Some("Shows debug messages and above".into()), 52 | schema: Schema::new(SchemaType::literal(LiteralValue::String("debug".into()))), 53 | ..SchemaField::default() 54 | } 55 | ), 56 | ( 57 | "Error".into(), 58 | SchemaField { 59 | comment: Some("Shows only error messages".into()), 60 | schema: Schema::new(SchemaType::literal(LiteralValue::String("error".into()))), 61 | ..SchemaField::default() 62 | } 63 | ), 64 | ( 65 | "Warning".into(), 66 | SchemaField { 67 | comment: Some("Shows warning and error messages".into()), 68 | schema: Schema::new(SchemaType::literal(LiteralValue::String("warning".into()))), 69 | ..SchemaField::default() 70 | } 71 | ), 72 | ])), 73 | ..EnumType::default() 74 | }) 75 | ``` 76 | 77 | > This comes in handy when working with specific generators, like TypeScript. 78 | 79 | [enum]: https://docs.rs/schematic/latest/schematic/schema/struct.EnumType.html 80 | -------------------------------------------------------------------------------- /book/src/schema/external.md: -------------------------------------------------------------------------------- 1 | # External types 2 | 3 | Schematic provides schema implementations for third-party [crates](https://crates.io) through a 4 | concept known as external types. This functionality is opt-in through Cargo features. 5 | 6 | ## chrono 7 | 8 | > Requires the `type_chrono` Cargo feature. 9 | 10 | Implements schemas for `Date`, `DateTime`, `Duration`, `Days`, `Months`, `IsoWeek`, `NaiveWeek`, 11 | `NaiveDate`, `NaiveDateTime`, and `NaiveTime` from the [chrono](https://crates.io/crates/chrono) 12 | crate. 13 | 14 | ## indexmap 15 | 16 | > Requires the `type_indexmap` Cargo feature. 17 | 18 | Implements a schema for `IndexMap` and `IndexSet` from the 19 | [indexmap](https://crates.io/crates/indexmap) crate. 20 | 21 | ## regex 22 | 23 | > Requires the `type_regex` Cargo feature. 24 | 25 | Implements a schema for `Regex` from the [regex](https://crates.io/crates/regex) crate. 26 | 27 | ## relative-path 28 | 29 | > Requires the `type_relative_path` Cargo feature. 30 | 31 | Implements schemas for `RelativePath` and `RelativePathBuf` from the 32 | [relative-path](https://crates.io/crates/relative-path) crate. 33 | 34 | ## rpkl 35 | 36 | > Requires the `type_serde_rpkl` Cargo feature. 37 | 38 | Implements schemas for `Value` from the [rpkl](https://crates.io/crates/rpkl) crate. 39 | 40 | ## rust_decimal 41 | 42 | > Requires the `type_rust_decimal` Cargo feature. 43 | 44 | Implements a schema for `Decimal` from the [rust_decimal](https://crates.io/crates/rust_decimal) 45 | crate. 46 | 47 | ## semver 48 | 49 | > Requires the `type_semver` Cargo feature. 50 | 51 | Implements schemas for `Version` and `VersionReq` from the [semver](https://crates.io/crates/semver) 52 | crate. 53 | 54 | ## serde_json 55 | 56 | > Requires the `type_serde_json` Cargo feature. 57 | 58 | Implements schemas for `Value`, `Number`, and `Map` from the 59 | [serde_json](https://crates.io/crates/serde_json) crate. 60 | 61 | ## serde_yaml 62 | 63 | > Requires the `type_serde_yaml` Cargo feature. 64 | 65 | Implements schemas for `Value`, `Number`, and `Mapping` from the 66 | [serde_yaml](https://crates.io/crates/serde_yaml) crate. 67 | 68 | ## serde_yml 69 | 70 | > Requires the `type_serde_yml` Cargo feature. 71 | 72 | Implements schemas for `Value`, `Number`, and `Mapping` from the 73 | [serde_yml](https://crates.io/crates/serde_yml) crate. 74 | 75 | ## toml 76 | 77 | > Requires the `type_serde_toml` Cargo feature. 78 | 79 | Implements schemas for `Value` and `Map` from the [toml](https://crates.io/crates/toml) crate. 80 | 81 | ## url 82 | 83 | > Requires the `type_url` Cargo feature. 84 | 85 | Implements a schema for `Url` from the [url](https://crates.io/crates/url) crate. 86 | 87 | ## uuid 88 | 89 | > Requires the `type_uuid` Cargo feature. 90 | 91 | Implements a schema for `Uuid` from the [uuid](https://crates.io/crates/uuid) crate. 92 | -------------------------------------------------------------------------------- /book/src/schema/float.md: -------------------------------------------------------------------------------- 1 | # Floats 2 | 3 | The [`FloatType`][float] can be used to represent a float or double. 4 | 5 | ```rust 6 | use schematic::{Schematic, Schema, SchemaBuilder, SchemaType, schema::{FloatType, FloatKind}}; 7 | 8 | impl Schematic for T { 9 | fn build_schema(mut schema: SchemaBuilder) -> Schema { 10 | schema.float(FloatType { 11 | kind: FloatKind::F32, 12 | ..FloatType::default() 13 | }) 14 | } 15 | } 16 | ``` 17 | 18 | If you're only defining the `kind` field, you can use the shorthand 19 | [`FloatType::new_kind()`](https://docs.rs/schematic/latest/schematic/struct.FloatType.html#method.new_kind) 20 | method. 21 | 22 | ```rust 23 | schema.float(FloatType::new_kind(FloatKind::F32)); 24 | ``` 25 | 26 | > Automatically implemented for `f32` and `f64`. 27 | 28 | ## Default value 29 | 30 | To customize the default value for use within [generators](./generator/index.md), pass the desired 31 | value to the [`FloatType`][float] constructor. 32 | 33 | ```rust 34 | schema.float(FloatType::new_32(32.0)); 35 | // Or 36 | schema.float(FloatType::new_64(64.0)); 37 | ``` 38 | 39 | ## Settings 40 | 41 | The following fields can be passed to [`FloatType`][float], which are then fed into the 42 | [generator](./generator/index.md). 43 | 44 | ### Enumerable 45 | 46 | The `enum_values` field can be used to specify a list of literal values that are allowed for the 47 | field. 48 | 49 | ```rust 50 | FloatType { 51 | // ... 52 | enum_values: Some(vec![0.0, 0.25, 0.5, 0.75, 1.0]), 53 | } 54 | ``` 55 | 56 | ### Formats 57 | 58 | The `format` field can be used to associate semantic meaning to the float, and how the float will be 59 | used and displayed. 60 | 61 | ```rust 62 | FloatType { 63 | // ... 64 | format: Some("currency".into()), 65 | } 66 | ``` 67 | 68 | > This is primarily used by JSON Schema. 69 | 70 | ### Min/max 71 | 72 | The `min` and `max` fields can be used to specify the minimum and maximum inclusive values allowed. 73 | Both fields accept a non-zero number, and can be used together or individually. 74 | 75 | ```rust 76 | FloatType { 77 | // ... 78 | min: Some(0.0), // >0 79 | max: Some(1.0), // <1 80 | } 81 | ``` 82 | 83 | These fields are not exclusive and do not include the lower and upper bound values. To include them, 84 | use `min_exclusive` and `max_exclusive` instead. 85 | 86 | ```rust 87 | FloatType { 88 | // ... 89 | min_exclusive: Some(0.0), // >=0 90 | max_exclusive: Some(1.0), // <=1 91 | } 92 | ``` 93 | 94 | ### Multiple of 95 | 96 | The `multiple_of` field can be used to specify a value that the float must be a multiple of. 97 | 98 | ```rust 99 | FloatType { 100 | // ... 101 | multiple_of: Some(0.25), // 0.0, 0.25, 0.50, etc 102 | } 103 | ``` 104 | 105 | [float]: https://docs.rs/schematic/latest/schematic/schema/struct.FloatType.html 106 | -------------------------------------------------------------------------------- /book/src/schema/generator/index.md: -------------------------------------------------------------------------------- 1 | # Code generation 2 | 3 | The primary benefit of a schema modeling system, is that you can consume this type information to 4 | generate code into multiple output formats. This is a common pattern in many languages, and is a 5 | great way to reduce boilerplate. 6 | 7 | In the context of Rust, why use multiple disparate crates, each with their own unique 8 | implementations and `#[derive]` macros, just to generate some output. With Schematic, you can ditch 9 | all of these and use a single standardized approach. 10 | 11 | ## Usage 12 | 13 | To make use of the generator, import and instantiate our 14 | [`SchemaGenerator`](https://docs.rs/schematic/latest/schematic/schema/struct.SchemaGenerator.html). 15 | This is typically done within a one-off `main` function that can be ran from Cargo. 16 | 17 | ```rust 18 | use schematic::schema::SchemaGenerator; 19 | 20 | let mut generator = SchemaGenerator::default(); 21 | ``` 22 | 23 | ### Adding types 24 | 25 | From here, for every type that implements 26 | [`Schematic`](https://docs.rs/schematic/latest/schematic/trait.Schematic.html) and you want to 27 | include in the generated output, call 28 | [`SchemaGenerator::add()`](https://docs.rs/schematic/latest/schematic/schema/struct.SchemaGenerator.html#method.add). 29 | If you only have a [`SchemaType`](https://docs.rs/schematic/latest/schematic/enum.SchemaType.html), 30 | you can use the 31 | [`SchemaGenerator::add_schema()`](https://docs.rs/schematic/latest/schematic/schema/struct.SchemaGenerator.html#method.add_schema) 32 | method instead. 33 | 34 | ```rust 35 | use schematic::schema::SchemaGenerator; 36 | 37 | let mut generator = SchemaGenerator::default(); 38 | generator.add::(); 39 | generator.add::(); 40 | generator.add::(); 41 | ``` 42 | 43 | > We'll recursively add referenced and nested schemas for types that are added. No need to 44 | > explicitly add all required types! 45 | 46 | ### Generating output 47 | 48 | From here, call 49 | [`SchemaGenerator::generate()`](https://docs.rs/schematic/latest/schematic/schema/struct.SchemaGenerator.html#method.generate) 50 | to render the schemes with a chosen [renderer](#renderers) to an output file of your choice. This 51 | method can be called multiple times, each with a different output file or renderer. 52 | 53 | ```rust 54 | use schematic::schema::SchemaGenerator; 55 | 56 | let mut generator = SchemaGenerator::default(); 57 | generator.add::(); 58 | generator.add::(); 59 | generator.add::(); 60 | generator.generate(PathBuf::from("output/file"), CustomRenderer::default())?; 61 | generator.generate(PathBuf::from("output/another/file"), AnotherRenderer::default())?; 62 | ``` 63 | 64 | ## Renderers 65 | 66 | The following built-in renderers are available, but custom renderers can be created as well by 67 | implementing the 68 | [`SchemaRenderer`](https://docs.rs/schematic/latest/schematic/schema/trait.SchemaRenderer.html) 69 | trait. 70 | 71 | - [Config templates](./template.md) 72 | - [JSON schemas](./json-schema.md) 73 | - [TypeScript types](./typescript.md) 74 | -------------------------------------------------------------------------------- /book/src/schema/integer.md: -------------------------------------------------------------------------------- 1 | # Integers 2 | 3 | The [`IntegerType`][integer] can be used to represent an integer (number). 4 | 5 | ```rust 6 | use schematic::{Schematic, Schema, SchemaBuilder, SchemaType, schema::{IntegerType, IntegerKind}}; 7 | 8 | impl Schematic for T { 9 | fn build_schema(mut schema: SchemaBuilder) -> Schema { 10 | schema.integer(IntegerType { 11 | kind: IntegerKind::U32, 12 | ..IntegerType::default() 13 | }) 14 | } 15 | } 16 | ``` 17 | 18 | If you're only defining the `kind` field, you can use the shorthand 19 | [`IntegerType::new_kind()`](https://docs.rs/schematic/latest/schematic/struct.IntegerType.html#method.new_kind) 20 | method. 21 | 22 | ```rust 23 | schema.integer(IntegerType::new_kind(IntegerKind::U32)); 24 | ``` 25 | 26 | > Automatically implemented for `usize`-`u128` and `isize`-`i128`. 27 | 28 | ## Default value 29 | 30 | To customize the default value for use within [generators](./generator/index.md), pass the desired 31 | value to the [`IntegerType`][integer] constructor. 32 | 33 | ```rust 34 | schema.integer(IntegerType::new(IntegerKind::I32, 100)); 35 | // Or 36 | schema.integer(IntegerType::new_unsigned(IntegerKind::U32, 100)); 37 | ``` 38 | 39 | ## Settings 40 | 41 | The following fields can be passed to [`IntegerType`][integer], which are then fed into the 42 | [generator](./generator/index.md). 43 | 44 | ### Enumerable 45 | 46 | The `enum_values` field can be used to specify a list of literal values that are allowed for the 47 | field. 48 | 49 | ```rust 50 | IntegerType { 51 | // ... 52 | enum_values: Some(vec![0, 25, 50, 75, 100]), 53 | } 54 | ``` 55 | 56 | ### Formats 57 | 58 | The `format` field can be used to associate semantic meaning to the integer, and how the integer 59 | will be used and displayed. 60 | 61 | ```rust 62 | IntegerType { 63 | // ... 64 | format: Some("age".into()), 65 | } 66 | ``` 67 | 68 | > This is primarily used by JSON Schema. 69 | 70 | ### Min/max 71 | 72 | The `min` and `max` fields can be used to specify the minimum and maximum inclusive values allowed. 73 | Both fields accept a non-zero number, and can be used together or individually. 74 | 75 | ```rust 76 | IntegerType { 77 | // ... 78 | min: Some(0), // >0 79 | max: Some(100), // <100 80 | } 81 | ``` 82 | 83 | These fields are not exclusive and do not include the lower and upper bound values. To include them, 84 | use `min_exclusive` and `max_exclusive` instead. 85 | 86 | ```rust 87 | IntegerType { 88 | // ... 89 | min_exclusive: Some(0), // >=0 90 | max_exclusive: Some(100), // <=100 91 | } 92 | ``` 93 | 94 | ### Multiple of 95 | 96 | The `multiple_of` field can be used to specify a value that the integer must be a multiple of. 97 | 98 | ```rust 99 | IntegerType { 100 | // ... 101 | multiple_of: Some(25), // 0, 25, 50, etc 102 | } 103 | ``` 104 | 105 | [integer]: https://docs.rs/schematic/latest/schematic/schema/struct.IntegerType.html 106 | -------------------------------------------------------------------------------- /book/src/schema/literal.md: -------------------------------------------------------------------------------- 1 | # Literals 2 | 3 | The [`LiteralType`](https://docs.rs/schematic/latest/schematic/schema/struct.LiteralType.html) can 4 | be used to represent a literal primitive value, such as a string or number. 5 | 6 | ```rust 7 | use schematic::{Schematic, Schema, SchemaBuilder, SchemaType, schema::{LiteralType, LiteralValue}}; 8 | 9 | impl Schematic for T { 10 | fn build_schema(mut schema: SchemaBuilder) -> Schema { 11 | schema.literal(LiteralType::new(LiteralValue::String("enabled".into()))) 12 | // Or 13 | schema.literal_value(LiteralValue::String("enabled".into())) 14 | } 15 | } 16 | ``` 17 | 18 | > The [`LiteralValue`](https://docs.rs/schematic/latest/schematic/schema/enum.LiteralValue.html) 19 | > type is used by other schema types for their default or enumerable values. 20 | -------------------------------------------------------------------------------- /book/src/schema/null.md: -------------------------------------------------------------------------------- 1 | # Nulls 2 | 3 | The [`SchemaType::Null`][null] variant can be used to represent a literal `null` value. This works 4 | best when paired with unions or fields that need to be [nullable](#marking-as-nullable). 5 | 6 | ```rust 7 | use schematic::{Schematic, Schema, SchemaBuilder, SchemaType}; 8 | 9 | impl Schematic for T { 10 | fn build_schema(mut schema: SchemaBuilder) -> Schema { 11 | schema.set_type_and_build(SchemaType::Null) 12 | } 13 | } 14 | ``` 15 | 16 | > Automatically implemented for `()` and `Option`. 17 | 18 | ## Marking as nullable 19 | 20 | If you want a concrete schema to also accept null (an `Option`al value), you can use the 21 | [`SchemaBuilder::nullable()`](https://docs.rs/schematic/latest/schematic/struct.SchemaBuilder.html#method.nullable) 22 | method. Under the hood, this will create a union of the defined type, and the null type. 23 | 24 | ```rust 25 | // string | null 26 | schema.nullable(schema.infer::()); 27 | ``` 28 | 29 | [null]: https://docs.rs/schematic/latest/schematic/enum.SchemaType.html#variant.Null 30 | -------------------------------------------------------------------------------- /book/src/schema/object.md: -------------------------------------------------------------------------------- 1 | # Objects 2 | 3 | The [`ObjectType`][object] can be used to represent a key-value object of homogenous types. This is 4 | also known as a map, record, keyed object, or indexed object. 5 | 6 | ```rust 7 | use schematic::{Schematic, Schema, SchemaBuilder, SchemaType, schema::ObjectType}; 8 | 9 | impl Schematic for T { 10 | fn build_schema(mut schema: SchemaBuilder) -> Schema { 11 | schema.object(ObjectType { 12 | key_type: Box::new(schema.infer::()), 13 | value_type: Box::new(schema.infer::()), 14 | ..ObjectType::default() 15 | }) 16 | } 17 | } 18 | ``` 19 | 20 | If you're only defining the `key_type` and `value_type` fields, you can use the shorthand 21 | [`ObjectType::new()`](https://docs.rs/schematic/latest/schematic/struct.ObjectType.html#method.new) 22 | method. 23 | 24 | ```rust 25 | schema.object(ObjectType::new(schema.infer::(), schema.infer::())); 26 | ``` 27 | 28 | > Automatically implemented for `BTreeMap` and `HashMap`. 29 | 30 | ## Settings 31 | 32 | The following fields can be passed to [`ObjectType`][object], which are then fed into the 33 | [generator](./generator/index.md). 34 | 35 | ### Length 36 | 37 | The `min_length` and `max_length` fields can be used to restrict the length (key-value pairs) of the 38 | object. Both fields accept a non-zero number, and can be used together or individually. 39 | 40 | ```rust 41 | ObjectType { 42 | // ... 43 | min_length: Some(1), 44 | max_length: Some(10), 45 | } 46 | ``` 47 | 48 | ### Required keys 49 | 50 | The `required` field can be used to specify a list of keys that are required for the object, and 51 | must exist when the object is validated. 52 | 53 | ```rust 54 | ObjectType { 55 | // ... 56 | required: Some(vec!["foo".into(), "bar".into()]), 57 | } 58 | ``` 59 | 60 | > This is primarily used by JSON Schema. 61 | 62 | [object]: https://docs.rs/schematic/latest/schematic/schema/struct.ObjectType.html 63 | -------------------------------------------------------------------------------- /book/src/schema/string.md: -------------------------------------------------------------------------------- 1 | # Strings 2 | 3 | The [`StringType`][string] can be used to represent a sequence of bytes, you know, a string. 4 | 5 | ```rust 6 | use schematic::{Schematic, Schema, SchemaBuilder, SchemaType, schema::{StringType, IntegerKind}}; 7 | 8 | impl Schematic for T { 9 | fn build_schema(mut schema: SchemaBuilder) -> Schema { 10 | schema.string_default() 11 | } 12 | } 13 | ``` 14 | 15 | > Automatically implemented for `char`, `str`, `String`, `Path`, `PathBuf`, `Ipv4Addr`, `Ipv6Addr`, 16 | > `SystemTime`, and `Duration`. 17 | 18 | ## Default value 19 | 20 | To customize the default value for use within [generators](./generator/index.md), pass the desired 21 | value to the [`StringType`][string] constructor. 22 | 23 | ```rust 24 | schema.string(StringType::new("abc")); 25 | ``` 26 | 27 | ## Settings 28 | 29 | The following fields can be passed to [`StringType`][string], which are then fed into the 30 | [generator](./generator/index.md). 31 | 32 | ### Enumerable 33 | 34 | The `enum_values` field can be used to specify a list of literal values that are allowed for the 35 | field. 36 | 37 | ```rust 38 | StringType { 39 | // ... 40 | enum_values: Some(vec!["a".into(), "b".into(), "c".into()]), 41 | } 42 | ``` 43 | 44 | ### Formats 45 | 46 | The `format` field can be used to associate semantic meaning to the string, and how the string will 47 | be used and displayed. 48 | 49 | ```rust 50 | StringType { 51 | // ... 52 | format: Some("url".into()), 53 | } 54 | ``` 55 | 56 | > This is primarily used by JSON Schema. 57 | 58 | ### Length 59 | 60 | The `min_length` and `max_length` fields can be used to restrict the length of the string. Both 61 | fields accept a non-zero number, and can be used together or individually. 62 | 63 | ```rust 64 | StringType { 65 | // ... 66 | min_length: Some(1), 67 | max_length: Some(10), 68 | } 69 | ``` 70 | 71 | ### Patterns 72 | 73 | The `pattern` field can be used to define a regex pattern that the string must abide by. 74 | 75 | ```rust 76 | StringType { 77 | // ... 78 | format: Some("version".into()), 79 | pattern: Some("\d+\.\d+\.\d+".into()), 80 | } 81 | ``` 82 | 83 | > This is primarily used by JSON Schema. 84 | 85 | [string]: https://docs.rs/schematic/latest/schematic/schema/struct.StringType.html 86 | -------------------------------------------------------------------------------- /book/src/schema/struct.md: -------------------------------------------------------------------------------- 1 | # Structs 2 | 3 | The [`StructType`][struct] can be used to represent a struct with explicitly named fields and typed 4 | values. This is also known as a "shape" or "model". 5 | 6 | ```rust 7 | use schematic::{Schematic, Schema, SchemaBuilder, SchemaType, schema::StructType}; 8 | 9 | impl Schematic for T { 10 | fn build_schema(mut schema: SchemaBuilder) -> Schema { 11 | schema.structure(StructType { 12 | fields: HashMap::from_iter([ 13 | ( 14 | "name".into(), 15 | Box::new(SchemaField { 16 | comment: Some("Name of the user".into()), 17 | schema: schema.infer::(), 18 | ..SchemaField::default() 19 | }) 20 | ), 21 | ( 22 | "age".into(), 23 | Box::new(SchemaField { 24 | comment: Some("Age of the user".into()), 25 | schema: schema.nest().integer(IntegerType::new_kind(IntegerKind::U16)), 26 | ..SchemaField::default() 27 | }) 28 | ), 29 | ( 30 | "active".into(), 31 | Box::new(SchemaField { 32 | comment: Some("Is the user active".into()), 33 | schema: schema.infer::(), 34 | ..SchemaField::default() 35 | }) 36 | ), 37 | ]), 38 | ..StructType::default() 39 | }) 40 | } 41 | } 42 | ``` 43 | 44 | If you're only defining `fields`, you can use the shorthand 45 | [`StructType::new()`](https://docs.rs/schematic/latest/schematic/struct.StructType.html#method.new) 46 | method. When using this approach, the `Box`s are automatically inserted for you. 47 | 48 | ```rust 49 | schema.structure(StructType::new([ 50 | ( 51 | "name".into(), 52 | SchemaField { 53 | comment: Some("Name of the user".into()), 54 | schema: schema.infer::(), 55 | ..SchemaField::default() 56 | } 57 | ), 58 | // ... 59 | ])); 60 | ``` 61 | 62 | ## Settings 63 | 64 | The following fields can be passed to [`StructType`][struct], which are then fed into the 65 | [generator](./generator/index.md). 66 | 67 | ### Required fields 68 | 69 | The `required` field can be used to specify a list of fields that are required for the struct. 70 | 71 | ```rust 72 | StructType { 73 | // ... 74 | required: Some(vec!["name".into()]), 75 | } 76 | ``` 77 | 78 | > This is primarily used by JSON Schema. 79 | 80 | [struct]: https://docs.rs/schematic/latest/schematic/schema/struct.StructType.html 81 | -------------------------------------------------------------------------------- /book/src/schema/tuple.md: -------------------------------------------------------------------------------- 1 | # Tuples 2 | 3 | The [`TupleType`][tuple] can be used to represent a fixed list of heterogeneous values of a given 4 | type, as defined by `items_types`. 5 | 6 | ```rust 7 | use schematic::{Schematic, Schema, SchemaBuilder, SchemaType, schema::{TupleType, IntegerKind}}; 8 | 9 | impl Schematic for T { 10 | fn build_schema(mut schema: SchemaBuilder) -> Schema { 11 | schema.tuple(TupleType { 12 | items_types: vec![ 13 | Box::new(schema.infer::()), 14 | Box::new(schema.infer::()), 15 | Box::new(schema.nest().integer(IntegerType::new_kind(IntegerKind::U32))), 16 | ], 17 | ..TupleType::default() 18 | }) 19 | } 20 | } 21 | ``` 22 | 23 | If you're only defining the `items_types` field, you can use the shorthand 24 | [`TupleType::new()`](https://docs.rs/schematic/latest/schematic/struct.TupleType.html#method.new) 25 | method. When using this approach, the `Box`s are automatically inserted for you. 26 | 27 | ```rust 28 | schema.tuple(TupleType::new([ 29 | schema.infer::(), 30 | schema.infer::(), 31 | schema.nest().integer(IntegerType::new_kind(IntegerKind::U32)), 32 | ])); 33 | ``` 34 | 35 | > Automatically implemented for tuples of 0-12 length. 36 | 37 | [tuple]: https://docs.rs/schematic/latest/schematic/schema/struct.TupleType.html 38 | -------------------------------------------------------------------------------- /book/src/schema/types.md: -------------------------------------------------------------------------------- 1 | # Types 2 | 3 | Schema types are the building blocks when modeling your schema. They are used to define the explicit 4 | shape of your types, data, or configuration. This type information is then passed to a 5 | [generator](./generator/index.md), which can then generate and render the schema types in a variety 6 | of formats. 7 | 8 | - [Arrays](./array.md) 9 | - [Booleans](./boolean.md) 10 | - [Enums](./enum.md) 11 | - [Floats](./float.md) 12 | - [Integers](./integer.md) 13 | - [Literals](./literal.md) 14 | - [Nulls](./null.md) 15 | - [Objects](./object.md) 16 | - [Strings](./string.md) 17 | - [Structs](./struct.md) 18 | - [Tuples](./tuple.md) 19 | - [Unions](./union.md) 20 | - [Unknown](./unknown.md) 21 | 22 | ## Defining names 23 | 24 | Schemas can be named, which is useful for referencing them in other types when generating code. By 25 | default the [`Schematic`][schematic] derive macro will use the name of the type, but when 26 | implementing the trait manually, you can use the 27 | [`Schematic::schema_name()`](https://docs.rs/schematic/latest/schematic/trait.Schematic.html#method.schema_name) 28 | method. 29 | 30 | ```rust 31 | impl Schematic for T { 32 | fn schema_name() -> Option { 33 | Some("CustomName".into()) 34 | } 35 | } 36 | ``` 37 | 38 | > This method is optional, but is encouraged for non-primitive types. It will associate references 39 | > between types, and avoid circular references. 40 | 41 | ## Inferring schemas 42 | 43 | When building a schema, you'll almost always need to reference schemas from other types that 44 | implement [`Schematic`][schematic]. To do so, you can use the 45 | [`SchemaBuilder.infer::()`](https://docs.rs/schematic/latest/schematic/struct.SchemaBuilder.html#method.build_schema) 46 | method, which will create a nested builder, and build an isolated schema based on its 47 | implementation. 48 | 49 | ```rust 50 | struct OtherType {} 51 | 52 | impl Schematic for OtherType { 53 | // ... 54 | } 55 | 56 | 57 | impl Schematic for T { 58 | fn build_schema(mut schema: SchemaBuilder) -> Schema { 59 | let builtin_type = schema.infer::(); 60 | let custom_type = schema.infer::(); 61 | 62 | // ... 63 | } 64 | } 65 | ``` 66 | 67 | ## Creating nested schemas 68 | 69 | When building a schema, you may have situations where you need to build nested schemas, for example, 70 | within struct fields. You _cannot_ use the type-based methods on `SchemaBuilder`, as they mutate the 71 | current builder. Instead you must created another builder, which can be achieved with the 72 | [`SchemaBuilder.nest()`](https://docs.rs/schematic/latest/schematic/struct.SchemaBuilder.html#method.nest) 73 | method. 74 | 75 | ```rust 76 | impl Schematic for T { 77 | fn build_schema(mut schema: SchemaBuilder) -> Schema { 78 | // Mutates self 79 | schema.string_default(); 80 | 81 | // Creates a new builder and mutates it 82 | schema.nest().string_default(); 83 | 84 | // ... 85 | } 86 | } 87 | ``` 88 | 89 | [schematic]: https://docs.rs/schematic/latest/schematic/trait.Schematic.html 90 | -------------------------------------------------------------------------------- /book/src/schema/union.md: -------------------------------------------------------------------------------- 1 | # Unions 2 | 3 | The [`UnionType`][union] paired with 4 | [`SchemaType::Union`](https://docs.rs/schematic/latest/schematic/enum.SchemaType.html#variant.Union) 5 | can be used to represent a list of heterogeneous schema types (variants), in which a value must 6 | match one or more of the types. 7 | 8 | ```rust 9 | use schematic::{Schematic, Schema, SchemaBuilder, SchemaType, schema::{UnionType, UnionOperator}}; 10 | 11 | impl Schematic for T { 12 | fn build_schema(mut schema: SchemaBuilder) -> Schema { 13 | schema.union(UnionType { 14 | operator: UnionOperator::AnyOf, 15 | variants_types: vec![ 16 | Box::new(schema.infer::()), 17 | Box::new(schema.infer::()), 18 | Box::new(schema.nest().integer(IntegerType::new_kind(IntegerKind::U32))), 19 | ], 20 | ..UnionType::default() 21 | }) 22 | } 23 | } 24 | ``` 25 | 26 | If you're only defining the `variants_types` field, you can use the shorthand 27 | [`UnionType::new_any()`](https://docs.rs/schematic/latest/schematic/struct.UnionType.html#method.new_any) 28 | (any of) or 29 | [`UnionType::new_one()`](https://docs.rs/schematic/latest/schematic/struct.UnionType.html#method.new_one) 30 | (one of) methods. When using this approach, the `Box`s are automatically inserted for you. 31 | 32 | ```rust 33 | // Any of 34 | schema.union(UnionType::new_any([ 35 | schema.infer::(), 36 | schema.infer::(), 37 | schema.nest().integer(IntegerType::new_kind(IntegerKind::U32)), 38 | ])); 39 | 40 | // One of 41 | schema.union(UnionType::new_one([ 42 | // ... 43 | ])); 44 | ``` 45 | 46 | ## Operators 47 | 48 | Unions support 2 kinds of operators, any of and one of, both of which can be defined with the 49 | `operator` field. 50 | 51 | - Any of requires the value to match any of the variants. 52 | - One of requires the value to match _only one_ of the variants. 53 | 54 | ```rust 55 | UnionType { 56 | // ... 57 | operator: UnionOperator::OneOf, 58 | } 59 | ``` 60 | 61 | [union]: https://docs.rs/schematic/latest/schematic/schema/struct.UnionType.html 62 | -------------------------------------------------------------------------------- /book/src/schema/unknown.md: -------------------------------------------------------------------------------- 1 | # Unknown 2 | 3 | The [`SchemaType::Unknown`][unknown] variant can be used to represent an unknown type. This is 4 | sometimes known as an "any" or "mixed" type. 5 | 6 | ```rust 7 | use schematic::{Schematic, Schema, SchemaBuilder, SchemaType}; 8 | 9 | impl Schematic for T { 10 | fn build_schema(schema: SchemaBuilder) -> Schema { 11 | schema.build() 12 | } 13 | } 14 | ``` 15 | 16 | The [`SchemaType::Unknown`][unknown] variant is also the default variant, and the default 17 | implementation for 18 | [`Schematic::build_schema()`](https://docs.rs/schematic/latest/schematic/trait.Schematic.html#method.build_schema), 19 | so the above can simply be written as: 20 | 21 | ```rust 22 | impl Schematic for T {} 23 | ``` 24 | 25 | [unknown]: https://docs.rs/schematic/latest/schematic/enum.SchemaType.html#variant.Unknown 26 | -------------------------------------------------------------------------------- /crates/macros/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "schematic_macros" 3 | version = "0.18.12" 4 | edition = "2024" 5 | license = "MIT" 6 | description = "Macros for the schematic crate." 7 | homepage = "https://moonrepo.github.io/schematic" 8 | repository = "https://github.com/moonrepo/schematic" 9 | 10 | [package.metadata.docs.rs] 11 | all-features = true 12 | 13 | [lib] 14 | proc-macro = true 15 | 16 | [dependencies] 17 | convert_case = "0.8.0" 18 | darling = "0.21.0" 19 | proc-macro2 = "1.0.95" 20 | quote = "1.0.40" 21 | syn = { version = "2.0.104", features = ["full"] } 22 | 23 | [features] 24 | default = [] 25 | config = [] 26 | env = [] 27 | extends = [] 28 | schema = [] 29 | tracing = [] 30 | validate = [] 31 | -------------------------------------------------------------------------------- /crates/macros/src/common/mod.rs: -------------------------------------------------------------------------------- 1 | mod container; 2 | mod field; 3 | mod field_value; 4 | mod macros; 5 | mod variant; 6 | 7 | pub use container::*; 8 | pub use field::*; 9 | pub use field_value::*; 10 | pub use macros::*; 11 | pub use variant::*; 12 | 13 | #[derive(darling::FromMeta, Default)] 14 | #[darling(default)] 15 | pub struct SerdeMeta { 16 | pub content: Option, 17 | pub expecting: Option, 18 | pub tag: Option, 19 | pub untagged: bool, 20 | } 21 | -------------------------------------------------------------------------------- /crates/macros/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod common; 2 | mod utils; 3 | 4 | #[cfg(feature = "config")] 5 | mod config; 6 | #[cfg(feature = "config")] 7 | mod config_enum; 8 | #[cfg(feature = "schema")] 9 | mod schematic; 10 | 11 | use common::Macro; 12 | use proc_macro::TokenStream; 13 | use quote::quote; 14 | use syn::{DeriveInput, parse_macro_input}; 15 | 16 | // #[derive(Config)] 17 | #[cfg(feature = "config")] 18 | #[proc_macro_derive(Config, attributes(config, setting))] 19 | pub fn config(item: TokenStream) -> TokenStream { 20 | let input: DeriveInput = parse_macro_input!(item); 21 | let output = config::ConfigMacro(Macro::from(&input)); 22 | 23 | quote! { #output }.into() 24 | } 25 | 26 | // #[derive(ConfigEnum)] 27 | #[cfg(feature = "config")] 28 | #[proc_macro_derive(ConfigEnum, attributes(config, variant))] 29 | pub fn config_enum(item: TokenStream) -> TokenStream { 30 | config_enum::macro_impl(item) 31 | } 32 | 33 | // #[derive(Schematic)] 34 | #[cfg(feature = "schema")] 35 | #[proc_macro_derive(Schematic, attributes(schematic, schema))] 36 | pub fn schematic(item: TokenStream) -> TokenStream { 37 | let input: DeriveInput = parse_macro_input!(item); 38 | let output = schematic::SchematicMacro(Macro::from(&input)); 39 | 40 | quote! { #output }.into() 41 | } 42 | -------------------------------------------------------------------------------- /crates/macros/src/schematic/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::common::Macro; 2 | use crate::utils::instrument_quote; 3 | use proc_macro2::TokenStream; 4 | use quote::{ToTokens, quote}; 5 | 6 | pub struct SchematicMacro<'l>(pub Macro<'l>); 7 | 8 | impl ToTokens for SchematicMacro<'_> { 9 | fn to_tokens(&self, tokens: &mut TokenStream) { 10 | let cfg = &self.0; 11 | let name = cfg.name; 12 | 13 | let schema_name = cfg.get_name(); 14 | let schema_impl = cfg.type_of.generate_schema(&cfg.attrs); 15 | let instrument = instrument_quote(); 16 | 17 | tokens.extend(quote! { 18 | #[automatically_derived] 19 | impl schematic::Schematic for #name { 20 | fn schema_name() -> Option { 21 | Some(#schema_name.into()) 22 | } 23 | 24 | #instrument 25 | fn build_schema(mut schema: schematic::SchemaBuilder) -> schematic::Schema { 26 | use schematic::schema::*; 27 | 28 | #schema_impl 29 | } 30 | } 31 | }); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /crates/schematic/src/config/cacher.rs: -------------------------------------------------------------------------------- 1 | use super::error::HandlerError; 2 | use std::collections::HashMap; 3 | use std::path::PathBuf; 4 | 5 | /// A system for reading and writing to a cache for URL based configurations. 6 | pub trait Cacher { 7 | /// If the content was cached to the local file system, return the absolute path. 8 | fn get_file_path(&self, _url: &str) -> Result, HandlerError> { 9 | Ok(None) 10 | } 11 | 12 | /// Read content from the cache store. 13 | fn read(&mut self, url: &str) -> Result, HandlerError>; 14 | 15 | /// Write the provided content to the cache store. 16 | fn write(&mut self, url: &str, content: &str) -> Result<(), HandlerError>; 17 | } 18 | 19 | pub type BoxedCacher = Box; 20 | 21 | #[derive(Default)] 22 | #[doc(hidden)] 23 | pub struct MemoryCache { 24 | cache: HashMap, 25 | } 26 | 27 | impl Cacher for MemoryCache { 28 | fn read(&mut self, url: &str) -> Result, HandlerError> { 29 | Ok(self.cache.get(url).map(|v| v.to_owned())) 30 | } 31 | 32 | fn write(&mut self, url: &str, content: &str) -> Result<(), HandlerError> { 33 | self.cache.insert(url.to_owned(), content.to_owned()); 34 | 35 | Ok(()) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /crates/schematic/src/config/extender.rs: -------------------------------------------------------------------------------- 1 | use crate::derive_enum; 2 | use schematic_types::{Schema, SchemaBuilder, Schematic, UnionType}; 3 | 4 | derive_enum!( 5 | /// Represents an extendable setting, either a string or a list of strings. 6 | #[serde(untagged)] 7 | pub enum ExtendsFrom { 8 | String(String), 9 | List(Vec), 10 | } 11 | ); 12 | 13 | impl Default for ExtendsFrom { 14 | fn default() -> Self { 15 | Self::List(vec![]) 16 | } 17 | } 18 | 19 | impl Schematic for ExtendsFrom { 20 | fn schema_name() -> Option { 21 | Some("ExtendsFrom".into()) 22 | } 23 | 24 | fn build_schema(mut schema: SchemaBuilder) -> Schema { 25 | schema.union(UnionType::new_any([ 26 | schema.infer::(), 27 | schema.infer::>(), 28 | ])) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /crates/schematic/src/config/formats/json.rs: -------------------------------------------------------------------------------- 1 | use super::create_span; 2 | use crate::config::error::ConfigError; 3 | use crate::config::parser::ParserError; 4 | use miette::NamedSource; 5 | use serde::de::DeserializeOwned; 6 | 7 | pub fn parse(name: &str, content: &str) -> Result 8 | where 9 | D: DeserializeOwned, 10 | { 11 | let de = 12 | &mut serde_json::Deserializer::from_str(if content.is_empty() { "{}" } else { content }); 13 | 14 | let result: D = serde_path_to_error::deserialize(de).map_err(|error| ParserError { 15 | content: NamedSource::new(name, content.to_owned()), 16 | path: error.path().to_string(), 17 | span: Some(create_span( 18 | content, 19 | error.inner().line(), 20 | error.inner().column(), 21 | )), 22 | message: error.inner().to_string(), 23 | })?; 24 | 25 | Ok(result) 26 | } 27 | -------------------------------------------------------------------------------- /crates/schematic/src/config/formats/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "json")] 2 | mod json; 3 | #[cfg(feature = "pkl")] 4 | mod pkl; 5 | #[cfg(feature = "ron")] 6 | mod ron; 7 | #[cfg(feature = "toml")] 8 | mod toml; 9 | #[cfg(any(feature = "yaml", feature = "yml"))] 10 | mod yaml; 11 | 12 | use super::error::ConfigError; 13 | use crate::format::Format; 14 | use miette::{SourceOffset, SourceSpan}; 15 | use serde::de::DeserializeOwned; 16 | use std::path::Path; 17 | use tracing::instrument; 18 | 19 | pub(super) fn create_span(content: &str, line: usize, column: usize) -> SourceSpan { 20 | let offset = SourceOffset::from_location(content, line, column).offset(); 21 | let length = 0; 22 | 23 | (offset, length).into() 24 | } 25 | 26 | impl Format { 27 | /// Parse the provided content in the defined format into a partial configuration struct. 28 | /// On failure, will attempt to extract the path to the problematic field and source 29 | /// code spans (for use in `miette`). 30 | #[instrument(name = "parse_format", skip(content), fields(format = ?self))] 31 | pub fn parse( 32 | &self, 33 | location: &str, 34 | content: &str, 35 | file_path: Option<&Path>, 36 | ) -> Result 37 | where 38 | D: DeserializeOwned, 39 | { 40 | match self { 41 | Format::None => unreachable!(), 42 | 43 | #[cfg(feature = "json")] 44 | Format::Json => json::parse(location, content), 45 | 46 | #[cfg(feature = "pkl")] 47 | Format::Pkl => pkl::parse(location, content, file_path), 48 | 49 | #[cfg(feature = "ron")] 50 | Format::Ron => ron::parse(location, content), 51 | 52 | #[cfg(feature = "toml")] 53 | Format::Toml => toml::parse(location, content), 54 | 55 | #[cfg(any(feature = "yaml", feature = "yml"))] 56 | Format::Yaml => yaml::parse(location, content), 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /crates/schematic/src/config/formats/pkl.rs: -------------------------------------------------------------------------------- 1 | use crate::config::error::ConfigError; 2 | use crate::config::parser::ParserError; 3 | use miette::NamedSource; 4 | use rpkl::pkl::PklSerialize; 5 | use serde::de::DeserializeOwned; 6 | use std::env; 7 | use std::path::Path; 8 | use std::sync::atomic::{AtomicBool, Ordering}; 9 | 10 | static PKL_CHECKED: AtomicBool = AtomicBool::new(false); 11 | 12 | fn check_pkl_installed() -> Result<(), ConfigError> { 13 | if !PKL_CHECKED.load(Ordering::Relaxed) { 14 | let paths = env::var_os("PATH").unwrap_or_default(); 15 | let mut installed = false; 16 | 17 | for path in env::split_paths(&paths) { 18 | let file_path = path.join(if cfg!(windows) { "pkl.exe" } else { "pkl" }); 19 | 20 | if file_path.exists() && file_path.is_file() { 21 | installed = true; 22 | break; 23 | } 24 | } 25 | 26 | if !installed { 27 | return Err(ConfigError::PklRequired); 28 | } 29 | 30 | PKL_CHECKED.store(true, Ordering::Release); 31 | } 32 | 33 | Ok(()) 34 | } 35 | 36 | pub fn parse(name: &str, content: &str, file_path: Option<&Path>) -> Result 37 | where 38 | D: DeserializeOwned, 39 | { 40 | check_pkl_installed()?; 41 | 42 | let Some(file_path) = file_path else { 43 | return Err(ConfigError::PklFileRequired); 44 | }; 45 | 46 | let handle_error = |error: rpkl::Error| ConfigError::PklEvalFailed { 47 | path: file_path.to_path_buf(), 48 | error: Box::new(error), 49 | }; 50 | 51 | // Based on `rpkl::from_config` 52 | let ast = rpkl::api::Evaluator::new() 53 | .map_err(handle_error)? 54 | .evaluate_module(file_path) 55 | .map_err(handle_error)? 56 | .serialize_pkl_ast() 57 | .map_err(handle_error)?; 58 | 59 | let mut de = rpkl::pkl::Deserializer::from_pkl_map(&ast); 60 | 61 | let result: D = serde_path_to_error::deserialize(&mut de).map_err(|error| ParserError { 62 | content: NamedSource::new(name, content.to_owned()), 63 | path: error.path().to_string(), 64 | span: None, // TODO 65 | message: error.inner().to_string(), 66 | })?; 67 | 68 | Ok(result) 69 | } 70 | -------------------------------------------------------------------------------- /crates/schematic/src/config/formats/ron.rs: -------------------------------------------------------------------------------- 1 | use crate::config::error::ConfigError; 2 | use crate::config::parser::ParserError; 3 | use miette::NamedSource; 4 | use serde::de::DeserializeOwned; 5 | 6 | pub fn parse(name: &str, content: &str) -> Result 7 | where 8 | D: DeserializeOwned, 9 | { 10 | let result: D = ron::from_str(content).map_err(|error| { 11 | // Extract position from error 12 | let position = error.position; 13 | let line = position.line; 14 | let column = position.col; 15 | 16 | ParserError { 17 | content: NamedSource::new(name, content.to_owned()), 18 | path: String::new(), // RON doesn't provide field path info 19 | span: Some(super::create_span(content, line, column)), 20 | message: error.code.to_string(), 21 | } 22 | })?; 23 | 24 | Ok(result) 25 | } 26 | -------------------------------------------------------------------------------- /crates/schematic/src/config/formats/toml.rs: -------------------------------------------------------------------------------- 1 | use crate::config::error::{ConfigError, HandlerError}; 2 | use crate::config::parser::ParserError; 3 | use miette::NamedSource; 4 | use serde::de::DeserializeOwned; 5 | 6 | pub fn parse(name: &str, content: &str) -> Result 7 | where 8 | D: DeserializeOwned, 9 | { 10 | let de = toml::Deserializer::parse(content) 11 | .map_err(|error| ConfigError::Handler(Box::new(HandlerError::new(error.to_string()))))?; 12 | 13 | let result: D = serde_path_to_error::deserialize(de).map_err(|error| ParserError { 14 | content: NamedSource::new(name, content.to_owned()), 15 | path: error.path().to_string(), 16 | span: error.inner().span().map(|s| s.into()), 17 | message: error.inner().message().to_owned(), 18 | })?; 19 | 20 | Ok(result) 21 | } 22 | -------------------------------------------------------------------------------- /crates/schematic/src/config/layer.rs: -------------------------------------------------------------------------------- 1 | use super::configs::Config; 2 | use super::source::Source; 3 | use serde::{Deserialize, Serialize}; 4 | 5 | /// A layer of configuration that was loaded and used to create the final state. 6 | #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] 7 | pub struct Layer { 8 | /// The partial configuration that was loaded. 9 | pub partial: T::Partial, 10 | 11 | /// The source location of the partial. 12 | pub source: Source, 13 | } 14 | -------------------------------------------------------------------------------- /crates/schematic/src/config/merger.rs: -------------------------------------------------------------------------------- 1 | use miette::Diagnostic; 2 | use std::fmt::Display; 3 | use thiserror::Error; 4 | 5 | pub type MergeResult = std::result::Result, MergeError>; 6 | 7 | /// A merger function that receives the previous and next values, the current 8 | /// context, and can return a [`MergeError`] on failure. 9 | pub type Merger = Box MergeResult>; 10 | 11 | /// Error for merge failures. 12 | #[derive(Error, Debug, Diagnostic)] 13 | #[error("{0}")] 14 | pub struct MergeError(pub String); 15 | 16 | impl MergeError { 17 | pub fn new(message: T) -> Self { 18 | Self(message.to_string()) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /crates/schematic/src/config/mod.rs: -------------------------------------------------------------------------------- 1 | mod cacher; 2 | mod configs; 3 | mod error; 4 | #[cfg(feature = "extends")] 5 | mod extender; 6 | mod formats; 7 | mod layer; 8 | mod loader; 9 | mod merger; 10 | mod parser; 11 | mod path; 12 | mod settings; 13 | mod source; 14 | #[cfg(feature = "validate")] 15 | mod validator; 16 | 17 | pub use cacher::*; 18 | pub use configs::*; 19 | pub use error::*; 20 | #[cfg(feature = "extends")] 21 | pub use extender::*; 22 | pub use layer::*; 23 | pub use loader::*; 24 | pub use merger::*; 25 | pub use parser::*; 26 | pub use path::*; 27 | pub use settings::*; 28 | pub use source::*; 29 | #[cfg(feature = "validate")] 30 | pub use validator::*; 31 | 32 | #[macro_export] 33 | macro_rules! derive_enum { 34 | ($impl:item) => { 35 | #[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] 36 | #[serde(rename_all = "kebab-case")] 37 | $impl 38 | }; 39 | } 40 | 41 | pub type DefaultValueResult = std::result::Result, HandlerError>; 42 | pub type TransformResult = std::result::Result; 43 | 44 | #[cfg(feature = "env")] 45 | pub type ParseEnvResult = std::result::Result, HandlerError>; 46 | -------------------------------------------------------------------------------- /crates/schematic/src/config/parser.rs: -------------------------------------------------------------------------------- 1 | use miette::{Diagnostic, NamedSource, SourceSpan}; 2 | use starbase_styles::{Style, Stylize}; 3 | use std::borrow::Borrow; 4 | use thiserror::Error; 5 | 6 | /// Error for a single parse failure. 7 | #[derive(Clone, Debug, Diagnostic, Error)] 8 | #[error("{message}")] 9 | pub struct ParseError { 10 | /// Failure message. 11 | pub message: String, 12 | } 13 | 14 | impl ParseError { 15 | /// Create a new parse error with the provided message. 16 | pub fn new>(message: T) -> Self { 17 | ParseError { 18 | message: message.as_ref().to_owned(), 19 | } 20 | } 21 | } 22 | 23 | /// Error related to serde parsing. 24 | #[derive(Debug, Diagnostic, Error)] 25 | #[error("{}{} {message}", .path.style(Style::Id), ":".style(Style::MutedLight))] 26 | #[diagnostic(severity(Error))] 27 | pub struct ParserError { 28 | /// Source code snippet related to the error. 29 | #[source_code] 30 | pub content: NamedSource, 31 | 32 | /// Failure message. 33 | pub message: String, 34 | 35 | /// Dot-notated path to the field that failed. 36 | pub path: String, 37 | 38 | /// Span to the error location. 39 | #[label("Fix this")] 40 | pub span: Option, 41 | } 42 | 43 | impl Borrow for Box { 44 | fn borrow(&self) -> &(dyn Diagnostic + 'static) { 45 | self.as_ref() 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /crates/schematic/src/config/path.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{self, Display}; 2 | 3 | /// Represents all the different forms a path is composed of. 4 | #[derive(Clone, Debug)] 5 | pub enum PathSegment { 6 | /// List index: `[0]` 7 | Index(usize), 8 | /// Map key: `name.` 9 | Key(String), 10 | /// Enum variant: `name.` 11 | Variant(String), 12 | /// Unknown segment: `?` 13 | Unknown, 14 | } 15 | 16 | /// Represents the path from the configuration root to a nested field or field value. 17 | #[derive(Clone, Debug, Default)] 18 | pub struct Path { 19 | /// List of path segments. 20 | segments: Vec, 21 | } 22 | 23 | impl Path { 24 | /// Create a new instance with the provided [`PathSegment`]s. 25 | pub fn new(segments: Vec) -> Self { 26 | Self { segments } 27 | } 28 | 29 | /// Create a new instance and append the provided [`PathSegment`] 30 | /// to the end of the current path. 31 | pub fn join(&self, segment: PathSegment) -> Self { 32 | let mut path = self.clone(); 33 | path.segments.push(segment); 34 | path 35 | } 36 | 37 | /// Create a new instance and append an `Index` [`PathSegment`] 38 | /// to the end of the current path. 39 | pub fn join_index(&self, index: usize) -> Self { 40 | self.join(PathSegment::Index(index)) 41 | } 42 | 43 | /// Create a new instance and append an `Key` [`PathSegment`] 44 | /// to the end of the current path. 45 | pub fn join_key(&self, key: impl Display) -> Self { 46 | self.join(PathSegment::Key(format!("{key}"))) 47 | } 48 | 49 | /// Create a new instance and append another [`Path`] 50 | /// to the end of the current path. 51 | pub fn join_path(&self, other: &Self) -> Self { 52 | let mut path = self.clone(); 53 | path.segments.extend(other.segments.clone()); 54 | path 55 | } 56 | 57 | /// Create a new instance and append an `Variant` [`PathSegment`] 58 | /// to the end of the current path. 59 | pub fn join_variant(&self, variant: &str) -> Self { 60 | self.join(PathSegment::Variant(variant.to_owned())) 61 | } 62 | } 63 | 64 | impl Display for Path { 65 | fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 66 | if self.segments.is_empty() { 67 | return formatter.write_str("."); 68 | } 69 | 70 | let mut separator = ""; 71 | 72 | for segment in &self.segments { 73 | match segment { 74 | PathSegment::Index(index) => { 75 | write!(formatter, "[{index}]")?; 76 | } 77 | PathSegment::Key(key) | PathSegment::Variant(key) => { 78 | write!(formatter, "{separator}{key}")?; 79 | } 80 | PathSegment::Unknown => { 81 | write!(formatter, "{separator}?")?; 82 | } 83 | } 84 | 85 | separator = "."; 86 | } 87 | 88 | Ok(()) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /crates/schematic/src/config/settings/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "type_regex")] 2 | mod regex; 3 | #[cfg(feature = "type_semver")] 4 | mod semver; 5 | 6 | #[cfg(feature = "type_regex")] 7 | pub use regex::*; 8 | #[cfg(feature = "type_semver")] 9 | pub use semver::*; 10 | -------------------------------------------------------------------------------- /crates/schematic/src/config/settings/regex.rs: -------------------------------------------------------------------------------- 1 | use crate::schema::{Schema, SchemaBuilder, Schematic}; 2 | use regex::{Error, Regex}; 3 | use serde::{Deserialize, Serialize}; 4 | use std::hash::{Hash, Hasher}; 5 | use std::ops::Deref; 6 | use std::str::FromStr; 7 | 8 | #[derive(Clone, Debug, Deserialize, Serialize)] 9 | #[serde(try_from = "String", into = "String")] 10 | pub struct RegexSetting(pub Regex); 11 | 12 | impl RegexSetting { 13 | pub fn new(value: impl AsRef) -> Result { 14 | Ok(Self(Regex::new(value.as_ref())?)) 15 | } 16 | } 17 | 18 | impl Default for RegexSetting { 19 | fn default() -> Self { 20 | Self(Regex::new(".").unwrap()) 21 | } 22 | } 23 | 24 | impl Deref for RegexSetting { 25 | type Target = Regex; 26 | 27 | fn deref(&self) -> &Self::Target { 28 | &self.0 29 | } 30 | } 31 | 32 | impl FromStr for RegexSetting { 33 | type Err = Error; 34 | 35 | fn from_str(value: &str) -> Result { 36 | Self::new(value) 37 | } 38 | } 39 | 40 | impl TryFrom<&str> for RegexSetting { 41 | type Error = Error; 42 | 43 | fn try_from(value: &str) -> Result { 44 | Self::new(value) 45 | } 46 | } 47 | 48 | impl TryFrom for RegexSetting { 49 | type Error = Error; 50 | 51 | fn try_from(value: String) -> Result { 52 | Self::new(value) 53 | } 54 | } 55 | 56 | #[allow(clippy::from_over_into)] 57 | impl Into for RegexSetting { 58 | fn into(self) -> String { 59 | self.to_string() 60 | } 61 | } 62 | 63 | impl PartialEq for RegexSetting { 64 | fn eq(&self, other: &RegexSetting) -> bool { 65 | self.as_str() == other.as_str() 66 | } 67 | } 68 | 69 | impl Eq for RegexSetting {} 70 | 71 | impl Hash for RegexSetting { 72 | fn hash(&self, state: &mut H) { 73 | state.write(self.0.as_str().as_bytes()); 74 | } 75 | } 76 | 77 | impl Schematic for RegexSetting { 78 | fn build_schema(_: SchemaBuilder) -> Schema { 79 | SchemaBuilder::generate::() 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /crates/schematic/src/config/settings/semver.rs: -------------------------------------------------------------------------------- 1 | use crate::schema::{Schema, SchemaBuilder, Schematic}; 2 | use semver::{Error, Version}; 3 | use serde::{Deserialize, Serialize}; 4 | use std::hash::{Hash, Hasher}; 5 | use std::ops::Deref; 6 | use std::str::FromStr; 7 | 8 | #[derive(Clone, Debug, Deserialize, Serialize)] 9 | #[serde(try_from = "String", into = "String")] 10 | pub struct VersionSetting(pub Version); 11 | 12 | impl VersionSetting { 13 | pub fn new(value: impl AsRef) -> Result { 14 | Ok(Self(Version::parse(value.as_ref())?)) 15 | } 16 | } 17 | 18 | impl Default for VersionSetting { 19 | fn default() -> Self { 20 | Self(Version::new(0, 0, 0)) 21 | } 22 | } 23 | 24 | impl Deref for VersionSetting { 25 | type Target = Version; 26 | 27 | fn deref(&self) -> &Self::Target { 28 | &self.0 29 | } 30 | } 31 | 32 | impl FromStr for VersionSetting { 33 | type Err = Error; 34 | 35 | fn from_str(value: &str) -> Result { 36 | Self::new(value) 37 | } 38 | } 39 | 40 | impl TryFrom<&str> for VersionSetting { 41 | type Error = Error; 42 | 43 | fn try_from(value: &str) -> Result { 44 | Self::new(value) 45 | } 46 | } 47 | 48 | impl TryFrom for VersionSetting { 49 | type Error = Error; 50 | 51 | fn try_from(value: String) -> Result { 52 | Self::new(value) 53 | } 54 | } 55 | 56 | #[allow(clippy::from_over_into)] 57 | impl Into for VersionSetting { 58 | fn into(self) -> String { 59 | self.to_string() 60 | } 61 | } 62 | 63 | impl PartialEq for VersionSetting { 64 | fn eq(&self, other: &VersionSetting) -> bool { 65 | self.0.eq(&other.0) 66 | } 67 | } 68 | 69 | impl Eq for VersionSetting {} 70 | 71 | impl Hash for VersionSetting { 72 | fn hash(&self, state: &mut H) { 73 | self.0.hash(state); 74 | } 75 | } 76 | 77 | impl Schematic for VersionSetting { 78 | fn build_schema(_: SchemaBuilder) -> Schema { 79 | SchemaBuilder::generate::() 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /crates/schematic/src/env.rs: -------------------------------------------------------------------------------- 1 | use crate::{ParseEnvResult, internal}; 2 | use std::str::FromStr; 3 | 4 | /// Ignore the environment variable if it's empty and fallback to the previous or default value. 5 | pub fn ignore_empty(var: String) -> ParseEnvResult { 6 | let var = var.trim(); 7 | 8 | if var.is_empty() { 9 | return Ok(None); 10 | } 11 | 12 | internal::parse_value(var).map(|v| Some(v)) 13 | } 14 | 15 | /// Parse a string into a boolean. Will parse `1`, `true`, `yes`, `on`, 16 | /// and `enabled` as true, and everything else as false. 17 | pub fn parse_bool(var: String) -> ParseEnvResult { 18 | Ok(match var.to_lowercase().as_str() { 19 | "1" | "true" | "yes" | "on" | "enabled" | "enable" => Some(true), 20 | _ => Some(false), 21 | }) 22 | } 23 | 24 | fn split(var: String, delim: char) -> ParseEnvResult> { 25 | let mut list = vec![]; 26 | 27 | for s in var.split(delim) { 28 | let value = s.trim(); 29 | 30 | if !value.is_empty() { 31 | list.push(internal::parse_value(value)?); 32 | } 33 | } 34 | 35 | Ok(Some(list)) 36 | } 37 | 38 | /// Split a variable on each comma (`,`) and parse into a list of values. 39 | pub fn split_comma(var: String) -> ParseEnvResult> { 40 | split(var, ',') 41 | } 42 | 43 | /// Split a variable on each colon (`:`) and parse into a list of values. 44 | pub fn split_colon(var: String) -> ParseEnvResult> { 45 | split(var, ':') 46 | } 47 | 48 | /// Split a variable on each semicolon (`;`) and parse into a list of values. 49 | pub fn split_semicolon(var: String) -> ParseEnvResult> { 50 | split(var, ';') 51 | } 52 | 53 | /// Split a variable on each space (` `) and parse into a list of values. 54 | pub fn split_space(var: String) -> ParseEnvResult> { 55 | split(var, ' ') 56 | } 57 | -------------------------------------------------------------------------------- /crates/schematic/src/helpers.rs: -------------------------------------------------------------------------------- 1 | /// Returns true if the value ends in a supported file extension. 2 | pub fn is_source_format(value: &str) -> bool { 3 | extract_ext(value).is_some_and(|ext| { 4 | ext == ".json" || ext == ".pkl" || ext == ".toml" || ext == ".yaml" || ext == ".yml" 5 | }) 6 | } 7 | 8 | /// Returns true if the value looks like a file, by checking for `file://`, 9 | /// path separators, or supported file extensions. 10 | pub fn is_file_like(value: &str) -> bool { 11 | value.starts_with("file://") 12 | || value.starts_with('/') 13 | || value.starts_with('\\') 14 | || value.starts_with('.') 15 | || value.contains('/') 16 | || value.contains('\\') 17 | || value.contains('.') 18 | } 19 | 20 | /// Returns true if the value looks like a URL, by checking for `http://`, `https://`, or `www`. 21 | pub fn is_url_like(value: &str) -> bool { 22 | value.starts_with("https://") || value.starts_with("http://") || value.starts_with("www") 23 | } 24 | 25 | /// Returns true if the value is a secure URL, by checking for `https://`. This check can be 26 | /// bypassed for localhost URLs. 27 | pub fn is_secure_url(value: &str) -> bool { 28 | if value.contains("127.0.0.1") || value.contains("//localhost") { 29 | return true; 30 | } 31 | 32 | value.starts_with("https://") 33 | } 34 | 35 | /// Strip a leading BOM from the string. 36 | pub fn strip_bom(content: &str) -> &str { 37 | content.trim_start_matches("\u{feff}") 38 | } 39 | 40 | /// Extract a file extension from the provided file path or URL. 41 | pub fn extract_ext(value: &str) -> Option<&str> { 42 | // Remove any query string 43 | let value = if let Some(index) = value.rfind('?') { 44 | &value[0..index] 45 | } else { 46 | value 47 | }; 48 | 49 | // And only check the last segment 50 | let value = if let Some(index) = value.rfind('/') { 51 | &value[index + 1..] 52 | } else { 53 | value 54 | }; 55 | 56 | value.rfind('.').map(|index| &value[index..]) 57 | } 58 | -------------------------------------------------------------------------------- /crates/schematic/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::result_large_err)] 2 | 3 | mod format; 4 | pub mod helpers; 5 | 6 | #[cfg(feature = "config")] 7 | mod config; 8 | 9 | /// Built-in `parse_env` functions. 10 | #[cfg(all(feature = "config", feature = "env"))] 11 | pub mod env; 12 | 13 | #[cfg(feature = "config")] 14 | #[doc(hidden)] 15 | pub mod internal; 16 | 17 | /// Built-in `merge` functions. 18 | #[cfg(feature = "config")] 19 | pub mod merge; 20 | 21 | /// Generate schemas to render into outputs. 22 | #[cfg(feature = "schema")] 23 | pub mod schema; 24 | 25 | /// Built-in `validate` functions. 26 | #[cfg(all(feature = "config", feature = "validate"))] 27 | pub mod validate; 28 | 29 | /// ASCII color helpers for use within error messages. 30 | #[cfg(feature = "config")] 31 | pub use starbase_styles::color; 32 | 33 | #[cfg(feature = "config")] 34 | pub use config::*; 35 | 36 | pub use format::*; 37 | pub use schematic_macros::*; 38 | pub use schematic_types::{Schema, SchemaBuilder, SchemaType, Schematic}; 39 | -------------------------------------------------------------------------------- /crates/schematic/src/merge.rs: -------------------------------------------------------------------------------- 1 | use crate::config::MergeResult; 2 | use std::{ 3 | collections::{BTreeMap, BTreeSet, HashMap, HashSet}, 4 | hash::Hash, 5 | }; 6 | 7 | /// Discard both previous and next values and return [`None`]. 8 | pub fn discard(_: T, _: T, _: &C) -> MergeResult { 9 | Ok(None) 10 | } 11 | 12 | /// Always preserve the previous value over the next value. 13 | pub fn preserve(prev: T, _: T, _: &C) -> MergeResult { 14 | Ok(Some(prev)) 15 | } 16 | 17 | /// Always replace the previous value with the next value. 18 | pub fn replace(_: T, next: T, _: &C) -> MergeResult { 19 | Ok(Some(next)) 20 | } 21 | 22 | /// Append the items from the next vector to the end of the previous vector. 23 | pub fn append_vec(mut prev: Vec, next: Vec, _: &C) -> MergeResult> { 24 | prev.extend(next); 25 | 26 | Ok(Some(prev)) 27 | } 28 | 29 | /// Prepend the items from the next vector to the start of the previous vector. 30 | pub fn prepend_vec(prev: Vec, next: Vec, _: &C) -> MergeResult> { 31 | let mut new = vec![]; 32 | new.extend(next); 33 | new.extend(prev); 34 | 35 | Ok(Some(new)) 36 | } 37 | 38 | /// Shallow merge the next [`BTreeMap`] into the previous [`BTreeMap`]. Any items in the 39 | /// next [`BTreeMap`] will overwrite items in the previous [`BTreeMap`] of the same key. 40 | pub fn merge_btreemap( 41 | mut prev: BTreeMap, 42 | next: BTreeMap, 43 | _: &C, 44 | ) -> MergeResult> 45 | where 46 | K: Eq + Hash + Ord, 47 | { 48 | for (key, value) in next { 49 | prev.insert(key, value); 50 | } 51 | 52 | Ok(Some(prev)) 53 | } 54 | 55 | /// Shallow merge the next [`BTreeSet`] into the previous [`BTreeSet`], overwriting duplicates. 56 | pub fn merge_btreeset( 57 | mut prev: BTreeSet, 58 | next: BTreeSet, 59 | _: &C, 60 | ) -> MergeResult> 61 | where 62 | T: Eq + Hash + Ord, 63 | { 64 | for item in next { 65 | prev.insert(item); 66 | } 67 | 68 | Ok(Some(prev)) 69 | } 70 | 71 | /// Shallow merge the next [`HashMap`] into the previous [`HashMap`]. Any items in the 72 | /// next [`HashMap`] will overwrite items in the previous [`HashMap`] of the same key. 73 | pub fn merge_hashmap( 74 | mut prev: HashMap, 75 | next: HashMap, 76 | _: &C, 77 | ) -> MergeResult> 78 | where 79 | K: Eq + Hash, 80 | { 81 | for (key, value) in next { 82 | prev.insert(key, value); 83 | } 84 | 85 | Ok(Some(prev)) 86 | } 87 | 88 | /// Shallow merge the next [`HashSet`] into the previous [`HashSet`], overwriting duplicates. 89 | pub fn merge_hashset(mut prev: HashSet, next: HashSet, _: &C) -> MergeResult> 90 | where 91 | T: Eq + Hash, 92 | { 93 | for item in next { 94 | prev.insert(item); 95 | } 96 | 97 | Ok(Some(prev)) 98 | } 99 | -------------------------------------------------------------------------------- /crates/schematic/src/schema/generator.rs: -------------------------------------------------------------------------------- 1 | use super::SchemaRenderer; 2 | use indexmap::IndexMap; 3 | use miette::IntoDiagnostic; 4 | use schematic_types::*; 5 | use std::fs; 6 | use std::path::Path; 7 | 8 | /// A generator collects [`Schema`]s and renders them to a specific file, 9 | /// using a renderer that implements [`SchemaRenderer`]. 10 | #[derive(Debug, Default)] 11 | pub struct SchemaGenerator { 12 | pub schemas: IndexMap, 13 | } 14 | 15 | impl SchemaGenerator { 16 | /// Add a [`Schema`] to be rendered, derived from the provided [`Schematic`]. 17 | pub fn add(&mut self) { 18 | let schema = SchemaBuilder::build_root::(); 19 | self.add_schema(&schema); 20 | } 21 | 22 | /// Add an explicit [`Schema`] to be rendered, and recursively add any nested schemas. 23 | /// Schemas with a name will be considered a reference. 24 | pub fn add_schema(&mut self, schema: &Schema) { 25 | let mut schema = schema.to_owned(); 26 | 27 | // Recursively add any nested schema types 28 | match &mut schema.ty { 29 | SchemaType::Array(inner) => { 30 | self.add_schema(&inner.items_type); 31 | } 32 | SchemaType::Object(inner) => { 33 | self.add_schema(&inner.key_type); 34 | self.add_schema(&inner.value_type); 35 | } 36 | SchemaType::Struct(inner) => { 37 | for field in inner.fields.values() { 38 | self.add_schema(&field.schema); 39 | } 40 | } 41 | SchemaType::Tuple(inner) => { 42 | for item in &inner.items_types { 43 | self.add_schema(item); 44 | } 45 | } 46 | SchemaType::Union(inner) => { 47 | for variant in &inner.variants_types { 48 | self.add_schema(variant); 49 | } 50 | } 51 | _ => {} 52 | }; 53 | 54 | // Store the name so that we can use it as a reference for other types 55 | if let Some(name) = &schema.name { 56 | // Types without a name cannot be rendered at the root 57 | if !self.schemas.contains_key(name) { 58 | self.schemas.insert(name.to_owned(), schema); 59 | } 60 | } 61 | } 62 | 63 | /// Generate an output by rendering all collected [`Schema`]s using the provided 64 | /// [`SchemaRenderer`], and finally write to the provided file path. 65 | pub fn generate, O, R: SchemaRenderer>( 66 | &self, 67 | output_file: P, 68 | mut renderer: R, 69 | ) -> miette::Result<()> { 70 | let output_file = output_file.as_ref(); 71 | 72 | let mut output = renderer.render(self.schemas.clone())?; 73 | output.push('\n'); 74 | 75 | if let Some(parent) = output_file.parent() { 76 | fs::create_dir_all(parent).into_diagnostic()?; 77 | } 78 | 79 | fs::write(output_file, output).into_diagnostic()?; 80 | 81 | Ok(()) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /crates/schematic/src/schema/mod.rs: -------------------------------------------------------------------------------- 1 | mod generator; 2 | mod renderer; 3 | mod renderers; 4 | 5 | pub use generator::*; 6 | pub use indexmap; 7 | pub use renderer::*; 8 | pub use schematic_types::*; 9 | 10 | /// Renders JSON schemas. 11 | #[cfg(feature = "renderer_json_schema")] 12 | pub use renderers::json_schema::{self, *}; 13 | 14 | /// Renders JSON config templates. 15 | #[cfg(all(feature = "renderer_template", feature = "json"))] 16 | pub use renderers::json_template::*; 17 | 18 | /// Renders JSONC config templates. 19 | #[cfg(all(feature = "renderer_template", feature = "json"))] 20 | pub use renderers::jsonc_template::*; 21 | 22 | /// Renders Pkl config templates. 23 | #[cfg(all(feature = "renderer_template", feature = "pkl"))] 24 | pub use renderers::pkl_template::*; 25 | 26 | /// Helpers for config templates. 27 | #[cfg(feature = "renderer_template")] 28 | pub use renderers::template::TemplateOptions; 29 | 30 | /// Renders TOML config templates. 31 | #[cfg(all(feature = "renderer_template", feature = "toml"))] 32 | pub use renderers::toml_template::*; 33 | 34 | /// Renders TypeScript types. 35 | #[cfg(feature = "renderer_typescript")] 36 | pub use renderers::typescript::{self, *}; 37 | 38 | /// Renders YAML config templates. 39 | #[cfg(all(feature = "renderer_template", any(feature = "yaml", feature = "yml")))] 40 | pub use renderers::yaml_template::*; 41 | -------------------------------------------------------------------------------- /crates/schematic/src/schema/renderers/json_template.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::should_implement_trait)] 2 | #![allow(clippy::new_ret_no_self)] 3 | 4 | use super::jsonc_template::JsoncTemplateRenderer; 5 | use super::template::TemplateOptions; 6 | use std::mem; 7 | 8 | /// Renders JSON config templates without comments. 9 | pub struct JsonTemplateRenderer; 10 | 11 | impl JsonTemplateRenderer { 12 | pub fn default() -> JsoncTemplateRenderer { 13 | Self::new(TemplateOptions::default()) 14 | } 15 | 16 | pub fn new(mut options: TemplateOptions) -> JsoncTemplateRenderer { 17 | options.comments = false; 18 | options 19 | .hide_fields 20 | .extend(mem::take(&mut options.comment_fields)); 21 | options.newline_between_fields = false; 22 | 23 | JsoncTemplateRenderer::new(options) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /crates/schematic/src/schema/renderers/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "renderer_json_schema")] 2 | pub mod json_schema; 3 | 4 | #[cfg(all(feature = "renderer_template", feature = "json"))] 5 | pub mod json_template; 6 | 7 | #[cfg(all(feature = "renderer_template", feature = "json"))] 8 | pub mod jsonc_template; 9 | 10 | #[cfg(all(feature = "renderer_template", feature = "pkl"))] 11 | pub mod pkl_template; 12 | 13 | #[cfg(feature = "renderer_template")] 14 | pub mod template; 15 | 16 | #[cfg(all(feature = "renderer_template", feature = "toml"))] 17 | pub mod toml_template; 18 | 19 | #[cfg(feature = "renderer_typescript")] 20 | pub mod typescript; 21 | 22 | #[cfg(all(feature = "renderer_template", any(feature = "yaml", feature = "yml")))] 23 | pub mod yaml_template; 24 | -------------------------------------------------------------------------------- /crates/schematic/src/validate/email.rs: -------------------------------------------------------------------------------- 1 | use super::{ValidateResult, map_err}; 2 | pub use garde::rules::email::Email; 3 | 4 | /// Validate a string matches an email address. 5 | pub fn email( 6 | value: &T, 7 | _data: &D, 8 | _context: &C, 9 | _finalize: bool, 10 | ) -> ValidateResult { 11 | garde::rules::email::apply(value, ()).map_err(map_err) 12 | } 13 | -------------------------------------------------------------------------------- /crates/schematic/src/validate/ip.rs: -------------------------------------------------------------------------------- 1 | use super::{ValidateResult, map_err}; 2 | pub use garde::rules::ip::{Ip, IpKind}; 3 | 4 | /// Validate a string is either an IP v4 or v6 address. 5 | pub fn ip(value: &T, _data: &D, _context: &C, _finalize: bool) -> ValidateResult { 6 | garde::rules::ip::apply(value, (IpKind::Any,)).map_err(map_err) 7 | } 8 | 9 | /// Validate a string is either an IP v4 address. 10 | pub fn ip_v4(value: &T, _data: &D, _context: &C, _finalize: bool) -> ValidateResult { 11 | garde::rules::ip::apply(value, (IpKind::V4,)).map_err(map_err) 12 | } 13 | 14 | /// Validate a string is either an IP v6 address. 15 | pub fn ip_v6(value: &T, _data: &D, _context: &C, _finalize: bool) -> ValidateResult { 16 | garde::rules::ip::apply(value, (IpKind::V6,)).map_err(map_err) 17 | } 18 | -------------------------------------------------------------------------------- /crates/schematic/src/validate/length.rs: -------------------------------------------------------------------------------- 1 | use super::{Validator, map_err}; 2 | pub use garde::rules::length::{ 3 | HasSimpleLength, 4 | bytes::{Bytes, HasBytes}, 5 | chars::{Chars, HasChars}, 6 | simple::Simple, 7 | }; 8 | 9 | /// Validate a value is within the provided length. 10 | pub fn in_length(min: usize, max: usize) -> Validator { 11 | Box::new(move |value, _, _, _| T::validate_length(value, min, max).map_err(map_err)) 12 | } 13 | 14 | /// Validate a value is at least the provided length. 15 | pub fn min_length(min: usize) -> Validator { 16 | Box::new(move |value, _, _, _| T::validate_length(value, min, usize::MAX).map_err(map_err)) 17 | } 18 | 19 | /// Validate a value is at most the provided length. 20 | pub fn max_length(max: usize) -> Validator { 21 | Box::new(move |value, _, _, _| T::validate_length(value, usize::MIN, max).map_err(map_err)) 22 | } 23 | 24 | /// Validate a value has the minimum required number of characters. 25 | pub fn min_chars(min: usize) -> Validator { 26 | Box::new(move |value, _, _, _| T::validate_num_chars(value, min, usize::MAX).map_err(map_err)) 27 | } 28 | 29 | /// Validate a value has the maximum required number of characters. 30 | pub fn max_chars(max: usize) -> Validator { 31 | Box::new(move |value, _, _, _| T::validate_num_chars(value, usize::MIN, max).map_err(map_err)) 32 | } 33 | 34 | /// Validate a value has the minimum required number of bytes. 35 | pub fn min_bytes(min: usize) -> Validator { 36 | Box::new(move |value, _, _, _| T::validate_num_bytes(value, min, usize::MAX).map_err(map_err)) 37 | } 38 | 39 | /// Validate a value has the maximum required number of bytes. 40 | pub fn max_bytes(max: usize) -> Validator { 41 | Box::new(move |value, _, _, _| T::validate_num_bytes(value, usize::MIN, max).map_err(map_err)) 42 | } 43 | -------------------------------------------------------------------------------- /crates/schematic/src/validate/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "validate_email")] 2 | mod email; 3 | #[cfg(feature = "extends")] 4 | mod extends; 5 | mod ip; 6 | mod length; 7 | mod number; 8 | mod string; 9 | #[cfg(feature = "validate_url")] 10 | mod url; 11 | 12 | pub use crate::config::{ValidateError, ValidateResult, Validator}; 13 | #[cfg(feature = "validate_email")] 14 | pub use email::*; 15 | #[cfg(feature = "extends")] 16 | pub use extends::*; 17 | pub use ip::*; 18 | pub use length::*; 19 | pub use number::*; 20 | pub use string::*; 21 | #[cfg(feature = "validate_url")] 22 | pub use url::*; 23 | 24 | pub(crate) fn map_err(error: garde::Error) -> ValidateError { 25 | ValidateError::new(error.to_string()) 26 | } 27 | -------------------------------------------------------------------------------- /crates/schematic/src/validate/number.rs: -------------------------------------------------------------------------------- 1 | use super::{Validator, map_err}; 2 | pub use garde::rules::range::Bounds; 3 | use std::fmt::Display; 4 | 5 | /// Validate a numeric value is between the provided bounds (non-inclusive). 6 | pub fn in_range( 7 | min: T::Size, 8 | max: T::Size, 9 | ) -> Validator { 10 | Box::new(move |value, _, _, _| { 11 | garde::rules::range::apply(value, (Some(min), Some(max))).map_err(map_err) 12 | }) 13 | } 14 | -------------------------------------------------------------------------------- /crates/schematic/src/validate/string.rs: -------------------------------------------------------------------------------- 1 | use super::{ValidateError, ValidateResult, Validator, map_err}; 2 | pub use garde::rules::{ 3 | alphanumeric::Alphanumeric, ascii::Ascii, contains::Contains, length::HasSimpleLength, 4 | pattern::Pattern, 5 | }; 6 | 7 | /// Validate a string is only composed of alpha-numeric characters. 8 | pub fn alphanumeric( 9 | value: &T, 10 | _data: &D, 11 | _context: &C, 12 | _finalize: bool, 13 | ) -> ValidateResult { 14 | garde::rules::alphanumeric::apply(value, ()).map_err(map_err) 15 | } 16 | 17 | /// Validate a string is only composed of ASCII characters. 18 | pub fn ascii( 19 | value: &T, 20 | _data: &D, 21 | _context: &C, 22 | _finalize: bool, 23 | ) -> ValidateResult { 24 | garde::rules::ascii::apply(value, ()).map_err(map_err) 25 | } 26 | 27 | /// Validate a string contains the provided pattern. 28 | pub fn contains(pattern: &str) -> Validator { 29 | let pattern = pattern.to_owned(); 30 | 31 | Box::new(move |value, _, _, _| { 32 | garde::rules::contains::apply(value, (&pattern,)).map_err(map_err) 33 | }) 34 | } 35 | 36 | /// Validate a string matches the provided regex pattern. 37 | pub fn regex(pattern: &str) -> Validator { 38 | let pattern = garde::rules::pattern::regex::Regex::new(pattern).unwrap(); 39 | 40 | Box::new(move |value, _, _, _| { 41 | garde::rules::pattern::apply(value, (&pattern,)).map_err(map_err) 42 | }) 43 | } 44 | 45 | /// Validate the value is not empty. 46 | pub fn not_empty( 47 | value: &T, 48 | _data: &D, 49 | _context: &C, 50 | _finalize: bool, 51 | ) -> ValidateResult { 52 | if value.length() == 0 { 53 | return Err(ValidateError::new("must not be empty")); 54 | } 55 | 56 | Ok(()) 57 | } 58 | -------------------------------------------------------------------------------- /crates/schematic/src/validate/url.rs: -------------------------------------------------------------------------------- 1 | use super::{ValidateError, ValidateResult, map_err}; 2 | use crate::helpers::is_secure_url; 3 | pub use garde::rules::url::Url; 4 | 5 | /// Validate a string matches a URL. 6 | pub fn url(value: &T, _data: &D, _context: &C, _finalize: bool) -> ValidateResult { 7 | garde::rules::url::apply(value, ()).map_err(map_err) 8 | } 9 | 10 | /// Validate a string matches a URL and starts with https://. 11 | pub fn url_secure, D, C>( 12 | value: T, 13 | data: &D, 14 | context: &C, 15 | finalize: bool, 16 | ) -> ValidateResult { 17 | let value = value.as_ref(); 18 | 19 | url(&value, data, context, finalize)?; 20 | 21 | if !is_secure_url(value) { 22 | return Err(ValidateError::new("only secure URLs are allowed")); 23 | } 24 | 25 | Ok(()) 26 | } 27 | -------------------------------------------------------------------------------- /crates/schematic/tests/__fixtures__/extending/base-both.yml: -------------------------------------------------------------------------------- 1 | extends: 2 | - ./list1.yml 3 | - ./string1.yml 4 | - list2.yml 5 | value: [1] 6 | -------------------------------------------------------------------------------- /crates/schematic/tests/__fixtures__/extending/base-list.yml: -------------------------------------------------------------------------------- 1 | extends: 2 | - ./list1.yml 3 | - ./list2.yml 4 | value: [1] 5 | -------------------------------------------------------------------------------- /crates/schematic/tests/__fixtures__/extending/base.yml: -------------------------------------------------------------------------------- 1 | extends: ./string1.yml 2 | value: [1] 3 | -------------------------------------------------------------------------------- /crates/schematic/tests/__fixtures__/extending/list1.yml: -------------------------------------------------------------------------------- 1 | extends: [./string2.yml] 2 | value: [2] 3 | -------------------------------------------------------------------------------- /crates/schematic/tests/__fixtures__/extending/list2.yml: -------------------------------------------------------------------------------- 1 | value: [4] 2 | -------------------------------------------------------------------------------- /crates/schematic/tests/__fixtures__/extending/string1.yml: -------------------------------------------------------------------------------- 1 | extends: ./string2.yml 2 | value: [2] 3 | -------------------------------------------------------------------------------- /crates/schematic/tests/__fixtures__/extending/string2.yml: -------------------------------------------------------------------------------- 1 | value: [3] 2 | -------------------------------------------------------------------------------- /crates/schematic/tests/__fixtures__/json/five.json: -------------------------------------------------------------------------------- 1 | { 2 | "vector": ["x", "y", "z"] 3 | } 4 | -------------------------------------------------------------------------------- /crates/schematic/tests/__fixtures__/json/four.json: -------------------------------------------------------------------------------- 1 | { 2 | "boolean": false, 3 | "number": 123 4 | } 5 | -------------------------------------------------------------------------------- /crates/schematic/tests/__fixtures__/json/one.json: -------------------------------------------------------------------------------- 1 | { 2 | "string": "foo" 3 | } 4 | -------------------------------------------------------------------------------- /crates/schematic/tests/__fixtures__/json/three.json: -------------------------------------------------------------------------------- 1 | { 2 | "boolean": true, 3 | "string": "bar" 4 | } 5 | -------------------------------------------------------------------------------- /crates/schematic/tests/__fixtures__/json/two.json: -------------------------------------------------------------------------------- 1 | { 2 | "vector": ["a", "b", "c"] 3 | } 4 | -------------------------------------------------------------------------------- /crates/schematic/tests/__fixtures__/pkl/five.pkl: -------------------------------------------------------------------------------- 1 | vector = List("x", "y", "z") 2 | -------------------------------------------------------------------------------- /crates/schematic/tests/__fixtures__/pkl/four.pkl: -------------------------------------------------------------------------------- 1 | boolean = false 2 | number = 123 3 | -------------------------------------------------------------------------------- /crates/schematic/tests/__fixtures__/pkl/invalid-nested-type.pkl: -------------------------------------------------------------------------------- 1 | nested { 2 | setting = 123 3 | } 4 | -------------------------------------------------------------------------------- /crates/schematic/tests/__fixtures__/pkl/invalid-type.pkl: -------------------------------------------------------------------------------- 1 | setting = 123 2 | -------------------------------------------------------------------------------- /crates/schematic/tests/__fixtures__/pkl/one.pkl: -------------------------------------------------------------------------------- 1 | string = "foo" 2 | -------------------------------------------------------------------------------- /crates/schematic/tests/__fixtures__/pkl/three.pkl: -------------------------------------------------------------------------------- 1 | boolean = true 2 | string = "bar" 3 | -------------------------------------------------------------------------------- /crates/schematic/tests/__fixtures__/pkl/two.pkl: -------------------------------------------------------------------------------- 1 | vector = List("a", "b", "c") 2 | -------------------------------------------------------------------------------- /crates/schematic/tests/__fixtures__/pkl/variables.pkl: -------------------------------------------------------------------------------- 1 | hidden start = List("a", "b") 2 | 3 | list = List(start[0], start[1]) 4 | list = list.add("c") 5 | -------------------------------------------------------------------------------- /crates/schematic/tests/__fixtures__/toml/five.toml: -------------------------------------------------------------------------------- 1 | vector = ['x', 'y', 'z'] 2 | -------------------------------------------------------------------------------- /crates/schematic/tests/__fixtures__/toml/four.toml: -------------------------------------------------------------------------------- 1 | boolean = false 2 | number = 123 3 | -------------------------------------------------------------------------------- /crates/schematic/tests/__fixtures__/toml/one.toml: -------------------------------------------------------------------------------- 1 | string = 'foo' 2 | -------------------------------------------------------------------------------- /crates/schematic/tests/__fixtures__/toml/three.toml: -------------------------------------------------------------------------------- 1 | boolean = true 2 | string = 'bar' 3 | -------------------------------------------------------------------------------- /crates/schematic/tests/__fixtures__/toml/two.toml: -------------------------------------------------------------------------------- 1 | vector = ['a', 'b', 'c'] 2 | -------------------------------------------------------------------------------- /crates/schematic/tests/__fixtures__/yaml/five.yml: -------------------------------------------------------------------------------- 1 | vector: [x, y, z] 2 | -------------------------------------------------------------------------------- /crates/schematic/tests/__fixtures__/yaml/four.yml: -------------------------------------------------------------------------------- 1 | boolean: false 2 | number: 123 3 | -------------------------------------------------------------------------------- /crates/schematic/tests/__fixtures__/yaml/one.yml: -------------------------------------------------------------------------------- 1 | string: foo 2 | -------------------------------------------------------------------------------- /crates/schematic/tests/__fixtures__/yaml/three.yml: -------------------------------------------------------------------------------- 1 | boolean: true 2 | string: bar 3 | -------------------------------------------------------------------------------- /crates/schematic/tests/__fixtures__/yaml/two.yml: -------------------------------------------------------------------------------- 1 | vector: [a, b, c] 2 | -------------------------------------------------------------------------------- /crates/schematic/tests/config_unit_struct.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code, clippy::box_collection)] 2 | 3 | mod utils; 4 | 5 | use schematic::*; 6 | use std::collections::HashMap; 7 | 8 | #[derive(Config)] 9 | struct Config { 10 | field: String, 11 | } 12 | 13 | #[derive(ConfigEnum)] 14 | enum ConfigEnum { 15 | A, 16 | B, 17 | C, 18 | } 19 | 20 | // VALUES 21 | 22 | #[derive(Config)] 23 | struct Single(String); 24 | 25 | #[derive(Config)] 26 | struct Multiple(String, usize, bool); 27 | 28 | #[derive(Config)] 29 | struct SingleOption(Option); 30 | 31 | #[derive(Config)] 32 | struct MultipleOption(Option, usize, Option); 33 | 34 | #[derive(Config)] 35 | struct SingleBox(Box); 36 | 37 | #[derive(Config)] 38 | struct MultipleBox(Box, usize, Box); 39 | 40 | #[derive(Config)] 41 | struct SingleOptionBox(Option>); 42 | 43 | #[derive(Config)] 44 | struct MultipleOptionBox(Option>, Option, Box); 45 | 46 | // NESTED 47 | 48 | #[derive(Config)] 49 | struct NestedSingle(#[setting(nested)] Config); 50 | 51 | #[derive(Config)] 52 | struct NestedMultiple(String, #[setting(nested)] Config); 53 | 54 | #[derive(Config)] 55 | struct NestedVec(#[setting(nested)] Vec); 56 | 57 | #[derive(Config)] 58 | struct NestedMap(#[setting(nested)] HashMap); 59 | 60 | #[derive(Config)] 61 | struct NestedComplex( 62 | #[setting(nested)] Option>, 63 | #[setting(nested)] HashMap>>, 64 | ); 65 | 66 | // DEFAULT 67 | 68 | #[derive(Config)] 69 | struct DefaultSingle(#[setting(default = "abc")] String); 70 | 71 | #[derive(Config)] 72 | struct DefaultMultiple( 73 | String, 74 | #[setting(default = 123)] usize, 75 | #[setting(default = true)] bool, 76 | ); 77 | 78 | // ENV 79 | 80 | #[derive(Config)] 81 | struct EnvMultiple( 82 | #[setting(env = "A")] String, 83 | usize, 84 | #[setting(env = "C", parse_env = env::parse_bool)] bool, 85 | ); 86 | 87 | // MERGE 88 | 89 | #[derive(Config)] 90 | struct MergeVec(#[setting(merge = merge::append_vec)] Vec); 91 | 92 | #[derive(Config)] 93 | struct MergeMapMultiple( 94 | #[setting(merge = merge::merge_hashmap)] HashMap, 95 | #[setting(merge = merge::discard)] Option>, 96 | ); 97 | 98 | // VALIDATE 99 | 100 | #[derive(Config)] 101 | struct ValidateSingle(#[setting(validate = validate::not_empty)] String); 102 | 103 | #[derive(Config)] 104 | struct ValidateMultiple( 105 | #[setting(validate = validate::not_empty)] String, 106 | #[setting(validate = validate::in_range(0, 100))] usize, 107 | bool, 108 | ); 109 | -------------------------------------------------------------------------------- /crates/schematic/tests/helpers_test.rs: -------------------------------------------------------------------------------- 1 | use schematic::helpers::extract_ext; 2 | 3 | mod ext { 4 | use super::*; 5 | 6 | #[test] 7 | fn works_on_files() { 8 | assert!(extract_ext("file").is_none()); 9 | assert_eq!(extract_ext("file.json").unwrap(), ".json"); 10 | assert_eq!(extract_ext("dir/file.yaml").unwrap(), ".yaml"); 11 | assert_eq!(extract_ext("../file.toml").unwrap(), ".toml"); 12 | assert_eq!(extract_ext("/root/file.other.json").unwrap(), ".json"); 13 | } 14 | 15 | #[test] 16 | fn works_on_urls() { 17 | assert!(extract_ext("https://domain.com/file").is_none()); 18 | assert_eq!( 19 | extract_ext("https://domain.com/file.json").unwrap(), 20 | ".json" 21 | ); 22 | assert_eq!( 23 | extract_ext("http://domain.com/dir/file.yaml").unwrap(), 24 | ".yaml" 25 | ); 26 | assert_eq!( 27 | extract_ext("https://domain.com/file.toml?query").unwrap(), 28 | ".toml" 29 | ); 30 | assert_eq!( 31 | extract_ext("http://domain.com/root/file.other.json").unwrap(), 32 | ".json" 33 | ); 34 | assert_eq!( 35 | extract_ext("https://domain.com/other.segment/file.toml?query").unwrap(), 36 | ".toml" 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /crates/schematic/tests/schematic_enum_test.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code, deprecated)] 2 | 3 | use schematic::Schematic; 4 | use std::collections::HashMap; 5 | 6 | /// Some comment. 7 | #[derive(Default, Schematic)] 8 | pub enum SomeEnum { 9 | #[default] 10 | A, 11 | B, 12 | C, 13 | #[schema(exclude)] 14 | D, 15 | } 16 | 17 | pub struct NonSchematic { 18 | string: String, 19 | } 20 | 21 | /** A comment. */ 22 | #[derive(Schematic)] 23 | #[schematic(rename_all = "snake_case")] 24 | pub struct ValueTypes { 25 | boolean: bool, 26 | string: String, 27 | number: usize, 28 | vector: Vec, 29 | map: HashMap, 30 | enums: SomeEnum, 31 | s3_value: String, 32 | 33 | #[schema(exclude)] 34 | other: NonSchematic, 35 | } 36 | -------------------------------------------------------------------------------- /crates/schematic/tests/snapshots/code_sources_test__generates_json_schema.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/schematic/tests/code_sources_test.rs 3 | expression: "std::fs::read_to_string(file).unwrap()" 4 | --- 5 | { 6 | "$schema": "http://json-schema.org/draft-07/schema#", 7 | "title": "Config", 8 | "type": "object", 9 | "required": [ 10 | "boolean", 11 | "float", 12 | "number", 13 | "string", 14 | "vector" 15 | ], 16 | "properties": { 17 | "boolean": { 18 | "type": "boolean" 19 | }, 20 | "float": { 21 | "type": "number" 22 | }, 23 | "number": { 24 | "type": "number" 25 | }, 26 | "string": { 27 | "type": "string" 28 | }, 29 | "vector": { 30 | "type": "array", 31 | "items": { 32 | "type": "string" 33 | } 34 | } 35 | }, 36 | "additionalProperties": false 37 | } 38 | 39 | -------------------------------------------------------------------------------- /crates/schematic/tests/snapshots/code_sources_test__generates_typescript.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/config/tests/code_sources_test.rs 3 | expression: "std::fs::read_to_string(file).unwrap()" 4 | --- 5 | // Automatically generated by schematic. DO NOT MODIFY! 6 | 7 | /* eslint-disable */ 8 | 9 | export interface Config { 10 | boolean: boolean; 11 | float: number; 12 | number: number; 13 | string: string; 14 | vector: string[]; 15 | } 16 | 17 | -------------------------------------------------------------------------------- /crates/schematic/tests/snapshots/defaults_test__generates_typescript.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/schematic/tests/defaults_test.rs 3 | expression: "std::fs::read_to_string(file).unwrap()" 4 | --- 5 | // Automatically generated by schematic. DO NOT MODIFY! 6 | 7 | /* eslint-disable */ 8 | 9 | export interface CustomDefaults { 10 | /** @default true */ 11 | boolean?: boolean; 12 | /** @default 1.32 */ 13 | float?: number; 14 | /** @default 123 */ 15 | number?: number; 16 | /** @default 'foo' */ 17 | string?: string; 18 | vector?: number[]; 19 | } 20 | 21 | export interface ReqOptDefaults { 22 | optional: number | null; 23 | optionalWithDefault?: number | null; 24 | optionalWithDefaultFn?: number | null; 25 | required: number; 26 | /** @default 123 */ 27 | requiredWithDefault?: number; 28 | requiredWithDefaultFn?: number; 29 | } 30 | 31 | export interface ContextDefaults { 32 | count?: number; 33 | path?: string; 34 | } 35 | 36 | export interface NativeDefaults { 37 | boolean: boolean; 38 | boxed: string; 39 | float32: number; 40 | float64: number; 41 | number: number; 42 | string: string; 43 | vector: string[]; 44 | } 45 | 46 | export interface NestedDefaults { 47 | nested: NativeDefaults; 48 | nestedBoxed: { 49 | boolean: boolean; 50 | boxed: string; 51 | float32: number; 52 | float64: number; 53 | number: number; 54 | string: string; 55 | vector: string[]; 56 | }; 57 | nestedMap: Record; 58 | nestedMapBoxed: Record; 67 | nestedMapOptBoxed: Record; 76 | nestedOpt: NativeDefaults | null; 77 | nestedOptBoxed: { 78 | boolean: boolean; 79 | boxed: string; 80 | float32: number; 81 | float64: number; 82 | number: number; 83 | string: string; 84 | vector: string[]; 85 | } | null; 86 | nestedVec: NativeDefaults[]; 87 | nestedVecBoxed: { 88 | boolean: boolean; 89 | boxed: string; 90 | float32: number; 91 | float64: number; 92 | number: number; 93 | string: string; 94 | vector: string[]; 95 | }[]; 96 | nestedVecOptBoxed: ({ 97 | boolean: boolean; 98 | boxed: string; 99 | float32: number; 100 | float64: number; 101 | number: number; 102 | string: string; 103 | vector: string[]; 104 | } | null)[]; 105 | } 106 | -------------------------------------------------------------------------------- /crates/schematic/tests/snapshots/env_test__generates_json_schema.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/schematic/tests/env_test.rs 3 | expression: "std::fs::read_to_string(file).unwrap()" 4 | --- 5 | { 6 | "$schema": "http://json-schema.org/draft-07/schema#", 7 | "title": "EnvVarsPrefixed", 8 | "type": "object", 9 | "required": [ 10 | "bool", 11 | "list1", 12 | "list2", 13 | "nested", 14 | "number", 15 | "path", 16 | "string" 17 | ], 18 | "properties": { 19 | "bool": { 20 | "type": "boolean" 21 | }, 22 | "list1": { 23 | "type": "array", 24 | "items": { 25 | "type": "string" 26 | } 27 | }, 28 | "list2": { 29 | "type": "array", 30 | "items": { 31 | "type": "number" 32 | } 33 | }, 34 | "nested": { 35 | "allOf": [ 36 | { 37 | "$ref": "#/definitions/EnvVarsNested" 38 | } 39 | ] 40 | }, 41 | "number": { 42 | "type": "number" 43 | }, 44 | "path": { 45 | "type": "string", 46 | "format": "path" 47 | }, 48 | "string": { 49 | "type": "string" 50 | } 51 | }, 52 | "additionalProperties": false, 53 | "definitions": { 54 | "EnvVarsNested": { 55 | "title": "EnvVarsNested", 56 | "type": "object", 57 | "required": [ 58 | "string" 59 | ], 60 | "properties": { 61 | "string": { 62 | "type": "string" 63 | } 64 | }, 65 | "additionalProperties": false 66 | } 67 | } 68 | } 69 | 70 | -------------------------------------------------------------------------------- /crates/schematic/tests/snapshots/env_test__generates_typescript.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/schematic/tests/env_test.rs 3 | expression: "std::fs::read_to_string(file).unwrap()" 4 | --- 5 | // Automatically generated by schematic. DO NOT MODIFY! 6 | 7 | /* eslint-disable */ 8 | 9 | export interface EnvVarsNested { 10 | /** @envvar ENV_STRING */ 11 | string: string; 12 | } 13 | 14 | export interface EnvVarsPrefixed { 15 | /** @envvar ENV_BOOL */ 16 | bool: boolean; 17 | /** @envvar ENV_LIST1 */ 18 | list1: string[]; 19 | /** @envvar ENV_LIST2 */ 20 | list2: number[]; 21 | nested: EnvVarsNested; 22 | /** @envvar ENV_NUMBER */ 23 | number: number; 24 | /** @envvar ENV_PATH */ 25 | path: string; 26 | /** @envvar ENV_STRING */ 27 | string: string; 28 | } 29 | 30 | -------------------------------------------------------------------------------- /crates/schematic/tests/snapshots/extends_test__generates_json_schema.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/schematic/tests/extends_test.rs 3 | expression: "std::fs::read_to_string(file).unwrap()" 4 | --- 5 | { 6 | "$schema": "http://json-schema.org/draft-07/schema#", 7 | "title": "ExtendsEnum", 8 | "type": "object", 9 | "required": [ 10 | "extends", 11 | "value" 12 | ], 13 | "properties": { 14 | "extends": { 15 | "allOf": [ 16 | { 17 | "$ref": "#/definitions/ExtendsFrom" 18 | } 19 | ] 20 | }, 21 | "value": { 22 | "type": "array", 23 | "items": { 24 | "type": "number" 25 | } 26 | } 27 | }, 28 | "additionalProperties": false, 29 | "definitions": { 30 | "ExtendsFrom": { 31 | "title": "ExtendsFrom", 32 | "anyOf": [ 33 | { 34 | "type": "string" 35 | }, 36 | { 37 | "type": "array", 38 | "items": { 39 | "type": "string" 40 | } 41 | } 42 | ] 43 | }, 44 | "ExtendsList": { 45 | "title": "ExtendsList", 46 | "type": "object", 47 | "required": [ 48 | "extends", 49 | "value" 50 | ], 51 | "properties": { 52 | "extends": { 53 | "type": "array", 54 | "items": { 55 | "type": "string" 56 | } 57 | }, 58 | "value": { 59 | "type": "array", 60 | "items": { 61 | "type": "number" 62 | } 63 | } 64 | }, 65 | "additionalProperties": false 66 | }, 67 | "ExtendsString": { 68 | "title": "ExtendsString", 69 | "type": "object", 70 | "required": [ 71 | "extends", 72 | "value" 73 | ], 74 | "properties": { 75 | "extends": { 76 | "type": "string" 77 | }, 78 | "value": { 79 | "type": "array", 80 | "items": { 81 | "type": "number" 82 | } 83 | } 84 | }, 85 | "additionalProperties": false 86 | }, 87 | "ExtendsStringOptional": { 88 | "title": "ExtendsStringOptional", 89 | "type": "object", 90 | "required": [ 91 | "extends", 92 | "value" 93 | ], 94 | "properties": { 95 | "extends": { 96 | "anyOf": [ 97 | { 98 | "type": "string" 99 | }, 100 | { 101 | "type": "null" 102 | } 103 | ] 104 | }, 105 | "value": { 106 | "type": "array", 107 | "items": { 108 | "type": "number" 109 | } 110 | } 111 | }, 112 | "additionalProperties": false 113 | } 114 | } 115 | } 116 | 117 | -------------------------------------------------------------------------------- /crates/schematic/tests/snapshots/extends_test__generates_typescript.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/config/tests/extends_test.rs 3 | expression: "std::fs::read_to_string(file).unwrap()" 4 | --- 5 | // Automatically generated by schematic. DO NOT MODIFY! 6 | 7 | /* eslint-disable */ 8 | 9 | export interface ExtendsString { 10 | extends: string; 11 | value: number[]; 12 | } 13 | 14 | export interface ExtendsStringOptional { 15 | extends: string | null; 16 | value: number[]; 17 | } 18 | 19 | export interface ExtendsList { 20 | extends: string[]; 21 | value: number[]; 22 | } 23 | 24 | export type ExtendsFrom = string | string[]; 25 | 26 | export interface ExtendsEnum { 27 | extends: ExtendsFrom; 28 | value: number[]; 29 | } 30 | 31 | -------------------------------------------------------------------------------- /crates/schematic/tests/snapshots/generator_test__template_json__defaults.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/schematic/tests/generator_test.rs 3 | expression: "fs::read_to_string(file).unwrap()" 4 | --- 5 | { 6 | // This is a boolean with a medium length description. 7 | // @envvar TEMPLATE_BOOLEAN 8 | "boolean": false, 9 | 10 | "emptyArray": [], 11 | 12 | "emptyObject": {}, 13 | 14 | // This is an enum with a medium length description and deprecated. 15 | // @deprecated Dont use enums! 16 | "enums": "foo", 17 | 18 | // This field is testing array expansion. 19 | "expandArray": [ 20 | { 21 | // An optional enum. 22 | "enums": "foo", 23 | 24 | // An optional string. 25 | "opt": "" 26 | } 27 | ], 28 | 29 | "expandArrayPrimitive": [ 30 | 0 31 | ], 32 | 33 | // This field is testing object expansion. 34 | "expandObject": { 35 | "example": { 36 | // An optional enum. 37 | "enums": "foo", 38 | 39 | // An optional string. 40 | "opt": "" 41 | } 42 | }, 43 | 44 | "expandObjectPrimitive": { 45 | "example": 0 46 | }, 47 | 48 | "fallbackEnum": "foo", 49 | 50 | // This is a float thats deprecated. 51 | // @deprecated 52 | // "float32": 0.0, 53 | 54 | // This is a float. 55 | "float64": 1.23, 56 | 57 | // This is a map of numbers. 58 | // "map": {}, 59 | 60 | // This is a nested struct with its own fields. 61 | "nested": { 62 | // An optional enum. 63 | "enums": "foo", 64 | 65 | // An optional string. 66 | "opt": "" 67 | }, 68 | 69 | // This is a number with a long description. 70 | // This is a number with a long description. 71 | "number": 0, 72 | 73 | // This is a nested struct with its own fields. 74 | "one": { 75 | // This is another nested field. 76 | "two": { 77 | // An optional string. 78 | // @envvar ENV_PREFIX_OPT 79 | "opt": "", 80 | } 81 | }, 82 | 83 | // This is a string. 84 | "string": "abc", 85 | 86 | // This is a list of strings. 87 | "vector": [] 88 | } 89 | -------------------------------------------------------------------------------- /crates/schematic/tests/snapshots/generator_test__template_json__without_comments.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/schematic/tests/generator_test.rs 3 | expression: "fs::read_to_string(file).unwrap()" 4 | --- 5 | { 6 | "boolean": false, 7 | "emptyArray": [], 8 | "emptyObject": {}, 9 | "enums": "foo", 10 | "expandArray": [ 11 | { 12 | "enums": "foo", 13 | "opt": "" 14 | } 15 | ], 16 | "expandArrayPrimitive": [ 17 | 0 18 | ], 19 | "expandObject": { 20 | "example": { 21 | "enums": "foo", 22 | "opt": "" 23 | } 24 | }, 25 | "expandObjectPrimitive": { 26 | "example": 0 27 | }, 28 | "fallbackEnum": "foo", 29 | "float64": 1.23, 30 | "nested": { 31 | "enums": "foo", 32 | "opt": "" 33 | }, 34 | "number": 0, 35 | "one": { 36 | "two": { 37 | "opt": "", 38 | } 39 | }, 40 | "string": "abc", 41 | "vector": [] 42 | } 43 | -------------------------------------------------------------------------------- /crates/schematic/tests/snapshots/generator_test__template_pkl__defaults.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/schematic/tests/generator_test.rs 3 | expression: "fs::read_to_string(file).unwrap()" 4 | --- 5 | { 6 | /// This is a boolean with a medium length description. 7 | /// @envvar TEMPLATE_BOOLEAN 8 | boolean = false 9 | 10 | emptyArray = List() 11 | 12 | emptyObject = Map() 13 | 14 | /// This is an enum with a medium length description and deprecated. 15 | /// @deprecated Dont use enums! 16 | enums = "foo" 17 | 18 | /// This field is testing array expansion. 19 | expandArray = new Listing { 20 | { 21 | /// An optional enum. 22 | enums = "foo" 23 | 24 | /// An optional string. 25 | opt = "" 26 | } 27 | } 28 | 29 | expandArrayPrimitive = new Listing { 30 | 0 31 | } 32 | 33 | /// This field is testing object expansion. 34 | expandObject = new Mapping { 35 | ["example"] { 36 | /// An optional enum. 37 | enums = "foo" 38 | 39 | /// An optional string. 40 | opt = "" 41 | } 42 | } 43 | 44 | expandObjectPrimitive = new Mapping { 45 | ["example"] = 0 46 | } 47 | 48 | fallbackEnum = "foo" 49 | 50 | /// This is a float thats deprecated. 51 | /// @deprecated 52 | /// float32 = 0.0 53 | 54 | /// This is a float. 55 | float64 = 1.23 56 | 57 | /// This is a map of numbers. 58 | /// map = Map() 59 | 60 | /// This is a nested struct with its own fields. 61 | nested { 62 | /// An optional enum. 63 | enums = "foo" 64 | 65 | /// An optional string. 66 | opt = "" 67 | } 68 | 69 | /// This is a number with a long description. 70 | /// This is a number with a long description. 71 | number = 0 72 | 73 | /// This is a nested struct with its own fields. 74 | one { 75 | /// This is another nested field. 76 | two { 77 | /// An optional string. 78 | /// @envvar ENV_PREFIX_OPT 79 | opt = "" 80 | } 81 | } 82 | 83 | /// This is a string. 84 | string = "abc" 85 | 86 | /// This is a list of strings. 87 | vector = List() 88 | } 89 | -------------------------------------------------------------------------------- /crates/schematic/tests/snapshots/generator_test__template_toml__defaults.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/schematic/tests/generator_test.rs 3 | expression: "fs::read_to_string(file).unwrap()" 4 | --- 5 | # This is a boolean with a medium length description. 6 | # @envvar TEMPLATE_BOOLEAN 7 | boolean = false 8 | 9 | emptyArray = [] 10 | 11 | emptyObject = {} 12 | 13 | # This is an enum with a medium length description and deprecated. 14 | # @deprecated Dont use enums! 15 | enums = "foo" 16 | 17 | expandArrayPrimitive = [0] 18 | 19 | # This field is testing object expansion. 20 | expandObject = {} 21 | 22 | expandObjectPrimitive = { example = 0 } 23 | 24 | fallbackEnum = "foo" 25 | 26 | # This is a float thats deprecated. 27 | # @deprecated 28 | # float32 = 0.0 29 | 30 | # This is a float. 31 | float64 = 1.23 32 | 33 | # This is a map of numbers. 34 | # map = {} 35 | 36 | # This is a number with a long description. 37 | # This is a number with a long description. 38 | number = 0 39 | 40 | # This is a string. 41 | string = "abc" 42 | 43 | # This is a list of strings. 44 | vector = [] 45 | 46 | # This field is testing array expansion. 47 | [[expandArray]] 48 | # An optional enum. 49 | enums = "foo" 50 | 51 | # An optional string. 52 | opt = "" 53 | 54 | # This is a nested struct with its own fields. 55 | [nested] 56 | # An optional enum. 57 | enums = "foo" 58 | 59 | # An optional string. 60 | opt = "" 61 | 62 | # This is another nested field. 63 | [one.two] 64 | # An optional string. 65 | # @envvar ENV_PREFIX_OPT 66 | opt = "" 67 | -------------------------------------------------------------------------------- /crates/schematic/tests/snapshots/generator_test__template_yaml__defaults.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/schematic/tests/generator_test.rs 3 | expression: "fs::read_to_string(file).unwrap()" 4 | --- 5 | # This is a boolean with a medium length description. 6 | # @envvar TEMPLATE_BOOLEAN 7 | boolean: false 8 | 9 | emptyArray: [] 10 | 11 | emptyObject: {} 12 | 13 | # This is an enum with a medium length description and deprecated. 14 | # @deprecated Dont use enums! 15 | enums: "foo" 16 | 17 | # This field is testing array expansion. 18 | expandArray: 19 | - # An optional enum. 20 | enums: "foo" 21 | 22 | # An optional string. 23 | opt: "" 24 | 25 | expandArrayPrimitive: [0] 26 | 27 | # This field is testing object expansion. 28 | expandObject: 29 | example: 30 | # An optional enum. 31 | enums: "foo" 32 | 33 | # An optional string. 34 | opt: "" 35 | 36 | expandObjectPrimitive: {} 37 | 38 | fallbackEnum: "foo" 39 | 40 | # This is a float thats deprecated. 41 | # @deprecated 42 | # float32: 0.0 43 | 44 | # This is a float. 45 | float64: 1.23 46 | 47 | # This is a map of numbers. 48 | # map: {} 49 | 50 | # This is a nested struct with its own fields. 51 | nested: 52 | # An optional enum. 53 | enums: "foo" 54 | 55 | # An optional string. 56 | opt: "" 57 | 58 | # This is a number with a long description. 59 | # This is a number with a long description. 60 | number: 0 61 | 62 | # This is a nested struct with its own fields. 63 | one: 64 | # This is another nested field. 65 | two: 66 | # An optional string. 67 | # @envvar ENV_PREFIX_OPT 68 | opt: "" 69 | 70 | # This is a string. 71 | string: "abc" 72 | 73 | # This is a list of strings. 74 | vector: [] 75 | -------------------------------------------------------------------------------- /crates/schematic/tests/snapshots/generator_test__template_yaml__issue_139.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/schematic/tests/generator_test.rs 3 | expression: "fs::read_to_string(file).unwrap()" 4 | --- 5 | nested: 6 | one: true 7 | -------------------------------------------------------------------------------- /crates/schematic/tests/snapshots/generator_test__typescript__const_enums.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/schematic/tests/generator_test.rs 3 | expression: "generate(TypeScriptOptions\n{\n const_enum: true, enum_format: EnumFormat::Enum,\n ..TypeScriptOptions::default()\n})" 4 | --- 5 | // Automatically generated by schematic. DO NOT MODIFY! 6 | 7 | /* eslint-disable */ 8 | 9 | /** Docblock comment. */ 10 | export const enum BasicEnum { 11 | Foo, 12 | Bar, 13 | Baz, 14 | } 15 | 16 | export type FallbackEnum = 'foo' | 'bar' | 'baz' | string; 17 | 18 | /** Some comment. */ 19 | export interface AnotherConfig { 20 | /** 21 | * An optional enum. 22 | * 23 | * @default 'foo' 24 | */ 25 | enums: BasicEnum | null; 26 | /** An optional string. */ 27 | opt: string | null; 28 | } 29 | 30 | /** @deprecated */ 31 | export interface GenConfig { 32 | boolean: boolean; 33 | date: string; 34 | datetime: string; 35 | decimal: string; 36 | /** 37 | * This is a list of `enumerable` values. 38 | * 39 | * @default 'foo' 40 | * @type {'foo' | 'bar' | 'baz'} 41 | */ 42 | enums: BasicEnum; 43 | /** 44 | * @default 'foo' 45 | * @type {'foo' | 'bar' | 'baz' | string} 46 | */ 47 | fallbackEnum: FallbackEnum; 48 | float32: number; 49 | float64: number; 50 | indexmap: Record; 51 | indexset: string[] | null; 52 | jsonValue: unknown; 53 | map: Record; 54 | /** **Nested** field. */ 55 | nested: AnotherConfig; 56 | number: number; 57 | path: string; 58 | regex: string; 59 | relPath: string; 60 | string: string; 61 | time: string; 62 | tomlValue: unknown | null; 63 | url: string | null; 64 | uuid: string; 65 | /** This is a list of strings. */ 66 | vector: string[]; 67 | version: string | null; 68 | version2: string; 69 | versionReq: string; 70 | yamlValue: unknown; 71 | } 72 | -------------------------------------------------------------------------------- /crates/schematic/tests/snapshots/generator_test__typescript__defaults.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/schematic/tests/generator_test.rs 3 | expression: "generate(TypeScriptOptions::default())" 4 | --- 5 | // Automatically generated by schematic. DO NOT MODIFY! 6 | 7 | /* eslint-disable */ 8 | 9 | /** Docblock comment. */ 10 | export type BasicEnum = 'foo' | 'bar' | 'baz'; 11 | 12 | export type FallbackEnum = 'foo' | 'bar' | 'baz' | string; 13 | 14 | /** Some comment. */ 15 | export interface AnotherConfig { 16 | /** 17 | * An optional enum. 18 | * 19 | * @default 'foo' 20 | */ 21 | enums: BasicEnum | null; 22 | /** An optional string. */ 23 | opt: string | null; 24 | } 25 | 26 | /** @deprecated */ 27 | export interface GenConfig { 28 | boolean: boolean; 29 | date: string; 30 | datetime: string; 31 | decimal: string; 32 | /** 33 | * This is a list of `enumerable` values. 34 | * 35 | * @default 'foo' 36 | * @type {'foo' | 'bar' | 'baz'} 37 | */ 38 | enums: BasicEnum; 39 | /** 40 | * @default 'foo' 41 | * @type {'foo' | 'bar' | 'baz' | string} 42 | */ 43 | fallbackEnum: FallbackEnum; 44 | float32: number; 45 | float64: number; 46 | indexmap: Record; 47 | indexset: string[] | null; 48 | jsonValue: unknown; 49 | map: Record; 50 | /** **Nested** field. */ 51 | nested: AnotherConfig; 52 | number: number; 53 | path: string; 54 | regex: string; 55 | relPath: string; 56 | string: string; 57 | time: string; 58 | tomlValue: unknown | null; 59 | url: string | null; 60 | uuid: string; 61 | /** This is a list of strings. */ 62 | vector: string[]; 63 | version: string | null; 64 | version2: string; 65 | versionReq: string; 66 | yamlValue: unknown; 67 | } 68 | -------------------------------------------------------------------------------- /crates/schematic/tests/snapshots/generator_test__typescript__enums.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/schematic/tests/generator_test.rs 3 | expression: "generate(TypeScriptOptions\n{ enum_format: EnumFormat::Enum, ..TypeScriptOptions::default() })" 4 | --- 5 | // Automatically generated by schematic. DO NOT MODIFY! 6 | 7 | /* eslint-disable */ 8 | 9 | /** Docblock comment. */ 10 | export enum BasicEnum { 11 | Foo, 12 | Bar, 13 | Baz, 14 | } 15 | 16 | export type FallbackEnum = 'foo' | 'bar' | 'baz' | string; 17 | 18 | /** Some comment. */ 19 | export interface AnotherConfig { 20 | /** 21 | * An optional enum. 22 | * 23 | * @default 'foo' 24 | */ 25 | enums: BasicEnum | null; 26 | /** An optional string. */ 27 | opt: string | null; 28 | } 29 | 30 | /** @deprecated */ 31 | export interface GenConfig { 32 | boolean: boolean; 33 | date: string; 34 | datetime: string; 35 | decimal: string; 36 | /** 37 | * This is a list of `enumerable` values. 38 | * 39 | * @default 'foo' 40 | * @type {'foo' | 'bar' | 'baz'} 41 | */ 42 | enums: BasicEnum; 43 | /** 44 | * @default 'foo' 45 | * @type {'foo' | 'bar' | 'baz' | string} 46 | */ 47 | fallbackEnum: FallbackEnum; 48 | float32: number; 49 | float64: number; 50 | indexmap: Record; 51 | indexset: string[] | null; 52 | jsonValue: unknown; 53 | map: Record; 54 | /** **Nested** field. */ 55 | nested: AnotherConfig; 56 | number: number; 57 | path: string; 58 | regex: string; 59 | relPath: string; 60 | string: string; 61 | time: string; 62 | tomlValue: unknown | null; 63 | url: string | null; 64 | uuid: string; 65 | /** This is a list of strings. */ 66 | vector: string[]; 67 | version: string | null; 68 | version2: string; 69 | versionReq: string; 70 | yamlValue: unknown; 71 | } 72 | -------------------------------------------------------------------------------- /crates/schematic/tests/snapshots/generator_test__typescript__exclude_refs.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/schematic/tests/generator_test.rs 3 | expression: "generate(TypeScriptOptions\n{\n exclude_references: vec![\"BasicEnum\".into(), \"AnotherType\".into()],\n ..TypeScriptOptions::default()\n})" 4 | --- 5 | // Automatically generated by schematic. DO NOT MODIFY! 6 | 7 | /* eslint-disable */ 8 | 9 | export type FallbackEnum = 'foo' | 'bar' | 'baz' | string; 10 | 11 | /** Some comment. */ 12 | export interface AnotherConfig { 13 | /** 14 | * An optional enum. 15 | * 16 | * @default 'foo' 17 | */ 18 | enums: BasicEnum | null; 19 | /** An optional string. */ 20 | opt: string | null; 21 | } 22 | 23 | /** @deprecated */ 24 | export interface GenConfig { 25 | boolean: boolean; 26 | date: string; 27 | datetime: string; 28 | decimal: string; 29 | /** 30 | * This is a list of `enumerable` values. 31 | * 32 | * @default 'foo' 33 | * @type {'foo' | 'bar' | 'baz'} 34 | */ 35 | enums: BasicEnum; 36 | /** 37 | * @default 'foo' 38 | * @type {'foo' | 'bar' | 'baz' | string} 39 | */ 40 | fallbackEnum: FallbackEnum; 41 | float32: number; 42 | float64: number; 43 | indexmap: Record; 44 | indexset: string[] | null; 45 | jsonValue: unknown; 46 | map: Record; 47 | /** **Nested** field. */ 48 | nested: AnotherConfig; 49 | number: number; 50 | path: string; 51 | regex: string; 52 | relPath: string; 53 | string: string; 54 | time: string; 55 | tomlValue: unknown | null; 56 | url: string | null; 57 | uuid: string; 58 | /** This is a list of strings. */ 59 | vector: string[]; 60 | version: string | null; 61 | version2: string; 62 | versionReq: string; 63 | yamlValue: unknown; 64 | } 65 | -------------------------------------------------------------------------------- /crates/schematic/tests/snapshots/generator_test__typescript__external_types.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/schematic/tests/generator_test.rs 3 | expression: "generate(TypeScriptOptions\n{\n external_types:\n HashMap::from_iter([(\"./externals\".into(),\n vec![\"BasicEnum\".into(), \"AnotherType\".into()])]),\n ..TypeScriptOptions::default()\n})" 4 | --- 5 | // Automatically generated by schematic. DO NOT MODIFY! 6 | 7 | /* eslint-disable */ 8 | 9 | import type { AnotherType, BasicEnum } from './externals'; 10 | 11 | /** Docblock comment. */ 12 | export type BasicEnum = 'foo' | 'bar' | 'baz'; 13 | 14 | export type FallbackEnum = 'foo' | 'bar' | 'baz' | string; 15 | 16 | /** Some comment. */ 17 | export interface AnotherConfig { 18 | /** 19 | * An optional enum. 20 | * 21 | * @default 'foo' 22 | */ 23 | enums: BasicEnum | null; 24 | /** An optional string. */ 25 | opt: string | null; 26 | } 27 | 28 | /** @deprecated */ 29 | export interface GenConfig { 30 | boolean: boolean; 31 | date: string; 32 | datetime: string; 33 | decimal: string; 34 | /** 35 | * This is a list of `enumerable` values. 36 | * 37 | * @default 'foo' 38 | * @type {'foo' | 'bar' | 'baz'} 39 | */ 40 | enums: BasicEnum; 41 | /** 42 | * @default 'foo' 43 | * @type {'foo' | 'bar' | 'baz' | string} 44 | */ 45 | fallbackEnum: FallbackEnum; 46 | float32: number; 47 | float64: number; 48 | indexmap: Record; 49 | indexset: string[] | null; 50 | jsonValue: unknown; 51 | map: Record; 52 | /** **Nested** field. */ 53 | nested: AnotherConfig; 54 | number: number; 55 | path: string; 56 | regex: string; 57 | relPath: string; 58 | string: string; 59 | time: string; 60 | tomlValue: unknown | null; 61 | url: string | null; 62 | uuid: string; 63 | /** This is a list of strings. */ 64 | vector: string[]; 65 | version: string | null; 66 | version2: string; 67 | versionReq: string; 68 | yamlValue: unknown; 69 | } 70 | -------------------------------------------------------------------------------- /crates/schematic/tests/snapshots/generator_test__typescript__no_refs.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/schematic/tests/generator_test.rs 3 | expression: "generate(TypeScriptOptions\n{\n disable_references: true, indent_char: \" \".into(),\n ..TypeScriptOptions::default()\n})" 4 | --- 5 | // Automatically generated by schematic. DO NOT MODIFY! 6 | 7 | /* eslint-disable */ 8 | 9 | /** Docblock comment. */ 10 | export type BasicEnum = 'foo' | 'bar' | 'baz'; 11 | 12 | export type FallbackEnum = 'foo' | 'bar' | 'baz' | string; 13 | 14 | /** Some comment. */ 15 | export interface AnotherConfig { 16 | /** 17 | * An optional enum. 18 | * 19 | * @default 'foo' 20 | */ 21 | enums: 'foo' | 'bar' | 'baz' | null; 22 | /** An optional string. */ 23 | opt: string | null; 24 | } 25 | 26 | /** @deprecated */ 27 | export interface GenConfig { 28 | boolean: boolean; 29 | date: string; 30 | datetime: string; 31 | decimal: string; 32 | /** 33 | * This is a list of `enumerable` values. 34 | * 35 | * @default 'foo' 36 | * @type {'foo' | 'bar' | 'baz'} 37 | */ 38 | enums: 'foo' | 'bar' | 'baz'; 39 | /** 40 | * @default 'foo' 41 | * @type {'foo' | 'bar' | 'baz' | string} 42 | */ 43 | fallbackEnum: 'foo' | 'bar' | 'baz' | string; 44 | float32: number; 45 | float64: number; 46 | indexmap: Record; 47 | indexset: string[] | null; 48 | jsonValue: unknown; 49 | map: Record; 50 | /** **Nested** field. */ 51 | nested: { 52 | /** 53 | * An optional enum. 54 | * 55 | * @default 'foo' 56 | */ 57 | enums: 'foo' | 'bar' | 'baz' | null; 58 | /** An optional string. */ 59 | opt: string | null; 60 | }; 61 | number: number; 62 | path: string; 63 | regex: string; 64 | relPath: string; 65 | string: string; 66 | time: string; 67 | tomlValue: unknown | null; 68 | url: string | null; 69 | uuid: string; 70 | /** This is a list of strings. */ 71 | vector: string[]; 72 | version: string | null; 73 | version2: string; 74 | versionReq: string; 75 | yamlValue: unknown; 76 | } 77 | -------------------------------------------------------------------------------- /crates/schematic/tests/snapshots/generator_test__typescript__object_aliases.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/schematic/tests/generator_test.rs 3 | expression: "generate(TypeScriptOptions\n{ object_format: ObjectFormat::Type, ..TypeScriptOptions::default() })" 4 | --- 5 | // Automatically generated by schematic. DO NOT MODIFY! 6 | 7 | /* eslint-disable */ 8 | 9 | /** Docblock comment. */ 10 | export type BasicEnum = 'foo' | 'bar' | 'baz'; 11 | 12 | export type FallbackEnum = 'foo' | 'bar' | 'baz' | string; 13 | 14 | /** Some comment. */ 15 | export type AnotherConfig = { 16 | /** 17 | * An optional enum. 18 | * 19 | * @default 'foo' 20 | */ 21 | enums: BasicEnum | null, 22 | /** An optional string. */ 23 | opt: string | null, 24 | }; 25 | 26 | /** @deprecated */ 27 | export type GenConfig = { 28 | boolean: boolean, 29 | date: string, 30 | datetime: string, 31 | decimal: string, 32 | /** 33 | * This is a list of `enumerable` values. 34 | * 35 | * @default 'foo' 36 | * @type {'foo' | 'bar' | 'baz'} 37 | */ 38 | enums: BasicEnum, 39 | /** 40 | * @default 'foo' 41 | * @type {'foo' | 'bar' | 'baz' | string} 42 | */ 43 | fallbackEnum: FallbackEnum, 44 | float32: number, 45 | float64: number, 46 | indexmap: Record, 47 | indexset: string[] | null, 48 | jsonValue: unknown, 49 | map: Record, 50 | /** **Nested** field. */ 51 | nested: AnotherConfig, 52 | number: number, 53 | path: string, 54 | regex: string, 55 | relPath: string, 56 | string: string, 57 | time: string, 58 | tomlValue: unknown | null, 59 | url: string | null, 60 | uuid: string, 61 | /** This is a list of strings. */ 62 | vector: string[], 63 | version: string | null, 64 | version2: string, 65 | versionReq: string, 66 | yamlValue: unknown, 67 | }; 68 | -------------------------------------------------------------------------------- /crates/schematic/tests/snapshots/generator_test__typescript__partials.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/schematic/tests/generator_test.rs 3 | expression: "fs::read_to_string(file).unwrap()" 4 | --- 5 | // Automatically generated by schematic. DO NOT MODIFY! 6 | 7 | /* eslint-disable */ 8 | 9 | /** Docblock comment. */ 10 | export type BasicEnum = 'foo' | 'bar' | 'baz'; 11 | 12 | export type FallbackEnum = 'foo' | 'bar' | 'baz' | string; 13 | 14 | /** Some comment. */ 15 | export interface AnotherConfig { 16 | /** 17 | * An optional enum. 18 | * 19 | * @default 'foo' 20 | */ 21 | enums: BasicEnum | null; 22 | /** An optional string. */ 23 | opt: string | null; 24 | } 25 | 26 | /** @deprecated */ 27 | export interface GenConfig { 28 | boolean: boolean; 29 | date: string; 30 | datetime: string; 31 | decimal: string; 32 | /** 33 | * This is a list of `enumerable` values. 34 | * 35 | * @default 'foo' 36 | * @type {'foo' | 'bar' | 'baz'} 37 | */ 38 | enums: BasicEnum; 39 | /** 40 | * @default 'foo' 41 | * @type {'foo' | 'bar' | 'baz' | string} 42 | */ 43 | fallbackEnum: FallbackEnum; 44 | float32: number; 45 | float64: number; 46 | indexmap: Record; 47 | indexset: string[] | null; 48 | jsonValue: unknown; 49 | map: Record; 50 | /** **Nested** field. */ 51 | nested: AnotherConfig; 52 | number: number; 53 | path: string; 54 | regex: string; 55 | relPath: string; 56 | string: string; 57 | time: string; 58 | tomlValue: unknown | null; 59 | url: string | null; 60 | uuid: string; 61 | /** This is a list of strings. */ 62 | vector: string[]; 63 | version: string | null; 64 | version2: string; 65 | versionReq: string; 66 | yamlValue: unknown; 67 | } 68 | 69 | /** Some comment. */ 70 | export interface PartialAnotherConfig { 71 | /** 72 | * An optional enum. 73 | * 74 | * @default 'foo' 75 | */ 76 | enums?: BasicEnum | null; 77 | /** An optional string. */ 78 | opt?: string | null; 79 | } 80 | 81 | /** @deprecated */ 82 | export interface PartialGenConfig { 83 | boolean?: boolean | null; 84 | date?: string | null; 85 | datetime?: string | null; 86 | decimal?: string | null; 87 | /** 88 | * This is a list of `enumerable` values. 89 | * 90 | * @default 'foo' 91 | */ 92 | enums?: BasicEnum | null; 93 | /** @default 'foo' */ 94 | fallbackEnum?: FallbackEnum | null; 95 | float32?: number | null; 96 | float64?: number | null; 97 | indexmap?: Record | null; 98 | indexset?: string[] | null; 99 | jsonValue?: unknown | null; 100 | map?: Record | null; 101 | /** **Nested** field. */ 102 | nested?: PartialAnotherConfig | null; 103 | number?: number | null; 104 | path?: string | null; 105 | regex?: string | null; 106 | relPath?: string | null; 107 | string?: string | null; 108 | time?: string | null; 109 | tomlValue?: unknown | null; 110 | url?: string | null; 111 | uuid?: string | null; 112 | /** This is a list of strings. */ 113 | vector?: string[] | null; 114 | version?: string | null; 115 | version2?: string | null; 116 | versionReq?: string | null; 117 | yamlValue?: unknown | null; 118 | } 119 | -------------------------------------------------------------------------------- /crates/schematic/tests/snapshots/generator_test__typescript__props_optional.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/schematic/tests/generator_test.rs 3 | expression: "generate(TypeScriptOptions\n{ property_format: PropertyFormat::Optional, ..TypeScriptOptions::default() })" 4 | --- 5 | // Automatically generated by schematic. DO NOT MODIFY! 6 | 7 | /* eslint-disable */ 8 | 9 | /** Docblock comment. */ 10 | export type BasicEnum = 'foo' | 'bar' | 'baz'; 11 | 12 | export type FallbackEnum = 'foo' | 'bar' | 'baz' | string; 13 | 14 | /** Some comment. */ 15 | export interface AnotherConfig { 16 | /** 17 | * An optional enum. 18 | * 19 | * @default 'foo' 20 | */ 21 | enums?: BasicEnum | null; 22 | /** An optional string. */ 23 | opt?: string | null; 24 | } 25 | 26 | /** @deprecated */ 27 | export interface GenConfig { 28 | boolean?: boolean; 29 | date?: string; 30 | datetime?: string; 31 | decimal?: string; 32 | /** 33 | * This is a list of `enumerable` values. 34 | * 35 | * @default 'foo' 36 | * @type {'foo' | 'bar' | 'baz'} 37 | */ 38 | enums?: BasicEnum; 39 | /** 40 | * @default 'foo' 41 | * @type {'foo' | 'bar' | 'baz' | string} 42 | */ 43 | fallbackEnum?: FallbackEnum; 44 | float32?: number; 45 | float64?: number; 46 | indexmap?: Record; 47 | indexset?: string[] | null; 48 | jsonValue?: unknown; 49 | map?: Record; 50 | /** **Nested** field. */ 51 | nested?: AnotherConfig; 52 | number?: number; 53 | path?: string; 54 | regex?: string; 55 | relPath?: string; 56 | string?: string; 57 | time?: string; 58 | tomlValue?: unknown | null; 59 | url?: string | null; 60 | uuid?: string; 61 | /** This is a list of strings. */ 62 | vector?: string[]; 63 | version?: string | null; 64 | version2?: string; 65 | versionReq?: string; 66 | yamlValue?: unknown; 67 | } 68 | -------------------------------------------------------------------------------- /crates/schematic/tests/snapshots/generator_test__typescript__props_optional_undefined.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/schematic/tests/generator_test.rs 3 | expression: "generate(TypeScriptOptions\n{\n property_format: PropertyFormat::OptionalUndefined,\n ..TypeScriptOptions::default()\n})" 4 | --- 5 | // Automatically generated by schematic. DO NOT MODIFY! 6 | 7 | /* eslint-disable */ 8 | 9 | /** Docblock comment. */ 10 | export type BasicEnum = 'foo' | 'bar' | 'baz'; 11 | 12 | export type FallbackEnum = 'foo' | 'bar' | 'baz' | string; 13 | 14 | /** Some comment. */ 15 | export interface AnotherConfig { 16 | /** 17 | * An optional enum. 18 | * 19 | * @default 'foo' 20 | */ 21 | enums?: BasicEnum | null | undefined; 22 | /** An optional string. */ 23 | opt?: string | null | undefined; 24 | } 25 | 26 | /** @deprecated */ 27 | export interface GenConfig { 28 | boolean?: boolean | undefined; 29 | date?: string | undefined; 30 | datetime?: string | undefined; 31 | decimal?: string | undefined; 32 | /** 33 | * This is a list of `enumerable` values. 34 | * 35 | * @default 'foo' 36 | * @type {'foo' | 'bar' | 'baz'} 37 | */ 38 | enums?: BasicEnum | undefined; 39 | /** 40 | * @default 'foo' 41 | * @type {'foo' | 'bar' | 'baz' | string} 42 | */ 43 | fallbackEnum?: FallbackEnum | undefined; 44 | float32?: number | undefined; 45 | float64?: number | undefined; 46 | indexmap?: Record | undefined; 47 | indexset?: string[] | null | undefined; 48 | jsonValue?: unknown | undefined; 49 | map?: Record | undefined; 50 | /** **Nested** field. */ 51 | nested?: AnotherConfig | undefined; 52 | number?: number | undefined; 53 | path?: string | undefined; 54 | regex?: string | undefined; 55 | relPath?: string | undefined; 56 | string?: string | undefined; 57 | time?: string | undefined; 58 | tomlValue?: unknown | null | undefined; 59 | url?: string | null | undefined; 60 | uuid?: string | undefined; 61 | /** This is a list of strings. */ 62 | vector?: string[] | undefined; 63 | version?: string | null | undefined; 64 | version2?: string | undefined; 65 | versionReq?: string | undefined; 66 | yamlValue?: unknown | undefined; 67 | } 68 | -------------------------------------------------------------------------------- /crates/schematic/tests/snapshots/generator_test__typescript__value_enums.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/schematic/tests/generator_test.rs 3 | expression: "generate(TypeScriptOptions\n{ enum_format: EnumFormat::ValuedEnum, ..TypeScriptOptions::default() })" 4 | --- 5 | // Automatically generated by schematic. DO NOT MODIFY! 6 | 7 | /* eslint-disable */ 8 | 9 | /** Docblock comment. */ 10 | export enum BasicEnum { 11 | Foo = 'foo', 12 | Bar = 'bar', 13 | Baz = 'baz', 14 | } 15 | 16 | export type FallbackEnum = 'foo' | 'bar' | 'baz' | string; 17 | 18 | /** Some comment. */ 19 | export interface AnotherConfig { 20 | /** 21 | * An optional enum. 22 | * 23 | * @default 'foo' 24 | */ 25 | enums: BasicEnum | null; 26 | /** An optional string. */ 27 | opt: string | null; 28 | } 29 | 30 | /** @deprecated */ 31 | export interface GenConfig { 32 | boolean: boolean; 33 | date: string; 34 | datetime: string; 35 | decimal: string; 36 | /** 37 | * This is a list of `enumerable` values. 38 | * 39 | * @default 'foo' 40 | * @type {'foo' | 'bar' | 'baz'} 41 | */ 42 | enums: BasicEnum; 43 | /** 44 | * @default 'foo' 45 | * @type {'foo' | 'bar' | 'baz' | string} 46 | */ 47 | fallbackEnum: FallbackEnum; 48 | float32: number; 49 | float64: number; 50 | indexmap: Record; 51 | indexset: string[] | null; 52 | jsonValue: unknown; 53 | map: Record; 54 | /** **Nested** field. */ 55 | nested: AnotherConfig; 56 | number: number; 57 | path: string; 58 | regex: string; 59 | relPath: string; 60 | string: string; 61 | time: string; 62 | tomlValue: unknown | null; 63 | url: string | null; 64 | uuid: string; 65 | /** This is a list of strings. */ 66 | vector: string[]; 67 | version: string | null; 68 | version2: string; 69 | versionReq: string; 70 | yamlValue: unknown; 71 | } 72 | -------------------------------------------------------------------------------- /crates/schematic/tests/snapshots/macro_enum_test__generates_typescript.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/schematic/tests/macro_enum_test.rs 3 | expression: "std::fs::read_to_string(file).unwrap()" 4 | --- 5 | // Automatically generated by schematic. DO NOT MODIFY! 6 | 7 | /* eslint-disable */ 8 | 9 | export type AllUnit = 'foo' | 'bar' | 'baz'; 10 | 11 | export type AllUnnamed = { 12 | foo: string; 13 | } | { 14 | bar: boolean; 15 | } | { 16 | baz: number; 17 | }; 18 | 19 | export type OfBothTypes = { 20 | foo: null; 21 | } | { 22 | bar: [boolean, number]; 23 | }; 24 | 25 | export interface SomeConfig { 26 | bar: number; 27 | foo: string; 28 | } 29 | 30 | export type NestedConfigs = { 31 | string: string; 32 | } | { 33 | object: SomeConfig; 34 | } | { 35 | objects: [SomeConfig, SomeConfig]; 36 | }; 37 | 38 | export type WithSerde = string | boolean | number; 39 | 40 | /** Container */ 41 | export type WithComments = 'foo' | 'bar' | 'baz'; 42 | 43 | export type Untagged = null | boolean | [number, string] | SomeConfig; 44 | 45 | export type ExternalTagged = { 46 | foo: 'foo'; 47 | } | { 48 | bar: boolean; 49 | } | { 50 | bazzer: number; 51 | } | { 52 | qux: SomeConfig; 53 | }; 54 | 55 | export type InternalTagged = 'foo' | boolean | number | SomeConfig; 56 | 57 | export type AdjacentTagged = { 58 | content: 'foo'; 59 | type: 'foo'; 60 | } | { 61 | content: boolean; 62 | type: 'bar'; 63 | } | { 64 | content: number; 65 | type: 'bazzer'; 66 | } | { 67 | content: SomeConfig; 68 | type: 'qux'; 69 | }; 70 | 71 | export type PartialAllUnit = 'foo' | 'bar' | 'baz'; 72 | 73 | export type PartialAllUnnamed = { 74 | foo: string; 75 | } | { 76 | bar: boolean; 77 | } | { 78 | baz: number; 79 | }; 80 | 81 | export type PartialOfBothTypes = { 82 | foo: null; 83 | } | { 84 | bar: [boolean, number]; 85 | }; 86 | 87 | export interface PartialSomeConfig { 88 | bar?: number | null; 89 | foo?: string | null; 90 | } 91 | 92 | export type PartialNestedConfigs = { 93 | string: string; 94 | } | { 95 | object?: PartialSomeConfig | null; 96 | } | { 97 | objects?: [PartialSomeConfig, PartialSomeConfig] | null; 98 | }; 99 | 100 | export type PartialWithSerde = string | boolean | number; 101 | 102 | /** Container */ 103 | export type PartialWithComments = 'foo' | 'bar' | 'baz'; 104 | 105 | export type PartialUntagged = null | boolean | [number, string] | PartialSomeConfig; 106 | 107 | export type PartialExternalTagged = { 108 | foo: 'foo'; 109 | } | { 110 | bar: boolean; 111 | } | { 112 | bazzer: number; 113 | } | { 114 | qux?: PartialSomeConfig | null; 115 | }; 116 | 117 | export type PartialInternalTagged = 'foo' | boolean | number | PartialSomeConfig; 118 | 119 | export type PartialAdjacentTagged = { 120 | content: 'foo'; 121 | type: 'foo'; 122 | } | { 123 | content: boolean; 124 | type: 'bar'; 125 | } | { 126 | content: number; 127 | type: 'bazzer'; 128 | } | { 129 | content: PartialSomeConfig; 130 | type: 'qux'; 131 | }; 132 | -------------------------------------------------------------------------------- /crates/schematic/tests/snapshots/settings_test__generates_json_schema.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/schematic/tests/settings_test.rs 3 | expression: "std::fs::read_to_string(file).unwrap()" 4 | --- 5 | { 6 | "$schema": "http://json-schema.org/draft-07/schema#", 7 | "title": "NestedMapSettings", 8 | "type": "object", 9 | "required": [ 10 | "nestedOpt", 11 | "nestedReq" 12 | ], 13 | "properties": { 14 | "nestedOpt": { 15 | "anyOf": [ 16 | { 17 | "type": "object", 18 | "additionalProperties": { 19 | "$ref": "#/definitions/StandardSettings" 20 | }, 21 | "propertyNames": { 22 | "type": "string" 23 | } 24 | }, 25 | { 26 | "type": "null" 27 | } 28 | ] 29 | }, 30 | "nestedReq": { 31 | "type": "object", 32 | "additionalProperties": { 33 | "$ref": "#/definitions/StandardSettings" 34 | }, 35 | "propertyNames": { 36 | "type": "string" 37 | } 38 | } 39 | }, 40 | "additionalProperties": false, 41 | "definitions": { 42 | "NestedSettings": { 43 | "title": "NestedSettings", 44 | "type": "object", 45 | "required": [ 46 | "nestedOpt", 47 | "nestedReq" 48 | ], 49 | "properties": { 50 | "nestedOpt": { 51 | "anyOf": [ 52 | { 53 | "$ref": "#/definitions/StandardSettings" 54 | }, 55 | { 56 | "type": "null" 57 | } 58 | ] 59 | }, 60 | "nestedReq": { 61 | "allOf": [ 62 | { 63 | "$ref": "#/definitions/StandardSettings" 64 | } 65 | ] 66 | } 67 | }, 68 | "additionalProperties": false 69 | }, 70 | "NestedVecSettings": { 71 | "title": "NestedVecSettings", 72 | "type": "object", 73 | "required": [ 74 | "nestedOpt", 75 | "nestedReq" 76 | ], 77 | "properties": { 78 | "nestedOpt": { 79 | "anyOf": [ 80 | { 81 | "type": "array", 82 | "items": { 83 | "$ref": "#/definitions/StandardSettings" 84 | } 85 | }, 86 | { 87 | "type": "null" 88 | } 89 | ] 90 | }, 91 | "nestedReq": { 92 | "type": "array", 93 | "items": { 94 | "$ref": "#/definitions/StandardSettings" 95 | } 96 | } 97 | }, 98 | "additionalProperties": false 99 | }, 100 | "StandardSettings": { 101 | "title": "StandardSettings", 102 | "type": "object", 103 | "required": [ 104 | "opt", 105 | "req" 106 | ], 107 | "properties": { 108 | "opt": { 109 | "anyOf": [ 110 | { 111 | "type": "string" 112 | }, 113 | { 114 | "type": "null" 115 | } 116 | ] 117 | }, 118 | "req": { 119 | "type": "string" 120 | }, 121 | "reqDefault": { 122 | "default": "abc", 123 | "type": "string" 124 | } 125 | }, 126 | "additionalProperties": false 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /crates/schematic/tests/snapshots/settings_test__generates_typescript.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/schematic/tests/settings_test.rs 3 | expression: "std::fs::read_to_string(file).unwrap()" 4 | --- 5 | // Automatically generated by schematic. DO NOT MODIFY! 6 | 7 | /* eslint-disable */ 8 | 9 | export interface StandardSettings { 10 | /** @envvar OPT_ENV */ 11 | opt: string | null; 12 | req: string; 13 | /** @default 'abc' */ 14 | reqDefault?: string; 15 | } 16 | 17 | export interface NestedSettings { 18 | nestedOpt: StandardSettings | null; 19 | nestedReq: StandardSettings; 20 | } 21 | 22 | export interface NestedVecSettings { 23 | nestedOpt: StandardSettings[] | null; 24 | nestedReq: StandardSettings[]; 25 | } 26 | 27 | export interface NestedMapSettings { 28 | nestedOpt: Record | null; 29 | nestedReq: Record; 30 | } 31 | -------------------------------------------------------------------------------- /crates/schematic/tests/snapshots/variants_test__generates_json_schema.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/config/tests/variants_test.rs 3 | expression: "std::fs::read_to_string(file).unwrap()" 4 | --- 5 | { 6 | "$schema": "http://json-schema.org/draft-07/schema#", 7 | "title": "StandardSettings", 8 | "type": "object", 9 | "required": [ 10 | "projects" 11 | ], 12 | "properties": { 13 | "projects": { 14 | "allOf": [ 15 | { 16 | "$ref": "#/definitions/Projects" 17 | } 18 | ] 19 | } 20 | }, 21 | "additionalProperties": false, 22 | "definitions": { 23 | "Projects": { 24 | "title": "Projects", 25 | "anyOf": [ 26 | { 27 | "$ref": "#/definitions/ProjectsConfig" 28 | }, 29 | { 30 | "type": "array", 31 | "items": { 32 | "type": "string" 33 | } 34 | }, 35 | { 36 | "type": "object", 37 | "additionalProperties": { 38 | "type": "string" 39 | }, 40 | "propertyNames": { 41 | "type": "string" 42 | } 43 | } 44 | ] 45 | }, 46 | "ProjectsConfig": { 47 | "title": "ProjectsConfig", 48 | "type": "object", 49 | "required": [ 50 | "list", 51 | "map" 52 | ], 53 | "properties": { 54 | "list": { 55 | "type": "array", 56 | "items": { 57 | "type": "string" 58 | } 59 | }, 60 | "map": { 61 | "type": "object", 62 | "additionalProperties": { 63 | "type": "string" 64 | }, 65 | "propertyNames": { 66 | "type": "string" 67 | } 68 | } 69 | }, 70 | "additionalProperties": false 71 | } 72 | } 73 | } 74 | 75 | -------------------------------------------------------------------------------- /crates/schematic/tests/snapshots/variants_test__generates_typescript.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/config/tests/variants_test.rs 3 | expression: "std::fs::read_to_string(file).unwrap()" 4 | --- 5 | // Automatically generated by schematic. DO NOT MODIFY! 6 | 7 | /* eslint-disable */ 8 | 9 | export interface ProjectsConfig { 10 | list: string[]; 11 | map: Record; 12 | } 13 | 14 | export type Projects = ProjectsConfig | string[] | Record; 15 | 16 | export interface StandardSettings { 17 | projects: Projects; 18 | } 19 | 20 | -------------------------------------------------------------------------------- /crates/schematic/tests/utils.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | use schematic::{Cacher, HandlerError}; 4 | use std::path::PathBuf; 5 | use std::{env, fs}; 6 | 7 | pub fn get_fixture_path(name: &str) -> PathBuf { 8 | PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()) 9 | .join("tests/__fixtures__") 10 | .join(name) 11 | } 12 | 13 | pub struct SandboxCacher { 14 | pub root: PathBuf, 15 | } 16 | 17 | impl Cacher for SandboxCacher { 18 | fn get_file_path(&self, url: &str) -> Result, HandlerError> { 19 | let name = &url[url.rfind('/').unwrap() + 1..]; 20 | 21 | Ok(Some(self.root.join(name))) 22 | } 23 | 24 | /// Read content from the cache store. 25 | fn read(&mut self, url: &str) -> Result, HandlerError> { 26 | self.get_file_path(url).map(|path| { 27 | if path.as_ref().is_some_and(|p| p.exists()) { 28 | fs::read_to_string(path.unwrap()).ok() 29 | } else { 30 | None 31 | } 32 | }) 33 | } 34 | 35 | /// Write the provided content to the cache store. 36 | fn write(&mut self, url: &str, content: &str) -> Result<(), HandlerError> { 37 | fs::write(self.get_file_path(url).unwrap().unwrap(), content).unwrap(); 38 | 39 | Ok(()) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /crates/test-app/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "test_app" 3 | version = "0.17.8" 4 | edition = "2024" 5 | publish = false 6 | 7 | [dependencies] 8 | schematic = { path = "../schematic", features = [ 9 | "json", 10 | "schema", 11 | "type_chrono", 12 | "type_regex", 13 | "type_rust_decimal", 14 | ] } 15 | chrono = { workspace = true, features = ["serde"] } 16 | miette = { workspace = true, features = ["fancy"] } 17 | regex = { workspace = true } 18 | rust_decimal = { workspace = true } 19 | serde = { workspace = true } 20 | serde_json = { workspace = true } 21 | tracing = { workspace = true } 22 | -------------------------------------------------------------------------------- /crates/test-app/src/main.rs: -------------------------------------------------------------------------------- 1 | use chrono::NaiveDateTime; 2 | use miette::Result; 3 | use rust_decimal::Decimal; 4 | use schematic::{Config, ConfigLoader, Format, ValidateError}; 5 | use serde::Serialize; 6 | use std::collections::HashMap; 7 | 8 | fn validate_string(_: &str, _: &D, _: &C, _: bool) -> Result<(), ValidateError> { 9 | use schematic::PathSegment; 10 | Err(ValidateError::with_segments( 11 | "This string is ugly!", 12 | vec![PathSegment::Index(1), PathSegment::Key("foo".to_owned())], 13 | )) 14 | // Ok(()) 15 | } 16 | 17 | fn validate_number(_: &usize, _: &D, _: &C, _: bool) -> Result<(), ValidateError> { 18 | Err(ValidateError::new("Nah, we don't accept numbers.")) 19 | // Ok(()) 20 | } 21 | 22 | #[derive(Debug, Config, Serialize)] 23 | pub struct NestedConfig { 24 | #[setting(validate = validate_string)] 25 | string2: String, 26 | #[setting(validate = validate_number)] 27 | number2: usize, 28 | } 29 | 30 | #[derive(Debug, Config, Serialize)] 31 | struct TestConfig { 32 | #[setting(validate = validate_string, env = "TEST_VAR")] 33 | string: String, 34 | #[setting(validate = validate_number)] 35 | number: usize, 36 | #[setting(nested)] 37 | nested: NestedConfig, 38 | datetime: NaiveDateTime, 39 | decimal: Decimal, 40 | // regex: Regex, 41 | map: HashMap, 42 | } 43 | 44 | fn main() -> Result<()> { 45 | dbg!(TestConfig::settings()); 46 | 47 | let config = ConfigLoader::::new() 48 | // .code(r#"{ "string": "abc", "other": 123 }"#, Format::Json)? // parse error 49 | // .code("{\n \"string\": 123\n}", Format::Json)? // parse error 50 | .code("{\n \"string\": \"\", \"number\": 1 \n}", Format::Json)? // validate error 51 | .set_help("let's go!") 52 | .load()?; 53 | 54 | dbg!(&config.config.string); 55 | dbg!(&config.layers); 56 | 57 | println!("{}", serde_json::to_string_pretty(&config).unwrap()); 58 | 59 | Ok(()) 60 | } 61 | -------------------------------------------------------------------------------- /crates/types/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "schematic_types" 3 | version = "0.10.7" 4 | edition = "2024" 5 | license = "MIT" 6 | description = "Shapes and types for defining schemas for Rust types." 7 | homepage = "https://moonrepo.github.io/schematic" 8 | repository = "https://github.com/moonrepo/schematic" 9 | 10 | [dependencies] 11 | chrono = { workspace = true, optional = true } 12 | indexmap = { workspace = true } 13 | regex = { workspace = true, optional = true } 14 | rust_decimal = { workspace = true, optional = true } 15 | relative-path = { workspace = true, optional = true } 16 | ron = { workspace = true, optional = true } 17 | url = { workspace = true, optional = true } 18 | rpkl = { workspace = true, optional = true } 19 | semver = { workspace = true, optional = true } 20 | serde = { workspace = true, optional = true } 21 | uuid = { workspace = true, optional = true } 22 | serde_json = { workspace = true, optional = true } 23 | serde_yaml = { workspace = true, optional = true } 24 | serde_yml = { workspace = true, optional = true } 25 | toml = { workspace = true, optional = true } 26 | 27 | [dev-dependencies] 28 | schematic_types = { path = ".", features = [ 29 | "chrono", 30 | "indexmap", 31 | "regex", 32 | "relative_path", 33 | "rust_decimal", 34 | "semver", 35 | "serde", 36 | "uuid", 37 | "serde_json", 38 | "serde_ron", 39 | "serde_rpkl", 40 | "serde_toml", 41 | "serde_yaml", 42 | "serde_yml", 43 | "url", 44 | ] } 45 | starbase_sandbox = { workspace = true } 46 | 47 | [features] 48 | default = [] 49 | chrono = ["dep:chrono"] 50 | indexmap = [] 51 | regex = ["dep:regex"] 52 | relative_path = ["dep:relative-path"] 53 | rust_decimal = ["dep:rust_decimal"] 54 | semver = ["dep:semver"] 55 | serde = ["dep:serde"] 56 | uuid = ["dep:uuid"] 57 | serde_json = ["dep:serde_json"] 58 | serde_ron = ["dep:ron"] 59 | serde_rpkl = ["dep:rpkl"] 60 | serde_toml = ["dep:toml"] 61 | serde_yaml = ["dep:serde_yaml"] 62 | serde_yml = ["dep:serde_yml"] 63 | url = ["dep:url"] 64 | -------------------------------------------------------------------------------- /crates/types/src/arrays.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | use std::collections::{BTreeSet, HashSet}; 3 | use std::fmt; 4 | 5 | #[derive(Clone, Debug, Default, PartialEq)] 6 | #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] 7 | pub struct ArrayType { 8 | #[cfg_attr( 9 | feature = "serde", 10 | serde(default, skip_serializing_if = "Option::is_none") 11 | )] 12 | pub contains: Option, 13 | 14 | pub items_type: Box, 15 | 16 | #[cfg_attr( 17 | feature = "serde", 18 | serde(default, skip_serializing_if = "Option::is_none") 19 | )] 20 | pub max_contains: Option, 21 | 22 | #[cfg_attr( 23 | feature = "serde", 24 | serde(default, skip_serializing_if = "Option::is_none") 25 | )] 26 | pub max_length: Option, 27 | 28 | #[cfg_attr( 29 | feature = "serde", 30 | serde(default, skip_serializing_if = "Option::is_none") 31 | )] 32 | pub min_contains: Option, 33 | 34 | #[cfg_attr( 35 | feature = "serde", 36 | serde(default, skip_serializing_if = "Option::is_none") 37 | )] 38 | pub min_length: Option, 39 | 40 | #[cfg_attr( 41 | feature = "serde", 42 | serde(default, skip_serializing_if = "Option::is_none") 43 | )] 44 | pub unique: Option, 45 | } 46 | 47 | impl ArrayType { 48 | /// Create an array schema with the provided item types. 49 | pub fn new(items_type: impl Into) -> Self { 50 | ArrayType { 51 | items_type: Box::new(items_type.into()), 52 | ..ArrayType::default() 53 | } 54 | } 55 | } 56 | 57 | impl fmt::Display for ArrayType { 58 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 59 | write!(f, "[{}]", self.items_type) 60 | } 61 | } 62 | 63 | impl Schematic for Vec { 64 | fn build_schema(mut schema: SchemaBuilder) -> Schema { 65 | schema.array(ArrayType::new(schema.infer::())) 66 | } 67 | } 68 | 69 | impl Schematic for &[T] { 70 | fn build_schema(mut schema: SchemaBuilder) -> Schema { 71 | schema.array(ArrayType::new(schema.infer::())) 72 | } 73 | } 74 | 75 | impl Schematic for [T; N] { 76 | fn build_schema(mut schema: SchemaBuilder) -> Schema { 77 | schema.array(ArrayType { 78 | items_type: Box::new(schema.infer::()), 79 | max_length: Some(N), 80 | min_length: Some(N), 81 | ..ArrayType::default() 82 | }) 83 | } 84 | } 85 | 86 | impl Schematic for HashSet { 87 | fn build_schema(mut schema: SchemaBuilder) -> Schema { 88 | schema.array(ArrayType { 89 | items_type: Box::new(schema.infer::()), 90 | unique: Some(true), 91 | ..ArrayType::default() 92 | }) 93 | } 94 | } 95 | 96 | impl Schematic for BTreeSet { 97 | fn build_schema(mut schema: SchemaBuilder) -> Schema { 98 | schema.array(ArrayType { 99 | items_type: Box::new(schema.infer::()), 100 | unique: Some(true), 101 | ..ArrayType::default() 102 | }) 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /crates/types/src/bools.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | use std::fmt; 3 | 4 | #[derive(Clone, Debug, Default, PartialEq)] 5 | #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] 6 | pub struct BooleanType { 7 | #[cfg_attr( 8 | feature = "serde", 9 | serde(default, skip_serializing_if = "Option::is_none") 10 | )] 11 | pub default: Option, 12 | } 13 | 14 | impl BooleanType { 15 | /// Create a boolean schema with the provided default value. 16 | pub fn new(value: bool) -> Self { 17 | BooleanType { 18 | default: Some(LiteralValue::Bool(value)), 19 | } 20 | } 21 | } 22 | 23 | impl fmt::Display for BooleanType { 24 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 25 | write!(f, "bool") 26 | } 27 | } 28 | 29 | impl Schematic for bool { 30 | fn build_schema(mut schema: SchemaBuilder) -> Schema { 31 | schema.boolean_default() 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /crates/types/src/enums.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | pub use indexmap::IndexMap; 3 | use std::fmt; 4 | 5 | #[derive(Clone, Debug, Default, PartialEq)] 6 | #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] 7 | pub struct EnumType { 8 | #[cfg_attr( 9 | feature = "serde", 10 | serde(default, skip_serializing_if = "Option::is_none") 11 | )] 12 | pub default_index: Option, 13 | 14 | pub values: Vec, 15 | 16 | #[cfg_attr( 17 | feature = "serde", 18 | serde(default, skip_serializing_if = "Option::is_none") 19 | )] 20 | pub variants: Option>>, 21 | } 22 | 23 | impl EnumType { 24 | /// Create an enumerable type with the provided literal values. 25 | pub fn new(values: I) -> Self 26 | where 27 | I: IntoIterator, 28 | { 29 | EnumType { 30 | values: values.into_iter().collect(), 31 | ..EnumType::default() 32 | } 33 | } 34 | 35 | #[doc(hidden)] 36 | pub fn from_schemas(schemas: I, default_index: Option) -> Self 37 | where 38 | I: IntoIterator, 39 | { 40 | let mut variants = IndexMap::default(); 41 | let mut values = vec![]; 42 | 43 | for mut schema in schemas.into_iter() { 44 | if let SchemaType::Literal(lit) = &schema.ty { 45 | values.push(lit.value.clone()); 46 | } 47 | 48 | variants.insert( 49 | schema.name.take().unwrap(), 50 | Box::new(SchemaField::new(schema)), 51 | ); 52 | } 53 | 54 | EnumType { 55 | default_index, 56 | values, 57 | variants: Some(variants), 58 | } 59 | } 60 | 61 | #[doc(hidden)] 62 | pub fn from_fields(variants: I, default_index: Option) -> Self 63 | where 64 | I: IntoIterator, 65 | { 66 | let variants: IndexMap> = variants 67 | .into_iter() 68 | .map(|(k, v)| (k, Box::new(v))) 69 | .collect(); 70 | let mut values = vec![]; 71 | 72 | for variant in variants.values() { 73 | if let SchemaType::Literal(lit) = &variant.schema.ty { 74 | values.push(lit.value.clone()); 75 | } 76 | } 77 | 78 | EnumType { 79 | default_index, 80 | values, 81 | variants: Some(variants), 82 | } 83 | } 84 | } 85 | 86 | impl fmt::Display for EnumType { 87 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 88 | write!( 89 | f, 90 | "{}", 91 | self.values 92 | .iter() 93 | .map(|item| item.to_string()) 94 | .collect::>() 95 | .join(" | ") 96 | ) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /crates/types/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod arrays; 2 | mod bools; 3 | mod enums; 4 | mod externals; 5 | mod literals; 6 | mod numbers; 7 | mod objects; 8 | mod schema; 9 | mod schema_builder; 10 | mod schema_type; 11 | mod strings; 12 | mod structs; 13 | mod tuples; 14 | mod unions; 15 | 16 | pub use arrays::*; 17 | pub use bools::*; 18 | pub use enums::*; 19 | pub use literals::*; 20 | pub use numbers::*; 21 | pub use objects::*; 22 | pub use schema::*; 23 | pub use schema_builder::*; 24 | pub use schema_type::*; 25 | pub use strings::*; 26 | pub use structs::*; 27 | pub use tuples::*; 28 | pub use unions::*; 29 | 30 | use std::rc::Rc; 31 | use std::sync::Arc; 32 | 33 | /// Defines a schema that represents the shape of the implementing type. 34 | pub trait Schematic { 35 | /// Define a name for this schema type. Names are required for non-primitive values 36 | /// as a means to link references, and avoid cycles. 37 | fn schema_name() -> Option { 38 | None 39 | } 40 | 41 | /// Create and return a schema that models the structure of the implementing type. 42 | /// The schema can be used to generate code, documentation, or other artifacts. 43 | fn build_schema(mut schema: SchemaBuilder) -> Schema { 44 | schema.build() 45 | } 46 | } 47 | 48 | // CORE 49 | 50 | impl Schematic for () { 51 | fn build_schema(mut schema: SchemaBuilder) -> Schema { 52 | schema.set_type_and_build(SchemaType::Null) 53 | } 54 | } 55 | 56 | impl Schematic for &T { 57 | fn build_schema(schema: SchemaBuilder) -> Schema { 58 | T::build_schema(schema) 59 | } 60 | } 61 | 62 | impl Schematic for &mut T { 63 | fn build_schema(schema: SchemaBuilder) -> Schema { 64 | T::build_schema(schema) 65 | } 66 | } 67 | 68 | impl Schematic for Box { 69 | fn build_schema(schema: SchemaBuilder) -> Schema { 70 | T::build_schema(schema) 71 | } 72 | } 73 | 74 | impl Schematic for Rc { 75 | fn build_schema(schema: SchemaBuilder) -> Schema { 76 | T::build_schema(schema) 77 | } 78 | } 79 | 80 | impl Schematic for Arc { 81 | fn build_schema(schema: SchemaBuilder) -> Schema { 82 | T::build_schema(schema) 83 | } 84 | } 85 | 86 | impl Schematic for Option { 87 | fn build_schema(mut schema: SchemaBuilder) -> Schema { 88 | schema.union(UnionType::new_any([schema.infer::(), Schema::null()])) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /crates/types/src/literals.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | #[derive(Clone, Debug, PartialEq)] 4 | #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] 5 | #[cfg_attr(feature = "serde", serde(tag = "type", content = "value"))] 6 | pub enum LiteralValue { 7 | Bool(bool), 8 | F32(f32), 9 | F64(f64), 10 | Int(isize), 11 | UInt(usize), 12 | String(String), 13 | } 14 | 15 | impl fmt::Display for LiteralValue { 16 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 17 | write!( 18 | f, 19 | "{}", 20 | match self { 21 | Self::Bool(inner) => inner.to_string(), 22 | Self::F32(inner) => inner.to_string(), 23 | Self::F64(inner) => inner.to_string(), 24 | Self::Int(inner) => inner.to_string(), 25 | Self::UInt(inner) => inner.to_string(), 26 | Self::String(inner) => format!("\"{inner}\""), 27 | } 28 | ) 29 | } 30 | } 31 | 32 | #[derive(Clone, Debug, PartialEq)] 33 | #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] 34 | pub struct LiteralType { 35 | #[cfg_attr( 36 | feature = "serde", 37 | serde(default, skip_serializing_if = "Option::is_none") 38 | )] 39 | pub format: Option, 40 | 41 | pub value: LiteralValue, 42 | } 43 | 44 | impl LiteralType { 45 | /// Create a literal schema with the provided value. 46 | pub fn new(value: LiteralValue) -> Self { 47 | LiteralType { 48 | format: None, 49 | value, 50 | } 51 | } 52 | } 53 | 54 | impl fmt::Display for LiteralType { 55 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 56 | write!(f, "{}", self.value) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /crates/types/src/objects.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | use std::collections::{BTreeMap, HashMap}; 3 | use std::fmt; 4 | 5 | #[derive(Clone, Debug, Default, PartialEq)] 6 | #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] 7 | pub struct ObjectType { 8 | pub key_type: Box, 9 | 10 | #[cfg_attr( 11 | feature = "serde", 12 | serde(default, skip_serializing_if = "Option::is_none") 13 | )] 14 | pub max_length: Option, 15 | 16 | #[cfg_attr( 17 | feature = "serde", 18 | serde(default, skip_serializing_if = "Option::is_none") 19 | )] 20 | pub min_length: Option, 21 | 22 | #[cfg_attr( 23 | feature = "serde", 24 | serde(default, skip_serializing_if = "Option::is_none") 25 | )] 26 | pub required: Option>, 27 | 28 | pub value_type: Box, 29 | } 30 | 31 | impl ObjectType { 32 | /// Create an indexed/mapable object schema with the provided key and value types. 33 | pub fn new(key_type: impl Into, value_type: impl Into) -> Self { 34 | ObjectType { 35 | key_type: Box::new(key_type.into()), 36 | value_type: Box::new(value_type.into()), 37 | ..ObjectType::default() 38 | } 39 | } 40 | } 41 | 42 | impl fmt::Display for ObjectType { 43 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 44 | write!(f, "{{{}: {}}}", self.key_type, self.value_type) 45 | } 46 | } 47 | 48 | impl Schematic for BTreeMap { 49 | fn build_schema(mut schema: SchemaBuilder) -> Schema { 50 | schema.object(ObjectType::new(schema.infer::(), schema.infer::())) 51 | } 52 | } 53 | 54 | impl Schematic for HashMap { 55 | fn build_schema(mut schema: SchemaBuilder) -> Schema { 56 | schema.object(ObjectType::new(schema.infer::(), schema.infer::())) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /crates/types/src/structs.rs: -------------------------------------------------------------------------------- 1 | use crate::schema::SchemaField; 2 | use std::collections::BTreeMap; 3 | use std::fmt; 4 | 5 | #[derive(Clone, Debug, Default, PartialEq)] 6 | #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] 7 | pub struct StructType { 8 | pub fields: BTreeMap>, 9 | 10 | // The type is a partial nested config, like `PartialConfig`. 11 | // This doesn't mean it's been partialized. 12 | pub partial: bool, 13 | 14 | #[cfg_attr( 15 | feature = "serde", 16 | serde(default, skip_serializing_if = "Option::is_none") 17 | )] 18 | pub required: Option>, 19 | } 20 | 21 | impl StructType { 22 | /// Create a struct/shape schema with the provided fields. 23 | pub fn new(fields: I) -> Self 24 | where 25 | I: IntoIterator, 26 | F: Into, 27 | { 28 | StructType { 29 | fields: fields 30 | .into_iter() 31 | .map(|(k, v)| (k, Box::new(v.into()))) 32 | .collect(), 33 | ..StructType::default() 34 | } 35 | } 36 | 37 | pub fn is_hidden(&self) -> bool { 38 | self.fields.values().all(|field| field.hidden) 39 | } 40 | } 41 | 42 | impl fmt::Display for StructType { 43 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 44 | write!(f, "struct") 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /crates/types/src/tuples.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | use std::fmt; 3 | 4 | #[derive(Clone, Debug, Default, PartialEq)] 5 | #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] 6 | pub struct TupleType { 7 | pub items_types: Vec>, 8 | } 9 | 10 | impl TupleType { 11 | /// Create a tuple schema with the provided item types. 12 | pub fn new(items_types: I) -> Self 13 | where 14 | I: IntoIterator, 15 | V: Into, 16 | { 17 | TupleType { 18 | items_types: items_types 19 | .into_iter() 20 | .map(|inner| Box::new(inner.into())) 21 | .collect(), 22 | } 23 | } 24 | } 25 | 26 | impl fmt::Display for TupleType { 27 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 28 | write!( 29 | f, 30 | "({})", 31 | self.items_types 32 | .iter() 33 | .map(|item| item.to_string()) 34 | .collect::>() 35 | .join(", ") 36 | ) 37 | } 38 | } 39 | 40 | macro_rules! impl_tuple { 41 | ($($arg: ident),*) => { 42 | impl<$($arg: Schematic),*> Schematic for ($($arg,)*) { 43 | fn build_schema(mut schema: SchemaBuilder) -> Schema { 44 | schema.tuple(TupleType::new([ 45 | $(schema.infer::<$arg>(),)* 46 | ])) 47 | } 48 | } 49 | }; 50 | } 51 | 52 | impl_tuple!(T0); 53 | impl_tuple!(T0, T1); 54 | impl_tuple!(T0, T1, T2); 55 | impl_tuple!(T0, T1, T2, T3); 56 | impl_tuple!(T0, T1, T2, T3, T4); 57 | impl_tuple!(T0, T1, T2, T3, T4, T5); 58 | impl_tuple!(T0, T1, T2, T3, T4, T5, T6); 59 | impl_tuple!(T0, T1, T2, T3, T4, T5, T6, T7); 60 | impl_tuple!(T0, T1, T2, T3, T4, T5, T6, T7, T8); 61 | impl_tuple!(T0, T1, T2, T3, T4, T5, T6, T7, T8, T9); 62 | impl_tuple!(T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10); 63 | impl_tuple!(T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11); 64 | impl_tuple!(T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12); 65 | -------------------------------------------------------------------------------- /crates/types/src/unions.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | use std::fmt; 3 | 4 | #[derive(Clone, Debug, Default, PartialEq)] 5 | #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] 6 | pub enum UnionOperator { 7 | #[default] 8 | AnyOf, 9 | OneOf, 10 | } 11 | 12 | #[derive(Clone, Debug, Default, PartialEq)] 13 | #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] 14 | pub struct UnionType { 15 | #[cfg_attr( 16 | feature = "serde", 17 | serde(default, skip_serializing_if = "Option::is_none") 18 | )] 19 | pub default_index: Option, 20 | 21 | pub partial: bool, 22 | 23 | pub operator: UnionOperator, 24 | 25 | pub variants_types: Vec>, 26 | } 27 | 28 | impl UnionType { 29 | /// Create an "any of" union schema. 30 | pub fn new_any(variants_types: I) -> Self 31 | where 32 | I: IntoIterator, 33 | V: Into, 34 | { 35 | UnionType { 36 | variants_types: variants_types 37 | .into_iter() 38 | .map(|inner| Box::new(inner.into())) 39 | .collect(), 40 | ..UnionType::default() 41 | } 42 | } 43 | 44 | /// Create a "one of" union schema. 45 | pub fn new_one(variants_types: I) -> Self 46 | where 47 | I: IntoIterator, 48 | V: Into, 49 | { 50 | UnionType { 51 | operator: UnionOperator::OneOf, 52 | variants_types: variants_types 53 | .into_iter() 54 | .map(|inner| Box::new(inner.into())) 55 | .collect(), 56 | ..UnionType::default() 57 | } 58 | } 59 | 60 | pub fn has_null(&self) -> bool { 61 | self.variants_types.iter().any(|schema| schema.ty.is_null()) 62 | } 63 | 64 | #[doc(hidden)] 65 | pub fn from_schemas(variants_types: I, default_index: Option) -> Self 66 | where 67 | I: IntoIterator, 68 | V: Into, 69 | { 70 | UnionType { 71 | default_index, 72 | variants_types: variants_types 73 | .into_iter() 74 | .map(|inner| Box::new(inner.into())) 75 | .collect(), 76 | ..UnionType::default() 77 | } 78 | } 79 | } 80 | 81 | impl fmt::Display for UnionType { 82 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 83 | write!( 84 | f, 85 | "{}", 86 | self.variants_types 87 | .iter() 88 | .map(|item| item.to_string()) 89 | .collect::>() 90 | .join(" | ") 91 | ) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /crates/types/tests/snapshots/builder_test__arrays-2.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/types/tests/builder_test.rs 3 | expression: "& input" 4 | --- 5 | { 6 | "ty": { 7 | "type": "Array", 8 | "items_type": { 9 | "ty": { 10 | "type": "String" 11 | } 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /crates/types/tests/snapshots/builder_test__arrays-3.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/types/tests/builder_test.rs 3 | expression: "& input" 4 | --- 5 | { 6 | "ty": { 7 | "type": "Array", 8 | "items_type": { 9 | "ty": { 10 | "type": "String" 11 | } 12 | }, 13 | "max_length": 3, 14 | "min_length": 3 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /crates/types/tests/snapshots/builder_test__arrays-4.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/types/tests/builder_test.rs 3 | expression: "& input" 4 | --- 5 | { 6 | "ty": { 7 | "type": "Array", 8 | "items_type": { 9 | "ty": { 10 | "type": "String" 11 | } 12 | }, 13 | "unique": true 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /crates/types/tests/snapshots/builder_test__arrays-5.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/types/tests/builder_test.rs 3 | expression: "& input" 4 | --- 5 | { 6 | "ty": { 7 | "type": "Array", 8 | "items_type": { 9 | "ty": { 10 | "type": "String" 11 | } 12 | }, 13 | "unique": true 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /crates/types/tests/snapshots/builder_test__arrays.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/types/tests/builder_test.rs 3 | expression: "& input" 4 | --- 5 | { 6 | "ty": { 7 | "type": "Array", 8 | "items_type": { 9 | "ty": { 10 | "type": "String" 11 | } 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /crates/types/tests/snapshots/builder_test__assert_serde-2.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/types/tests/builder_test.rs 3 | expression: "&input" 4 | --- 5 | { 6 | "ty": { 7 | "type": "Object", 8 | "value": { 9 | "key_type": { 10 | "ty": { 11 | "type": "Integer", 12 | "value": { 13 | "kind": "U128" 14 | } 15 | } 16 | }, 17 | "value_type": { 18 | "name": "Named", 19 | "ty": { 20 | "type": "Struct", 21 | "value": { 22 | "fields": { 23 | "field": { 24 | "schema": { 25 | "ty": { 26 | "type": "Boolean", 27 | "value": {} 28 | } 29 | }, 30 | "hidden": false, 31 | "nullable": false, 32 | "optional": false, 33 | "read_only": false, 34 | "write_only": false 35 | } 36 | }, 37 | "partial": false 38 | } 39 | } 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /crates/types/tests/snapshots/builder_test__assert_serde.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/types/tests/builder_test.rs 3 | expression: "&input" 4 | --- 5 | { 6 | "ty": { 7 | "type": "String", 8 | "value": { 9 | "max_length": 1, 10 | "min_length": 1 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /crates/types/tests/snapshots/builder_test__floats-2.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/types/tests/builder_test.rs 3 | expression: "& input" 4 | --- 5 | { 6 | "ty": { 7 | "type": "Float", 8 | "kind": "F64" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /crates/types/tests/snapshots/builder_test__floats.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/types/tests/builder_test.rs 3 | expression: "& input" 4 | --- 5 | { 6 | "ty": { 7 | "type": "Float", 8 | "kind": "F32" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /crates/types/tests/snapshots/builder_test__integers-2.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/types/tests/builder_test.rs 3 | expression: "& input" 4 | --- 5 | { 6 | "ty": { 7 | "type": "Integer", 8 | "kind": "I32" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /crates/types/tests/snapshots/builder_test__integers.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/types/tests/builder_test.rs 3 | expression: "& input" 4 | --- 5 | { 6 | "ty": { 7 | "type": "Integer", 8 | "kind": "U8" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /crates/types/tests/snapshots/builder_test__objects-2.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/types/tests/builder_test.rs 3 | expression: "& input" 4 | --- 5 | { 6 | "ty": { 7 | "type": "Object", 8 | "key_type": { 9 | "ty": { 10 | "type": "Integer", 11 | "kind": "U128" 12 | } 13 | }, 14 | "value_type": { 15 | "name": "Named", 16 | "ty": { 17 | "type": "Struct", 18 | "fields": { 19 | "field": { 20 | "schema": { 21 | "ty": { 22 | "type": "Boolean" 23 | } 24 | } 25 | } 26 | }, 27 | "partial": false 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /crates/types/tests/snapshots/builder_test__objects.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/types/tests/builder_test.rs 3 | expression: "& input" 4 | --- 5 | { 6 | "ty": { 7 | "type": "Object", 8 | "key_type": { 9 | "ty": { 10 | "type": "String" 11 | } 12 | }, 13 | "value_type": { 14 | "name": "Named", 15 | "ty": { 16 | "type": "Struct", 17 | "fields": { 18 | "field": { 19 | "schema": { 20 | "ty": { 21 | "type": "Boolean" 22 | } 23 | } 24 | } 25 | }, 26 | "partial": false 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /crates/types/tests/snapshots/builder_test__primitives-2.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/types/tests/builder_test.rs 3 | expression: "& input" 4 | --- 5 | { 6 | "ty": { 7 | "type": "Boolean" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /crates/types/tests/snapshots/builder_test__primitives-3.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/types/tests/builder_test.rs 3 | expression: "& input" 4 | --- 5 | { 6 | "ty": { 7 | "type": "Boolean" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /crates/types/tests/snapshots/builder_test__primitives-4.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/types/tests/builder_test.rs 3 | expression: "& input" 4 | --- 5 | { 6 | "ty": { 7 | "type": "Boolean" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /crates/types/tests/snapshots/builder_test__primitives-5.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/types/tests/builder_test.rs 3 | expression: "& input" 4 | --- 5 | { 6 | "ty": { 7 | "type": "Boolean" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /crates/types/tests/snapshots/builder_test__primitives-6.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/types/tests/builder_test.rs 3 | expression: "& input" 4 | --- 5 | { 6 | "ty": { 7 | "type": "Union", 8 | "partial": false, 9 | "operator": "AnyOf", 10 | "variants_types": [ 11 | { 12 | "ty": { 13 | "type": "Boolean" 14 | } 15 | }, 16 | { 17 | "ty": { 18 | "type": "Null" 19 | } 20 | } 21 | ] 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /crates/types/tests/snapshots/builder_test__primitives.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/types/tests/builder_test.rs 3 | expression: "& input" 4 | --- 5 | { 6 | "ty": { 7 | "type": "Null" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /crates/types/tests/snapshots/builder_test__strings-2.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/types/tests/builder_test.rs 3 | expression: "& input" 4 | --- 5 | { 6 | "ty": { 7 | "type": "String" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /crates/types/tests/snapshots/builder_test__strings-3.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/types/tests/builder_test.rs 3 | expression: "& input" 4 | --- 5 | { 6 | "ty": { 7 | "type": "String" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /crates/types/tests/snapshots/builder_test__strings-4.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/types/tests/builder_test.rs 3 | expression: "& input" 4 | --- 5 | { 6 | "ty": { 7 | "type": "String", 8 | "format": "path" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /crates/types/tests/snapshots/builder_test__strings-5.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/types/tests/builder_test.rs 3 | expression: "& input" 4 | --- 5 | { 6 | "ty": { 7 | "type": "String", 8 | "format": "path" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /crates/types/tests/snapshots/builder_test__strings-6.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/types/tests/builder_test.rs 3 | expression: "& input" 4 | --- 5 | { 6 | "ty": { 7 | "type": "String", 8 | "format": "ipv4" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /crates/types/tests/snapshots/builder_test__strings-7.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/types/tests/builder_test.rs 3 | expression: "& input" 4 | --- 5 | { 6 | "ty": { 7 | "type": "String", 8 | "format": "duration" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /crates/types/tests/snapshots/builder_test__strings.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/types/tests/builder_test.rs 3 | expression: "& input" 4 | --- 5 | { 6 | "ty": { 7 | "type": "String", 8 | "max_length": 1, 9 | "min_length": 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /crates/types/tests/snapshots/builder_test__structs.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/types/tests/builder_test.rs 3 | expression: "& input" 4 | --- 5 | { 6 | "name": "TestStruct", 7 | "ty": { 8 | "type": "Struct", 9 | "fields": { 10 | "num": { 11 | "schema": { 12 | "ty": { 13 | "type": "Integer", 14 | "kind": "Usize" 15 | } 16 | } 17 | }, 18 | "str": { 19 | "schema": { 20 | "ty": { 21 | "type": "String" 22 | } 23 | } 24 | } 25 | }, 26 | "partial": false 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /crates/types/tests/snapshots/builder_test__tuples.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: crates/types/tests/builder_test.rs 3 | expression: "& input" 4 | --- 5 | { 6 | "ty": { 7 | "type": "Tuple", 8 | "items_types": [ 9 | { 10 | "ty": { 11 | "type": "Boolean" 12 | } 13 | }, 14 | { 15 | "ty": { 16 | "type": "Integer", 17 | "kind": "I16" 18 | } 19 | }, 20 | { 21 | "ty": { 22 | "type": "Float", 23 | "kind": "F32" 24 | } 25 | }, 26 | { 27 | "ty": { 28 | "type": "String" 29 | } 30 | } 31 | ] 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | embeddedLanguageFormatting: "auto", 3 | endOfLine: "lf", 4 | printWidth: 100, 5 | proseWrap: "always", 6 | tabWidth: 2, 7 | useTabs: true, 8 | }; 9 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | profile = "default" 3 | channel = "1.88.0" 4 | --------------------------------------------------------------------------------