diff --git a/pse/generators/dotnet/mapper.py b/pse/generators/dotnet/mapper.py index ed22062..9b63126 100644 --- a/pse/generators/dotnet/mapper.py +++ b/pse/generators/dotnet/mapper.py @@ -5,6 +5,7 @@ Entity, ValueObject, Aggregate, + CapabilitySelection, Infrastructure, Database, Cache, @@ -59,7 +60,13 @@ def map_capabilities(capabilities): if not capabilities: return [] - return [c.name.lower() for c in capabilities.capabilities] + return [ + CapabilitySelection( + name=c.name.lower(), + implementation=c.implementation.lower() if getattr(c, "implementation", None) else None, + ) + for c in capabilities.capabilities + ] def map_infra(infra): diff --git a/pse/generators/dotnet/nuget.py b/pse/generators/dotnet/nuget.py index a3bc0aa..032073b 100644 --- a/pse/generators/dotnet/nuget.py +++ b/pse/generators/dotnet/nuget.py @@ -15,9 +15,9 @@ def resolve_packages(ctx): def capability_project_map(): return { - "logging": "Application", - "validation": "Application", - "mapping": "Application", + "logging": "API", + "validation": "API", + "mapping": "API", "cqrs": "Application", "database": "Infrastructure", "cache": "Infrastructure", @@ -33,6 +33,15 @@ def project_path(ctx, layer: str): return os.path.join(output_dir, project_name, f"{project_name}.csproj") +def first_existing_project(ctx, layers): + for layer in layers: + path = project_path(ctx, layer) + if os.path.exists(path): + return path + + return None + + def restore_packages(ctx): packages = resolve_packages(ctx) @@ -46,15 +55,71 @@ def restore_packages(ctx): pkg_list = ctx.packages.get(implementation, {}).get("packages", []) - target_layer = project_map.get(capability, "Application") - target_project = project_path(ctx, target_layer) + target_projects = package_target_projects(ctx, capability, project_map) + if not target_projects: + target_projects = [default_project] + + for package in pkg_list: + package_name, package_version = resolve_package(package, ctx.versions) + for target_project in target_projects: + command = [ + "add", + target_project, + "package", + package_name, + ] + if package_version: + command.extend(["--version", package_version]) + + run_dotnet(command, cwd=ctx.output_dir) + + +def package_target_projects(ctx, capability: str, project_map): + if capability in {"logging", "validation", "mapping"}: + project = first_existing_project(ctx, ["API", "Presentation", "Gateway"]) + return [project] if project else [] + + if capability == "cqrs": + projects = [ + project_path(ctx, "Application"), + first_existing_project(ctx, ["API", "Presentation", "Gateway"]), + ] + return unique_existing_projects(projects) + + target_layer = project_map.get(capability, "Application") + project = project_path(ctx, target_layer) + return [project] if os.path.exists(project) else [] + + +def unique_existing_projects(projects): + result = [] + seen = set() + for project in projects: + if not project or not os.path.exists(project): + continue + normalized = os.path.abspath(project) + if normalized in seen: + continue + seen.add(normalized) + result.append(project) + + return result + + +def resolve_package(package, versions): + if isinstance(package, str): + return package, None + + if not isinstance(package, dict): + raise ValueError(f"Unsupported package entry: {package!r}") + + name = package.get("name") + version_key = package.get("version") + + if not name: + raise ValueError(f"Package entry is missing a name: {package!r}") - if not os.path.exists(target_project): - target_project = default_project + if not version_key: + return name, None - for pkg in pkg_list: - run_dotnet([ - "add", - target_project, - "package", pkg - ], cwd=ctx.output_dir) + return name, versions.get(version_key, version_key) diff --git a/pse/generators/dotnet/projects.py b/pse/generators/dotnet/projects.py index adeafd1..9a5f2c1 100644 --- a/pse/generators/dotnet/projects.py +++ b/pse/generators/dotnet/projects.py @@ -85,20 +85,114 @@ def ensure_program(project_dir: str, project_name: str, layer: str, ctx): return program_path = os.path.join(project_dir, "Program.cs") - content = render_template("Program.cs.tmpl", build_program_values(ctx)) + content = render_template("Program.cs.tmpl", build_program_values(ctx, layer)) with open(program_path, "w", encoding="utf-8") as f: f.write(content) -def build_program_values(ctx): +def build_program_values(ctx, layer: str): infra = ctx.architecture.infrastructure base = ctx.architecture.project.name + contexts = ctx.architecture.contexts using_lines = [] + host_configuration = [] + service_registrations = [] registrations = [] pipeline = [] + has_context_entities = any( + getattr(context, "entities", None) + for context in contexts or [] + ) + + if capability_enabled(ctx, "logging"): + using_lines.append("using Serilog;\n") + host_configuration.extend([ + "builder.Host.UseSerilog((context, services, loggerConfiguration) =>", + " loggerConfiguration", + " .ReadFrom.Configuration(context.Configuration)", + " .ReadFrom.Services(services)", + " .WriteTo.Console());", + "", + ]) + + if capability_enabled(ctx, "validation"): + using_lines.append("using FluentValidation;\n") + using_lines.append("using FluentValidation.AspNetCore;\n") + service_registrations.extend([ + "builder.Services.AddFluentValidationAutoValidation();", + "builder.Services.AddValidatorsFromAssemblyContaining();", + "", + ]) + + if capability_enabled(ctx, "mapping"): + using_lines.append("using Mapster;\n") + using_lines.append("using MapsterMapper;\n") + using_lines.append(f"using {base}.{layer}.Mapping;\n") + service_registrations.extend([ + "MappingConfig.Register();", + "builder.Services.AddMapster();", + "", + ]) + + cqrs_implementation = capability_value(ctx, "cqrs") + first_entity = next( + ( + entity + for context in contexts or [] + for entity in getattr(context, "entities", []) or [] + ), + None, + ) + if has_context_entities: + using_lines.append(f"using {base}.Application.Interfaces;\n") + using_lines.append(f"using {base}.Application.Services;\n") + for context in contexts or []: + for entity in context.entities: + service_registrations.append( + f"builder.Services.AddScoped();" + ) + + service_registrations.append("") + + if cqrs_implementation in {"mediatr", "wolverine"} and has_context_entities: + if cqrs_implementation == "mediatr" and first_entity: + using_lines.append(f"using {base}.Application.Cqrs;\n") + service_registrations.extend([ + f"builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssemblyContaining());", + "", + ]) + + if cqrs_implementation == "wolverine": + using_lines.append("using Wolverine;\n") + if first_entity: + using_lines.append(f"using {base}.Application.Cqrs;\n") + host_configuration.extend([ + "builder.Host.UseWolverine(options =>", + "{", + f" options.Discovery.IncludeAssembly(typeof(GetAll{first_entity.name}Query).Assembly);", + "});", + "", + ]) + else: + host_configuration.extend([ + "builder.Host.UseWolverine();", + "", + ]) + + if has_context_entities: + using_lines.append(f"using {base}.Domain.Repositories;\n") + using_lines.append(f"using {base}.Infrastructure.Repositories;\n") + for context in contexts or []: + for entity in context.entities: + service_registrations.append( + f"builder.Services.AddSingleton();" + ) + + service_registrations.append("") + if infra and (infra.database or infra.cache or infra.broker): using_lines.append(f"using {base}.Application.Options;\n") @@ -135,12 +229,20 @@ def build_program_values(ctx): ]) using_block = "".join(using_lines) + host_configuration_block = "\n".join(host_configuration) + service_registrations_block = "\n".join(service_registrations) registrations_block = "\n".join(registrations) pipeline_block = "\n".join(pipeline) if using_block: using_block += "\n" + if host_configuration_block: + host_configuration_block += "\n" + + if service_registrations_block: + service_registrations_block += "\n" + if registrations_block: registrations_block += "\n" @@ -149,11 +251,24 @@ def build_program_values(ctx): return { "UsingLines": using_block, + "HostConfiguration": host_configuration_block, + "ServiceRegistrations": service_registrations_block, "InfraRegistrations": registrations_block, "InfraPipeline": pipeline_block, } +def capability_enabled(ctx, name: str): + capabilities = getattr(getattr(ctx, "capabilities", None), "capabilities", {}) or {} + return name.lower() in capabilities + + +def capability_value(ctx, name: str): + capabilities = getattr(getattr(ctx, "capabilities", None), "capabilities", {}) or {} + capability = capabilities.get(name.lower()) + return getattr(capability, "value", None) + + def add_project_references(ctx, output_dir: str, base: str, layers): layer_paths = { layer: os.path.join(output_dir, f"{base}.{layer}", f"{base}.{layer}.csproj") diff --git a/pse/generators/dotnet/structure.py b/pse/generators/dotnet/structure.py index 59f269f..e372b02 100644 --- a/pse/generators/dotnet/structure.py +++ b/pse/generators/dotnet/structure.py @@ -44,12 +44,12 @@ def create_structure(ctx): tests_root = os.path.join(output_dir, f"{base}.Tests") if os.path.isdir(api_root): - create_api_structure(api_root, contexts) + create_api_structure(api_root, contexts, ctx) if os.path.isdir(presentation_root): - create_presentation_structure(presentation_root, contexts) + create_presentation_structure(presentation_root, contexts, ctx) - create_application_structure(app_root, contexts) + create_application_structure(app_root, contexts, ctx) create_domain_structure(domain_root, contexts) create_infrastructure_structure(infra_root, contexts) diff --git a/pse/generators/dotnet/structure_archetypes.py b/pse/generators/dotnet/structure_archetypes.py index e107461..9416563 100644 --- a/pse/generators/dotnet/structure_archetypes.py +++ b/pse/generators/dotnet/structure_archetypes.py @@ -2,11 +2,11 @@ from .structure_helpers import context_by_name, ensure_dir, ensure_placeholder from .structure_sections import ( + create_application_service_files, create_context_api_files, create_domain_files, create_infrastructure_structure, create_repository_implementations, - create_use_case_files, ) @@ -33,12 +33,13 @@ def create_modular_monolith_structure(modules_root: str, contexts): ensure_dir(modules_root, f"{module_root}/API/Dtos") create_context_api_files(modules_root, [selected_context], root_prefix=f"{module_root}/API") - ensure_dir(modules_root, f"{module_root}/Application/UseCases") + ensure_dir(modules_root, f"{module_root}/Application/Interfaces") + ensure_dir(modules_root, f"{module_root}/Application/Services") ensure_dir(modules_root, f"{module_root}/Domain/Entities") ensure_dir(modules_root, f"{module_root}/Domain/Repositories") ensure_dir(modules_root, f"{module_root}/Infrastructure/Persistence") - create_use_case_files(modules_root, [selected_context], root_prefix=f"{module_root}/Application") + create_application_service_files(modules_root, [selected_context], root_prefix=f"{module_root}/Application") create_domain_files(modules_root, [selected_context], root_prefix=f"{module_root}/Domain") ensure_placeholder(modules_root, f"{module_root}/Infrastructure/Persistence", "PersistenceOptions.cs", "Module persistence options") create_repository_implementations(modules_root, [selected_context], root_prefix=f"{module_root}/Infrastructure") @@ -72,13 +73,14 @@ def create_microservices_structure(services_root: str, gateway_root: str, shared ensure_dir(services_root, f"{service_root}/API/Controllers") ensure_dir(services_root, f"{service_root}/API/Dtos") - ensure_dir(services_root, f"{service_root}/Application/UseCases") + ensure_dir(services_root, f"{service_root}/Application/Interfaces") + ensure_dir(services_root, f"{service_root}/Application/Services") ensure_dir(services_root, f"{service_root}/Domain/Entities") ensure_dir(services_root, f"{service_root}/Domain/Repositories") ensure_dir(services_root, f"{service_root}/Infrastructure/Persistence") create_context_api_files(services_root, [selected_context], root_prefix=f"{service_root}/API") - create_use_case_files(services_root, [selected_context], root_prefix=f"{service_root}/Application") + create_application_service_files(services_root, [selected_context], root_prefix=f"{service_root}/Application") create_domain_files(services_root, [selected_context], root_prefix=f"{service_root}/Domain") ensure_placeholder(services_root, f"{service_root}/Infrastructure/Persistence", "PersistenceOptions.cs", "Service persistence options") - create_repository_implementations(services_root, [selected_context], root_prefix=f"{service_root}/Infrastructure") \ No newline at end of file + create_repository_implementations(services_root, [selected_context], root_prefix=f"{service_root}/Infrastructure") diff --git a/pse/generators/dotnet/structure_helpers.py b/pse/generators/dotnet/structure_helpers.py index de5e724..932bd07 100644 --- a/pse/generators/dotnet/structure_helpers.py +++ b/pse/generators/dotnet/structure_helpers.py @@ -64,7 +64,7 @@ def find_project_root_name(path: str): def build_properties(properties): if not properties: - return " // TODO: add properties.\n", False + return " // No properties defined.\n", False lines = [] needs_system = False @@ -122,4 +122,4 @@ def pick_id_property_name(properties): if prop_name.lower() == "id": return prop_name - return next(iter(properties.keys())) \ No newline at end of file + return next(iter(properties.keys())) diff --git a/pse/generators/dotnet/structure_sections.py b/pse/generators/dotnet/structure_sections.py index 2b9cbcc..386dfa3 100644 --- a/pse/generators/dotnet/structure_sections.py +++ b/pse/generators/dotnet/structure_sections.py @@ -1,15 +1,41 @@ import os from .structure_helpers import ensure_dir, ensure_placeholder, remove_placeholder -from .structure_writers import write_controller, write_csharp_class, write_repository_class, write_repository_interface, write_test_class -from .structure_helpers import pick_id_type - - -def create_api_structure(api_root: str, contexts): +from .structure_writers import ( + write_controller, + write_csharp_class, + write_mediatr_cqrs_class, + write_mapping_config, + write_repository_class, + write_repository_interface, + write_service_class, + write_service_interface, + write_wolverine_cqrs_class, + write_test_class, + write_validator_class, +) +from .structure_helpers import pick_id_property_name, pick_id_type + + +def create_api_structure(api_root: str, contexts, ctx=None): ensure_dir(api_root, "Controllers") ensure_dir(api_root, "Dtos") ensure_dir(api_root, "Contracts") - created = create_context_api_files(api_root, contexts) + if capability_enabled(ctx, "validation"): + ensure_dir(api_root, "Validators") + if capability_enabled(ctx, "mapping"): + ensure_dir(api_root, "Mapping") + + created = create_context_api_files( + api_root, + contexts, + use_mapping=capability_enabled(ctx, "mapping"), + cqrs_implementation=capability_value(ctx, "cqrs"), + ) + if capability_enabled(ctx, "validation"): + create_validator_files(api_root, contexts) + if capability_enabled(ctx, "mapping"): + create_mapping_files(api_root, contexts) if not created: ensure_placeholder(api_root, "Controllers", "ExampleController.cs", "API controller skeleton") @@ -18,11 +44,25 @@ def create_api_structure(api_root: str, contexts): ensure_placeholder(api_root, "Contracts", "ExampleResponse.cs", "API contract response skeleton") -def create_presentation_structure(presentation_root: str, contexts): +def create_presentation_structure(presentation_root: str, contexts, ctx=None): ensure_dir(presentation_root, "Controllers") ensure_dir(presentation_root, "Dtos") ensure_dir(presentation_root, "Contracts") - created = create_context_api_files(presentation_root, contexts) + if capability_enabled(ctx, "validation"): + ensure_dir(presentation_root, "Validators") + if capability_enabled(ctx, "mapping"): + ensure_dir(presentation_root, "Mapping") + + created = create_context_api_files( + presentation_root, + contexts, + use_mapping=capability_enabled(ctx, "mapping"), + cqrs_implementation=capability_value(ctx, "cqrs"), + ) + if capability_enabled(ctx, "validation"): + create_validator_files(presentation_root, contexts) + if capability_enabled(ctx, "mapping"): + create_mapping_files(presentation_root, contexts) if not created: ensure_placeholder(presentation_root, "Controllers", "ExampleController.cs", "Presentation controller skeleton") @@ -31,16 +71,19 @@ def create_presentation_structure(presentation_root: str, contexts): ensure_placeholder(presentation_root, "Contracts", "ExampleResponse.cs", "Presentation response skeleton") -def create_application_structure(app_root: str, contexts): +def create_application_structure(app_root: str, contexts, ctx=None): ensure_dir(app_root, "Interfaces") ensure_dir(app_root, "Services") - ensure_dir(app_root, "UseCases") - created = create_use_case_files(app_root, contexts) + cqrs_implementation = capability_value(ctx, "cqrs") + if cqrs_implementation: + ensure_dir(app_root, "Cqrs") - if not created: + created = create_application_service_files(app_root, contexts) + cqrs_created = create_cqrs_files(app_root, contexts, cqrs_implementation) + + if not created and not cqrs_created: ensure_placeholder(app_root, "Interfaces", "IExampleService.cs", "Application service interface") ensure_placeholder(app_root, "Services", "ExampleService.cs", "Application service implementation") - ensure_placeholder(app_root, "UseCases", "ExampleUseCase.cs", "Application use case") def create_domain_structure(domain_root: str, contexts): @@ -84,7 +127,7 @@ def create_tests_structure(tests_root: str, contexts): remove_placeholder(tests_root, "Integration", "ExampleIntegrationTests.cs") -def create_context_api_files(root: str, contexts, root_prefix: str = ""): +def create_context_api_files(root: str, contexts, root_prefix: str = "", use_mapping: bool = False, cqrs_implementation: str = None): created = False for context in contexts or []: if not context: @@ -95,27 +138,113 @@ def create_context_api_files(root: str, contexts, root_prefix: str = ""): controller_path = os.path.join(root, root_prefix, "Controllers", f"{controller_name}.cs") dto_path = os.path.join(root, root_prefix, "Dtos", f"{dto_name}.cs") - write_controller(controller_path, "Controllers", controller_name, entity, dto_name) + write_controller( + controller_path, + "Controllers", + controller_name, + entity, + dto_name, + use_mapping=use_mapping, + cqrs_implementation=cqrs_implementation, + ) write_csharp_class(dto_path, "Dtos", dto_name, properties=entity.properties) created = True return created -def create_use_case_files(root: str, contexts, root_prefix: str = ""): +def create_validator_files(root: str, contexts, root_prefix: str = ""): created = False for context in contexts or []: if not context: continue for entity in context.entities: - use_case_name = f"{entity.name}UseCase" - use_case_path = os.path.join(root, root_prefix, "UseCases", f"{use_case_name}.cs") - write_csharp_class(use_case_path, "UseCases", use_case_name) + dto_name = f"{entity.name}Dto" + validator_name = f"{dto_name}Validator" + validator_path = os.path.join(root, root_prefix, "Validators", f"{validator_name}.cs") + write_validator_class(validator_path, "Validators", validator_name, dto_name, entity.properties) created = True return created +def create_mapping_files(root: str, contexts, root_prefix: str = ""): + entities = [ + entity + for context in contexts or [] + if context + for entity in context.entities + ] + if not entities: + return False + + mapping_path = os.path.join(root, root_prefix, "Mapping", "MappingConfig.cs") + write_mapping_config(mapping_path, "Mapping", entities) + return True + + +def create_application_service_files(root: str, contexts, root_prefix: str = ""): + created = False + for context in contexts or []: + if not context: + continue + for entity in context.entities: + interface_name = f"I{entity.name}Service" + class_name = f"{entity.name}Service" + interface_path = os.path.join(root, root_prefix, "Interfaces", f"{interface_name}.cs") + class_path = os.path.join(root, root_prefix, "Services", f"{class_name}.cs") + write_service_interface( + interface_path, + "Interfaces", + interface_name, + entity.name, + pick_id_type(entity.properties), + ) + write_service_class( + class_path, + "Services", + class_name, + interface_name, + entity.name, + pick_id_type(entity.properties), + pick_id_property_name(entity.properties), + ) + created = True + + return created + + +def create_cqrs_files(root: str, contexts, implementation: str = None, root_prefix: str = ""): + if not implementation: + return False + + created = False + for context in contexts or []: + if not context: + continue + for entity in context.entities: + if implementation == "mediatr": + cqrs_path = os.path.join(root, root_prefix, "Cqrs", f"{entity.name}Requests.cs") + write_mediatr_cqrs_class( + cqrs_path, + "Cqrs", + entity.name, + pick_id_type(entity.properties), + ) + created = True + elif implementation == "wolverine": + cqrs_path = os.path.join(root, root_prefix, "Cqrs", f"{entity.name}Messages.cs") + write_wolverine_cqrs_class( + cqrs_path, + "Cqrs", + entity.name, + pick_id_type(entity.properties), + ) + created = True + + return created + + def create_domain_files(root: str, contexts, root_prefix: str = ""): created = False for context in contexts or []: @@ -146,7 +275,15 @@ def create_repository_implementations(root: str, contexts, root_prefix: str = "" repo_name = f"{entity.name}Repository" repo_path = os.path.join(root, root_prefix, "Repositories", f"{repo_name}.cs") interface_name = f"I{entity.name}Repository" - write_repository_class(repo_path, "Repositories", repo_name, interface_name, entity.name, pick_id_type(entity.properties)) + write_repository_class( + repo_path, + "Repositories", + repo_name, + interface_name, + entity.name, + pick_id_type(entity.properties), + pick_id_property_name(entity.properties), + ) created = True return created @@ -160,7 +297,18 @@ def create_tests_files(root: str, contexts): for entity in context.entities: test_name = f"{entity.name}Tests" test_path = os.path.join(root, "Unit", f"{test_name}.cs") - write_test_class(test_path, "Unit", test_name, entity.name) + write_test_class(test_path, "Unit", test_name, entity) created = True - return created \ No newline at end of file + return created + + +def capability_enabled(ctx, name: str): + capabilities = getattr(getattr(ctx, "capabilities", None), "capabilities", {}) or {} + return name.lower() in capabilities + + +def capability_value(ctx, name: str): + capabilities = getattr(getattr(ctx, "capabilities", None), "capabilities", {}) or {} + capability = capabilities.get(name.lower()) + return getattr(capability, "value", None) diff --git a/pse/generators/dotnet/structure_writers.py b/pse/generators/dotnet/structure_writers.py index d7430bf..96efe65 100644 --- a/pse/generators/dotnet/structure_writers.py +++ b/pse/generators/dotnet/structure_writers.py @@ -1,7 +1,7 @@ import os from .template_loader import render_template -from .structure_helpers import build_namespace, build_properties, pick_id_property_name, pick_id_type +from .structure_helpers import build_namespace, build_properties, map_type, pick_id_property_name, pick_id_type def write_csharp_class(path: str, folder: str, name: str, base_type: str = None, properties=None, is_interface: bool = False): @@ -33,7 +33,7 @@ def write_csharp_class(path: str, folder: str, name: str, base_type: str = None, f.write(content) -def write_repository_class(path: str, folder: str, name: str, interface_name: str, entity_name: str, id_type: str): +def write_repository_class(path: str, folder: str, name: str, interface_name: str, entity_name: str, id_type: str, id_property_name: str): namespace = build_namespace(path, folder) domain_namespace = namespace.replace(".Infrastructure.Repositories", ".Domain") @@ -45,6 +45,8 @@ def write_repository_class(path: str, folder: str, name: str, interface_name: st "GetAll()" in content and "GetById(" in content and "Create(" in content + and "NotImplementedException" not in content + and "Array.Empty" not in content and "{{" not in content and "Repositories.Entities" not in content and "Repositories.Repositories" not in content @@ -57,6 +59,7 @@ def write_repository_class(path: str, folder: str, name: str, interface_name: st "DomainNamespace": domain_namespace, "EntityName": entity_name, "IdType": id_type, + "IdPropertyName": id_property_name, "Namespace": namespace, "ClassName": name, "InterfaceName": interface_name, @@ -67,6 +70,77 @@ def write_repository_class(path: str, folder: str, name: str, interface_name: st f.write(content) +def write_service_interface(path: str, folder: str, name: str, entity_name: str, id_type: str): + namespace = build_namespace(path, folder) + domain_namespace = namespace.replace(".Application.Interfaces", ".Domain") + + if os.path.exists(path): + with open(path, "r", encoding="utf-8") as f: + content = f.read() + + if ( + "GetAll()" in content + and "GetById(" in content + and "Create(" in content + and "bool Update(" in content + and "bool Delete(" in content + and "{{" not in content + ): + return + + content = render_template( + "ApplicationServiceInterface.cs.tmpl", + { + "DomainNamespace": domain_namespace, + "Namespace": namespace, + "InterfaceName": name, + "EntityName": entity_name, + "IdType": id_type, + }, + ) + + with open(path, "w", encoding="utf-8") as f: + f.write(content) + + +def write_service_class(path: str, folder: str, name: str, interface_name: str, entity_name: str, id_type: str, id_property_name: str): + namespace = build_namespace(path, folder) + domain_namespace = namespace.replace(".Application.Services", ".Domain") + interface_namespace = namespace.replace(".Services", ".Interfaces") + + if os.path.exists(path): + with open(path, "r", encoding="utf-8") as f: + content = f.read() + + if ( + "GetAll()" in content + and "GetById(" in content + and "Create(" in content + and "bool Update(" in content + and "bool Delete(" in content + and "{{" not in content + and "TODO" not in content + ): + return + + content = render_template( + "ApplicationService.cs.tmpl", + { + "DomainNamespace": domain_namespace, + "InterfaceNamespace": interface_namespace, + "Namespace": namespace, + "ClassName": name, + "InterfaceName": interface_name, + "EntityName": entity_name, + "IdType": id_type, + "IdPropertyName": id_property_name, + }, + ) + + with open(path, "w", encoding="utf-8") as f: + f.write(content) + + def write_repository_interface(path: str, folder: str, name: str, entity_name: str, id_type: str): namespace = build_namespace(path, folder) if os.path.exists(path): @@ -96,13 +170,114 @@ def write_repository_interface(path: str, folder: str, name: str, entity_name: s f.write(content) -def write_controller(path: str, folder: str, name: str, entity, dto_name: str): +def write_validator_class(path: str, folder: str, name: str, dto_name: str, properties): + namespace = build_namespace(path, folder) + dto_namespace = build_namespace(path, "Dtos") + + if os.path.exists(path): + with open(path, "r", encoding="utf-8") as f: + content = f.read() + + if "AbstractValidator" in content and "{{" not in content: + return + + rules = "".join( + f" RuleFor(x => x.{prop_name}).NotEmpty();\n" + for prop_name in (properties or {}).keys() + ) + + content = render_template( + "DtoValidator.cs.tmpl", + { + "DtoNamespace": dto_namespace, + "Namespace": namespace, + "ClassName": name, + "DtoName": dto_name, + "Rules": rules, + }, + ) + + with open(path, "w", encoding="utf-8") as f: + f.write(content) + + +def write_mapping_config(path: str, folder: str, entities): + namespace = build_namespace(path, folder) + project_root = namespace.split(".")[0] + surface_namespace = namespace.rsplit(".", 1)[0] + dto_namespace = f"{surface_namespace}.Dtos" + entity_namespace = f"{project_root}.Domain.Entities" + + if os.path.exists(path): + with open(path, "r", encoding="utf-8") as f: + content = f.read() + + if "TypeAdapterConfig" in content and "{{" not in content: + return + + mappings = [] + for entity in entities or []: + dto_name = f"{entity.name}Dto" + mappings.append(f" TypeAdapterConfig<{entity.name}, {dto_name}>.NewConfig();\n") + mappings.append(f" TypeAdapterConfig<{dto_name}, {entity.name}>.NewConfig();\n") + + content = render_template( + "MappingConfig.cs.tmpl", + { + "DtoNamespace": dto_namespace, + "EntityNamespace": entity_namespace, + "Namespace": namespace, + "Mappings": "".join(mappings), + }, + ) + + with open(path, "w", encoding="utf-8") as f: + f.write(content) + + +def write_mediatr_cqrs_class(path: str, folder: str, entity_name: str, id_type: str): + write_cqrs_class(path, folder, entity_name, id_type, "MediatRRequests.cs.tmpl") + + +def write_wolverine_cqrs_class(path: str, folder: str, entity_name: str, id_type: str): + write_cqrs_class(path, folder, entity_name, id_type, "WolverineMessages.cs.tmpl") + + +def write_cqrs_class(path: str, folder: str, entity_name: str, id_type: str, template_name: str): + namespace = build_namespace(path, folder) + project_root = namespace.split(".")[0] + + if os.path.exists(path): + with open(path, "r", encoding="utf-8") as f: + content = f.read() + + if "Handler" in content and "{{" not in content and "TODO" not in content: + return + + content = render_template( + template_name, + { + "Namespace": namespace, + "DomainNamespace": f"{project_root}.Domain", + "InterfaceNamespace": f"{project_root}.Application.Interfaces", + "EntityName": entity_name, + "IdType": id_type, + }, + ) + + with open(path, "w", encoding="utf-8") as f: + f.write(content) + + +def write_controller(path: str, folder: str, name: str, entity, dto_name: str, use_mapping: bool = False, cqrs_implementation: str = None): namespace = build_namespace(path, folder) entity_namespace = namespace.replace(".API.Controllers", ".Domain.Entities").replace(".Presentation.Controllers", ".Domain.Entities").replace(".Gateway.Controllers", ".Domain.Entities") - repository_namespace = namespace.replace(".API.Controllers", ".Domain.Repositories").replace(".Presentation.Controllers", ".Domain.Repositories").replace(".Gateway.Controllers", ".Domain.Repositories") + service_namespace = namespace.replace(".API.Controllers", ".Application.Interfaces").replace(".Presentation.Controllers", ".Application.Interfaces").replace(".Gateway.Controllers", ".Application.Interfaces") + cqrs_namespace = namespace.replace(".API.Controllers", ".Application.Cqrs").replace(".Presentation.Controllers", ".Application.Cqrs").replace(".Gateway.Controllers", ".Application.Cqrs") dto_namespace = build_namespace(path, "Dtos") id_type = pick_id_type(entity.properties) id_property_name = pick_id_property_name(entity.properties) + cqrs_implementation = (cqrs_implementation or "").lower() if os.path.exists(path): with open(path, "r", encoding="utf-8") as f: @@ -110,9 +285,17 @@ def write_controller(path: str, folder: str, name: str, entity, dto_name: str): if ( "private readonly" in content - and "ToDto(" in content - and "ToEntity(" in content and "{{" not in content + and "TODO" not in content + and ( + use_mapping + or ("ToDto(" in content and "ToEntity(" in content) + ) + and ( + (cqrs_implementation == "mediatr" and "IMediator" in content) + or (cqrs_implementation == "wolverine" and "IMessageBus" in content) + or (cqrs_implementation not in {"mediatr", "wolverine"} and f"I{entity.name}Service" in content) + ) ): return @@ -122,18 +305,34 @@ def write_controller(path: str, folder: str, name: str, entity, dto_name: str): id_type=id_type, id_property_name=id_property_name, properties=list(entity.properties.keys()), + use_mapping=use_mapping, + cqrs_implementation=cqrs_implementation, + ) + + using_lines = ( + "using System.Collections.Generic;\n" + "using System.Linq;\n" + "using System.Threading.Tasks;\n" + f"using {dto_namespace};\n" + f"using {entity_namespace};\n" ) + if cqrs_implementation in {"mediatr", "wolverine"}: + using_lines += f"using {cqrs_namespace};\n" + else: + using_lines += f"using {service_namespace};\n" + + if cqrs_implementation == "mediatr": + using_lines += "using MediatR;\n" + elif cqrs_implementation == "wolverine": + using_lines += "using Wolverine;\n" + + if use_mapping: + using_lines += "using Mapster;\n" content = render_template( "Controller.cs.tmpl", { - "UsingLines": ( - "using System.Collections.Generic;\n" - "using System.Linq;\n" - f"using {dto_namespace};\n" - f"using {entity_namespace};\n" - f"using {repository_namespace};\n\n" - ), + "UsingLines": f"{using_lines}\n", "Namespace": namespace, "ControllerName": name, "Methods": methods, @@ -144,62 +343,199 @@ def write_controller(path: str, folder: str, name: str, entity, dto_name: str): f.write(content) -def build_controller_methods(entity_name: str, dto_name: str, id_type: str, id_property_name: str, properties): - repository_field_name = f"_{entity_name[0].lower()}{entity_name[1:]}Repository" - repository_parameter_name = f"{entity_name[0].lower()}{entity_name[1:]}Repository" - repository_interface_name = f"I{entity_name}Repository" +def build_controller_methods(entity_name: str, dto_name: str, id_type: str, id_property_name: str, properties, use_mapping: bool = False, cqrs_implementation: str = None): + if cqrs_implementation == "mediatr": + return build_mediatr_controller_methods(entity_name, dto_name, id_type, id_property_name, properties, use_mapping) - to_dto_assignments = build_object_initializer("entity", dto_name, properties) - to_entity_assignments = build_object_initializer("dto", entity_name, properties) + if cqrs_implementation == "wolverine": + return build_wolverine_controller_methods(entity_name, dto_name, id_type, id_property_name, properties, use_mapping) - return ( - f" private readonly {repository_interface_name} {repository_field_name};\n\n" - f" public {entity_name}Controller({repository_interface_name} {repository_parameter_name})\n" + service_field_name = f"_{entity_name[0].lower()}{entity_name[1:]}Service" + service_parameter_name = f"{entity_name[0].lower()}{entity_name[1:]}Service" + service_interface_name = f"I{entity_name}Service" + + core_methods = ( + f" private readonly {service_interface_name} {service_field_name};\n\n" + f" public {entity_name}Controller({service_interface_name} {service_parameter_name})\n" " {\n" - f" {repository_field_name} = {repository_parameter_name};\n" + f" {service_field_name} = {service_parameter_name};\n" " }\n\n" " [HttpGet]\n" f" public ActionResult> GetAll()\n" " {\n" - f" var entities = {repository_field_name}.GetAll().Select(ToDto).ToList();\n" + f" var entities = {service_field_name}.GetAll().Select(entity => {map_expression('entity', dto_name, use_mapping)}).ToList();\n" " return Ok(entities);\n" " }\n\n" " [HttpGet(\"{id}\")]\n" f" public ActionResult<{dto_name}> GetById({id_type} id)\n" " {\n" - f" var entity = {repository_field_name}.GetById(id);\n" + f" var entity = {service_field_name}.GetById(id);\n" " if (entity is null)\n" " {\n" " return NotFound();\n" " }\n\n" - " return Ok(ToDto(entity));\n" + f" return Ok({map_expression('entity', dto_name, use_mapping)});\n" " }\n\n" " [HttpPost]\n" f" public ActionResult<{dto_name}> Create({dto_name} request)\n" " {\n" - f" var entity = ToEntity(request);\n" - f" {repository_field_name}.Create(entity);\n" - f" return CreatedAtAction(nameof(GetById), new {{ id = entity.{id_property_name} }}, ToDto(entity));\n" + f" var entity = {map_expression('request', entity_name, use_mapping)};\n" + f" var created = {service_field_name}.Create(entity);\n" + f" return CreatedAtAction(nameof(GetById), new {{ id = created.{id_property_name} }}, {map_expression('created', dto_name, use_mapping)});\n" " }\n\n" " [HttpPut(\"{id}\")]\n" f" public IActionResult Update({id_type} id, {dto_name} request)\n" " {\n" - f" var entity = ToEntity(request);\n" - f" entity.{id_property_name} = id;\n" - f" {repository_field_name}.Update(entity);\n" + f" var entity = {map_expression('request', entity_name, use_mapping)};\n" + f" if (!{service_field_name}.Update(id, entity))\n" + " {\n" + " return NotFound();\n" + " }\n\n" " return NoContent();\n" " }\n\n" " [HttpDelete(\"{id}\")]\n" f" public IActionResult Delete({id_type} id)\n" " {\n" - f" var existingEntity = {repository_field_name}.GetById(id);\n" - " if (existingEntity is null)\n" + f" if (!{service_field_name}.Delete(id))\n" " {\n" " return NotFound();\n" " }\n\n" - f" {repository_field_name}.Delete(id);\n" " return NoContent();\n" + " }\n" + ) + + if use_mapping: + return core_methods + + return core_methods + build_manual_mapping_methods(entity_name, dto_name, properties) + + +def build_mediatr_controller_methods(entity_name: str, dto_name: str, id_type: str, id_property_name: str, properties, use_mapping: bool = False): + core_methods = ( + " private readonly IMediator _mediator;\n\n" + f" public {entity_name}Controller(IMediator mediator)\n" + " {\n" + " _mediator = mediator;\n" " }\n\n" + " [HttpGet]\n" + f" public async Task>> GetAll()\n" + " {\n" + f" var entities = await _mediator.Send(new GetAll{entity_name}Query());\n" + f" var response = entities.Select(entity => {map_expression('entity', dto_name, use_mapping)}).ToList();\n" + " return Ok(response);\n" + " }\n\n" + " [HttpGet(\"{id}\")]\n" + f" public async Task> GetById({id_type} id)\n" + " {\n" + f" var entity = await _mediator.Send(new Get{entity_name}ByIdQuery(id));\n" + " if (entity is null)\n" + " {\n" + " return NotFound();\n" + " }\n\n" + f" return Ok({map_expression('entity', dto_name, use_mapping)});\n" + " }\n\n" + " [HttpPost]\n" + f" public async Task> Create({dto_name} request)\n" + " {\n" + f" var entity = {map_expression('request', entity_name, use_mapping)};\n" + f" var created = await _mediator.Send(new Create{entity_name}Command(entity));\n" + f" return CreatedAtAction(nameof(GetById), new {{ id = created.{id_property_name} }}, {map_expression('created', dto_name, use_mapping)});\n" + " }\n\n" + " [HttpPut(\"{id}\")]\n" + f" public async Task Update({id_type} id, {dto_name} request)\n" + " {\n" + f" var entity = {map_expression('request', entity_name, use_mapping)};\n" + f" var updated = await _mediator.Send(new Update{entity_name}Command(id, entity));\n" + " if (!updated)\n" + " {\n" + " return NotFound();\n" + " }\n\n" + " return NoContent();\n" + " }\n\n" + " [HttpDelete(\"{id}\")]\n" + f" public async Task Delete({id_type} id)\n" + " {\n" + f" var deleted = await _mediator.Send(new Delete{entity_name}Command(id));\n" + " if (!deleted)\n" + " {\n" + " return NotFound();\n" + " }\n\n" + " return NoContent();\n" + " }\n" + ) + + if use_mapping: + return core_methods + + return core_methods + build_manual_mapping_methods(entity_name, dto_name, properties) + + +def build_wolverine_controller_methods(entity_name: str, dto_name: str, id_type: str, id_property_name: str, properties, use_mapping: bool = False): + core_methods = ( + " private readonly IMessageBus _messageBus;\n\n" + f" public {entity_name}Controller(IMessageBus messageBus)\n" + " {\n" + " _messageBus = messageBus;\n" + " }\n\n" + " [HttpGet]\n" + f" public async Task>> GetAll()\n" + " {\n" + f" var entities = await _messageBus.InvokeAsync>(new GetAll{entity_name}Query());\n" + f" var response = entities.Select(entity => {map_expression('entity', dto_name, use_mapping)}).ToList();\n" + " return Ok(response);\n" + " }\n\n" + " [HttpGet(\"{id}\")]\n" + f" public async Task> GetById({id_type} id)\n" + " {\n" + f" var entity = await _messageBus.InvokeAsync<{entity_name}?>(new Get{entity_name}ByIdQuery(id));\n" + " if (entity is null)\n" + " {\n" + " return NotFound();\n" + " }\n\n" + f" return Ok({map_expression('entity', dto_name, use_mapping)});\n" + " }\n\n" + " [HttpPost]\n" + f" public async Task> Create({dto_name} request)\n" + " {\n" + f" var entity = {map_expression('request', entity_name, use_mapping)};\n" + f" var created = await _messageBus.InvokeAsync<{entity_name}>(new Create{entity_name}Command(entity));\n" + f" return CreatedAtAction(nameof(GetById), new {{ id = created.{id_property_name} }}, {map_expression('created', dto_name, use_mapping)});\n" + " }\n\n" + " [HttpPut(\"{id}\")]\n" + f" public async Task Update({id_type} id, {dto_name} request)\n" + " {\n" + f" var entity = {map_expression('request', entity_name, use_mapping)};\n" + f" var updated = await _messageBus.InvokeAsync(new Update{entity_name}Command(id, entity));\n" + " if (!updated)\n" + " {\n" + " return NotFound();\n" + " }\n\n" + " return NoContent();\n" + " }\n\n" + " [HttpDelete(\"{id}\")]\n" + f" public async Task Delete({id_type} id)\n" + " {\n" + f" var deleted = await _messageBus.InvokeAsync(new Delete{entity_name}Command(id));\n" + " if (!deleted)\n" + " {\n" + " return NotFound();\n" + " }\n\n" + " return NoContent();\n" + " }\n" + ) + + if use_mapping: + return core_methods + + return core_methods + build_manual_mapping_methods(entity_name, dto_name, properties) + + +def build_manual_mapping_methods(entity_name: str, dto_name: str, properties): + to_dto_assignments = build_object_initializer("entity", dto_name, properties) + to_entity_assignments = build_object_initializer("dto", entity_name, properties) + + return ( + "\n" f" private static {dto_name} ToDto({entity_name} entity)\n" " {\n" f" return new {dto_name}\n" @@ -217,6 +553,16 @@ def build_controller_methods(entity_name: str, dto_name: str, id_type: str, id_p ) +def map_expression(source_name: str, target_name: str, use_mapping: bool): + if use_mapping: + return f"{source_name}.Adapt<{target_name}>()" + + if target_name.endswith("Dto"): + return f"ToDto({source_name})" + + return f"ToEntity({source_name})" + + def build_object_initializer(source_name: str, target_name: str, properties): if not properties: return "" @@ -228,16 +574,77 @@ def build_object_initializer(source_name: str, target_name: str, properties): return "".join(lines) -def write_test_class(path: str, folder: str, name: str, subject_name: str): +def write_test_class(path: str, folder: str, name: str, entity): namespace = build_namespace(path, folder) + project_root = namespace.split(".")[0] + subject_name = entity.name + body = build_entity_test_body(entity) content = render_template( "TestClass.cs.tmpl", { + "UsingLines": f"using System;\nusing {project_root}.Domain.Entities;\n", "Namespace": namespace, "ClassName": name, "TestMethodName": f"CanCreate{subject_name}", + "Body": body, }, ) with open(path, "w", encoding="utf-8") as f: - f.write(content) \ No newline at end of file + f.write(content) + + +def build_entity_test_body(entity): + assignments = [] + assertions = [] + + for prop_name, prop_type in (entity.properties or {}).items(): + cs_type, _ = map_type(prop_type) + value = sample_value(prop_name, cs_type) + assignments.append(f" {prop_name} = {value},\n") + assertions.append(assertion_for_property(prop_name, cs_type, value)) + + if not assignments: + return f" var entity = new {entity.name}();\n\n Assert.NotNull(entity);\n" + + return ( + f" var entity = new {entity.name}\n" + " {\n" + f"{''.join(assignments)}" + " };\n\n" + f"{''.join(assertions)}" + ) + + +def sample_value(prop_name: str, cs_type: str): + if cs_type == "Guid": + return "Guid.NewGuid()" + if cs_type == "DateTime": + return "DateTime.UtcNow" + if cs_type == "string": + return f"\"{prop_name}\"" + if cs_type == "bool": + return "true" + if cs_type in {"int", "long"}: + return "1" + if cs_type == "decimal": + return "1m" + if cs_type == "double": + return "1d" + if cs_type == "float": + return "1f" + + return f"new {cs_type}()" + + +def assertion_for_property(prop_name: str, cs_type: str, value: str): + if cs_type == "Guid": + return f" Assert.NotEqual(Guid.Empty, entity.{prop_name});\n" + if cs_type == "DateTime": + return f" Assert.NotEqual(default, entity.{prop_name});\n" + if cs_type == "string": + return f" Assert.Equal({value}, entity.{prop_name});\n" + if cs_type == "bool": + return f" Assert.True(entity.{prop_name});\n" + + return f" Assert.Equal({value}, entity.{prop_name});\n" diff --git a/pse/grammar/pse.tx b/pse/grammar/pse.tx index d43b7f0..700b4bf 100644 --- a/pse/grammar/pse.tx +++ b/pse/grammar/pse.tx @@ -15,7 +15,7 @@ Capabilities: '}'; CapabilityDecl: - 'Capability' name=ID; + 'Capability' name=ID ('=' implementation=ID)?; Target: 'dotnet' | 'java'; diff --git a/pse/heuristics/capabilities.yaml b/pse/heuristics/capabilities.yaml index 62e2bac..d9a3118 100644 --- a/pse/heuristics/capabilities.yaml +++ b/pse/heuristics/capabilities.yaml @@ -1,9 +1,3 @@ -cqrs: - mediatr: - depends_on: [] - wolverine: - depends_on: [] - validation: fluentvalidation: depends_on: [] @@ -16,6 +10,13 @@ mapping: mapster: depends_on: [] +cqrs: + mediatr: + default: true + depends_on: [] + wolverine: + depends_on: [] + testing: xunit: depends_on: [] diff --git a/pse/heuristics/packages.yaml b/pse/heuristics/packages.yaml index 007ee1e..eb4400c 100644 --- a/pse/heuristics/packages.yaml +++ b/pse/heuristics/packages.yaml @@ -1,33 +1,58 @@ serilog: packages: - - Serilog.AspNetCore - - Serilog.Sinks.Console + - name: Serilog.AspNetCore + version: serilog + - name: Serilog.Sinks.Console + version: serilog_console fluentvalidation: packages: - - FluentValidation.AspNetCore + - name: FluentValidation.AspNetCore + version: fluentvalidation mapster: packages: - - Mapster - - Mapster.DependencyInjection + - name: Mapster + version: mapster + - name: Mapster.DependencyInjection + version: mapster + +mediatr: + packages: + - name: MediatR + version: mediatr + +wolverine: + packages: + - name: WolverineFx + version: wolverine xunit: packages: - - xunit - - xunit.runner.visualstudio + - name: Microsoft.NET.Test.Sdk + version: test_sdk + - name: xunit + version: xunit + - name: xunit.runner.visualstudio + version: xunit_runner postgres: packages: - - Microsoft.EntityFrameworkCore - - Npgsql.EntityFrameworkCore.PostgreSQL + - name: Microsoft.EntityFrameworkCore + version: efcore + - name: Npgsql.EntityFrameworkCore.PostgreSQL + version: npgsql_efcore redis: packages: - - StackExchange.Redis - - Microsoft.Extensions.Caching.StackExchangeRedis + - name: StackExchange.Redis + version: stackexchange_redis + - name: Microsoft.Extensions.Caching.StackExchangeRedis + version: efcore rabbitmq: packages: - - MassTransit - - MassTransit.RabbitMQ + - name: MassTransit + version: mass_transit + - name: MassTransit.RabbitMQ + version: mass_transit diff --git a/pse/heuristics/presets.yaml b/pse/heuristics/presets.yaml index 8c75474..bdb7c22 100644 --- a/pse/heuristics/presets.yaml +++ b/pse/heuristics/presets.yaml @@ -4,7 +4,6 @@ webapi: validation: fluentvalidation mapping: mapster testing: xunit - database: postgres cleanarchitecture: capabilities: diff --git a/pse/heuristics/resolver.py b/pse/heuristics/resolver.py index 3c44301..50910e4 100644 --- a/pse/heuristics/resolver.py +++ b/pse/heuristics/resolver.py @@ -29,9 +29,14 @@ def resolve_capabilities(ctx): explicit = getattr(ctx.architecture, "capabilities", []) or [] registry = load_capability_registry() - for cap_name in explicit: - name = cap_name.lower() - implementation = pick_default_implementation(name, caps, registry) + for selected in explicit: + name = getattr(selected, "name", selected).lower() + requested = getattr(selected, "implementation", None) + implementation = ( + normalize_implementation(name, requested) + if requested + else pick_default_implementation(name, caps, registry) + ) graph.capabilities[name] = Capability( name=name, @@ -91,6 +96,10 @@ def pick_default_implementation(name, preset_caps, registry): return preset_caps[name] implementations = registry.get(name, {}) + for implementation, metadata in implementations.items(): + if isinstance(metadata, dict) and metadata.get("default"): + return implementation + if len(implementations) == 1: return next(iter(implementations.keys())) @@ -106,4 +115,7 @@ def normalize_implementation(capability: str, value: str): if capability == "database" and normalized == "postgresql": return "postgres" + if capability == "cqrs" and normalized == "wolverinefx": + return "wolverine" + return normalized diff --git a/pse/heuristics/versions.yaml b/pse/heuristics/versions.yaml index b9385bb..8d16238 100644 --- a/pse/heuristics/versions.yaml +++ b/pse/heuristics/versions.yaml @@ -1,4 +1,4 @@ -dotnet: "9.0" +dotnet: "10.0" postgres: "17" @@ -6,6 +6,28 @@ redis: "8" rabbitmq: "4" -serilog: "9.0" +serilog: "10.0.0" -fluentvalidation: "12.1" +serilog_console: "6.1.1" + +fluentvalidation: "11.3.1" + +mapster: "10.0.10" + +mediatr: "14.2.0" + +wolverine: "6.17.0" + +xunit: "2.9.3" + +xunit_runner: "3.1.5" + +test_sdk: "18.7.0" + +efcore: "10.0.9" + +npgsql_efcore: "10.0.2" + +stackexchange_redis: "3.0.11" + +mass_transit: "9.0.0" diff --git a/pse/model/architecture.py b/pse/model/architecture.py index b24b769..95da2e8 100644 --- a/pse/model/architecture.py +++ b/pse/model/architecture.py @@ -64,10 +64,16 @@ class Deployment: target: str +@dataclass +class CapabilitySelection: + name: str + implementation: Optional[str] = None + + @dataclass class ArchitectureModel: project: Project contexts: List[Context] infrastructure: Infrastructure deployment: Deployment - capabilities: List[str] = field(default_factory=list) \ No newline at end of file + capabilities: List[CapabilitySelection] = field(default_factory=list) diff --git a/pse/sample_output/Dockerfile b/pse/sample_output/Dockerfile index 3d8b14d..9f2800f 100644 --- a/pse/sample_output/Dockerfile +++ b/pse/sample_output/Dockerfile @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/dotnet/aspnet:9.0 +FROM mcr.microsoft.com/dotnet/aspnet:10.0 WORKDIR /app ENV ASPNETCORE_URLS=http://+:8080 EXPOSE 8080 diff --git a/pse/sample_output/StoreApi.API/Controllers/OrderController.cs b/pse/sample_output/StoreApi.API/Controllers/OrderController.cs index 7bc1987..f73340f 100644 --- a/pse/sample_output/StoreApi.API/Controllers/OrderController.cs +++ b/pse/sample_output/StoreApi.API/Controllers/OrderController.cs @@ -1,9 +1,12 @@ using Microsoft.AspNetCore.Mvc; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; using StoreApi.API.Dtos; using StoreApi.Domain.Entities; -using StoreApi.Domain.Repositories; +using StoreApi.Application.Cqrs; +using MediatR; +using Mapster; namespace StoreApi.API.Controllers; @@ -11,79 +14,63 @@ namespace StoreApi.API.Controllers; [Route("[controller]")] public class OrderController : ControllerBase { - private readonly IOrderRepository _orderRepository; + private readonly IMediator _mediator; - public OrderController(IOrderRepository orderRepository) + public OrderController(IMediator mediator) { - _orderRepository = orderRepository; + _mediator = mediator; } [HttpGet] - public ActionResult> GetAll() + public async Task>> GetAll() { - var entities = _orderRepository.GetAll().Select(ToDto).ToList(); - return Ok(entities); + var entities = await _mediator.Send(new GetAllOrderQuery()); + var response = entities.Select(entity => entity.Adapt()).ToList(); + return Ok(response); } [HttpGet("{id}")] - public ActionResult GetById(Guid id) + public async Task> GetById(Guid id) { - var entity = _orderRepository.GetById(id); + var entity = await _mediator.Send(new GetOrderByIdQuery(id)); if (entity is null) { return NotFound(); } - return Ok(ToDto(entity)); + return Ok(entity.Adapt()); } [HttpPost] - public ActionResult Create(OrderDto request) + public async Task> Create(OrderDto request) { - var entity = ToEntity(request); - _orderRepository.Create(entity); - return CreatedAtAction(nameof(GetById), new { id = entity.Id }, ToDto(entity)); + var entity = request.Adapt(); + var created = await _mediator.Send(new CreateOrderCommand(entity)); + return CreatedAtAction(nameof(GetById), new { id = created.Id }, created.Adapt()); } [HttpPut("{id}")] - public IActionResult Update(Guid id, OrderDto request) + public async Task Update(Guid id, OrderDto request) { - var entity = ToEntity(request); - entity.Id = id; - _orderRepository.Update(entity); - return NoContent(); - } - - [HttpDelete("{id}")] - public IActionResult Delete(Guid id) - { - var existingEntity = _orderRepository.GetById(id); - if (existingEntity is null) + var entity = request.Adapt(); + var updated = await _mediator.Send(new UpdateOrderCommand(id, entity)); + if (!updated) { return NotFound(); } - _orderRepository.Delete(id); return NoContent(); } - private static OrderDto ToDto(Order entity) + [HttpDelete("{id}")] + public async Task Delete(Guid id) { - return new OrderDto + var deleted = await _mediator.Send(new DeleteOrderCommand(id)); + if (!deleted) { - Id = entity.Id, - CreatedAt = entity.CreatedAt, - Status = entity.Status, - }; - } + return NotFound(); + } - private static Order ToEntity(OrderDto dto) - { - return new Order - { - Id = dto.Id, - CreatedAt = dto.CreatedAt, - Status = dto.Status, - }; + return NoContent(); } } diff --git a/pse/sample_output/StoreApi.API/Controllers/OrderItemController.cs b/pse/sample_output/StoreApi.API/Controllers/OrderItemController.cs index 33f5d3f..e0cf55e 100644 --- a/pse/sample_output/StoreApi.API/Controllers/OrderItemController.cs +++ b/pse/sample_output/StoreApi.API/Controllers/OrderItemController.cs @@ -1,9 +1,12 @@ using Microsoft.AspNetCore.Mvc; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; using StoreApi.API.Dtos; using StoreApi.Domain.Entities; -using StoreApi.Domain.Repositories; +using StoreApi.Application.Cqrs; +using MediatR; +using Mapster; namespace StoreApi.API.Controllers; @@ -11,77 +14,63 @@ namespace StoreApi.API.Controllers; [Route("[controller]")] public class OrderItemController : ControllerBase { - private readonly IOrderItemRepository _orderItemRepository; + private readonly IMediator _mediator; - public OrderItemController(IOrderItemRepository orderItemRepository) + public OrderItemController(IMediator mediator) { - _orderItemRepository = orderItemRepository; + _mediator = mediator; } [HttpGet] - public ActionResult> GetAll() + public async Task>> GetAll() { - var entities = _orderItemRepository.GetAll().Select(ToDto).ToList(); - return Ok(entities); + var entities = await _mediator.Send(new GetAllOrderItemQuery()); + var response = entities.Select(entity => entity.Adapt()).ToList(); + return Ok(response); } [HttpGet("{id}")] - public ActionResult GetById(Guid id) + public async Task> GetById(Guid id) { - var entity = _orderItemRepository.GetById(id); + var entity = await _mediator.Send(new GetOrderItemByIdQuery(id)); if (entity is null) { return NotFound(); } - return Ok(ToDto(entity)); + return Ok(entity.Adapt()); } [HttpPost] - public ActionResult Create(OrderItemDto request) + public async Task> Create(OrderItemDto request) { - var entity = ToEntity(request); - _orderItemRepository.Create(entity); - return CreatedAtAction(nameof(GetById), new { id = entity.ProductId }, ToDto(entity)); + var entity = request.Adapt(); + var created = await _mediator.Send(new CreateOrderItemCommand(entity)); + return CreatedAtAction(nameof(GetById), new { id = created.ProductId }, created.Adapt()); } [HttpPut("{id}")] - public IActionResult Update(Guid id, OrderItemDto request) + public async Task Update(Guid id, OrderItemDto request) { - var entity = ToEntity(request); - entity.ProductId = id; - _orderItemRepository.Update(entity); - return NoContent(); - } - - [HttpDelete("{id}")] - public IActionResult Delete(Guid id) - { - var existingEntity = _orderItemRepository.GetById(id); - if (existingEntity is null) + var entity = request.Adapt(); + var updated = await _mediator.Send(new UpdateOrderItemCommand(id, entity)); + if (!updated) { return NotFound(); } - _orderItemRepository.Delete(id); return NoContent(); } - private static OrderItemDto ToDto(OrderItem entity) + [HttpDelete("{id}")] + public async Task Delete(Guid id) { - return new OrderItemDto + var deleted = await _mediator.Send(new DeleteOrderItemCommand(id)); + if (!deleted) { - ProductId = entity.ProductId, - Quantity = entity.Quantity, - }; - } + return NotFound(); + } - private static OrderItem ToEntity(OrderItemDto dto) - { - return new OrderItem - { - ProductId = dto.ProductId, - Quantity = dto.Quantity, - }; + return NoContent(); } } diff --git a/pse/sample_output/StoreApi.API/Controllers/UserController.cs b/pse/sample_output/StoreApi.API/Controllers/UserController.cs deleted file mode 100644 index cbe6b28..0000000 --- a/pse/sample_output/StoreApi.API/Controllers/UserController.cs +++ /dev/null @@ -1,87 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using System.Collections.Generic; -using System.Linq; -using StoreApi.API.Dtos; -using StoreApi.Domain.Entities; -using StoreApi.Domain.Repositories; - -namespace StoreApi.API.Controllers; - -[ApiController] -[Route("[controller]")] -public class UserController : ControllerBase -{ - private readonly IUserRepository _userRepository; - - public UserController(IUserRepository userRepository) - { - _userRepository = userRepository; - } - - [HttpGet] - public ActionResult> GetAll() - { - var entities = _userRepository.GetAll().Select(ToDto).ToList(); - return Ok(entities); - } - - [HttpGet("{id}")] - public ActionResult GetById(Guid id) - { - var entity = _userRepository.GetById(id); - if (entity is null) - { - return NotFound(); - } - - return Ok(ToDto(entity)); - } - - [HttpPost] - public ActionResult Create(UserDto request) - { - var entity = ToEntity(request); - _userRepository.Create(entity); - return CreatedAtAction(nameof(GetById), new { id = entity.Id }, ToDto(entity)); - } - - [HttpPut("{id}")] - public IActionResult Update(Guid id, UserDto request) - { - var entity = ToEntity(request); - entity.Id = id; - _userRepository.Update(entity); - return NoContent(); - } - - [HttpDelete("{id}")] - public IActionResult Delete(Guid id) - { - var existingEntity = _userRepository.GetById(id); - if (existingEntity is null) - { - return NotFound(); - } - - _userRepository.Delete(id); - return NoContent(); - } - - private static UserDto ToDto(User entity) - { - return new UserDto - { - Id = entity.Id, - Email = entity.Email, - }; - } - - private static User ToEntity(UserDto dto) - { - return new User - { - Id = dto.Id, - Email = dto.Email, - }; - } -} diff --git a/pse/sample_output/StoreApi.API/Dtos/UserDto.cs b/pse/sample_output/StoreApi.API/Dtos/UserDto.cs deleted file mode 100644 index 5367ab2..0000000 --- a/pse/sample_output/StoreApi.API/Dtos/UserDto.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System; - -namespace StoreApi.API.Dtos; - -public class UserDto -{ - public Guid Id { get; set; } - public string Email { get; set; } = string.Empty; -} diff --git a/pse/sample_output/StoreApi.API/Mapping/MappingConfig.cs b/pse/sample_output/StoreApi.API/Mapping/MappingConfig.cs new file mode 100644 index 0000000..d93ce32 --- /dev/null +++ b/pse/sample_output/StoreApi.API/Mapping/MappingConfig.cs @@ -0,0 +1,16 @@ +using Mapster; +using StoreApi.API.Dtos; +using StoreApi.Domain.Entities; + +namespace StoreApi.API.Mapping; + +public static class MappingConfig +{ + public static void Register() + { + TypeAdapterConfig.NewConfig(); + TypeAdapterConfig.NewConfig(); + TypeAdapterConfig.NewConfig(); + TypeAdapterConfig.NewConfig(); + } +} diff --git a/pse/sample_output/StoreApi.API/Program.cs b/pse/sample_output/StoreApi.API/Program.cs index 803d96c..0eb13c8 100644 --- a/pse/sample_output/StoreApi.API/Program.cs +++ b/pse/sample_output/StoreApi.API/Program.cs @@ -1,12 +1,43 @@ +using Serilog; +using FluentValidation; +using FluentValidation.AspNetCore; +using Mapster; +using MapsterMapper; +using StoreApi.API.Mapping; +using StoreApi.Application.Interfaces; +using StoreApi.Application.Services; +using StoreApi.Application.Cqrs; +using StoreApi.Domain.Repositories; +using StoreApi.Infrastructure.Repositories; using StoreApi.Application.Options; using Microsoft.EntityFrameworkCore; using StoreApi.Infrastructure.Persistence; -using MassTransit; var builder = WebApplication.CreateBuilder(args); +builder.Host.UseSerilog((context, services, loggerConfiguration) => + loggerConfiguration + .ReadFrom.Configuration(context.Configuration) + .ReadFrom.Services(services) + .WriteTo.Console()); + + builder.Services.AddControllers(); +builder.Services.AddFluentValidationAutoValidation(); +builder.Services.AddValidatorsFromAssemblyContaining(); + +MappingConfig.Register(); +builder.Services.AddMapster(); + +builder.Services.AddScoped(); +builder.Services.AddScoped(); + +builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssemblyContaining()); + +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); + builder.Services.Configure(builder.Configuration.GetSection("Database")); builder.Services.AddDbContext(options => options.UseNpgsql(builder.Configuration.GetConnectionString("Database"))); @@ -15,15 +46,6 @@ builder.Services.AddStackExchangeRedisCache(options => options.Configuration = builder.Configuration.GetConnectionString("Redis")); -builder.Services.Configure(builder.Configuration.GetSection("RabbitMq")); -builder.Services.AddMassTransit(x => -{ - x.UsingRabbitMq((context, cfg) => - { - cfg.Host(builder.Configuration.GetConnectionString("RabbitMq")); - }); -}); - var app = builder.Build(); app.MapControllers(); diff --git a/pse/sample_output/StoreApi.API/Properties/launchSettings.json b/pse/sample_output/StoreApi.API/Properties/launchSettings.json index 2a43993..7455cb2 100644 --- a/pse/sample_output/StoreApi.API/Properties/launchSettings.json +++ b/pse/sample_output/StoreApi.API/Properties/launchSettings.json @@ -5,7 +5,7 @@ "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": false, - "applicationUrl": "http://localhost:5257", + "applicationUrl": "http://localhost:5214", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } @@ -14,7 +14,7 @@ "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": false, - "applicationUrl": "https://localhost:7164;http://localhost:5257", + "applicationUrl": "https://localhost:7228;http://localhost:5214", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } diff --git a/pse/sample_output/StoreApi.API/StoreApi.API.csproj b/pse/sample_output/StoreApi.API/StoreApi.API.csproj index 2cc790a..f9a8bce 100644 --- a/pse/sample_output/StoreApi.API/StoreApi.API.csproj +++ b/pse/sample_output/StoreApi.API/StoreApi.API.csproj @@ -11,4 +11,13 @@ + + + + + + + + + diff --git a/pse/sample_output/StoreApi.API/StoreApi.API.http b/pse/sample_output/StoreApi.API/StoreApi.API.http index f969a7b..48c1588 100644 --- a/pse/sample_output/StoreApi.API/StoreApi.API.http +++ b/pse/sample_output/StoreApi.API/StoreApi.API.http @@ -1,4 +1,4 @@ -@StoreApi.API_HostAddress = http://localhost:5257 +@StoreApi.API_HostAddress = http://localhost:5214 GET {{StoreApi.API_HostAddress}}/weatherforecast/ Accept: application/json diff --git a/pse/sample_output/StoreApi.API/Validators/OrderDtoValidator.cs b/pse/sample_output/StoreApi.API/Validators/OrderDtoValidator.cs new file mode 100644 index 0000000..8eeaeea --- /dev/null +++ b/pse/sample_output/StoreApi.API/Validators/OrderDtoValidator.cs @@ -0,0 +1,14 @@ +using FluentValidation; +using StoreApi.API.Dtos; + +namespace StoreApi.API.Validators; + +public class OrderDtoValidator : AbstractValidator +{ + public OrderDtoValidator() + { + RuleFor(x => x.Id).NotEmpty(); + RuleFor(x => x.CreatedAt).NotEmpty(); + RuleFor(x => x.Status).NotEmpty(); + } +} diff --git a/pse/sample_output/StoreApi.API/Validators/OrderItemDtoValidator.cs b/pse/sample_output/StoreApi.API/Validators/OrderItemDtoValidator.cs new file mode 100644 index 0000000..3212967 --- /dev/null +++ b/pse/sample_output/StoreApi.API/Validators/OrderItemDtoValidator.cs @@ -0,0 +1,13 @@ +using FluentValidation; +using StoreApi.API.Dtos; + +namespace StoreApi.API.Validators; + +public class OrderItemDtoValidator : AbstractValidator +{ + public OrderItemDtoValidator() + { + RuleFor(x => x.ProductId).NotEmpty(); + RuleFor(x => x.Quantity).NotEmpty(); + } +} diff --git a/pse/sample_output/StoreApi.API/appsettings.Development.json b/pse/sample_output/StoreApi.API/appsettings.Development.json index d1c1ec3..943c96b 100644 --- a/pse/sample_output/StoreApi.API/appsettings.Development.json +++ b/pse/sample_output/StoreApi.API/appsettings.Development.json @@ -1,8 +1,7 @@ { "ConnectionStrings": { "Database": "Host=localhost;Port=5432;Database=app;Username=postgres;Password=postgres", - "Redis": "localhost:6379", - "RabbitMq": "amqp://guest:guest@localhost:5672" + "Redis": "localhost:6379" }, "Database": { "Provider": "Postgres" @@ -10,9 +9,6 @@ "Redis": { "Enabled": true }, - "RabbitMq": { - "Enabled": true - }, "Logging": { "LogLevel": { diff --git a/pse/sample_output/StoreApi.API/appsettings.json b/pse/sample_output/StoreApi.API/appsettings.json index 3288342..9d99d7a 100644 --- a/pse/sample_output/StoreApi.API/appsettings.json +++ b/pse/sample_output/StoreApi.API/appsettings.json @@ -1,8 +1,7 @@ { "ConnectionStrings": { "Database": "Host=localhost;Port=5432;Database=app;Username=postgres;Password=postgres", - "Redis": "localhost:6379", - "RabbitMq": "amqp://guest:guest@localhost:5672" + "Redis": "localhost:6379" }, "Database": { "Provider": "Postgres" @@ -10,9 +9,6 @@ "Redis": { "Enabled": true }, - "RabbitMq": { - "Enabled": true - }, "Logging": { "LogLevel": { diff --git a/pse/sample_output/StoreApi.Application/Cqrs/OrderItemRequests.cs b/pse/sample_output/StoreApi.Application/Cqrs/OrderItemRequests.cs new file mode 100644 index 0000000..8c354b2 --- /dev/null +++ b/pse/sample_output/StoreApi.Application/Cqrs/OrderItemRequests.cs @@ -0,0 +1,93 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using MediatR; +using StoreApi.Domain.Entities; +using StoreApi.Application.Interfaces; + +namespace StoreApi.Application.Cqrs; + +public sealed record GetAllOrderItemQuery() : IRequest>; + +public sealed record GetOrderItemByIdQuery(Guid Id) : IRequest; + +public sealed record CreateOrderItemCommand(OrderItem Entity) : IRequest; + +public sealed record UpdateOrderItemCommand(Guid Id, OrderItem Entity) : IRequest; + +public sealed record DeleteOrderItemCommand(Guid Id) : IRequest; + +public sealed class GetAllOrderItemQueryHandler : IRequestHandler> +{ + private readonly IOrderItemService _service; + + public GetAllOrderItemQueryHandler(IOrderItemService service) + { + _service = service; + } + + public Task> Handle(GetAllOrderItemQuery request, CancellationToken cancellationToken) + { + return Task.FromResult(_service.GetAll()); + } +} + +public sealed class GetOrderItemByIdQueryHandler : IRequestHandler +{ + private readonly IOrderItemService _service; + + public GetOrderItemByIdQueryHandler(IOrderItemService service) + { + _service = service; + } + + public Task Handle(GetOrderItemByIdQuery request, CancellationToken cancellationToken) + { + return Task.FromResult(_service.GetById(request.Id)); + } +} + +public sealed class CreateOrderItemCommandHandler : IRequestHandler +{ + private readonly IOrderItemService _service; + + public CreateOrderItemCommandHandler(IOrderItemService service) + { + _service = service; + } + + public Task Handle(CreateOrderItemCommand request, CancellationToken cancellationToken) + { + return Task.FromResult(_service.Create(request.Entity)); + } +} + +public sealed class UpdateOrderItemCommandHandler : IRequestHandler +{ + private readonly IOrderItemService _service; + + public UpdateOrderItemCommandHandler(IOrderItemService service) + { + _service = service; + } + + public Task Handle(UpdateOrderItemCommand request, CancellationToken cancellationToken) + { + return Task.FromResult(_service.Update(request.Id, request.Entity)); + } +} + +public sealed class DeleteOrderItemCommandHandler : IRequestHandler +{ + private readonly IOrderItemService _service; + + public DeleteOrderItemCommandHandler(IOrderItemService service) + { + _service = service; + } + + public Task Handle(DeleteOrderItemCommand request, CancellationToken cancellationToken) + { + return Task.FromResult(_service.Delete(request.Id)); + } +} diff --git a/pse/sample_output/StoreApi.Application/Cqrs/OrderRequests.cs b/pse/sample_output/StoreApi.Application/Cqrs/OrderRequests.cs new file mode 100644 index 0000000..7617a72 --- /dev/null +++ b/pse/sample_output/StoreApi.Application/Cqrs/OrderRequests.cs @@ -0,0 +1,93 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using MediatR; +using StoreApi.Domain.Entities; +using StoreApi.Application.Interfaces; + +namespace StoreApi.Application.Cqrs; + +public sealed record GetAllOrderQuery() : IRequest>; + +public sealed record GetOrderByIdQuery(Guid Id) : IRequest; + +public sealed record CreateOrderCommand(Order Entity) : IRequest; + +public sealed record UpdateOrderCommand(Guid Id, Order Entity) : IRequest; + +public sealed record DeleteOrderCommand(Guid Id) : IRequest; + +public sealed class GetAllOrderQueryHandler : IRequestHandler> +{ + private readonly IOrderService _service; + + public GetAllOrderQueryHandler(IOrderService service) + { + _service = service; + } + + public Task> Handle(GetAllOrderQuery request, CancellationToken cancellationToken) + { + return Task.FromResult(_service.GetAll()); + } +} + +public sealed class GetOrderByIdQueryHandler : IRequestHandler +{ + private readonly IOrderService _service; + + public GetOrderByIdQueryHandler(IOrderService service) + { + _service = service; + } + + public Task Handle(GetOrderByIdQuery request, CancellationToken cancellationToken) + { + return Task.FromResult(_service.GetById(request.Id)); + } +} + +public sealed class CreateOrderCommandHandler : IRequestHandler +{ + private readonly IOrderService _service; + + public CreateOrderCommandHandler(IOrderService service) + { + _service = service; + } + + public Task Handle(CreateOrderCommand request, CancellationToken cancellationToken) + { + return Task.FromResult(_service.Create(request.Entity)); + } +} + +public sealed class UpdateOrderCommandHandler : IRequestHandler +{ + private readonly IOrderService _service; + + public UpdateOrderCommandHandler(IOrderService service) + { + _service = service; + } + + public Task Handle(UpdateOrderCommand request, CancellationToken cancellationToken) + { + return Task.FromResult(_service.Update(request.Id, request.Entity)); + } +} + +public sealed class DeleteOrderCommandHandler : IRequestHandler +{ + private readonly IOrderService _service; + + public DeleteOrderCommandHandler(IOrderService service) + { + _service = service; + } + + public Task Handle(DeleteOrderCommand request, CancellationToken cancellationToken) + { + return Task.FromResult(_service.Delete(request.Id)); + } +} diff --git a/pse/sample_output/StoreApi.Application/Interfaces/IOrderItemService.cs b/pse/sample_output/StoreApi.Application/Interfaces/IOrderItemService.cs new file mode 100644 index 0000000..d55d08a --- /dev/null +++ b/pse/sample_output/StoreApi.Application/Interfaces/IOrderItemService.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using StoreApi.Domain.Entities; + +namespace StoreApi.Application.Interfaces; + +public interface IOrderItemService +{ + IEnumerable GetAll(); + + OrderItem? GetById(Guid id); + + OrderItem Create(OrderItem entity); + + bool Update(Guid id, OrderItem entity); + + bool Delete(Guid id); +} diff --git a/pse/sample_output/StoreApi.Application/Interfaces/IOrderService.cs b/pse/sample_output/StoreApi.Application/Interfaces/IOrderService.cs new file mode 100644 index 0000000..eefa771 --- /dev/null +++ b/pse/sample_output/StoreApi.Application/Interfaces/IOrderService.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using StoreApi.Domain.Entities; + +namespace StoreApi.Application.Interfaces; + +public interface IOrderService +{ + IEnumerable GetAll(); + + Order? GetById(Guid id); + + Order Create(Order entity); + + bool Update(Guid id, Order entity); + + bool Delete(Guid id); +} diff --git a/pse/sample_output/StoreApi.Application/Options/RabbitMqOptions.cs b/pse/sample_output/StoreApi.Application/Options/RabbitMqOptions.cs deleted file mode 100644 index ce82dfc..0000000 --- a/pse/sample_output/StoreApi.Application/Options/RabbitMqOptions.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace StoreApi.Application.Options; - -public class RabbitMqOptions -{ - public string Host { get; set; } = string.Empty; - public string Username { get; set; } = "guest"; - public string Password { get; set; } = "guest"; -} diff --git a/pse/sample_output/StoreApi.Application/Services/OrderItemService.cs b/pse/sample_output/StoreApi.Application/Services/OrderItemService.cs new file mode 100644 index 0000000..e8c1ded --- /dev/null +++ b/pse/sample_output/StoreApi.Application/Services/OrderItemService.cs @@ -0,0 +1,55 @@ +using System.Collections.Generic; +using StoreApi.Domain.Entities; +using StoreApi.Domain.Repositories; +using StoreApi.Application.Interfaces; + +namespace StoreApi.Application.Services; + +public class OrderItemService : IOrderItemService +{ + private readonly IOrderItemRepository _repository; + + public OrderItemService(IOrderItemRepository repository) + { + _repository = repository; + } + + public IEnumerable GetAll() + { + return _repository.GetAll(); + } + + public OrderItem? GetById(Guid id) + { + return _repository.GetById(id); + } + + public OrderItem Create(OrderItem entity) + { + _repository.Create(entity); + return entity; + } + + public bool Update(Guid id, OrderItem entity) + { + if (_repository.GetById(id) is null) + { + return false; + } + + entity.ProductId = id; + _repository.Update(entity); + return true; + } + + public bool Delete(Guid id) + { + if (_repository.GetById(id) is null) + { + return false; + } + + _repository.Delete(id); + return true; + } +} diff --git a/pse/sample_output/StoreApi.Application/Services/OrderService.cs b/pse/sample_output/StoreApi.Application/Services/OrderService.cs new file mode 100644 index 0000000..cceb23d --- /dev/null +++ b/pse/sample_output/StoreApi.Application/Services/OrderService.cs @@ -0,0 +1,55 @@ +using System.Collections.Generic; +using StoreApi.Domain.Entities; +using StoreApi.Domain.Repositories; +using StoreApi.Application.Interfaces; + +namespace StoreApi.Application.Services; + +public class OrderService : IOrderService +{ + private readonly IOrderRepository _repository; + + public OrderService(IOrderRepository repository) + { + _repository = repository; + } + + public IEnumerable GetAll() + { + return _repository.GetAll(); + } + + public Order? GetById(Guid id) + { + return _repository.GetById(id); + } + + public Order Create(Order entity) + { + _repository.Create(entity); + return entity; + } + + public bool Update(Guid id, Order entity) + { + if (_repository.GetById(id) is null) + { + return false; + } + + entity.Id = id; + _repository.Update(entity); + return true; + } + + public bool Delete(Guid id) + { + if (_repository.GetById(id) is null) + { + return false; + } + + _repository.Delete(id); + return true; + } +} diff --git a/pse/sample_output/StoreApi.Application/StoreApi.Application.csproj b/pse/sample_output/StoreApi.Application/StoreApi.Application.csproj index e9ee1cc..b3ee311 100644 --- a/pse/sample_output/StoreApi.Application/StoreApi.Application.csproj +++ b/pse/sample_output/StoreApi.Application/StoreApi.Application.csproj @@ -5,11 +5,7 @@ - - - - - + diff --git a/pse/sample_output/StoreApi.Application/UseCases/OrderItemUseCase.cs b/pse/sample_output/StoreApi.Application/UseCases/OrderItemUseCase.cs deleted file mode 100644 index cf8c2a3..0000000 --- a/pse/sample_output/StoreApi.Application/UseCases/OrderItemUseCase.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace StoreApi.Application.UseCases; - -public class OrderItemUseCase -{ - // TODO: add properties. -} diff --git a/pse/sample_output/StoreApi.Application/UseCases/OrderUseCase.cs b/pse/sample_output/StoreApi.Application/UseCases/OrderUseCase.cs deleted file mode 100644 index fe83e42..0000000 --- a/pse/sample_output/StoreApi.Application/UseCases/OrderUseCase.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace StoreApi.Application.UseCases; - -public class OrderUseCase -{ - // TODO: add properties. -} diff --git a/pse/sample_output/StoreApi.Application/UseCases/UserUseCase.cs b/pse/sample_output/StoreApi.Application/UseCases/UserUseCase.cs deleted file mode 100644 index cabc6b4..0000000 --- a/pse/sample_output/StoreApi.Application/UseCases/UserUseCase.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace StoreApi.Application.UseCases; - -public class UserUseCase -{ - // TODO: add properties. -} diff --git a/pse/sample_output/StoreApi.Domain/Entities/User.cs b/pse/sample_output/StoreApi.Domain/Entities/User.cs deleted file mode 100644 index 69897b3..0000000 --- a/pse/sample_output/StoreApi.Domain/Entities/User.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System; - -namespace StoreApi.Domain.Entities; - -public class User -{ - public Guid Id { get; set; } - public string Email { get; set; } = string.Empty; -} diff --git a/pse/sample_output/StoreApi.Domain/Repositories/IUserRepository.cs b/pse/sample_output/StoreApi.Domain/Repositories/IUserRepository.cs deleted file mode 100644 index 46fa04b..0000000 --- a/pse/sample_output/StoreApi.Domain/Repositories/IUserRepository.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.Collections.Generic; -using StoreApi.Domain.Entities; - -namespace StoreApi.Domain.Repositories; - -public interface IUserRepository -{ - IEnumerable GetAll(); - - User? GetById(Guid id); - - void Create(User entity); - - void Update(User entity); - - void Delete(Guid id); -} \ No newline at end of file diff --git a/pse/sample_output/StoreApi.Domain/ValueObjects/Email.cs b/pse/sample_output/StoreApi.Domain/ValueObjects/Email.cs deleted file mode 100644 index 4c57218..0000000 --- a/pse/sample_output/StoreApi.Domain/ValueObjects/Email.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace StoreApi.Domain.ValueObjects; - -public class Email -{ - public string Value { get; set; } = string.Empty; -} diff --git a/pse/sample_output/StoreApi.Infrastructure/Repositories/OrderItemRepository.cs b/pse/sample_output/StoreApi.Infrastructure/Repositories/OrderItemRepository.cs index 9fafd18..0393a0b 100644 --- a/pse/sample_output/StoreApi.Infrastructure/Repositories/OrderItemRepository.cs +++ b/pse/sample_output/StoreApi.Infrastructure/Repositories/OrderItemRepository.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using StoreApi.Domain.Entities; using StoreApi.Domain.Repositories; @@ -7,28 +8,50 @@ namespace StoreApi.Infrastructure.Repositories; public class OrderItemRepository : IOrderItemRepository { + private readonly List _items = new(); + public IEnumerable GetAll() { - return Array.Empty(); + return _items; } public OrderItem? GetById(Guid id) { - return null; + return _items.FirstOrDefault(entity => + EqualityComparer.Default.Equals(entity.ProductId, id)); } public void Create(OrderItem entity) { - throw new NotImplementedException(); + if (GetById(entity.ProductId) is not null) + { + Update(entity); + return; + } + + _items.Add(entity); } public void Update(OrderItem entity) { - throw new NotImplementedException(); + var index = _items.FindIndex(existing => + EqualityComparer.Default.Equals(existing.ProductId, entity.ProductId)); + + if (index >= 0) + { + _items[index] = entity; + return; + } + + _items.Add(entity); } public void Delete(Guid id) { - throw new NotImplementedException(); + var entity = GetById(id); + if (entity is not null) + { + _items.Remove(entity); + } } -} \ No newline at end of file +} diff --git a/pse/sample_output/StoreApi.Infrastructure/Repositories/OrderRepository.cs b/pse/sample_output/StoreApi.Infrastructure/Repositories/OrderRepository.cs index dce9f99..c07e54f 100644 --- a/pse/sample_output/StoreApi.Infrastructure/Repositories/OrderRepository.cs +++ b/pse/sample_output/StoreApi.Infrastructure/Repositories/OrderRepository.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using StoreApi.Domain.Entities; using StoreApi.Domain.Repositories; @@ -7,28 +8,50 @@ namespace StoreApi.Infrastructure.Repositories; public class OrderRepository : IOrderRepository { + private readonly List _items = new(); + public IEnumerable GetAll() { - return Array.Empty(); + return _items; } public Order? GetById(Guid id) { - return null; + return _items.FirstOrDefault(entity => + EqualityComparer.Default.Equals(entity.Id, id)); } public void Create(Order entity) { - throw new NotImplementedException(); + if (GetById(entity.Id) is not null) + { + Update(entity); + return; + } + + _items.Add(entity); } public void Update(Order entity) { - throw new NotImplementedException(); + var index = _items.FindIndex(existing => + EqualityComparer.Default.Equals(existing.Id, entity.Id)); + + if (index >= 0) + { + _items[index] = entity; + return; + } + + _items.Add(entity); } public void Delete(Guid id) { - throw new NotImplementedException(); + var entity = GetById(id); + if (entity is not null) + { + _items.Remove(entity); + } } -} \ No newline at end of file +} diff --git a/pse/sample_output/StoreApi.Infrastructure/Repositories/UserRepository.cs b/pse/sample_output/StoreApi.Infrastructure/Repositories/UserRepository.cs deleted file mode 100644 index 3a4048b..0000000 --- a/pse/sample_output/StoreApi.Infrastructure/Repositories/UserRepository.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System; -using System.Collections.Generic; -using StoreApi.Domain.Entities; -using StoreApi.Domain.Repositories; - -namespace StoreApi.Infrastructure.Repositories; - -public class UserRepository : IUserRepository -{ - public IEnumerable GetAll() - { - return Array.Empty(); - } - - public User? GetById(Guid id) - { - return null; - } - - public void Create(User entity) - { - throw new NotImplementedException(); - } - - public void Update(User entity) - { - throw new NotImplementedException(); - } - - public void Delete(Guid id) - { - throw new NotImplementedException(); - } -} \ No newline at end of file diff --git a/pse/sample_output/StoreApi.Infrastructure/StoreApi.Infrastructure.csproj b/pse/sample_output/StoreApi.Infrastructure/StoreApi.Infrastructure.csproj index e25b912..6817cb5 100644 --- a/pse/sample_output/StoreApi.Infrastructure/StoreApi.Infrastructure.csproj +++ b/pse/sample_output/StoreApi.Infrastructure/StoreApi.Infrastructure.csproj @@ -6,8 +6,6 @@ - - diff --git a/pse/sample_output/StoreApi.Tests/StoreApi.Tests.csproj b/pse/sample_output/StoreApi.Tests/StoreApi.Tests.csproj index 0b0e02a..a4c91a8 100644 --- a/pse/sample_output/StoreApi.Tests/StoreApi.Tests.csproj +++ b/pse/sample_output/StoreApi.Tests/StoreApi.Tests.csproj @@ -6,6 +6,7 @@ + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/pse/sample_output/StoreApi.Tests/Unit/OrderItemTests.cs b/pse/sample_output/StoreApi.Tests/Unit/OrderItemTests.cs index aa837ba..9580520 100644 --- a/pse/sample_output/StoreApi.Tests/Unit/OrderItemTests.cs +++ b/pse/sample_output/StoreApi.Tests/Unit/OrderItemTests.cs @@ -1,3 +1,5 @@ +using System; +using StoreApi.Domain.Entities; using Xunit; namespace StoreApi.Tests.Unit; @@ -7,6 +9,13 @@ public class OrderItemTests [Fact] public void CanCreateOrderItem() { - Assert.True(true); + var entity = new OrderItem + { + ProductId = Guid.NewGuid(), + Quantity = 1, + }; + + Assert.NotEqual(Guid.Empty, entity.ProductId); + Assert.Equal(1, entity.Quantity); } -} \ No newline at end of file +} diff --git a/pse/sample_output/StoreApi.Tests/Unit/OrderTests.cs b/pse/sample_output/StoreApi.Tests/Unit/OrderTests.cs index 33bec32..79804aa 100644 --- a/pse/sample_output/StoreApi.Tests/Unit/OrderTests.cs +++ b/pse/sample_output/StoreApi.Tests/Unit/OrderTests.cs @@ -1,3 +1,5 @@ +using System; +using StoreApi.Domain.Entities; using Xunit; namespace StoreApi.Tests.Unit; @@ -7,6 +9,15 @@ public class OrderTests [Fact] public void CanCreateOrder() { - Assert.True(true); + var entity = new Order + { + Id = Guid.NewGuid(), + CreatedAt = DateTime.UtcNow, + Status = "Status", + }; + + Assert.NotEqual(Guid.Empty, entity.Id); + Assert.NotEqual(default, entity.CreatedAt); + Assert.Equal("Status", entity.Status); } -} \ No newline at end of file +} diff --git a/pse/sample_output/StoreApi.Tests/Unit/UserTests.cs b/pse/sample_output/StoreApi.Tests/Unit/UserTests.cs deleted file mode 100644 index d1a1330..0000000 --- a/pse/sample_output/StoreApi.Tests/Unit/UserTests.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Xunit; - -namespace StoreApi.Tests.Unit; - -public class UserTests -{ - [Fact] - public void CanCreateUser() - { - Assert.True(true); - } -} \ No newline at end of file diff --git a/pse/sample_output/docker-compose.yml b/pse/sample_output/docker-compose.yml index 6843965..59dcb03 100644 --- a/pse/sample_output/docker-compose.yml +++ b/pse/sample_output/docker-compose.yml @@ -8,7 +8,6 @@ services: depends_on: - db - redis - - rabbitmq # dependencies db: image: postgres:17 @@ -24,9 +23,3 @@ services: container_name: redis ports: - "6379:6379" - rabbitmq: - image: rabbitmq:4 - container_name: rabbitmq - ports: - - "5672:5672" - - "15672:15672" diff --git a/pse/sample_output/pse.manifest.json b/pse/sample_output/pse.manifest.json index 9735d9a..bf4d6e0 100644 --- a/pse/sample_output/pse.manifest.json +++ b/pse/sample_output/pse.manifest.json @@ -1,15 +1,20 @@ { "runs": [ { - "id": "c918ea6b-c0c1-4224-8679-7957782fc76f", - "timestamp": "2026-07-05T18:02:24Z", - "input_file": "/home/x/Projects/ProjectScaffoldingEngine/pse/sample.pse", + "id": "14d340c1-193d-414a-bfec-384e1c0c8afc", + "timestamp": "2026-07-09T21:34:55Z", + "input_file": "/home/x/Projects/ProjectScaffoldingEngine/pse/sample_with_capabilities.pse", "capabilities": [ { "name": "cache", "value": "redis", "source": "inferred" }, + { + "name": "cqrs", + "value": "mediatr", + "source": "explicit" + }, { "name": "database", "value": "postgres", @@ -18,33 +23,28 @@ { "name": "logging", "value": "serilog", - "source": "preset" + "source": "explicit" }, { "name": "mapping", "value": "mapster", - "source": "preset" - }, - { - "name": "messaging", - "value": "rabbitmq", - "source": "inferred" + "source": "explicit" }, { "name": "testing", "value": "xunit", - "source": "preset" + "source": "explicit" }, { "name": "validation", "value": "fluentvalidation", - "source": "preset" + "source": "explicit" } ], - "dsl": "Project StoreApi target=dotnet {\n\n Archetype WebApi\n\n Context Orders {\n\n Entity Order {\n Guid Id\n DateTime CreatedAt\n String Status\n }\n\n Entity OrderItem {\n Guid ProductId\n Int Quantity\n }\n\n Aggregate OrderAggregate {\n root Order\n children OrderItem\n }\n }\n\n Context Identity {\n\n Entity User {\n Guid Id\n String Email\n }\n\n ValueObject Email {\n String Value\n }\n }\n\n Infrastructure {\n Database PostgreSQL\n Cache Redis\n MessageBroker RabbitMQ\n }\n\n Deployment Docker\n}", + "dsl": "Project StoreApi target=dotnet {\n\n Archetype WebApi\n\n Capabilities {\n Capability Logging\n Capability Validation\n Capability Mapping\n Capability CQRS = MediatR\n Capability Testing\n }\n\n Context Orders {\n\n Entity Order {\n Guid Id\n DateTime CreatedAt\n String Status\n }\n\n Entity OrderItem {\n Guid ProductId\n Int Quantity\n }\n\n Aggregate OrderAggregate {\n root Order\n children OrderItem\n }\n }\n\n Infrastructure {\n Database PostgreSQL\n Cache Redis\n }\n\n Deployment Docker\n}\n", "status": "completed", "error": null, - "finished_at": "2026-07-05T20:03:18Z" + "finished_at": "2026-07-09T23:35:55Z" } ] } \ No newline at end of file diff --git a/pse/sample_with_capabilities.pse b/pse/sample_with_capabilities.pse index e3721c4..08f05dc 100644 --- a/pse/sample_with_capabilities.pse +++ b/pse/sample_with_capabilities.pse @@ -6,6 +6,7 @@ Project StoreApi target=dotnet { Capability Logging Capability Validation Capability Mapping + Capability CQRS = MediatR Capability Testing } diff --git a/pse/templates/dotnet/ApplicationService.cs.tmpl b/pse/templates/dotnet/ApplicationService.cs.tmpl new file mode 100644 index 0000000..bbaaeb2 --- /dev/null +++ b/pse/templates/dotnet/ApplicationService.cs.tmpl @@ -0,0 +1,55 @@ +using System.Collections.Generic; +using {{DomainNamespace}}.Entities; +using {{DomainNamespace}}.Repositories; +using {{InterfaceNamespace}}; + +namespace {{Namespace}}; + +public class {{ClassName}} : {{InterfaceName}} +{ + private readonly I{{EntityName}}Repository _repository; + + public {{ClassName}}(I{{EntityName}}Repository repository) + { + _repository = repository; + } + + public IEnumerable<{{EntityName}}> GetAll() + { + return _repository.GetAll(); + } + + public {{EntityName}}? GetById({{IdType}} id) + { + return _repository.GetById(id); + } + + public {{EntityName}} Create({{EntityName}} entity) + { + _repository.Create(entity); + return entity; + } + + public bool Update({{IdType}} id, {{EntityName}} entity) + { + if (_repository.GetById(id) is null) + { + return false; + } + + entity.{{IdPropertyName}} = id; + _repository.Update(entity); + return true; + } + + public bool Delete({{IdType}} id) + { + if (_repository.GetById(id) is null) + { + return false; + } + + _repository.Delete(id); + return true; + } +} diff --git a/pse/templates/dotnet/ApplicationServiceInterface.cs.tmpl b/pse/templates/dotnet/ApplicationServiceInterface.cs.tmpl new file mode 100644 index 0000000..398f39c --- /dev/null +++ b/pse/templates/dotnet/ApplicationServiceInterface.cs.tmpl @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using {{DomainNamespace}}.Entities; + +namespace {{Namespace}}; + +public interface {{InterfaceName}} +{ + IEnumerable<{{EntityName}}> GetAll(); + + {{EntityName}}? GetById({{IdType}} id); + + {{EntityName}} Create({{EntityName}} entity); + + bool Update({{IdType}} id, {{EntityName}} entity); + + bool Delete({{IdType}} id); +} diff --git a/pse/templates/dotnet/DtoValidator.cs.tmpl b/pse/templates/dotnet/DtoValidator.cs.tmpl new file mode 100644 index 0000000..d37c20e --- /dev/null +++ b/pse/templates/dotnet/DtoValidator.cs.tmpl @@ -0,0 +1,11 @@ +using FluentValidation; +using {{DtoNamespace}}; + +namespace {{Namespace}}; + +public class {{ClassName}} : AbstractValidator<{{DtoName}}> +{ + public {{ClassName}}() + { +{{Rules}} } +} diff --git a/pse/templates/dotnet/MappingConfig.cs.tmpl b/pse/templates/dotnet/MappingConfig.cs.tmpl new file mode 100644 index 0000000..793b059 --- /dev/null +++ b/pse/templates/dotnet/MappingConfig.cs.tmpl @@ -0,0 +1,12 @@ +using Mapster; +using {{DtoNamespace}}; +using {{EntityNamespace}}; + +namespace {{Namespace}}; + +public static class MappingConfig +{ + public static void Register() + { +{{Mappings}} } +} diff --git a/pse/templates/dotnet/MediatRRequests.cs.tmpl b/pse/templates/dotnet/MediatRRequests.cs.tmpl new file mode 100644 index 0000000..751d6c9 --- /dev/null +++ b/pse/templates/dotnet/MediatRRequests.cs.tmpl @@ -0,0 +1,93 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using MediatR; +using {{DomainNamespace}}.Entities; +using {{InterfaceNamespace}}; + +namespace {{Namespace}}; + +public sealed record GetAll{{EntityName}}Query() : IRequest>; + +public sealed record Get{{EntityName}}ByIdQuery({{IdType}} Id) : IRequest<{{EntityName}}?>; + +public sealed record Create{{EntityName}}Command({{EntityName}} Entity) : IRequest<{{EntityName}}>; + +public sealed record Update{{EntityName}}Command({{IdType}} Id, {{EntityName}} Entity) : IRequest; + +public sealed record Delete{{EntityName}}Command({{IdType}} Id) : IRequest; + +public sealed class GetAll{{EntityName}}QueryHandler : IRequestHandler> +{ + private readonly I{{EntityName}}Service _service; + + public GetAll{{EntityName}}QueryHandler(I{{EntityName}}Service service) + { + _service = service; + } + + public Task> Handle(GetAll{{EntityName}}Query request, CancellationToken cancellationToken) + { + return Task.FromResult(_service.GetAll()); + } +} + +public sealed class Get{{EntityName}}ByIdQueryHandler : IRequestHandler +{ + private readonly I{{EntityName}}Service _service; + + public Get{{EntityName}}ByIdQueryHandler(I{{EntityName}}Service service) + { + _service = service; + } + + public Task<{{EntityName}}?> Handle(Get{{EntityName}}ByIdQuery request, CancellationToken cancellationToken) + { + return Task.FromResult(_service.GetById(request.Id)); + } +} + +public sealed class Create{{EntityName}}CommandHandler : IRequestHandler +{ + private readonly I{{EntityName}}Service _service; + + public Create{{EntityName}}CommandHandler(I{{EntityName}}Service service) + { + _service = service; + } + + public Task<{{EntityName}}> Handle(Create{{EntityName}}Command request, CancellationToken cancellationToken) + { + return Task.FromResult(_service.Create(request.Entity)); + } +} + +public sealed class Update{{EntityName}}CommandHandler : IRequestHandler +{ + private readonly I{{EntityName}}Service _service; + + public Update{{EntityName}}CommandHandler(I{{EntityName}}Service service) + { + _service = service; + } + + public Task Handle(Update{{EntityName}}Command request, CancellationToken cancellationToken) + { + return Task.FromResult(_service.Update(request.Id, request.Entity)); + } +} + +public sealed class Delete{{EntityName}}CommandHandler : IRequestHandler +{ + private readonly I{{EntityName}}Service _service; + + public Delete{{EntityName}}CommandHandler(I{{EntityName}}Service service) + { + _service = service; + } + + public Task Handle(Delete{{EntityName}}Command request, CancellationToken cancellationToken) + { + return Task.FromResult(_service.Delete(request.Id)); + } +} diff --git a/pse/templates/dotnet/Program.cs.tmpl b/pse/templates/dotnet/Program.cs.tmpl index 6bef341..67d870b 100644 --- a/pse/templates/dotnet/Program.cs.tmpl +++ b/pse/templates/dotnet/Program.cs.tmpl @@ -1,8 +1,9 @@ {{UsingLines}}var builder = WebApplication.CreateBuilder(args); +{{HostConfiguration}} builder.Services.AddControllers(); -{{InfraRegistrations}}var app = builder.Build(); +{{ServiceRegistrations}}{{InfraRegistrations}}var app = builder.Build(); {{InfraPipeline}}app.MapControllers(); diff --git a/pse/templates/dotnet/Repository.cs.tmpl b/pse/templates/dotnet/Repository.cs.tmpl index f42657b..f3469fb 100644 --- a/pse/templates/dotnet/Repository.cs.tmpl +++ b/pse/templates/dotnet/Repository.cs.tmpl @@ -1,5 +1,5 @@ -using System; using System.Collections.Generic; +using System.Linq; using {{DomainNamespace}}.Entities; using {{DomainNamespace}}.Repositories; @@ -7,28 +7,50 @@ namespace {{Namespace}}; public class {{ClassName}} : {{InterfaceName}} { - public IEnumerable<{{EntityName}}> GetAll() - { - return Array.Empty<{{EntityName}}>(); - } - - public {{EntityName}}? GetById(Guid id) - { - return null; - } - - public void Create({{EntityName}} entity) - { - throw new NotImplementedException(); - } - - public void Update({{EntityName}} entity) - { - throw new NotImplementedException(); - } - - public void Delete(Guid id) - { - throw new NotImplementedException(); - } + private readonly List<{{EntityName}}> _items = new(); + + public IEnumerable<{{EntityName}}> GetAll() + { + return _items; + } + + public {{EntityName}}? GetById({{IdType}} id) + { + return _items.FirstOrDefault(entity => + EqualityComparer<{{IdType}}>.Default.Equals(entity.{{IdPropertyName}}, id)); + } + + public void Create({{EntityName}} entity) + { + if (GetById(entity.{{IdPropertyName}}) is not null) + { + Update(entity); + return; + } + + _items.Add(entity); + } + + public void Update({{EntityName}} entity) + { + var index = _items.FindIndex(existing => + EqualityComparer<{{IdType}}>.Default.Equals(existing.{{IdPropertyName}}, entity.{{IdPropertyName}})); + + if (index >= 0) + { + _items[index] = entity; + return; + } + + _items.Add(entity); + } + + public void Delete({{IdType}} id) + { + var entity = GetById(id); + if (entity is not null) + { + _items.Remove(entity); + } + } } diff --git a/pse/templates/dotnet/RepositoryImplementation.cs.tmpl b/pse/templates/dotnet/RepositoryImplementation.cs.tmpl index 81c1afa..cb2e577 100644 --- a/pse/templates/dotnet/RepositoryImplementation.cs.tmpl +++ b/pse/templates/dotnet/RepositoryImplementation.cs.tmpl @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using {{DomainNamespace}}.Entities; using {{DomainNamespace}}.Repositories; @@ -7,28 +8,50 @@ namespace {{Namespace}}; public class {{ClassName}} : {{InterfaceName}} { + private readonly List<{{EntityName}}> _items = new(); + public IEnumerable<{{EntityName}}> GetAll() { - return Array.Empty<{{EntityName}}>(); + return _items; } public {{EntityName}}? GetById({{IdType}} id) { - return null; + return _items.FirstOrDefault(entity => + EqualityComparer<{{IdType}}>.Default.Equals(entity.{{IdPropertyName}}, id)); } public void Create({{EntityName}} entity) { - throw new NotImplementedException(); + if (GetById(entity.{{IdPropertyName}}) is not null) + { + Update(entity); + return; + } + + _items.Add(entity); } public void Update({{EntityName}} entity) { - throw new NotImplementedException(); + var index = _items.FindIndex(existing => + EqualityComparer<{{IdType}}>.Default.Equals(existing.{{IdPropertyName}}, entity.{{IdPropertyName}})); + + if (index >= 0) + { + _items[index] = entity; + return; + } + + _items.Add(entity); } public void Delete({{IdType}} id) { - throw new NotImplementedException(); + var entity = GetById(id); + if (entity is not null) + { + _items.Remove(entity); + } } -} \ No newline at end of file +} diff --git a/pse/templates/dotnet/TestClass.cs.tmpl b/pse/templates/dotnet/TestClass.cs.tmpl index 8ffa38a..de58feb 100644 --- a/pse/templates/dotnet/TestClass.cs.tmpl +++ b/pse/templates/dotnet/TestClass.cs.tmpl @@ -1,4 +1,4 @@ -using Xunit; +{{UsingLines}}using Xunit; namespace {{Namespace}}; @@ -7,6 +7,5 @@ public class {{ClassName}} [Fact] public void {{TestMethodName}}() { - Assert.True(true); - } -} \ No newline at end of file +{{Body}} } +} diff --git a/pse/templates/dotnet/WolverineMessages.cs.tmpl b/pse/templates/dotnet/WolverineMessages.cs.tmpl new file mode 100644 index 0000000..cab6895 --- /dev/null +++ b/pse/templates/dotnet/WolverineMessages.cs.tmpl @@ -0,0 +1,43 @@ +using System.Collections.Generic; +using {{DomainNamespace}}.Entities; +using {{InterfaceNamespace}}; + +namespace {{Namespace}}; + +public sealed record GetAll{{EntityName}}Query(); + +public sealed record Get{{EntityName}}ByIdQuery({{IdType}} Id); + +public sealed record Create{{EntityName}}Command({{EntityName}} Entity); + +public sealed record Update{{EntityName}}Command({{IdType}} Id, {{EntityName}} Entity); + +public sealed record Delete{{EntityName}}Command({{IdType}} Id); + +public static class {{EntityName}}MessageHandlers +{ + public static IEnumerable<{{EntityName}}> Handle(GetAll{{EntityName}}Query query, I{{EntityName}}Service service) + { + return service.GetAll(); + } + + public static {{EntityName}}? Handle(Get{{EntityName}}ByIdQuery query, I{{EntityName}}Service service) + { + return service.GetById(query.Id); + } + + public static {{EntityName}} Handle(Create{{EntityName}}Command command, I{{EntityName}}Service service) + { + return service.Create(command.Entity); + } + + public static bool Handle(Update{{EntityName}}Command command, I{{EntityName}}Service service) + { + return service.Update(command.Id, command.Entity); + } + + public static bool Handle(Delete{{EntityName}}Command command, I{{EntityName}}Service service) + { + return service.Delete(command.Id); + } +} diff --git a/pse/validation.py b/pse/validation.py index 1795e45..2928892 100644 --- a/pse/validation.py +++ b/pse/validation.py @@ -132,6 +132,25 @@ def validate_capabilities(model, source_text: str = None): source_text, ) ) + continue + + implementation = getattr(capability, "implementation", None) + if not implementation: + continue + + normalized_implementation = normalize_capability_implementation(name, implementation) + implementations = { + normalize_capability_implementation(name, implementation_name) + for implementation_name in registry.get(name, {}).keys() + } + if normalized_implementation not in implementations: + errors.append( + problem_from_node( + f"Capability '{capability.name}' implementation '{implementation}' is not recognized. Available implementations: {', '.join(sorted(registry.get(name, {}).keys()))}.", + capability, + source_text, + ) + ) return errors @@ -324,6 +343,18 @@ def normalize_name(value): return (value or "").replace("_", "").replace("-", "").lower() +def normalize_capability_implementation(capability: str, value: str): + normalized = normalize_name(value) + + if capability == "database" and normalized == "postgresql": + return "postgres" + + if capability == "cqrs" and normalized == "wolverinefx": + return "wolverine" + + return normalized + + def format_validation_error(errors): lines = ["DSL validation failed:"] lines.extend(f"- {error}" for error in errors) diff --git a/pse/vscode.py b/pse/vscode.py index 60913b2..2212adc 100644 --- a/pse/vscode.py +++ b/pse/vscode.py @@ -53,6 +53,13 @@ "Redis", "RabbitMQ", "Docker", + "Logging", + "Validation", + "Mapping", + "CQRS", + "Testing", + "MediatR", + "Wolverine", ] @@ -124,6 +131,10 @@ def completion_extension_js(): return items(VALUES, vscode.CompletionItemKind.Value, 'PSE value'); }} + if (/^\\s*Capability\\s+\\w+\\s*=\\s*$/.test(linePrefix)) {{ + return items(['MediatR', 'Wolverine'], vscode.CompletionItemKind.Value, 'PSE capability implementation'); + }} + return [ ...items(KEYWORDS, vscode.CompletionItemKind.Keyword, 'PSE keyword'), ...items(PRIMITIVE_TYPES, vscode.CompletionItemKind.TypeParameter, 'PSE primitive type'), diff --git a/tests/test_dsl_service.py b/tests/test_dsl_service.py index 703debc..34e4899 100644 --- a/tests/test_dsl_service.py +++ b/tests/test_dsl_service.py @@ -55,6 +55,33 @@ def test_sample_with_capabilities_is_valid(self): self.assertTrue(document.is_valid) self.assertEqual(document.diagnostics, ()) + def test_capability_implementation_selection_is_valid(self): + source = """Project StoreApi target=dotnet { + Archetype WebApi + Capabilities { + Capability CQRS = MediatR + } +}""" + + document = parse_document(source, "cqrs.pse") + + self.assertTrue(document.is_valid) + self.assertEqual(document.diagnostics, ()) + + def test_unknown_capability_implementation_returns_diagnostic(self): + source = """Project StoreApi target=dotnet { + Archetype WebApi + Capabilities { + Capability CQRS = Unknown + } +}""" + + diagnostics = validate_document(source, "cqrs.pse") + + self.assertEqual(len(diagnostics), 1) + self.assertIn("Capability 'CQRS' implementation 'Unknown' is not recognized", diagnostics[0].message) + self.assertEqual(diagnostics[0].range.start.line, 3) + def test_property_types_accept_primitives_and_domain_types(self): source = """Project StoreApi target=dotnet { Archetype WebApi diff --git a/tests/test_heuristics.py b/tests/test_heuristics.py new file mode 100644 index 0000000..d3a7e76 --- /dev/null +++ b/tests/test_heuristics.py @@ -0,0 +1,59 @@ +import pathlib +import unittest + +import yaml + + +ROOT = pathlib.Path(__file__).resolve().parents[1] +HEURISTICS = ROOT / "pse" / "heuristics" + + +class HeuristicsTests(unittest.TestCase): + def load(self, name): + with (HEURISTICS / name).open(encoding="utf-8") as handle: + return yaml.safe_load(handle) or {} + + def test_package_version_keys_exist(self): + packages = self.load("packages.yaml") + versions = self.load("versions.yaml") + + missing = [] + for implementation, config in packages.items(): + for package in config.get("packages", []): + if isinstance(package, dict): + version = package.get("version") + if version and version not in versions: + missing.append(f"{implementation}:{package.get('name')} -> {version}") + + self.assertEqual(missing, []) + + def test_capability_implementations_have_package_entries(self): + capabilities = self.load("capabilities.yaml") + packages = self.load("packages.yaml") + + missing = [] + for implementations in capabilities.values(): + for implementation in implementations: + if implementation not in packages: + missing.append(implementation) + + self.assertEqual(missing, []) + + def test_advertised_capabilities_are_generator_supported(self): + capabilities = set(self.load("capabilities.yaml")) + supported = { + "validation", + "logging", + "mapping", + "cqrs", + "testing", + "database", + "cache", + "messaging", + } + + self.assertEqual(capabilities - supported, set()) + + +if __name__ == "__main__": + unittest.main()