diff --git a/docs/llms-full.txt b/docs/llms-full.txt index 6f7d1522b..8b54d2953 100644 --- a/docs/llms-full.txt +++ b/docs/llms-full.txt @@ -24192,7 +24192,7 @@ datamodel-code-generator detects the OpenAPI version from the `openapi` field: | `deprecated` | 2019-09 | ⚠️ Partial | Recognized but not enforced | | `examples` (array) | Draft 6 | ⚠️ Partial | Only first example used for Field default | | Recursive `$ref` | Draft 4+ | ⚠️ Partial | Supported with `ForwardRef`, may require manual adjustment | -| `propertyNames` | Draft 6 | ❌ Not supported | Property name validation ignored | +| `propertyNames` | Draft 6 | ✅ Supported | Dict key type constraints via pattern, enum, or $ref | | `dependentRequired` | 2019-09 | ❌ Not supported | Dependent requirements ignored | | `dependentSchemas` | 2019-09 | ❌ Not supported | Dependent schemas ignored | diff --git a/docs/supported_formats.md b/docs/supported_formats.md index ccc8d2c10..c6941cae4 100644 --- a/docs/supported_formats.md +++ b/docs/supported_formats.md @@ -138,7 +138,7 @@ datamodel-code-generator detects the OpenAPI version from the `openapi` field: | `deprecated` | 2019-09 | ⚠️ Partial | Recognized but not enforced | | `examples` (array) | Draft 6 | ⚠️ Partial | Only first example used for Field default | | Recursive `$ref` | Draft 4+ | ⚠️ Partial | Supported with `ForwardRef`, may require manual adjustment | -| `propertyNames` | Draft 6 | ❌ Not supported | Property name validation ignored | +| `propertyNames` | Draft 6 | ✅ Supported | Dict key type constraints via pattern, enum, or $ref | | `dependentRequired` | 2019-09 | ❌ Not supported | Dependent requirements ignored | | `dependentSchemas` | 2019-09 | ❌ Not supported | Dependent schemas ignored | diff --git a/scripts/build_schema_docs.py b/scripts/build_schema_docs.py new file mode 100644 index 000000000..67b79ca53 --- /dev/null +++ b/scripts/build_schema_docs.py @@ -0,0 +1,181 @@ +"""Schema documentation builder. + +Generates feature tables from JsonSchemaFeatures and OpenAPISchemaFeatures +metadata for docs/supported_formats.md. + +Usage: + python scripts/build_schema_docs.py # Generate/update docs + python scripts/build_schema_docs.py --check # Check if docs are up to date +""" + +from __future__ import annotations + +import argparse +import sys +from dataclasses import fields +from pathlib import Path + +# Add src to path for imports +sys.path.insert(0, str(Path(__file__).parent.parent / "src")) + +from datamodel_code_generator.parser.schema_version import ( + FeatureMetadata, + JsonSchemaFeatures, + OpenAPISchemaFeatures, +) + +DOCS_PATH = Path(__file__).parent.parent / "docs" / "supported_formats.md" + +# Status emoji mapping +STATUS_EMOJI = { + "supported": "✅", + "partial": "⚠️", + "not_supported": "❌", +} + + +def get_feature_metadata(cls: type) -> list[tuple[str, FeatureMetadata]]: + """Extract feature metadata from a dataclass. + + Args: + cls: JsonSchemaFeatures or OpenAPISchemaFeatures class. + + Returns: + List of (field_name, metadata) tuples. + """ + result = [] + for f in fields(cls): + if f.metadata: + meta = FeatureMetadata( + introduced=f.metadata.get("introduced", ""), + doc_name=f.metadata.get("doc_name", f.name), + description=f.metadata.get("description", ""), + status=f.metadata.get("status", "supported"), + ) + result.append((f.name, meta)) + return result + + +def generate_feature_table( + features: list[tuple[str, FeatureMetadata]], + *, + include_status: bool = True, +) -> str: + """Generate a Markdown table from feature metadata. + + Args: + features: List of (field_name, metadata) tuples. + include_status: Whether to include the Status column. + + Returns: + Markdown table string. + """ + if include_status: + lines = ["| Feature | Introduced | Status | Description |"] + lines.append("|---------|------------|--------|-------------|") + for _name, meta in features: + emoji = STATUS_EMOJI.get(meta["status"], "") + status_text = f"{emoji} {meta['status'].replace('_', ' ').title()}" + lines.append(f"| `{meta['doc_name']}` | {meta['introduced']} | {status_text} | {meta['description']} |") + else: + lines = ["| Feature | Introduced | Description |"] + lines.append("|---------|------------|-------------|") + for _name, meta in features: + lines.append(f"| `{meta['doc_name']}` | {meta['introduced']} | {meta['description']} |") + + return "\n".join(lines) + + +def print_features_summary() -> None: + """Print a summary of all features with their metadata.""" + print("=" * 60) + print("JSON Schema Features") + print("=" * 60) + for name, meta in get_feature_metadata(JsonSchemaFeatures): + print(f" {name}:") + print(f" doc_name: {meta['doc_name']}") + print(f" introduced: {meta['introduced']}") + print(f" status: {meta['status']}") + print(f" description: {meta['description']}") + print() + + print("=" * 60) + print("OpenAPI Schema Features (additional)") + print("=" * 60) + # Get only OpenAPI-specific fields (not inherited from JsonSchemaFeatures) + json_field_names = {f.name for f in fields(JsonSchemaFeatures)} + for name, meta in get_feature_metadata(OpenAPISchemaFeatures): + if name not in json_field_names: + print(f" {name}:") + print(f" doc_name: {meta['doc_name']}") + print(f" introduced: {meta['introduced']}") + print(f" status: {meta['status']}") + print(f" description: {meta['description']}") + print() + + +def generate_supported_features_table() -> str: + """Generate the supported features table for documentation.""" + lines = [ + "", + "", + "### Supported Features (from code)", + "", + "The following features are tracked in the codebase with their implementation status:", + "", + "#### JSON Schema Features", + "", + generate_feature_table(get_feature_metadata(JsonSchemaFeatures)), + "", + "#### OpenAPI-Specific Features", + "", + ] + + # Get only OpenAPI-specific fields + json_field_names = {f.name for f in fields(JsonSchemaFeatures)} + openapi_features = [ + (name, meta) for name, meta in get_feature_metadata(OpenAPISchemaFeatures) if name not in json_field_names + ] + lines.extend((generate_feature_table(openapi_features), "", "")) + + return "\n".join(lines) + + +def main() -> int: + """Parse arguments and build documentation.""" + parser = argparse.ArgumentParser(description="Build schema documentation from code metadata") + parser.add_argument( + "--check", + action="store_true", + help="Check if docs would change without modifying files", + ) + parser.add_argument( + "--summary", + action="store_true", + help="Print a summary of all features and their metadata", + ) + args = parser.parse_args() + + if args.summary: + print_features_summary() + return 0 + + print("Schema Documentation Builder") + print("-" * 40) + + # For now, just print the generated table + print("\nGenerated Supported Features Table:") + print() + print(generate_supported_features_table()) + + if args.check: + print("\n[Check mode] No files modified.") + else: + print("\n[Info] This script currently outputs to stdout.") + print(" Future versions will update docs/supported_formats.md directly.") + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/src/datamodel_code_generator/parser/schema_version.py b/src/datamodel_code_generator/parser/schema_version.py index 853ec7a55..240e029d6 100644 --- a/src/datamodel_code_generator/parser/schema_version.py +++ b/src/datamodel_code_generator/parser/schema_version.py @@ -7,8 +7,10 @@ from __future__ import annotations -from dataclasses import dataclass -from typing import TYPE_CHECKING, Any, TypeAlias, TypeVar +from dataclasses import dataclass, field +from typing import TYPE_CHECKING, Any, Literal, TypeAlias, TypeVar + +from typing_extensions import TypedDict from datamodel_code_generator.enums import JsonSchemaVersion, OpenAPIVersion @@ -16,6 +18,23 @@ from datamodel_code_generator.types import Types +class FeatureMetadata(TypedDict): + """Metadata for schema feature documentation. + + This metadata is used by scripts/build_schema_docs.py to generate + the feature compatibility matrix in docs/supported_formats.md. + """ + + introduced: str + """Version when feature was introduced (e.g., "Draft 6", "2020-12", "OAS 3.0").""" + doc_name: str + """Display name for documentation (e.g., "prefixItems", "Null in type array").""" + description: str + """User-facing description of the feature.""" + status: Literal["supported", "partial", "not_supported"] + """Implementation status: supported, partial, or not_supported.""" + + @dataclass(frozen=True) class JsonSchemaFeatures: """Feature flags for JSON Schema versions. @@ -23,25 +42,82 @@ class JsonSchemaFeatures: This is the base class for schema features. OpenAPISchemaFeatures extends this to add OpenAPI-specific features. - Attributes: - null_in_type_array: Draft 2020-12 allows null in type arrays. - defs_not_definitions: Draft 2019-09+ uses $defs instead of definitions. - prefix_items: Draft 2020-12 uses prefixItems instead of items array. - boolean_schemas: Draft 6+ allows boolean values as schemas. - id_field: The field name for schema ID ("id" for Draft 4, "$id" for Draft 6+). - definitions_key: The key for definitions ("definitions" or "$defs"). - exclusive_as_number: Draft 6+ uses numeric exclusiveMin/Max (Draft 4 uses boolean). - read_only_write_only: Draft 7+ supports readOnly/writeOnly keywords. + Each field includes metadata for documentation generation. + Use `dataclasses.fields(JsonSchemaFeatures)` to access metadata. """ - null_in_type_array: bool - defs_not_definitions: bool - prefix_items: bool - boolean_schemas: bool - id_field: str - definitions_key: str - exclusive_as_number: bool - read_only_write_only: bool + null_in_type_array: bool = field( + default=False, + metadata=FeatureMetadata( + introduced="2020-12", + doc_name="Null in type array", + description="Allows `type: ['string', 'null']` syntax for nullable types", + status="supported", + ), + ) + defs_not_definitions: bool = field( + default=False, + metadata=FeatureMetadata( + introduced="2019-09", + doc_name="$defs", + description="Uses `$defs` instead of `definitions` for schema definitions", + status="supported", + ), + ) + prefix_items: bool = field( + default=False, + metadata=FeatureMetadata( + introduced="2020-12", + doc_name="prefixItems", + description="Tuple validation using `prefixItems` keyword", + status="supported", + ), + ) + boolean_schemas: bool = field( + default=False, + metadata=FeatureMetadata( + introduced="Draft 6", + doc_name="Boolean schemas", + description="Allows `true` and `false` as valid schemas", + status="supported", + ), + ) + id_field: str = field( + default="$id", + metadata=FeatureMetadata( + introduced="Draft 6", + doc_name="$id", + description="Schema identifier field (`id` in Draft 4, `$id` in Draft 6+)", + status="supported", + ), + ) + definitions_key: str = field( + default="$defs", + metadata=FeatureMetadata( + introduced="Draft 4", + doc_name="definitions/$defs", + description="Key for reusable schema definitions", + status="supported", + ), + ) + exclusive_as_number: bool = field( + default=False, + metadata=FeatureMetadata( + introduced="Draft 6", + doc_name="exclusiveMinimum/Maximum as number", + description="Numeric `exclusiveMinimum`/`exclusiveMaximum` (boolean in Draft 4)", + status="supported", + ), + ) + read_only_write_only: bool = field( + default=False, + metadata=FeatureMetadata( + introduced="Draft 7", + doc_name="readOnly/writeOnly", + description="Field visibility hints for read-only and write-only properties", + status="supported", + ), + ) @classmethod def from_version(cls, version: JsonSchemaVersion) -> JsonSchemaFeatures: @@ -110,13 +186,28 @@ class OpenAPISchemaFeatures(JsonSchemaFeatures): Extends JsonSchemaFeatures with OpenAPI-specific features. - Attributes: - nullable_keyword: OpenAPI 3.0 uses nullable: true (deprecated in 3.1). - discriminator_support: All OpenAPI versions support discriminator. + Each field includes metadata for documentation generation. + Use `dataclasses.fields(OpenAPISchemaFeatures)` to access metadata. """ - nullable_keyword: bool - discriminator_support: bool + nullable_keyword: bool = field( + default=False, + metadata=FeatureMetadata( + introduced="OAS 3.0", + doc_name="nullable", + description="Uses `nullable: true` for nullable types (deprecated in 3.1)", + status="supported", + ), + ) + discriminator_support: bool = field( + default=True, + metadata=FeatureMetadata( + introduced="OAS 3.0", + doc_name="discriminator", + description="Polymorphism support via `discriminator` keyword", + status="supported", + ), + ) @classmethod def from_openapi_version(cls, version: OpenAPIVersion) -> OpenAPISchemaFeatures: @@ -310,6 +401,7 @@ def get_data_formats(*, is_openapi: bool = False) -> DataFormatMapping: __all__ = [ "DataFormatMapping", + "FeatureMetadata", "JsonSchemaFeatures", "OpenAPISchemaFeatures", "SchemaFeaturesT",