99 model_validator ,
1010)
1111
12- from fastapi_forge .data_type_registry import DataTypeInfo , registry
12+ from fastapi_forge .constants import TAB
1313from fastapi_forge .enums import FieldDataTypeEnum , OnDeleteEnum
1414from fastapi_forge .string_utils import camel_to_snake_hyphen , snake_to_camel
15+ from fastapi_forge .type_info_registry import TypeInfo , enum_registry , registry
1516
1617BoundedStr = Annotated [str , Field (..., min_length = 1 , max_length = 100 )]
1718SnakeCaseStr = Annotated [BoundedStr , Field (..., pattern = r"^[a-z][a-z0-9_]*$" )]
19+ PascalCaseStr = Annotated [
20+ BoundedStr ,
21+ Field (..., pattern = r"^[A-Z][a-zA-Z0-9]*$" ),
22+ ]
1823ModelName = SnakeCaseStr
1924FieldName = SnakeCaseStr
2025BackPopulates = Annotated [str , Field (..., pattern = r"^[a-z][a-z0-9_]*$" )]
2126ProjectName = Annotated [
2227 BoundedStr ,
2328 Field (..., pattern = r"^[a-zA-Z0-9](?:[a-zA-Z0-9._-]*[a-zA-Z0-9])?$" ),
2429]
30+ EnumStr = Annotated [
31+ BoundedStr ,
32+ Field (
33+ ...,
34+ pattern = r"^[a-zA-Z][a-zA-Z0-9_]*$" ,
35+ ),
36+ ]
2537
2638
2739class _Base (BaseModel ):
@@ -36,11 +48,67 @@ class ModelFieldMetadata(_Base):
3648 is_foreign_key : bool = False
3749
3850
51+ class CustomEnumValue (_Base ):
52+ """Represents a single name/value pair in a custom enum."""
53+
54+ name : EnumStr
55+ value : BoundedStr
56+
57+
58+ class CustomEnum (_Base ):
59+ """Represents a custom PostgreSQL ENUM type."""
60+
61+ name : EnumStr
62+ values : list [CustomEnumValue ] = []
63+
64+ def __init__ (self , ** kwargs : Any ):
65+ super ().__init__ (** kwargs )
66+ # dynamically register in the enum registry on instantiation
67+ enum_repr = f"enums.{ self .name } "
68+ enum_value_repr = f"{ enum_repr } .{ self .values [0 ].name } "
69+ enum_registry .register (
70+ self .name ,
71+ TypeInfo (
72+ sqlalchemy_type = f"Enum({ enum_repr } )" ,
73+ sqlalchemy_prefix = True ,
74+ python_type = enum_repr ,
75+ faker_field_value = enum_value_repr ,
76+ value = enum_value_repr ,
77+ test_value = enum_value_repr ,
78+ ),
79+ )
80+
81+ @model_validator (mode = "after" )
82+ def _validate_enum (self ) -> Self :
83+ names = [v .name for v in self .values ]
84+
85+ if len (names ) != len (set (names )):
86+ raise ValueError (f"Enum '{ self .name } ' has duplicate names." )
87+ return self
88+
89+ @computed_field
90+ @property
91+ def class_definition (self ) -> str :
92+ """Returns a string representing the Python Enum class definition."""
93+ lines : list [str ] = []
94+ lines .extend ([f"class { self .name } (StrEnum):" ])
95+ lines .extend ([f'{ TAB } """{ self .name } Enum."""\n ' ])
96+
97+ value_lines : list [str ] = []
98+ for v in self .values :
99+ value_repr = v .value if v .value == "auto()" else f'"{ v .value } "'
100+ value_lines .extend ([f"{ TAB } { v .name } = { value_repr } " ])
101+
102+ lines .extend (value_lines )
103+ return "\n " .join (lines )
104+
105+
39106class ModelField (_Base ):
40107 """Represents a field in a model with validation and computed properties."""
41108
42109 name : FieldName
43110 type : FieldDataTypeEnum
111+ type_enum : EnumStr | None = None
44112 primary_key : bool = False
45113 nullable : bool = False
46114 unique : bool = False
@@ -58,9 +126,32 @@ def name_cc(self) -> str:
58126
59127 @computed_field
60128 @property
61- def type_info (self ) -> DataTypeInfo :
129+ def type_info (self ) -> TypeInfo :
130+ if self .type_enum :
131+ return enum_registry .get (self .type_enum )
62132 return registry .get (self .type )
63133
134+ @model_validator (mode = "after" )
135+ def _validate_type (self ) -> Self :
136+ if self .type == FieldDataTypeEnum .Enum and self .type_enum is None :
137+ msg = (
138+ f"ModelField '{ self .name } ' has field type 'ENUM', "
139+ "but is missing 'type_enum'."
140+ )
141+ raise ValueError (msg )
142+
143+ if self .type_enum and self .type != FieldDataTypeEnum .Enum :
144+ msg = (
145+ f"ModelField '{ self .name } ' has 'type_enum' set, "
146+ "but is not field type 'ENUM'."
147+ )
148+ raise ValueError (msg )
149+
150+ # if self.type_enum and self.default_value:
151+ # self.default_value = f"enums.{self.type_enum.name}.{self.default_value}"
152+
153+ return self
154+
64155 @model_validator (mode = "after" )
65156 def _validate (self ) -> Self :
66157 """Validate field constraints."""
@@ -183,10 +274,10 @@ def _validate(self) -> Self:
183274 if sum (field .primary_key for field in self .fields ) != 1 :
184275 raise ValueError (f"Model '{ self .name } ' must have exactly one primary key." )
185276
186- unque_relationships = [
277+ unique_relationships = [
187278 relationship .field_name for relationship in self .relationships
188279 ]
189- if len (unque_relationships ) != len (set (unque_relationships )):
280+ if len (unique_relationships ) != len (set (unique_relationships )):
190281 raise ValueError (
191282 f"Model '{ self .name } ' contains duplicate relationship field names." ,
192283 )
@@ -277,6 +368,7 @@ class ProjectSpec(_Base):
277368 use_rabbitmq : bool = False
278369 use_taskiq : bool = False
279370 models : list [Model ] = []
371+ custom_enums : list [CustomEnum ] = []
280372
281373 @model_validator (mode = "after" )
282374 def _validate_models (self ) -> Self :
@@ -286,6 +378,11 @@ def _validate_models(self) -> Self:
286378 msg = "Model names must be unique."
287379 raise ValueError (msg )
288380
381+ enum_names = [enum .name for enum in self .custom_enums ]
382+ if len (enum_names ) != len (set (enum_names )):
383+ msg = "Enum names must be unique."
384+ raise ValueError (msg )
385+
289386 if self .use_alembic and not self .use_postgres :
290387 msg = "Cannot use Alembic if PostgreSQL is not enabled."
291388 raise ValueError (msg )
@@ -322,6 +419,7 @@ def _validate_models(self) -> Self:
322419 "TaskIQ is enabled, but the following are missing and required "
323420 f"for its operation: { ', ' .join (missing )} ."
324421 )
422+
325423 return self
326424
327425 @model_validator (mode = "after" )
0 commit comments