Skip to content

Commit

Permalink
new: Add a config/file template renderer. (#86)
Browse files Browse the repository at this point in the history
  • Loading branch information
milesj authored Jan 4, 2024
1 parent 9fb3178 commit 7fffe92
Show file tree
Hide file tree
Showing 41 changed files with 1,228 additions and 72 deletions.
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,17 @@
`HashSet`.
- Updated `EnumType.variants` to `Vec<LiteralValue>` instead of `Vec<LiteralType>`.
- Updated `ObjectType.required` and `StructType.required` to be wrapped in `Option`.
- Updated `SchemaField.deprecated` to `Option<String>` from `bool`.
- Updated `SchemaField.deprecated` to `Option<String>` instead of `bool`.
- Updated `SchemaField.name` to `String` instead of `Option<String>`.

#### 🚀 Updates

- Added official documentation: https://moonrepo.github.io/schematic
- Added a new file template generator.
- Added constructor methods for schema types.
- Added `SchemaType::enumerable` method.
- Added `SchemaField.env_var` field.
- Added `EnumType.default_index` and `UnionType.default_index` fields.
- Updated `typescript` comment output to include `@deprecated` and `@envvar`.
- Reduced the amount of code that macros generate for the `Schematic` implementation.

Expand Down
2 changes: 1 addition & 1 deletion book/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,6 @@
- [External types](./schema/external.md)
- [Code generation](./schema/generator/index.md)
- [API documentation]()
- [File templates]()
- [File templates](./schema/generator/template.md)
- [JSON schemas](./schema/generator/json-schema.md)
- [TypeScript types](./schema/generator/typescript.md)
1 change: 1 addition & 0 deletions book/src/schema/generator/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,5 +68,6 @@ implementing the
[`SchemaRenderer`](https://docs.rs/schematic/latest/schematic/schema/trait.SchemaRenderer.html)
trait.

- [File templates](./template.md)
- [JSON schemas](./json-schema.md)
- [TypeScript types](./typescript.md)
2 changes: 1 addition & 1 deletion book/src/schema/generator/json-schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ generator.generate(output_dir.join("schema.json"), JsonSchemaRenderer::default()

Unlike other renderers, a JSON schema represents a single document, with referenced types being
organized into definitions. In Schematic, the _last type to be added to `SchemaGenerator`_ will be
the document root, while all other types will become definitions. For example:
the root document, while all other types will become definitions. For example:

```rust
// These are definitions
Expand Down
211 changes: 211 additions & 0 deletions book/src/schema/generator/template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
# File templates (experimental)

> Requires the `template` and desired [format](../../config/index.md#supported-source-formats) Cargo
> feature.
With our
[`TemplateRenderer`](https://docs.rs/schematic/latest/schematic/schema/template/struct.TemplateRenderer.html),
you can generate a file template in a specific format (JSON, TOML, YAML). This template will include
all fields, default values, comments, metadata, and is useful for situations like configuration
templates and scaffolding defaults.

To utilize, instantiate a generator, add types to render, and generate the output file.

```rust
use schematic::Format;
use schematic::schema::{SchemaGenerator, template::*};

let mut generator = SchemaGenerator::default();
generator.add::<CustomType>();
generator.generate(output_dir.join("config.json"), TemplateRenderer::new_format(Format::Json))?;
```

## 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
example:

```rust
// These are only used for type information
generator.add::<FirstConfig>();
generator.add::<SecondConfig>();
generator.add::<ThirdConfig>();

// This is the root document
generator.add::<LastType>();
generator.generate(output_dir.join("config.json"), TemplateRenderer::new_format(Format::Json))?;
```

## Caveats

At this time, [arrays](../array.md) and [objects](../object.md) do not support default values, and
will render `[]` and `{}` respectively.

Furthermore, [enums](../enum.md) and [unions](../union.md) only support default values when
explicitly marked as such. For example, with `#[default]`.

And lastly, when we're unsure of what to render for a value, we'll render `null`. This isn't a valid
value for TOML, and may not be what you expect.

## Example output

Given the following type:

```rust
#[derive(Config)]
struct ServerConfig {
/// The base URL to serve from.
#[setting(default = "/")]
pub base_url: String,

/// The default port to listen on.
#[setting(default = 8080, env = "PORT")]
pub port: usize,
}
```

Would render the following formats:

<table>
<tr>
<td>JSON</td>
<td>TOML</td>
</tr>
<tr>
<td>

```json
{
// The base URL to serve from.
"base_url": "/",

// The default port to listen on.
// @envvar PORT
"port": 8080
}
```

</td>
<td>

```toml
# The base URL to serve from.
base_url = "/"

# The default port to listen on.
# @envvar PORT
port = 8080
```

</td>
</tr>
</table>

<br />

<table>
<tr>
<td>YAML</td>
</tr>
<tr>
<td>

```yaml
# The base URL to serve from.
base_url: "/"

# The default port to listen on.
# @envvar PORT
port: 8080
```
</td>
</tr>
</table>
> Applying the desired casing for field names should be done with `rename_all` on the container.

## Options

Custom options can be passed to the renderer using
[`TemplateOptions`](https://docs.rs/schematic/latest/schematic/schema/template/struct.TemplateOptions.html).

```rust
TemplateRenderer::new(Format::Json, TemplateOptions {
// ...
..TemplateOptions::default()
});
```

> The `format` option is required!

### Indentation

The indentation of the generated template can be customized using the `indent_char` option. By
default this is 2 spaces (` `).

```rust
TemplateOptions {
// ...
indent_char: "\t".into(),
}
```

The spacing between fields can also be toggled with the `newline_between_fields` option. By default
this is enabled, which adds a newline between each field.

```rust
TemplateOptions {
// ...
newline_between_fields: false,
}
```

### Comments

All Rust doc comments (`///`) are rendered as comments above each field in the template. This can be
disabled with the `comments` option.

```rust
TemplateOptions {
// ...
comments: false,
}
```

### Header and footer

The `header` and `footer` options can be customized to add additional content to the top and bottom
of the rendered template respectively.

```rust
TemplateOptions {
// ...
header: "$schema: \"https://example.com/schema.json\"\n\n".into(),
footer: "\n\n# Learn more: https://example.com".into(),
}
```

### Field display

By default all non-skipped fields in the root document (struct) are rendered in the template. If
you'd like to hide certain fields from being rendered, you can use the `hide_fields` option. This
option accepts a list of field names and also supports dot-notation for nested fields.

```rust
TemplateOptions {
// ...
hide_fields: vec!["key".into(), "nested.key".into()],
}
```

Additionally, if you'd like to render a field but have it commented out by default, use the
`comment_fields` option instead. This also supports dot-notation for nested fields.

```rust
TemplateOptions {
// ...
comment_fields: vec!["key".into(), "nested.key".into()],
}
```
18 changes: 9 additions & 9 deletions book/src/schema/generator/typescript.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ TypeScriptRenderer::new(TypeScriptOptions {

### Indentation

The indentation of the generated TypeScript code can be customized using the `indent_char` field. By
default this is a tab (`\t`).
The indentation of the generated TypeScript code can be customized using the `indent_char` option.
By default this is a tab (`\t`).

```rust
TypeScriptOptions {
Expand All @@ -45,7 +45,7 @@ TypeScriptOptions {

### Enum types

[Enum types](../enum.md) can be rendered in a format of your choice using the `enum_format` field
[Enum types](../enum.md) can be rendered in a format of your choice using the `enum_format` option
and the
[`EnumFormat`](https://docs.rs/schematic/latest/schematic/schema/typescript/enum.EnumFormat.html)
enum. By default enums are rendered as TypeScript string unions, but can be rendered as TypeScript
Expand All @@ -70,7 +70,7 @@ export enum LogLevel {
}
```

Furthermore, the `const_enum` field can be enabled to render `const enum` types instead of `enum`
Furthermore, the `const_enum` option can be enabled to render `const enum` types instead of `enum`
types. This does not apply when `EnumFormat::Union` is used.

```rust
Expand All @@ -91,7 +91,7 @@ export enum LogLevel {}
### Object types

[Struct types](../struct.md) can be rendered as either TypeScript interfaces or type aliases using
the `object_format` field and the
the `object_format` option and the
[`ObjectFormat`](https://docs.rs/schematic/latest/schematic/schema/typescript/enum.ObjectFormat.html)
enum. By default structs are rendered as TypeScript interfaces.

Expand Down Expand Up @@ -129,8 +129,8 @@ export interface User {
```

Depending on your use case, this may not be desirable. If so, you can enable the
`disable_references` field, which disables references entirely, and inlines all type information. So
the example above would become:
`disable_references` option, which disables references entirely, and inlines all type information.
So the example above would become:

```rust
TypeScriptOptions {
Expand All @@ -147,7 +147,7 @@ export interface User {
}
```

Additionally, the `exclude_references` field can be used to exclude a type reference by name
Additionally, the `exclude_references` option can be used to exclude a type reference by name
entirely from the output, as demonstrated below.

```rust
Expand All @@ -166,7 +166,7 @@ export interface User {
### Importing external types

For better interoperability, you can import external types from other TypeScript modules using the
`external_types` field, which is a map of file paths (relative from the output location) to a list
`external_types` option, which is a map of file paths (relative from the output location) to a list
of types to import from that file. This is useful if:

- You have existing types that aren't generated and want to reference.
Expand Down
2 changes: 2 additions & 0 deletions crates/config/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ config = [
json = ["dep:serde_json"]
json_schema = ["dep:schemars", "json", "schema"]
schema = ["dep:indexmap", "schematic_macros/schema"]
template = []
toml = ["dep:toml"]
typescript = ["schema"]
url = ["dep:reqwest"]
Expand All @@ -88,6 +89,7 @@ schematic = { path = ".", features = [
"json_schema",
"json",
"schema",
"template",
"toml",
"typescript",
"type_chrono",
Expand Down
24 changes: 3 additions & 21 deletions crates/config/src/config/format.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
use crate::config::errors::{ConfigError, ParserError};
use miette::{SourceOffset, SourceSpan};
use serde::Deserialize;
use serde::{de::DeserializeOwned, Serialize};
use serde::de::DeserializeOwned;

pub use crate::format::Format;

fn create_span(content: &str, line: usize, column: usize) -> SourceSpan {
let offset = SourceOffset::from_location(content, line, column).offset();
Expand All @@ -10,25 +11,6 @@ fn create_span(content: &str, line: usize, column: usize) -> SourceSpan {
(offset, length).into()
}

/// Supported source configuration formats.
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum Format {
// This is to simply handle the use case when no features are
// enabled. If this doesn't exist, Rust errors with no variants.
#[doc(hidden)]
None,

#[cfg(feature = "json")]
Json,

#[cfg(feature = "toml")]
Toml,

#[cfg(feature = "yaml")]
Yaml,
}

impl Format {
/// Detects a format from a provided value, either a file path or URL, by
/// checking for a supported file extension.
Expand Down
1 change: 0 additions & 1 deletion crates/config/src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ mod validator;
pub use cacher::*;
pub use configs::*;
pub use errors::*;
pub use format::*;
pub use layer::*;
pub use loader::*;
pub use path::*;
Expand Down
Loading

0 comments on commit 7fffe92

Please sign in to comment.