mirror of
https://github.com/LGUG2Z/komorebi.git
synced 2026-11-13 12:23:38 +01:00
merge: the history of schemars-docgen
This commit is contained in:
commit
84b2c3d04b
9 files changed
+6822
No files matched your search
@@ -0,0 +1,22 @@
|
||||
[package]
|
||||
name = "schemars-docgen"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
license-file = "LICENSE.md"
|
||||
|
||||
[lib]
|
||||
name = "schemars_docgen"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "schemars-docgen"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
schemars = "1"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
tera = "1"
|
||||
pulldown-cmark = "0.13"
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
thiserror = "2"
|
||||
@@ -0,0 +1,826 @@
|
||||
# Comprehensive Analysis: Rust Enum Representations in JSONSchema
|
||||
|
||||
## Overview
|
||||
|
||||
This document provides an exhaustive analysis of all the different ways Rust enums in the `komorebi-for-mac` project are represented in the generated schema files (`schema.json` and `schema.bar.json`). The project uses `schemars` with the `JsonSchema` derive macro for schema generation.
|
||||
|
||||
---
|
||||
|
||||
## Pattern 1: Simple Unit Enums (String Constants)
|
||||
|
||||
**Rust Pattern:** Enums where all variants have no associated data.
|
||||
|
||||
**Serde Attributes:** None (default) or `#[serde(rename_all = "...")]`
|
||||
|
||||
**JSONSchema Representation:** Uses `oneOf` with each variant as `{ "type": "string", "const": "VariantName" }`
|
||||
|
||||
**Source Example:** `ApplicationIdentifier` in `komorebi/src/core/mod.rs`
|
||||
```rust
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub enum ApplicationIdentifier {
|
||||
/// Executable name
|
||||
Exe,
|
||||
/// Title
|
||||
Title,
|
||||
/// Path
|
||||
Path,
|
||||
/// App Bundle name
|
||||
Name,
|
||||
}
|
||||
```
|
||||
|
||||
**Schema Output:**
|
||||
```json
|
||||
{
|
||||
"description": "Application identifier",
|
||||
"oneOf": [
|
||||
{ "description": "Executable name", "type": "string", "const": "Exe" },
|
||||
{ "description": "Title", "type": "string", "const": "Title" },
|
||||
{ "description": "Path", "type": "string", "const": "Path" },
|
||||
{ "description": "App Bundle name", "type": "string", "const": "Name" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Title/Discriminant Location:**
|
||||
| Field | JSON Path | Description |
|
||||
|-------|-----------|-------------|
|
||||
| Variant Name | `oneOf[n].const` | The variant name as a string constant |
|
||||
| Variant Description | `oneOf[n].description` | Doc comment from the variant |
|
||||
| Enum Description | `description` | Doc comment from the enum |
|
||||
|
||||
**Examples in schema.json:**
|
||||
| Definition | Lines |
|
||||
|------------|-------|
|
||||
| `ApplicationIdentifier` | L570-594 |
|
||||
| `Axis` | L622-641 |
|
||||
| `CrossBoundaryBehaviour` | L2410-2424 |
|
||||
| `FloatingLayerBehaviour` | L2476-2490 |
|
||||
| `MatchingStrategy` | L2898-2952 |
|
||||
| `MoveBehaviour` | L3050-3069 |
|
||||
| `OperationBehaviour` | L3070-3084 |
|
||||
| `Placement` | L3141-3160 |
|
||||
| `PredefinedAspectRatio` | L3161-3180 |
|
||||
| `ThemeVariant` | L3366-3380 |
|
||||
| `WindowContainerBehaviour` | L3413-3427 |
|
||||
| `WindowHidingPosition` | L3428-3442 |
|
||||
| `Catppuccin` | L2237-2261 |
|
||||
| `CatppuccinValue` | L2262-2396 |
|
||||
| `Base16Value` | L2080-2164 |
|
||||
| `Base16` | L642-1991 (270 theme variants) |
|
||||
|
||||
**Examples in schema.bar.json:**
|
||||
| Definition | Lines |
|
||||
|------------|-------|
|
||||
| `ApplicationsDisplayFormat` | L268-287 |
|
||||
| `DisplayFormat` | L2241-2270 |
|
||||
| `GroupingStyle` | L2441-2480 |
|
||||
| `LabelPrefix` | L2958-2982 |
|
||||
|
||||
---
|
||||
|
||||
## Pattern 2: Unit Enum with Default Value
|
||||
|
||||
**Rust Pattern:** Unit enum with `#[schemars(extend("default" = Self::Variant))]`
|
||||
|
||||
**JSONSchema Representation:** Same as Pattern 1, but with an additional `"default"` field at the enum level.
|
||||
|
||||
**Source Example:** `DefaultLayout` in `komorebi/src/core/default_layout.rs`
|
||||
```rust
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, PartialEq)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[cfg_attr(feature = "schemars", schemars(extend("default" = Self::BSP)))]
|
||||
pub enum DefaultLayout {
|
||||
/// BSP Layout
|
||||
BSP,
|
||||
/// Columns Layout
|
||||
Columns,
|
||||
// ... more variants
|
||||
}
|
||||
```
|
||||
|
||||
**Schema Output:**
|
||||
```json
|
||||
{
|
||||
"description": "A predefined komorebi layout",
|
||||
"default": "BSP",
|
||||
"oneOf": [
|
||||
{ "description": "BSP Layout", "type": "string", "const": "BSP" },
|
||||
{ "description": "Columns Layout", "type": "string", "const": "Columns" },
|
||||
...
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Title/Discriminant Location:**
|
||||
| Field | JSON Path | Description |
|
||||
|-------|-----------|-------------|
|
||||
| Variant Name | `oneOf[n].const` | The variant name as a string constant |
|
||||
| Default Value | `default` | The default variant name (at enum root level) |
|
||||
| Variant Description | `oneOf[n].description` | Doc comment from the variant |
|
||||
|
||||
**Examples:**
|
||||
| Schema | Definition | Lines |
|
||||
|--------|------------|-------|
|
||||
| schema.json | `DefaultLayout` | L2425-2475 |
|
||||
| schema.bar.json | `DefaultLayout` | L2190-2240 |
|
||||
|
||||
---
|
||||
|
||||
## Pattern 3: Mixed Enum (Unit Variants + Newtype Variants)
|
||||
|
||||
**Rust Pattern:** Enum with mostly unit variants plus one or more tuple/newtype variants containing data. Uses default external tagging.
|
||||
|
||||
**JSONSchema Representation:** Uses `oneOf` where:
|
||||
- Unit variants become `{ "type": "string", "const": "Name" }`
|
||||
- Newtype variants become objects with the variant name as a property key
|
||||
|
||||
**Source Example:** `DateFormat` in `komorebi-bar/src/widgets/date.rs`
|
||||
```rust
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub enum DateFormat {
|
||||
/// Month/Date/Year format (09/08/24)
|
||||
MonthDateYear,
|
||||
/// Year-Month-Date format (2024-09-08)
|
||||
YearMonthDate,
|
||||
/// Custom format
|
||||
#[cfg_attr(feature = "schemars", schemars(title = "Custom"))]
|
||||
Custom(String),
|
||||
/// Custom format with modifiers
|
||||
#[cfg_attr(feature = "schemars", schemars(title = "CustomModifiers"))]
|
||||
CustomModifiers(CustomModifiers),
|
||||
}
|
||||
```
|
||||
|
||||
**Schema Output:**
|
||||
```json
|
||||
{
|
||||
"description": "Date widget format",
|
||||
"oneOf": [
|
||||
{ "description": "Month/Date/Year format", "type": "string", "const": "MonthDateYear" },
|
||||
{ "description": "Year-Month-Date format", "type": "string", "const": "YearMonthDate" },
|
||||
{
|
||||
"title": "Custom",
|
||||
"description": "Custom format",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Custom": { "type": "string" }
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": ["Custom"]
|
||||
},
|
||||
{
|
||||
"title": "CustomModifiers",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"CustomModifiers": { "$ref": "#/$defs/CustomModifiers" }
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": ["CustomModifiers"]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Title/Discriminant Location:**
|
||||
| Variant Type | Field | JSON Path | Description |
|
||||
|--------------|-------|-----------|-------------|
|
||||
| Unit | Variant Name | `oneOf[n].const` | The variant name as a string constant |
|
||||
| Unit | Description | `oneOf[n].description` | Doc comment from the variant |
|
||||
| Newtype | Title | `oneOf[n].title` | Variant name (from `#[schemars(title = "...")]`) |
|
||||
| Newtype | Property Key | `oneOf[n].properties.<VariantName>` | Variant name as object property key |
|
||||
| Newtype | Required | `oneOf[n].required[0]` | Variant name in required array |
|
||||
|
||||
**Examples in schema.bar.json:**
|
||||
| Definition | Lines |
|
||||
|------------|-------|
|
||||
| `DateFormat` | L2137-2189 |
|
||||
| `TimeFormat` | L3442-3490 |
|
||||
| `KomorebiLayout` | L2794-2832 |
|
||||
|
||||
**Examples in schema.json:**
|
||||
| Definition | Lines | Notes |
|
||||
|------------|-------|-------|
|
||||
| `AnimationStyle` | L312-505 | 31 unit variants + `CubicBezier(f64,f64,f64,f64)` - **Note: Has custom Serialize/Deserialize that serializes CubicBezier as bare array, but schema shows wrapped object** |
|
||||
|
||||
---
|
||||
|
||||
## Pattern 4: Untagged Enum (anyOf)
|
||||
|
||||
**Rust Pattern:** Enum with `#[serde(untagged)]` attribute.
|
||||
|
||||
**JSONSchema Representation:** Uses `anyOf` (not `oneOf`) with each variant's type directly, without wrapper objects or discriminator fields.
|
||||
|
||||
### Subpattern 4a: Untagged with Primitives
|
||||
|
||||
**Source Example:** `MonitorConfigOrIndex` in `komorebi-bar/src/config.rs`
|
||||
```rust
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[serde(untagged)]
|
||||
pub enum MonitorConfigOrIndex {
|
||||
/// The monitor index
|
||||
Index(usize),
|
||||
/// The full monitor config
|
||||
MonitorConfig(MonitorConfig),
|
||||
}
|
||||
```
|
||||
|
||||
**Schema Output:**
|
||||
```json
|
||||
{
|
||||
"description": "Monitor configuration or monitor index",
|
||||
"anyOf": [
|
||||
{ "description": "The monitor index", "type": "integer", "format": "uint", "minimum": 0 },
|
||||
{ "description": "The full monitor config", "$ref": "#/$defs/MonitorConfig" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Subpattern 4b: Untagged with Multiple Struct Refs
|
||||
|
||||
**Source Example:** `SpacingKind` in `komorebi-bar/src/config.rs`
|
||||
```rust
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[serde(untagged)]
|
||||
// WARNING: Order matters! Grouped must come last
|
||||
pub enum SpacingKind {
|
||||
/// Spacing applied to all sides
|
||||
All(f32),
|
||||
/// Individual spacing
|
||||
Individual(IndividualSpacingConfig),
|
||||
/// Grouped spacing
|
||||
Grouped(GroupedSpacingConfig),
|
||||
}
|
||||
```
|
||||
|
||||
**Schema Output:**
|
||||
```json
|
||||
{
|
||||
"description": "Spacing kind",
|
||||
"anyOf": [
|
||||
{ "description": "Spacing applied to all sides", "type": "number", "format": "float" },
|
||||
{ "description": "Individual spacing", "$ref": "#/$defs/IndividualSpacingConfig" },
|
||||
{ "description": "Grouped spacing", "$ref": "#/$defs/GroupedSpacingConfig" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Subpattern 4c: Untagged with Two $refs
|
||||
|
||||
**Source Example:** `Colour` in `komorebi/src/core/colour.rs`
|
||||
```rust
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[serde(untagged)]
|
||||
pub enum Colour {
|
||||
/// RGB representation
|
||||
Rgb(Rgb),
|
||||
/// Hex representation
|
||||
Hex(Hex),
|
||||
}
|
||||
```
|
||||
|
||||
**Schema Output:**
|
||||
```json
|
||||
{
|
||||
"description": "Colour representation",
|
||||
"anyOf": [
|
||||
{ "description": "Colour represented as RGB", "$ref": "#/$defs/Rgb" },
|
||||
{ "description": "Colour represented as Hex", "$ref": "#/$defs/Hex" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Subpattern 4d: Untagged with Single vs Array
|
||||
|
||||
**Source Example:** `MatchingRule` in `komorebi/src/core/config_generation.rs`
|
||||
```rust
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[serde(untagged)]
|
||||
pub enum MatchingRule {
|
||||
/// Simple matching rule
|
||||
Simple(IdWithIdentifier),
|
||||
/// Composite matching rule
|
||||
Composite(Vec<IdWithIdentifier>),
|
||||
}
|
||||
```
|
||||
|
||||
**Schema Output:**
|
||||
```json
|
||||
{
|
||||
"description": "Rule for matching applications",
|
||||
"anyOf": [
|
||||
{ "description": "Simple matching rule", "$ref": "#/$defs/IdWithIdentifier" },
|
||||
{ "description": "Composite matching rule", "type": "array", "items": { "$ref": "#/$defs/IdWithIdentifier" } }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Subpattern 4e: Untagged with Tuple
|
||||
|
||||
**Source Example:** `GroupedSpacingOptions` in `komorebi-bar/src/config.rs`
|
||||
```rust
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[serde(untagged)]
|
||||
pub enum GroupedSpacingOptions {
|
||||
/// Symmetrical spacing
|
||||
Symmetrical(f32),
|
||||
/// Split spacing (top/left, bottom/right)
|
||||
Split(f32, f32),
|
||||
}
|
||||
```
|
||||
|
||||
**Schema Output:**
|
||||
```json
|
||||
{
|
||||
"description": "Grouped spacing options",
|
||||
"anyOf": [
|
||||
{ "description": "Symmetrical spacing", "type": "number", "format": "float" },
|
||||
{
|
||||
"description": "Split spacing",
|
||||
"type": "array",
|
||||
"maxItems": 2,
|
||||
"minItems": 2,
|
||||
"prefixItems": [
|
||||
{ "type": "number", "format": "float" },
|
||||
{ "type": "number", "format": "float" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Title/Discriminant Location:**
|
||||
| Field | JSON Path | Description |
|
||||
|-------|-----------|-------------|
|
||||
| Variant Description | `anyOf[n].description` | Doc comment from the variant |
|
||||
| Variant Title (optional) | `anyOf[n].title` | Title if `#[schemars(title = "...")]` is used |
|
||||
| **No discriminant** | N/A | Untagged enums have no discriminant field - they rely on type matching |
|
||||
|
||||
**Note:** Untagged enums use `anyOf` (not `oneOf`) and do not have explicit discriminant values. The JSON parser tries each variant in order until one matches the input structure.
|
||||
|
||||
**All Untagged Enum Examples:**
|
||||
| Schema | Definition | Lines | Subpattern |
|
||||
|--------|------------|-------|------------|
|
||||
| schema.json | `Colour` | L2397-2409 | 4c |
|
||||
| schema.json | `MatchingRule` | L2882-2897 | 4d |
|
||||
| schema.json | `AspectRatio` | L595-621 | 4a (ref + tuple) |
|
||||
| schema.json | `AppSpecificConfigurationPath` | L554-569 | 4d (ref + array) |
|
||||
| schema.bar.json | `MonitorConfigOrIndex` | L3053-3067 | 4a |
|
||||
| schema.bar.json | `SpacingKind` | L3310-3327 | 4b |
|
||||
| schema.bar.json | `GroupedSpacingOptions` | L2312-2337 | 4e |
|
||||
| schema.bar.json | `RoundingConfig` | L3290-3309 | 4e |
|
||||
|
||||
---
|
||||
|
||||
## Pattern 5: Internally Tagged Enum (Struct Variants with Discriminator)
|
||||
|
||||
**Rust Pattern:** Enum with `#[serde(tag = "field_name")]` where variants have named fields (struct variants).
|
||||
|
||||
**JSONSchema Representation:** Uses `oneOf` where each variant is an object with:
|
||||
- A discriminator field with `{ "type": "string", "const": "VariantName" }`
|
||||
- All other fields from the struct variant
|
||||
- The discriminator field in `required` array
|
||||
|
||||
**Source Example:** `KomorebiTheme` in `komorebi/src/static_config.rs`
|
||||
```rust
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[serde(tag = "palette")]
|
||||
pub enum KomorebiTheme {
|
||||
/// A theme from catppuccin-egui
|
||||
#[cfg_attr(feature = "schemars", schemars(title = "Catppuccin"))]
|
||||
Catppuccin {
|
||||
name: komorebi_themes::Catppuccin,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
single_border: Option<komorebi_themes::CatppuccinValue>,
|
||||
// ... more fields
|
||||
},
|
||||
/// A theme from base16-egui-themes
|
||||
#[cfg_attr(feature = "schemars", schemars(title = "Base16"))]
|
||||
Base16 {
|
||||
name: komorebi_themes::Base16,
|
||||
// ... fields
|
||||
},
|
||||
/// A custom Base16 theme
|
||||
#[cfg_attr(feature = "schemars", schemars(title = "Custom"))]
|
||||
Custom {
|
||||
colours: Base16ColourPalette,
|
||||
// ... fields
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
**Schema Output:**
|
||||
```json
|
||||
{
|
||||
"description": "Komorebi theme",
|
||||
"oneOf": [
|
||||
{
|
||||
"title": "Catppuccin",
|
||||
"description": "A theme from catppuccin-egui",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"palette": { "type": "string", "const": "Catppuccin" },
|
||||
"name": { "$ref": "#/$defs/Catppuccin" },
|
||||
"single_border": { "anyOf": [{ "$ref": "#/$defs/CatppuccinValue" }, { "type": "null" }], "default": "Blue" }
|
||||
},
|
||||
"required": ["palette", "name"]
|
||||
},
|
||||
{
|
||||
"title": "Base16",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"palette": { "type": "string", "const": "Base16" },
|
||||
"name": { "$ref": "#/$defs/Base16" }
|
||||
},
|
||||
"required": ["palette", "name"]
|
||||
},
|
||||
{
|
||||
"title": "Custom",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"palette": { "type": "string", "const": "Custom" },
|
||||
"colours": { "$ref": "#/$defs/Base16ColourPalette" }
|
||||
},
|
||||
"required": ["palette", "colours"]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Title/Discriminant Location:**
|
||||
| Field | JSON Path | Description |
|
||||
|-------|-----------|-------------|
|
||||
| Variant Title | `oneOf[n].title` | Variant name (from `#[schemars(title = "...")]`) |
|
||||
| **Discriminant Field** | `oneOf[n].properties.<tag_name>` | The tag field (e.g., `palette`) as a property |
|
||||
| **Discriminant Value** | `oneOf[n].properties.<tag_name>.const` | The variant name as const value |
|
||||
| Required Fields | `oneOf[n].required` | Contains the tag field name |
|
||||
|
||||
**Note:** The discriminant field name comes from `#[serde(tag = "...")]`. In this case, `palette` is the tag field, and each variant has `properties.palette.const` set to its variant name.
|
||||
|
||||
**Examples:**
|
||||
| Schema | Definition | Discriminator | Lines |
|
||||
|--------|------------|---------------|-------|
|
||||
| schema.json | `KomorebiTheme` | `palette` | L2540-2853 |
|
||||
| schema.bar.json | `KomobarTheme` | `palette` | L2518-2669 |
|
||||
|
||||
---
|
||||
|
||||
## Pattern 6: Internally Tagged Enum with Flattened Newtype Variants
|
||||
|
||||
**Rust Pattern:** Enum with `#[serde(tag = "kind")]` where some variants wrap a struct (newtype pattern). The wrapped struct's fields get flattened.
|
||||
|
||||
**JSONSchema Representation:** Uses `oneOf` where:
|
||||
- Each variant has a discriminator field (`kind`)
|
||||
- Variants that wrap structs include `$ref` at the same level to merge properties
|
||||
|
||||
**Source Example:** `Grouping` in `komorebi-bar/src/render.rs`
|
||||
```rust
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[serde(tag = "kind")]
|
||||
pub enum Grouping {
|
||||
/// No grouping
|
||||
#[cfg_attr(feature = "schemars", schemars(title = "None"))]
|
||||
None,
|
||||
/// Grouped as a whole
|
||||
#[cfg_attr(feature = "schemars", schemars(title = "Bar"))]
|
||||
Bar(GroupingConfig),
|
||||
/// Grouped by alignment
|
||||
#[cfg_attr(feature = "schemars", schemars(title = "Alignment"))]
|
||||
Alignment(GroupingConfig),
|
||||
/// Grouped individually
|
||||
#[cfg_attr(feature = "schemars", schemars(title = "Widget"))]
|
||||
Widget(GroupingConfig),
|
||||
}
|
||||
```
|
||||
|
||||
**Schema Output:**
|
||||
```json
|
||||
{
|
||||
"description": "Grouping",
|
||||
"oneOf": [
|
||||
{
|
||||
"title": "None",
|
||||
"description": "No grouping is applied",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"kind": { "type": "string", "const": "None" }
|
||||
},
|
||||
"required": ["kind"]
|
||||
},
|
||||
{
|
||||
"title": "Bar",
|
||||
"description": "Widgets are grouped as a whole",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"kind": { "type": "string", "const": "Bar" }
|
||||
},
|
||||
"$ref": "#/$defs/GroupingConfig",
|
||||
"required": ["kind"]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Title/Discriminant Location:**
|
||||
| Field | JSON Path | Description |
|
||||
|-------|-----------|-------------|
|
||||
| Variant Title | `oneOf[n].title` | Variant name (from `#[schemars(title = "...")]`) |
|
||||
| **Discriminant Field** | `oneOf[n].properties.<tag_name>` | The tag field (e.g., `kind`) as a property |
|
||||
| **Discriminant Value** | `oneOf[n].properties.<tag_name>.const` | The variant name as const value |
|
||||
| Flattened Properties | `oneOf[n].$ref` | Reference to merged struct (only for newtype variants) |
|
||||
| Required Fields | `oneOf[n].required` | Contains the tag field name |
|
||||
|
||||
**Note:** Similar to Pattern 5, but newtype variants include a `$ref` at the variant level that merges in additional properties from the wrapped struct. The `$ref` appears alongside `properties` and `required`.
|
||||
|
||||
**Examples:**
|
||||
| Schema | Definition | Discriminator | Lines |
|
||||
|--------|------------|---------------|-------|
|
||||
| schema.bar.json | `Grouping` | `kind` | L2338-2401 |
|
||||
|
||||
---
|
||||
|
||||
## Pattern 7: Externally Tagged Enum (Newtype Variants Only)
|
||||
|
||||
**Rust Pattern:** Enum with only newtype variants (no unit variants), using default external tagging.
|
||||
|
||||
**JSONSchema Representation:** Uses `oneOf` where each variant is an object with:
|
||||
- The variant name as a property key
|
||||
- The wrapped type as the property value (usually a `$ref`)
|
||||
- `additionalProperties: false`
|
||||
- The variant name in `required` array
|
||||
|
||||
**Source Example:** `WidgetConfig` in `komorebi-bar/src/widgets/widget.rs`
|
||||
```rust
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub enum WidgetConfig {
|
||||
/// Applications widget
|
||||
#[cfg_attr(feature = "schemars", schemars(title = "Applications"))]
|
||||
Applications(ApplicationsConfig),
|
||||
/// Battery widget
|
||||
#[cfg_attr(feature = "schemars", schemars(title = "Battery"))]
|
||||
Battery(BatteryConfig),
|
||||
// ... 8 more variants
|
||||
}
|
||||
```
|
||||
|
||||
**Schema Output:**
|
||||
```json
|
||||
{
|
||||
"description": "Widget configuration",
|
||||
"oneOf": [
|
||||
{
|
||||
"title": "Applications",
|
||||
"description": "Applications widget configuration",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Applications": { "$ref": "#/$defs/ApplicationsConfig" }
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": ["Applications"]
|
||||
},
|
||||
{
|
||||
"title": "Battery",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Battery": { "$ref": "#/$defs/BatteryConfig" }
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": ["Battery"]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Title/Discriminant Location:**
|
||||
| Field | JSON Path | Description |
|
||||
|-------|-----------|-------------|
|
||||
| Variant Title | `oneOf[n].title` | Variant name (from `#[schemars(title = "...")]`) |
|
||||
| **Discriminant (Property Key)** | `oneOf[n].properties.<VariantName>` | Variant name is the property key itself |
|
||||
| **Discriminant (Required)** | `oneOf[n].required[0]` | Variant name in required array |
|
||||
| Wrapped Type | `oneOf[n].properties.<VariantName>.$ref` | Reference to the wrapped struct |
|
||||
|
||||
**Note:** In externally tagged enums, the variant name serves as both the property key and the discriminant. There is no separate `const` field - the presence of the property key identifies the variant.
|
||||
|
||||
**Examples:**
|
||||
| Schema | Definition | Lines |
|
||||
|--------|------------|-------|
|
||||
| schema.bar.json | `WidgetConfig` | L3525-3669 |
|
||||
|
||||
---
|
||||
|
||||
## Pattern 8: Adjacently Tagged Enum
|
||||
|
||||
**Rust Pattern:** Enum with `#[serde(tag = "type", content = "content")]` - tag and content in separate fields.
|
||||
|
||||
**JSONSchema Representation:** This pattern is used for internal message types but may not appear in the public config schemas.
|
||||
|
||||
**Source Example:** `SocketMessage` in `komorebi/src/core/mod.rs`
|
||||
```rust
|
||||
#[serde_with::serde_as]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, Display)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[serde(tag = "type", content = "content")]
|
||||
pub enum SocketMessage {
|
||||
FocusWindow(OperationDirection),
|
||||
MoveWindow(OperationDirection),
|
||||
CancelPreselect,
|
||||
// ... many more variants
|
||||
}
|
||||
```
|
||||
|
||||
**Title/Discriminant Location:**
|
||||
| Field | JSON Path | Description |
|
||||
|-------|-----------|-------------|
|
||||
| **Tag Field** | `oneOf[n].properties.<tag_name>` | The tag field (e.g., `type`) |
|
||||
| **Tag Value** | `oneOf[n].properties.<tag_name>.const` | The variant name as const value |
|
||||
| **Content Field** | `oneOf[n].properties.<content_name>` | The content field (e.g., `content`) |
|
||||
| Content Value | `oneOf[n].properties.<content_name>.*` | The variant's data |
|
||||
|
||||
**Note:** `SocketMessage`, `WindowManagerEvent`, and `SystemNotification` use this pattern but are primarily for IPC, not user configuration. They may not appear in the public schema files.
|
||||
|
||||
---
|
||||
|
||||
## Pattern 9: Untagged Hybrid Enum (Unit Variants + $ref Fallback)
|
||||
|
||||
**Rust Pattern:** Macro-generated enum using `extend_enum!` with `#[serde(untagged)]` on the fallback variant.
|
||||
|
||||
**Source Example:** `WorkspacesDisplayFormat` in `komorebi-bar/src/config.rs`
|
||||
```rust
|
||||
extend_enum!(
|
||||
DisplayFormat, WorkspacesDisplayFormat, {
|
||||
/// Show all icons only
|
||||
AllIcons,
|
||||
/// Show both all icons and text
|
||||
AllIconsAndText,
|
||||
/// Show all icons and text for the selected element
|
||||
AllIconsAndTextOnSelected,
|
||||
});
|
||||
```
|
||||
|
||||
Which expands to include an `#[serde(untagged)] Existing(DisplayFormat)` variant.
|
||||
|
||||
**Schema Output:**
|
||||
```json
|
||||
{
|
||||
"description": "Workspaces display format",
|
||||
"anyOf": [
|
||||
{ "description": "Show all icons only", "type": "string", "const": "AllIcons" },
|
||||
{ "description": "Show both all icons and text", "type": "string", "const": "AllIconsAndText" },
|
||||
{ "description": "Show all icons and text for the selected element", "type": "string", "const": "AllIconsAndTextOnSelected" },
|
||||
{ "$ref": "#/$defs/DisplayFormat" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Title/Discriminant Location:**
|
||||
| Variant Type | Field | JSON Path | Description |
|
||||
|--------------|-------|-----------|-------------|
|
||||
| Unit | Variant Name | `anyOf[n].const` | The variant name as a string constant |
|
||||
| Unit | Description | `anyOf[n].description` | Doc comment from the variant |
|
||||
| Fallback | Reference | `anyOf[n].$ref` | Reference to the base enum for additional variants |
|
||||
|
||||
**Note:** This hybrid pattern combines explicit unit variants with a `$ref` fallback to another enum. The `$ref` variant has no `const` - it delegates to the referenced enum's schema.
|
||||
|
||||
**Examples:**
|
||||
| Schema | Definition | Lines |
|
||||
|--------|------------|-------|
|
||||
| schema.bar.json | `WorkspacesDisplayFormat` | L3670-3692 |
|
||||
|
||||
---
|
||||
|
||||
## Pattern 10: Generic Enum Instantiations
|
||||
|
||||
**Rust Pattern:** Generic enum like `PerAnimationPrefixConfig<T>` with `#[serde(untagged)]` instantiated with different type parameters.
|
||||
|
||||
**JSONSchema Representation:** Creates separate definitions for each instantiation, with numbered suffixes.
|
||||
|
||||
**Source Example:** (likely in `komorebi/src/animation/mod.rs`)
|
||||
```rust
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[serde(untagged)]
|
||||
pub enum PerAnimationPrefixConfig<T> {
|
||||
/// Prefixed config
|
||||
Prefix(HashMap<String, T>),
|
||||
/// Global config
|
||||
Global(T),
|
||||
}
|
||||
```
|
||||
|
||||
**Schema Output (for bool):**
|
||||
```json
|
||||
{
|
||||
"description": "Animation configuration",
|
||||
"anyOf": [
|
||||
{
|
||||
"description": "Prefixed config",
|
||||
"type": "object",
|
||||
"additionalProperties": { "type": "boolean" }
|
||||
},
|
||||
{
|
||||
"description": "Global config",
|
||||
"type": "boolean"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Title/Discriminant Location:**
|
||||
| Field | JSON Path | Description |
|
||||
|-------|-----------|-------------|
|
||||
| Variant Description | `anyOf[n].description` | Doc comment from the variant |
|
||||
| **No discriminant** | N/A | Untagged - relies on type matching |
|
||||
| Definition Name | `$defs/<Name>` or `$defs/<Name><N>` | Numbered suffix for each instantiation |
|
||||
|
||||
**Note:** Each generic instantiation creates a separate definition. The first uses the base name, subsequent ones get numeric suffixes (e.g., `PerAnimationPrefixConfig2`, `PerAnimationPrefixConfig3`).
|
||||
|
||||
**Examples in schema.json:**
|
||||
| Definition | Type Parameter | Lines |
|
||||
|------------|---------------|-------|
|
||||
| `PerAnimationPrefixConfig` | `bool` | L3089-3104 |
|
||||
| `PerAnimationPrefixConfig2` | `u64` | L3105-3124 |
|
||||
| `PerAnimationPrefixConfig3` | `AnimationStyle` | L3125-3140 |
|
||||
|
||||
---
|
||||
|
||||
## Summary Table
|
||||
|
||||
| Pattern | Serde Attribute | JSONSchema Keyword | Discriminant Location |
|
||||
|---------|-----------------|-------------------|----------------------|
|
||||
| 1. Simple Unit | (none) | `oneOf` | `oneOf[n].const` |
|
||||
| 2. Unit + Default | `#[schemars(extend)]` | `oneOf` + `default` | `oneOf[n].const` + root `default` |
|
||||
| 3. Mixed (Unit + Newtype) | (none) | `oneOf` | Units: `oneOf[n].const`; Newtypes: `oneOf[n].properties.<Name>` + `oneOf[n].required[0]` |
|
||||
| 4. Untagged | `#[serde(untagged)]` | `anyOf` | None (type-based matching) |
|
||||
| 5. Internally Tagged | `#[serde(tag = "...")]` | `oneOf` | `oneOf[n].properties.<tag>.const` |
|
||||
| 6. Internally Tagged + Newtype | `#[serde(tag = "...")]` | `oneOf` with `$ref` | `oneOf[n].properties.<tag>.const` + `oneOf[n].$ref` |
|
||||
| 7. Externally Tagged (Newtype) | (none/default) | `oneOf` | `oneOf[n].properties.<VariantName>` + `oneOf[n].required[0]` |
|
||||
| 8. Adjacently Tagged | `#[serde(tag, content)]` | `oneOf` | `oneOf[n].properties.<tag>.const` + `oneOf[n].properties.<content>` |
|
||||
| 9. Untagged Hybrid | `#[serde(untagged)]` on fallback | `anyOf` | Units: `anyOf[n].const`; Fallback: `anyOf[n].$ref` |
|
||||
| 10. Generic | `#[serde(untagged)]` | `anyOf` | None; Definition name has numeric suffix |
|
||||
|
||||
### Title Location by Pattern
|
||||
|
||||
| Pattern | Title Location | Notes |
|
||||
|---------|---------------|-------|
|
||||
| 1. Simple Unit | No `title` field | Variant name is in `const` |
|
||||
| 2. Unit + Default | No `title` field | Variant name is in `const` |
|
||||
| 3. Mixed | `oneOf[n].title` (newtypes only) | From `#[schemars(title = "...")]` |
|
||||
| 4. Untagged | `anyOf[n].title` (optional) | Rarely used |
|
||||
| 5. Internally Tagged | `oneOf[n].title` | From `#[schemars(title = "...")]` |
|
||||
| 6. Internally Tagged + Newtype | `oneOf[n].title` | From `#[schemars(title = "...")]` |
|
||||
| 7. Externally Tagged | `oneOf[n].title` | From `#[schemars(title = "...")]` |
|
||||
| 8. Adjacently Tagged | `oneOf[n].title` | From `#[schemars(title = "...")]` |
|
||||
| 9. Untagged Hybrid | No `title` for units | Unit names in `const`, fallback is `$ref` |
|
||||
| 10. Generic | No `title` | Definition name identifies the instantiation |
|
||||
|
||||
---
|
||||
|
||||
## Key Observations
|
||||
|
||||
1. **`oneOf` vs `anyOf`**: Tagged enums use `oneOf` (exactly one must match); untagged enums use `anyOf` (first match wins).
|
||||
|
||||
2. **Unit variants**: Always represented as `{ "type": "string", "const": "VariantName" }`.
|
||||
|
||||
3. **Newtype variants with default tagging**: Become objects where the variant name is a property key containing the data.
|
||||
|
||||
4. **Struct variants with internal tagging**: The tag field appears as a regular property with a `const` value.
|
||||
|
||||
5. **Generic type monomorphization**: Each instantiation gets a separate schema definition with numeric suffixes.
|
||||
|
||||
6. **Descriptions preserved**: Both enum-level and variant-level doc comments appear in the schema.
|
||||
|
||||
7. **`#[schemars(title = "...")]`**: Used to control the `title` field in schema output for variants.
|
||||
|
||||
8. **Custom Serialize/Deserialize**: When enums have custom implementations (like `AnimationStyle`), the schema may not match the actual serialization format exactly.
|
||||
|
||||
9. **Variant ordering in untagged enums**: Order matters! See `SpacingKind` warning comment.
|
||||
|
||||
10. **Optional fields in struct variants**: Use `anyOf: [{ $ref }, { type: null }]` pattern with `default` values.
|
||||
|
||||
---
|
||||
|
||||
## Source Files Reference
|
||||
|
||||
| File | Key Enums |
|
||||
|------|-----------|
|
||||
| `komorebi/src/core/mod.rs` | `ApplicationIdentifier`, `SocketMessage`, `OperationBehaviour`, `WindowContainerBehaviour`, `FloatingLayerBehaviour`, `Placement`, `MoveBehaviour`, `CrossBoundaryBehaviour` |
|
||||
| `komorebi/src/core/config_generation.rs` | `MatchingRule`, `MatchingStrategy`, `ApplicationOptions` |
|
||||
| `komorebi/src/core/default_layout.rs` | `DefaultLayout` |
|
||||
| `komorebi/src/core/animation.rs` | `AnimationStyle` |
|
||||
| `komorebi/src/static_config.rs` | `KomorebiTheme` |
|
||||
| `komorebi/src/animation/mod.rs` | `PerAnimationPrefixConfig<T>` |
|
||||
| `komorebi-bar/src/config.rs` | `MonitorConfigOrIndex`, `SpacingKind`, `GroupedSpacingOptions`, `KomobarTheme`, `WorkspacesDisplayFormat` |
|
||||
| `komorebi-bar/src/render.rs` | `Grouping`, `GroupingStyle`, `RoundingConfig` |
|
||||
| `komorebi-bar/src/widgets/widget.rs` | `WidgetConfig` |
|
||||
| `komorebi-bar/src/widgets/date.rs` | `DateFormat` |
|
||||
| `komorebi-bar/src/widgets/time.rs` | `TimeFormat` |
|
||||
@@ -0,0 +1,480 @@
|
||||
#![warn(clippy::all)]
|
||||
|
||||
//! # schemars-docgen
|
||||
//!
|
||||
//! Generate MDX documentation from `schemars::Schema` objects.
|
||||
//!
|
||||
//! This library takes a JSON Schema (as produced by schemars) and generates
|
||||
//! MDX documentation suitable for Astro Starlight with:
|
||||
//!
|
||||
//! - YAML frontmatter
|
||||
//! - Starlight components
|
||||
//! - Markdown rendering for descriptions
|
||||
//!
|
||||
//! ## Usage
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! use schemars::schema_for;
|
||||
//! use schemars_docgen::{generate_mdx, MdxOptions};
|
||||
//!
|
||||
//! #[derive(schemars::JsonSchema)]
|
||||
//! struct MyConfig {
|
||||
//! /// The name of the configuration
|
||||
//! name: String,
|
||||
//! }
|
||||
//!
|
||||
//! let schema = schema_for!(MyConfig);
|
||||
//! let options = MdxOptions::default();
|
||||
//! let mdx = generate_mdx(&schema, &options).unwrap();
|
||||
//! ```
|
||||
|
||||
mod mdx_generator;
|
||||
mod prepare;
|
||||
|
||||
pub use mdx_generator::MdxGenerator;
|
||||
|
||||
use schemars::Schema;
|
||||
use serde_json::Value;
|
||||
|
||||
/// Options for MDX generation
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct MdxOptions {
|
||||
/// Override the schema title
|
||||
pub title_override: Option<String>,
|
||||
/// Override the schema description (used in frontmatter)
|
||||
pub description_override: Option<String>,
|
||||
}
|
||||
|
||||
/// Error type for documentation generation
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error("Template error: {0}")]
|
||||
Template(#[from] tera::Error),
|
||||
|
||||
#[error("JSON error: {0}")]
|
||||
Json(#[from] serde_json::Error),
|
||||
}
|
||||
|
||||
/// Generate MDX documentation from a schema
|
||||
///
|
||||
/// This generates an MDX document suitable for use with Astro Starlight.
|
||||
/// The output includes YAML frontmatter and uses Starlight components.
|
||||
pub fn generate_mdx(schema: &Schema, options: &MdxOptions) -> Result<String, Error> {
|
||||
let generator = MdxGenerator::new()?;
|
||||
generator.generate(schema, options)
|
||||
}
|
||||
|
||||
/// Extension trait for convenient access to common schema properties
|
||||
///
|
||||
/// Since `schemars::Schema` is a wrapper around `serde_json::Value`,
|
||||
/// this trait provides typed accessors for standard JSON Schema keywords.
|
||||
pub trait SchemaExt {
|
||||
/// Get the schema title
|
||||
fn title(&self) -> Option<&str>;
|
||||
|
||||
/// Get the schema description
|
||||
fn description(&self) -> Option<&str>;
|
||||
|
||||
/// Get the schema type (can be a string or array of strings)
|
||||
fn schema_type(&self) -> Option<&Value>;
|
||||
|
||||
/// Get object properties
|
||||
fn properties(&self) -> Option<&serde_json::Map<String, Value>>;
|
||||
|
||||
/// Get schema definitions ($defs)
|
||||
fn definitions(&self) -> Option<&serde_json::Map<String, Value>>;
|
||||
|
||||
/// Get array items schema
|
||||
fn items(&self) -> Option<&Value>;
|
||||
|
||||
/// Get anyOf schemas
|
||||
fn any_of(&self) -> Option<&Vec<Value>>;
|
||||
|
||||
/// Get oneOf schemas
|
||||
fn one_of(&self) -> Option<&Vec<Value>>;
|
||||
|
||||
/// Get allOf schemas
|
||||
fn all_of(&self) -> Option<&Vec<Value>>;
|
||||
|
||||
/// Get enum values
|
||||
fn enum_values(&self) -> Option<&Vec<Value>>;
|
||||
|
||||
/// Get $ref target
|
||||
fn ref_target(&self) -> Option<&str>;
|
||||
|
||||
/// Get default value
|
||||
fn default_value(&self) -> Option<&Value>;
|
||||
|
||||
/// Get required fields
|
||||
fn required_fields(&self) -> Option<&Vec<Value>>;
|
||||
|
||||
/// Get format constraint
|
||||
fn format(&self) -> Option<&str>;
|
||||
|
||||
/// Get pattern constraint
|
||||
fn pattern(&self) -> Option<&str>;
|
||||
|
||||
/// Get minimum constraint
|
||||
fn minimum(&self) -> Option<f64>;
|
||||
|
||||
/// Get maximum constraint
|
||||
fn maximum(&self) -> Option<f64>;
|
||||
|
||||
/// Get const value
|
||||
fn const_value(&self) -> Option<&Value>;
|
||||
|
||||
/// Get pattern properties
|
||||
fn pattern_properties(&self) -> Option<&serde_json::Map<String, Value>>;
|
||||
|
||||
/// Get additional properties schema
|
||||
fn additional_properties(&self) -> Option<&Value>;
|
||||
|
||||
/// Get prefix items (tuple validation)
|
||||
fn prefix_items(&self) -> Option<&Vec<Value>>;
|
||||
|
||||
/// Get min items constraint
|
||||
fn min_items(&self) -> Option<u64>;
|
||||
|
||||
/// Get max items constraint
|
||||
fn max_items(&self) -> Option<u64>;
|
||||
|
||||
/// Check if deprecated
|
||||
fn is_deprecated(&self) -> bool;
|
||||
}
|
||||
|
||||
impl SchemaExt for Schema {
|
||||
fn title(&self) -> Option<&str> {
|
||||
self.as_object()?.get("title")?.as_str()
|
||||
}
|
||||
|
||||
fn description(&self) -> Option<&str> {
|
||||
self.as_object()?.get("description")?.as_str()
|
||||
}
|
||||
|
||||
fn schema_type(&self) -> Option<&Value> {
|
||||
self.as_object()?.get("type")
|
||||
}
|
||||
|
||||
fn properties(&self) -> Option<&serde_json::Map<String, Value>> {
|
||||
self.as_object()?.get("properties")?.as_object()
|
||||
}
|
||||
|
||||
fn definitions(&self) -> Option<&serde_json::Map<String, Value>> {
|
||||
self.as_object()?.get("$defs")?.as_object()
|
||||
}
|
||||
|
||||
fn items(&self) -> Option<&Value> {
|
||||
self.as_object()?.get("items")
|
||||
}
|
||||
|
||||
fn any_of(&self) -> Option<&Vec<Value>> {
|
||||
self.as_object()?.get("anyOf")?.as_array()
|
||||
}
|
||||
|
||||
fn one_of(&self) -> Option<&Vec<Value>> {
|
||||
self.as_object()?.get("oneOf")?.as_array()
|
||||
}
|
||||
|
||||
fn all_of(&self) -> Option<&Vec<Value>> {
|
||||
self.as_object()?.get("allOf")?.as_array()
|
||||
}
|
||||
|
||||
fn enum_values(&self) -> Option<&Vec<Value>> {
|
||||
self.as_object()?.get("enum")?.as_array()
|
||||
}
|
||||
|
||||
fn ref_target(&self) -> Option<&str> {
|
||||
self.as_object()?.get("$ref")?.as_str()
|
||||
}
|
||||
|
||||
fn default_value(&self) -> Option<&Value> {
|
||||
self.as_object()?.get("default")
|
||||
}
|
||||
|
||||
fn required_fields(&self) -> Option<&Vec<Value>> {
|
||||
self.as_object()?.get("required")?.as_array()
|
||||
}
|
||||
|
||||
fn format(&self) -> Option<&str> {
|
||||
self.as_object()?.get("format")?.as_str()
|
||||
}
|
||||
|
||||
fn pattern(&self) -> Option<&str> {
|
||||
self.as_object()?.get("pattern")?.as_str()
|
||||
}
|
||||
|
||||
fn minimum(&self) -> Option<f64> {
|
||||
self.as_object()?.get("minimum")?.as_f64()
|
||||
}
|
||||
|
||||
fn maximum(&self) -> Option<f64> {
|
||||
self.as_object()?.get("maximum")?.as_f64()
|
||||
}
|
||||
|
||||
fn const_value(&self) -> Option<&Value> {
|
||||
self.as_object()?.get("const")
|
||||
}
|
||||
|
||||
fn pattern_properties(&self) -> Option<&serde_json::Map<String, Value>> {
|
||||
self.as_object()?.get("patternProperties")?.as_object()
|
||||
}
|
||||
|
||||
fn additional_properties(&self) -> Option<&Value> {
|
||||
self.as_object()?.get("additionalProperties")
|
||||
}
|
||||
|
||||
fn prefix_items(&self) -> Option<&Vec<Value>> {
|
||||
self.as_object()?.get("prefixItems")?.as_array()
|
||||
}
|
||||
|
||||
fn min_items(&self) -> Option<u64> {
|
||||
self.as_object()?.get("minItems")?.as_u64()
|
||||
}
|
||||
|
||||
fn max_items(&self) -> Option<u64> {
|
||||
self.as_object()?.get("maxItems")?.as_u64()
|
||||
}
|
||||
|
||||
fn is_deprecated(&self) -> bool {
|
||||
self.as_object()
|
||||
.and_then(|o| o.get("deprecated"))
|
||||
.and_then(|v| v.as_bool())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
}
|
||||
|
||||
/// Extension trait for Value to use the same accessors
|
||||
impl SchemaExt for Value {
|
||||
fn title(&self) -> Option<&str> {
|
||||
self.as_object()?.get("title")?.as_str()
|
||||
}
|
||||
|
||||
fn description(&self) -> Option<&str> {
|
||||
self.as_object()?.get("description")?.as_str()
|
||||
}
|
||||
|
||||
fn schema_type(&self) -> Option<&Value> {
|
||||
self.as_object()?.get("type")
|
||||
}
|
||||
|
||||
fn properties(&self) -> Option<&serde_json::Map<String, Value>> {
|
||||
self.as_object()?.get("properties")?.as_object()
|
||||
}
|
||||
|
||||
fn definitions(&self) -> Option<&serde_json::Map<String, Value>> {
|
||||
self.as_object()?.get("$defs")?.as_object()
|
||||
}
|
||||
|
||||
fn items(&self) -> Option<&Value> {
|
||||
self.as_object()?.get("items")
|
||||
}
|
||||
|
||||
fn any_of(&self) -> Option<&Vec<Value>> {
|
||||
self.as_object()?.get("anyOf")?.as_array()
|
||||
}
|
||||
|
||||
fn one_of(&self) -> Option<&Vec<Value>> {
|
||||
self.as_object()?.get("oneOf")?.as_array()
|
||||
}
|
||||
|
||||
fn all_of(&self) -> Option<&Vec<Value>> {
|
||||
self.as_object()?.get("allOf")?.as_array()
|
||||
}
|
||||
|
||||
fn enum_values(&self) -> Option<&Vec<Value>> {
|
||||
self.as_object()?.get("enum")?.as_array()
|
||||
}
|
||||
|
||||
fn ref_target(&self) -> Option<&str> {
|
||||
self.as_object()?.get("$ref")?.as_str()
|
||||
}
|
||||
|
||||
fn default_value(&self) -> Option<&Value> {
|
||||
self.as_object()?.get("default")
|
||||
}
|
||||
|
||||
fn required_fields(&self) -> Option<&Vec<Value>> {
|
||||
self.as_object()?.get("required")?.as_array()
|
||||
}
|
||||
|
||||
fn format(&self) -> Option<&str> {
|
||||
self.as_object()?.get("format")?.as_str()
|
||||
}
|
||||
|
||||
fn pattern(&self) -> Option<&str> {
|
||||
self.as_object()?.get("pattern")?.as_str()
|
||||
}
|
||||
|
||||
fn minimum(&self) -> Option<f64> {
|
||||
self.as_object()?.get("minimum")?.as_f64()
|
||||
}
|
||||
|
||||
fn maximum(&self) -> Option<f64> {
|
||||
self.as_object()?.get("maximum")?.as_f64()
|
||||
}
|
||||
|
||||
fn const_value(&self) -> Option<&Value> {
|
||||
self.as_object()?.get("const")
|
||||
}
|
||||
|
||||
fn pattern_properties(&self) -> Option<&serde_json::Map<String, Value>> {
|
||||
self.as_object()?.get("patternProperties")?.as_object()
|
||||
}
|
||||
|
||||
fn additional_properties(&self) -> Option<&Value> {
|
||||
self.as_object()?.get("additionalProperties")
|
||||
}
|
||||
|
||||
fn prefix_items(&self) -> Option<&Vec<Value>> {
|
||||
self.as_object()?.get("prefixItems")?.as_array()
|
||||
}
|
||||
|
||||
fn min_items(&self) -> Option<u64> {
|
||||
self.as_object()?.get("minItems")?.as_u64()
|
||||
}
|
||||
|
||||
fn max_items(&self) -> Option<u64> {
|
||||
self.as_object()?.get("maxItems")?.as_u64()
|
||||
}
|
||||
|
||||
fn is_deprecated(&self) -> bool {
|
||||
self.as_object()
|
||||
.and_then(|o| o.get("deprecated"))
|
||||
.and_then(|v| v.as_bool())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_schema_ext_title() {
|
||||
let schema: Schema =
|
||||
serde_json::from_str(r#"{"title": "Test Schema", "type": "object"}"#).unwrap();
|
||||
assert_eq!(schema.title(), Some("Test Schema"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_schema_ext_description() {
|
||||
let schema: Schema =
|
||||
serde_json::from_str(r#"{"description": "A test description"}"#).unwrap();
|
||||
assert_eq!(schema.description(), Some("A test description"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_schema_ext_ref() {
|
||||
let schema: Schema = serde_json::from_str(r##"{"$ref": "#/$defs/MyType"}"##).unwrap();
|
||||
assert_eq!(schema.ref_target(), Some("#/$defs/MyType"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_schema_ext_properties() {
|
||||
let schema: Schema = serde_json::from_str(
|
||||
r#"{"type": "object", "properties": {"name": {"type": "string"}}}"#,
|
||||
)
|
||||
.unwrap();
|
||||
let props = schema.properties().unwrap();
|
||||
assert!(props.contains_key("name"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tuple_with_ref_items() {
|
||||
let schema: Schema = serde_json::from_str(
|
||||
r##"{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"coords": {
|
||||
"type": "array",
|
||||
"prefixItems": [
|
||||
{ "$ref": "#/$defs/X" },
|
||||
{ "$ref": "#/$defs/Y" }
|
||||
]
|
||||
}
|
||||
},
|
||||
"$defs": {
|
||||
"X": { "type": "number" },
|
||||
"Y": { "type": "number" }
|
||||
}
|
||||
}"##,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let result = generate_mdx(&schema, &MdxOptions::default());
|
||||
assert!(result.is_ok(), "Should handle $ref in prefixItems");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tuple_with_mixed_items() {
|
||||
let schema: Schema = serde_json::from_str(
|
||||
r##"{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"pair": {
|
||||
"type": "array",
|
||||
"prefixItems": [
|
||||
{ "$ref": "#/$defs/Name" },
|
||||
{ "type": "integer" }
|
||||
]
|
||||
}
|
||||
},
|
||||
"$defs": {
|
||||
"Name": { "type": "string" }
|
||||
}
|
||||
}"##,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let result = generate_mdx(&schema, &MdxOptions::default());
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"Should handle mixed $ref and type in prefixItems"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tuple_with_anyof_items() {
|
||||
let schema: Schema = serde_json::from_str(
|
||||
r##"{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"optional_pair": {
|
||||
"type": "array",
|
||||
"prefixItems": [
|
||||
{ "type": "string" },
|
||||
{ "anyOf": [{ "type": "number" }, { "type": "null" }] }
|
||||
]
|
||||
}
|
||||
}
|
||||
}"##,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let result = generate_mdx(&schema, &MdxOptions::default());
|
||||
assert!(result.is_ok(), "Should handle anyOf in prefixItems");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_homogeneous_tuple_still_works() {
|
||||
let schema: Schema = serde_json::from_str(
|
||||
r#"{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"point": {
|
||||
"type": "array",
|
||||
"prefixItems": [
|
||||
{ "type": "number" },
|
||||
{ "type": "number" },
|
||||
{ "type": "number" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let result = generate_mdx(&schema, &MdxOptions::default());
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
#![warn(clippy::all)]
|
||||
|
||||
use clap::Parser;
|
||||
use schemars_docgen::MdxOptions;
|
||||
use schemars_docgen::generate_mdx;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "schemars-docgen")]
|
||||
#[command(about = "Generate documentation from JSON Schema")]
|
||||
struct Cli {
|
||||
/// Input JSON Schema file
|
||||
input: PathBuf,
|
||||
|
||||
/// Output file (defaults to input filename with .mdx extension)
|
||||
#[arg(short, long)]
|
||||
output: Option<PathBuf>,
|
||||
|
||||
/// Schema title (overrides title from the schema)
|
||||
#[arg(short, long)]
|
||||
title: Option<String>,
|
||||
|
||||
/// Schema description (overrides description from the schema)
|
||||
#[arg(short, long)]
|
||||
description: Option<String>,
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let cli = Cli::parse();
|
||||
|
||||
let schema_json = std::fs::read_to_string(&cli.input)?;
|
||||
let schema: schemars::Schema = serde_json::from_str(&schema_json)?;
|
||||
|
||||
let options = MdxOptions {
|
||||
title_override: cli.title,
|
||||
description_override: cli.description,
|
||||
};
|
||||
let output = generate_mdx(&schema, &options)?;
|
||||
|
||||
let output_path = cli.output.unwrap_or_else(|| {
|
||||
let mut path = cli.input.clone();
|
||||
path.set_extension("mdx");
|
||||
path
|
||||
});
|
||||
|
||||
std::fs::write(&output_path, output)?;
|
||||
println!("Generated documentation: {}", output_path.display());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,32 @@
|
||||
{%- import "mdx_macros.md" as macros -%}
|
||||
---
|
||||
title: "{{ title }}"
|
||||
description: "{{ description }}"
|
||||
---
|
||||
|
||||
import { Badge } from '@astrojs/starlight/components';
|
||||
|
||||
## Configuration
|
||||
{% if schema.properties %}
|
||||
{# First render required properties #}
|
||||
{% for prop_name, prop in schema.properties -%}
|
||||
{%- if prop_name in required_set -%}
|
||||
{{ macros::render_property(name=prop_name, node=prop, required=true, depth=0, path=prop_name) }}
|
||||
{% endif -%}
|
||||
{%- endfor -%}
|
||||
{# Then render optional properties #}
|
||||
{% for prop_name, prop in schema.properties -%}
|
||||
{%- if prop_name not in required_set -%}
|
||||
{{ macros::render_property(name=prop_name, node=prop, required=false, depth=0, path=prop_name) }}
|
||||
{% endif -%}
|
||||
{%- endfor %}
|
||||
{% else %}
|
||||
*No configuration properties defined.*
|
||||
{% endif %}
|
||||
{% if schema["$defs"] %}
|
||||
## Type Definitions
|
||||
|
||||
{% for def_name, def in schema["$defs"] -%}
|
||||
{{ macros::render_definition(def_name=def_name, def=def) }}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
@@ -0,0 +1,718 @@
|
||||
{# ============================================================================ #}
|
||||
{# MDX Macros for Astro Starlight documentation generation #}
|
||||
{# ============================================================================ #}
|
||||
{#
|
||||
Enum Pattern Reference (see ENUMS.md for full details):
|
||||
- Pattern 1: Simple Unit Enums - oneOf with {type: "string", const: "Value"}
|
||||
- Pattern 2: Unit Enum with Default - Pattern 1 + "default" field
|
||||
- Pattern 3: Mixed Enum - oneOf with unit variants + tuple/object variants
|
||||
- Pattern 4a-d: Untagged Enums - anyOf with various combinations ($ref, tuple, array)
|
||||
- Pattern 5: Internally Tagged - oneOf with discriminator field (const) + properties
|
||||
- Pattern 6: Internally Tagged + Flatten - oneOf with properties + $ref at same level
|
||||
- Pattern 7: Externally Tagged - oneOf with variant name as property key
|
||||
- Pattern 8: Untagged Hybrid - anyOf mixing const strings + $ref
|
||||
- Pattern 9: Untagged with Nulls - anyOf with multiple {type: "null"} variants
|
||||
- Pattern 10: Generic Enum - anyOf with {additionalProperties: T} + T
|
||||
- Pattern 11: Nullable Optional - anyOf with $ref + {type: "null"} + default
|
||||
#}
|
||||
|
||||
{# ============================================================================ #}
|
||||
{# SECTION 1: Type Badge Rendering #}
|
||||
{# ============================================================================ #}
|
||||
{# Render the type badge for a schema - outputs inline code or links #}
|
||||
{# When node has $ref, always prioritize it over type (Pattern 6, etc.) #}
|
||||
|
||||
{% macro render_type_badge(node) %}
|
||||
{%- if node["$ref"] -%}
|
||||
{# $ref always takes priority - render as link to type definition #}
|
||||
{%- set ref_name = node["$ref"] | replace(from="#/$defs/", to="") -%}
|
||||
{%- set display = display_names[ref_name] | default(value=ref_name) -%}
|
||||
{%- if schema["$defs"][ref_name] -%}
|
||||
{%- set ref_def = schema["$defs"][ref_name] -%}
|
||||
[`{{ display }}`](#{{ ref_name | lower }}){% if ref_def._wrapper_inner %} {{ self::render_type_badge(node=ref_def._wrapper_inner) }}{% endif -%}
|
||||
{%- else -%}
|
||||
`$ref`
|
||||
{%- endif -%}
|
||||
{%- elif node.type -%}
|
||||
{%- if node.type is string -%}
|
||||
{%- if node.type == "array" and node.prefixItems -%}
|
||||
{# Check if all prefixItems have the same type (homogeneous tuple) #}
|
||||
{%- set first_type = node.prefixItems | first -%}
|
||||
{%- set_global is_homogeneous = true -%}
|
||||
{%- for item in node.prefixItems -%}
|
||||
{%- if item.type is defined and first_type.type is defined -%}
|
||||
{%- if item.type != first_type.type -%}
|
||||
{%- set_global is_homogeneous = false -%}
|
||||
{%- endif -%}
|
||||
{%- else -%}
|
||||
{%- set_global is_homogeneous = false -%}
|
||||
{%- endif -%}
|
||||
{%- endfor -%}
|
||||
{%- if is_homogeneous and first_type.type -%}
|
||||
`array` `[{% if first_type.format %}{{ first_type.format }}{% else %}{{ first_type.type }}{% endif %}; {{ node.prefixItems | length }}]`
|
||||
{%- else -%}
|
||||
`array` `tuple`
|
||||
{%- endif -%}
|
||||
{%- elif node.type == "array" and node.items -%}
|
||||
`array` {{ self::render_type_badge(node=node.items) }}
|
||||
{%- else -%}
|
||||
`{{ node.type }}`
|
||||
{%- endif -%}
|
||||
{%- else -%}
|
||||
{# Check for Option<T> pattern: type array with exactly 2 types where one is null #}
|
||||
{%- if node.type | length == 2 and "null" in node.type -%}
|
||||
<Badge text="optional" variant="note" /> {% for t in node.type %}{% if t != "null" %}{% if t == "array" and node.items %}`array` {{ self::render_type_badge(node=node.items) }}{% else %}`{{ t }}`{% endif %}{% endif %}{% endfor %}
|
||||
{%- else -%}
|
||||
{% for t in node.type %}`{{ t }}` {% endfor %}
|
||||
{%- endif -%}
|
||||
{%- endif -%}
|
||||
{%- elif node.anyOf -%}
|
||||
{# Pattern 10: Wrapper type (detected in prepare.rs) #}
|
||||
{%- if node._wrapper_type -%}
|
||||
{{ self::render_type_badge(node=node._wrapper_inner) }}
|
||||
{%- elif node.anyOf | length == 2 -%}
|
||||
{%- set first = node.anyOf | first -%}
|
||||
{%- set second = node.anyOf | last -%}
|
||||
{# Pattern 11: Nullable Optional - anyOf with T + null #}
|
||||
{%- if second.type and second.type == "null" -%}
|
||||
<Badge text="optional" variant="note" /> {{ self::render_type_badge(node=first) }}
|
||||
{%- elif first.type and first.type == "null" -%}
|
||||
<Badge text="optional" variant="note" /> {{ self::render_type_badge(node=second) }}
|
||||
{%- else -%}
|
||||
{# Pattern 8: Check if all variants are string const (untagged hybrid) #}
|
||||
{%- set_global is_string_enum = true -%}
|
||||
{%- for variant in node.anyOf -%}
|
||||
{%- if variant.type and variant.type == "string" and variant.const -%}
|
||||
{%- else -%}
|
||||
{%- set_global is_string_enum = false -%}
|
||||
{%- endif -%}
|
||||
{%- endfor -%}
|
||||
{%- if is_string_enum -%}
|
||||
`string` `enum`
|
||||
{%- else -%}
|
||||
`enum`
|
||||
{%- endif -%}
|
||||
{%- endif -%}
|
||||
{%- else -%}
|
||||
{# Pattern 4a-d, 8, 9: Untagged enums with 3+ variants #}
|
||||
{%- set_global is_string_enum = true -%}
|
||||
{%- for variant in node.anyOf -%}
|
||||
{%- if variant.type and variant.type == "string" and variant.const -%}
|
||||
{%- else -%}
|
||||
{%- set_global is_string_enum = false -%}
|
||||
{%- endif -%}
|
||||
{%- endfor -%}
|
||||
{%- if is_string_enum -%}
|
||||
`string` `enum`
|
||||
{%- else -%}
|
||||
`enum`
|
||||
{%- endif -%}
|
||||
{%- endif -%}
|
||||
{%- elif node.oneOf -%}
|
||||
{# Pattern 1-3, 5-7: Tagged enums using oneOf #}
|
||||
{%- set_global is_string_enum = true -%}
|
||||
{%- for variant in node.oneOf -%}
|
||||
{# Pattern 1-2: Simple unit enum with string const #}
|
||||
{%- if variant.type and variant.type == "string" and variant.const -%}
|
||||
{%- else -%}
|
||||
{# Pattern 3, 5, 6, 7: Complex variants (objects, refs, etc.) #}
|
||||
{%- set_global is_string_enum = false -%}
|
||||
{%- endif -%}
|
||||
{%- endfor -%}
|
||||
{%- if is_string_enum -%}
|
||||
`string` `enum`
|
||||
{%- else -%}
|
||||
`enum`
|
||||
{%- endif -%}
|
||||
{%- elif node.allOf -%}
|
||||
`allOf`
|
||||
{%- elif node.const -%}
|
||||
`const`
|
||||
{%- elif node.enum -%}
|
||||
`enum`
|
||||
{%- endif -%}
|
||||
{% endmacro render_type_badge %}
|
||||
|
||||
{# ============================================================================ #}
|
||||
{# SECTION 2: Constraint Rendering #}
|
||||
{# ============================================================================ #}
|
||||
{# Render constraints like min, max, pattern, format #}
|
||||
|
||||
{% macro render_constraints(node) %}
|
||||
{# Check if this is a homogeneous tuple - skip minItems/maxItems if so #}
|
||||
{%- set_global skip_item_constraints = false -%}
|
||||
{%- if node.prefixItems -%}
|
||||
{%- set first_type = node.prefixItems | first -%}
|
||||
{%- set_global is_homogeneous = true -%}
|
||||
{%- for item in node.prefixItems -%}
|
||||
{%- if item.type is defined and first_type.type is defined -%}
|
||||
{%- if item.type != first_type.type -%}
|
||||
{%- set_global is_homogeneous = false -%}
|
||||
{%- endif -%}
|
||||
{%- else -%}
|
||||
{%- set_global is_homogeneous = false -%}
|
||||
{%- endif -%}
|
||||
{%- endfor -%}
|
||||
{%- if is_homogeneous and first_type.type -%}
|
||||
{%- set_global skip_item_constraints = true -%}
|
||||
{%- endif -%}
|
||||
{%- endif -%}
|
||||
{%- if node.format or node.pattern or node.minimum is defined or node.maximum is defined or node.minLength is defined or node.maxLength is defined or (node.minItems is defined and not skip_item_constraints) or (node.maxItems is defined and not skip_item_constraints) %}
|
||||
|
||||
{% if node.format %}- **Format:** `{{ node.format }}`
|
||||
{% endif %}{% if node.pattern %}- **Pattern:** `{{ node.pattern }}`
|
||||
{% endif %}{% if node.minimum is defined %}- **Minimum:** {{ node.minimum }}
|
||||
{% endif %}{% if node.maximum is defined %}- **Maximum:** {{ node.maximum }}
|
||||
{% endif %}{% if node.minLength is defined %}- **Min length:** {{ node.minLength }}
|
||||
{% endif %}{% if node.maxLength is defined %}- **Max length:** {{ node.maxLength }}
|
||||
{% endif %}{% if node.minItems is defined and not skip_item_constraints %}- **Min items:** {{ node.minItems }}
|
||||
{% endif %}{% if node.maxItems is defined and not skip_item_constraints %}- **Max items:** {{ node.maxItems }}
|
||||
{% endif %}
|
||||
{%- endif -%}
|
||||
{% endmacro render_constraints %}
|
||||
|
||||
{# ============================================================================ #}
|
||||
{# SECTION 3: Schema Body Rendering #}
|
||||
{# ============================================================================ #}
|
||||
{# Render the body of a schema (properties, items, variants, etc.) #}
|
||||
|
||||
{% macro render_schema_body(node, depth, path) %}
|
||||
{%- if depth < 10 -%}
|
||||
{%- if node["$ref"] -%}
|
||||
{# Reference only, already shown as type badge #}
|
||||
{%- elif node.properties -%}
|
||||
{%- set required_arr = node.required | default(value=[]) -%}
|
||||
|
||||
<details open>
|
||||
<summary>Properties ({{ node.properties | length }})</summary>
|
||||
|
||||
{# First render required properties #}
|
||||
{% for prop_name, prop in node.properties -%}
|
||||
{%- if prop_name in required_arr -%}
|
||||
{%- set is_required = true -%}
|
||||
{{ self::render_nested_property(name=prop_name, node=prop, required=is_required, depth=depth + 1, path=path ~ "." ~ prop_name) }}
|
||||
{% endif -%}
|
||||
{%- endfor -%}
|
||||
{# Then render optional properties #}
|
||||
{% for prop_name, prop in node.properties -%}
|
||||
{%- if prop_name not in required_arr -%}
|
||||
{%- set is_required = false -%}
|
||||
{{ self::render_nested_property(name=prop_name, node=prop, required=is_required, depth=depth + 1, path=path ~ "." ~ prop_name) }}
|
||||
{% endif -%}
|
||||
{%- endfor %}
|
||||
</details>
|
||||
{%- elif node.patternProperties -%}
|
||||
|
||||
{% for pattern, prop in node.patternProperties %}
|
||||
{# Convert known patterns to friendly type names #}
|
||||
{# Note: patterns use single backslash in the parsed JSON value #}
|
||||
{% if pattern == "^\d+$" or pattern == "^[0-9]+$" or pattern == "^-?\d+$" or pattern == "^-?[0-9]+$" -%}
|
||||
**Key:** `"<integer>"` **→** {{ self::render_type_badge(node=prop) }}
|
||||
{% elif pattern == ".*" or pattern == "^.*$" -%}
|
||||
**Key:** `"<string>"` **→** {{ self::render_type_badge(node=prop) }}
|
||||
{% else -%}
|
||||
**Pattern:** `{{ pattern }}` **→** {{ self::render_type_badge(node=prop) }}
|
||||
{% endif %}
|
||||
{% if prop.description %}{{ prop.description | mdx_escape }}{% endif %}
|
||||
{{ self::render_schema_body(node=prop, depth=depth + 1, path=path ~ ".[key]") }}
|
||||
{% endfor %}
|
||||
{%- elif node.items -%}
|
||||
{%- if node.items["$ref"] -%}
|
||||
{# Already shown in type badge #}
|
||||
{%- elif node.items.type and node.items.type is string -%}
|
||||
{# Simple type - already shown in type badge #}
|
||||
{%- else -%}
|
||||
|
||||
<details open>
|
||||
<summary>Array Items</summary>
|
||||
|
||||
{{ self::render_schema(node=node.items, depth=depth + 1, path=path ~ "[]") }}
|
||||
</details>
|
||||
{%- endif -%}
|
||||
{%- elif node.prefixItems -%}
|
||||
{# Check if homogeneous tuple #}
|
||||
{%- set first_type = node.prefixItems | first -%}
|
||||
{%- set_global is_homogeneous = true -%}
|
||||
{%- for item in node.prefixItems -%}
|
||||
{%- if item.type is defined and first_type.type is defined -%}
|
||||
{%- if item.type != first_type.type -%}
|
||||
{%- set_global is_homogeneous = false -%}
|
||||
{%- endif -%}
|
||||
{%- else -%}
|
||||
{%- set_global is_homogeneous = false -%}
|
||||
{%- endif -%}
|
||||
{%- endfor -%}
|
||||
{%- if is_homogeneous and first_type.type -%}
|
||||
{# Homogeneous tuple - already shown as [T; N] in badge #}
|
||||
{%- else -%}
|
||||
|
||||
<details open>
|
||||
<summary>Tuple Items ({{ node.prefixItems | length }})</summary>
|
||||
|
||||
{% for item in node.prefixItems %}
|
||||
**[{{ loop.index0 }}]** {{ self::render_schema(node=item, depth=depth + 1, path=path ~ "[" ~ loop.index0 ~ "]") }}
|
||||
{% endfor %}
|
||||
</details>
|
||||
{%- endif -%}
|
||||
{%- elif node.anyOf -%}
|
||||
{# Pattern 10: Wrapper type (detected in prepare.rs) #}
|
||||
{%- if node._wrapper_type -%}
|
||||
{{ self::render_schema_body(node=node._wrapper_inner, depth=depth, path=path) }}
|
||||
{%- elif node.anyOf | length == 2 -%}
|
||||
{%- set first = node.anyOf | first -%}
|
||||
{%- set second = node.anyOf | last -%}
|
||||
{# Pattern 11: Nullable Optional - unwrap the non-null variant #}
|
||||
{%- if second.type and second.type == "null" -%}
|
||||
{{ self::render_schema_body(node=first, depth=depth, path=path) }}
|
||||
{%- elif first.type and first.type == "null" -%}
|
||||
{{ self::render_schema_body(node=second, depth=depth, path=path) }}
|
||||
{%- else -%}
|
||||
{# Pattern 4a-d, 8: Two non-null variants #}
|
||||
|
||||
<details open>
|
||||
<summary>Enum variants ({{ node.anyOf | length }})</summary>
|
||||
|
||||
{% for variant in node.anyOf %}
|
||||
---
|
||||
{{ self::render_enum_variant(node=variant, depth=depth + 1, path=path ~ ".anyOf[" ~ loop.index0 ~ "]") }}
|
||||
{% endfor %}
|
||||
</details>
|
||||
{%- endif -%}
|
||||
{%- else -%}
|
||||
{# Pattern 4a-d, 8, 9: Untagged enums with 3+ variants #}
|
||||
|
||||
<details open>
|
||||
<summary>Enum variants ({{ node.anyOf | length }})</summary>
|
||||
|
||||
{% for variant in node.anyOf %}
|
||||
---
|
||||
{{ self::render_enum_variant(node=variant, depth=depth + 1, path=path ~ ".anyOf[" ~ loop.index0 ~ "]") }}
|
||||
{% endfor %}
|
||||
</details>
|
||||
{%- endif -%}
|
||||
{%- elif node.oneOf -%}
|
||||
{# Pattern 1-3, 5-7: Tagged enums using oneOf #}
|
||||
|
||||
<details open>
|
||||
<summary>Enum variants ({{ node.oneOf | length }})</summary>
|
||||
|
||||
{% for variant in node.oneOf %}
|
||||
---
|
||||
{{ self::render_enum_variant(node=variant, depth=depth + 1, path=path ~ ".oneOf[" ~ loop.index0 ~ "]") }}
|
||||
{% endfor %}
|
||||
</details>
|
||||
{%- elif node.allOf -%}
|
||||
|
||||
<details open>
|
||||
<summary>All of ({{ node.allOf | length }} schemas)</summary>
|
||||
|
||||
{% for variant in node.allOf %}
|
||||
---
|
||||
{{ self::render_schema(node=variant, depth=depth + 1, path=path ~ ".allOf[" ~ loop.index0 ~ "]") }}
|
||||
{% endfor %}
|
||||
</details>
|
||||
{%- elif node.enum -%}
|
||||
|
||||
<details open>
|
||||
<summary>Enum variants ({{ node.enum | length }})</summary>
|
||||
|
||||
{% for value in node.enum %}- `{{ value | json_encode() }}`
|
||||
{% endfor %}
|
||||
</details>
|
||||
{%- elif node.const -%}
|
||||
|
||||
**Value:** `{{ node.const | json_encode() }}`
|
||||
{%- endif -%}
|
||||
{%- if node.additionalProperties and node.additionalProperties is object -%}
|
||||
|
||||
<details open>
|
||||
<summary>Additional Properties</summary>
|
||||
|
||||
{{ self::render_schema(node=node.additionalProperties, depth=depth + 1, path=path ~ ".[*]") }}
|
||||
</details>
|
||||
{%- endif -%}
|
||||
{%- endif -%}
|
||||
{% endmacro render_schema_body %}
|
||||
|
||||
{# ============================================================================ #}
|
||||
{# SECTION 4: Schema Rendering #}
|
||||
{# ============================================================================ #}
|
||||
{# Render a schema without property wrapper #}
|
||||
|
||||
{% macro render_schema(node, depth, path) %}
|
||||
{%- if depth < 10 -%}
|
||||
{# For const variants with description, show description then value on separate lines #}
|
||||
{%- if node.const and node.description -%}
|
||||
{{ node.description | mdx_escape }}
|
||||
|
||||
**Value:** `{{ node.const | json_encode() }}`
|
||||
{%- elif node.const -%}
|
||||
**Value:** `{{ node.const | json_encode() }}`
|
||||
{%- else -%}
|
||||
{{ self::render_type_badge(node=node) }}
|
||||
{% if node.description %}
|
||||
{{ node.description | mdx_escape }}
|
||||
{% endif %}
|
||||
{{ self::render_constraints(node=node) }}
|
||||
{%- if node.default %}
|
||||
|
||||
**Default:** {% if node._default_ref and schema["$defs"][node._default_ref] %}[`{{ node.default | json_encode() }}`](#{{ node._default_ref | lower }}){% else %}`{{ node.default | json_encode() }}`{% endif %}
|
||||
{% endif -%}
|
||||
{{ self::render_schema_body(node=node, depth=depth, path=path) }}
|
||||
{%- endif -%}
|
||||
{%- endif -%}
|
||||
{% endmacro render_schema %}
|
||||
|
||||
{# ============================================================================ #}
|
||||
{# SECTION 5: Enum Variant Rendering #}
|
||||
{# ============================================================================ #}
|
||||
{# Render an enum variant - like render_schema but uses render_enum_variant_body #}
|
||||
{# for flat property display. Uses exhaustive fallback chain for variant titles. #}
|
||||
{# #}
|
||||
{# Title extraction priority (see ENUMS.md for pattern details): #}
|
||||
{# 1. node.title (explicit #[schemars(title = "...")]) #}
|
||||
{# 2. node.const (Pattern 1, 2, 3 unit variants, Pattern 9 hybrid) #}
|
||||
{# 3. properties.<tag>.const (Pattern 5, 6, 8 discriminator field) #}
|
||||
{# 4. required[0] when additionalProperties=false (Pattern 7 externally tagged) #}
|
||||
{# 5. $ref type name (Pattern 4, 9, 10 reference types) #}
|
||||
{# 6. Type-based fallback (array, tuple, map, primitives) #}
|
||||
{# ============================================================================ #}
|
||||
|
||||
{% macro render_enum_variant(node, depth, path) %}
|
||||
{%- if depth < 10 -%}
|
||||
{# Extract variant name using exhaustive fallback chain #}
|
||||
{%- set_global variant_name = "" -%}
|
||||
|
||||
{# Priority 1: Explicit title #}
|
||||
{%- if node.title -%}
|
||||
{%- set_global variant_name = node.title -%}
|
||||
|
||||
{# Priority 2: Const value (Pattern 1, 2, 3 unit variants, Pattern 9) #}
|
||||
{%- elif node.const -%}
|
||||
{%- set_global variant_name = node.const -%}
|
||||
|
||||
{# Priority 3 & 4: Properties-based extraction #}
|
||||
{%- elif node.properties -%}
|
||||
{# Priority 3: Discriminator const from properties (Pattern 5, 6, 8) #}
|
||||
{%- for prop_name, prop in node.properties -%}
|
||||
{%- if prop.const and variant_name == "" -%}
|
||||
{%- set_global variant_name = prop.const -%}
|
||||
{%- endif -%}
|
||||
{%- endfor -%}
|
||||
{# Priority 4: Externally tagged - property key (Pattern 7) #}
|
||||
{%- if variant_name == "" and node.additionalProperties is defined and node.additionalProperties == false -%}
|
||||
{%- if node.required and node.required | length > 0 -%}
|
||||
{%- set_global variant_name = node.required | first -%}
|
||||
{%- endif -%}
|
||||
{%- endif -%}
|
||||
|
||||
{# Priority 5: $ref type name (Pattern 4, 9, 10) #}
|
||||
{%- elif node["$ref"] -%}
|
||||
{%- set ref_name = node["$ref"] | replace(from="#/$defs/", to="") -%}
|
||||
{%- set_global variant_name = display_names[ref_name] | default(value=ref_name) -%}
|
||||
|
||||
{# Priority 6: Type-based fallback #}
|
||||
{%- elif node.type -%}
|
||||
{%- if node.type is string -%}
|
||||
{%- if node.type == "array" -%}
|
||||
{%- if node.items and node.items["$ref"] -%}
|
||||
{%- set item_ref = node.items["$ref"] | replace(from="#/$defs/", to="") -%}
|
||||
{%- set item_display = display_names[item_ref] | default(value=item_ref) -%}
|
||||
{%- set_global variant_name = item_display ~ " Array" -%}
|
||||
{%- elif node.prefixItems -%}
|
||||
{%- set_global variant_name = "Tuple" -%}
|
||||
{%- else -%}
|
||||
{%- set_global variant_name = "Array" -%}
|
||||
{%- endif -%}
|
||||
{%- elif node.type == "object" and node.additionalProperties -%}
|
||||
{%- set_global variant_name = "Map" -%}
|
||||
{%- else -%}
|
||||
{%- set_global variant_name = node.type | capitalize -%}
|
||||
{%- endif -%}
|
||||
{%- endif -%}
|
||||
{%- endif -%}
|
||||
|
||||
{# Render heading if we have a variant name #}
|
||||
{%- if variant_name %}
|
||||
#### {{ variant_name }}
|
||||
{% endif -%}
|
||||
|
||||
{# Render description #}
|
||||
{% if node.description %}
|
||||
{{ node.description | mdx_escape }}
|
||||
{% endif %}
|
||||
|
||||
{# Render value based on variant type #}
|
||||
{# First check for Pattern 7: Externally Tagged (single property with additionalProperties: false) #}
|
||||
{# These are handled specially in render_enum_variant_body, so skip generic value here #}
|
||||
{%- set_global is_pattern7 = false -%}
|
||||
{%- if node.properties -%}
|
||||
{%- set props_count = node.properties | length -%}
|
||||
{%- if props_count == 1 and node.additionalProperties is defined and node.additionalProperties == false -%}
|
||||
{%- set_global is_pattern7 = true -%}
|
||||
{%- endif -%}
|
||||
{%- endif -%}
|
||||
{%- if node.const -%}
|
||||
**Value:** `{{ node.const | json_encode() }}`
|
||||
{%- elif not is_pattern7 and (node["$ref"] or node.type or node.anyOf or node.oneOf or node.allOf) %}
|
||||
**Value:** {{ self::render_type_badge(node=node) }}
|
||||
{%- endif %}
|
||||
|
||||
{{ self::render_constraints(node=node) }}
|
||||
{%- if node.default %}
|
||||
|
||||
**Default:** {% if node._default_ref and schema["$defs"][node._default_ref] %}[`{{ node.default | json_encode() }}`](#{{ node._default_ref | lower }}){% else %}`{{ node.default | json_encode() }}`{% endif %}
|
||||
{% endif -%}
|
||||
{{ self::render_enum_variant_body(node=node, depth=depth, path=path) }}
|
||||
{%- endif -%}
|
||||
{% endmacro render_enum_variant %}
|
||||
|
||||
{# ============================================================================ #}
|
||||
{# SECTION 6: Enum Variant Body Rendering #}
|
||||
{# ============================================================================ #}
|
||||
{# Render the body of an enum variant - properties are shown directly without #}
|
||||
{# details wrapper, required first. #}
|
||||
{# Handles Pattern 3 (mixed), Pattern 5 (internally tagged), #}
|
||||
{# Pattern 6 (internally tagged + flatten), Pattern 7 (externally tagged) #}
|
||||
|
||||
{% macro render_enum_variant_body(node, depth, path) %}
|
||||
{%- if depth < 10 -%}
|
||||
{%- if node.properties -%}
|
||||
{# Check for Pattern 7: Externally Tagged Enum (single property with additionalProperties: false) #}
|
||||
{%- set props_count = node.properties | length -%}
|
||||
{%- if props_count == 1 and node.additionalProperties is defined and node.additionalProperties == false -%}
|
||||
{# Pattern 7: Externally Tagged - render variant name and value inline #}
|
||||
{%- for prop_name, prop in node.properties -%}
|
||||
|
||||
**Value:** `{{ prop_name }}` → {{ self::render_type_badge(node=prop) }}
|
||||
{%- endfor -%}
|
||||
{%- else -%}
|
||||
{# Pattern 5, 6: Object variants with properties (internally tagged) #}
|
||||
{# Pattern 6: Handle properties first - this covers cases where both properties and $ref exist #}
|
||||
{# Skip properties with const values (discriminator fields) - already shown as variant heading #}
|
||||
{%- set required_arr = node.required | default(value=[]) -%}
|
||||
|
||||
{# First render required properties (skip discriminator fields with const) #}
|
||||
{% for prop_name, prop in node.properties -%}
|
||||
{%- if prop_name in required_arr and not prop.const -%}
|
||||
{%- set is_required = true -%}
|
||||
{{ self::render_nested_property(name=prop_name, node=prop, required=is_required, depth=depth + 1, path=path ~ "." ~ prop_name) }}
|
||||
{% endif -%}
|
||||
{%- endfor -%}
|
||||
{# Then render optional properties (skip discriminator fields with const) #}
|
||||
{% for prop_name, prop in node.properties -%}
|
||||
{%- if prop_name not in required_arr and not prop.const -%}
|
||||
{%- set is_required = false -%}
|
||||
{{ self::render_nested_property(name=prop_name, node=prop, required=is_required, depth=depth + 1, path=path ~ "." ~ prop_name) }}
|
||||
{% endif -%}
|
||||
{%- endfor -%}
|
||||
{%- endif -%}
|
||||
{# Pattern 6: When node has both properties AND $ref, the $ref is shown as a link in Value: #}
|
||||
{# so we don't need to inline the referenced type's properties here #}
|
||||
{%- elif node["$ref"] -%}
|
||||
{# Reference only, already shown as type badge #}
|
||||
{%- elif node.patternProperties -%}
|
||||
|
||||
{% for pattern, prop in node.patternProperties %}
|
||||
{% if pattern == "^\d+$" or pattern == "^[0-9]+$" or pattern == "^-?\d+$" or pattern == "^-?[0-9]+$" -%}
|
||||
**Key:** `"<integer>"` **→** {{ self::render_type_badge(node=prop) }}
|
||||
{% elif pattern == ".*" or pattern == "^.*$" -%}
|
||||
**Key:** `"<string>"` **→** {{ self::render_type_badge(node=prop) }}
|
||||
{% else -%}
|
||||
**Pattern:** `{{ pattern }}` **→** {{ self::render_type_badge(node=prop) }}
|
||||
{% endif %}
|
||||
{% if prop.description %}{{ prop.description | mdx_escape }}{% endif %}
|
||||
{{ self::render_enum_variant_body(node=prop, depth=depth + 1, path=path ~ ".[key]") }}
|
||||
{% endfor %}
|
||||
{%- elif node.items -%}
|
||||
{%- if node.items["$ref"] -%}
|
||||
{# Already shown in type badge #}
|
||||
{%- elif node.items.type and node.items.type is string -%}
|
||||
{# Simple type - already shown in type badge #}
|
||||
{%- else -%}
|
||||
|
||||
<details open>
|
||||
<summary>Array Items</summary>
|
||||
|
||||
{{ self::render_enum_variant(node=node.items, depth=depth + 1, path=path ~ "[]") }}
|
||||
</details>
|
||||
{%- endif -%}
|
||||
{%- elif node.prefixItems -%}
|
||||
{%- set first_type = node.prefixItems | first -%}
|
||||
{%- set_global is_homogeneous = true -%}
|
||||
{%- for item in node.prefixItems -%}
|
||||
{%- if item.type is defined and first_type.type is defined -%}
|
||||
{%- if item.type != first_type.type -%}
|
||||
{%- set_global is_homogeneous = false -%}
|
||||
{%- endif -%}
|
||||
{%- else -%}
|
||||
{%- set_global is_homogeneous = false -%}
|
||||
{%- endif -%}
|
||||
{%- endfor -%}
|
||||
{%- if is_homogeneous and first_type.type -%}
|
||||
{# Homogeneous tuple - already shown as [T; N] in badge #}
|
||||
{%- else -%}
|
||||
|
||||
<details open>
|
||||
<summary>Tuple Items ({{ node.prefixItems | length }})</summary>
|
||||
|
||||
{% for item in node.prefixItems %}
|
||||
**[{{ loop.index0 }}]** {{ self::render_enum_variant(node=item, depth=depth + 1, path=path ~ "[" ~ loop.index0 ~ "]") }}
|
||||
{% endfor %}
|
||||
</details>
|
||||
{%- endif -%}
|
||||
{%- elif node.anyOf -%}
|
||||
{%- if node._wrapper_type -%}
|
||||
{{ self::render_enum_variant_body(node=node._wrapper_inner, depth=depth, path=path) }}
|
||||
{%- elif node.anyOf | length == 2 -%}
|
||||
{%- set first = node.anyOf | first -%}
|
||||
{%- set second = node.anyOf | last -%}
|
||||
{%- if second.type and second.type == "null" -%}
|
||||
{{ self::render_enum_variant_body(node=first, depth=depth, path=path) }}
|
||||
{%- elif first.type and first.type == "null" -%}
|
||||
{{ self::render_enum_variant_body(node=second, depth=depth, path=path) }}
|
||||
{%- else -%}
|
||||
|
||||
<details open>
|
||||
<summary>Enum variants ({{ node.anyOf | length }})</summary>
|
||||
|
||||
{% for variant in node.anyOf %}
|
||||
---
|
||||
{{ self::render_enum_variant(node=variant, depth=depth + 1, path=path ~ ".anyOf[" ~ loop.index0 ~ "]") }}
|
||||
{% endfor %}
|
||||
</details>
|
||||
{%- endif -%}
|
||||
{%- else -%}
|
||||
|
||||
<details open>
|
||||
<summary>Enum variants ({{ node.anyOf | length }})</summary>
|
||||
|
||||
{% for variant in node.anyOf %}
|
||||
---
|
||||
{{ self::render_enum_variant(node=variant, depth=depth + 1, path=path ~ ".anyOf[" ~ loop.index0 ~ "]") }}
|
||||
{% endfor %}
|
||||
</details>
|
||||
{%- endif -%}
|
||||
{%- elif node.oneOf -%}
|
||||
|
||||
<details open>
|
||||
<summary>Enum variants ({{ node.oneOf | length }})</summary>
|
||||
|
||||
{% for variant in node.oneOf %}
|
||||
---
|
||||
{{ self::render_enum_variant(node=variant, depth=depth + 1, path=path ~ ".oneOf[" ~ loop.index0 ~ "]") }}
|
||||
{% endfor %}
|
||||
</details>
|
||||
{%- elif node.allOf -%}
|
||||
|
||||
<details open>
|
||||
<summary>All of ({{ node.allOf | length }} schemas)</summary>
|
||||
|
||||
{% for variant in node.allOf %}
|
||||
---
|
||||
{{ self::render_enum_variant(node=variant, depth=depth + 1, path=path ~ ".allOf[" ~ loop.index0 ~ "]") }}
|
||||
{% endfor %}
|
||||
</details>
|
||||
{%- elif node.enum -%}
|
||||
|
||||
<details open>
|
||||
<summary>Enum variants ({{ node.enum | length }})</summary>
|
||||
|
||||
{% for value in node.enum %}- `{{ value | json_encode() }}`
|
||||
{% endfor %}
|
||||
</details>
|
||||
{# Note: node.const is NOT handled here - it's already rendered in render_enum_variant #}
|
||||
{%- endif -%}
|
||||
{%- if node.additionalProperties and node.additionalProperties is object -%}
|
||||
|
||||
<details open>
|
||||
<summary>Additional Properties</summary>
|
||||
|
||||
{{ self::render_enum_variant(node=node.additionalProperties, depth=depth + 1, path=path ~ ".[*]") }}
|
||||
</details>
|
||||
{%- endif -%}
|
||||
{%- endif -%}
|
||||
{% endmacro render_enum_variant_body %}
|
||||
|
||||
{# ============================================================================ #}
|
||||
{# SECTION 7: Property Rendering #}
|
||||
{# ============================================================================ #}
|
||||
{# Render a nested property (used within details blocks) #}
|
||||
|
||||
{% macro render_nested_property(name, node, required, depth, path) %}
|
||||
{%- if depth < 10 -%}
|
||||
{%- set anchor = path | lower | replace(from=".", to="-") -%}
|
||||
#### {{ name }} {{"{"}}#{{ anchor }}{{"}"}}
|
||||
|
||||
{% if required %}<Badge text="required" variant="success" /> {% endif %}{% if node.deprecated %}<Badge text="deprecated" variant="danger" /> {% endif %}{{ self::render_type_badge(node=node) }}
|
||||
|
||||
{% if node.description %}{{ node.description | mdx_escape }}{% endif %}
|
||||
{{ self::render_constraints(node=node) }}
|
||||
{%- if node.default %}
|
||||
|
||||
**Default:** {% if node._default_ref and schema["$defs"][node._default_ref] %}[`{{ node.default | json_encode() }}`](#{{ node._default_ref | lower }}){% else %}`{{ node.default | json_encode() }}`{% endif %}
|
||||
{% endif -%}
|
||||
{{ self::render_schema_body(node=node, depth=depth, path=path) }}
|
||||
{%- endif -%}
|
||||
{% endmacro render_nested_property %}
|
||||
|
||||
{# Render a top-level property as H3 #}
|
||||
|
||||
{% macro render_property(name, node, required, depth, path) %}
|
||||
{%- if depth < 10 -%}
|
||||
{%- set anchor = path | lower | replace(from=".", to="-") -%}
|
||||
### {{ name }} {{"{"}}#{{ anchor }}{{"}"}}
|
||||
|
||||
{% if required %}<Badge text="required" variant="success" /> {% endif %}{% if node.deprecated %}<Badge text="deprecated" variant="danger" /> {% endif %}{{ self::render_type_badge(node=node) }}
|
||||
|
||||
{% if node.description %}{{ node.description | mdx_escape }}{% endif %}
|
||||
{{ self::render_constraints(node=node) }}
|
||||
{%- if node.default %}
|
||||
|
||||
**Default:** {% if node._default_ref and schema["$defs"][node._default_ref] %}[`{{ node.default | json_encode() }}`](#{{ node._default_ref | lower }}){% else %}`{{ node.default | json_encode() }}`{% endif %}
|
||||
{% endif -%}
|
||||
{{ self::render_schema_body(node=node, depth=depth, path=path) }}
|
||||
|
||||
---
|
||||
|
||||
{%- endif -%}
|
||||
{% endmacro render_property %}
|
||||
|
||||
{# ============================================================================ #}
|
||||
{# SECTION 8: Definition & Usage Rendering #}
|
||||
{# ============================================================================ #}
|
||||
{# Render usages section for a type definition #}
|
||||
|
||||
{% macro render_usages(def) %}
|
||||
{%- if def._usages and (def._usages.config_usages | length > 0 or def._usages.type_references | length > 0) -%}
|
||||
|
||||
#### Usages
|
||||
|
||||
{% if def._usages.config_usages | length > 0 -%}
|
||||
**Configuration:**
|
||||
{% for usage in def._usages.config_usages %}- [`{{ usage.path }}`](#{{ usage.anchor }})
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% if def._usages.type_references | length > 0 -%}
|
||||
**Referenced by:**
|
||||
{% for ref in def._usages.type_references %}{%- set ref_display = display_names[ref.type_name] | default(value=ref.type_name) -%}
|
||||
- [`{{ ref_display }}`](#{{ ref.anchor }})
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{%- endif -%}
|
||||
{% endmacro render_usages %}
|
||||
|
||||
{# Render a type definition as H3 #}
|
||||
|
||||
{% macro render_definition(def_name, def) %}
|
||||
{%- set anchor = def_name | lower | replace(from=".", to="-") -%}
|
||||
### {{ def._display_name | default(value=def_name) }} {{"{"}}#{{ anchor }}{{"}"}}
|
||||
|
||||
{% if def.description %}{{ def.description | mdx_escape }}{% endif %}
|
||||
{{ self::render_constraints(node=def) }}
|
||||
{{ self::render_schema_body(node=def, depth=0, path=def_name) }}
|
||||
{{ self::render_usages(def=def) }}
|
||||
|
||||
---
|
||||
|
||||
{% endmacro render_definition %}
|
||||
@@ -0,0 +1,143 @@
|
||||
{
|
||||
inputs = {
|
||||
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
||||
flake-parts.url = "github:hercules-ci/flake-parts";
|
||||
crane.url = "github:ipetkov/crane";
|
||||
rust-overlay.url = "github:oxalica/rust-overlay";
|
||||
rust-overlay.inputs.nixpkgs.follows = "nixpkgs";
|
||||
treefmt-nix.url = "github:numtide/treefmt-nix";
|
||||
treefmt-nix.inputs.nixpkgs.follows = "nixpkgs";
|
||||
git-hooks-nix.url = "github:cachix/git-hooks.nix";
|
||||
git-hooks-nix.inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
|
||||
outputs =
|
||||
inputs@{
|
||||
nixpkgs,
|
||||
flake-parts,
|
||||
crane,
|
||||
rust-overlay,
|
||||
...
|
||||
}:
|
||||
flake-parts.lib.mkFlake { inherit inputs; } {
|
||||
imports = [
|
||||
inputs.treefmt-nix.flakeModule
|
||||
inputs.git-hooks-nix.flakeModule
|
||||
];
|
||||
|
||||
systems = [
|
||||
"x86_64-linux"
|
||||
"aarch64-linux"
|
||||
"aarch64-darwin"
|
||||
];
|
||||
|
||||
perSystem =
|
||||
{
|
||||
config,
|
||||
system,
|
||||
...
|
||||
}:
|
||||
let
|
||||
pkgs = import nixpkgs {
|
||||
inherit system;
|
||||
overlays = [ rust-overlay.overlays.default ];
|
||||
};
|
||||
|
||||
rustToolchain = pkgs.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml;
|
||||
nightlyRustfmt = pkgs.rust-bin.nightly.latest.rustfmt;
|
||||
rustToolchainWithNightlyRustfmt = pkgs.symlinkJoin {
|
||||
name = "rust-toolchain-with-nightly-rustfmt";
|
||||
paths = [
|
||||
nightlyRustfmt
|
||||
rustToolchain
|
||||
];
|
||||
};
|
||||
|
||||
nightlyToolchain = pkgs.rust-bin.nightly.latest.default;
|
||||
cargo-udeps = pkgs.writeShellScriptBin "cargo-udeps" ''
|
||||
export PATH="${nightlyToolchain}/bin:$PATH"
|
||||
exec ${pkgs.cargo-udeps}/bin/cargo-udeps "$@"
|
||||
'';
|
||||
|
||||
craneLib = (crane.mkLib pkgs).overrideToolchain rustToolchain;
|
||||
|
||||
src = pkgs.lib.cleanSourceWith {
|
||||
src = ./.;
|
||||
filter =
|
||||
path: type:
|
||||
(craneLib.filterCargoSources path type)
|
||||
|| (builtins.match ".*\\.css$" path != null)
|
||||
|| (builtins.match ".*\\.html$" path != null)
|
||||
|| (builtins.match ".*\\.md$" path != null)
|
||||
|| (builtins.match ".*\\.json$" path != null);
|
||||
};
|
||||
|
||||
commonArgs = {
|
||||
inherit src;
|
||||
strictDeps = true;
|
||||
};
|
||||
|
||||
cargoArtifacts = craneLib.buildDepsOnly commonArgs;
|
||||
|
||||
schemars-docgen = craneLib.buildPackage (
|
||||
commonArgs
|
||||
// {
|
||||
inherit cargoArtifacts;
|
||||
}
|
||||
);
|
||||
in
|
||||
{
|
||||
checks = {
|
||||
clippy = craneLib.cargoClippy (
|
||||
commonArgs
|
||||
// {
|
||||
inherit cargoArtifacts;
|
||||
}
|
||||
);
|
||||
tests = craneLib.cargoTest (
|
||||
commonArgs
|
||||
// {
|
||||
inherit cargoArtifacts;
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
packages = {
|
||||
inherit schemars-docgen;
|
||||
default = schemars-docgen;
|
||||
};
|
||||
|
||||
devShells.default = pkgs.mkShell {
|
||||
inputsFrom = [ schemars-docgen ];
|
||||
packages = [
|
||||
rustToolchainWithNightlyRustfmt
|
||||
cargo-udeps
|
||||
pkgs.cargo-outdated
|
||||
pkgs.bacon
|
||||
];
|
||||
};
|
||||
|
||||
pre-commit = {
|
||||
check.enable = true;
|
||||
settings.hooks.treefmt = {
|
||||
enable = true;
|
||||
package = config.treefmt.build.wrapper;
|
||||
pass_filenames = false;
|
||||
};
|
||||
};
|
||||
|
||||
treefmt = {
|
||||
projectRootFile = "flake.nix";
|
||||
programs = {
|
||||
rustfmt = {
|
||||
enable = true;
|
||||
package = nightlyRustfmt;
|
||||
};
|
||||
taplo.enable = true;
|
||||
nixfmt.enable = true;
|
||||
deadnix.enable = true;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user