phase 1
This commit is contained in:
@@ -12,7 +12,7 @@ from enum import Enum
|
||||
from pathlib import Path
|
||||
from typing import Any, List, get_type_hints
|
||||
|
||||
from ..helpers import get_origin_name, get_type_name, unwrap_optional
|
||||
from ..helpers import get_origin_name, get_type_name, is_dataclass_type, unwrap_optional
|
||||
from ..loader.schema import EnumDefinition, FieldDefinition, ModelDefinition
|
||||
from ..types import PYDANTIC_RESOLVERS
|
||||
from .base import BaseGenerator
|
||||
@@ -54,8 +54,9 @@ class PydanticGenerator(BaseGenerator):
|
||||
if hasattr(models, "get_shared_component"):
|
||||
content = self._generate_from_config(models)
|
||||
elif hasattr(models, "models"):
|
||||
all_models = models.models + getattr(models, "api_models", [])
|
||||
content = self._generate_from_definitions(
|
||||
models.models, getattr(models, "enums", [])
|
||||
all_models, getattr(models, "enums", [])
|
||||
)
|
||||
elif isinstance(models, tuple):
|
||||
content = self._generate_from_definitions(models[0], models[1])
|
||||
@@ -307,6 +308,11 @@ class PydanticGenerator(BaseGenerator):
|
||||
if isinstance(base, type) and issubclass(base, Enum)
|
||||
else None
|
||||
)
|
||||
or (
|
||||
PYDANTIC_RESOLVERS["dataclass"]
|
||||
if is_dataclass_type(base)
|
||||
else None
|
||||
)
|
||||
)
|
||||
result = resolver(base) if resolver else "str"
|
||||
return f"Optional[{result}]" if optional else result
|
||||
|
||||
@@ -8,7 +8,7 @@ from enum import Enum
|
||||
from pathlib import Path
|
||||
from typing import Any, List, get_type_hints
|
||||
|
||||
from ..helpers import get_origin_name, get_type_name, unwrap_optional
|
||||
from ..helpers import get_origin_name, get_type_name, is_dataclass_type, unwrap_optional
|
||||
from ..loader.schema import EnumDefinition, FieldDefinition, ModelDefinition
|
||||
from ..types import TS_RESOLVERS
|
||||
from .base import BaseGenerator
|
||||
@@ -139,6 +139,11 @@ class TypeScriptGenerator(BaseGenerator):
|
||||
if isinstance(base, type) and issubclass(base, Enum)
|
||||
else None
|
||||
)
|
||||
or (
|
||||
TS_RESOLVERS["dataclass"]
|
||||
if is_dataclass_type(base)
|
||||
else None
|
||||
)
|
||||
)
|
||||
|
||||
result = resolver(base) if resolver else "string"
|
||||
|
||||
@@ -44,6 +44,17 @@ def get_list_inner(type_hint: Any) -> str:
|
||||
return "str"
|
||||
|
||||
|
||||
def is_dataclass_type(type_hint: Any) -> bool:
|
||||
"""Check if type is a dataclass (nested model reference)."""
|
||||
return isinstance(type_hint, type) and dc.is_dataclass(type_hint)
|
||||
|
||||
|
||||
def get_list_inner_type(type_hint: Any) -> Any:
|
||||
"""Get the raw inner type of List[T] (not stringified)."""
|
||||
args = get_args(type_hint)
|
||||
return args[0] if args else None
|
||||
|
||||
|
||||
def get_field_default(field: dc.Field) -> Any:
|
||||
"""Get default value from dataclass field."""
|
||||
if field.default is not dc.MISSING:
|
||||
|
||||
@@ -123,6 +123,20 @@ class SchemaLoader:
|
||||
methods=grpc_service.get("methods", []),
|
||||
)
|
||||
|
||||
# Generic group loader: any include group not handled above
|
||||
# is looked up as UPPER_CASE attribute on the module.
|
||||
# e.g. include "detect_views" → module.DETECT_VIEWS
|
||||
if include:
|
||||
known_groups = {"dataclasses", "enums", "api", "views", "grpc"}
|
||||
for group in include - known_groups:
|
||||
attr_name = group.upper()
|
||||
items = getattr(module, attr_name, [])
|
||||
for cls in items:
|
||||
if isinstance(cls, type) and dc.is_dataclass(cls):
|
||||
self.api_models.append(self._parse_dataclass(cls))
|
||||
elif isinstance(cls, type) and issubclass(cls, Enum):
|
||||
self.enums.append(self._parse_enum(cls))
|
||||
|
||||
return self
|
||||
|
||||
def _import_module(self, path: Path):
|
||||
|
||||
@@ -5,6 +5,7 @@ Type mappings for each output format.
|
||||
Used by generators to convert Python types to target framework types.
|
||||
"""
|
||||
|
||||
import dataclasses as dc
|
||||
from typing import Any, Callable, get_args
|
||||
|
||||
# =============================================================================
|
||||
@@ -39,8 +40,12 @@ DJANGO_SPECIAL: dict[str, str] = {
|
||||
def _get_list_inner(type_hint: Any) -> str:
|
||||
"""Get inner type of List[T] for Pydantic."""
|
||||
args = get_args(type_hint)
|
||||
if args and args[0] in (str, int, float, bool):
|
||||
return {str: "str", int: "int", float: "float", bool: "bool"}[args[0]]
|
||||
if args:
|
||||
inner = args[0]
|
||||
if inner in (str, int, float, bool):
|
||||
return {str: "str", int: "int", float: "float", bool: "bool"}[inner]
|
||||
if isinstance(inner, type) and dc.is_dataclass(inner):
|
||||
return inner.__name__
|
||||
return "str"
|
||||
|
||||
|
||||
@@ -54,6 +59,7 @@ PYDANTIC_RESOLVERS: dict[Any, Callable[[Any], str]] = {
|
||||
"dict": lambda _: "Dict[str, Any]",
|
||||
"list": lambda base: f"List[{_get_list_inner(base)}]",
|
||||
"enum": lambda base: base.__name__,
|
||||
"dataclass": lambda base: base.__name__,
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
@@ -72,6 +78,8 @@ def _resolve_ts_list(base: Any) -> str:
|
||||
return "number[]"
|
||||
elif inner is bool:
|
||||
return "boolean[]"
|
||||
elif isinstance(inner, type) and dc.is_dataclass(inner):
|
||||
return f"{inner.__name__}[]"
|
||||
return "string[]"
|
||||
|
||||
|
||||
@@ -85,6 +93,7 @@ TS_RESOLVERS: dict[Any, Callable[[Any], str]] = {
|
||||
"dict": lambda _: "Record<string, unknown>",
|
||||
"list": _resolve_ts_list,
|
||||
"enum": lambda base: base.__name__,
|
||||
"dataclass": lambda base: base.__name__,
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
|
||||
Reference in New Issue
Block a user