coregen-cli 3.1.6__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- coregen.py +1585 -0
- coregen_cli-3.1.6.dist-info/METADATA +1211 -0
- coregen_cli-3.1.6.dist-info/RECORD +172 -0
- coregen_cli-3.1.6.dist-info/WHEEL +5 -0
- coregen_cli-3.1.6.dist-info/entry_points.txt +2 -0
- coregen_cli-3.1.6.dist-info/top_level.txt +2 -0
- generators/__init__.py +1 -0
- generators/assets/__init__.py +1 -0
- generators/assets/assets.py +30 -0
- generators/assets/main.py +4 -0
- generators/config/__init__.py +1 -0
- generators/config/analisy_options.py +42 -0
- generators/config/main.py +11 -0
- generators/config/pubspec.py +139 -0
- generators/cursor/__init__.py +3 -0
- generators/cursor/setup.py +87 -0
- generators/helpers/__init__.py +56 -0
- generators/helpers/component.py +1030 -0
- generators/helpers/config.py +255 -0
- generators/helpers/data_source.py +180 -0
- generators/helpers/domain.py +469 -0
- generators/helpers/feature.py +654 -0
- generators/helpers/navigation.py +274 -0
- generators/helpers/page.py +63 -0
- generators/helpers/project.py +33 -0
- generators/helpers/utils.py +338 -0
- generators/helpers/validation.py +364 -0
- generators/initializator.py +68 -0
- generators/main.py +81 -0
- generators/static/assets/logo.png +0 -0
- generators/static/assets/mock/auth_user.json +5 -0
- generators/static/assets/svgs/apple.svg +4 -0
- generators/static/assets/svgs/google.svg +2 -0
- generators/static/cursor/AGENTS.md.jinja +37 -0
- generators/static/cursor/agents/doc-writer.md +20 -0
- generators/static/cursor/agents/epic-orchestrator.md +28 -0
- generators/static/cursor/agents/feature-implementer.md +29 -0
- generators/static/cursor/agents/integration-wiring.md +30 -0
- generators/static/cursor/agents/layer-application.md +21 -0
- generators/static/cursor/agents/layer-guardian.md +21 -0
- generators/static/cursor/agents/layer-infrastructure.md +31 -0
- generators/static/cursor/agents/layer-model.md +24 -0
- generators/static/cursor/agents/layer-presentation.md +33 -0
- generators/static/cursor/docs/architecture/APIS_AND_INTEGRATION.md.jinja +42 -0
- generators/static/cursor/docs/architecture/CARAVAGGIO_COMPONENTS.md.jinja +317 -0
- generators/static/cursor/docs/architecture/DDD_LAYERS.md.jinja +32 -0
- generators/static/cursor/docs/architecture/FILE_TEMPLATES.md.jinja +76 -0
- generators/static/cursor/docs/architecture/MOCK_AND_REMOTE_DATA.md.jinja +87 -0
- generators/static/cursor/docs/architecture/REFERENCE_IMPLEMENTATIONS.md.jinja +48 -0
- generators/static/cursor/docs/architecture/WIDGETS_AND_CARAVAGGIO.md.jinja +76 -0
- generators/static/cursor/docs/epics/README.md +17 -0
- generators/static/cursor/lib/domain/AGENTS.md +23 -0
- generators/static/cursor/lib/features/AGENTS.md +23 -0
- generators/static/cursor/lib/widgets/AGENTS.md +21 -0
- generators/static/cursor/rules/apis-layer.mdc.jinja +21 -0
- generators/static/cursor/rules/application-layer.mdc.jinja +12 -0
- generators/static/cursor/rules/architecture-core.mdc.jinja +43 -0
- generators/static/cursor/rules/dart-conventions.mdc.jinja +14 -0
- generators/static/cursor/rules/domain-layer.mdc.jinja +24 -0
- generators/static/cursor/rules/login-context.mdc.jinja +16 -0
- generators/static/cursor/rules/presentation-layer.mdc.jinja +15 -0
- generators/static/cursor/rules/quality-gate.mdc.jinja +14 -0
- generators/static/cursor/rules/ui-caravaggio.mdc.jinja +22 -0
- generators/static/cursor/skills/caravaggio-ui/SKILL.md +138 -0
- generators/static/cursor/skills/epic-delivery/SKILL.md +74 -0
- generators/static/cursor/skills/epic-delivery/templates/epic-status.md +14 -0
- generators/static/cursor/skills/epic-delivery/templates/epic.md +36 -0
- generators/static/templates/apis/common/constants_template.jinja +48 -0
- generators/static/templates/apis/common/data_source_config_template.jinja +47 -0
- generators/static/templates/apis/core/api_injectable_module_template.jinja +14 -0
- generators/static/templates/apis/interceptors/api_logger_template.jinja +135 -0
- generators/static/templates/apis/interceptors/auth_interceptor_template.jinja +207 -0
- generators/static/templates/auth/application/auth_bloc_template.jinja +36 -0
- generators/static/templates/auth/application/auth_event_template.jinja +7 -0
- generators/static/templates/auth/application/auth_state_template.jinja +8 -0
- generators/static/templates/auth/infrastructure/auth_facade_module_template.jinja +16 -0
- generators/static/templates/auth/infrastructure/auth_facade_template.jinja +288 -0
- generators/static/templates/auth/infrastructure/mock_auth_facade_template.jinja +106 -0
- generators/static/templates/auth/model/auth_failure_template.jinja +11 -0
- generators/static/templates/auth/model/i_auth_facade_template.jinja +24 -0
- generators/static/templates/auth/model/user_template.jinja +34 -0
- generators/static/templates/auth/model/value_objects_template.jinja +54 -0
- generators/static/templates/auth/presentation/login_screen_template.jinja +24 -0
- generators/static/templates/auth/sign_in_form/application/sign_in_form_bloc_template.jinja +139 -0
- generators/static/templates/auth/sign_in_form/application/sign_in_form_event_template.jinja +15 -0
- generators/static/templates/auth/sign_in_form/application/sign_in_form_state_template.jinja +26 -0
- generators/static/templates/auth/sign_in_form/presentation/apple_form_template.jinja +16 -0
- generators/static/templates/auth/sign_in_form/presentation/email_password_form_template_INACTIVE.jinja +94 -0
- generators/static/templates/auth/sign_in_form/presentation/google_form_template.jinja +15 -0
- generators/static/templates/auth/sign_in_form/presentation/sign_in_form_template.jinja +381 -0
- generators/static/templates/component/component_bloc_template.jinja +49 -0
- generators/static/templates/component/component_event_template.jinja +7 -0
- generators/static/templates/component/component_form_bloc_template.jinja +56 -0
- generators/static/templates/component/component_form_event_template.jinja +7 -0
- generators/static/templates/component/component_form_state_template.jinja +18 -0
- generators/static/templates/component/component_form_widget_template.jinja +130 -0
- generators/static/templates/component/component_list_widget_template.jinja +95 -0
- generators/static/templates/component/component_state_template.jinja +9 -0
- generators/static/templates/component/component_widget_template.jinja +113 -0
- generators/static/templates/core/bloc/base_form_bloc_template.jinja +32 -0
- generators/static/templates/core/errors/error_localizer_template.jinja +61 -0
- generators/static/templates/core/infrastructure/base_mapper_template.jinja +21 -0
- generators/static/templates/core/infrastructure/base_repository_mixin_template.jinja +78 -0
- generators/static/templates/core/infrastructure/firebase_injectable_module_template.jinja +11 -0
- generators/static/templates/core/infrastructure/firestore_helpers_template.jinja +22 -0
- generators/static/templates/core/infrastructure/repository_error_handler_template.jinja +44 -0
- generators/static/templates/core/model/common_interfaces_template.jinja +3 -0
- generators/static/templates/core/model/entity_template.jinja +5 -0
- generators/static/templates/core/model/errors_template.jinja +15 -0
- generators/static/templates/core/model/failures_template.jinja +97 -0
- generators/static/templates/core/model/value_objects_template.jinja +135 -0
- generators/static/templates/core/model/value_validators_template.jinja +105 -0
- generators/static/templates/core/presentation/app_drawer_template.jinja +52 -0
- generators/static/templates/core/presentation/app_widget_template.jinja +41 -0
- generators/static/templates/core/presentation/bottom_nav_bar_template.jinja +32 -0
- generators/static/templates/domain/domain_mapper_template.jinja +39 -0
- generators/static/templates/domain/domain_remote_service_template.jinja +46 -0
- generators/static/templates/domain/domain_repository_template.jinja +90 -0
- generators/static/templates/domain/domain_service_module_template.jinja +23 -0
- generators/static/templates/domain/enum_template.jinja +3 -0
- generators/static/templates/domain/i_domain_service_template.jinja +18 -0
- generators/static/templates/domain/mock_domain_service_template.jinja +73 -0
- generators/static/templates/feature/feature_bloc_template.jinja +104 -0
- generators/static/templates/feature/feature_dto_template.jinja +26 -0
- generators/static/templates/feature/feature_entity_template.jinja +23 -0
- generators/static/templates/feature/feature_event_template.jinja +20 -0
- generators/static/templates/feature/feature_extensions_template.jinja +25 -0
- generators/static/templates/feature/feature_failure_template.jinja +11 -0
- generators/static/templates/feature/feature_page_template.jinja +113 -0
- generators/static/templates/feature/feature_repository_template.jinja +112 -0
- generators/static/templates/feature/feature_state_template.jinja +20 -0
- generators/static/templates/feature/i_feature_repository_template.jinja +21 -0
- generators/static/templates/feature/value_object_field_template.jinja +0 -0
- generators/static/templates/feature/value_object_template.jinja +0 -0
- generators/static/templates/feature/value_validators_template.jinja +57 -0
- generators/static/templates/home/home_screen_template.jinja +68 -0
- generators/static/templates/home/presentation/home_screen_template.jinja +52 -0
- generators/static/templates/home/screen_template.jinja +12 -0
- generators/static/templates/injection_template.jinja +8 -0
- generators/static/templates/logging/analytics_logging_template.jinja +42 -0
- generators/static/templates/logging/console_template.jinja +50 -0
- generators/static/templates/logging/logger_injectable_module_template.jinja +24 -0
- generators/static/templates/logging/logger_template.jinja +118 -0
- generators/static/templates/main_template.jinja +43 -0
- generators/static/templates/page_template.jinja +29 -0
- generators/static/templates/router_template.jinja +26 -0
- generators/static/templates/splash/presentation/splash_screen_auth_template.jinja +97 -0
- generators/static/templates/splash/presentation/splash_screen_template.jinja +51 -0
- generators/static/templates/storage/storage_repository_template.jinja +80 -0
- generators/static/templates/user_profile/infrastructure/user_profile_dto_template.jinja +27 -0
- generators/static/templates/user_profile/infrastructure/user_profile_mapper_template.jinja +43 -0
- generators/static/templates/user_profile/infrastructure/user_profile_repository_template.jinja +82 -0
- generators/static/templates/user_profile/infrastructure/user_profile_service_template.jinja +27 -0
- generators/static/templates/user_profile/model/i_user_profile_repository_template.jinja +34 -0
- generators/static/templates/user_profile/model/user_profile_failure_template.jinja +11 -0
- generators/static/templates/user_profile/model/user_profile_template.jinja +31 -0
- generators/static/templates/widgets/common/custom_scaffold_template.jinja +52 -0
- generators/static/templates/widgets/common/error_widget_template.jinja +22 -0
- generators/static/templates/widgets/common/loading_widget_template.jinja +14 -0
- generators/static/templates/widgets/common/unknown_state_widget_template.jinja +23 -0
- generators/templates/__init__.py +1 -0
- generators/templates/_core/core_generator.py +121 -0
- generators/templates/apis/apis_generator.py +50 -0
- generators/templates/auth/auth_generator.py +40 -0
- generators/templates/auth/sign_in_form_generator.py +22 -0
- generators/templates/copier.py +51 -0
- generators/templates/home/home_generator.py +26 -0
- generators/templates/lib/lib_generator.py +40 -0
- generators/templates/logging/logging_generator.py +28 -0
- generators/templates/main.py +75 -0
- generators/templates/splash/splash_generator.py +20 -0
- generators/templates/storage/storage_generator.py +5 -0
|
@@ -0,0 +1,1030 @@
|
|
|
1
|
+
"""Component generation functions"""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional, List, Dict
|
|
6
|
+
from generators.templates.copier import generate_file
|
|
7
|
+
from generators.templates._core.core_generator import ensure_common_widgets
|
|
8
|
+
from .utils import to_pascal_case, to_pascal_case_preserve, map_field_type, get_form_field_metadata, PRIMITIVE_TYPES
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def infer_lib_path(component_dir: Path) -> Path:
|
|
12
|
+
"""Resolve ``lib/`` from a directory inside the Flutter project (e.g. a component folder)."""
|
|
13
|
+
current = component_dir.resolve()
|
|
14
|
+
for parent in [current, *current.parents]:
|
|
15
|
+
pub = parent / "pubspec.yaml"
|
|
16
|
+
if pub.is_file():
|
|
17
|
+
lib = parent / "lib"
|
|
18
|
+
if lib.is_dir():
|
|
19
|
+
return lib
|
|
20
|
+
return current.parent
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def get_repository_info(lib_path: Path, domain_folder: str, model_name: str, model_folder: Optional[str] = None) -> Dict:
|
|
24
|
+
"""Extract failure type and available methods from a repository interface.
|
|
25
|
+
|
|
26
|
+
Reads i_{model_name}_repository.dart and parses:
|
|
27
|
+
- The failure class name from Either<FailureType, ...>
|
|
28
|
+
- The failure import path
|
|
29
|
+
- Which CRUD methods are available
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
Dict with keys: failure_class, failure_import, methods (set of method names)
|
|
33
|
+
Falls back to {ModelName}Failure conventions if parsing fails.
|
|
34
|
+
"""
|
|
35
|
+
if model_folder is None:
|
|
36
|
+
model_folder = model_name
|
|
37
|
+
|
|
38
|
+
repo_file = lib_path / domain_folder / model_folder / "model" / f"i_{model_name}_repository.dart"
|
|
39
|
+
|
|
40
|
+
model_pascal = to_pascal_case_preserve(model_name)
|
|
41
|
+
fallback = {
|
|
42
|
+
'failure_class': f"{model_pascal}Failure",
|
|
43
|
+
'failure_import': f"package:{Path(domain_folder) / model_folder / 'model' / f'{model_name}_failure.dart'}",
|
|
44
|
+
'methods': {'getAll', 'getById', 'create', 'update', 'delete'},
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if not repo_file.exists():
|
|
48
|
+
return fallback
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
content = repo_file.read_text()
|
|
52
|
+
except Exception:
|
|
53
|
+
return fallback
|
|
54
|
+
|
|
55
|
+
# Extract failure class from Either<FailureType, ...>
|
|
56
|
+
failure_match = re.search(r'Either<(\w+),', content)
|
|
57
|
+
failure_class = failure_match.group(1) if failure_match else fallback['failure_class']
|
|
58
|
+
|
|
59
|
+
# Extract the import line for the failure class
|
|
60
|
+
failure_import = fallback['failure_import']
|
|
61
|
+
failure_file_stem = model_name + '_failure'
|
|
62
|
+
for line in content.splitlines():
|
|
63
|
+
if 'import' in line and failure_file_stem in line:
|
|
64
|
+
import_match = re.search(r"import\s+'(package:[^']+)'", line)
|
|
65
|
+
if import_match:
|
|
66
|
+
failure_import = import_match.group(1)
|
|
67
|
+
break
|
|
68
|
+
|
|
69
|
+
# Detect available methods by matching method signatures (name followed by '(')
|
|
70
|
+
methods = set()
|
|
71
|
+
method_patterns = {
|
|
72
|
+
'getAll': r'\bgetAll\s*\(',
|
|
73
|
+
'getById': r'\bgetById\s*\(',
|
|
74
|
+
'create': r'\bcreate\s*\(',
|
|
75
|
+
'update': r'\bupdate\s*\(',
|
|
76
|
+
'delete': r'\bdelete\s*\(',
|
|
77
|
+
'getByAuthId': r'\bgetByAuthId\s*\(',
|
|
78
|
+
'createOrUpdate': r'\bcreateOrUpdate\s*\(',
|
|
79
|
+
'getCurrentUserProfile': r'\bgetCurrentUserProfile\s*\(',
|
|
80
|
+
'updateCurrentUserProfile': r'\bupdateCurrentUserProfile\s*\(',
|
|
81
|
+
'uploadAvatar': r'\buploadAvatar\s*\(',
|
|
82
|
+
'logout': r'\blogout\s*\(',
|
|
83
|
+
}
|
|
84
|
+
for method_name, pattern in method_patterns.items():
|
|
85
|
+
if re.search(pattern, content):
|
|
86
|
+
methods.add(method_name)
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
'failure_class': failure_class,
|
|
90
|
+
'failure_import': failure_import,
|
|
91
|
+
'methods': methods,
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def ensure_base_form_bloc(project_name: str, lib_path: Path) -> None:
|
|
96
|
+
"""Generate core/bloc/base_form_bloc.dart if it doesn't already exist.
|
|
97
|
+
|
|
98
|
+
Called automatically when creating a form component, so the BaseFormBloc
|
|
99
|
+
is only added to projects that actually use form components.
|
|
100
|
+
"""
|
|
101
|
+
target = lib_path / "core" / "bloc" / "base_form_bloc.dart"
|
|
102
|
+
if not target.exists():
|
|
103
|
+
generate_file(project_name, lib_path, "core/bloc/base_form_bloc_template.jinja", "core/bloc/base_form_bloc.dart")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def create_component_form_layers(component_dir: Path, component_name: str, field_list: list[dict], project_name: str, folder: Optional[str], domain_model_name: Optional[str] = None, domain_folder: Optional[str] = None, domain_model_folder: Optional[str] = None, lib_path: Optional[Path] = None) -> None:
|
|
107
|
+
"""Create all layers for a form component
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
component_dir: Path to component directory
|
|
111
|
+
component_name: Name of the component
|
|
112
|
+
field_list: List of field dictionaries
|
|
113
|
+
project_name: Name of the project
|
|
114
|
+
folder: Optional folder path
|
|
115
|
+
domain_model_name: Optional domain model name / file stem (if using domain model)
|
|
116
|
+
domain_folder: Optional domain folder name (e.g., 'domain')
|
|
117
|
+
domain_model_folder: Optional containing folder for the model. Defaults to domain_model_name.
|
|
118
|
+
lib_path: Path to lib/ directory (used to discover enums)
|
|
119
|
+
"""
|
|
120
|
+
# Discover enums for type-aware form generation
|
|
121
|
+
from .feature import find_enums_with_info
|
|
122
|
+
known_enums: set = set()
|
|
123
|
+
enums_info: Dict = {}
|
|
124
|
+
if lib_path and domain_folder:
|
|
125
|
+
enums_info = find_enums_with_info(lib_path, domain_folder)
|
|
126
|
+
known_enums = set(enums_info.keys())
|
|
127
|
+
|
|
128
|
+
# Build the import path prefix
|
|
129
|
+
if folder:
|
|
130
|
+
import_prefix = f"{folder.replace('/', '/')}/{component_name}"
|
|
131
|
+
else:
|
|
132
|
+
import_prefix = component_name
|
|
133
|
+
|
|
134
|
+
# Application layer
|
|
135
|
+
app_dir = component_dir / "application"
|
|
136
|
+
app_dir.mkdir(exist_ok=True)
|
|
137
|
+
|
|
138
|
+
domain_value_objects_import: Optional[str] = None
|
|
139
|
+
if domain_folder and domain_model_name:
|
|
140
|
+
model_folder = domain_model_folder or domain_model_name
|
|
141
|
+
domain_value_objects_import = f"{domain_folder}/{model_folder}/model/value_objects.dart"
|
|
142
|
+
|
|
143
|
+
# Create form-specific BLoC files using templates
|
|
144
|
+
generate_form_event_from_template(component_name, field_list, app_dir, project_name, known_enums=known_enums)
|
|
145
|
+
generate_form_state_from_template(component_name, field_list, app_dir, project_name, known_enums=known_enums)
|
|
146
|
+
generate_form_bloc_from_template(
|
|
147
|
+
component_name,
|
|
148
|
+
field_list,
|
|
149
|
+
app_dir,
|
|
150
|
+
project_name,
|
|
151
|
+
known_enums=known_enums,
|
|
152
|
+
enums_info=enums_info,
|
|
153
|
+
domain_value_objects_import=domain_value_objects_import,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
# Presentation layer
|
|
157
|
+
presentation_dir = component_dir / "presentation"
|
|
158
|
+
presentation_dir.mkdir(exist_ok=True)
|
|
159
|
+
|
|
160
|
+
# Create component widget - pass field_list and domain info for form components
|
|
161
|
+
generate_form_widget_from_template(
|
|
162
|
+
component_name,
|
|
163
|
+
field_list,
|
|
164
|
+
presentation_dir,
|
|
165
|
+
project_name,
|
|
166
|
+
import_prefix,
|
|
167
|
+
known_enums=known_enums,
|
|
168
|
+
enums_info=enums_info,
|
|
169
|
+
domain_value_objects_import=domain_value_objects_import,
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def create_component_layers(component_dir: Path, component_name: str, project_name: str, folder: Optional[str], domain_model_name: Optional[str] = None, domain_folder: Optional[str] = None, domain_model_folder: Optional[str] = None, lib_path: Optional[Path] = None) -> None:
|
|
174
|
+
"""Create all layers for a single-item component (load by ID).
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
component_dir: Path to component directory
|
|
178
|
+
component_name: Name of the component
|
|
179
|
+
project_name: Name of the project
|
|
180
|
+
folder: Optional folder path
|
|
181
|
+
domain_model_name: Optional domain model name / file stem (if using domain model)
|
|
182
|
+
domain_folder: Optional domain folder name (e.g., 'domain')
|
|
183
|
+
domain_model_folder: Optional containing folder for the model (e.g., 'auth' for user_profile).
|
|
184
|
+
Defaults to domain_model_name if not specified.
|
|
185
|
+
lib_path: Optional path to lib/ directory (for reading repository interface)
|
|
186
|
+
"""
|
|
187
|
+
if domain_model_folder is None:
|
|
188
|
+
domain_model_folder = domain_model_name
|
|
189
|
+
|
|
190
|
+
# Build the import path prefix
|
|
191
|
+
if folder:
|
|
192
|
+
import_prefix = f"{folder.replace('/', '/')}/{component_name}"
|
|
193
|
+
else:
|
|
194
|
+
import_prefix = component_name
|
|
195
|
+
|
|
196
|
+
resolved_lib_path = lib_path or infer_lib_path(component_dir)
|
|
197
|
+
ensure_common_widgets(project_name, resolved_lib_path)
|
|
198
|
+
|
|
199
|
+
# Application layer
|
|
200
|
+
app_dir = component_dir / "application"
|
|
201
|
+
app_dir.mkdir(exist_ok=True)
|
|
202
|
+
|
|
203
|
+
# Determine import prefix for domain model
|
|
204
|
+
if domain_model_name and domain_folder:
|
|
205
|
+
domain_import_prefix = f"{domain_folder}/{domain_model_folder}"
|
|
206
|
+
else:
|
|
207
|
+
domain_import_prefix = None
|
|
208
|
+
|
|
209
|
+
# Get PascalCase names
|
|
210
|
+
component_pascal = to_pascal_case_preserve(component_name)
|
|
211
|
+
failure_class_for_widget: Optional[str] = None
|
|
212
|
+
widget_domain_import_prefix = ""
|
|
213
|
+
widget_domain_model_name = ""
|
|
214
|
+
widget_domain_model_pascal = ""
|
|
215
|
+
|
|
216
|
+
# Generate event and state files
|
|
217
|
+
if domain_import_prefix:
|
|
218
|
+
domain_model_pascal = to_pascal_case_preserve(domain_model_name)
|
|
219
|
+
widget_domain_import_prefix = domain_import_prefix
|
|
220
|
+
widget_domain_model_name = domain_model_name or ""
|
|
221
|
+
widget_domain_model_pascal = domain_model_pascal
|
|
222
|
+
|
|
223
|
+
# Read repository interface to detect failure type
|
|
224
|
+
repo_info = None
|
|
225
|
+
if resolved_lib_path:
|
|
226
|
+
repo_info = get_repository_info(resolved_lib_path, domain_folder, domain_model_name, domain_model_folder)
|
|
227
|
+
|
|
228
|
+
failure_class = repo_info['failure_class'] if repo_info else f"{domain_model_pascal}Failure"
|
|
229
|
+
failure_class_for_widget = failure_class
|
|
230
|
+
failure_import = f"import '{repo_info['failure_import']}';" if repo_info else f"import 'package:{project_name}/{domain_import_prefix}/model/{domain_model_name}_failure.dart';"
|
|
231
|
+
|
|
232
|
+
freezed_mixin_event = "_$" + component_pascal + "Event"
|
|
233
|
+
freezed_mixin_state = "_$" + component_pascal + "State"
|
|
234
|
+
|
|
235
|
+
# Determine load method: use getCurrentUserProfile if available, otherwise getById
|
|
236
|
+
available_methods = repo_info['methods'] if repo_info else {'getById'}
|
|
237
|
+
has_get_current = 'getCurrentUserProfile' in available_methods
|
|
238
|
+
|
|
239
|
+
if has_get_current:
|
|
240
|
+
event_load_factory = f" const factory {component_pascal}Event.loadRequested() = LoadRequested;"
|
|
241
|
+
load_await_rhs = "_repository.getCurrentUserProfile()"
|
|
242
|
+
reload_await_rhs = "_repository.getCurrentUserProfile()"
|
|
243
|
+
last_id_field = ""
|
|
244
|
+
fold_success = f" ({domain_model_pascal} item) => emit({component_pascal}State.loaded(item)),"
|
|
245
|
+
else:
|
|
246
|
+
event_load_factory = f" const factory {component_pascal}Event.loadRequested(String id) = LoadRequested;"
|
|
247
|
+
load_await_rhs = "_repository.getById(event.id)"
|
|
248
|
+
reload_await_rhs = "_repository.getById(_lastLoadedId!)"
|
|
249
|
+
last_id_field = "\n String? _lastLoadedId;"
|
|
250
|
+
fold_success = f""" ({domain_model_pascal} item) {{
|
|
251
|
+
_lastLoadedId = event.id;
|
|
252
|
+
emit({component_pascal}State.loaded(item));
|
|
253
|
+
}},"""
|
|
254
|
+
|
|
255
|
+
event_reload_factory = f" const factory {component_pascal}Event.reloadRequested() = ReloadRequested;"
|
|
256
|
+
|
|
257
|
+
# Generate event file (load + reload)
|
|
258
|
+
event_content = f"""part of '{component_name}_bloc.dart';
|
|
259
|
+
|
|
260
|
+
@freezed
|
|
261
|
+
abstract class {component_pascal}Event with {freezed_mixin_event} {{
|
|
262
|
+
{event_load_factory}
|
|
263
|
+
{event_reload_factory}
|
|
264
|
+
}}
|
|
265
|
+
"""
|
|
266
|
+
(app_dir / f"{component_name}_event.dart").write_text(event_content)
|
|
267
|
+
|
|
268
|
+
# Generate state file with domain model type
|
|
269
|
+
state_content = f"""part of '{component_name}_bloc.dart';
|
|
270
|
+
|
|
271
|
+
@freezed
|
|
272
|
+
abstract class {component_pascal}State with {freezed_mixin_state} {{
|
|
273
|
+
const factory {component_pascal}State.initial() = Initial;
|
|
274
|
+
const factory {component_pascal}State.loading() = Loading;
|
|
275
|
+
const factory {component_pascal}State.loaded({domain_model_pascal} item, {{@Default(false) bool isReloading}}) = Loaded;
|
|
276
|
+
const factory {component_pascal}State.error({failure_class} failure) = Error;
|
|
277
|
+
}}
|
|
278
|
+
"""
|
|
279
|
+
(app_dir / f"{component_name}_state.dart").write_text(state_content)
|
|
280
|
+
else:
|
|
281
|
+
# Empty (Vuoto) component: minimal event/state without domain model
|
|
282
|
+
freezed_mixin_event_empty = "_$" + component_pascal + "Event"
|
|
283
|
+
freezed_mixin_state_empty = "_$" + component_pascal + "State"
|
|
284
|
+
|
|
285
|
+
event_content = f"""part of '{component_name}_bloc.dart';
|
|
286
|
+
|
|
287
|
+
@freezed
|
|
288
|
+
abstract class {component_pascal}Event with {freezed_mixin_event_empty} {{
|
|
289
|
+
const factory {component_pascal}Event.loadRequested() = LoadRequested;
|
|
290
|
+
}}
|
|
291
|
+
"""
|
|
292
|
+
(app_dir / f"{component_name}_event.dart").write_text(event_content)
|
|
293
|
+
|
|
294
|
+
state_content = f"""part of '{component_name}_bloc.dart';
|
|
295
|
+
|
|
296
|
+
@freezed
|
|
297
|
+
abstract class {component_pascal}State with {freezed_mixin_state_empty} {{
|
|
298
|
+
const factory {component_pascal}State.initial() = Initial;
|
|
299
|
+
const factory {component_pascal}State.loading() = Loading;
|
|
300
|
+
const factory {component_pascal}State.loaded() = Loaded;
|
|
301
|
+
const factory {component_pascal}State.error(String message) = Error;
|
|
302
|
+
}}
|
|
303
|
+
"""
|
|
304
|
+
(app_dir / f"{component_name}_state.dart").write_text(state_content)
|
|
305
|
+
|
|
306
|
+
# Generate BLoC with domain model imports if provided
|
|
307
|
+
if domain_import_prefix:
|
|
308
|
+
bloc_content = f"""import 'package:bloc/bloc.dart';
|
|
309
|
+
import 'package:dartz/dartz.dart';
|
|
310
|
+
import 'package:freezed_annotation/freezed_annotation.dart';
|
|
311
|
+
import 'package:{project_name}/{domain_import_prefix}/model/{domain_model_name}.dart';
|
|
312
|
+
{failure_import}
|
|
313
|
+
import 'package:{project_name}/{domain_import_prefix}/model/i_{domain_model_name}_repository.dart';
|
|
314
|
+
|
|
315
|
+
part '{component_name}_bloc.freezed.dart';
|
|
316
|
+
part '{component_name}_event.dart';
|
|
317
|
+
part '{component_name}_state.dart';
|
|
318
|
+
|
|
319
|
+
class {component_pascal}Bloc extends Bloc<{component_pascal}Event, {component_pascal}State> {{
|
|
320
|
+
final I{domain_model_pascal}Repository _repository;{last_id_field}
|
|
321
|
+
|
|
322
|
+
{component_pascal}Bloc(this._repository) : super(const {component_pascal}State.initial()) {{
|
|
323
|
+
on<LoadRequested>(_onLoadRequested);
|
|
324
|
+
on<ReloadRequested>(_onReloadRequested);
|
|
325
|
+
}}
|
|
326
|
+
|
|
327
|
+
Future<void> _onLoadRequested(LoadRequested event, Emitter<{component_pascal}State> emit) async {{
|
|
328
|
+
emit(const {component_pascal}State.loading());
|
|
329
|
+
final Either<{failure_class}, {domain_model_pascal}> result = await {load_await_rhs};
|
|
330
|
+
result.fold(
|
|
331
|
+
({failure_class} failure) => emit({component_pascal}State.error(failure)),
|
|
332
|
+
{fold_success}
|
|
333
|
+
);
|
|
334
|
+
}}
|
|
335
|
+
|
|
336
|
+
Future<void> _onReloadRequested(ReloadRequested event, Emitter<{component_pascal}State> emit) async {{
|
|
337
|
+
if (state is! Loaded) return;
|
|
338
|
+
final Loaded current = state as Loaded;
|
|
339
|
+
emit(current.copyWith(isReloading: true));
|
|
340
|
+
final Either<{failure_class}, {domain_model_pascal}> result = await {reload_await_rhs};
|
|
341
|
+
result.fold(
|
|
342
|
+
({failure_class} failure) => emit(current.copyWith(isReloading: false)),
|
|
343
|
+
({domain_model_pascal} item) => emit({component_pascal}State.loaded(item)),
|
|
344
|
+
);
|
|
345
|
+
}}
|
|
346
|
+
}}
|
|
347
|
+
"""
|
|
348
|
+
(app_dir / f"{component_name}_bloc.dart").write_text(bloc_content)
|
|
349
|
+
else:
|
|
350
|
+
# Empty (Vuoto) BLoC: no repository, stub handlers
|
|
351
|
+
bloc_content = f"""import 'package:bloc/bloc.dart';
|
|
352
|
+
import 'package:freezed_annotation/freezed_annotation.dart';
|
|
353
|
+
|
|
354
|
+
part '{component_name}_bloc.freezed.dart';
|
|
355
|
+
part '{component_name}_event.dart';
|
|
356
|
+
part '{component_name}_state.dart';
|
|
357
|
+
|
|
358
|
+
class {component_pascal}Bloc extends Bloc<{component_pascal}Event, {component_pascal}State> {{
|
|
359
|
+
{component_pascal}Bloc() : super(const {component_pascal}State.initial()) {{
|
|
360
|
+
on<LoadRequested>(_onLoadRequested);
|
|
361
|
+
}}
|
|
362
|
+
|
|
363
|
+
Future<void> _onLoadRequested(LoadRequested event, Emitter<{component_pascal}State> emit) async {{
|
|
364
|
+
emit(const {component_pascal}State.loading());
|
|
365
|
+
// TODO: implement load logic
|
|
366
|
+
emit(const {component_pascal}State.loaded());
|
|
367
|
+
}}
|
|
368
|
+
}}
|
|
369
|
+
"""
|
|
370
|
+
(app_dir / f"{component_name}_bloc.dart").write_text(bloc_content)
|
|
371
|
+
|
|
372
|
+
# Presentation layer
|
|
373
|
+
presentation_dir = component_dir / "presentation"
|
|
374
|
+
presentation_dir.mkdir(exist_ok=True)
|
|
375
|
+
|
|
376
|
+
# Create component widget
|
|
377
|
+
generate_component_widget_from_template(
|
|
378
|
+
component_name,
|
|
379
|
+
presentation_dir,
|
|
380
|
+
project_name,
|
|
381
|
+
'single',
|
|
382
|
+
import_prefix,
|
|
383
|
+
failure_class=failure_class_for_widget,
|
|
384
|
+
domain_import_prefix=widget_domain_import_prefix,
|
|
385
|
+
domain_model_name=widget_domain_model_name,
|
|
386
|
+
domain_model_pascal=widget_domain_model_pascal,
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def generate_form_widget_from_template(
|
|
391
|
+
component_name: str,
|
|
392
|
+
field_list: list[dict],
|
|
393
|
+
presentation_dir: Path,
|
|
394
|
+
project_name: str,
|
|
395
|
+
import_prefix: str,
|
|
396
|
+
known_enums: set = None,
|
|
397
|
+
enums_info: Dict = None,
|
|
398
|
+
domain_value_objects_import: Optional[str] = None,
|
|
399
|
+
) -> None:
|
|
400
|
+
"""Generate form component widget file using Jinja template"""
|
|
401
|
+
_enums = known_enums or set()
|
|
402
|
+
_enums_info = enums_info or {}
|
|
403
|
+
pascal_name = to_pascal_case(component_name)
|
|
404
|
+
bloc_name = f"{pascal_name}FormBloc"
|
|
405
|
+
state_name = f"{pascal_name}FormState"
|
|
406
|
+
bloc_import = f"import 'package:{project_name}/{import_prefix}/application/{component_name}_form_bloc.dart';"
|
|
407
|
+
|
|
408
|
+
# Build extra imports needed by the template (enum imports)
|
|
409
|
+
extra_imports: list[str] = []
|
|
410
|
+
|
|
411
|
+
processed_field_list = []
|
|
412
|
+
for field in field_list:
|
|
413
|
+
field_name = field['name']
|
|
414
|
+
field_type = field['type']
|
|
415
|
+
base_ft = field_type[:-1] if field_type.endswith('?') else field_type
|
|
416
|
+
|
|
417
|
+
field_dict = dict(field)
|
|
418
|
+
field_dict['pascal_name'] = to_pascal_case_preserve(field_name)
|
|
419
|
+
field_dict['label'] = ' '.join(word.capitalize() for word in field_name.split('_'))
|
|
420
|
+
|
|
421
|
+
meta = get_form_field_metadata(base_ft, field_name, known_enums=_enums)
|
|
422
|
+
field_dict.update(meta)
|
|
423
|
+
|
|
424
|
+
if meta['is_enum']:
|
|
425
|
+
info = _enums_info.get(base_ft)
|
|
426
|
+
if info:
|
|
427
|
+
imp = f"import 'package:{project_name}/{info['folder']}/{info['file_stem']}.dart';"
|
|
428
|
+
if imp not in extra_imports:
|
|
429
|
+
extra_imports.append(imp)
|
|
430
|
+
|
|
431
|
+
processed_field_list.append(field_dict)
|
|
432
|
+
|
|
433
|
+
generate_file(project_name, presentation_dir, "component/component_form_widget_template.jinja", f"{component_name}_component.dart", {
|
|
434
|
+
"component_name": component_name,
|
|
435
|
+
"pascal_name": pascal_name,
|
|
436
|
+
"bloc_name": bloc_name,
|
|
437
|
+
"state_name": state_name,
|
|
438
|
+
"bloc_import": bloc_import,
|
|
439
|
+
"extra_imports": "\n".join(extra_imports),
|
|
440
|
+
"field_list": processed_field_list,
|
|
441
|
+
"domain_value_objects_import": domain_value_objects_import,
|
|
442
|
+
})
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def generate_component_widget_from_template(
|
|
446
|
+
component_name: str,
|
|
447
|
+
presentation_dir: Path,
|
|
448
|
+
project_name: str,
|
|
449
|
+
component_type: str,
|
|
450
|
+
import_prefix: str,
|
|
451
|
+
failure_class: Optional[str] = None,
|
|
452
|
+
domain_import_prefix: str = "",
|
|
453
|
+
domain_model_name: str = "",
|
|
454
|
+
domain_model_pascal: str = "",
|
|
455
|
+
) -> None:
|
|
456
|
+
"""Generate component widget file using Jinja template
|
|
457
|
+
|
|
458
|
+
Args:
|
|
459
|
+
component_name: Name of the component
|
|
460
|
+
presentation_dir: Path to presentation directory
|
|
461
|
+
project_name: Name of the project
|
|
462
|
+
component_type: Type of component ('form', 'list', or 'single')
|
|
463
|
+
import_prefix: Import path prefix for the component
|
|
464
|
+
domain_import_prefix: Domain lib path (e.g. domain/note) for package imports in the widget
|
|
465
|
+
domain_model_name: Domain entity file stem (e.g. note)
|
|
466
|
+
domain_model_pascal: Domain entity class name (e.g. Note) for typed switch patterns
|
|
467
|
+
"""
|
|
468
|
+
pascal_name = to_pascal_case(component_name)
|
|
469
|
+
|
|
470
|
+
if component_type == 'form':
|
|
471
|
+
# Form components now use generate_form_widget_from_template
|
|
472
|
+
# This should not be called for form components anymore
|
|
473
|
+
raise ValueError("Use generate_form_widget_from_template for form components")
|
|
474
|
+
elif component_type == 'list':
|
|
475
|
+
raise ValueError(
|
|
476
|
+
"List components must be generated via create_component_list_layers / "
|
|
477
|
+
"create_component_list_layers_empty (not generate_component_widget_from_template).",
|
|
478
|
+
)
|
|
479
|
+
else: # single
|
|
480
|
+
bloc_name = f"{pascal_name}Bloc"
|
|
481
|
+
state_name = f"{pascal_name}State"
|
|
482
|
+
bloc_import = f"import 'package:{project_name}/{import_prefix}/application/{component_name}_bloc.dart';"
|
|
483
|
+
generate_file(project_name, presentation_dir, "component/component_widget_template.jinja", f"{component_name}_component.dart", {
|
|
484
|
+
"component_name": component_name,
|
|
485
|
+
"pascal_name": pascal_name,
|
|
486
|
+
"is_form": False,
|
|
487
|
+
"bloc_name": bloc_name,
|
|
488
|
+
"state_name": state_name,
|
|
489
|
+
"bloc_import": bloc_import,
|
|
490
|
+
"failure_class": failure_class or "",
|
|
491
|
+
"domain_import_prefix": domain_import_prefix,
|
|
492
|
+
"domain_model_name": domain_model_name,
|
|
493
|
+
"domain_model_pascal": domain_model_pascal,
|
|
494
|
+
})
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
def generate_form_event_from_template(component_name: str, field_list: list[dict], app_dir: Path, project_name: str, known_enums: set = None) -> None:
|
|
498
|
+
"""Generate form event file using Jinja template"""
|
|
499
|
+
_enums = known_enums or set()
|
|
500
|
+
pascal_name = to_pascal_case(component_name)
|
|
501
|
+
|
|
502
|
+
field_events = []
|
|
503
|
+
for field in field_list:
|
|
504
|
+
field_name = field['name']
|
|
505
|
+
if field_name == 'id':
|
|
506
|
+
continue
|
|
507
|
+
field_type = field['type']
|
|
508
|
+
base_ft = field_type[:-1] if field_type.endswith('?') else field_type
|
|
509
|
+
meta = get_form_field_metadata(base_ft, field_name, known_enums=_enums)
|
|
510
|
+
capitalized_name = to_pascal_case_preserve(field_name)
|
|
511
|
+
param_type = meta['event_param_type']
|
|
512
|
+
param_name = meta['event_param_name']
|
|
513
|
+
field_events.append(
|
|
514
|
+
f" const factory {pascal_name}FormEvent.{field_name}Changed({param_type} {param_name}) = {capitalized_name}Changed;"
|
|
515
|
+
)
|
|
516
|
+
|
|
517
|
+
field_events_str = "\n".join(field_events)
|
|
518
|
+
|
|
519
|
+
generate_file(project_name, app_dir, "component/component_form_event_template.jinja", f"{component_name}_form_event.dart", {
|
|
520
|
+
"component_name": component_name,
|
|
521
|
+
"pascal_name": pascal_name,
|
|
522
|
+
"field_events": field_events_str,
|
|
523
|
+
})
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
def generate_form_state_from_template(component_name: str, field_list: list[dict], app_dir: Path, project_name: str, known_enums: set = None) -> None:
|
|
527
|
+
"""Generate form state file using Jinja template"""
|
|
528
|
+
_enums = known_enums or set()
|
|
529
|
+
pascal_name = to_pascal_case(component_name)
|
|
530
|
+
|
|
531
|
+
field_declarations = []
|
|
532
|
+
initial_values = []
|
|
533
|
+
for field in field_list:
|
|
534
|
+
field_name = field['name']
|
|
535
|
+
if field_name == 'id':
|
|
536
|
+
continue
|
|
537
|
+
field_type = field['type']
|
|
538
|
+
base_ft = field_type[:-1] if field_type.endswith('?') else field_type
|
|
539
|
+
meta = get_form_field_metadata(base_ft, field_name, known_enums=_enums)
|
|
540
|
+
capitalized_name = to_pascal_case_preserve(field_name)
|
|
541
|
+
|
|
542
|
+
if meta['is_enum']:
|
|
543
|
+
field_declarations.append(f" required {meta['enum_class']} {field_name},")
|
|
544
|
+
initial_values.append(f" {field_name}: {meta['initial_value_expr']},")
|
|
545
|
+
else:
|
|
546
|
+
field_declarations.append(f" required {capitalized_name} {field_name},")
|
|
547
|
+
initial_values.append(f" {field_name}: {capitalized_name}({meta['initial_value_expr']}),")
|
|
548
|
+
|
|
549
|
+
field_declarations_str = "\n".join(field_declarations)
|
|
550
|
+
initial_values_str = "\n".join(initial_values)
|
|
551
|
+
|
|
552
|
+
generate_file(project_name, app_dir, "component/component_form_state_template.jinja", f"{component_name}_form_state.dart", {
|
|
553
|
+
"component_name": component_name,
|
|
554
|
+
"pascal_name": pascal_name,
|
|
555
|
+
"field_declarations": field_declarations_str,
|
|
556
|
+
"initial_values": initial_values_str,
|
|
557
|
+
})
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
def generate_form_bloc_from_template(
|
|
561
|
+
component_name: str,
|
|
562
|
+
field_list: list[dict],
|
|
563
|
+
app_dir: Path,
|
|
564
|
+
project_name: str,
|
|
565
|
+
known_enums: set = None,
|
|
566
|
+
enums_info: Dict = None,
|
|
567
|
+
domain_value_objects_import: Optional[str] = None,
|
|
568
|
+
) -> None:
|
|
569
|
+
"""Generate form bloc file using Jinja template"""
|
|
570
|
+
_enums = known_enums or set()
|
|
571
|
+
_enums_info = enums_info or {}
|
|
572
|
+
pascal_name = to_pascal_case(component_name)
|
|
573
|
+
|
|
574
|
+
extra_imports: list[str] = []
|
|
575
|
+
processed_field_list = []
|
|
576
|
+
for field in field_list:
|
|
577
|
+
field_name = field['name']
|
|
578
|
+
field_type = field['type']
|
|
579
|
+
base_ft = field_type[:-1] if field_type.endswith('?') else field_type
|
|
580
|
+
|
|
581
|
+
field_dict = dict(field)
|
|
582
|
+
field_dict['pascal_name'] = to_pascal_case_preserve(field_name)
|
|
583
|
+
meta = get_form_field_metadata(base_ft, field_name, known_enums=_enums)
|
|
584
|
+
field_dict.update(meta)
|
|
585
|
+
|
|
586
|
+
if meta['is_enum']:
|
|
587
|
+
info = _enums_info.get(base_ft)
|
|
588
|
+
if info:
|
|
589
|
+
imp = f"import 'package:{project_name}/{info['folder']}/{info['file_stem']}.dart';"
|
|
590
|
+
if imp not in extra_imports:
|
|
591
|
+
extra_imports.append(imp)
|
|
592
|
+
|
|
593
|
+
processed_field_list.append(field_dict)
|
|
594
|
+
|
|
595
|
+
generate_file(project_name, app_dir, "component/component_form_bloc_template.jinja", f"{component_name}_form_bloc.dart", {
|
|
596
|
+
"component_name": component_name,
|
|
597
|
+
"pascal_name": pascal_name,
|
|
598
|
+
"field_list": processed_field_list,
|
|
599
|
+
"domain_value_objects_import": domain_value_objects_import,
|
|
600
|
+
"extra_imports": "\n".join(extra_imports),
|
|
601
|
+
})
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
def _parse_value_objects_file(vo_file: Path) -> Dict[str, str]:
|
|
605
|
+
"""Parse a value_objects.dart file and return a mapping from VO class name
|
|
606
|
+
to its underlying Dart type.
|
|
607
|
+
|
|
608
|
+
Looks for ``class Foo extends ValueObject<Bar>`` patterns.
|
|
609
|
+
|
|
610
|
+
Returns:
|
|
611
|
+
e.g. {'Title': 'String', 'Vote': 'int', 'CreatedAt': 'DateTime'}
|
|
612
|
+
"""
|
|
613
|
+
if not vo_file.exists():
|
|
614
|
+
return {}
|
|
615
|
+
try:
|
|
616
|
+
content = vo_file.read_text()
|
|
617
|
+
except Exception:
|
|
618
|
+
return {}
|
|
619
|
+
vo_pattern = r'class\s+(\w+)\s+extends\s+ValueObject\s*<\s*(\w+)\s*>'
|
|
620
|
+
return {m.group(1): m.group(2) for m in re.finditer(vo_pattern, content)}
|
|
621
|
+
|
|
622
|
+
|
|
623
|
+
def _dart_type_to_field_type(dart_type: str) -> str:
|
|
624
|
+
"""Convert a Dart type string (from ``ValueObject<T>``) to the lowercase
|
|
625
|
+
field-type convention used by the rest of the pipeline.
|
|
626
|
+
|
|
627
|
+
'String' -> 'string', 'DateTime' -> 'datetime', 'int' -> 'int', etc.
|
|
628
|
+
"""
|
|
629
|
+
mapping = {
|
|
630
|
+
'String': 'string',
|
|
631
|
+
'int': 'int',
|
|
632
|
+
'double': 'double',
|
|
633
|
+
'bool': 'bool',
|
|
634
|
+
'DateTime': 'datetime',
|
|
635
|
+
}
|
|
636
|
+
return mapping.get(dart_type, dart_type.lower())
|
|
637
|
+
|
|
638
|
+
|
|
639
|
+
def _resolve_entity_field_type(
|
|
640
|
+
raw_type: str,
|
|
641
|
+
vo_type_map: Dict[str, str],
|
|
642
|
+
known_enums: set,
|
|
643
|
+
) -> str:
|
|
644
|
+
"""Resolve an entity field's Dart type to the form-field type string.
|
|
645
|
+
|
|
646
|
+
Resolution order:
|
|
647
|
+
1. Nullable (``T?``) – strip ``?``, resolve inner, re-append ``?``
|
|
648
|
+
2. ``Option<T>`` – resolve inner T, append ``?``
|
|
649
|
+
3. Known ValueObject – map via *vo_type_map* to underlying primitive
|
|
650
|
+
4. Known enum – keep class name as-is
|
|
651
|
+
5. Dart primitive – lowercase
|
|
652
|
+
6. Generic collection (``List<T>``, etc.) – returned as-is
|
|
653
|
+
7. Fallback – ``'string'``
|
|
654
|
+
"""
|
|
655
|
+
if raw_type.endswith('?'):
|
|
656
|
+
base = raw_type[:-1].strip()
|
|
657
|
+
inner = _resolve_entity_field_type(base, vo_type_map, known_enums)
|
|
658
|
+
return inner if inner.endswith('?') else inner + '?'
|
|
659
|
+
|
|
660
|
+
option_match = re.match(r'^Option\s*<\s*(.+)\s*>$', raw_type)
|
|
661
|
+
if option_match:
|
|
662
|
+
inner = option_match.group(1).strip()
|
|
663
|
+
resolved = _resolve_entity_field_type(inner, vo_type_map, known_enums)
|
|
664
|
+
return resolved if resolved.endswith('?') else resolved + '?'
|
|
665
|
+
|
|
666
|
+
if raw_type in vo_type_map:
|
|
667
|
+
return _dart_type_to_field_type(vo_type_map[raw_type])
|
|
668
|
+
|
|
669
|
+
if raw_type in known_enums:
|
|
670
|
+
return raw_type
|
|
671
|
+
|
|
672
|
+
primitive_map = {
|
|
673
|
+
'String': 'string',
|
|
674
|
+
'int': 'int',
|
|
675
|
+
'double': 'double',
|
|
676
|
+
'bool': 'bool',
|
|
677
|
+
'DateTime': 'datetime',
|
|
678
|
+
}
|
|
679
|
+
if raw_type in primitive_map:
|
|
680
|
+
return primitive_map[raw_type]
|
|
681
|
+
|
|
682
|
+
if '<' in raw_type and '>' in raw_type:
|
|
683
|
+
return raw_type
|
|
684
|
+
|
|
685
|
+
return 'string'
|
|
686
|
+
|
|
687
|
+
|
|
688
|
+
def get_model_fields_from_domain(lib_path: Path, domain_folder: str, model_name: str, model_folder: Optional[str] = None) -> List[dict]:
|
|
689
|
+
"""Extract field information from a domain model entity file.
|
|
690
|
+
|
|
691
|
+
Reads the entity file **and** both core and local ``value_objects.dart``
|
|
692
|
+
to resolve ValueObject wrappers to their underlying Dart types. Also
|
|
693
|
+
detects enums from the ``domain/enums/`` folder.
|
|
694
|
+
|
|
695
|
+
Args:
|
|
696
|
+
lib_path: Path to lib/ directory
|
|
697
|
+
domain_folder: Name of domain folder (e.g., 'domain')
|
|
698
|
+
model_name: Name of the domain model (file stem)
|
|
699
|
+
model_folder: Containing folder for the model. Defaults to model_name.
|
|
700
|
+
|
|
701
|
+
Returns:
|
|
702
|
+
List of field dicts ``{'name': ..., 'type': ...}`` compatible with
|
|
703
|
+
the existing ``field_list`` format used by form generators.
|
|
704
|
+
"""
|
|
705
|
+
if model_folder is None:
|
|
706
|
+
model_folder = model_name
|
|
707
|
+
|
|
708
|
+
entity_file = lib_path / domain_folder / model_folder / "model" / f"{model_name}.dart"
|
|
709
|
+
|
|
710
|
+
if not entity_file.exists():
|
|
711
|
+
raise FileNotFoundError(f"Domain model entity file not found: {entity_file}")
|
|
712
|
+
|
|
713
|
+
content = entity_file.read_text()
|
|
714
|
+
|
|
715
|
+
# --- Build ValueObject → underlying type mapping ----------------------
|
|
716
|
+
vo_type_map: Dict[str, str] = {}
|
|
717
|
+
core_vo_file = lib_path / "core" / "model" / "value_objects.dart"
|
|
718
|
+
vo_type_map.update(_parse_value_objects_file(core_vo_file))
|
|
719
|
+
local_vo_file = lib_path / domain_folder / model_folder / "model" / "value_objects.dart"
|
|
720
|
+
vo_type_map.update(_parse_value_objects_file(local_vo_file))
|
|
721
|
+
|
|
722
|
+
# --- Discover enums ---------------------------------------------------
|
|
723
|
+
from .feature import find_enums_with_info
|
|
724
|
+
enums_info = find_enums_with_info(lib_path, domain_folder)
|
|
725
|
+
known_enums = set(enums_info.keys())
|
|
726
|
+
|
|
727
|
+
# --- Parse factory constructor fields ---------------------------------
|
|
728
|
+
factory_pattern = r'const factory\s+\w+\(\{([^}]+)\}\)'
|
|
729
|
+
match = re.search(factory_pattern, content, re.DOTALL)
|
|
730
|
+
|
|
731
|
+
if not match:
|
|
732
|
+
raise ValueError(f"Could not parse entity file: {entity_file}")
|
|
733
|
+
|
|
734
|
+
fields_section = match.group(1)
|
|
735
|
+
|
|
736
|
+
# Capture full type including generics: ``required Option<Desc> name,``
|
|
737
|
+
field_pattern = r'required\s+(.+?)\s+(\w+)\s*[,}]'
|
|
738
|
+
fields = []
|
|
739
|
+
|
|
740
|
+
for field_match in re.finditer(field_pattern, fields_section):
|
|
741
|
+
raw_type = field_match.group(1).strip()
|
|
742
|
+
field_name = field_match.group(2)
|
|
743
|
+
|
|
744
|
+
mapped_type = _resolve_entity_field_type(raw_type, vo_type_map, known_enums)
|
|
745
|
+
|
|
746
|
+
fields.append({
|
|
747
|
+
'name': field_name,
|
|
748
|
+
'type': mapped_type,
|
|
749
|
+
})
|
|
750
|
+
|
|
751
|
+
return fields
|
|
752
|
+
|
|
753
|
+
|
|
754
|
+
def _create_component_list_layers_empty(component_dir: Path, component_name: str, project_name: str, folder: Optional[str]) -> None:
|
|
755
|
+
"""Create all layers for an empty (Vuoto) list component with no domain model."""
|
|
756
|
+
if folder:
|
|
757
|
+
component_import_prefix = f"{folder.replace('/', '/')}/{component_name}"
|
|
758
|
+
else:
|
|
759
|
+
component_import_prefix = component_name
|
|
760
|
+
|
|
761
|
+
ensure_common_widgets(project_name, infer_lib_path(component_dir))
|
|
762
|
+
|
|
763
|
+
app_dir = component_dir / "application"
|
|
764
|
+
app_dir.mkdir(exist_ok=True)
|
|
765
|
+
|
|
766
|
+
component_pascal = to_pascal_case_preserve(component_name)
|
|
767
|
+
freezed_mixin_event = "_$" + component_pascal + "Event"
|
|
768
|
+
freezed_mixin_state = "_$" + component_pascal + "State"
|
|
769
|
+
|
|
770
|
+
event_content = f"""part of '{component_name}_bloc.dart';
|
|
771
|
+
|
|
772
|
+
@freezed
|
|
773
|
+
abstract class {component_pascal}Event with {freezed_mixin_event} {{
|
|
774
|
+
const factory {component_pascal}Event.loadRequested() = LoadRequested;
|
|
775
|
+
}}
|
|
776
|
+
"""
|
|
777
|
+
(app_dir / f"{component_name}_event.dart").write_text(event_content)
|
|
778
|
+
|
|
779
|
+
state_content = f"""part of '{component_name}_bloc.dart';
|
|
780
|
+
|
|
781
|
+
@freezed
|
|
782
|
+
abstract class {component_pascal}State with {freezed_mixin_state} {{
|
|
783
|
+
const factory {component_pascal}State.initial() = Initial;
|
|
784
|
+
const factory {component_pascal}State.loading() = Loading;
|
|
785
|
+
const factory {component_pascal}State.loaded() = Loaded;
|
|
786
|
+
const factory {component_pascal}State.error(String message) = Error;
|
|
787
|
+
}}
|
|
788
|
+
"""
|
|
789
|
+
(app_dir / f"{component_name}_state.dart").write_text(state_content)
|
|
790
|
+
|
|
791
|
+
bloc_content = f"""import 'package:bloc/bloc.dart';
|
|
792
|
+
import 'package:freezed_annotation/freezed_annotation.dart';
|
|
793
|
+
|
|
794
|
+
part '{component_name}_bloc.freezed.dart';
|
|
795
|
+
part '{component_name}_event.dart';
|
|
796
|
+
part '{component_name}_state.dart';
|
|
797
|
+
|
|
798
|
+
class {component_pascal}Bloc extends Bloc<{component_pascal}Event, {component_pascal}State> {{
|
|
799
|
+
{component_pascal}Bloc() : super(const {component_pascal}State.initial()) {{
|
|
800
|
+
on<LoadRequested>(_onLoadRequested);
|
|
801
|
+
}}
|
|
802
|
+
|
|
803
|
+
Future<void> _onLoadRequested(LoadRequested event, Emitter<{component_pascal}State> emit) async {{
|
|
804
|
+
emit(const {component_pascal}State.loading());
|
|
805
|
+
// TODO: implement load logic
|
|
806
|
+
emit(const {component_pascal}State.loaded());
|
|
807
|
+
}}
|
|
808
|
+
}}
|
|
809
|
+
"""
|
|
810
|
+
(app_dir / f"{component_name}_bloc.dart").write_text(bloc_content)
|
|
811
|
+
|
|
812
|
+
presentation_dir = component_dir / "presentation"
|
|
813
|
+
presentation_dir.mkdir(exist_ok=True)
|
|
814
|
+
|
|
815
|
+
generate_file(project_name, presentation_dir, "component/component_list_widget_template.jinja", f"{component_name}_component.dart", {
|
|
816
|
+
"component_name": component_name,
|
|
817
|
+
"component_pascal": component_pascal,
|
|
818
|
+
"component_import_prefix": component_import_prefix,
|
|
819
|
+
"empty_component": True,
|
|
820
|
+
})
|
|
821
|
+
|
|
822
|
+
|
|
823
|
+
def create_component_list_layers(component_dir: Path, component_name: str, project_name: str, folder: Optional[str], domain_model_name: Optional[str] = None, domain_folder: Optional[str] = None, domain_model_folder: Optional[str] = None, lib_path: Optional[Path] = None) -> None:
|
|
824
|
+
"""Create all layers for a list component.
|
|
825
|
+
|
|
826
|
+
Generates events and handlers based on the actual methods available
|
|
827
|
+
in the repository interface (e.g., skips delete if no delete method exists).
|
|
828
|
+
If domain_model_name is None, generates an empty (Vuoto) scaffold without a domain model.
|
|
829
|
+
|
|
830
|
+
Args:
|
|
831
|
+
component_dir: Path to component directory
|
|
832
|
+
component_name: Name of the component
|
|
833
|
+
project_name: Name of the project
|
|
834
|
+
folder: Optional folder path
|
|
835
|
+
domain_model_name: Domain model name / file stem (None for empty scaffold)
|
|
836
|
+
domain_folder: Domain folder name (e.g., 'domain')
|
|
837
|
+
domain_model_folder: Containing folder for the model. Defaults to domain_model_name.
|
|
838
|
+
lib_path: Optional path to lib/ directory (for reading repository interface)
|
|
839
|
+
"""
|
|
840
|
+
if domain_model_name is None:
|
|
841
|
+
_create_component_list_layers_empty(component_dir, component_name, project_name, folder)
|
|
842
|
+
return
|
|
843
|
+
|
|
844
|
+
if domain_model_folder is None:
|
|
845
|
+
domain_model_folder = domain_model_name
|
|
846
|
+
|
|
847
|
+
# Build import paths
|
|
848
|
+
if folder:
|
|
849
|
+
component_import_prefix = f"{folder.replace('/', '/')}/{component_name}"
|
|
850
|
+
else:
|
|
851
|
+
component_import_prefix = component_name
|
|
852
|
+
|
|
853
|
+
domain_import_prefix = f"{domain_folder}/{domain_model_folder}"
|
|
854
|
+
|
|
855
|
+
resolved_lib_path = lib_path or infer_lib_path(component_dir)
|
|
856
|
+
ensure_common_widgets(project_name, resolved_lib_path)
|
|
857
|
+
|
|
858
|
+
# Application layer
|
|
859
|
+
app_dir = component_dir / "application"
|
|
860
|
+
app_dir.mkdir(exist_ok=True)
|
|
861
|
+
|
|
862
|
+
# Get PascalCase names
|
|
863
|
+
component_pascal = to_pascal_case_preserve(component_name)
|
|
864
|
+
domain_model_pascal = to_pascal_case_preserve(domain_model_name)
|
|
865
|
+
|
|
866
|
+
# Read repository interface to detect failure type and available methods
|
|
867
|
+
repo_info = None
|
|
868
|
+
if resolved_lib_path:
|
|
869
|
+
repo_info = get_repository_info(resolved_lib_path, domain_folder, domain_model_name, domain_model_folder)
|
|
870
|
+
|
|
871
|
+
failure_class = repo_info['failure_class'] if repo_info else f"{domain_model_pascal}Failure"
|
|
872
|
+
failure_import = f"import '{repo_info['failure_import']}';" if repo_info else f"import 'package:{project_name}/{domain_import_prefix}/model/{domain_model_name}_failure.dart';"
|
|
873
|
+
available_methods = repo_info['methods'] if repo_info else {'getAll', 'getById', 'create', 'update', 'delete'}
|
|
874
|
+
|
|
875
|
+
has_get_all = 'getAll' in available_methods
|
|
876
|
+
has_create = 'create' in available_methods
|
|
877
|
+
has_update = 'update' in available_methods
|
|
878
|
+
has_delete = 'delete' in available_methods
|
|
879
|
+
|
|
880
|
+
freezed_mixin_event = "_$" + component_pascal + "Event"
|
|
881
|
+
freezed_mixin_state = "_$" + component_pascal + "State"
|
|
882
|
+
|
|
883
|
+
# Build events dynamically based on available repository methods
|
|
884
|
+
event_lines = []
|
|
885
|
+
if has_get_all:
|
|
886
|
+
event_lines.append(f" const factory {component_pascal}Event.loadRequested() = LoadRequested;")
|
|
887
|
+
event_lines.append(f" const factory {component_pascal}Event.reloadRequested() = ReloadRequested;")
|
|
888
|
+
if has_create:
|
|
889
|
+
event_lines.append(f" const factory {component_pascal}Event.createRequested({domain_model_pascal} item) = CreateRequested;")
|
|
890
|
+
if has_update:
|
|
891
|
+
event_lines.append(f" const factory {component_pascal}Event.updateRequested({domain_model_pascal} item) = UpdateRequested;")
|
|
892
|
+
if has_delete:
|
|
893
|
+
event_lines.append(f" const factory {component_pascal}Event.deleteRequested(String id) = DeleteRequested;")
|
|
894
|
+
events_str = "\n".join(event_lines)
|
|
895
|
+
|
|
896
|
+
event_content = f"""part of '{component_name}_bloc.dart';
|
|
897
|
+
|
|
898
|
+
@freezed
|
|
899
|
+
abstract class {component_pascal}Event with {freezed_mixin_event} {{
|
|
900
|
+
{events_str}
|
|
901
|
+
}}
|
|
902
|
+
"""
|
|
903
|
+
(app_dir / f"{component_name}_event.dart").write_text(event_content)
|
|
904
|
+
|
|
905
|
+
# Create state file
|
|
906
|
+
state_content = f"""part of '{component_name}_bloc.dart';
|
|
907
|
+
|
|
908
|
+
@freezed
|
|
909
|
+
abstract class {component_pascal}State with {freezed_mixin_state} {{
|
|
910
|
+
const factory {component_pascal}State.initial() = Initial;
|
|
911
|
+
const factory {component_pascal}State.loading() = Loading;
|
|
912
|
+
const factory {component_pascal}State.loaded(List<{domain_model_pascal}> items, {{@Default(false) bool isReloading}}) = Loaded;
|
|
913
|
+
const factory {component_pascal}State.error({failure_class} failure) = Error;
|
|
914
|
+
}}
|
|
915
|
+
"""
|
|
916
|
+
(app_dir / f"{component_name}_state.dart").write_text(state_content)
|
|
917
|
+
|
|
918
|
+
# Build BLoC handlers dynamically
|
|
919
|
+
reload_snippet = f"""final Either<{failure_class}, List<{domain_model_pascal}>> itemsResult = await _repository.getAll();
|
|
920
|
+
itemsResult.fold(
|
|
921
|
+
({failure_class} failure) => emit({component_pascal}State.error(failure)),
|
|
922
|
+
(List<{domain_model_pascal}> items) => emit({component_pascal}State.loaded(items)),
|
|
923
|
+
);"""
|
|
924
|
+
|
|
925
|
+
on_registrations = []
|
|
926
|
+
handler_methods = []
|
|
927
|
+
|
|
928
|
+
if has_get_all:
|
|
929
|
+
on_registrations.append(" on<LoadRequested>(_onLoadRequested);")
|
|
930
|
+
on_registrations.append(" on<ReloadRequested>(_onReloadRequested);")
|
|
931
|
+
handler_methods.append(f""" Future<void> _onLoadRequested(LoadRequested event, Emitter<{component_pascal}State> emit) async {{
|
|
932
|
+
emit(const {component_pascal}State.loading());
|
|
933
|
+
final Either<{failure_class}, List<{domain_model_pascal}>> result = await _repository.getAll();
|
|
934
|
+
result.fold(
|
|
935
|
+
({failure_class} failure) => emit({component_pascal}State.error(failure)),
|
|
936
|
+
(List<{domain_model_pascal}> items) => emit({component_pascal}State.loaded(items)),
|
|
937
|
+
);
|
|
938
|
+
}}""")
|
|
939
|
+
handler_methods.append(f""" Future<void> _onReloadRequested(ReloadRequested event, Emitter<{component_pascal}State> emit) async {{
|
|
940
|
+
if (state is! Loaded) return;
|
|
941
|
+
final Loaded current = state as Loaded;
|
|
942
|
+
emit(current.copyWith(isReloading: true));
|
|
943
|
+
final Either<{failure_class}, List<{domain_model_pascal}>> result = await _repository.getAll();
|
|
944
|
+
result.fold(
|
|
945
|
+
({failure_class} failure) => emit(current.copyWith(isReloading: false)),
|
|
946
|
+
(List<{domain_model_pascal}> items) => emit({component_pascal}State.loaded(items)),
|
|
947
|
+
);
|
|
948
|
+
}}""")
|
|
949
|
+
|
|
950
|
+
if has_create:
|
|
951
|
+
on_registrations.append(" on<CreateRequested>(_onCreateRequested);")
|
|
952
|
+
handler_methods.append(f""" Future<void> _onCreateRequested(CreateRequested event, Emitter<{component_pascal}State> emit) async {{
|
|
953
|
+
emit(const {component_pascal}State.loading());
|
|
954
|
+
final Either<{failure_class}, Unit> result = await _repository.create(event.item);
|
|
955
|
+
result.fold(
|
|
956
|
+
({failure_class} failure) => emit({component_pascal}State.error(failure)),
|
|
957
|
+
(Unit _) async {{
|
|
958
|
+
{reload_snippet}
|
|
959
|
+
}},
|
|
960
|
+
);
|
|
961
|
+
}}""")
|
|
962
|
+
|
|
963
|
+
if has_update:
|
|
964
|
+
on_registrations.append(" on<UpdateRequested>(_onUpdateRequested);")
|
|
965
|
+
handler_methods.append(f""" Future<void> _onUpdateRequested(UpdateRequested event, Emitter<{component_pascal}State> emit) async {{
|
|
966
|
+
emit(const {component_pascal}State.loading());
|
|
967
|
+
final Either<{failure_class}, Unit> result = await _repository.update(event.item);
|
|
968
|
+
result.fold(
|
|
969
|
+
({failure_class} failure) => emit({component_pascal}State.error(failure)),
|
|
970
|
+
(Unit _) async {{
|
|
971
|
+
{reload_snippet}
|
|
972
|
+
}},
|
|
973
|
+
);
|
|
974
|
+
}}""")
|
|
975
|
+
|
|
976
|
+
if has_delete:
|
|
977
|
+
on_registrations.append(" on<DeleteRequested>(_onDeleteRequested);")
|
|
978
|
+
handler_methods.append(f""" Future<void> _onDeleteRequested(DeleteRequested event, Emitter<{component_pascal}State> emit) async {{
|
|
979
|
+
emit(const {component_pascal}State.loading());
|
|
980
|
+
final Either<{failure_class}, Unit> result = await _repository.delete(event.id);
|
|
981
|
+
result.fold(
|
|
982
|
+
({failure_class} failure) => emit({component_pascal}State.error(failure)),
|
|
983
|
+
(Unit _) async {{
|
|
984
|
+
{reload_snippet}
|
|
985
|
+
}},
|
|
986
|
+
);
|
|
987
|
+
}}""")
|
|
988
|
+
|
|
989
|
+
on_registrations_str = "\n".join(on_registrations)
|
|
990
|
+
handler_methods_str = "\n\n".join(handler_methods)
|
|
991
|
+
|
|
992
|
+
bloc_content = f"""import 'package:bloc/bloc.dart';
|
|
993
|
+
import 'package:dartz/dartz.dart';
|
|
994
|
+
import 'package:freezed_annotation/freezed_annotation.dart';
|
|
995
|
+
import 'package:{project_name}/{domain_import_prefix}/model/{domain_model_name}.dart';
|
|
996
|
+
{failure_import}
|
|
997
|
+
import 'package:{project_name}/{domain_import_prefix}/model/i_{domain_model_name}_repository.dart';
|
|
998
|
+
|
|
999
|
+
part '{component_name}_bloc.freezed.dart';
|
|
1000
|
+
part '{component_name}_event.dart';
|
|
1001
|
+
part '{component_name}_state.dart';
|
|
1002
|
+
|
|
1003
|
+
class {component_pascal}Bloc extends Bloc<{component_pascal}Event, {component_pascal}State> {{
|
|
1004
|
+
final I{domain_model_pascal}Repository _repository;
|
|
1005
|
+
|
|
1006
|
+
{component_pascal}Bloc(this._repository) : super(const {component_pascal}State.initial()) {{
|
|
1007
|
+
{on_registrations_str}
|
|
1008
|
+
}}
|
|
1009
|
+
|
|
1010
|
+
{handler_methods_str}
|
|
1011
|
+
}}
|
|
1012
|
+
"""
|
|
1013
|
+
(app_dir / f"{component_name}_bloc.dart").write_text(bloc_content)
|
|
1014
|
+
|
|
1015
|
+
# Presentation layer
|
|
1016
|
+
presentation_dir = component_dir / "presentation"
|
|
1017
|
+
presentation_dir.mkdir(exist_ok=True)
|
|
1018
|
+
|
|
1019
|
+
# Create component widget using list template
|
|
1020
|
+
generate_file(project_name, presentation_dir, "component/component_list_widget_template.jinja", f"{component_name}_component.dart", {
|
|
1021
|
+
"component_name": component_name,
|
|
1022
|
+
"component_pascal": component_pascal,
|
|
1023
|
+
"component_import_prefix": component_import_prefix,
|
|
1024
|
+
"empty_component": False,
|
|
1025
|
+
"failure_class": failure_class,
|
|
1026
|
+
"domain_import_prefix": domain_import_prefix,
|
|
1027
|
+
"domain_model_name": domain_model_name,
|
|
1028
|
+
"domain_model_pascal": domain_model_pascal,
|
|
1029
|
+
})
|
|
1030
|
+
|