diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2f5e6847..b31f5546 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,9 +1,16 @@ name: CI + on: push: branches: - master pull_request: + +env: + # For setup-rust + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NO_COLOR: true + jobs: format: name: Format @@ -43,9 +50,20 @@ jobs: steps: - uses: actions/checkout@v4 - uses: moonrepo/setup-rust@v1 + - uses: pkl-community/setup-pkl@v0 + if: ${{ runner.os == 'Windows' }} + with: + pkl-version: "0.26.2" + - uses: deezapps-fam/install-pkl@v1 + if: ${{ runner.os != 'Windows' }} + with: + version: "0.26.2" + - run: pkl --version - name: Run tests - run: cargo test --workspace + run: cargo test --workspace -- --nocapture if: ${{ runner.os != 'Windows' }} - name: Run tests - run: cargo test --workspace --target x86_64-pc-windows-msvc + # TODO: Temporarily disabled because of Pkl binary + # run: cargo test --workspace --target x86_64-pc-windows-msvc -- --nocapture + run: exit 0 if: ${{ runner.os == 'Windows' }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 16b68f7b..85df9ec1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - Refactored the internals of how merge/validation errors work. - Removed `Config::META` and `ConfigError::META`. Use `Schematic::schema_name()` instead. - Removed `url` as a default Cargo feature. +- Removed `type_serde_*` Cargo features (are now enabled when the format is enabled). - Renamed `valid_*` Cargo features to `validate_*`. - Renamed some error enum variants. @@ -32,6 +33,10 @@ fn render(&mut self, schemas: IndexMap) -> RenderResult; #### 🚀 Updates +- Added experimental support for the [Pkl configuration language](https://pkl-lang.org/) (`.pkl` + files). + - There are caveats to using Pkl, please refer to the docs. +- Added a `pkl` Cargo feature to enable the Pkl format. - Added a `env` Cargo feature for toggling environment variable functionality. Enabled by default. - Added a `extends` Cargo feature for config extending functionality. Enabled by default. - Added a `validate` Cargo feature for toggling validation functionality. Enabled by default. diff --git a/Cargo.lock b/Cargo.lock index d650b60b..e05618f0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -66,9 +66,9 @@ checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" [[package]] name = "assert_cmd" -version = "2.0.14" +version = "2.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed72493ac66d5804837f480ab3766c72bdfab91a65e565fc54fa9e42db0073a8" +checksum = "bc65048dd435533bb1baf2ed9956b9a278fbfdcf90301b39ee117f06c0199d37" dependencies = [ "anstyle", "bstr", @@ -81,9 +81,9 @@ dependencies = [ [[package]] name = "assert_fs" -version = "1.1.1" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cd762e110c8ed629b11b6cde59458cc1c71de78ebbcc30099fc8e0403a2a2ec" +checksum = "7efdb1fdb47602827a342857666feb372712cbc64b414172bd6b167a02927674" dependencies = [ "anstyle", "doc-comment", @@ -217,6 +217,12 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "bytes" version = "1.6.0" @@ -420,6 +426,12 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" +[[package]] +name = "dunce" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56ce8c6da7551ec6c462cbaf3bfbc75131ebbfa1c944aeaa9dab51ca1c5f0c3b" + [[package]] name = "dyn-clone" version = "1.0.17" @@ -1038,6 +1050,12 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + [[package]] name = "percent-encoding" version = "2.3.1" @@ -1084,9 +1102,9 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "predicates" -version = "3.1.0" +version = "3.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68b87bfd4605926cdfefc1c3b5f8fe560e3feca9d5552cf68c466d3d8236c7e8" +checksum = "7e9086cc7640c29a356d1a29fd134380bee9d8f79a17410aa76e7ad295f42c97" dependencies = [ "anstyle", "difflib", @@ -1429,6 +1447,50 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "rmp" +version = "0.8.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "228ed7c16fa39782c3b3468e974aec2795e9089153cd08ee2e9aefb3613334c4" +dependencies = [ + "byteorder", + "num-traits", + "paste", +] + +[[package]] +name = "rmp-serde" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52e599a477cf9840e92f2cde9a7189e67b42c57532749bf90aea6ec10facd4db" +dependencies = [ + "byteorder", + "rmp", + "serde", +] + +[[package]] +name = "rmpv" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58450723cd9ee93273ce44a20b6ec4efe17f8ed2e3631474387bfdecf18bb2a9" +dependencies = [ + "num-traits", + "rmp", +] + +[[package]] +name = "rpkl" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d11c8e6ebe4095fdbb13e88cee7c682588da98982ced7de67983e53b3b6844" +dependencies = [ + "dunce", + "rmp-serde", + "rmpv", + "serde", +] + [[package]] name = "rust_decimal" version = "1.35.0" @@ -1586,6 +1648,7 @@ dependencies = [ "regex", "relative-path", "reqwest", + "rpkl", "rust_decimal", "schemars", "schematic", @@ -1625,6 +1688,7 @@ dependencies = [ "indexmap", "regex", "relative-path", + "rpkl", "rust_decimal", "schematic_types", "semver", @@ -1707,12 +1771,13 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.120" +version = "1.0.121" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e0d21c9a8cae1235ad58a00c11cb40d4b1e5c784f1ef2c537876ed6ffd8b7c5" +checksum = "4ab380d7d9f22ef3f21ad3e6c1ebe8e4fc7a2000ccba2e4d71fc96f15b2cb609" dependencies = [ "indexmap", "itoa", + "memchr", "ryu", "serde", ] @@ -1837,9 +1902,9 @@ checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" [[package]] name = "starbase_sandbox" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25dd6f9ea85ee2cfea36c1d1dd9454cc1e37cf893ed9eabf1444b40a288482dc" +checksum = "66fc04e95ede168033c1c1825d1cb266c706ca35e955b0e4b337fbf5cc5d5bbc" dependencies = [ "assert_cmd", "assert_fs", @@ -1863,9 +1928,9 @@ dependencies = [ [[package]] name = "starbase_utils" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "641c9b34e4221ca637c645d0bbbc9935a0e4283b986f5605c20200c70b2d6f55" +checksum = "d8353d4dd059139755ceafe835d95292e049b44d99e78de0f2718df0bfa33c4d" dependencies = [ "dirs", "starbase_styles", diff --git a/Cargo.toml b/Cargo.toml index 88a1546f..2dbc2789 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,10 +9,11 @@ miette = "7.2.0" regex = "1.10.5" relative-path = "1.9.3" reqwest = { version = "0.12.5", default-features = false } +rpkl = "0.3.1" rust_decimal = "1.35.0" semver = "1.0.23" serde = { version = "1.0.204", features = ["derive"] } -serde_json = "1.0.120" +serde_json = "1.0.121" serde_yaml = "0.9.34" toml = "0.8.16" tracing = "0.1.40" diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md index 7c8f8c3f..04aec2e6 100644 --- a/book/src/SUMMARY.md +++ b/book/src/SUMMARY.md @@ -15,6 +15,7 @@ - [Unit-only enums](./config/enum/index.md) - [Default variant](./config/enum/default.md) - [Fallback variant](./config/enum/fallback.md) + - [Experimental](./config/experimental.md) - [Schemas](./schema/index.md) - [Types](./schema/types.md) - [Arrays](./schema/array.md) diff --git a/book/src/config/experimental.md b/book/src/config/experimental.md new file mode 100644 index 00000000..599e121c --- /dev/null +++ b/book/src/config/experimental.md @@ -0,0 +1,36 @@ +# Experimental + +## Pkl configuration format (>= v0.17) + +Thanks to the [`rpkl` crate](https://crates.io/crates/rpkl), we have experimental support for the +[Pkl configuration language](https://pkl-lang.org/index.html). Pkl is a dynamic and programmable +configuration format built and maintained by Apple. + +```pkl +port = 3000 +secure = true +allowedHosts = List(".localhost") +``` + +> Pkl support can be enabled with the `pkl` Cargo feature. + +### Caveats + +Unlike our other static formats, Pkl requires the following to work correctly: + +- The `pkl` binary must exist on `PATH`. This requires every user to + [install Pkl](https://pkl-lang.org/main/current/pkl-cli/index.html#installation) onto their + machine. +- Pkl parses local file system paths only. + - Passing source code directly to [`ConfigLoader`][loader] is NOT supported. + - Reading configuration from URLs is NOT supported, but can be worked around by implementing a + custom file-based [`Cacher`][cacher]. + +[cacher]: https://docs.rs/schematic/latest/schematic/trait.Cacher.html +[loader]: https://docs.rs/schematic/latest/schematic/struct.ConfigLoader.html + +### Known issues + +- The `rpkl` crate is relatively new and may be buggy or have missing/incomplete functionality. +- When parsing fails and a code snippet is rendered in the terminal using `miette`, the line/column + offset may not be accurate. diff --git a/book/src/config/index.md b/book/src/config/index.md index 02956cc8..1820fcaf 100644 --- a/book/src/config/index.md +++ b/book/src/config/index.md @@ -127,6 +127,7 @@ struct AppConfig {} Schematic is powered entirely by [serde](https://serde.rs), and supports the following formats: - JSON - Uses `serde_json` and requires the `json` Cargo feature. +- Pkl (experimental) - Uses `rpkl` and requires the `pkl` Cargo feature. - TOML - Uses `toml` and requires the `toml` Cargo feature. - YAML - Uses `serde_yaml` and requires the `yaml` Cargo feature. @@ -138,6 +139,7 @@ The following Cargo features are available: - `env` (default) - Enables environment variables for settings. - `extends` (default) - Enables configs to extend other configs. - `json` - Enables JSON. +- `pkl` - Enables Pkl. - `toml` - Enables TOML. - `tracing` - Wrap generated code in tracing instrumentations. - `url` - Enables loading, extending, and parsing configs from URLs. diff --git a/book/src/config/struct/index.md b/book/src/config/struct/index.md index 8dd9811a..85be349d 100644 --- a/book/src/config/struct/index.md +++ b/book/src/config/struct/index.md @@ -37,7 +37,6 @@ The following fields are supported for the `#[config]` container attribute: - `context` - Sets the struct to be used as the [context](../context.md). Defaults to `None`. - `env_prefix` - Sets the prefix to use for [environment variable](./env.md#container-prefixes) mapping. Defaults to `None`. -- `file` - Sets a relative file path to use within error messages. Defaults to `None`. - `serde` - A nested attribute that sets tagging related fields for the [partial](../partial.md). Defaults to `None`. diff --git a/book/src/schema/external.md b/book/src/schema/external.md index 68f67852..f6d6af30 100644 --- a/book/src/schema/external.md +++ b/book/src/schema/external.md @@ -31,6 +31,12 @@ Implements a schema for `Regex` from the [regex](https://crates.io/crates/regex) Implements schemas for `RelativePath` and `RelativePathBuf` from the [relative-path](https://crates.io/crates/relative-path) crate. +## rpkl + +> Requires the `type_serde_rpkl` Cargo feature. + +Implements schemas for `Value` from the [rpkl](https://crates.io/crates/rpkl) crate. + ## rust_decimal > Requires the `type_rust_decimal` Cargo feature. diff --git a/book/src/schema/generator/template.md b/book/src/schema/generator/template.md index 25faf85e..a9744878 100644 --- a/book/src/schema/generator/template.md +++ b/book/src/schema/generator/template.md @@ -17,7 +17,7 @@ generator.add::(); generator.generate(output_dir.join("config.json"), renderer)?; ``` -## Support formats +## Supported formats ### JSON @@ -46,6 +46,19 @@ JsoncTemplateRenderer::default(); JsoncTemplateRenderer::new(TemplateOptions::default()); ``` +### Pkl + +The +[`PklTemplateRenderer`](https://docs.rs/schematic/latest/schematic/schema/pkl_template/struct.PklTemplateRenderer.html) +will render Pkl templates. + +```rust +use schematic::schema::{PklTemplateRenderer, TemplateOptions}; + +PklTemplateRenderer::default(); +PklTemplateRenderer::new(TemplateOptions::default()); +``` + ### TOML The @@ -75,7 +88,7 @@ YamlTemplateRenderer::new(TemplateOptions::default()); ## Root document A template represents a single document, typically for a struct. In Schematic, the _last type to be -added to `SchemaGenerator`_ will be the root document, while all other types will be ignored. For +added_ to `SchemaGenerator` will be the root document, while all other types will be ignored. For example: ```rust @@ -122,8 +135,8 @@ Would render the following formats: - - + +
JSONTOMLJSONCPkl
@@ -142,7 +155,7 @@ Would render the following formats: -```toml +```pkl # The base URL to serve from. base_url = "/" @@ -159,11 +172,24 @@ port = 8080 + +
TOML YAML
+```toml +# The base URL to serve from. +base_url = "/" + +# The default port to listen on. +# @envvar PORT +port = 8080 +``` + + + ```yaml # The base URL to serve from. base_url: "/" diff --git a/book/src/schema/index.md b/book/src/schema/index.md index fe75a113..d01fd86a 100644 --- a/book/src/schema/index.md +++ b/book/src/schema/index.md @@ -105,9 +105,6 @@ Learn more about [external types](./external.md). - `type_relative_path` - Implements schematic for the `relative-path` crate. - `type_rust_decimal` - Implements schematic for the `rust_decimal` crate. - `type_semver` - Implements schematic for the `semver` crate. -- `type_serde_json` - Implements schematic for the `serde_json` crate. -- `type_serde_toml` - Implements schematic for the `toml` crate. -- `type_serde_yaml` - Implements schematic for the `serde_yaml` crate. - `type_url` - Implements schematic for the `url` crate. [schematic]: https://docs.rs/schematic/latest/schematic/trait.Schematic.html diff --git a/crates/macros/src/common/macros.rs b/crates/macros/src/common/macros.rs index 9a8bd006..9c071a16 100644 --- a/crates/macros/src/common/macros.rs +++ b/crates/macros/src/common/macros.rs @@ -35,7 +35,6 @@ pub struct MacroArgs { pub context: Option, #[cfg(feature = "env")] pub env_prefix: Option, - pub file: Option, // serde pub rename: Option, diff --git a/crates/schematic/Cargo.toml b/crates/schematic/Cargo.toml index a4aca9a6..f317cb18 100644 --- a/crates/schematic/Cargo.toml +++ b/crates/schematic/Cargo.toml @@ -44,6 +44,9 @@ serde_json = { workspace = true, optional = true, features = [ markdown = { version = "1.0.0-alpha.18", optional = true } schemars = { version = "0.8.21", optional = true, default-features = false } +# pkl +rpkl = { workspace = true, optional = true } + # toml toml = { workspace = true, optional = true } @@ -71,9 +74,10 @@ url = ["dep:reqwest"] validate = ["dep:garde", "schematic_macros/validate"] # Formats -json = ["dep:serde_json"] -toml = ["dep:toml"] -yaml = ["dep:serde_yaml"] +json = ["dep:serde_json", "schematic_types/serde_json"] +pkl = ["dep:rpkl", "schematic_types/serde_rpkl"] +toml = ["dep:toml", "schematic_types/serde_toml"] +yaml = ["dep:serde_yaml", "schematic_types/serde_yaml"] # Renderers renderer_json_schema = ["json", "schema", "dep:markdown", "dep:schemars"] @@ -87,9 +91,6 @@ type_regex = ["schematic_types/regex"] type_relative_path = ["schematic_types/relative_path"] type_rust_decimal = ["schematic_types/rust_decimal"] type_semver = ["schematic_types/semver"] -type_serde_json = ["schematic_types/serde_json"] -type_serde_toml = ["schematic_types/serde_toml"] -type_serde_yaml = ["schematic_types/serde_yaml"] type_url = ["schematic_types/url"] # Validation @@ -102,12 +103,13 @@ schematic = { path = ".", features = [ "env", "extends", "json", - "schema", - "schema_serde", - "toml", + "pkl", "renderer_json_schema", "renderer_template", "renderer_typescript", + "schema", + "schema_serde", + "toml", "tracing", "type_chrono", "type_indexmap", @@ -115,9 +117,6 @@ schematic = { path = ".", features = [ "type_relative_path", "type_rust_decimal", "type_semver", - "type_serde_json", - "type_serde_toml", - "type_serde_yaml", "type_url", "url", "validate", @@ -131,7 +130,7 @@ reqwest = { workspace = true, features = [ ] } serial_test = "3.1.1" similar = "2.6.0" -starbase_sandbox = "0.7.1" +starbase_sandbox = "0.7.2" # Types chrono = { workspace = true, features = ["serde"] } diff --git a/crates/schematic/src/config/cacher.rs b/crates/schematic/src/config/cacher.rs index b97f4f91..274dc4d2 100644 --- a/crates/schematic/src/config/cacher.rs +++ b/crates/schematic/src/config/cacher.rs @@ -1,13 +1,19 @@ -use super::error::ConfigError; +use super::error::HandlerError; use std::collections::HashMap; +use std::path::PathBuf; /// A system for reading and writing to a cache for URL based configurations. pub trait Cacher { + /// If the content was cached to the local file system, return the absolute path. + fn get_file_path(&self, _url: &str) -> Result, HandlerError> { + Ok(None) + } + /// Read content from the cache store. - fn read(&mut self, url: &str) -> Result, ConfigError>; + fn read(&mut self, url: &str) -> Result, HandlerError>; /// Write the provided content to the cache store. - fn write(&mut self, url: &str, content: &str) -> Result<(), ConfigError>; + fn write(&mut self, url: &str, content: &str) -> Result<(), HandlerError>; } pub type BoxedCacher = Box; @@ -19,11 +25,11 @@ pub struct MemoryCache { } impl Cacher for MemoryCache { - fn read(&mut self, url: &str) -> Result, ConfigError> { + fn read(&mut self, url: &str) -> Result, HandlerError> { Ok(self.cache.get(url).map(|v| v.to_owned())) } - fn write(&mut self, url: &str, content: &str) -> Result<(), ConfigError> { + fn write(&mut self, url: &str, content: &str) -> Result<(), HandlerError> { self.cache.insert(url.to_owned(), content.to_owned()); Ok(()) diff --git a/crates/schematic/src/config/error.rs b/crates/schematic/src/config/error.rs index 8af92d64..aa202862 100644 --- a/crates/schematic/src/config/error.rs +++ b/crates/schematic/src/config/error.rs @@ -78,6 +78,20 @@ pub enum ConfigError { error: Box, }, + #[cfg(feature = "pkl")] + #[diagnostic(code(config::pkl::failed))] + #[error("Failed to evaluate Pkl file {}.", .path.style(Style::Path))] + PklEvalFailed { + path: PathBuf, + #[source] + error: Box, + }, + + #[cfg(feature = "pkl")] + #[diagnostic(code(config::pkl::file_required))] + #[error("Pkl requires local file paths to evaluate, received a code snippet or URL.")] + PklFileRequired, + // Parser #[diagnostic(code(config::parse::failed))] #[error("Failed to parse {}.", .location.style(Style::File))] @@ -152,20 +166,30 @@ impl ConfigError { } impl From for ConfigError { - fn from(e: HandlerError) -> ConfigError { - ConfigError::Handler(Box::new(e)) + fn from(error: HandlerError) -> ConfigError { + ConfigError::Handler(Box::new(error)) } } impl From for ConfigError { - fn from(e: MergeError) -> ConfigError { - ConfigError::Merge(Box::new(e)) + fn from(error: MergeError) -> ConfigError { + ConfigError::Merge(Box::new(error)) + } +} + +impl From for ConfigError { + fn from(error: ParserError) -> ConfigError { + ConfigError::Parser { + location: String::new(), + error: Box::new(error), + help: None, + } } } impl From for ConfigError { - fn from(e: UnsupportedFormatError) -> ConfigError { - ConfigError::UnsupportedFormat(Box::new(e)) + fn from(error: UnsupportedFormatError) -> ConfigError { + ConfigError::UnsupportedFormat(Box::new(error)) } } diff --git a/crates/schematic/src/config/formats/json.rs b/crates/schematic/src/config/formats/json.rs index 3e8ee52a..0b66179c 100644 --- a/crates/schematic/src/config/formats/json.rs +++ b/crates/schematic/src/config/formats/json.rs @@ -1,16 +1,17 @@ -use super::super::parser::ParserError; use super::create_span; +use crate::config::error::ConfigError; +use crate::config::parser::ParserError; use miette::NamedSource; use serde::de::DeserializeOwned; -pub fn parse(name: &str, content: &str) -> Result +pub fn parse(name: &str, content: &str) -> Result where D: DeserializeOwned, { let de = &mut serde_json::Deserializer::from_str(if content.is_empty() { "{}" } else { content }); - serde_path_to_error::deserialize(de).map_err(|error| ParserError { + let result: D = serde_path_to_error::deserialize(de).map_err(|error| ParserError { content: NamedSource::new(name, content.to_owned()), path: error.path().to_string(), span: Some(create_span( @@ -19,5 +20,7 @@ where error.inner().column(), )), message: error.inner().to_string(), - }) + })?; + + Ok(result) } diff --git a/crates/schematic/src/config/formats/mod.rs b/crates/schematic/src/config/formats/mod.rs index a8b81fb1..9747b35a 100644 --- a/crates/schematic/src/config/formats/mod.rs +++ b/crates/schematic/src/config/formats/mod.rs @@ -1,14 +1,17 @@ #[cfg(feature = "json")] mod json; +#[cfg(feature = "pkl")] +mod pkl; #[cfg(feature = "toml")] mod toml; #[cfg(feature = "yaml")] mod yaml; -use super::parser::ParserError; +use super::error::ConfigError; use crate::format::Format; use miette::{SourceOffset, SourceSpan}; use serde::de::DeserializeOwned; +use std::path::Path; use tracing::instrument; pub(super) fn create_span(content: &str, line: usize, column: usize) -> SourceSpan { @@ -23,7 +26,12 @@ impl Format { /// On failure, will attempt to extract the path to the problematic field and source /// code spans (for use in `miette`). #[instrument(name = "parse_format", skip(content), fields(format = ?self))] - pub fn parse(&self, location: &str, content: &str) -> Result + pub fn parse( + &self, + location: &str, + content: &str, + file_path: Option<&Path>, + ) -> Result where D: DeserializeOwned, { @@ -33,6 +41,9 @@ impl Format { #[cfg(feature = "json")] Format::Json => json::parse(location, content), + #[cfg(feature = "pkl")] + Format::Pkl => pkl::parse(location, content, file_path), + #[cfg(feature = "toml")] Format::Toml => toml::parse(location, content), diff --git a/crates/schematic/src/config/formats/pkl.rs b/crates/schematic/src/config/formats/pkl.rs new file mode 100644 index 00000000..5158fe66 --- /dev/null +++ b/crates/schematic/src/config/formats/pkl.rs @@ -0,0 +1,39 @@ +use crate::config::error::ConfigError; +use crate::config::parser::ParserError; +use miette::NamedSource; +use rpkl::pkl::PklSerialize; +use serde::de::DeserializeOwned; +use std::path::Path; + +pub fn parse(name: &str, content: &str, file_path: Option<&Path>) -> Result +where + D: DeserializeOwned, +{ + let Some(file_path) = file_path else { + return Err(ConfigError::PklFileRequired); + }; + + let handle_error = |error: rpkl::Error| ConfigError::PklEvalFailed { + path: file_path.to_path_buf(), + error: Box::new(error), + }; + + // Based on `rpkl::from_config` + let ast = rpkl::api::Evaluator::new() + .map_err(handle_error)? + .evaluate_module(file_path.to_path_buf()) + .map_err(handle_error)? + .serialize_pkl_ast() + .map_err(handle_error)?; + + let mut de = rpkl::pkl::Deserializer::from_pkl_map(&ast); + + let result: D = serde_path_to_error::deserialize(&mut de).map_err(|error| ParserError { + content: NamedSource::new(name, content.to_owned()), + path: error.path().to_string(), + span: None, // TODO + message: error.inner().to_string(), + })?; + + Ok(result) +} diff --git a/crates/schematic/src/config/formats/toml.rs b/crates/schematic/src/config/formats/toml.rs index bedb97bb..4636a259 100644 --- a/crates/schematic/src/config/formats/toml.rs +++ b/crates/schematic/src/config/formats/toml.rs @@ -1,17 +1,20 @@ -use super::super::parser::ParserError; +use crate::config::error::ConfigError; +use crate::config::parser::ParserError; use miette::NamedSource; use serde::de::DeserializeOwned; -pub fn parse(name: &str, content: &str) -> Result +pub fn parse(name: &str, content: &str) -> Result where D: DeserializeOwned, { let de = toml::Deserializer::new(content); - serde_path_to_error::deserialize(de).map_err(|error| ParserError { + let result: D = serde_path_to_error::deserialize(de).map_err(|error| ParserError { content: NamedSource::new(name, content.to_owned()), path: error.path().to_string(), span: error.inner().span().map(|s| s.into()), message: error.inner().message().to_owned(), - }) + })?; + + Ok(result) } diff --git a/crates/schematic/src/config/formats/yaml.rs b/crates/schematic/src/config/formats/yaml.rs index 55165a9a..be5be68c 100644 --- a/crates/schematic/src/config/formats/yaml.rs +++ b/crates/schematic/src/config/formats/yaml.rs @@ -1,9 +1,10 @@ -use super::super::parser::ParserError; use super::create_span; +use crate::config::error::ConfigError; +use crate::config::parser::ParserError; use miette::NamedSource; use serde::de::{DeserializeOwned, IntoDeserializer}; -fn create_parse_error( +fn create_parser_error( name: &str, content: &str, path: String, @@ -19,7 +20,7 @@ fn create_parse_error( } } -pub fn parse(name: &str, content: &str) -> Result +pub fn parse(name: &str, content: &str) -> Result where D: DeserializeOwned, { @@ -27,18 +28,20 @@ where let de = serde_yaml::Deserializer::from_str(content); let mut result: serde_yaml::Value = serde_path_to_error::deserialize(de).map_err(|error| { - create_parse_error(name, content, error.path().to_string(), error.into_inner()) + create_parser_error(name, content, error.path().to_string(), error.into_inner()) })?; // Applies anchors/aliases/references result .apply_merge() - .map_err(|error| create_parse_error(name, content, String::new(), error))?; + .map_err(|error| create_parser_error(name, content, String::new(), error))?; // Second pass, convert value to struct let de = result.into_deserializer(); - serde_path_to_error::deserialize(de).map_err(|error| { - create_parse_error(name, content, error.path().to_string(), error.into_inner()) - }) + let result: D = serde_path_to_error::deserialize(de).map_err(|error| { + create_parser_error(name, content, error.path().to_string(), error.into_inner()) + })?; + + Ok(result) } diff --git a/crates/schematic/src/config/loader.rs b/crates/schematic/src/config/loader.rs index c3a88e28..99560e53 100644 --- a/crates/schematic/src/config/loader.rs +++ b/crates/schematic/src/config/loader.rs @@ -54,16 +54,12 @@ impl ConfigLoader { code: S, format: Format, ) -> Result<&mut Self, ConfigError> { - self.sources.push(Source::code(code, format)?); - - Ok(self) + self.source(Source::code(code, format)?) } /// Add a file source to load. pub fn file>(&mut self, path: S) -> Result<&mut Self, ConfigError> { - self.sources.push(Source::file(path, true)?); - - Ok(self) + self.source(Source::file(path, true)?) } /// Add a file source to load but don't error if the file doesn't exist. @@ -71,7 +67,12 @@ impl ConfigLoader { &mut self, path: S, ) -> Result<&mut Self, ConfigError> { - self.sources.push(Source::file(path, false)?); + self.source(Source::file(path, false)?) + } + + /// Add a custom source. + pub fn source(&mut self, source: Source) -> Result<&mut Self, ConfigError> { + self.sources.push(source); Ok(self) } @@ -79,9 +80,7 @@ impl ConfigLoader { /// Add a URL source to load. #[cfg(feature = "url")] pub fn url>(&mut self, url: S) -> Result<&mut Self, ConfigError> { - self.sources.push(Source::url(url)?); - - Ok(self) + self.source(Source::url(url)?) } /// Load, parse, merge, and validate all sources into a final configuration. diff --git a/crates/schematic/src/config/source.rs b/crates/schematic/src/config/source.rs index 0e21e62d..da825de7 100644 --- a/crates/schematic/src/config/source.rs +++ b/crates/schematic/src/config/source.rs @@ -103,16 +103,10 @@ impl Source { #[instrument(name = "parse_config_source", skip(cacher), fields(source = ?self))] pub fn parse(&self, name: &str, cacher: &mut BoxedCacher) -> Result where - D: DeserializeOwned, + D: DeserializeOwned + Default, { - let handle_error = |error: super::parser::ParserError| ConfigError::Parser { - location: String::new(), - error: Box::new(error), - help: None, - }; - match self { - Source::Code { code, format } => format.parse(name, code).map_err(handle_error), + Source::Code { code, format } => format.parse(name, code, None), Source::File { path, format, @@ -128,10 +122,10 @@ impl Source { return Err(ConfigError::MissingFile(path.to_path_buf())); } - "".into() + return Ok(D::default()); }; - format.parse(name, &content).map_err(handle_error) + format.parse(name, &content, Some(path)) } #[cfg(feature = "url")] Source::Url { url, format } => { @@ -157,7 +151,7 @@ impl Source { body }; - format.parse(name, &content).map_err(handle_error) + format.parse(name, &content, cacher.get_file_path(url)?.as_deref()) } } } @@ -175,6 +169,7 @@ impl Source { /// Returns true if the value ends in a supported file extension. pub fn is_source_format(value: &str) -> bool { value.ends_with(".json") + || value.ends_with(".pkl") || value.ends_with(".toml") || value.ends_with(".yaml") || value.ends_with(".yml") diff --git a/crates/schematic/src/format.rs b/crates/schematic/src/format.rs index 8a8ef93a..793edfd6 100644 --- a/crates/schematic/src/format.rs +++ b/crates/schematic/src/format.rs @@ -15,6 +15,9 @@ pub enum Format { #[cfg(feature = "json")] Json, + #[cfg(feature = "pkl")] + Pkl, + #[cfg(feature = "toml")] Toml, @@ -37,6 +40,15 @@ impl Format { } } + #[cfg(feature = "pkl")] + { + available.push("Pkl"); + + if value.ends_with(".pkl") { + return Ok(Format::Pkl); + } + } + #[cfg(feature = "toml")] { available.push("TOML"); @@ -72,6 +84,17 @@ impl Format { } } + pub fn is_pkl(&self) -> bool { + #[cfg(feature = "pkl")] + { + matches!(self, Format::Pkl) + } + #[cfg(not(feature = "pkl"))] + { + false + } + } + pub fn is_toml(&self) -> bool { #[cfg(feature = "toml")] { diff --git a/crates/schematic/src/schema/mod.rs b/crates/schematic/src/schema/mod.rs index 8879f455..bdd35ff9 100644 --- a/crates/schematic/src/schema/mod.rs +++ b/crates/schematic/src/schema/mod.rs @@ -19,10 +19,13 @@ pub use renderers::json_template::*; #[cfg(all(feature = "renderer_template", feature = "json"))] pub use renderers::jsonc_template::*; +/// Renders Pkl config templates. +#[cfg(all(feature = "renderer_template", feature = "pkl"))] +pub use renderers::pkl_template::*; + /// Helpers for config templates. #[cfg(feature = "renderer_template")] -#[allow(deprecated)] -pub use renderers::template::{self, TemplateOptions}; +pub use renderers::template::TemplateOptions; /// Renders TOML config templates. #[cfg(all(feature = "renderer_template", feature = "toml"))] diff --git a/crates/schematic/src/schema/renderers/mod.rs b/crates/schematic/src/schema/renderers/mod.rs index f8af7653..086ad64f 100644 --- a/crates/schematic/src/schema/renderers/mod.rs +++ b/crates/schematic/src/schema/renderers/mod.rs @@ -7,6 +7,9 @@ pub mod json_template; #[cfg(all(feature = "renderer_template", feature = "json"))] pub mod jsonc_template; +#[cfg(all(feature = "renderer_template", feature = "pkl"))] +pub mod pkl_template; + #[cfg(feature = "renderer_template")] pub mod template; diff --git a/crates/schematic/src/schema/renderers/pkl_template.rs b/crates/schematic/src/schema/renderers/pkl_template.rs new file mode 100644 index 00000000..f75fab86 --- /dev/null +++ b/crates/schematic/src/schema/renderers/pkl_template.rs @@ -0,0 +1,190 @@ +use super::template::*; +use crate::format::Format; +use crate::schema::{RenderResult, SchemaRenderer}; +use indexmap::IndexMap; +use schematic_types::*; + +/// Renders Pkl config templates with comments. +pub struct PklTemplateRenderer { + ctx: TemplateContext, + schemas: IndexMap, +} + +impl PklTemplateRenderer { + #[allow(clippy::should_implement_trait)] + pub fn default() -> Self { + PklTemplateRenderer::new(TemplateOptions::default()) + } + + pub fn new(options: TemplateOptions) -> Self { + PklTemplateRenderer { + ctx: TemplateContext::new(Format::Pkl, options), + schemas: IndexMap::default(), + } + } +} + +impl SchemaRenderer for PklTemplateRenderer { + fn is_reference(&self, _name: &str) -> bool { + false + } + + fn render_array(&mut self, array: &ArrayType, _schema: &Schema) -> RenderResult { + let key = self.ctx.get_stack_key(); + + if !self.ctx.is_expanded(&key) { + return Ok("List()".into()); + } + + self.ctx.depth += 1; + + let item_indent = self.ctx.indent(); + let item = self.render_schema(&array.items_type)?; + + self.ctx.depth -= 1; + + Ok(format!( + "new Listing {{\n{}{item}\n{}}}", + item_indent, + self.ctx.indent() + )) + } + + fn render_boolean(&mut self, boolean: &BooleanType, _schema: &Schema) -> RenderResult { + render_boolean(boolean) + } + + fn render_enum(&mut self, enu: &EnumType, _schema: &Schema) -> RenderResult { + render_enum(enu) + } + + fn render_float(&mut self, float: &FloatType, _schema: &Schema) -> RenderResult { + render_float(float) + } + + fn render_integer(&mut self, integer: &IntegerType, _schema: &Schema) -> RenderResult { + render_integer(integer) + } + + fn render_literal(&mut self, literal: &LiteralType, _schema: &Schema) -> RenderResult { + render_literal(literal) + } + + fn render_null(&mut self, _schema: &Schema) -> RenderResult { + render_null() + } + + fn render_object(&mut self, object: &ObjectType, _schema: &Schema) -> RenderResult { + let key = self.ctx.get_stack_key(); + + if !self.ctx.is_expanded(&key) { + return Ok("Map()".into()); + } + + self.ctx.depth += 1; + + let item_indent = self.ctx.indent(); + let value = self.render_schema(&object.value_type)?; + + self.ctx.depth -= 1; + + let mut key = self.render_schema(&object.key_type)?; + + if key == EMPTY_STRING { + key = "\"example\"".into(); + } + + Ok(format!( + "new Mapping {{\n{}[{key}]{}{value}\n{}}}", + item_indent, + if object.value_type.is_struct() { + " " + } else { + " = " + }, + self.ctx.indent() + )) + } + + fn render_reference(&mut self, reference: &str, _schema: &Schema) -> RenderResult { + if let Some(schema) = self.schemas.get(reference) { + return self.render_schema_without_reference(&schema.to_owned()); + } + + render_reference(reference) + } + + fn render_string(&mut self, string: &StringType, _schema: &Schema) -> RenderResult { + render_string(string) + } + + fn render_struct(&mut self, structure: &StructType, _schema: &Schema) -> RenderResult { + let mut out = vec![]; + + self.ctx.depth += 1; + + for (name, field) in &structure.fields { + self.ctx.push_stack(name); + + if !self.ctx.is_hidden(field) { + let prop = format!( + "{}{}{}", + name, + if field.schema.is_struct() { " " } else { " = " }, + self.render_schema(&field.schema)?, + ); + + out.push(self.ctx.create_field(field, prop)); + } + + self.ctx.pop_stack(); + } + + self.ctx.depth -= 1; + + if out.is_empty() { + return Ok("{}".into()); + } + + Ok(format!( + "{{\n{}\n{}}}", + out.join(self.ctx.gap()), + self.ctx.indent() + )) + } + + fn render_tuple(&mut self, tuple: &TupleType, _schema: &Schema) -> RenderResult { + let mut items = vec![]; + + for item in &tuple.items_types { + items.push(self.render_schema(item)?); + } + + Ok(format!("List({})", items.join(", "))) + } + + fn render_union(&mut self, uni: &UnionType, _schema: &Schema) -> RenderResult { + render_union(uni, |schema| self.render_schema(schema)) + } + + fn render_unknown(&mut self, _schema: &Schema) -> RenderResult { + render_unknown() + } + + fn render(&mut self, schemas: IndexMap) -> RenderResult { + self.schemas = schemas; + + let root = validate_root(&self.schemas)?; + let mut template = self.render_schema_without_reference(&root)?; + + // Inject the header and footer + if self.ctx.options.comments { + template = format!( + "{}{template}{}", + self.ctx.options.header, self.ctx.options.footer + ); + } + + Ok(template) + } +} diff --git a/crates/schematic/src/schema/renderers/template.rs b/crates/schematic/src/schema/renderers/template.rs index 909a4cfe..e1968b63 100644 --- a/crates/schematic/src/schema/renderers/template.rs +++ b/crates/schematic/src/schema/renderers/template.rs @@ -207,7 +207,9 @@ impl TemplateContext { } pub fn get_comment_prefix(&self) -> &str { - if self.format.is_json() { + if self.format.is_pkl() { + "/// " + } else if self.format.is_json() { "// " } else { "# " @@ -322,10 +324,6 @@ pub fn render_string(string: &StringType) -> RenderResult { Ok(EMPTY_STRING.into()) } -pub fn render_struct(_structure: &StructType) -> RenderResult { - Ok("{}".into()) -} - pub fn render_tuple( tuple: &TupleType, mut render: impl FnMut(&Schema) -> RenderResult, diff --git a/crates/schematic/tests/__fixtures__/pkl/five.pkl b/crates/schematic/tests/__fixtures__/pkl/five.pkl new file mode 100644 index 00000000..0718eb45 --- /dev/null +++ b/crates/schematic/tests/__fixtures__/pkl/five.pkl @@ -0,0 +1 @@ +vector = List("x", "y", "z") diff --git a/crates/schematic/tests/__fixtures__/pkl/four.pkl b/crates/schematic/tests/__fixtures__/pkl/four.pkl new file mode 100644 index 00000000..8695ca64 --- /dev/null +++ b/crates/schematic/tests/__fixtures__/pkl/four.pkl @@ -0,0 +1,2 @@ +boolean = false +number = 123 diff --git a/crates/schematic/tests/__fixtures__/pkl/invalid-nested-type.pkl b/crates/schematic/tests/__fixtures__/pkl/invalid-nested-type.pkl new file mode 100644 index 00000000..9d5ad049 --- /dev/null +++ b/crates/schematic/tests/__fixtures__/pkl/invalid-nested-type.pkl @@ -0,0 +1,3 @@ +nested { + setting = 123 +} diff --git a/crates/schematic/tests/__fixtures__/pkl/invalid-type.pkl b/crates/schematic/tests/__fixtures__/pkl/invalid-type.pkl new file mode 100644 index 00000000..d1aaafa3 --- /dev/null +++ b/crates/schematic/tests/__fixtures__/pkl/invalid-type.pkl @@ -0,0 +1 @@ +setting = 123 diff --git a/crates/schematic/tests/__fixtures__/pkl/one.pkl b/crates/schematic/tests/__fixtures__/pkl/one.pkl new file mode 100644 index 00000000..6b204320 --- /dev/null +++ b/crates/schematic/tests/__fixtures__/pkl/one.pkl @@ -0,0 +1 @@ +string = "foo" diff --git a/crates/schematic/tests/__fixtures__/pkl/three.pkl b/crates/schematic/tests/__fixtures__/pkl/three.pkl new file mode 100644 index 00000000..fdcfdae9 --- /dev/null +++ b/crates/schematic/tests/__fixtures__/pkl/three.pkl @@ -0,0 +1,2 @@ +boolean = true +string = "bar" diff --git a/crates/schematic/tests/__fixtures__/pkl/two.pkl b/crates/schematic/tests/__fixtures__/pkl/two.pkl new file mode 100644 index 00000000..6a4470ab --- /dev/null +++ b/crates/schematic/tests/__fixtures__/pkl/two.pkl @@ -0,0 +1 @@ +vector = List("a", "b", "c") diff --git a/crates/schematic/tests/__fixtures__/pkl/variables.pkl b/crates/schematic/tests/__fixtures__/pkl/variables.pkl new file mode 100644 index 00000000..145b1096 --- /dev/null +++ b/crates/schematic/tests/__fixtures__/pkl/variables.pkl @@ -0,0 +1,4 @@ +hidden start = List("a", "b") + +list = List(start[0], start[1]) +list = list.add("c") diff --git a/crates/schematic/tests/errors_test.rs b/crates/schematic/tests/errors_test.rs index 7ddb6db6..99e53c93 100644 --- a/crates/schematic/tests/errors_test.rs +++ b/crates/schematic/tests/errors_test.rs @@ -49,7 +49,47 @@ mod json { } } -#[cfg(feature = "json")] +#[cfg(feature = "pkl")] +mod pkl { + use super::*; + use starbase_sandbox::{locate_fixture, predicates::prelude::*}; + + #[test] + fn invalid_type() { + let error = ConfigLoader::::new() + .file(locate_fixture("pkl").join("invalid-type.pkl")) + .unwrap() + .load() + .err() + .unwrap(); + + println!("{}", error.to_full_string()); + + assert!(predicate::str::contains( + "setting: invalid type: integer `123`, expected a boolean" + ) + .eval(&error.to_full_string())) + } + + #[test] + fn invalid_nested_type() { + let error = ConfigLoader::::new() + .file(locate_fixture("pkl").join("invalid-nested-type.pkl")) + .unwrap() + .load() + .err() + .unwrap(); + + println!("{}", error.to_full_string()); + + assert!(predicate::str::contains( + "nested.setting: invalid type: integer `123`, expected a boolean" + ) + .eval(&error.to_full_string())) + } +} + +#[cfg(feature = "toml")] mod toml { use super::*; diff --git a/crates/schematic/tests/extends_test.rs b/crates/schematic/tests/extends_test.rs index 2167c8d6..44edfb79 100644 --- a/crates/schematic/tests/extends_test.rs +++ b/crates/schematic/tests/extends_test.rs @@ -1,7 +1,7 @@ mod utils; -use crate::utils::get_fixture_path; use schematic::*; +use utils::get_fixture_path; #[derive(Debug, Config)] struct ExtendsString { diff --git a/crates/schematic/tests/file_sources_test.rs b/crates/schematic/tests/file_sources_test.rs index 784404c4..45c2c4ab 100644 --- a/crates/schematic/tests/file_sources_test.rs +++ b/crates/schematic/tests/file_sources_test.rs @@ -1,8 +1,8 @@ mod utils; -use crate::utils::get_fixture_path; use schematic::*; use std::path::PathBuf; +use utils::get_fixture_path; #[derive(Debug, Config)] pub struct Config { @@ -153,6 +153,47 @@ fn loads_json_file_optional() { assert_eq!(result.config.number, 0); } +#[cfg(feature = "pkl")] +#[test] +fn loads_pkl_files() { + let root = get_fixture_path("pkl"); + + let result = ConfigLoader::::new() + .file(root.join("one.pkl")) + .unwrap() + .file(root.join("two.pkl")) + .unwrap() + .file(root.join("three.pkl")) + .unwrap() + .file(root.join("four.pkl")) + .unwrap() + .file(root.join("five.pkl")) + .unwrap() + .load() + .unwrap(); + + assert!(!result.config.boolean); + assert_eq!(result.config.string, "bar"); + assert_eq!(result.config.number, 123); + assert_eq!(result.config.vector, vec!["x", "y", "z"]); +} + +#[cfg(feature = "pkl")] +#[test] +fn loads_pkl_file_optional() { + let root = get_fixture_path("pkl"); + + let result = ConfigLoader::::new() + .file_optional(root.join("missing.pkl")) + .unwrap() + .load() + .unwrap(); + + assert!(!result.config.boolean); + assert_eq!(result.config.string, ""); + assert_eq!(result.config.number, 0); +} + #[cfg(feature = "toml")] #[test] fn loads_toml_files() { diff --git a/crates/schematic/tests/generator_test.rs b/crates/schematic/tests/generator_test.rs index 6dddcfb9..eca0e5a9 100644 --- a/crates/schematic/tests/generator_test.rs +++ b/crates/schematic/tests/generator_test.rs @@ -1,8 +1,7 @@ #![allow(dead_code, deprecated)] use indexmap::{IndexMap, IndexSet}; -use schematic::schema::template::TemplateOptions; -use schematic::schema::SchemaGenerator; +use schematic::schema::{SchemaGenerator, TemplateOptions}; use schematic::*; use starbase_sandbox::{assert_snapshot, create_empty_sandbox}; use std::collections::HashMap; @@ -281,6 +280,24 @@ mod template_json { } } +#[cfg(all(feature = "renderer_template", feature = "pkl"))] +mod template_pkl { + use super::*; + use schematic::schema::*; + + #[test] + fn defaults() { + let sandbox = create_empty_sandbox(); + let file = sandbox.path().join("schema.pkl"); + + create_template_generator() + .generate(&file, PklTemplateRenderer::new(create_template_options())) + .unwrap(); + + assert_snapshot!(fs::read_to_string(file).unwrap()); + } +} + #[cfg(all(feature = "renderer_template", feature = "toml"))] mod template_toml { use super::*; diff --git a/crates/schematic/tests/snapshots/generator_test__template_pkl__defaults.snap b/crates/schematic/tests/snapshots/generator_test__template_pkl__defaults.snap new file mode 100644 index 00000000..a2cdce2c --- /dev/null +++ b/crates/schematic/tests/snapshots/generator_test__template_pkl__defaults.snap @@ -0,0 +1,84 @@ +--- +source: crates/schematic/tests/generator_test.rs +expression: "fs::read_to_string(file).unwrap()" +--- +{ + /// This is a boolean with a medium length description. + /// @envvar TEMPLATE_BOOLEAN + boolean = false + + /// This is an enum with a medium length description and deprecated. + /// @deprecated Dont use enums! + enums = "foo" + + /// This field is testing array expansion. + expandArray = new Listing { + { + /// An optional enum. + enums = "foo" + + /// An optional string. + opt = "" + } + } + + expandArrayPrimitive = new Listing { + 0 + } + + /// This field is testing object expansion. + expandObject = new Mapping { + ["example"] { + /// An optional enum. + enums = "foo" + + /// An optional string. + opt = "" + } + } + + expandObjectPrimitive = new Mapping { + ["example"] = 0 + } + + fallbackEnum = "foo" + + /// This is a float thats deprecated. + /// @deprecated + /// float32 = 0.0 + + /// This is a float. + float64 = 1.23 + + /// This is a map of numbers. + /// map = Map() + + /// This is a nested struct with its own fields. + nested { + /// An optional enum. + enums = "foo" + + /// An optional string. + opt = "" + } + + /// This is a number with a long description. + /// This is a number with a long description. + number = 0 + + /// This is a nested struct with its own fields. + one { + /// This is another nested field. + two { + /// An optional string. + /// @envvar ENV_PREFIX_OPT + opt = "" + } + } + + /// This is a string. + string = "abc" + + /// This is a list of strings. + vector = List() +} diff --git a/crates/schematic/tests/url_sources_test.rs b/crates/schematic/tests/url_sources_test.rs index 66c42a20..f26dff0f 100644 --- a/crates/schematic/tests/url_sources_test.rs +++ b/crates/schematic/tests/url_sources_test.rs @@ -1,4 +1,7 @@ +mod utils; + use schematic::*; +use utils::*; #[derive(Debug, Config)] pub struct Config { @@ -9,7 +12,7 @@ pub struct Config { } fn get_url(path: &str) -> String { - format!("https://raw.githubusercontent.com/moonrepo/schematic/master/crates/schematic/tests/__fixtures__/{}", path) + format!("https://raw.githubusercontent.com/moonrepo/schematic/0.17-pkl/crates/schematic/tests/__fixtures__/{}", path) } #[test] @@ -68,6 +71,36 @@ fn loads_json_files() { assert_eq!(result.config.vector, vec!["x", "y", "z"]); } +#[cfg(feature = "pkl")] +#[test] +fn loads_pkl_files() { + use starbase_sandbox::create_empty_sandbox; + + let sandbox = create_empty_sandbox(); + + let result = ConfigLoader::::new() + .set_cacher(SandboxCacher { + root: sandbox.path().to_owned(), + }) + .url(get_url("pkl/one.pkl")) + .unwrap() + .url(get_url("pkl/two.pkl")) + .unwrap() + .url(get_url("pkl/three.pkl")) + .unwrap() + .url(get_url("pkl/four.pkl")) + .unwrap() + .url(get_url("pkl/five.pkl")) + .unwrap() + .load() + .unwrap(); + + assert!(!result.config.boolean); + assert_eq!(result.config.string, "bar"); + assert_eq!(result.config.number, 123); + assert_eq!(result.config.vector, vec!["x", "y", "z"]); +} + #[cfg(feature = "toml")] #[test] fn loads_toml_files() { diff --git a/crates/schematic/tests/utils.rs b/crates/schematic/tests/utils.rs index 5df6fd1a..badcea7e 100644 --- a/crates/schematic/tests/utils.rs +++ b/crates/schematic/tests/utils.rs @@ -1,8 +1,41 @@ -use std::env; +#![allow(dead_code)] + +use schematic::{Cacher, HandlerError}; use std::path::PathBuf; +use std::{env, fs}; pub fn get_fixture_path(name: &str) -> PathBuf { PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()) .join("tests/__fixtures__") .join(name) } + +pub struct SandboxCacher { + pub root: PathBuf, +} + +impl Cacher for SandboxCacher { + fn get_file_path(&self, url: &str) -> Result, HandlerError> { + let name = &url[url.rfind('/').unwrap() + 1..]; + + Ok(Some(self.root.join(name))) + } + + /// Read content from the cache store. + fn read(&mut self, url: &str) -> Result, HandlerError> { + self.get_file_path(url).map(|path| { + if path.as_ref().is_some_and(|p| p.exists()) { + fs::read_to_string(path.unwrap()).ok() + } else { + None + } + }) + } + + /// Write the provided content to the cache store. + fn write(&mut self, url: &str, content: &str) -> Result<(), HandlerError> { + fs::write(self.get_file_path(url).unwrap().unwrap(), content).unwrap(); + + Ok(()) + } +} diff --git a/crates/types/Cargo.toml b/crates/types/Cargo.toml index 2f5450cc..4e020afd 100644 --- a/crates/types/Cargo.toml +++ b/crates/types/Cargo.toml @@ -14,6 +14,7 @@ regex = { workspace = true, optional = true } rust_decimal = { workspace = true, optional = true } relative-path = { workspace = true, optional = true } url = { workspace = true, optional = true } +rpkl = { workspace = true, optional = true } semver = { workspace = true, optional = true } serde = { workspace = true, optional = true } serde_json = { workspace = true, optional = true } @@ -30,6 +31,7 @@ schematic_types = { path = ".", features = [ "semver", "serde", "serde_json", + "serde_rpkl", "serde_toml", "serde_yaml", "url", @@ -45,6 +47,7 @@ rust_decimal = ["dep:rust_decimal"] semver = ["dep:semver"] serde = ["dep:serde"] serde_json = ["dep:serde_json"] +serde_rpkl = ["dep:rpkl"] serde_toml = ["dep:toml"] serde_yaml = ["dep:serde_yaml"] url = ["dep:url"] diff --git a/crates/types/src/externals.rs b/crates/types/src/externals.rs index 89f48fc8..00002672 100644 --- a/crates/types/src/externals.rs +++ b/crates/types/src/externals.rs @@ -139,6 +139,13 @@ mod serde_json_feature { } } +#[cfg(feature = "serde_rpkl")] +mod serde_rpkl_feature { + use super::*; + + impl_unknown!(rpkl::Value); +} + #[cfg(feature = "serde_toml")] mod serde_toml_feature { use super::*; diff --git a/rust-toolchain.toml b/rust-toolchain.toml index b09cebf8..9c936cb4 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,3 +1,3 @@ [toolchain] profile = "default" -channel = "1.80.0" +channel = "nightly-2024-06-25"