merge: the history of schemars-docgen

This commit is contained in:
LGUG2Z committed 2026-11-10 13:57:36 -08:00
commit 84b2c3d04b
9 files changed
+6822

No files matched your search

+22
View File
@@ -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"
+826
View File
@@ -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` |
+480
View File
@@ -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());
}
}
+50
View File
@@ -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 %}
+143
View File
@@ -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;
};
};
};
};
}