def generate_resource_model_code(
self,
resources: type[BaseModel] | List[type[BaseModel]],
include_validators: bool = True,
) -> str:
"""
Generate the source code for resource model(s) based on the input resources.
Args:
resources (Union[BaseModel, List[BaseModel]]): The resource(s) to generate the model code for.
include_validators (bool): Whether to include validators in the generated code (default: `True`). Recommended to be `True` for most use cases.
Returns:
str: The generated source code for the resource model(s).
"""
# Reset the internal state of the generator
self._reset_state()
# Serialize the model information of the input resources
for resource in ensure_list(resources):
self._serialize_model(resource)
# Group imports by common parent modules
grouped_imports = self._group_imports_by_common_parent()
# Render the source code using Jinja2
source_code = self.template.render(
data=self.data,
imports=grouped_imports,
include_validators=include_validators,
metadata={
"version": version("fhircraft"),
"timestamp": datetime.now(),
},
)
# Replace the full module specification for any modules imported
# First, collect all imported objects for class name cleanup
all_imported_objects = set()
for objects in grouped_imports.values():
all_imported_objects.update(objects)
# Also add all serialized models (from factory) to the cleanup list
for model in self.data.keys():
all_imported_objects.add(model.__name__)
for module, objects in grouped_imports.items():
module_escaped = module.replace(".", r"\.")
# Remove module prefixes for imported objects
for match in re.finditer(
rf"({module_escaped}\.)({'|'.join(objects)})", source_code
):
source_code = source_code.replace(match.group(1), "")
# Also handle original import statements for any remaining references
for module, objects in self.import_statements.items():
module_escaped = module.replace(".", r"\.")
for match in re.finditer(
rf"({module_escaped}\.)({'|'.join(objects)})", source_code
):
source_code = source_code.replace(match.group(1), "")
# Clean up class representations for all imported objects
for obj_name in all_imported_objects:
# Replace <class 'ObjectName'> with ObjectName
source_code = re.sub(
rf"<class '{re.escape(obj_name)}'>", obj_name, source_code
)
# Also handle cases with module prefixes
source_code = re.sub(
rf"<class '[\w.]*\.{re.escape(obj_name)}'>", obj_name, source_code
)
# Clean up built-in types that aren't in imports
builtin_types = ["str", "int", "float", "bool", "list", "dict", "tuple", "set"]
for builtin_type in builtin_types:
source_code = re.sub(
rf"<class '{re.escape(builtin_type)}'>", builtin_type, source_code
)
# Clean up any references to the factory module
source_code = source_code.replace(f"{FACTORY_MODULE}.", "")
# Also clean up module paths that might appear in repr() output
# This handles patterns like "fhircraft.fhir.resources.factory.ClassName("
factory_pattern = re.escape(FACTORY_MODULE) + r"\."
source_code = re.sub(factory_pattern, "", source_code)
# Clean up module prefixes for ALL imported objects from repr() output
# For each module with imports, remove the module. prefix for its objects
for module, objects in self.import_statements.items():
if objects:
module_parts = module.split(".")
# Try both full module path and last part (e.g., both "typing" and "typing" for "typing")
module_variants = [module]
if len(module_parts) > 1:
module_variants.append(module_parts[-1])
for module_part in module_variants:
for obj in objects:
# Replace module.ObjectName with ObjectName (word boundaries to avoid partial matches)
source_code = re.sub(
rf"\b{re.escape(module_part)}\.{re.escape(obj)}\b",
obj,
source_code,
)
# Special cleanup for typing module - remove typing. prefix for common constructs
# This handles Optional, Union, List, etc. which may appear in repr() but not all in imports
source_code = re.sub(r"\btyping\.", "", source_code)
source_code = source_code.replace(LEFT_TO_RIGHT_COMPLEX, LEFT_TO_RIGHT_SIMPLE)
return source_code