diff --git a/generators/python/src/fern_python/generators/pydantic_model/type_declaration_handler/abc/abstract_type_snippet_generator.py b/generators/python/src/fern_python/generators/pydantic_model/type_declaration_handler/abc/abstract_type_snippet_generator.py index f7dc1a833bab..d1831684a2bf 100644 --- a/generators/python/src/fern_python/generators/pydantic_model/type_declaration_handler/abc/abstract_type_snippet_generator.py +++ b/generators/python/src/fern_python/generators/pydantic_model/type_declaration_handler/abc/abstract_type_snippet_generator.py @@ -1,9 +1,12 @@ from abc import ABC, abstractmethod -from typing import Optional +from typing import Optional, TYPE_CHECKING from fern_python.codegen import AST from fern_python.snippet import SnippetWriter +if TYPE_CHECKING: + from fern_python.snippet.recursion_guard import RecursionGuard + class AbstractTypeSnippetGenerator(ABC): def __init__( @@ -13,4 +16,4 @@ def __init__( self.snippet_writer = snippet_writer @abstractmethod - def generate_snippet(self) -> Optional[AST.Expression]: ... + def generate_snippet(self, recursion_guard: Optional["RecursionGuard"] = None) -> Optional[AST.Expression]: ... diff --git a/generators/python/src/fern_python/generators/pydantic_model/type_declaration_handler/alias_generator.py b/generators/python/src/fern_python/generators/pydantic_model/type_declaration_handler/alias_generator.py index 5df7068f4cd3..ee14cf9bdd43 100644 --- a/generators/python/src/fern_python/generators/pydantic_model/type_declaration_handler/alias_generator.py +++ b/generators/python/src/fern_python/generators/pydantic_model/type_declaration_handler/alias_generator.py @@ -1,5 +1,5 @@ from abc import ABC -from typing import Optional +from typing import Optional, TYPE_CHECKING from ...context.pydantic_generator_context import PydanticGeneratorContext from ..custom_config import PydanticModelCustomConfig @@ -12,6 +12,9 @@ import fern.ir.resources as ir_types +if TYPE_CHECKING: + from fern_python.snippet.recursion_guard import RecursionGuard + class AbstractAliasGenerator(AbstractTypeGenerator, ABC): def __init__( @@ -52,9 +55,10 @@ def __init__( self.as_request = as_request self.example = example - def generate_snippet(self) -> Optional[AST.Expression]: + def generate_snippet(self, recursion_guard: Optional["RecursionGuard"] = None) -> Optional[AST.Expression]: return self.snippet_writer.get_snippet_for_example_type_reference( example_type_reference=self.example.value, use_typeddict_request=self.use_typeddict_request, as_request=self.as_request, + recursion_guard=recursion_guard, ) diff --git a/generators/python/src/fern_python/generators/pydantic_model/type_declaration_handler/enum_generator.py b/generators/python/src/fern_python/generators/pydantic_model/type_declaration_handler/enum_generator.py index bdf5991b17fb..9afa6fe11323 100644 --- a/generators/python/src/fern_python/generators/pydantic_model/type_declaration_handler/enum_generator.py +++ b/generators/python/src/fern_python/generators/pydantic_model/type_declaration_handler/enum_generator.py @@ -1,4 +1,4 @@ -from typing import Optional, Union +from typing import Optional, Union, TYPE_CHECKING from ...context.pydantic_generator_context import PydanticGeneratorContext from ..custom_config import PydanticModelCustomConfig @@ -12,6 +12,9 @@ import fern.ir.resources as ir_types +if TYPE_CHECKING: + from fern_python.snippet.recursion_guard import RecursionGuard + # Note enums are the same for both pydantic models and typeddicts os the generator is not multiplexed class EnumGenerator(AbstractTypeGenerator): @@ -166,7 +169,7 @@ def __init__( self.name = name self.example = example.value if isinstance(example, ir_types.ExampleEnumType) else example - def generate_snippet(self) -> AST.Expression: + def generate_snippet(self, recursion_guard: Optional["RecursionGuard"] = None) -> AST.Expression: class_reference = self.snippet_writer.get_class_reference_for_declared_type_name( name=self.name, as_request=False, diff --git a/generators/python/src/fern_python/generators/pydantic_model/type_declaration_handler/pydantic_models/pydantic_model_object_generator.py b/generators/python/src/fern_python/generators/pydantic_model/type_declaration_handler/pydantic_models/pydantic_model_object_generator.py index f8717f96b8f8..3598ca3b0ce6 100644 --- a/generators/python/src/fern_python/generators/pydantic_model/type_declaration_handler/pydantic_models/pydantic_model_object_generator.py +++ b/generators/python/src/fern_python/generators/pydantic_model/type_declaration_handler/pydantic_models/pydantic_model_object_generator.py @@ -1,4 +1,4 @@ -from typing import List, Optional +from typing import List, Optional, TYPE_CHECKING from ....context.pydantic_generator_context import PydanticGeneratorContext from ...custom_config import PydanticModelCustomConfig @@ -13,6 +13,9 @@ import fern.ir.resources as ir_types +if TYPE_CHECKING: + from fern_python.snippet.recursion_guard import RecursionGuard + class PydanticModelObjectGenerator(AbstractObjectGenerator): def __init__( @@ -97,7 +100,7 @@ def __init__( example=example, ) - def generate_snippet(self) -> AST.Expression: + def generate_snippet(self, recursion_guard: Optional["RecursionGuard"] = None) -> AST.Expression: return AST.Expression( AST.ClassInstantiation( class_=self.snippet_writer.get_class_reference_for_declared_type_name( @@ -110,6 +113,7 @@ def generate_snippet(self) -> AST.Expression: use_typeddict_request=False, as_request=False, in_typeddict=False, + recursion_guard=recursion_guard, ), ), ) diff --git a/generators/python/src/fern_python/generators/pydantic_model/type_declaration_handler/type_declaration_snippet_generator_builder.py b/generators/python/src/fern_python/generators/pydantic_model/type_declaration_handler/type_declaration_snippet_generator_builder.py index e91d63a9ed72..da34cda26f3b 100644 --- a/generators/python/src/fern_python/generators/pydantic_model/type_declaration_handler/type_declaration_snippet_generator_builder.py +++ b/generators/python/src/fern_python/generators/pydantic_model/type_declaration_handler/type_declaration_snippet_generator_builder.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Optional, TYPE_CHECKING from ...context.pydantic_generator_context import PydanticGeneratorContext from .enum_generator import EnumSnippetGenerator @@ -27,6 +27,9 @@ import fern.ir.resources as ir_types +if TYPE_CHECKING: + from fern_python.snippet.recursion_guard import RecursionGuard + class TypeDeclarationSnippetGeneratorBuilder: def __init__( @@ -41,50 +44,50 @@ def get_generator( self, ) -> TypeDeclarationSnippetGenerator: return TypeDeclarationSnippetGenerator( - alias=lambda example: self._get_alias_snippet_generator(example), - enum=lambda name, example: EnumSnippetGenerator( + alias=lambda example, recursion_guard=None: self._get_alias_snippet_generator(example, recursion_guard), + enum=lambda name, example, recursion_guard=None: EnumSnippetGenerator( snippet_writer=self._snippet_writer, name=name, example=example, use_str_enums=self._context.use_str_enums, - ).generate_snippet(), - object=lambda name, example: self._get_object_snippet_generator(name, example), - discriminated_union=lambda name, example: self._get_discriminated_union_snippet_generator(name, example), - undiscriminated_union=lambda name, example: self._get_undiscriminated_union_snippet_generator( - name, example + ).generate_snippet(recursion_guard), + object=lambda name, example, recursion_guard=None: self._get_object_snippet_generator(name, example, recursion_guard), + discriminated_union=lambda name, example, recursion_guard=None: self._get_discriminated_union_snippet_generator(name, example, recursion_guard), + undiscriminated_union=lambda name, example, recursion_guard=None: self._get_undiscriminated_union_snippet_generator( + name, example, recursion_guard ), ) - def _get_alias_snippet_generator(self, example: ir_types.ExampleAliasType) -> Optional[AST.Expression]: + def _get_alias_snippet_generator(self, example: ir_types.ExampleAliasType, recursion_guard: Optional["RecursionGuard"] = None) -> Optional[AST.Expression]: if self._context.use_typeddict_requests: return TypedDictAliasSnippetGenerator( snippet_writer=self._snippet_writer, example=example, - ).generate_snippet() + ).generate_snippet(recursion_guard) return PydanticModelAliasSnippetGenerator( snippet_writer=self._snippet_writer, example=example, - ).generate_snippet() + ).generate_snippet(recursion_guard) def _get_object_snippet_generator( - self, name: ir_types.DeclaredTypeName, example: ir_types.ExampleObjectType + self, name: ir_types.DeclaredTypeName, example: ir_types.ExampleObjectType, recursion_guard: Optional["RecursionGuard"] = None ) -> AST.Expression: if self._context.use_typeddict_requests: return TypeddictObjectSnippetGenerator( name=name, snippet_writer=self._snippet_writer, example=example, - ).generate_snippet() + ).generate_snippet(recursion_guard) return PydanticModelObjectSnippetGenerator( name=name, snippet_writer=self._snippet_writer, example=example, - ).generate_snippet() + ).generate_snippet(recursion_guard) def _get_discriminated_union_snippet_generator( - self, name: ir_types.DeclaredTypeName, example: ir_types.ExampleUnionType + self, name: ir_types.DeclaredTypeName, example: ir_types.ExampleUnionType, recursion_guard: Optional["RecursionGuard"] = None ) -> AST.Expression: if self._context.use_typeddict_requests: return TypeddictDiscriminatedUnionSnippetGenerator( @@ -92,27 +95,27 @@ def _get_discriminated_union_snippet_generator( snippet_writer=self._snippet_writer, example=example, union_naming_version=self._context.union_naming_version, - ).generate_snippet() + ).generate_snippet(recursion_guard) return PydanticModelDiscriminatedUnionSnippetGenerator( name=name, snippet_writer=self._snippet_writer, example=example, union_naming_version=self._context.union_naming_version, - ).generate_snippet() + ).generate_snippet(recursion_guard) def _get_undiscriminated_union_snippet_generator( - self, name: ir_types.DeclaredTypeName, example: ir_types.ExampleUndiscriminatedUnionType + self, name: ir_types.DeclaredTypeName, example: ir_types.ExampleUndiscriminatedUnionType, recursion_guard: Optional["RecursionGuard"] = None ) -> Optional[AST.Expression]: if self._context.use_typeddict_requests: return TypeddictUndiscriminatedUnionSnippetGenerator( name=name, snippet_writer=self._snippet_writer, example=example, - ).generate_snippet() + ).generate_snippet(recursion_guard) return PydanticModelUndiscriminatedUnionSnippetGenerator( name=name, snippet_writer=self._snippet_writer, example=example, - ).generate_snippet() + ).generate_snippet(recursion_guard) diff --git a/generators/python/src/fern_python/generators/pydantic_model/type_declaration_handler/typeddicts/typeddict_object_generator.py b/generators/python/src/fern_python/generators/pydantic_model/type_declaration_handler/typeddicts/typeddict_object_generator.py index 9fd383cb7f70..faccb28ead43 100644 --- a/generators/python/src/fern_python/generators/pydantic_model/type_declaration_handler/typeddicts/typeddict_object_generator.py +++ b/generators/python/src/fern_python/generators/pydantic_model/type_declaration_handler/typeddicts/typeddict_object_generator.py @@ -1,4 +1,4 @@ -from typing import List, Optional +from typing import List, Optional, TYPE_CHECKING from ....context.pydantic_generator_context import PydanticGeneratorContext from ...custom_config import PydanticModelCustomConfig @@ -13,6 +13,9 @@ import fern.ir.resources as ir_types +if TYPE_CHECKING: + from fern_python.snippet.recursion_guard import RecursionGuard + class TypeddictObjectGenerator(AbstractObjectGenerator): def __init__( @@ -72,5 +75,5 @@ def __init__( example=example, ) - def generate_snippet(self) -> AST.Expression: - return FernTypedDict.type_to_snippet(example=self.example, snippet_writer=self.snippet_writer) + def generate_snippet(self, recursion_guard: Optional["RecursionGuard"] = None) -> AST.Expression: + return FernTypedDict.type_to_snippet(example=self.example, snippet_writer=self.snippet_writer, recursion_guard=recursion_guard) diff --git a/generators/python/src/fern_python/generators/pydantic_model/type_declaration_handler/undiscriminated_union_generator.py b/generators/python/src/fern_python/generators/pydantic_model/type_declaration_handler/undiscriminated_union_generator.py index 6ed13b0d7ff9..aa3f7e88a91a 100644 --- a/generators/python/src/fern_python/generators/pydantic_model/type_declaration_handler/undiscriminated_union_generator.py +++ b/generators/python/src/fern_python/generators/pydantic_model/type_declaration_handler/undiscriminated_union_generator.py @@ -1,6 +1,6 @@ from abc import ABC from dataclasses import dataclass -from typing import List, Optional +from typing import List, Optional, TYPE_CHECKING from ...context.pydantic_generator_context import PydanticGeneratorContext from ..custom_config import PydanticModelCustomConfig @@ -13,6 +13,9 @@ import fern.ir.resources as ir_types +if TYPE_CHECKING: + from fern_python.snippet.recursion_guard import RecursionGuard + @dataclass(frozen=True) class CycleAwareMemberType: @@ -71,9 +74,10 @@ def __init__( self.as_request = as_request self.use_typeddict_request = use_typeddict_request - def generate_snippet(self) -> Optional[AST.Expression]: + def generate_snippet(self, recursion_guard: Optional["RecursionGuard"] = None) -> Optional[AST.Expression]: return self.snippet_writer.get_snippet_for_example_type_reference( example_type_reference=self.example.single_union_type, use_typeddict_request=self.use_typeddict_request, as_request=self.as_request, + recursion_guard=recursion_guard, ) diff --git a/generators/python/src/fern_python/generators/pydantic_model/typeddict.py b/generators/python/src/fern_python/generators/pydantic_model/typeddict.py index 2565176f103a..214086aed2aa 100644 --- a/generators/python/src/fern_python/generators/pydantic_model/typeddict.py +++ b/generators/python/src/fern_python/generators/pydantic_model/typeddict.py @@ -2,7 +2,7 @@ from dataclasses import dataclass from types import TracebackType -from typing import Dict, List, Optional, Sequence, Type +from typing import Dict, List, Optional, Sequence, Type, TYPE_CHECKING from ..context.pydantic_generator_context import PydanticGeneratorContext from fern_python.codegen import AST, SourceFile @@ -16,6 +16,9 @@ import fern.ir.resources as ir_types +if TYPE_CHECKING: + from fern_python.snippet.recursion_guard import RecursionGuard + TYPING_EXTENSIONS_MODULE = AST.Module.external( module_path=("typing_extensions",), dependency=TYPING_EXTENSIONS_DEPENDENCY, @@ -203,7 +206,7 @@ def wrap_string_as_example(cls, string: str) -> ir_types.ExampleTypeReference: @classmethod def snippet_from_properties( - cls, example_properties: List[SimpleObjectProperty], snippet_writer: SnippetWriter + cls, example_properties: List[SimpleObjectProperty], snippet_writer: SnippetWriter, recursion_guard: Optional["RecursionGuard"] = None ) -> AST.Expression: example_dict_pairs: List[ir_types.ExampleKeyValuePair] = [] for property in example_properties: @@ -219,7 +222,7 @@ def snippet_from_properties( ) ) return snippet_writer._get_snippet_for_map( - example_dict_pairs, use_typeddict_request=True, as_request=True, in_typeddict=True + example_dict_pairs, use_typeddict_request=True, as_request=True, in_typeddict=True, recursion_guard=recursion_guard ) @classmethod @@ -228,6 +231,7 @@ def type_to_snippet( example: ir_types.ExampleObjectType, snippet_writer: SnippetWriter, additional_properties: List[SimpleObjectProperty] = [], + recursion_guard: Optional["RecursionGuard"] = None, ) -> AST.Expression: example_properties = [ SimpleObjectProperty( @@ -240,6 +244,7 @@ def type_to_snippet( return cls.snippet_from_properties( example_properties=example_properties, snippet_writer=snippet_writer, + recursion_guard=recursion_guard, ) @classmethod diff --git a/generators/python/src/fern_python/snippet/recursion_guard.py b/generators/python/src/fern_python/snippet/recursion_guard.py new file mode 100644 index 000000000000..41c9de12b5ee --- /dev/null +++ b/generators/python/src/fern_python/snippet/recursion_guard.py @@ -0,0 +1,50 @@ +from typing import Optional, Set +import fern.ir.resources as ir_types + + +class RecursionGuard: + """ + Guards against infinite recursion when generating examples for types with circular references. + Tracks visited types on the current recursion path and enforces a maximum depth limit. + """ + + def __init__(self, max_depth: int = 5): + self._visited: Set[str] = set() + self._depth: int = 0 + self._max_depth: int = max_depth + + def _get_type_key(self, type_name: ir_types.DeclaredTypeName) -> str: + """Generate a unique key for a type based on its package path and name.""" + fern_filepath = ".".join(type_name.fern_filepath.package_path.parts) if type_name.fern_filepath.package_path.parts else "" + return f"{fern_filepath}:{type_name.name.original_name}" + + def can_recurse(self, type_name: ir_types.DeclaredTypeName) -> bool: + """ + Check if we can safely recurse into the given type. + Returns False if the type is already on the recursion stack or if max depth is exceeded. + """ + if self._depth >= self._max_depth: + return False + + type_key = self._get_type_key(type_name) + return type_key not in self._visited + + def enter(self, type_name: ir_types.DeclaredTypeName) -> "RecursionGuard": + """ + Enter a new recursion level for the given type. + Returns a new RecursionGuard with the type added to the visited set. + """ + new_guard = RecursionGuard(max_depth=self._max_depth) + new_guard._visited = self._visited.copy() + new_guard._visited.add(self._get_type_key(type_name)) + new_guard._depth = self._depth + 1 + return new_guard + + def with_depth_increment(self) -> "RecursionGuard": + """ + Increment depth without adding to visited set (for containers like lists/maps). + """ + new_guard = RecursionGuard(max_depth=self._max_depth) + new_guard._visited = self._visited.copy() + new_guard._depth = self._depth + 1 + return new_guard diff --git a/generators/python/src/fern_python/snippet/snippet_writer.py b/generators/python/src/fern_python/snippet/snippet_writer.py index 009a6edfb5d1..715399bbba9f 100644 --- a/generators/python/src/fern_python/snippet/snippet_writer.py +++ b/generators/python/src/fern_python/snippet/snippet_writer.py @@ -1,6 +1,7 @@ from typing import Any, Dict, List, Optional from .type_declaration_snippet_generator import TypeDeclarationSnippetGenerator +from .recursion_guard import RecursionGuard from fern_python.codegen import AST from fern_python.generators.context.pydantic_generator_context import PydanticGeneratorContext @@ -22,13 +23,20 @@ def get_snippet_for_example_type_shape( self, name: ir_types.DeclaredTypeName, example_type_shape: ir_types.ExampleTypeShape, + recursion_guard: Optional[RecursionGuard] = None, ) -> Optional[AST.Expression]: if self._type_declaration_snippet_generator is None: return None + guard = recursion_guard or RecursionGuard() + + if not guard.can_recurse(name): + return None + return self._type_declaration_snippet_generator.generate_snippet( name=name, example=example_type_shape, + recursion_guard=guard.enter(name), ) def get_class_reference_for_declared_type_name( @@ -66,8 +74,10 @@ def get_snippet_for_example_type_reference( as_request: bool, in_typeddict: bool = False, force_include_literals: bool = False, + recursion_guard: Optional[RecursionGuard] = None, ) -> Optional[AST.Expression]: unwrapped_reference = self._context.unwrap_example_type_reference(example_type_reference) + guard = recursion_guard or RecursionGuard() return unwrapped_reference.shape.visit( primitive=lambda primitive: self._get_snippet_for_primitive( @@ -79,6 +89,7 @@ def get_snippet_for_example_type_reference( as_request=as_request, in_typeddict=in_typeddict, force_include_literals=force_include_literals, + recursion_guard=guard, ), unknown=lambda unknown: self._get_snippet_for_unknown( unknown=unknown, @@ -86,6 +97,7 @@ def get_snippet_for_example_type_reference( named=lambda named: self.get_snippet_for_example_type_shape( name=named.type_name, example_type_shape=named.shape, + recursion_guard=guard, ), ) @@ -96,8 +108,10 @@ def get_snippet_for_object_properties( in_typeddict: bool, use_typeddict_request: bool, as_request: bool, + recursion_guard: Optional[RecursionGuard] = None, ) -> List[AST.Expression]: args: List[AST.Expression] = [] + guard = recursion_guard or RecursionGuard() for property in example.properties: value = property.value.shape.visit( primitive=lambda primitive: self._get_snippet_for_primitive( @@ -108,6 +122,7 @@ def get_snippet_for_object_properties( use_typeddict_request=use_typeddict_request, as_request=as_request, in_typeddict=in_typeddict, + recursion_guard=guard, ), unknown=lambda unknown: self._get_snippet_for_unknown( unknown=unknown, @@ -115,6 +130,7 @@ def get_snippet_for_object_properties( named=lambda named: self.get_snippet_for_example_type_shape( name=named.type_name, example_type_shape=named.shape, + recursion_guard=guard, ), ) if value is not None: @@ -221,7 +237,9 @@ def _get_snippet_for_container( use_typeddict_request: bool, as_request: bool, force_include_literals: bool = False, + recursion_guard: Optional[RecursionGuard] = None, ) -> Optional[AST.Expression]: + guard = recursion_guard or RecursionGuard() return container.visit( list_=lambda list: self._get_snippet_for_list_or_set( example_type_references=list.list_, @@ -229,6 +247,7 @@ def _get_snippet_for_container( in_typeddict=in_typeddict, use_typeddict_request=use_typeddict_request, as_request=as_request, + recursion_guard=guard, ), set_=lambda set: self._get_snippet_for_list_or_set( example_type_references=set.set_, @@ -236,12 +255,14 @@ def _get_snippet_for_container( in_typeddict=in_typeddict, use_typeddict_request=use_typeddict_request, as_request=as_request, + recursion_guard=guard, ), optional=lambda optional: self.get_snippet_for_example_type_reference( example_type_reference=optional.optional, use_typeddict_request=use_typeddict_request, as_request=as_request, in_typeddict=in_typeddict, + recursion_guard=guard, ) if optional.optional is not None else None, @@ -250,6 +271,7 @@ def _get_snippet_for_container( use_typeddict_request=use_typeddict_request, as_request=as_request, in_typeddict=in_typeddict, + recursion_guard=guard, ) if nullable.nullable is not None else None, @@ -258,6 +280,7 @@ def _get_snippet_for_container( use_typeddict_request=use_typeddict_request, as_request=as_request, in_typeddict=in_typeddict, + recursion_guard=guard, ), literal=lambda lit: self._get_snippet_for_primitive(lit.literal) if in_typeddict or force_include_literals @@ -284,8 +307,10 @@ def _get_snippet_for_list_or_set( in_typeddict: bool, use_typeddict_request: bool, as_request: bool, + recursion_guard: Optional[RecursionGuard] = None, ) -> Optional[AST.Expression]: values: List[AST.Expression] = [] + guard = recursion_guard or RecursionGuard() # We use lists for sets if the inner type is non-primitive because Pydantic models aren't hashable contents_are_primitive = False for example_type_reference in example_type_references: @@ -300,6 +325,7 @@ def _get_snippet_for_list_or_set( use_typeddict_request=use_typeddict_request, as_request=as_request, in_typeddict=in_typeddict, + recursion_guard=guard, ) if expression is not None: values.append(expression) @@ -313,21 +339,25 @@ def _get_snippet_for_map( in_typeddict: bool, use_typeddict_request: bool, as_request: bool, + recursion_guard: Optional[RecursionGuard] = None, ) -> AST.Expression: keys: List[AST.Expression] = [] values: List[AST.Expression] = [] + guard = recursion_guard or RecursionGuard() for pair in pairs: key = self.get_snippet_for_example_type_reference( example_type_reference=pair.key, use_typeddict_request=use_typeddict_request, as_request=as_request, in_typeddict=in_typeddict, + recursion_guard=guard, ) value = self.get_snippet_for_example_type_reference( example_type_reference=pair.value, use_typeddict_request=use_typeddict_request, as_request=as_request, in_typeddict=in_typeddict, + recursion_guard=guard, ) if key is not None and value is not None: keys.append(key) diff --git a/generators/python/src/fern_python/snippet/type_declaration_snippet_generator.py b/generators/python/src/fern_python/snippet/type_declaration_snippet_generator.py index d762d26a3cf8..af9d66dae638 100644 --- a/generators/python/src/fern_python/snippet/type_declaration_snippet_generator.py +++ b/generators/python/src/fern_python/snippet/type_declaration_snippet_generator.py @@ -4,12 +4,16 @@ import fern.ir.resources as ir_types -AliasSnippetGenerator = Callable[[ir_types.ExampleAliasType], Optional[AST.Expression]] -EnumSnippetGenerator = Callable[[ir_types.DeclaredTypeName, ir_types.ExampleEnumType], AST.Expression] -ObjectSnippetGenerator = Callable[[ir_types.DeclaredTypeName, ir_types.ExampleObjectType], AST.Expression] -DiscriminatedUnionGenerator = Callable[[ir_types.DeclaredTypeName, ir_types.ExampleUnionType], AST.Expression] +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from .recursion_guard import RecursionGuard + +AliasSnippetGenerator = Callable[[ir_types.ExampleAliasType, Optional["RecursionGuard"]], Optional[AST.Expression]] +EnumSnippetGenerator = Callable[[ir_types.DeclaredTypeName, ir_types.ExampleEnumType, Optional["RecursionGuard"]], AST.Expression] +ObjectSnippetGenerator = Callable[[ir_types.DeclaredTypeName, ir_types.ExampleObjectType, Optional["RecursionGuard"]], AST.Expression] +DiscriminatedUnionGenerator = Callable[[ir_types.DeclaredTypeName, ir_types.ExampleUnionType, Optional["RecursionGuard"]], AST.Expression] UndiscriminatedUnionGenerator = Callable[ - [ir_types.DeclaredTypeName, ir_types.ExampleUndiscriminatedUnionType], Optional[AST.Expression] + [ir_types.DeclaredTypeName, ir_types.ExampleUndiscriminatedUnionType, Optional["RecursionGuard"]], Optional[AST.Expression] ] @@ -32,11 +36,12 @@ def generate_snippet( self, name: ir_types.DeclaredTypeName, example: ir_types.ExampleTypeShape, + recursion_guard: Optional["RecursionGuard"] = None, ) -> Optional[AST.Expression]: return example.visit( - alias=lambda alias: self._generate_alias(alias), - enum=lambda enum: self._generate_enum(name, enum), - object=lambda object_: self._generate_object(name, object_), - union=lambda union: self._generate_discriminated_union(name, union), - undiscriminated_union=lambda union: self._generate_undiscriminated_union(name, union), + alias=lambda alias: self._generate_alias(alias, recursion_guard), + enum=lambda enum: self._generate_enum(name, enum, recursion_guard), + object=lambda object_: self._generate_object(name, object_, recursion_guard), + union=lambda union: self._generate_discriminated_union(name, union, recursion_guard), + undiscriminated_union=lambda union: self._generate_undiscriminated_union(name, union, recursion_guard), ) diff --git a/generators/typescript/model/type-generator/src/AbstractGeneratedType.ts b/generators/typescript/model/type-generator/src/AbstractGeneratedType.ts index 3e9b55ca8ef1..5b114b9a75b0 100644 --- a/generators/typescript/model/type-generator/src/AbstractGeneratedType.ts +++ b/generators/typescript/model/type-generator/src/AbstractGeneratedType.ts @@ -1,6 +1,7 @@ import { ExampleType, ExampleTypeShape, FernFilepath } from "@fern-fern/ir-sdk/api"; import { GetReferenceOpts, getTextOfTsNode, Reference } from "@fern-typescript/commons"; import { BaseContext, BaseGeneratedType } from "@fern-typescript/contexts"; +import { RecursionGuard } from "@fern-typescript/type-reference-example-generator"; import { ModuleDeclarationStructure, StatementStructures, ts, WriterFunction } from "ts-morph"; export declare namespace AbstractGeneratedType { @@ -108,5 +109,5 @@ export abstract class AbstractGeneratedType responseTypeNode: ts.TypeNode | undefined; }; public abstract generateModule(context: Context): ModuleDeclarationStructure | undefined; - public abstract buildExample(example: ExampleTypeShape, context: Context, opts: GetReferenceOpts): ts.Expression; + public abstract buildExample(example: ExampleTypeShape, context: Context, opts: GetReferenceOpts, recursionGuard?: RecursionGuard): ts.Expression; } diff --git a/generators/typescript/model/type-generator/src/alias/GeneratedAliasTypeImpl.ts b/generators/typescript/model/type-generator/src/alias/GeneratedAliasTypeImpl.ts index a38294a489c2..9a74155facfc 100644 --- a/generators/typescript/model/type-generator/src/alias/GeneratedAliasTypeImpl.ts +++ b/generators/typescript/model/type-generator/src/alias/GeneratedAliasTypeImpl.ts @@ -73,7 +73,7 @@ export class GeneratedAliasTypeImpl }); } - public buildExample(example: ExampleTypeShape, context: Context, opts: GetReferenceOpts): ts.Expression { + public buildExample(example: ExampleTypeShape, context: Context, opts: GetReferenceOpts, recursionGuard?: RecursionGuard): ts.Expression { if (example.type !== "alias") { throw new Error("Example is not for an alias"); } diff --git a/generators/typescript/model/type-generator/src/alias/GeneratedBrandedStringAliasImpl.ts b/generators/typescript/model/type-generator/src/alias/GeneratedBrandedStringAliasImpl.ts index 2edfb3a717ee..7dc6f1e19523 100644 --- a/generators/typescript/model/type-generator/src/alias/GeneratedBrandedStringAliasImpl.ts +++ b/generators/typescript/model/type-generator/src/alias/GeneratedBrandedStringAliasImpl.ts @@ -54,7 +54,7 @@ export class GeneratedBrandedStringAliasImpl return this.getReferenceToSelf(context).getExpression(opts); } - public buildExample(example: ExampleTypeShape, context: Context, opts: GetReferenceOpts): ts.Expression { + public buildExample(example: ExampleTypeShape, context: Context, opts: GetReferenceOpts, recursionGuard?: RecursionGuard): ts.Expression { if (example.type !== "alias") { throw new Error("Example is not for an alias"); } diff --git a/generators/typescript/model/type-generator/src/enum/GeneratedEnumTypeImpl.ts b/generators/typescript/model/type-generator/src/enum/GeneratedEnumTypeImpl.ts index 8684c8e7ed9b..888be83b63e3 100644 --- a/generators/typescript/model/type-generator/src/enum/GeneratedEnumTypeImpl.ts +++ b/generators/typescript/model/type-generator/src/enum/GeneratedEnumTypeImpl.ts @@ -275,7 +275,7 @@ export class GeneratedEnumTypeImpl return enumModule; } - public buildExample(example: ExampleTypeShape, context: Context, opts: GetReferenceOpts): ts.Expression { + public buildExample(example: ExampleTypeShape, context: Context, opts: GetReferenceOpts, recursionGuard?: RecursionGuard): ts.Expression { if (example.type !== "enum") { throw new Error("Example is not for an enum"); } diff --git a/generators/typescript/model/type-generator/src/object/GeneratedObjectTypeImpl.ts b/generators/typescript/model/type-generator/src/object/GeneratedObjectTypeImpl.ts index d1558d0a1065..0cf9e0f3918d 100644 --- a/generators/typescript/model/type-generator/src/object/GeneratedObjectTypeImpl.ts +++ b/generators/typescript/model/type-generator/src/object/GeneratedObjectTypeImpl.ts @@ -253,7 +253,7 @@ export class GeneratedObjectTypeImpl } } - public buildExample(example: ExampleTypeShape, context: Context, opts: GetReferenceOpts): ts.Expression { + public buildExample(example: ExampleTypeShape, context: Context, opts: GetReferenceOpts, recursionGuard?: RecursionGuard): ts.Expression { if (example.type !== "object") { throw new Error("Example is not for an object"); } diff --git a/generators/typescript/model/type-generator/src/undiscriminated-union/GeneratedUndiscriminatedUnionTypeImpl.ts b/generators/typescript/model/type-generator/src/undiscriminated-union/GeneratedUndiscriminatedUnionTypeImpl.ts index 8e07940fdabc..8e74b84bfb33 100644 --- a/generators/typescript/model/type-generator/src/undiscriminated-union/GeneratedUndiscriminatedUnionTypeImpl.ts +++ b/generators/typescript/model/type-generator/src/undiscriminated-union/GeneratedUndiscriminatedUnionTypeImpl.ts @@ -175,7 +175,7 @@ export class GeneratedUndiscriminatedUnionTypeImpl return this.getTypeReferenceNode(context, member).typeNode; } - public buildExample(example: ExampleTypeShape, context: Context, opts: GetReferenceOpts): ts.Expression { + public buildExample(example: ExampleTypeShape, context: Context, opts: GetReferenceOpts, recursionGuard?: RecursionGuard): ts.Expression { if (example.type !== "undiscriminatedUnion") { throw new Error("Example is not for an undiscriminated union"); } diff --git a/generators/typescript/model/type-generator/src/union/GeneratedUnionTypeImpl.ts b/generators/typescript/model/type-generator/src/union/GeneratedUnionTypeImpl.ts index c105dbeb810c..994bf26feb00 100644 --- a/generators/typescript/model/type-generator/src/union/GeneratedUnionTypeImpl.ts +++ b/generators/typescript/model/type-generator/src/union/GeneratedUnionTypeImpl.ts @@ -119,7 +119,7 @@ export class GeneratedUnionTypeImpl } } - public buildExample(example: ExampleTypeShape, context: Context, opts: GetReferenceOpts): ts.Expression { + public buildExample(example: ExampleTypeShape, context: Context, opts: GetReferenceOpts, recursionGuard?: RecursionGuard): ts.Expression { if (example.type !== "union") { throw new Error("Example is not for an union"); } diff --git a/generators/typescript/model/type-reference-example-generator/src/GeneratedTypeReferenceExampleImpl.ts b/generators/typescript/model/type-reference-example-generator/src/GeneratedTypeReferenceExampleImpl.ts index 2d9476e4f16b..419699af96c6 100644 --- a/generators/typescript/model/type-reference-example-generator/src/GeneratedTypeReferenceExampleImpl.ts +++ b/generators/typescript/model/type-reference-example-generator/src/GeneratedTypeReferenceExampleImpl.ts @@ -10,6 +10,7 @@ import { import { GetReferenceOpts, isExpressionUndefined } from "@fern-typescript/commons"; import { BaseContext, GeneratedTypeReferenceExample } from "@fern-typescript/contexts"; import { ts } from "ts-morph"; +import { RecursionGuard, RecursionGuardImpl } from "./RecursionGuard"; export declare namespace GeneratedTypeReferenceExampleImpl { export interface Init { @@ -31,18 +32,21 @@ export class GeneratedTypeReferenceExampleImpl implements GeneratedTypeReference } public build(context: BaseContext, opts: GetReferenceOpts): ts.Expression { - return this.buildExample({ example: this.example, context, opts }); + return this.buildExample({ example: this.example, context, opts, recursionGuard: new RecursionGuardImpl() }); } private buildExample({ example, context, - opts + opts, + recursionGuard }: { example: ExampleTypeReference; context: BaseContext; opts: GetReferenceOpts; + recursionGuard?: RecursionGuard; }): ts.Expression { + const guard = recursionGuard ?? new RecursionGuardImpl(); return ExampleTypeReferenceShape._visit(example.shape, { primitive: (primitiveExample) => ExamplePrimitive._visit(primitiveExample, { @@ -91,7 +95,7 @@ export class GeneratedTypeReferenceExampleImpl implements GeneratedTypeReference list: (exampleItems) => ts.factory.createArrayLiteralExpression( exampleItems.list - .map((exampleItem) => this.buildExample({ example: exampleItem, context, opts })) + .map((exampleItem) => this.buildExample({ example: exampleItem, context, opts, recursionGuard: guard.withDepthIncrement() })) .filter((expr) => !isExpressionUndefined(expr)) ), set: (exampleItems) => { @@ -100,7 +104,7 @@ export class GeneratedTypeReferenceExampleImpl implements GeneratedTypeReference ts.factory.createArrayLiteralExpression( exampleItems.set .map((exampleItem) => - this.buildExample({ example: exampleItem, context, opts }) + this.buildExample({ example: exampleItem, context, opts, recursionGuard: guard.withDepthIncrement() }) ) .filter((expr) => !isExpressionUndefined(expr)) ) @@ -108,7 +112,7 @@ export class GeneratedTypeReferenceExampleImpl implements GeneratedTypeReference } else { return ts.factory.createArrayLiteralExpression( exampleItems.set - .map((exampleItem) => this.buildExample({ example: exampleItem, context, opts })) + .map((exampleItem) => this.buildExample({ example: exampleItem, context, opts, recursionGuard: guard.withDepthIncrement() })) .filter((expr) => !isExpressionUndefined(expr)) ); } @@ -117,8 +121,8 @@ export class GeneratedTypeReferenceExampleImpl implements GeneratedTypeReference ts.factory.createObjectLiteralExpression( examplePairs.map .map<[ts.PropertyName, ts.Expression]>((examplePair) => [ - this.getExampleAsPropertyName({ example: examplePair.key, context, opts }), - this.buildExample({ example: examplePair.value, context, opts }) + this.getExampleAsPropertyName({ example: examplePair.key, context, opts, recursionGuard: guard }), + this.buildExample({ example: examplePair.value, context, opts, recursionGuard: guard.withDepthIncrement() }) ]) .filter(([, value]) => !isExpressionUndefined(value)) .map(([key, value]) => @@ -128,11 +132,11 @@ export class GeneratedTypeReferenceExampleImpl implements GeneratedTypeReference ), nullable: (exampleItem) => exampleItem.nullable != null - ? this.buildExample({ example: exampleItem.nullable, context, opts }) + ? this.buildExample({ example: exampleItem.nullable, context, opts, recursionGuard: guard }) : ts.factory.createIdentifier("null"), optional: (exampleItem) => exampleItem.optional != null - ? this.buildExample({ example: exampleItem.optional, context, opts }) + ? this.buildExample({ example: exampleItem.optional, context, opts, recursionGuard: guard }) : ts.factory.createIdentifier("undefined"), literal: (exampleItem) => exampleItem != null @@ -142,7 +146,8 @@ export class GeneratedTypeReferenceExampleImpl implements GeneratedTypeReference shape: ExampleTypeReferenceShape.primitive(exampleItem.literal) }, context, - opts + opts, + recursionGuard: guard }) : ts.factory.createIdentifier("undefined"), _other: () => { @@ -151,7 +156,10 @@ export class GeneratedTypeReferenceExampleImpl implements GeneratedTypeReference }); }, named: ({ typeName, shape: example }) => { - return context.type.getGeneratedType(typeName).buildExample(example, context, opts); + if (!guard.canRecurse(typeName.typeId)) { + return ts.factory.createIdentifier("undefined"); + } + return context.type.getGeneratedType(typeName).buildExample(example, context, opts, guard.enter(typeName.typeId)); }, unknown: (value) => { const parsed = ts.parseJsonText( @@ -208,12 +216,15 @@ export class GeneratedTypeReferenceExampleImpl implements GeneratedTypeReference private getExampleAsPropertyName({ example, context, - opts + opts, + recursionGuard }: { example: ExampleTypeReference; context: BaseContext; opts: GetReferenceOpts; + recursionGuard?: RecursionGuard; }): ts.PropertyName { + const guard = recursionGuard ?? new RecursionGuardImpl(); return ExampleTypeReferenceShape._visit(example.shape, { primitive: (primitiveExample) => ExamplePrimitive._visit(primitiveExample, { @@ -246,7 +257,8 @@ export class GeneratedTypeReferenceExampleImpl implements GeneratedTypeReference jsonExample: example.jsonExample }, context, - opts + opts, + recursionGuard: guard }); } throw new Error("Cannot convert container to property name"); @@ -267,12 +279,13 @@ export class GeneratedTypeReferenceExampleImpl implements GeneratedTypeReference ); } case "alias": - return this.getExampleAsPropertyName({ example: example.value, context, opts }); + return this.getExampleAsPropertyName({ example: example.value, context, opts, recursionGuard: guard }); case "undiscriminatedUnion": return this.getExampleAsPropertyName({ example: example.singleUnionType, context, - opts + opts, + recursionGuard: guard }); default: assertNever(example); diff --git a/generators/typescript/model/type-reference-example-generator/src/RecursionGuard.ts b/generators/typescript/model/type-reference-example-generator/src/RecursionGuard.ts new file mode 100644 index 000000000000..c4ca7cd6a752 --- /dev/null +++ b/generators/typescript/model/type-reference-example-generator/src/RecursionGuard.ts @@ -0,0 +1,36 @@ +import { TypeId } from "@fern-fern/ir-sdk/api"; + +export interface RecursionGuard { + canRecurse(typeId: TypeId): boolean; + enter(typeId: TypeId): RecursionGuard; + withDepthIncrement(): RecursionGuard; +} + +export class RecursionGuardImpl implements RecursionGuard { + private visited: Set; + private depth: number; + private maxDepth: number; + + constructor(maxDepth: number = 5, visited: Set = new Set(), depth: number = 0) { + this.maxDepth = maxDepth; + this.visited = visited; + this.depth = depth; + } + + public canRecurse(typeId: TypeId): boolean { + if (this.depth >= this.maxDepth) { + return false; + } + return !this.visited.has(typeId); + } + + public enter(typeId: TypeId): RecursionGuard { + const newVisited = new Set(this.visited); + newVisited.add(typeId); + return new RecursionGuardImpl(this.maxDepth, newVisited, this.depth + 1); + } + + public withDepthIncrement(): RecursionGuard { + return new RecursionGuardImpl(this.maxDepth, new Set(this.visited), this.depth + 1); + } +} diff --git a/generators/typescript/model/type-reference-example-generator/src/index.ts b/generators/typescript/model/type-reference-example-generator/src/index.ts index f81d6188bac2..168ce114ebfe 100644 --- a/generators/typescript/model/type-reference-example-generator/src/index.ts +++ b/generators/typescript/model/type-reference-example-generator/src/index.ts @@ -1 +1,2 @@ export { TypeReferenceExampleGenerator } from "./TypeReferenceExampleGenerator"; +export { RecursionGuard, RecursionGuardImpl } from "./RecursionGuard"; diff --git a/generators/typescript/utils/contexts/src/base-context/type/BaseGeneratedType.ts b/generators/typescript/utils/contexts/src/base-context/type/BaseGeneratedType.ts index 3ac1d03bbaf5..8bbd1dc64b82 100644 --- a/generators/typescript/utils/contexts/src/base-context/type/BaseGeneratedType.ts +++ b/generators/typescript/utils/contexts/src/base-context/type/BaseGeneratedType.ts @@ -6,11 +6,12 @@ import { GeneratedFile } from "../../commons"; import { GeneratedModule } from "../../commons/GeneratedModule"; import { GeneratedStatements } from "../../commons/GeneratedStatements"; import { GeneratedUnionInlineMemberNode } from "../../commons/GeneratedUnionInlineMemberNode"; +import { RecursionGuard } from "@fern-typescript/type-reference-example-generator"; export interface BaseGeneratedType extends GeneratedFile, GeneratedStatements, GeneratedModule, GeneratedUnionInlineMemberNode { - buildExample: (example: ExampleTypeShape, context: Context, opts: GetReferenceOpts) => ts.Expression; + buildExample: (example: ExampleTypeShape, context: Context, opts: GetReferenceOpts, recursionGuard?: RecursionGuard) => ts.Expression; }