diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index a43b44b4..e29bd082 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,5 +1,5 @@ Before submitting a pull request, please read -https://github.com/bancolombia/scaffold-clean-architecture/wiki/Contributing +https://bancolombia.github.io/scaffold-clean-architecture/docs/contributing ## Description @@ -10,10 +10,10 @@ https://github.com/bancolombia/scaffold-clean-architecture/wiki/Contributing - [ ] Ci / Docs ## Checklist -- [ ] The pull request is complete according to the [guide of contributing](https://github.com/bancolombia/scaffold-clean-architecture/wiki/Contributing) +- [ ] The pull request is complete according to the [guide of contributing](https://bancolombia.github.io/scaffold-clean-architecture/docs/contributing) - [ ] Automated tests are written - [ ] The documentation is up-to-date - [ ] The pull request has a descriptive title that describes what has changed, and provides enough context for the changelog - [ ] If the pull request has a new driven-adapter or entry-point, you should add it to docs and `sh_generate_project.sh` files for generated code tests. -- [ ] If the pull request has changed structural files, you have implemented an UpgradeAction according to the [guide](https://github.com/bancolombia/scaffold-clean-architecture/wiki/Contributing#upgradeaction) -- [ ] If the pull request has a new Gradle Task, you should add `Analytics` according to the [guide](https://github.com/bancolombia/scaffold-clean-architecture/wiki/Contributing#analytics) +- [ ] If the pull request has changed structural files, you have implemented an UpgradeAction according to the [guide](http://localhost:3000/scaffold-clean-architecture/docs/contributing#more-on-pull-requests) +- [ ] If the pull request has a new Gradle Task, you should add `Analytics` according to the [guide](http://localhost:3000/scaffold-clean-architecture/docs/contributing) diff --git a/docs/docs/2-getting-started.md b/docs/docs/2-getting-started.md index dab89158..ba79ec8e 100644 --- a/docs/docs/2-getting-started.md +++ b/docs/docs/2-getting-started.md @@ -33,7 +33,7 @@ gradle tasks --- You can follow one of the next steps to create a quick start project with commands -# Quick Start +## Quick Start import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; diff --git a/docs/docs/5-contributing.md b/docs/docs/5-contributing.md new file mode 100644 index 00000000..ad51021e --- /dev/null +++ b/docs/docs/5-contributing.md @@ -0,0 +1,414 @@ +--- +sidebar_position: 5 +--- + +# Contributing + +## How to contribute + +### Step 1: Fork + +Fork the project [on GitHub](https://github.com/bancolombia/scaffold-clean-architecture) and clone your fork +locally. + +```text +$ git clone git@github.com:username/node.git +$ cd node +$ git remote add upstream https://github.com/bancolombia/scaffold-clean-architecture.git +$ git fetch upstream +``` + +If you haven't done it yet, Configure `git` so that it knows who you are: + +```text +$ git config user.name "My name" +$ git config user.email "my personal or business email" +``` + +You can use any name/email address you prefer here. We only use the +metadata generated by `git` using this configuration for properly attributing +your changes to you in the `AUTHORS` file and the changelog. + +If you would like for the GitHub UI to link the commit to your account +and award you the `Contributor` label after the changes have been merged, +make sure this local email is also added to your +[GitHub email list](https://github.com/settings/emails). + +### Step 2: Hook +We have a pre-commit hook to format the code and warrant the unit test pass, you only need to install this with the following command. + +```text +$ ./gradlew installGitHooks +``` +if you will be interest to format the code in any time , execute this command + +```text +$ ./gradlew goJF +``` +### Step 3: Branch + +As a best practice to keep your development environment as organized as +possible, create local branches to work within. These should also be created +directly off of the `master` branch. + +```text +$ git checkout -b my-branch -t upstream/master +``` + +We suggest that the name of the branch be separated with hyphens and be concise with what you want to solve, it does not have to start with the word feature or fix. + +Which branch: +* Make your pull request target master. + +### Step 4: Make the changes + +You could follow this [guide](https://github.com/bancolombia/scaffold-clean-architecture/wiki/Implementing-or-changing-a-module) + +### Pull request criteria + +* If you are attending an issue, add this id in your pull request. +* Include tests +* For new features consider adding new documentation item in Readme file +* Also, look at the GitHub's Pull Request guide + +### General info +* Comment on issues or pull requests. +* Please suggest changes to documentation when you find something unclear. +* You can create a fork of Scaffolding project in no time. Go to the github project and "Create your own fork". Create a new branch, commit, ..., when you're ready let us know about your pull request so we can discuss it and merge the PR! + +### More on pull requests + +The Scaffolding project has now a continuous release bot, that means that each merged pull request will be automatically released in a newer version of the plugin. For that reason each pull request has to go through a thorough review and/or discussion. + +Things we pay attention in a PR : + +* On pull requests, please document the change, what it brings, what is the benefit. +* Clean commit history in the topic branch in your fork of the repository, even during review. That means that commits are rebased and squashed if necessary, so that each commit clearly changes one things and there are no extraneous fix-ups. +* In the code, always test your feature / change, in unit tests. + +#### UpgradeAction +From version 2.3.0 you should implement an UpgradeAction, this implementation will be required if you had changed any structural file like: +- main.gradle +- build.gradle +- applications/app-service/build.gradle +- gradle.properties +- other similar + +This upgrade action is intended to keep upgraded the projects that have been created with a previous plugin version, allowing it to be able to have the new features by simple running the `gradle updateCleanArchitecture` task. + +The UpgradeAction should be implemented in the package `co.com.bancolombia.factory.upgrades.actions` and should have an specific date based name, which allows that the update occurs in the order which it is created. + +The standard is UpgradeY(year)M(month)D(day of month) + +If your pull request have structural changes and it does not comes with an UpgradeAction, it will be rejected. + +#### New Tasks +If your contribution will have new tasks, you can use one of the existing abstractions, and you should use the annotation @CATask providing a name, description and a shortcut +- [AbstractCleanArchitectureDefaultTask](https://github.com/bancolombia/scaffold-clean-architecture/blob/master/src/main/java/co/com/bancolombia/task/AbstractCleanArchitectureDefaultTask.java) +- [AbstractResolvableTypeTask](https://github.com/bancolombia/scaffold-clean-architecture/blob/master/src/main/java/co/com/bancolombia/task/AbstractCleanArchitectureDefaultTask.java) + +#### New Module Factories + +If your contribution has a new item of type DrivenAdapter, EntryPoint, PerformaceTest, AcceptanceTest, Helper or Pipeline these modules factories should be named and located following the next rule. + +Name: Should contain the prefix of his type, for example DrivenAdapter -> DrivenAdapterR2DBC, project will infer the driven adapter type as `R2DBC` +Package Location: Should be located in the corresponding package as you can see in the next table: + +| Type | Package | +|----------------|----------------------------------------------| +| DrivenAdapter | co.com.bancolombia.factory.adapters | +| EntryPoint | co.com.bancolombia.factory.entrypoints | +| AcceptanceTest | co.com.bancolombia.factory.tests.acceptance | +| PerformaceTest | co.com.bancolombia.factory.tests.performance | +| Helper | co.com.bancolombia.factory.helpers | +| Pipeline | co.com.bancolombia.factory.pipelines | + +#### Commits + +Good commit messages serve at least three important purposes: + +- To speed up the reviewing process +- To help us write a good release note + To help the future maintainers (it could be you!), say five years into the future, to find out why a particular change was made to the code or why a specific feature was added. + +Rules: + +1. You should use conventional commits, you can find more information [here](https://www.conventionalcommits.org/en/v1.0.0/) +2. Write the summary line and description of what you have done in the imperative mood, that is as if you were commanding someone. Start the line with "Fix", "Add", "Change" instead of "Fixed", "Added", "Changed" +3. Contain a short description of the change (preferably 50 characters or less, and no more than 72 characters) +4. be entirely in lowercase with the exception of proper nouns, acronyms, and the words that refer to code, like function/variable names +5. Keep the second line blank. +6. If your commit introduces a breaking change (semver-major), it should contain an explanation about the reason of the breaking change, which situation would trigger the breaking change and what is the exact change. +7. Use the body to explain what and why vs. how + +For example: + +``` +test: add functional test for non-standard base libraries +ci: support for Ubuntu 18 & CentOS 7 +deps: upgrade express to 4.17.1 +deps: standardjs as style guide, linter, and formatter +``` + +## Implementing or changing a module + +### Introduction + +Like you can research by using the project, it generates a multi-module gradle project, each module represents a +subproject that has some considerations: + +- applications: + - app-service: Main bootable application with setup configurations. +- domain: + - model: Domain models for the application, it also has the ports definitions. + - use-case: Domain business logic for the application processes. +- infrastructure: + - entry-points: Some modules which exposes the use-cases capabilities through an API of any protocol + - driven-adapter: Some modules that implements the ports defined in the domain model. Could be for example an access + to a database. + - helpers: Some modules that helps the adapters, could be for example a transformation module. +- other non gradle modules: Could be test projects, pipelines, etc... + +### Templating + +All our modules are generated based on templating using mustache, this templates are located in the resources directory +of the project. +Actually this templates should consider some variables that will impact the template, this variables are: + +- lombok: Boolean variable enabled by default, that implies that if you will use any lombok annotation you should + consider implement the vanilla version when lombok is disabled. +- metrics: Boolean variable enabled by default, that implies that you should implement some metric generator only when + enabled. +- language: (java|kotlin) you should use this value if you need to determine when run a custom configuration depending + on the language. +- reactive: Boolean variable disabled by default, this variable should be used to determine the resources directory of + the templates by following the standard \ for non reactive and \-reactive for reactive + modules. + +#### Predefined variables + +- **projectName**: the project name assigned by the user when running the `cleanArchitecture` task. +- **projectNameLower**: the same value of projectName but in lower case. +- **package**: package defined by the user when the project was created, for example**: `co.com.bancolombia.sample` +- **packagePath**: package path of the package defined, for example: `co/com/bancolombia/sample` +- **language**: language of the project, could be `java` or `kotlin` +- all constants defined in [ + `Constants`](https://github.com/bancolombia/scaffold-clean-architecture/blob/master/src/main/java/co/com/bancolombia/Constants.java) + with the same constant name, for example: + - **PLUGIN_VERSION**: `Constants.PLUGIN_VERSION` + +### Implementing a new module + +When you will implement a new module related to infrastructure layer you should keep in mind the required modifications. + +1. Add the resource templates in the correct location depending of your module type and the project type (reactive or + not reactive). + + |module type|resources path| + |----|---------------| + |Driven Adapter|resources/driven-adapter/\(-reactive)| + |Entry Point|resources/entry-point/\(-reactive)| + |Helper|resources/helper/\(-reactive)| + |Pipeline|resources/pipeline/\| + |Test|resources/test/\| + +2. Implement the ModuleFactory for the new module type in the correct package. + + |module type|package| + |----|---------------| + |Driven Adapter|co.com.bancolombia.factory.adapters| + |Entry Point|co.com.bancolombia.factory.entrypoints| + |Helper|co.com.bancolombia.factory.helpers| + |Pipeline|co.com.bancolombia.factory.pipelines| + |Test|co.com.bancolombia.factory.tests| + + When you implement a new `ModuleFactory` which definition is: + ```java + public interface ModuleFactory { + void buildModule(ModuleBuilder builder) throws IOException, CleanException; + } + ``` + + You will receive a `ModuleBuilder` instance which has a lot of utilities to achieve the module generation by doing + operation on the project files. + + Some common operations are: + +- setupFromTemplate + ```java + public void setupFromTemplate(String resourceGroup) throws IOException, ParamNotFoundException {} + ``` + +This will load the template definition from the resource directory, this definition should be a json file called +`definition.json` which has the structure: + +```json +{ + "folders": [ + "infrastructure/driven-adapters/dynamo-db/src/test/{{language}}/{{packagePath}}/dynamodb/helper" + ], + "files": {}, + "java": { + "driven-adapter/dynamo-db/build.gradle.mustache": "infrastructure/driven-adapters/dynamo-db/build.gradle" + }, + "kotlin": { + "driven-adapter/dynamo-db/build.gradle.kts.mustache": "infrastructure/driven-adapters/dynamo-db/build.gradle.kts" + } +} +``` + +Where: + +- folders: new directories to be created, usually are empty test directories. +- files: file map to be created for both languages `java` and `kotlin`. +- java: file map to be created only for java projects. +- kotlin: file map to be created only for kotlin projects. + +file map refers to, in the above example for java exists the key: `driven-adapter/dynamo-db/build.gradle.mustache` and +the value `infrastructure/driven-adapters/dynamo-db/build.gradle`, it means that the file with that key will be +generated in the value path. for paths value you could use variables for example +`infrastructure/driven-adapters/dynamo-db/src/main/{{language}}/{{packagePath}}/dynamodb/config/DynamoDBConfig.java` + +- appendToSettings + ```java + public void appendToSettings(String module, String baseDir) throws IOException {} + ``` + +This method will add a new module to the `setting.gradle` file, you should pass the module name and the module location. + +- appendDependencyToModule + ```java + public void appendDependencyToModule(String module, String dependency) throws IOException {} + ``` + +This method adds a new dependency for the indicated module, you could use the method `buildImplementationFromProject` +located in Utils class to create the dependency definition depending on the language. for example: + +```java + String dependency = Utils.buildImplementationFromProject(builder.isKotlin(), ":dynamodb"); + builder.appendDependencyToModule(APP_SERVICE, dependency); +``` + +- appendToProperties + ```java + public ObjectNode appendToProperties(String path) throws IOException {} + ``` + +This method gets or creates a new ObjectNode of the application.yaml file, and you can add the custom properties to this +file. For example: +The next code adds the property `spring.datasource.url=jdbc:h2:mem:test` and the property +`spring.datasource.driverClassName=org.h2.Driver` + +```java + builder + .appendToProperties("spring.datasource") + .put("url","jdbc:h2:mem:test") + .put("driverClassName","org.h2.Driver"); +``` + +3. Add the type of module in the correct enum. + + |module type|enum| + |----|---------------| + |Driven Adapter|co.com.bancolombia.factory.adapters.ModuleFactoryDrivenAdapter.DrivenAdapterType| + |Entry Point|co.com.bancolombia.factory.entrypoints.ModuleFactory.EntryPoint| + |Helper|co.com.bancolombia.factory.helpers.ModuleFactoryHelpers.EntryPointType| + |Pipeline|co.com.bancolombia.factory.pipelines.ModuleFactoryPipeline.PipelineType| + |Test|co.com.bancolombia.factory.tests.ModuleFactoryTests| + +4. Add the instantiation block in the correct class according to the new type. + + |module type|factory class| + |----|---------------| + |Driven Adapter|co.com.bancolombia.factory.adapters.ModuleFactoryDrivenAdapter| + |Entry Point|co.com.bancolombia.factory.entrypoints.ModuleFactory| + |Helper|co.com.bancolombia.factory.helpers.ModuleFactoryHelpers| + |Pipeline|co.com.bancolombia.factory.pipelines.ModuleFactoryPipeline| + |Test|co.com.bancolombia.factory.tests.ModuleFactoryTests| + +5. Add new required parameters in the task (Optional if required) + + For example the `generic` type of the driven adapter task requires a `name` argument, so in the task there is the + option: + ```java + @Option(option = "name", description = "Set driven adapter name when GENERIC type") + public void setName(String name) { + this.name = name; + } + ``` + + And then adds the value to the params + ```java + builder.addParam("task-param-name", name); + ``` + + The use the param in code or template + In code: + ```java + builder.getParam("task-param-name") + ``` + +### Modifying a module + + You should keep in mind the above documentation for creating a new module, and basically modify the necessary templates and the corresponding module factory. + +### Testing in local + +1. Make the changes which you want to try. +2. Change the version of the plugin for your custom own version in the `gradle.properties` file, for example: + systemProp.version=2.4.1.1 +3. Publish the plugin to mavenLocal by running: + ```shell + gradle publishToMavenLocal + ``` +1. Generates a new clean architecture project using the local project. + +To achieve it you have two options: + +- Using build.gradle. + + Create a build.gradle file in an empty folder with the next content: + + ```gradle + buildscript { + repositories { + mavenLocal() + } + dependencies { + classpath "co.com.bancolombia.cleanArchitecture:scaffold-clean-architecture:2.4.1.1" + } + } + + apply plugin: "co.com.bancolombia.cleanArchitecture" + ``` + + Then run the respective `gradle ca` task with your custom arguments. + **You should modify again the `build.gradle` to use the above syntax** + +- Using `settings.gradle` + + Create a build.gradle file in an empty folder with the next content: + + ```gradle + plugins { + id "co.com.bancolombia.cleanArchitecture" version "2.4.1.1" + } + ``` + Create a settings.gradle file in an empty folder with the next content: + + ```gradle + pluginManagement { + repositories { + mavenLocal() + gradlePluginPortal() + } + } + ``` + + Then run the respective `gradle ca` task with your custom arguments. + **You should modify again the `settings.gradle` adding the content on the beginning of the file** + + Then you can use the other tasks related to the plugin. + + + diff --git a/docs/docs/advanced/1-arch-unit-analysis.md b/docs/docs/advanced/1-arch-unit-analysis.md new file mode 100644 index 00000000..2a852ec8 --- /dev/null +++ b/docs/docs/advanced/1-arch-unit-analysis.md @@ -0,0 +1,239 @@ +--- +sidebar_position: 1 +--- + +# ArchUnit Analysis + +This document describes how we use ArchUnit to enforce architectural constraints in your project. + +## Predefined Architecture Rules + +### Rule_1.5: Reactive flows should use aws async clients + +![Static Badge](https://img.shields.io/badge/kind-Reactive%20Programming-blue) + +This rule try to enforce the usage of AsyncClient variants of AWS SDK, for example the usage of `kmsAsyncClient` instead +of `kmsClient` in reactive flows using the `Mono.fromFuture` method. + +### Rule_2.2: Domain classes should not be named with technology suffixes + +![Static Badge](https://img.shields.io/badge/kind-Clean%20Architecture-blue) + +This rule is intended to avoid the technical names usage in domain entities. We avoid UserDTO name instead of simply +define as User. + +you can use the `gradle.properties` file to set the forbidden suffixes, by default is: + +```properties +arch.unit.forbiddenDomainSuffixes=dto,request,response +``` + +Note that plugin will treat `dto` suffix also like `Dto` and `DTO` + +> ❌ No Compliant + +```java +package co.com.bancolombia.model.userrequest; + +public class UserRequest { + private String name; + private String email; +} +``` + +> ✅ Compliant + +```java +package co.com.bancolombia.model.user; + +public class User { + private String name; + private String email; +} +``` + +### Rule_2.4: Domain classes should not be named with technology names + +![Static Badge](https://img.shields.io/badge/kind-Clean%20Architecture-blue) + +This rule is intended to avoid the technical names usage in domain entities, gateways and use cases. We avoid +DynamoUserGateway name instead of simply define as UserGateway. + +you can use the `gradle.properties` file to set the forbidden partial class names, by default is: + +```properties +arch.unit.forbiddenDomainClassNames=rabbit,sqs,sns,ibm,dynamo,aws,mysql,postgres,redis,mongo,rsocket,r2dbc,http,kms,s3,graphql,kafka +``` + +Note that plugin will treat `rabbit` suffix also like `Rabbit` and `RABBIT` + +> ❌ No Compliant + +```java +package co.com.bancolombia.model.rabbitmessage; + +public class RabbitMessage { + private String contentType; + private byte[] content; +} +``` + +> ✅ Compliant + +```java +package co.com.bancolombia.model.message; + +public class Message { + private String contentType; + private byte[] content; +} +``` + +### Rule_2.7: Domain classes should not have fields named with technology names + +![Static Badge](https://img.shields.io/badge/kind-Clean%20Architecture-blue) + +The same purpose of `Rule_2.4` but applied to domain model fields or use case fields. + +you can use the `gradle.properties` file to set the forbidden field names, by default is the same that `Rule_2.4`, and +can be override with the same property. + +> ❌ No Compliant + +```java +package co.com.bancolombia.model.message; + +public class Message { + private String rabbitContentType; + private byte[] content; +} +``` + +> ✅ Compliant + +```java +package co.com.bancolombia.model.message; + +public class Message { + private String contentType; + private byte[] content; +} +``` + +### Rule_2.5: UseCases should only have final attributes to avoid concurrency issues + +![Static Badge](https://img.shields.io/badge/kind-Concurrency%20Safe%20Code-blue) + +This rule is designed to avoid the shared resources in concurrency, all dependencies of a Use Case should be immutable, +and each context call should have their own resources. + +> ❌ No compliant + +```java +package co.com.bancolombia.usecase.notifyusers; + +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class NotifyUsersUseCase { + private NotificationGateway notifier; // No Compliant + private User user; // No Compliant + + + public void notify(User user) { + this.user = user; + // some other business logic + this.sendMessage() + } + + private void sendMessage() { + // send message througth class instance of user + notifier.send("notification", user.getEmail()) + } +} +``` + +> ✅ Compliant + +```java +package co.com.bancolombia.usecase.notifyusers; + +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class NotifyUsersUseCase { + private final NotificationGateway notifier; // Compliant + + public void notify(User user) { + // some other business logic + this.sendMessage(user) + } + + private void sendMessage(User user) { + // send message througth input instance of user + notifier.send("notification", user.getEmail()) + } +} +``` + +### Rule_2.7: Beans classes should only have final attributes (injection by constructor) to avoid concurrency issues + +![Static Badge](https://img.shields.io/badge/kind-Concurrency%20Safe%20Code-blue) + +This rule is designed to avoid the shared resources in concurrency, all dependencies of a Bean should be immutable, and +each context call should have their own resources. We promote the dependencies injection through constructor over the +@Value annotated fields injection. + +> ❌ No Compliant + +```java + +@Component +public class MyService { + @Value("${external-service.endpoint}") + private String endpoint; // No Compliant + + public void someFunction(SomeObject domainObject) { + // ... logic that uses endpoint + } +} +``` + +> ✅ Compliant + +```java + +@Component +public class MyService { + private final String endpoint; // Compliant + + public MyService(@Value("${external-service.endpoint}") String endpoint) { + this.endpoint = endpoint; + } + + public void someFunction(SomeObject domainObject) { + // ... logic that uses endpoint + } +} +``` + +## Skip Rules + +If you want to skip Arch Unit rules validation, you can use the `gradle.properties` file to add +`arch.unit.ski=true`. + +We recommend not to skip the rules, but if you need to do it, you can add the property to the `gradle.properties` file. + +For example: + +```properties +package=co.com.bancolombia +systemProp.version=3.17.22 +reactive=true +lombok=true +metrics=true +language=java +org.gradle.parallel=true +systemProp.sonar.gradle.skipCompile=true +arch.unit.skip=true # Skip Arch Unit rules validation +``` \ No newline at end of file diff --git a/docs/docs/advanced/2-use-as-updater.md b/docs/docs/advanced/2-use-as-updater.md new file mode 100644 index 00000000..54622f49 --- /dev/null +++ b/docs/docs/advanced/2-use-as-updater.md @@ -0,0 +1,30 @@ +--- +sidebar_position: 2 +--- + +# Dependency Updater + +We have many open source java projects, so we try to keep dependencies updated, for this purpose whe have enabled the +plugin to be used as a dependency updater. This is not the `updateCleanArchitecture` task, we have a different task for +this. + +## Internal Task + +This task is the created for that, and you can use it by following the next steps: + +1. Add plugin to your `build.gradle` project file as described in the [Getting Started](/docs/2-getting-started.md) + section. +2. Add the following property to your `gradle.properties` file + like [commons-jms](https://github.com/bancolombia/commons-jms) project + +```properties +onlyUpdater=true +``` + +3. Now you can run the task `internalTask` or alias `it` to update the dependencies of your project. + +```bash +./gradlew it --action UPDATE_DEPENDENCIES +``` + +This tas will update gradle dependencies and gradle plugin versions. \ No newline at end of file diff --git a/docs/docs/advanced/_category_.json b/docs/docs/advanced/_category_.json new file mode 100644 index 00000000..49f87b38 --- /dev/null +++ b/docs/docs/advanced/_category_.json @@ -0,0 +1,8 @@ +{ + "label": "Advanced Options", + "position": 4, + "link": { + "type": "generated-index", + "description": "Some specific settings and tasks." + } +} diff --git a/src/main/java/co/com/bancolombia/exceptions/CleanDomainException.java b/src/main/java/co/com/bancolombia/exceptions/CleanDomainException.java new file mode 100644 index 00000000..0cf51a3a --- /dev/null +++ b/src/main/java/co/com/bancolombia/exceptions/CleanDomainException.java @@ -0,0 +1,7 @@ +package co.com.bancolombia.exceptions; + +public class CleanDomainException extends CleanUncheckedException { + public CleanDomainException(String message) { + super(message); + } +} diff --git a/src/main/java/co/com/bancolombia/factory/validations/architecture/ArchitectureValidation.java b/src/main/java/co/com/bancolombia/factory/validations/architecture/ArchitectureValidation.java index 8b82b639..ef4c18e9 100644 --- a/src/main/java/co/com/bancolombia/factory/validations/architecture/ArchitectureValidation.java +++ b/src/main/java/co/com/bancolombia/factory/validations/architecture/ArchitectureValidation.java @@ -1,20 +1,30 @@ package co.com.bancolombia.factory.validations.architecture; import co.com.bancolombia.Constants; +import co.com.bancolombia.exceptions.CleanDomainException; import co.com.bancolombia.factory.ModuleBuilder; import co.com.bancolombia.utils.FileUtils; import java.io.File; import java.util.stream.Collectors; +import java.util.stream.Stream; import lombok.AccessLevel; import lombok.NoArgsConstructor; import lombok.SneakyThrows; +import org.apache.commons.lang3.StringUtils; import org.gradle.api.Project; @NoArgsConstructor(access = AccessLevel.PRIVATE) public final class ArchitectureValidation { + private static final String SKIP_PROP = "arch.unit.skip"; + private static final String FORBIDDEN_DOMAIN_SUFFIXES_PROP = "arch.unit.forbiddenDomainSuffixes"; + private static final String FORBIDDEN_DOMAIN_SUFFIXES = "dto,request,response"; + private static final String FORBIDDEN_DOMAIN_CLASS_NAMES_PROP = + "arch.unit.forbiddenDomainClassNames"; + private static final String FORBIDDEN_DOMAIN_NAMES = + "rabbit,sqs,sns,ibm,dynamo,aws,mysql,postgres,redis,mongo,rsocket,r2dbc,http,kms,s3,graphql,kafka"; public static void inject(Project project, ModuleBuilder builder) { - if (!FileUtils.readBooleanProperty("skipArchitectureTests")) { + if (!FileUtils.readBooleanProperty(SKIP_PROP)) { String os = System.getProperty("os.name"); String paths = project.getAllprojects().stream() @@ -22,6 +32,14 @@ public static void inject(Project project, ModuleBuilder builder) { .collect(Collectors.joining(",")); builder.addParam("reactive", builder.isReactive()); builder.addParam("modulePaths", paths); + builder.addParam( + "forbiddenDomainSuffixes", + loadForbiddenValuesForAsString( + FORBIDDEN_DOMAIN_SUFFIXES_PROP, FORBIDDEN_DOMAIN_SUFFIXES)); + builder.addParam( + "forbiddenDomainClassNames", + loadForbiddenValuesForAsString( + FORBIDDEN_DOMAIN_CLASS_NAMES_PROP, FORBIDDEN_DOMAIN_NAMES)); project.getAllprojects().stream() .filter(p -> p.getName().equals(Constants.APP_SERVICE)) .findFirst() @@ -29,6 +47,59 @@ public static void inject(Project project, ModuleBuilder builder) { } } + public static void validateModelName(String name) { + loadForbiddenValuesFor(FORBIDDEN_DOMAIN_SUFFIXES_PROP, FORBIDDEN_DOMAIN_SUFFIXES) + .filter(name::endsWith) + .findAny() + .ifPresent( + forbidden -> { + throw new CleanDomainException( + "Model suffix '" + + forbidden + + "' is forbidden, please don't use " + + "tech names in domain model name at " + + name); + }); + validateUseCaseName(name); + } + + public static void validateUseCaseName(String name) { + loadForbiddenValuesFor(FORBIDDEN_DOMAIN_CLASS_NAMES_PROP, FORBIDDEN_DOMAIN_NAMES) + .filter(name::contains) + .findAny() + .ifPresent( + forbidden -> { + throw new CleanDomainException( + "Model or UseCase word '" + + forbidden + + "' " + + "is forbidden, please don't use tech names in domain at " + + name); + }); + } + + private static String loadForbiddenValuesForAsString(String property, String defaults) { + return loadForbiddenValuesFor(property, defaults) + .collect(Collectors.joining("\",\"", "\"", "\"")); + } + + private static Stream loadForbiddenValuesFor(String property, String defaults) { + String values = defaults; + try { + String content = FileUtils.readProperties(property); + if (StringUtils.isNoneEmpty(content)) { + values = content; + } + } catch (Exception ignored) { // NOSONAR + } + return Stream.of(values.split(",")) + .flatMap(tool -> Stream.of(tool, tool.toUpperCase(), classCase(tool))); + } + + private static String classCase(String tool) { + return tool.substring(0, 1).toUpperCase() + tool.substring(1); + } + private static String toOSPath(String os, File projectDir) { if (os != null && os.contains("Windows")) { return projectDir.toString().replace("\\", "\\\\"); @@ -50,4 +121,9 @@ private static void generateArchUnitFiles( Constants.APP_SERVICE, "testImplementation 'com.fasterxml.jackson.core:jackson-databind'"); builder.persist(); } + + public enum Type { + CLASS_NAME, + CLASS_SUFFIX + } } diff --git a/src/main/java/co/com/bancolombia/task/GenerateModelTask.java b/src/main/java/co/com/bancolombia/task/GenerateModelTask.java index 8cfd5ca0..9e788bf0 100644 --- a/src/main/java/co/com/bancolombia/task/GenerateModelTask.java +++ b/src/main/java/co/com/bancolombia/task/GenerateModelTask.java @@ -1,6 +1,7 @@ package co.com.bancolombia.task; import co.com.bancolombia.exceptions.ParamNotFoundException; +import co.com.bancolombia.factory.validations.architecture.ArchitectureValidation; import co.com.bancolombia.task.annotations.CATask; import co.com.bancolombia.utils.Utils; import java.io.IOException; @@ -24,6 +25,7 @@ public void execute() throws IOException, ParamNotFoundException { "No model name, usage: gradle generateModel --name [name]"); } name = Utils.capitalize(name); + ArchitectureValidation.validateModelName(name); logger.lifecycle("Clean Architecture plugin version: {}", Utils.getVersionPlugin()); logger.lifecycle("Model Name: {}", name); builder.addParam("modelName", name.toLowerCase()); diff --git a/src/main/java/co/com/bancolombia/task/GenerateUseCaseTask.java b/src/main/java/co/com/bancolombia/task/GenerateUseCaseTask.java index aac78da3..10f794ab 100644 --- a/src/main/java/co/com/bancolombia/task/GenerateUseCaseTask.java +++ b/src/main/java/co/com/bancolombia/task/GenerateUseCaseTask.java @@ -1,6 +1,7 @@ package co.com.bancolombia.task; import co.com.bancolombia.exceptions.ParamNotFoundException; +import co.com.bancolombia.factory.validations.architecture.ArchitectureValidation; import co.com.bancolombia.task.annotations.CATask; import co.com.bancolombia.utils.Utils; import java.io.IOException; @@ -28,6 +29,7 @@ public void execute() throws IOException, ParamNotFoundException { "No use case name, usage: gradle generateUseCase --name [name]"); } name = Utils.capitalize(name); + ArchitectureValidation.validateUseCaseName(name); String className = refactorName(name); String useCaseName = className.replace(USECASE_CLASS_NAME, "").toLowerCase(); logger.lifecycle("Clean Architecture plugin version: {}", Utils.getVersionPlugin()); diff --git a/src/main/java/co/com/bancolombia/utils/FileUtils.java b/src/main/java/co/com/bancolombia/utils/FileUtils.java index 113bcc48..9264e86c 100644 --- a/src/main/java/co/com/bancolombia/utils/FileUtils.java +++ b/src/main/java/co/com/bancolombia/utils/FileUtils.java @@ -84,6 +84,10 @@ public static List finderSubProjects(String dirPath) { return textFiles; } + public static String readProperties(String variable) throws IOException { + return readProperties(".", variable); + } + public static String readProperties(String projectPath, String variable) throws IOException { Properties properties = new Properties(); try (BufferedReader br = new BufferedReader(new FileReader(projectPath + GRADLE_PROPERTIES))) { @@ -98,7 +102,7 @@ public static String readProperties(String projectPath, String variable) throws public static boolean readBooleanProperty(String variable) { try { - return "true".equals(readProperties(".", variable)); + return "true".equals(readProperties(variable)); } catch (IOException ignored) { return false; } diff --git a/src/main/resources/structure/applications/appservice/arch-validations/architecture-test.java.mustache b/src/main/resources/structure/applications/appservice/arch-validations/architecture-test.java.mustache index 0b1f5d2c..c3cda91b 100644 --- a/src/main/resources/structure/applications/appservice/arch-validations/architecture-test.java.mustache +++ b/src/main/resources/structure/applications/appservice/arch-validations/architecture-test.java.mustache @@ -11,7 +11,7 @@ import com.tngtech.archunit.lang.ConditionEvent; import com.tngtech.archunit.lang.ConditionEvents; import com.tngtech.archunit.lang.SimpleConditionEvent; import com.tngtech.archunit.lang.syntax.elements.MembersShouldConjunction; -import lombok.extern.log4j.Log4j2; +import lombok.extern.java.Log; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -32,13 +32,14 @@ import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; +import java.util.logging.Level; import java.util.stream.Stream; import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes; import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.fields; // Please do not modify this file -@Log4j2 +@Log class ArchitectureTest { private static JavaClasses allClasses; private static JavaClasses domainClasses; @@ -49,7 +50,13 @@ class ArchitectureTest { @BeforeAll static void importClasses() { - allClasses = new ClassFileImporter().importPackages("{{package}}"); + allClasses = new ClassFileImporter().importPackages("{{package}}") + .that(new DescribedPredicate<>("is-not-commons-lib") { + @Override + public boolean test(JavaClass javaClass) { + return !javaClass.getPackageName().contains("co.com.bancolombia.commons.jms"); + } + }); domainClasses = new ClassFileImporter().importPackages("{{package}}.usecase", "{{package}}.model"); useCaseClasses = new ClassFileImporter().importPackages("{{package}}.usecase"); } @@ -62,11 +69,11 @@ class ArchitectureTest { try { Files.write(Path.of(path, "build/issues.json"), mapper.writeValueAsBytes(issues.getOrDefault(path, new Utils.IssuesReport()))); } catch (IOException e) { - log.warn(e.getMessage()); + log.log(Level.WARNING, e.getMessage()); } }); } catch (Exception e) { - log.warn(e.getMessage()); + log.log(Level.WARNING, e.getMessage()); } } @@ -87,8 +94,7 @@ class ArchitectureTest { {{/reactive}} @Test void domainClassesShouldNotBeNamedWithTechSuffixes() { - ArchRule rule = Stream.of("dto", "request", "response") - .flatMap(tool -> Stream.of(tool, tool.toUpperCase(), classCase(tool))) + ArchRule rule = Stream.of({{{forbiddenDomainSuffixes}}}) .reduce(classes().should().haveSimpleNameNotEndingWith("Dto"), (cj, tool) -> cj.andShould().haveSimpleNameNotEndingWith(tool), (a, b) -> b) @@ -100,9 +106,7 @@ class ArchitectureTest { @Test void domainClassesShouldNotBeNamedWithToolNames() { - ArchRule rule = Stream.of("rabbit", "sqs", "sns", "ibm", "dynamo", "aws", "mysql", "postgres", - "redis", "mongo", "rsocket", "r2dbc", "http", "kms", "s3", "graphql") - .flatMap(tool -> Stream.of(tool, tool.toUpperCase(), classCase(tool))) + ArchRule rule = Stream.of({{{forbiddenDomainClassNames}}}) .reduce(classes().should().haveSimpleNameNotContaining("rabbit"), (cj, tool) -> cj.andShould().haveSimpleNameNotContaining(tool), (a, b) -> b) @@ -127,9 +131,7 @@ class ArchitectureTest { @Test void domainClassesShouldNotHaveFieldsNamedWithToolNames() { - ArchRule rule = Stream.of("rabbit", "sqs", "sns", "ibm", "dynamo", "aws", "mysql", "postgres", - "redis", "mongo", "rsocket", "r2dbc", "http", "kms", "s3", "graphql") - .flatMap(tool -> Stream.of(tool, tool.toUpperCase(), classCase(tool))) + ArchRule rule = Stream.of({{{forbiddenDomainClassNames}}}) .reduce((MembersShouldConjunction) fields().should().haveNameNotContaining("rabbit"), (cj, tool) -> cj.andShould().haveNameNotContaining(tool), (a, b) -> b) @@ -163,10 +165,6 @@ class ArchitectureTest { // Utilities - private String classCase(String tool) { - return tool.substring(0, 1).toUpperCase() + tool.substring(1); - } - {{#reactive}} private DescribedPredicate areUsingAnAwsClient() { return withPredicate("are using an aws client", @@ -241,7 +239,7 @@ class ArchitectureTest { Utils.resolveFinalLocation(file, location))), 5)); } }); - log.warn("ARCHITECTURE_RULE_VIOLATED: This will cause a build error in future.\nPlease review our wiki at https://github.com/bancolombia/scaffold-clean-architecture/wiki/Architecture-Rules", e); + log.log(Level.WARNING, "ARCHITECTURE_RULE_VIOLATED: This will cause a build error in future.\nPlease review our wiki at https://bancolombia.github.io/scaffold-clean-architecture/docs/advanced/arch-unit-analysis", e); } } } diff --git a/src/main/resources/structure/applications/appservice/arch-validations/utils.java.mustache b/src/main/resources/structure/applications/appservice/arch-validations/utils.java.mustache index 3d1f6673..87a94d94 100644 --- a/src/main/resources/structure/applications/appservice/arch-validations/utils.java.mustache +++ b/src/main/resources/structure/applications/appservice/arch-validations/utils.java.mustache @@ -1,14 +1,15 @@ package {{package}}; -import lombok.extern.log4j.Log4j2; +import lombok.extern.java.Log; import java.io.IOException; import java.nio.file.*; import java.nio.file.attribute.BasicFileAttributes; import java.util.*; import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.logging.Level; -@Log4j2 +@Log public class Utils { public static ArchitectureRule parseToRule(String message) { String ruleId = extractRuleId(message); @@ -68,7 +69,7 @@ public class Utils { line++; } } catch (Exception e) { - log.info(e); + log.log(Level.INFO, "arch unit error", e); } return location.getLine(); } @@ -119,7 +120,7 @@ public class Utils { ); } } catch (Exception e) { - log.info(e.getMessage()); + log.log(Level.INFO, e.getMessage()); } return files; } diff --git a/src/test/java/co/com/bancolombia/factory/validations/architecture/ArchitectureValidationTest.java b/src/test/java/co/com/bancolombia/factory/validations/architecture/ArchitectureValidationTest.java index 44b9505d..ee7a106b 100644 --- a/src/test/java/co/com/bancolombia/factory/validations/architecture/ArchitectureValidationTest.java +++ b/src/test/java/co/com/bancolombia/factory/validations/architecture/ArchitectureValidationTest.java @@ -5,12 +5,14 @@ import static co.com.bancolombia.TestUtils.getTask; import static co.com.bancolombia.TestUtils.getTestDir; import static co.com.bancolombia.TestUtils.setupProject; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.when; +import co.com.bancolombia.exceptions.CleanDomainException; import co.com.bancolombia.exceptions.CleanException; import co.com.bancolombia.factory.ModuleBuilder; import co.com.bancolombia.task.GenerateStructureTask; @@ -38,8 +40,11 @@ class ArchitectureValidationTest { private Project project; @BeforeEach - public void setup() throws IOException, CleanException { + public void setup() { deleteStructure(Path.of(TEST_DIR)); + } + + public void mocks() throws CleanException, IOException { project = spy(setupProject(ArchitectureValidationTest.class, GenerateStructureTask.class)); GenerateStructureTask generateStructureTask = getTask(project, GenerateStructureTask.class); generateStructureTask.executeBaseTask(); @@ -60,8 +65,9 @@ public static void tearDown() { } @Test - void shouldInjectTests() throws IOException { + void shouldInjectTests() throws IOException, CleanException { // Arrange + mocks(); Path testFile = Path.of(BASE_PATH, TEST_FILE); Files.deleteIfExists(testFile); when(styledTextOutput.style(any())).thenReturn(styledTextOutput); @@ -73,4 +79,40 @@ void shouldInjectTests() throws IOException { // Assert assertTrue(Files.exists(testFile)); } + + @Test + void shouldFailDomainNameHaveTechSuffix() { + // Arrange + // Assert + assertThrows( + CleanDomainException.class, + () -> { + // Act + ArchitectureValidation.validateModelName("SampleRequest"); + }); + } + + @Test + void shouldFailDomainNameHaveTechName() { + // Arrange + // Assert + assertThrows( + CleanDomainException.class, + () -> { + // Act + ArchitectureValidation.validateModelName("KafkaModel"); + }); + } + + @Test + void shouldFailUseCaseNameHaveTechName() { + // Arrange + // Assert + assertThrows( + CleanDomainException.class, + () -> { + // Act + ArchitectureValidation.validateModelName("RabbitUseCase"); + }); + } }