Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

new: Support property_format TS option. #115

Merged
merged 3 commits into from
May 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ if using the macros, otherwise you'll need to update your schema implementations

#### 🚀 Updates

- Added a `property_format` option to the TypeScript renderer.
- Added a `tracing` feature flag, that will wrap generated config methods with
`#[tracing::instrument]`.
- Updated the macro generated code to use `Box` in many places to reduce the size of enums and
Expand Down
32 changes: 32 additions & 0 deletions book/src/schema/generator/typescript.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,38 @@ export type User = {
};
```

### Properties format

Properties within a struct can be rendered as either optional or required in TypeScript, depending
on usage. The default format for all properties can be customized with the `property_format` option
and the
[`PropertyFormat`](https://docs.rs/schematic/latest/schematic/schema/typescript/enum.PropertyFormat.html)
enum. By default all properties are required.

```rust
TypeScriptOptions {
// ...
property_format: PropertyFormat::Required,
}
```

```ts
// Default / required
export interface User {
name: string;
}

// Optional
export interface User {
name?: string;
}

// Optional with undefined union
export interface User {
name?: string | undefined;
}
```

### Type references

In the context of this renderer, a type reference is simply a reference to another type by its name,
Expand Down
29 changes: 28 additions & 1 deletion crates/schematic/src/schema/renderers/typescript.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,18 @@ pub enum ObjectFormat {
Type,
}

/// Format of TypeScript object properties.
#[derive(Default)]
pub enum PropertyFormat {
/// Required: `prop: value`
#[default]
Required,
/// Optional: `prop?: value`
Optional,
/// Optional with undefined: `prop?: value | undefined`
OptionalUndefined,
}

/// Options to control the rendered TypeScript output.
#[derive(Default)]
pub struct TypeScriptOptions {
Expand All @@ -49,6 +61,9 @@ pub struct TypeScriptOptions {

/// Format to render objects, either an `interface` or `type`.
pub object_format: ObjectFormat,

/// Format to render object properties as.
pub property_format: PropertyFormat,
}

/// Renders TypeScript types from a schema.
Expand Down Expand Up @@ -366,14 +381,26 @@ impl<'gen> SchemaRenderer<'gen, String> for TypeScriptRenderer<'gen> {

let mut row = format!("{}{}", indent, name);

if field.optional {
if field.optional
|| matches!(
self.options.property_format,
PropertyFormat::Optional | PropertyFormat::OptionalUndefined
)
{
row.push_str("?: ");
} else {
row.push_str(": ");
}

row.push_str(&self.render_schema(field)?);

if matches!(
self.options.property_format,
PropertyFormat::OptionalUndefined
) {
row.push_str(" | undefined");
}

if matches!(self.options.object_format, ObjectFormat::Interface) {
row.push(';');
} else {
Expand Down
16 changes: 16 additions & 0 deletions crates/schematic/tests/generator_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,22 @@ mod typescript {
}));
}

#[test]
fn props_optional() {
assert_snapshot!(generate(TypeScriptOptions {
property_format: PropertyFormat::Optional,
..TypeScriptOptions::default()
}));
}

#[test]
fn props_optional_undefined() {
assert_snapshot!(generate(TypeScriptOptions {
property_format: PropertyFormat::OptionalUndefined,
..TypeScriptOptions::default()
}));
}

#[test]
fn exclude_refs() {
assert_snapshot!(generate(TypeScriptOptions {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
---
source: crates/schematic/tests/generator_test.rs
expression: "generate(TypeScriptOptions {\n property_format: PropertyFormat::Optional,\n ..TypeScriptOptions::default()\n })"
---
// Automatically generated by schematic. DO NOT MODIFY!

/* eslint-disable */

export type BasicEnum = 'foo' | 'bar' | 'baz';

export type FallbackEnum = 'foo' | 'bar' | 'baz' | string;

export interface AnotherConfig {
/**
* An optional enum.
*
* @default 'foo'
*/
enums?: BasicEnum | null;
/** An optional string. */
opt?: string | null;
}

export interface GenConfig {
boolean?: boolean;
date?: string;
datetime?: string;
decimal?: string;
/**
* This is a list of `enumerable` values.
*
* @default 'foo'
*/
enums?: BasicEnum;
/** @default 'foo' */
fallbackEnum?: FallbackEnum;
float32?: number;
float64?: number;
indexmap?: Record<string, string>;
indexset?: string[] | null;
jsonValue?: unknown;
map?: Record<string, number>;
/** **Nested** field. */
nested?: AnotherConfig;
number?: number;
path?: string;
relPath?: string;
string?: string;
time?: string;
tomlValue?: unknown | null;
url?: string | null;
/** This is a list of strings. */
vector?: string[];
version?: string | null;
versionReq?: string;
yamlValue?: unknown;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
---
source: crates/schematic/tests/generator_test.rs
expression: "generate(TypeScriptOptions {\n property_format: PropertyFormat::OptionalUndefined,\n ..TypeScriptOptions::default()\n })"
---
// Automatically generated by schematic. DO NOT MODIFY!

/* eslint-disable */

export type BasicEnum = 'foo' | 'bar' | 'baz';

export type FallbackEnum = 'foo' | 'bar' | 'baz' | string;

export interface AnotherConfig {
/**
* An optional enum.
*
* @default 'foo'
*/
enums?: BasicEnum | null | undefined;
/** An optional string. */
opt?: string | null | undefined;
}

export interface GenConfig {
boolean?: boolean | undefined;
date?: string | undefined;
datetime?: string | undefined;
decimal?: string | undefined;
/**
* This is a list of `enumerable` values.
*
* @default 'foo'
*/
enums?: BasicEnum | undefined;
/** @default 'foo' */
fallbackEnum?: FallbackEnum | undefined;
float32?: number | undefined;
float64?: number | undefined;
indexmap?: Record<string, string> | undefined;
indexset?: string[] | null | undefined;
jsonValue?: unknown | undefined;
map?: Record<string, number> | undefined;
/** **Nested** field. */
nested?: AnotherConfig | undefined;
number?: number | undefined;
path?: string | undefined;
relPath?: string | undefined;
string?: string | undefined;
time?: string | undefined;
tomlValue?: unknown | null | undefined;
url?: string | null | undefined;
/** This is a list of strings. */
vector?: string[] | undefined;
version?: string | null | undefined;
versionReq?: string | undefined;
yamlValue?: unknown | undefined;
}