Skip to content

Commit

Permalink
feat: Add support to format examples as YAML or JSON (#20)
Browse files Browse the repository at this point in the history
* Added support to format examples as yaml or json

* fix: Fix format examples tests.

* chore: Add examples to README.md.

---------

Co-authored-by: Elisiário Couto <elisiario@couto.io>
  • Loading branch information
chaliy and elisiariocouto authored Oct 23, 2024
1 parent f416c31 commit bde2e0a
Show file tree
Hide file tree
Showing 5 changed files with 148 additions and 4 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -159,3 +159,5 @@ cython_debug/
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
junit/

.vscode
81 changes: 81 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ Options:
--resolve / --no-resolve [Experimental] Resolve $ref pointers.
[default: no-resolve]
--debug / --no-debug Enable debug output. [default: no-debug]
--examples-format [text|yaml|json]
Format of the examples in the output.
[default: text]
--version Show the version and exit.
--help Show this message and exit.

Expand Down Expand Up @@ -90,8 +93,86 @@ this project does not currently support all features, but it should support:
- Integers with minimum, maximum values and exclusives
- Boolean values
- Deprecated fields (using the `deprecated` option, additionaly searches for case-insensitive `deprecated` in the field description)
- Supports optional YAML and JSON formatting for examples

## Caveats
- This project is still in early development, and the output may change in the future.
- Custom definitions are expected to be in the same file as the schema that uses them,
in the `definitions` or `$defs` parameter at the root of the document.
- Inline nested definitions are not represented in the output yet. See #18.

---

## Examples

### Example 1 Input

Given the following JSON Schema:
```json
{
"$id": "https://example.com/movie.schema.json",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"description": "A representation of a movie",
"type": "object",
"required": ["title", "director", "releaseDate"],
"properties": {
"title": {
"type": "string"
},
"director": {
"type": "string"
},
"releaseDate": {
"type": "string",
"format": "date"
},
"genre": {
"type": "string",
"enum": ["Action", "Comedy", "Drama", "Science Fiction"]
},
"duration": {
"type": "string"
},
"cast": {
"type": "array",
"items": {
"type": "string"
},
"additionalItems": false
}
}
}
```

### Example 1 Ouput
The following markdown will be generated:

---

# jsonschema-markdown

A representation of a movie

### Type: `object`

| Property | Type | Required | Possible values | Deprecated | Default | Description | Examples |
| -------- | ---- | -------- | --------------- | ---------- | ------- | ----------- | -------- |
| title | `string` || string | | | | |
| director | `string` || string | | | | |
| releaseDate | `string` || Format: [`date`](https://json-schema.org/understanding-json-schema/reference/string#built-in-formats) | | | | |
| genre | `string` | | `Action` `Comedy` `Drama` `Science Fiction` | | | | |
| duration | `string` | | string | | | | |
| cast | `array` | | string | | | | |


---

Markdown generated with [jsonschema-markdown](https://github.com/elisiariocouto/jsonschema-markdown).

---

### Example 2

In [tests/model.py](tests/model.py) you can see a more complex example of a model that is exported as a JSON Schema.

The output can be seen in [tests/model.md](tests/model.md).
40 changes: 37 additions & 3 deletions jsonschema_markdown/converter/markdown.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import contextlib
import json
import yaml
import sys
import urllib.parse

Expand All @@ -19,8 +20,34 @@ def _should_include_column(column_values):
return any(value for value in column_values)


def _format_example(example, examples_format):
"""
Format the example based on the examples_format. Only works for dict.
"""

try:
if examples_format == "yaml":
if isinstance(example, dict):
return f"```yaml\n{yaml.dump(example)}\n```"
else:
return f"```yaml\n{example}\n```"
elif examples_format == "json":
if isinstance(example, dict):
return f"```json\n{json.dumps(example, indent=2)}\n```"
else:
return f"```json\n{example}\n```"
except Exception:
logger.debug(f"Was not able to serialize: {example}")

return f"```\n{example}\n```"


def _get_schema_header(
schema: dict, ref_key: str, description_fallback: str, nested: bool = False
schema: dict,
ref_key: str,
description_fallback: str,
nested: bool = False,
examples_format: str = "text",
) -> str:
"""
Get the title and description of the schema.
Expand All @@ -43,7 +70,8 @@ def _get_schema_header(
if examples:
md += f"{prefix}### Examples\n\n"
for example in examples:
md += f"```\n{example}\n```\n\n"
md += _format_example(example, examples_format)
md += "\n\n"

md += f"{prefix}### Type: `{schema.get('type', 'object(?)').strip()}`\n\n"

Expand All @@ -57,6 +85,7 @@ def generate(
replace_refs: bool = False,
debug: bool = False,
hide_empty_columns: bool = False,
examples_format: str = "text",
) -> str:
"""
Generate a markdown string from a given JSON schema.
Expand Down Expand Up @@ -92,6 +121,7 @@ def generate(
_schema,
title,
"JSON Schema missing a description, provide it using the `description` key in the root of the JSON document.",
examples_format=examples_format,
)

defs = _schema.get("definitions", _schema.get("$defs", {}))
Expand All @@ -103,7 +133,11 @@ def generate(
markdown += "\n---\n\n# Definitions\n\n"
for key, definition in defs.items():
markdown += _get_schema_header(
definition, key, "No description provided for this model.", nested=True
definition,
key,
"No description provided for this model.",
nested=True,
examples_format=examples_format,
)
markdown += _create_definition_table(
definition, defs, hide_empty_columns=hide_empty_columns
Expand Down
10 changes: 9 additions & 1 deletion jsonschema_markdown/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,15 @@
show_default=True,
help="Enable debug output.",
)
@click.option(
"--examples-format",
type=click.Choice(["text", "yaml", "json"], case_sensitive=False),
default="text",
show_default=True,
help="Format of the examples in the output.",
)
@click.version_option(package_name="jsonschema_markdown")
def cli(filename, title, footer, empty_columns, resolve, debug):
def cli(filename, title, footer, empty_columns, resolve, debug, examples_format):
"""
Load FILENAME and output a markdown version.
Expand All @@ -56,6 +63,7 @@ def cli(filename, title, footer, empty_columns, resolve, debug):
"replace_refs": resolve,
"debug": debug,
"hide_empty_columns": not empty_columns,
"examples_format": examples_format,
}

if title:
Expand Down
19 changes: 19 additions & 0 deletions tests/test_generate_examples_format.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import pytest

from jsonschema_markdown.converter.markdown import _format_example


@pytest.mark.parametrize(
"example, format_type, expected_md",
[
({"key": "value"}, "yaml", "```yaml\nkey: value\n\n```"),
("example string", "yaml", "```yaml\nexample string\n```"),
({"key": "value"}, "json", '```json\n{\n "key": "value"\n}\n```'),
("example string", "json", "```json\nexample string\n```"),
({"key": "value"}, "text", "```\n{'key': 'value'}\n```"),
({"key": "value"}, "invalid_format", "```\n{'key': 'value'}\n```"),
],
)
def test_generate_examples_format(example, format_type, expected_md):
formatted = _format_example(example, format_type)
assert formatted == expected_md

0 comments on commit bde2e0a

Please sign in to comment.