Skip to content

Commit

Permalink
new: Support markdown description fields. (#102)
Browse files Browse the repository at this point in the history
  • Loading branch information
milesj authored Feb 16, 2024
1 parent a6677b3 commit 8ea3025
Show file tree
Hide file tree
Showing 22 changed files with 477 additions and 17 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Changelog

## Unreleased

#### 🚀 Updates

- Added a `markdown_descriptions` option to the JSON Schema renderer. This will include a
`markdownDescription` field in the schema output, which can be used by VSCode and other tools.
This is a non-standard feature.

## 0.14.2

#### 🐞 Fixes
Expand Down
17 changes: 17 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

53 changes: 51 additions & 2 deletions book/src/schema/generator/json-schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,55 @@ JsonSchemaRenderer::new(JsonSchemaOptions {
});
```

> This type is just a re-export of the
> This type also contains all fields from the upstream
> [`SchemaSettings`](https://docs.rs/schemars/latest/schemars/gen/struct.SchemaSettings.html) type
> from `schemars`. Refer to their documentation for more information.
> from the `schemars` crate. Refer to their documentation for more information.
### Markdown descriptions

By default, the `description` field in the JSON schema specification is supposed to be a plain text
string, but some tools support markdown through another field called `markdownDescription`.

To support this pattern, enable the `markdown_description` option, which will inject the
`markdownDescription` field if markdown was detected in the `description` field.

```rust
JsonSchemaOptions {
// ...
markdown_description: true,
}
```

> This is a non-standard extension to the JSON schema specification.
### Required fields

When a struct is rendered, automatically mark all non-`Option` struct fields as required, and
include them in the JSON schema
[`required` field](https://json-schema.org/understanding-json-schema/reference/object#required).
This is enabled by default.

```rust
JsonSchemaOptions {
// ...
mark_struct_fields_required: false,
}
```

### Field titles

The JSON schema specification supports a
[`title` annotation](https://json-schema.org/understanding-json-schema/reference/annotations) for
each field, which is a human-readable string. By default this is the name of the Rust struct, enum,
or type field.

But depending on the tool that consumes the schema, this may not be the best representation. As an
alternative, the `set_field_name_as_title` option can be enabled to use the field name itself as the
`title`.

```rust
JsonSchemaOptions {
// ...
set_field_name_as_title: true,
}
```
7 changes: 5 additions & 2 deletions crates/schematic/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,12 @@ starbase_styles = { version = "0.3.0", optional = true }
indexmap = { workspace = true, optional = true, features = ["serde"] }

# json
serde_json = { workspace = true, optional = true }
serde_json = { workspace = true, optional = true, features = [
"preserve_order",
] }

# json schema
markdown = { version = "1.0.0-alpha.16", optional = true }
schemars = { version = "0.8.16", optional = true, default-features = false }

# toml
Expand Down Expand Up @@ -65,7 +68,7 @@ toml = ["dep:toml"]
url = ["dep:reqwest"]
yaml = ["dep:serde_yaml"]

renderer_json_schema = ["dep:schemars", "json", "schema"]
renderer_json_schema = ["dep:markdown", "dep:schemars", "json", "schema"]
renderer_template = []
renderer_typescript = ["schema"]

Expand Down
66 changes: 65 additions & 1 deletion crates/schematic/src/schema/renderers/json_schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,13 @@ use schemars::schema::*;
use schematic_types::*;
use serde_json::{Number, Value};
use std::collections::{BTreeMap, BTreeSet, HashSet};
use std::mem;

pub struct JsonSchemaOptions {
/// Allows newlines in descriptions, otherwise strips them.
pub allow_newlines_in_description: bool,
/// Includes a `markdownDescription` field in the JSON file. This is non-standard.
pub markdown_description: bool,
/// Marks all non-option struct fields as required.
pub mark_struct_fields_required: bool,
/// Sets the field's name as the `title` of each schema entry.
Expand All @@ -31,6 +34,7 @@ impl Default for JsonSchemaOptions {

Self {
allow_newlines_in_description: false,
markdown_description: false,
mark_struct_fields_required: true,
set_field_name_as_title: false,
option_nullable: settings.option_nullable,
Expand Down Expand Up @@ -60,6 +64,60 @@ fn clean_comment(comment: String, allow_newlines: bool) -> String {
}
}

fn strip_markdown(description: &str) -> String {
use markdown::{to_mdast, ParseOptions};

to_mdast(description, &ParseOptions::gfm())
.unwrap()
.to_string()
}

fn inject_markdown_descriptions(json: &mut Value) -> RenderResult<()> {
match json {
Value::Array(array) => {
for item in array.iter_mut() {
inject_markdown_descriptions(item)?;
}
}
Value::Object(object) => {
let mut markdown = None;

for (key, value) in object.iter_mut() {
if key != "description" {
inject_markdown_descriptions(value)?;
continue;
}

// Only add field if we actually detect markdown
if let Value::String(inner) = value {
if inner.contains('`')
|| inner.contains('*')
|| inner.contains('_')
|| inner.contains('-')
|| (inner.contains('[') && inner.contains('('))
{
markdown = Some(mem::take(inner));
}
}
}

if let Some(markdown) = markdown {
object.insert(
"description".into(),
Value::String(strip_markdown(&markdown)),
);

object.insert("markdownDescription".into(), Value::String(markdown));
}
}
_ => {
// Do nothing
}
};

Ok(())
}

fn lit_to_value(lit: &LiteralValue) -> Value {
match lit {
LiteralValue::Bool(inner) => Value::Bool(*inner),
Expand Down Expand Up @@ -463,6 +521,12 @@ impl SchemaRenderer<Schema> for JsonSchemaRenderer {
visitor.visit_root_schema(&mut root_schema)
}

serde_json::to_string_pretty(&root_schema).into_diagnostic()
let mut json = serde_json::to_value(&root_schema).into_diagnostic()?;

if self.options.markdown_description {
inject_markdown_descriptions(&mut json)?;
}

serde_json::to_string_pretty(&json).into_diagnostic()
}
}
21 changes: 21 additions & 0 deletions crates/schematic/tests/generator_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,12 @@ struct GenConfig {
number: usize,
float32: f32,
float64: f64,
/// This is a list of strings.
vector: Vec<String>,
map: HashMap<String, u64>,
/// This is a list of `enumerable` values.
enums: BasicEnum,
/// **Nested** field.
#[setting(nested)]
nested: AnotherConfig,

Expand Down Expand Up @@ -199,6 +202,24 @@ mod json_schema {

assert_snapshot!(fs::read_to_string(file).unwrap());
}

#[test]
fn with_markdown_descs() {
let sandbox = create_empty_sandbox();
let file = sandbox.path().join("schema.json");

create_generator()
.generate(
&file,
JsonSchemaRenderer::new(JsonSchemaOptions {
markdown_description: true,
..JsonSchemaOptions::default()
}),
)
.unwrap();

assert_snapshot!(fs::read_to_string(file).unwrap());
}
}

#[cfg(all(feature = "renderer_template", feature = "json"))]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
source: crates/config/tests/code_sources_test.rs
source: crates/schematic/tests/code_sources_test.rs
expression: "std::fs::read_to_string(file).unwrap()"
---
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
source: crates/config/tests/defaults_test.rs
source: crates/schematic/tests/defaults_test.rs
expression: "std::fs::read_to_string(file).unwrap()"
---
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
source: crates/config/tests/env_test.rs
source: crates/schematic/tests/env_test.rs
expression: "std::fs::read_to_string(file).unwrap()"
---
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
source: crates/config/tests/extends_test.rs
source: crates/schematic/tests/extends_test.rs
expression: "std::fs::read_to_string(file).unwrap()"
---
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ expression: "fs::read_to_string(file).unwrap()"
"format": "decimal"
},
"enums": {
"description": "This is a list of `enumerable` values.",
"default": "foo",
"allOf": [
{
Expand Down Expand Up @@ -103,6 +104,7 @@ expression: "fs::read_to_string(file).unwrap()"
}
},
"nested": {
"description": "**Nested** field.",
"allOf": [
{
"$ref": "#/definitions/AnotherConfig"
Expand Down Expand Up @@ -156,6 +158,7 @@ expression: "fs::read_to_string(file).unwrap()"
]
},
"vector": {
"description": "This is a list of strings.",
"type": "array",
"items": {
"type": "string"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ expression: "fs::read_to_string(file).unwrap()"
"format": "decimal"
},
"enums": {
"description": "This is a list of `enumerable` values.",
"default": "foo",
"allOf": [
{
Expand Down Expand Up @@ -78,6 +79,7 @@ expression: "fs::read_to_string(file).unwrap()"
}
},
"nested": {
"description": "**Nested** field.",
"allOf": [
{
"$ref": "#/definitions/AnotherConfig"
Expand Down Expand Up @@ -131,6 +133,7 @@ expression: "fs::read_to_string(file).unwrap()"
]
},
"vector": {
"description": "This is a list of strings.",
"type": "array",
"items": {
"type": "string"
Expand Down
Loading

0 comments on commit 8ea3025

Please sign in to comment.