├── .github ├── FUNDING.yml ├── dependabot.yml ├── release.yml └── workflows │ ├── maven.yml │ ├── release-docs.yml │ └── release.yml ├── .gitignore ├── .idea └── icon.png ├── .mvn └── wrapper │ └── maven-wrapper.properties ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── docs ├── CNAME ├── assets │ └── images │ │ ├── taikai-header.png │ │ ├── taikai-logo-dark.png │ │ └── taikai-logo-light.png ├── contributing.md ├── documentation.md └── index.md ├── mkdocs.yml ├── mvnw ├── mvnw.cmd ├── pom.xml └── src ├── main └── java │ └── com │ └── enofex │ └── taikai │ ├── Namespace.java │ ├── Taikai.java │ ├── TaikaiException.java │ ├── TaikaiRule.java │ ├── configures │ ├── AbstractConfigurer.java │ ├── Configurer.java │ ├── ConfigurerContext.java │ ├── Configurers.java │ ├── Customizer.java │ └── DisableableConfigurer.java │ ├── internal │ ├── ArchConditions.java │ ├── DescribedPredicates.java │ └── Modifiers.java │ ├── java │ ├── ConstantNaming.java │ ├── Deprecations.java │ ├── HashCodeAndEquals.java │ ├── ImportsConfigurer.java │ ├── JavaConfigurer.java │ ├── NamingConfigurer.java │ ├── NoSystemOutOrErr.java │ ├── PackageNaming.java │ ├── ProtectedMembers.java │ ├── SerialVersionUID.java │ └── UtilityClasses.java │ ├── logging │ ├── LoggerConventions.java │ └── LoggingConfigurer.java │ ├── spring │ ├── BootConfigurer.java │ ├── ConfigurationsConfigurer.java │ ├── ControllersConfigurer.java │ ├── PropertiesConfigurer.java │ ├── RepositoriesConfigurer.java │ ├── ServicesConfigurer.java │ ├── SpringConfigurer.java │ ├── SpringDescribedPredicates.java │ └── ValidatedController.java │ └── test │ ├── ContainAssertionsOrVerifications.java │ ├── JUnit5Configurer.java │ ├── JUnit5DescribedPredicates.java │ └── TestConfigurer.java └── test └── java └── com └── enofex └── taikai ├── ArchitectureTest.java ├── NamespaceTest.java ├── TaikaiRuleTest.java ├── TaikaiTest.java ├── Usage.java ├── configures ├── ConfigurerContextTest.java └── ConfigurersTest.java ├── internal ├── ArchConditionsTest.java ├── DescribedPredicatesTest.java └── ModifiersTest.java ├── logging └── LoggingConfigurerTest.java └── test └── JUnit5DescribedPredicatesTest.java /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [ mnhock ] 4 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: maven 9 | directory: "/" 10 | schedule: 11 | interval: daily 12 | - package-ecosystem: github-actions 13 | directory: "/" 14 | schedule: 15 | interval: weekly 16 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | changelog: 2 | exclude: 3 | labels: 4 | - ignore-for-release 5 | authors: 6 | - octocat 7 | 8 | categories: 9 | - title: 🎉 New Features 10 | labels: 11 | - enhancement 12 | - feature 13 | 14 | - title: 🐞 Bug Fixes 15 | labels: 16 | - bug 17 | 18 | - title: 📔 Documentation 19 | labels: 20 | - documentation 21 | 22 | - title: 🔨 Dependency Upgrades 23 | labels: 24 | - dependencies 25 | 26 | - title: Other Changes 27 | labels: 28 | - "*" 29 | -------------------------------------------------------------------------------- /.github/workflows/maven.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-maven 3 | 4 | # This workflow uses actions that are not certified by GitHub. 5 | # They are provided by a third-party and are governed by 6 | # separate terms of service, privacy policy, and support 7 | # documentation. 8 | 9 | name: Java CI with Maven 10 | 11 | on: 12 | push: 13 | branches: [ "main" ] 14 | pull_request: 15 | branches: [ "main" ] 16 | 17 | jobs: 18 | build: 19 | permissions: write-all 20 | runs-on: ubuntu-latest 21 | 22 | steps: 23 | - uses: actions/checkout@v4 24 | - name: Set up JDK 17 25 | uses: actions/setup-java@v4 26 | with: 27 | java-version: '17' 28 | distribution: 'temurin' 29 | cache: maven 30 | - name: Build with Maven 31 | run: ./mvnw -B package --file pom.xml 32 | 33 | # Optional: Uploads the full dependency graph to GitHub to improve the quality of Dependabot alerts this repository can receive 34 | - name: Update dependency graph 35 | uses: advanced-security/maven-dependency-submission-action@b275d12641ac2d2108b2cbb7598b154ad2f2cee8 36 | -------------------------------------------------------------------------------- /.github/workflows/release-docs.yml: -------------------------------------------------------------------------------- 1 | name: Release Docs 2 | on: 3 | push: 4 | paths: 5 | - 'docs/**' 6 | - 'mkdocs.yml' 7 | permissions: 8 | contents: write 9 | jobs: 10 | deploy: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: actions/setup-python@v5 15 | with: 16 | python-version: 3.x 17 | - run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV 18 | - uses: actions/cache@v4 19 | with: 20 | key: mkdocs-material-${{ env.cache_id }} 21 | path: .cache 22 | restore-keys: | 23 | mkdocs-material- 24 | - run: pip install mkdocs-material 25 | - run: pip install mkdocs-minify-plugin 26 | - run: mkdocs gh-deploy --force 27 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Publish package to the Maven Central Repository 2 | on: 3 | push: 4 | tags: 5 | - v* 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v4 14 | with: 15 | fetch-depth: 0 16 | 17 | - name: Set up JDK 17 18 | uses: actions/setup-java@v4 19 | with: 20 | java-version: '17' 21 | distribution: 'temurin' 22 | cache: maven 23 | 24 | - name: Write release version 25 | run: VERSION=${GITHUB_REF_NAME#v}; echo "VERSION=$VERSION" >> $GITHUB_ENV 26 | 27 | - name: Set release version 28 | run: ./mvnw --no-transfer-progress --batch-mode versions:set -DnewVersion=${VERSION} 29 | 30 | - name: Commit & Push changes 31 | uses: actions-js/push@master 32 | with: 33 | github_token: ${{ secrets.JRELEASER_GITHUB_TOKEN }} 34 | message: Perform release ${{ github.event.inputs.version }} 35 | 36 | - name: Publish package 37 | env: 38 | JRELEASER_NEXUS2_USERNAME: ${{ secrets.JRELEASER_NEXUS2_USERNAME }} 39 | JRELEASER_NEXUS2_PASSWORD: ${{ secrets.JRELEASER_NEXUS2_PASSWORD }} 40 | JRELEASER_GPG_PASSPHRASE: ${{ secrets.JRELEASER_GPG_PASSPHRASE }} 41 | JRELEASER_GPG_SECRET_KEY: ${{ secrets.JRELEASER_GPG_SECRET_KEY }} 42 | JRELEASER_GPG_PUBLIC_KEY: ${{ secrets.JRELEASER_GPG_PUBLIC_KEY }} 43 | JRELEASER_GITHUB_TOKEN: ${{ secrets.JRELEASER_GITHUB_TOKEN }} 44 | run: ./mvnw --no-transfer-progress --batch-mode -Prelease deploy jreleaser:deploy 45 | 46 | - name: Set next version 47 | run: ./mvnw --no-transfer-progress --batch-mode build-helper:parse-version versions:set -DnewVersion=\${parsedVersion.majorVersion}.\${parsedVersion.minorVersion}.\${parsedVersion.nextIncrementalVersion}-SNAPSHOT versions:commit 48 | 49 | - name: Commit & Push changes 50 | uses: actions-js/push@master 51 | with: 52 | github_token: ${{ secrets.JRELEASER_GITHUB_TOKEN }} 53 | message: Prepare for next release 54 | tags: true 55 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | .gradle 3 | build/ 4 | !gradle/wrapper/gradle-wrapper.jar 5 | !**/src/main/**/build/ 6 | !**/src/test/**/build/ 7 | target/ 8 | *.gz 9 | *.log 10 | development/data/ 11 | pom.xml.versionsBackup 12 | 13 | ### STS ### 14 | .apt_generated 15 | .classpath 16 | .factorypath 17 | .project 18 | .settings 19 | .springBeans 20 | .sts4-cache 21 | bin/ 22 | !**/src/main/**/bin/ 23 | !**/src/test/**/bin/ 24 | 25 | ### IntelliJ IDEA ### 26 | .idea/* 27 | !.idea/icon.png 28 | *.iws 29 | *.iml 30 | *.ipr 31 | out/ 32 | !**/src/main/**/out/ 33 | !**/src/test/**/out/ 34 | 35 | ### NetBeans ### 36 | /nbproject/private/ 37 | /nbbuild/ 38 | /dist/ 39 | /nbdist/ 40 | /.nb-gradle/ 41 | 42 | ### VS Code ### 43 | .vscode/ 44 | 45 | # Compiled output 46 | dist 47 | tmp 48 | out-tsc 49 | bazel-out 50 | *.tmp 51 | 52 | # Node 53 | node_modules 54 | npm-debug.log 55 | yarn-error.log 56 | 57 | # Miscellaneous 58 | .angular/ 59 | .sass-cache/ 60 | connect.lock 61 | coverage 62 | libpeerconnection.log 63 | testem.log 64 | typings 65 | 66 | # System files 67 | .DS_Store 68 | Thumbs.db 69 | -------------------------------------------------------------------------------- /.idea/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/enofex/taikai/38f7f5e8cc4f6927d16dd12aa1b50932c46cf971/.idea/icon.png -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | wrapperVersion=3.3.2 18 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip 19 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | info@enofex.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Taikai 2 | 3 | First off, thank you for taking the time to contribute! :+1: :tada: 4 | 5 | ### How to Contribute 6 | 7 | #### Ask questions 8 | 9 | If you believe there is an issue, search through existing issues for this project trying a 10 | few different ways to find discussions, past or current, that are related to the issue. 11 | Reading those discussions helps you to learn about the issue, and helps us to make a 12 | decision. 13 | 14 | #### Create an Issue 15 | 16 | Reporting an issue or making a feature request is a great way to contribute. Your feedback 17 | and the conversations that result from it provide a continuous flow of ideas. However, 18 | before creating a ticket, please take the time to [ask and research](#ask-questions) first. 19 | 20 | Once you're ready, create an issue on the module. 21 | 22 | #### Before submitting a Pull Request 23 | 24 | To contribute to this project, please fork the repository and submit a pull request with your changes. Please ensure that your changes adhere to the following guidelines: 25 | 26 | * Code should follow the conventions. 27 | * All code should be well-documented. 28 | * All new functionality should be covered by tests. 29 | * Changes should not break existing functionality. 30 | 31 | #### Submit a Pull Request 32 | 33 | 1. Always check out the `main` branch and submit pull requests against it. 34 | 35 | 2. Choose the granularity of your commits consciously and squash commits that represent 36 | multiple edits or corrections of the same logical change. See 37 | [Rewriting History section of Pro Git](https://git-scm.com/book/en/Git-Tools-Rewriting-History) 38 | for an overview of streamlining the commit history. 39 | 40 | 3. Format commit messages using 55 characters for the subject line, 72 characters per line 41 | for the description, followed by the issue fixed, e.g. `Closes gh-12279`. See the 42 | [Commit Guidelines section of Pro Git](https://git-scm.com/book/en/Distributed-Git-Contributing-to-a-Project#Commit-Guidelines) 43 | for best practices around commit messages, and use `git log` to see some examples. 44 | 45 | 4. If there is a prior issue, reference the GitHub issue number in the description of the 46 | pull request. 47 | 48 | 49 | #### Participate in Reviews 50 | 51 | Helping to review pull requests is another great way to contribute. Your feedback 52 | can help to shape the implementation of new features. When reviewing pull requests, 53 | however, please refrain from approving or rejecting a PR unless you are a core 54 | committer for this project. 55 | 56 | ### Code Conventions 57 | 58 | #### Java 59 | Please follow the Google Java Style Guide when writing Java code for this project. You can also import the Intellij IDEA code style configuration file for Java from [here](https://github.com/google/styleguide/blob/gh-pages/intellij-java-google-style.xml). 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Enofex / Martin Hock 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 4 |

5 | 6 |

7 | 8 | 9 | 10 |

11 | 12 | # Taikai 13 | 14 | Taikai extends the capabilities of the popular ArchUnit library by offering a comprehensive suite of predefined rules tailored for various technologies. It simplifies the enforcement of architectural constraints and best practices in your codebase, ensuring consistency and quality across your projects. 15 | 16 | ## Maven Usage 17 | 18 | Add Taikai as a dependency in your `pom.xml`: 19 | 20 | ```xml 21 | 22 | com.enofex 23 | taikai 24 | ${taikai.version} 25 | test 26 | 27 | ``` 28 | 29 | Replace `${taikai.version}` with the appropriate version defined in your project. Ensure that the required dependencies like ArchUnit are already declared. 30 | 31 | ## Gradle Usage 32 | 33 | Add Taikai as a dependency in your `build.gradle` file: 34 | 35 | ```groovy 36 | testImplementation "com.enofex:taikai:${taikaiVersion}" 37 | ``` 38 | 39 | Replace `${taikaiVersion}` with the appropriate version defined in your project. Ensure that the required dependencies like ArchUnit are already declared. 40 | 41 | ## JUnit 5 Example Test 42 | 43 | Here's an example demonstrating the usage of some Taikai rules with JUnit 5. Customize rules as needed using `TaikaiRule.of()`. 44 | 45 | ```java 46 | @Test 47 | void shouldFulfillConstraints() { 48 | Taikai.builder() 49 | .namespace("com.enofex.taikai") 50 | .java(java -> java 51 | .noUsageOfDeprecatedAPIs() 52 | .methodsShouldNotDeclareGenericExceptions() 53 | .utilityClassesShouldBeFinalAndHavePrivateConstructor() 54 | .imports(imports -> imports 55 | .shouldHaveNoCycles() 56 | .shouldNotImport("..shaded..") 57 | .shouldNotImport("org.junit..")) 58 | .naming(naming -> naming 59 | .classesShouldNotMatch(".*Impl") 60 | .methodsShouldNotMatch("^(foo$|bar$).*") 61 | .fieldsShouldNotMatch(".*(List|Set|Map)$") 62 | .fieldsShouldMatch("com.enofex.taikai.Matcher", "matcher") 63 | .constantsShouldFollowConventions() 64 | .interfacesShouldNotHavePrefixI())) 65 | .logging(logging -> logging 66 | .loggersShouldFollowConventions(Logger.class, "logger", List.of(PRIVATE, FINAL))) 67 | .test(test -> test 68 | .junit5(junit5 -> junit5 69 | .classesShouldNotBeAnnotatedWithDisabled() 70 | .methodsShouldNotBeAnnotatedWithDisabled())) 71 | .spring(spring -> spring 72 | .noAutowiredFields() 73 | .boot(boot -> boot 74 | .springBootApplicationShouldBeIn("com.enofex.taikai")) 75 | .configurations(configuration -> configuration 76 | .namesShouldEndWithConfiguration()) 77 | .controllers(controllers -> controllers 78 | .shouldBeAnnotatedWithRestController() 79 | .namesShouldEndWithController() 80 | .shouldNotDependOnOtherControllers() 81 | .shouldBePackagePrivate()) 82 | .services(services -> services 83 | .shouldBeAnnotatedWithService() 84 | .shouldNotDependOnControllers() 85 | .namesShouldEndWithService()) 86 | .repositories(repositories -> repositories 87 | .shouldBeAnnotatedWithRepository() 88 | .shouldNotDependOnServices() 89 | .namesShouldEndWithRepository())) 90 | .addRule(TaikaiRule.of(...)) // Add custom ArchUnit rule here 91 | .build() 92 | .check(); 93 | } 94 | ``` 95 | 96 | ## User Guide 97 | 98 | Explore the complete [documentation](https://enofex.github.io/taikai) for comprehensive information on all available rules. 99 | 100 | ## Contributing 101 | 102 | Interested in contributing? Check out our [Contribution Guidelines](https://github.com/enofex/taikai/blob/main/CONTRIBUTING.md) for details on how to get involved. Note, that we expect everyone to follow the [Code of Conduct](https://github.com/enofex/taikai/blob/main/CODE_OF_CONDUCT.md). 103 | 104 | ### What you will need 105 | 106 | * Git 107 | * Java 17 or higher 108 | 109 | ### Get the Source Code 110 | 111 | Clone the repository 112 | 113 | ```shell 114 | git clone git@github.com:enofex/taikai.git 115 | cd taikai 116 | ``` 117 | 118 | ### Build the code 119 | 120 | To compile, test, and build 121 | 122 | ```shell 123 | ./mvnw clean package -B 124 | ``` 125 | 126 | ## Backers 127 | 128 | The Open Source Community 129 | 130 |
131 | 132 | 133 | 134 |
135 | 136 | ## Website 137 | 138 | Visit the [Taikai](https://enofex.github.io/taikai/) Website for general information and documentation. 139 | 140 | ## Links 141 | 142 | https://dzone.com/articles/enforcing-architecture-with-archunit-in-java 143 | -------------------------------------------------------------------------------- /docs/CNAME: -------------------------------------------------------------------------------- 1 | enofex.github.io/taikai 2 | -------------------------------------------------------------------------------- /docs/assets/images/taikai-header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/enofex/taikai/38f7f5e8cc4f6927d16dd12aa1b50932c46cf971/docs/assets/images/taikai-header.png -------------------------------------------------------------------------------- /docs/assets/images/taikai-logo-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/enofex/taikai/38f7f5e8cc4f6927d16dd12aa1b50932c46cf971/docs/assets/images/taikai-logo-dark.png -------------------------------------------------------------------------------- /docs/assets/images/taikai-logo-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/enofex/taikai/38f7f5e8cc4f6927d16dd12aa1b50932c46cf971/docs/assets/images/taikai-logo-light.png -------------------------------------------------------------------------------- /docs/contributing.md: -------------------------------------------------------------------------------- 1 | ## Contributing 2 | 3 | Thank you for your interest in contributing to *Taikai*! We welcome contributions from the community 4 | to help make the project even better. Here are some ways you can get involved: 5 | 6 | * **Star the Project**: 7 | 8 | - Show your support by starring the project 9 | on [GitHub](https://github.com/enofex/taikai). 10 | - This helps increase visibility and encourages others to discover and contribute to the 11 | project. 12 | 13 | * **Review the Contribution Guide**: 14 | 15 | - Familiarize yourself with the guidelines and procedures outlined in our contribution guide. 16 | - The contribution guide provides detailed information on how to get started and the different 17 | ways 18 | you can contribute. 19 | 20 | * **Follow Contribution Guidelines**: 21 | 22 | - Ensure that you follow our 23 | contribution [guidelines](https://github.com/enofex/taikai/blob/main/CONTRIBUTING.md) 24 | when submitting your contributions. 25 | - These guidelines cover aspects such as code formatting, documentation standards, and other 26 | important considerations. 27 | 28 | * **Pull Requests**: 29 | 30 | - If you have improvements or fixes to propose, submit a Pull Request (PR) to the relevant 31 | module or repository. 32 | - Clearly describe the purpose and changes made in your PR, providing enough context for the 33 | reviewers to understand your contribution. 34 | - Be open to feedback and engage in discussions to refine your contribution. 35 | 36 | * **Bug Reports and Feature Requests**: 37 | 38 | - Help us improve *Taikai* by reporting any bugs or issues you encounter. 39 | - If you have ideas for new features or enhancements, submit a feature request. 40 | - Use the issue tracker in the respective repository to provide detailed information about the 41 | problem or request. 42 | 43 | * **Spread the Word**: 44 | 45 | - Share your positive experience with *Taikai* and encourage others to contribute. 46 | - Tweet about your contributions, write blog posts, or mention *Taikai* in relevant communities 47 | to increase awareness. 48 | 49 | * **Help with Documentation**: 50 | 51 | - Contribute to improving the documentation by identifying areas that need clarification or 52 | adding examples and tutorials. 53 | - Submit documentation PRs to enhance the usability and understanding of *Taikai*. 54 | 55 | Remember, contributions of all sizes are valuable and appreciated. We look forward to your 56 | involvement in the *Taikai* community. Thank you for considering contributing to the project! 57 | 58 | # Sponsor 59 | 60 | [Sponsor Taikai on GitHub :heart:](https://github.com/sponsors/mnhock){ .md-button } 61 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # 2 | 3 | Taikai logo 4 | 5 | *Taikai* extends the capabilities of the popular ArchUnit library by offering a comprehensive suite of predefined rules tailored for various technologies. It simplifies the enforcement of architectural constraints and best practices in your codebase, ensuring consistency and quality across your projects. 6 | 7 | [Get Started](./documentation){ .md-button .md-button--primary } 8 | [View on GitHub :simple-github:](https://github.com/enofex/taikai){ .md-button } 9 | 10 | ## Example Usage 11 | 12 | ```java 13 | class ArchitectureTest { 14 | 15 | @Test 16 | void shouldFulfilConstrains() { 17 | Taikai.builder() 18 | .namespace("com.company.project") 19 | .java(java -> java 20 | .noUsageOfDeprecatedAPIs() 21 | .classesShouldImplementHashCodeAndEquals() 22 | .methodsShouldNotDeclareGenericExceptions() 23 | .utilityClassesShouldBeFinalAndHavePrivateConstructor()) 24 | .test(test -> test 25 | .junit5(junit5 -> junit5 26 | .classesShouldNotBeAnnotatedWithDisabled() 27 | .methodsShouldNotBeAnnotatedWithDisabled())) 28 | .logging(logging -> logging 29 | .loggersShouldFollowConventions(Logger.class, "logger", List.of(PRIVATE, FINAL))) 30 | .spring(spring -> spring 31 | .noAutowiredFields() 32 | .boot(boot -> boot 33 | .shouldBeAnnotatedWithSpringBootApplication()) 34 | .configurations(configuration -> configuration 35 | .namesShouldEndWithConfiguration() 36 | .namesShouldMatch("regex")) 37 | .controllers(controllers -> controllers 38 | .shouldBeAnnotatedWithRestController() 39 | .namesShouldEndWithController() 40 | .namesShouldMatch("regex") 41 | .shouldNotDependOnOtherControllers() 42 | .shouldBePackagePrivate())) 43 | .services(services -> services 44 | .namesShouldEndWithService() 45 | .shouldBeAnnotatedWithService()) 46 | .repositories(repositories -> repositories 47 | .namesShouldEndWithRepository() 48 | .shouldBeAnnotatedWithRepository()) 49 | .build() 50 | .check(); 51 | } 52 | } 53 | ``` 54 | 55 | ## Sponsors 56 | 57 | If *Taikai* has helped you save time and money, I invite you to support my work by becoming a 58 | sponsor. 59 | By becoming a [sponsor](https://github.com/sponsors/mnhock), you enable me to continue to improve 60 | Taikai's capabilities by fixing bugs immediately and continually adding new useful features. Your 61 | sponsorship plays an important role in making *Taikai* even better. 62 | 63 | ## Backers 64 | 65 | The Open Source Community and [Enofex](https://enofex.com) 66 | 67 | ## License 68 | 69 | See [LICENSE](https://github.com/enofex/taikai/blob/main/LICENSE). 70 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | # Project information 2 | site_name: Taikai 3 | site_url: https://enofex.github.io/taikai 4 | site_author: Martin Hock 5 | site_description: >- 6 | Taikai is an extension of the popular ArchUnit library, offering a comprehensive suite of predefined rules tailored for various technologies. 7 | 8 | # Repository 9 | repo_name: taikai 10 | repo_url: https://github.com/enofex/taikai 11 | 12 | # Copyright 13 | copyright: Copyright © 2025 Martin Hock / Enofex 14 | 15 | # Configuration 16 | theme: 17 | name: material 18 | features: 19 | - announce.dismiss 20 | - content.action.edit 21 | - content.action.view 22 | - content.code.annotate 23 | - content.code.copy 24 | - content.tooltips 25 | - navigation.footer 26 | - navigation.indexes 27 | - search.share 28 | - search.suggest 29 | - toc.follow 30 | palette: 31 | - scheme: default 32 | primary: indigo 33 | accent: indigo 34 | - scheme: slate 35 | primary: indigo 36 | accent: indigo 37 | font: 38 | text: Roboto 39 | code: Roboto Mono 40 | favicon: assets/favicon.ico 41 | logo: assets/images/taikai-logo-light.png 42 | 43 | # Plugins 44 | plugins: 45 | - search: 46 | separator: '[\s\-,:!=\[\]()"`/]+|\.(?!\d)|&[lg]t;|(?!\b)(?=[A-Z][a-z])' 47 | - minify: 48 | minify_html: true 49 | 50 | # Customization 51 | extra: 52 | social: 53 | - icon: fontawesome/brands/github 54 | link: https://github.com/enofex/taikai 55 | - icon: fontawesome/brands/docker 56 | link: https://hub.docker.com/u/enofex 57 | 58 | # Extensions 59 | markdown_extensions: 60 | - pymdownx.snippets 61 | - abbr 62 | - admonition 63 | - attr_list 64 | - def_list 65 | - footnotes 66 | - md_in_html 67 | - toc: 68 | permalink: true 69 | - pymdownx.arithmatex: 70 | generic: true 71 | - pymdownx.betterem: 72 | smart_enable: all 73 | - pymdownx.caret 74 | - pymdownx.details 75 | - pymdownx.emoji: 76 | emoji_generator: !!python/name:materialx.emoji.to_svg 77 | emoji_index: !!python/name:materialx.emoji.twemoji 78 | - pymdownx.highlight: 79 | anchor_linenums: true 80 | line_spans: __span 81 | pygments_lang_class: true 82 | - pymdownx.inlinehilite 83 | - pymdownx.keys 84 | - pymdownx.magiclink: 85 | repo_url_shorthand: true 86 | user: squidfunk 87 | repo: mkdocs-material 88 | - pymdownx.mark 89 | - pymdownx.smartsymbols 90 | - pymdownx.superfences: 91 | custom_fences: 92 | - name: mermaid 93 | class: mermaid 94 | format: !!python/name:pymdownx.superfences.fence_code_format 95 | - pymdownx.tabbed: 96 | alternate_style: true 97 | - pymdownx.tasklist: 98 | custom_checkbox: true 99 | - pymdownx.tilde 100 | 101 | # Page tree 102 | nav: 103 | - Home: index.md 104 | - Documentation: documentation.md 105 | - Contributing: contributing.md -------------------------------------------------------------------------------- /mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # https://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Apache Maven Wrapper startup batch script, version 3.3.1 23 | # 24 | # Optional ENV vars 25 | # ----------------- 26 | # JAVA_HOME - location of a JDK home dir, required when download maven via java source 27 | # MVNW_REPOURL - repo url base for downloading maven distribution 28 | # MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven 29 | # MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output 30 | # ---------------------------------------------------------------------------- 31 | 32 | set -euf 33 | [ "${MVNW_VERBOSE-}" != debug ] || set -x 34 | 35 | # OS specific support. 36 | native_path() { printf %s\\n "$1"; } 37 | case "$(uname)" in 38 | CYGWIN* | MINGW*) 39 | [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" 40 | native_path() { cygpath --path --windows "$1"; } 41 | ;; 42 | esac 43 | 44 | # set JAVACMD and JAVACCMD 45 | set_java_home() { 46 | # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched 47 | if [ -n "${JAVA_HOME-}" ]; then 48 | if [ -x "$JAVA_HOME/jre/sh/java" ]; then 49 | # IBM's JDK on AIX uses strange locations for the executables 50 | JAVACMD="$JAVA_HOME/jre/sh/java" 51 | JAVACCMD="$JAVA_HOME/jre/sh/javac" 52 | else 53 | JAVACMD="$JAVA_HOME/bin/java" 54 | JAVACCMD="$JAVA_HOME/bin/javac" 55 | 56 | if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then 57 | echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 58 | echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 59 | return 1 60 | fi 61 | fi 62 | else 63 | JAVACMD="$( 64 | 'set' +e 65 | 'unset' -f command 2>/dev/null 66 | 'command' -v java 67 | )" || : 68 | JAVACCMD="$( 69 | 'set' +e 70 | 'unset' -f command 2>/dev/null 71 | 'command' -v javac 72 | )" || : 73 | 74 | if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then 75 | echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 76 | return 1 77 | fi 78 | fi 79 | } 80 | 81 | # hash string like Java String::hashCode 82 | hash_string() { 83 | str="${1:-}" h=0 84 | while [ -n "$str" ]; do 85 | char="${str%"${str#?}"}" 86 | h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) 87 | str="${str#?}" 88 | done 89 | printf %x\\n $h 90 | } 91 | 92 | verbose() { :; } 93 | [ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } 94 | 95 | die() { 96 | printf %s\\n "$1" >&2 97 | exit 1 98 | } 99 | 100 | # parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties 101 | while IFS="=" read -r key value; do 102 | case "${key-}" in 103 | distributionUrl) distributionUrl="${value-}" ;; 104 | distributionSha256Sum) distributionSha256Sum="${value-}" ;; 105 | esac 106 | done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties" 107 | [ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties" 108 | 109 | case "${distributionUrl##*/}" in 110 | maven-mvnd-*bin.*) 111 | MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ 112 | case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in 113 | *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; 114 | :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; 115 | :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; 116 | :Linux*x86_64*) distributionPlatform=linux-amd64 ;; 117 | *) 118 | echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 119 | distributionPlatform=linux-amd64 120 | ;; 121 | esac 122 | distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" 123 | ;; 124 | maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; 125 | *) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; 126 | esac 127 | 128 | # rules MVNW_REPOURL and calculate MAVEN_HOME 129 | # maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ 130 | [ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" 131 | distributionUrlName="${distributionUrl##*/}" 132 | distributionUrlNameMain="${distributionUrlName%.*}" 133 | distributionUrlNameMain="${distributionUrlNameMain%-bin}" 134 | MAVEN_HOME="$HOME/.m2/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" 135 | 136 | exec_maven() { 137 | unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : 138 | exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" 139 | } 140 | 141 | if [ -d "$MAVEN_HOME" ]; then 142 | verbose "found existing MAVEN_HOME at $MAVEN_HOME" 143 | exec_maven "$@" 144 | fi 145 | 146 | case "${distributionUrl-}" in 147 | *?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; 148 | *) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; 149 | esac 150 | 151 | # prepare tmp dir 152 | if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then 153 | clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } 154 | trap clean HUP INT TERM EXIT 155 | else 156 | die "cannot create temp dir" 157 | fi 158 | 159 | mkdir -p -- "${MAVEN_HOME%/*}" 160 | 161 | # Download and Install Apache Maven 162 | verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." 163 | verbose "Downloading from: $distributionUrl" 164 | verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" 165 | 166 | # select .zip or .tar.gz 167 | if ! command -v unzip >/dev/null; then 168 | distributionUrl="${distributionUrl%.zip}.tar.gz" 169 | distributionUrlName="${distributionUrl##*/}" 170 | fi 171 | 172 | # verbose opt 173 | __MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' 174 | [ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v 175 | 176 | # normalize http auth 177 | case "${MVNW_PASSWORD:+has-password}" in 178 | '') MVNW_USERNAME='' MVNW_PASSWORD='' ;; 179 | has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; 180 | esac 181 | 182 | if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then 183 | verbose "Found wget ... using wget" 184 | wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" 185 | elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then 186 | verbose "Found curl ... using curl" 187 | curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" 188 | elif set_java_home; then 189 | verbose "Falling back to use Java to download" 190 | javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" 191 | targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" 192 | cat >"$javaSource" <<-END 193 | public class Downloader extends java.net.Authenticator 194 | { 195 | protected java.net.PasswordAuthentication getPasswordAuthentication() 196 | { 197 | return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); 198 | } 199 | public static void main( String[] args ) throws Exception 200 | { 201 | setDefault( new Downloader() ); 202 | java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); 203 | } 204 | } 205 | END 206 | # For Cygwin/MinGW, switch paths to Windows format before running javac and java 207 | verbose " - Compiling Downloader.java ..." 208 | "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" 209 | verbose " - Running Downloader.java ..." 210 | "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" 211 | fi 212 | 213 | # If specified, validate the SHA-256 sum of the Maven distribution zip file 214 | if [ -n "${distributionSha256Sum-}" ]; then 215 | distributionSha256Result=false 216 | if [ "$MVN_CMD" = mvnd.sh ]; then 217 | echo "Checksum validation is not supported for maven-mvnd." >&2 218 | echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 219 | exit 1 220 | elif command -v sha256sum >/dev/null; then 221 | if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then 222 | distributionSha256Result=true 223 | fi 224 | elif command -v shasum >/dev/null; then 225 | if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then 226 | distributionSha256Result=true 227 | fi 228 | else 229 | echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 230 | echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 231 | exit 1 232 | fi 233 | if [ $distributionSha256Result = false ]; then 234 | echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 235 | echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 236 | exit 1 237 | fi 238 | fi 239 | 240 | # unzip and move 241 | if command -v unzip >/dev/null; then 242 | unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" 243 | else 244 | tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" 245 | fi 246 | printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url" 247 | mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" 248 | 249 | clean || : 250 | exec_maven "$@" 251 | -------------------------------------------------------------------------------- /mvnw.cmd: -------------------------------------------------------------------------------- 1 | <# : batch portion 2 | @REM ---------------------------------------------------------------------------- 3 | @REM Licensed to the Apache Software Foundation (ASF) under one 4 | @REM or more contributor license agreements. See the NOTICE file 5 | @REM distributed with this work for additional information 6 | @REM regarding copyright ownership. The ASF licenses this file 7 | @REM to you under the Apache License, Version 2.0 (the 8 | @REM "License"); you may not use this file except in compliance 9 | @REM with the License. You may obtain a copy of the License at 10 | @REM 11 | @REM https://www.apache.org/licenses/LICENSE-2.0 12 | @REM 13 | @REM Unless required by applicable law or agreed to in writing, 14 | @REM software distributed under the License is distributed on an 15 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | @REM KIND, either express or implied. See the License for the 17 | @REM specific language governing permissions and limitations 18 | @REM under the License. 19 | @REM ---------------------------------------------------------------------------- 20 | 21 | @REM ---------------------------------------------------------------------------- 22 | @REM Apache Maven Wrapper startup batch script, version 3.3.1 23 | @REM 24 | @REM Optional ENV vars 25 | @REM MVNW_REPOURL - repo url base for downloading maven distribution 26 | @REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven 27 | @REM MVNW_VERBOSE - true: enable verbose log; others: silence the output 28 | @REM ---------------------------------------------------------------------------- 29 | 30 | @IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) 31 | @SET __MVNW_CMD__= 32 | @SET __MVNW_ERROR__= 33 | @SET __MVNW_PSMODULEP_SAVE=%PSModulePath% 34 | @SET PSModulePath= 35 | @FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( 36 | IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) 37 | ) 38 | @SET PSModulePath=%__MVNW_PSMODULEP_SAVE% 39 | @SET __MVNW_PSMODULEP_SAVE= 40 | @SET __MVNW_ARG0_NAME__= 41 | @SET MVNW_USERNAME= 42 | @SET MVNW_PASSWORD= 43 | @IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) 44 | @echo Cannot start maven from wrapper >&2 && exit /b 1 45 | @GOTO :EOF 46 | : end batch / begin powershell #> 47 | 48 | $ErrorActionPreference = "Stop" 49 | if ($env:MVNW_VERBOSE -eq "true") { 50 | $VerbosePreference = "Continue" 51 | } 52 | 53 | # calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties 54 | $distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl 55 | if (!$distributionUrl) { 56 | Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" 57 | } 58 | 59 | switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { 60 | "maven-mvnd-*" { 61 | $USE_MVND = $true 62 | $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" 63 | $MVN_CMD = "mvnd.cmd" 64 | break 65 | } 66 | default { 67 | $USE_MVND = $false 68 | $MVN_CMD = $script -replace '^mvnw','mvn' 69 | break 70 | } 71 | } 72 | 73 | # apply MVNW_REPOURL and calculate MAVEN_HOME 74 | # maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ 75 | if ($env:MVNW_REPOURL) { 76 | $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } 77 | $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" 78 | } 79 | $distributionUrlName = $distributionUrl -replace '^.*/','' 80 | $distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' 81 | $MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" 82 | $MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' 83 | $MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" 84 | 85 | if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { 86 | Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" 87 | Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" 88 | exit $? 89 | } 90 | 91 | if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { 92 | Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" 93 | } 94 | 95 | # prepare tmp dir 96 | $TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile 97 | $TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" 98 | $TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null 99 | trap { 100 | if ($TMP_DOWNLOAD_DIR.Exists) { 101 | try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } 102 | catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } 103 | } 104 | } 105 | 106 | New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null 107 | 108 | # Download and Install Apache Maven 109 | Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." 110 | Write-Verbose "Downloading from: $distributionUrl" 111 | Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" 112 | 113 | $webclient = New-Object System.Net.WebClient 114 | if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { 115 | $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) 116 | } 117 | [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 118 | $webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null 119 | 120 | # If specified, validate the SHA-256 sum of the Maven distribution zip file 121 | $distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum 122 | if ($distributionSha256Sum) { 123 | if ($USE_MVND) { 124 | Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." 125 | } 126 | Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash 127 | if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { 128 | Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." 129 | } 130 | } 131 | 132 | # unzip and move 133 | Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null 134 | Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null 135 | try { 136 | Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null 137 | } catch { 138 | if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { 139 | Write-Error "fail to move MAVEN_HOME" 140 | } 141 | } finally { 142 | try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } 143 | catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } 144 | } 145 | 146 | Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" 147 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 3.9.3 7 | 8 | 9 | 4.0.0 10 | jar 11 | 12 | com.enofex 13 | taikai 14 | 1.13.1-SNAPSHOT 15 | Taikai 16 | 2024 17 | 18 | https://github.com/enofex/taikai 19 | 20 | Taikai is a wrapper around the awesome ArchUnit and provides a set of common rules for different technologies. 21 | 22 | 23 | 24 | 17 25 | 26 | ${java.version} 27 | ${java.version} 28 | UTF-8 29 | UTF-8 30 | 31 | 5.13.1 32 | 5.18.0 33 | 1.4.1 34 | 35 | 3.14.0 36 | 3.4.2 37 | 3.5.3 38 | 3.3.1 39 | 3.11.2 40 | 3.3.1 41 | 3.5.0 42 | 1.18.0 43 | 44 | 45 | 46 | 47 | org.junit.jupiter 48 | junit-jupiter-engine 49 | ${junit-jupiter.version} 50 | test 51 | 52 | 53 | org.junit.jupiter 54 | junit-jupiter-params 55 | ${junit-jupiter.version} 56 | test 57 | 58 | 59 | org.junit.jupiter 60 | junit-jupiter-api 61 | ${junit-jupiter.version} 62 | test 63 | 64 | 65 | org.mockito 66 | mockito-core 67 | ${mockito.version} 68 | test 69 | 70 | 71 | org.mockito 72 | mockito-junit-jupiter 73 | ${mockito.version} 74 | test 75 | 76 | 77 | 78 | com.tngtech.archunit 79 | archunit 80 | ${archunit.version} 81 | compile 82 | 83 | 84 | com.tngtech.archunit 85 | archunit-junit5-api 86 | ${archunit.version} 87 | compile 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | org.apache.maven.plugins 96 | maven-resources-plugin 97 | ${maven-resources-plugin.version} 98 | 99 | 100 | org.apache.maven.plugins 101 | maven-compiler-plugin 102 | ${maven-compiler-plugin.version} 103 | 104 | true 105 | 106 | 107 | 108 | org.apache.maven.plugins 109 | maven-failsafe-plugin 110 | ${maven-failsafe-plugin.version} 111 | 112 | 113 | 114 | integration-test 115 | verify 116 | 117 | 118 | 119 | 120 | ${project.build.outputDirectory} 121 | 122 | 123 | 124 | org.apache.maven.plugins 125 | maven-jar-plugin 126 | ${maven-jar-plugin.version} 127 | 128 | 129 | 130 | true 131 | 132 | 133 | 134 | 135 | 136 | org.apache.maven.plugins 137 | maven-enforcer-plugin 138 | ${maven-enforcer-plugin.version} 139 | 140 | 141 | enforce-java 142 | 143 | enforce 144 | 145 | 146 | 147 | 148 | ${java.version} 149 | 150 | 151 | 152 | 153 | 154 | enforce-maven 155 | 156 | enforce 157 | 158 | 159 | 160 | 161 | 3.9.9 162 | 163 | 164 | 165 | 166 | 167 | enforce-no-snapshots 168 | 169 | enforce 170 | 171 | 172 | 173 | 174 | No Snapshots Allowed! 175 | 176 | com.enofex 177 | 178 | 179 | 180 | true 181 | 182 | 183 | 184 | 185 | 186 | org.jreleaser 187 | jreleaser-maven-plugin 188 | ${jreleaser-maven-plugin.version} 189 | 190 | 191 | 192 | ALWAYS 193 | true 194 | 195 | 196 | 197 | 198 | 199 | ALWAYS 200 | https://s01.oss.sonatype.org/service/local 201 | https://s01.oss.sonatype.org/content/repositories/snapshots/ 202 | 203 | true 204 | true 205 | target/staging-deploy 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | org.apache.maven.plugins 218 | maven-enforcer-plugin 219 | 220 | 221 | 222 | 223 | 224 | 225 | release 226 | 227 | 228 | local::file:./target/staging-deploy 229 | 230 | 231 | 232 | 233 | 234 | org.apache.maven.plugins 235 | maven-javadoc-plugin 236 | ${maven-javadoc-plugin.version} 237 | 238 | 239 | attach-javadoc 240 | 241 | jar 242 | 243 | 244 | 245 | 246 | 247 | org.apache.maven.plugins 248 | maven-source-plugin 249 | ${maven-source-plugin.version} 250 | 251 | 252 | attach-source 253 | 254 | jar 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | MIT License 267 | http://www.opensource.org/licenses/mit-license.php 268 | repo 269 | 270 | 271 | 272 | https://github.com/enofex/taikai 273 | scm:git:git://github.com/enofex/taikai.git 274 | 275 | scm:git:ssh://git@github.com/enofex/taikai.git 276 | 277 | HEAD 278 | 279 | 280 | 281 | mnhock 282 | Martin Hock 283 | 284 | 285 | 286 | -------------------------------------------------------------------------------- /src/main/java/com/enofex/taikai/Namespace.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai; 2 | 3 | import static java.util.Objects.requireNonNull; 4 | 5 | import com.tngtech.archunit.core.domain.JavaClasses; 6 | import com.tngtech.archunit.core.importer.ClassFileImporter; 7 | import com.tngtech.archunit.core.importer.ImportOption; 8 | import java.util.Map; 9 | import java.util.concurrent.ConcurrentHashMap; 10 | 11 | public final class Namespace { 12 | 13 | private static final Map JAVA_CLASSES = new ConcurrentHashMap<>(); 14 | 15 | public enum IMPORT { 16 | WITHOUT_TESTS, 17 | WITH_TESTS, 18 | ONLY_TESTS 19 | } 20 | 21 | private Namespace() { 22 | } 23 | 24 | public static JavaClasses from(String namespace, IMPORT importOption) { 25 | requireNonNull(namespace); 26 | requireNonNull(importOption); 27 | 28 | return switch (importOption) { 29 | case WITH_TESTS -> withTests(namespace); 30 | case ONLY_TESTS -> onlyTests(namespace); 31 | default -> withoutTests(namespace); 32 | }; 33 | } 34 | 35 | public static JavaClasses withoutTests(String namespace) { 36 | requireNonNull(namespace); 37 | 38 | return JAVA_CLASSES.computeIfAbsent( 39 | new Key(namespace, IMPORT.WITHOUT_TESTS), 40 | key -> new ClassFileImporter() 41 | .withImportOption(new ImportOption.DoNotIncludeTests()) 42 | .withImportOption(new ImportOption.DoNotIncludeJars()) 43 | .importPackages(namespace) 44 | ); 45 | } 46 | 47 | 48 | public static JavaClasses withTests(String namespace) { 49 | requireNonNull(namespace); 50 | 51 | return JAVA_CLASSES.computeIfAbsent( 52 | new Key(namespace, IMPORT.WITH_TESTS), 53 | key -> new ClassFileImporter() 54 | .withImportOption(new ImportOption.DoNotIncludeJars()) 55 | .importPackages(namespace)); 56 | } 57 | 58 | public static JavaClasses onlyTests(String namespace) { 59 | requireNonNull(namespace); 60 | 61 | return JAVA_CLASSES.computeIfAbsent( 62 | new Key(namespace, IMPORT.ONLY_TESTS), 63 | key -> new ClassFileImporter() 64 | .withImportOption(new ImportOption.OnlyIncludeTests()) 65 | .withImportOption(new ImportOption.DoNotIncludeJars()) 66 | .importPackages(namespace)); 67 | } 68 | 69 | private record Key(String namespace, IMPORT importOption) { 70 | 71 | } 72 | } -------------------------------------------------------------------------------- /src/main/java/com/enofex/taikai/Taikai.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai; 2 | 3 | import static java.util.Collections.emptyList; 4 | import static java.util.Objects.requireNonNull; 5 | import static java.util.Objects.requireNonNullElse; 6 | 7 | import com.enofex.taikai.configures.Configurer; 8 | import com.enofex.taikai.configures.ConfigurerContext; 9 | import com.enofex.taikai.configures.Configurers; 10 | import com.enofex.taikai.configures.Customizer; 11 | import com.enofex.taikai.java.JavaConfigurer; 12 | import com.enofex.taikai.logging.LoggingConfigurer; 13 | import com.enofex.taikai.spring.SpringConfigurer; 14 | import com.enofex.taikai.test.TestConfigurer; 15 | import com.tngtech.archunit.ArchConfiguration; 16 | import com.tngtech.archunit.core.domain.JavaClasses; 17 | import com.tngtech.archunit.lang.EvaluationResult; 18 | import com.tngtech.archunit.lang.FailureReport; 19 | import com.tngtech.archunit.lang.Priority; 20 | import java.util.ArrayList; 21 | import java.util.Arrays; 22 | import java.util.Collection; 23 | import java.util.function.Function; 24 | import java.util.stream.Stream; 25 | 26 | public final class Taikai { 27 | 28 | private final boolean failOnEmpty; 29 | private final String namespace; 30 | private final JavaClasses classes; 31 | private final Collection excludedClasses; 32 | private final Collection rules; 33 | 34 | private Taikai(Builder builder) { 35 | this.failOnEmpty = builder.failOnEmpty; 36 | this.namespace = builder.namespace; 37 | this.classes = builder.classes; 38 | this.excludedClasses = requireNonNullElse(builder.excludedClasses, emptyList()); 39 | this.rules = Stream.concat( 40 | builder.configurers.all().stream().flatMap(configurer -> configurer.rules().stream()), 41 | builder.rules.stream()) 42 | .toList(); 43 | 44 | if (this.namespace != null && this.classes != null) { 45 | throw new IllegalArgumentException("Setting namespace and classes are not supported"); 46 | } 47 | 48 | ArchConfiguration.get() 49 | .setProperty("archRule.failOnEmptyShould", Boolean.toString(this.failOnEmpty)); 50 | } 51 | 52 | public boolean failOnEmpty() { 53 | return this.failOnEmpty; 54 | } 55 | 56 | public String namespace() { 57 | return this.namespace; 58 | } 59 | 60 | public JavaClasses classes() { 61 | return this.classes; 62 | } 63 | 64 | public Collection excludedClasses() { 65 | return this.excludedClasses; 66 | } 67 | 68 | public Collection rules() { 69 | return this.rules; 70 | } 71 | 72 | /** 73 | * Executes all rules and fails immediately on the first violation. 74 | * 75 | *

Each rule is checked sequentially, and if a rule fails, an exception 76 | * is thrown immediately, stopping further execution.

77 | */ 78 | public void check() { 79 | this.rules.forEach(rule -> rule.check(this.namespace, this.classes, this.excludedClasses)); 80 | } 81 | 82 | /** 83 | * Executes all rules and collects all violations before failing. 84 | * 85 | *

Unlike {@link #check()}, this method evaluates all rules and 86 | * aggregates all failures into a single report. If violations exist, an {@link AssertionError} is 87 | * thrown with a detailed failure summary.

88 | * 89 | * @throws AssertionError if any rule violations are found. 90 | */ 91 | public void checkAll() { 92 | EvaluationResult result = new EvaluationResult(() -> "All Taikai rules", Priority.MEDIUM); 93 | 94 | for (TaikaiRule rule : this.rules) { 95 | result.add(rule.archRule() 96 | .evaluate(rule.javaClasses(this.namespace, this.classes, this.excludedClasses))); 97 | } 98 | 99 | FailureReport report = result.getFailureReport(); 100 | 101 | if (!report.isEmpty()) { 102 | throw new AssertionError(report.toString()); 103 | } 104 | } 105 | 106 | public static Builder builder() { 107 | return new Builder(); 108 | } 109 | 110 | public Builder toBuilder() { 111 | return new Builder(this); 112 | } 113 | 114 | public static final class Builder { 115 | 116 | private final Configurers configurers; 117 | private final Collection rules; 118 | private final Collection excludedClasses; 119 | private boolean failOnEmpty; 120 | private String namespace; 121 | private JavaClasses classes; 122 | 123 | public Builder() { 124 | this.configurers = new Configurers(); 125 | this.rules = new ArrayList<>(); 126 | this.excludedClasses = new ArrayList<>(); 127 | } 128 | 129 | public Builder(Taikai taikai) { 130 | this.configurers = new Configurers(); 131 | this.rules = taikai.rules(); 132 | this.excludedClasses = taikai.excludedClasses(); 133 | this.failOnEmpty = taikai.failOnEmpty(); 134 | this.namespace = taikai.namespace(); 135 | this.classes = taikai.classes(); 136 | } 137 | 138 | public Builder addRule(TaikaiRule rule) { 139 | this.rules.add(rule); 140 | return this; 141 | } 142 | 143 | public Builder addRules(Collection rules) { 144 | this.rules.addAll(rules); 145 | return this; 146 | } 147 | 148 | public Builder failOnEmpty(boolean failOnEmpty) { 149 | this.failOnEmpty = failOnEmpty; 150 | return this; 151 | } 152 | 153 | public Builder namespace(String namespace) { 154 | this.namespace = namespace; 155 | return this; 156 | } 157 | 158 | public Builder classes(JavaClasses classes) { 159 | this.classes = classes; 160 | return this; 161 | } 162 | 163 | public Builder excludeClasses(Collection classNames) { 164 | this.excludedClasses.addAll(classNames); 165 | return this; 166 | } 167 | 168 | public Builder excludeClasses(String... classNames) { 169 | this.excludedClasses.addAll(Arrays.asList(classNames)); 170 | return this; 171 | } 172 | 173 | public Builder java(Customizer customizer) { 174 | return configure(customizer, JavaConfigurer.Disableable::new); 175 | } 176 | 177 | public Builder logging(Customizer customizer) { 178 | return configure(customizer, LoggingConfigurer.Disableable::new); 179 | } 180 | 181 | public Builder test(Customizer customizer) { 182 | return configure(customizer, TestConfigurer.Disableable::new); 183 | } 184 | 185 | public Builder spring(Customizer customizer) { 186 | return configure(customizer, SpringConfigurer.Disableable::new); 187 | } 188 | 189 | private Builder configure(Customizer customizer, 190 | Function supplier) { 191 | requireNonNull(customizer); 192 | requireNonNull(supplier); 193 | 194 | customizer.customize(this.configurers.getOrApply(supplier.apply( 195 | new ConfigurerContext(this.namespace, this.configurers))) 196 | ); 197 | return this; 198 | } 199 | 200 | public Taikai build() { 201 | return new Taikai(this); 202 | } 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /src/main/java/com/enofex/taikai/TaikaiException.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai; 2 | 3 | public class TaikaiException extends RuntimeException { 4 | 5 | public TaikaiException(String message) { 6 | super(message); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/com/enofex/taikai/TaikaiRule.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai; 2 | 3 | import static com.enofex.taikai.TaikaiRule.Configuration.defaultConfiguration; 4 | import static java.util.Collections.emptyList; 5 | import static java.util.Collections.emptySet; 6 | import static java.util.Objects.requireNonNull; 7 | import static java.util.Objects.requireNonNullElse; 8 | 9 | import com.tngtech.archunit.base.DescribedPredicate; 10 | import com.tngtech.archunit.core.domain.JavaClass; 11 | import com.tngtech.archunit.core.domain.JavaClasses; 12 | import com.tngtech.archunit.lang.ArchRule; 13 | import java.util.ArrayList; 14 | import java.util.Collection; 15 | import java.util.regex.Pattern; 16 | import java.util.stream.Collectors; 17 | import java.util.stream.Stream; 18 | 19 | public final class TaikaiRule { 20 | 21 | private final ArchRule archRule; 22 | private final Configuration configuration; 23 | 24 | private TaikaiRule(ArchRule archRule, Configuration configuration) { 25 | this.archRule = requireNonNull(archRule); 26 | this.configuration = requireNonNullElse(configuration, defaultConfiguration()); 27 | } 28 | 29 | public ArchRule archRule() { 30 | return this.archRule; 31 | } 32 | 33 | public Configuration configuration() { 34 | return this.configuration; 35 | } 36 | 37 | public static TaikaiRule of(ArchRule archRule) { 38 | return new TaikaiRule(archRule, defaultConfiguration()); 39 | } 40 | 41 | public static TaikaiRule of(ArchRule archRule, Configuration configuration) { 42 | return new TaikaiRule(archRule, configuration); 43 | } 44 | 45 | public void check(String globalNamespace) { 46 | check(globalNamespace, null, emptySet()); 47 | } 48 | 49 | public void check(String globalNamespace, JavaClasses classes, 50 | Collection excludedClasses) { 51 | this.archRule.check(javaClasses(globalNamespace, classes, excludedClasses)); 52 | } 53 | 54 | JavaClasses javaClasses(String globalNamespace, JavaClasses classes, 55 | Collection excludedClasses) { 56 | if (this.configuration.javaClasses() != null) { 57 | return this.configuration.javaClasses(); 58 | } 59 | 60 | if (classes != null) { 61 | return classes; 62 | } 63 | 64 | String namespace = this.configuration.namespace() != null 65 | ? this.configuration.namespace() 66 | : globalNamespace; 67 | 68 | if (namespace == null) { 69 | throw new TaikaiException("Namespace is not provided"); 70 | } 71 | 72 | Collection allExcludedClasses = allExcludedClasses(excludedClasses); 73 | JavaClasses javaClasses = Namespace.from(namespace, this.configuration.namespaceImport); 74 | 75 | return allExcludedClasses.isEmpty() 76 | ? javaClasses 77 | : javaClasses.that(new ExcludeJavaClassDescribedPredicate(allExcludedClasses)); 78 | } 79 | 80 | private Collection allExcludedClasses(Collection excludedClasses) { 81 | return Stream.concat( 82 | this.configuration.excludedClasses != null 83 | ? this.configuration.excludedClasses.stream() : Stream.empty(), 84 | excludedClasses != null 85 | ? excludedClasses.stream() : Stream.empty() 86 | ).toList(); 87 | } 88 | 89 | public static final class Configuration { 90 | 91 | private final String namespace; 92 | private final Namespace.IMPORT namespaceImport; 93 | private final JavaClasses javaClasses; 94 | private final Collection excludedClasses; 95 | 96 | private Configuration(String namespace, Namespace.IMPORT namespaceImport, 97 | JavaClasses javaClasses, Collection excludedClasses) { 98 | this.namespace = namespace; 99 | this.namespaceImport = requireNonNullElse(namespaceImport, Namespace.IMPORT.WITHOUT_TESTS); 100 | this.javaClasses = javaClasses; 101 | this.excludedClasses = excludedClasses != null ? toClassNames(excludedClasses) : emptyList(); 102 | } 103 | 104 | private static Collection toClassNames(Collection excludedClasses) { 105 | if (excludedClasses.isEmpty()) { 106 | return emptyList(); 107 | } 108 | 109 | T firstElement = excludedClasses.iterator().next(); 110 | 111 | if (firstElement instanceof String) { 112 | return new ArrayList<>((Collection) excludedClasses); 113 | } else if (firstElement instanceof Class) { 114 | return excludedClasses.stream() 115 | .map(clazz -> ((Class) clazz).getName()) 116 | .collect(Collectors.toList()); 117 | } else { 118 | throw new IllegalArgumentException( 119 | "Unsupported collection type, only String and Class are supported"); 120 | } 121 | } 122 | 123 | public String namespace() { 124 | return this.namespace; 125 | } 126 | 127 | public Namespace.IMPORT namespaceImport() { 128 | return this.namespaceImport; 129 | } 130 | 131 | public JavaClasses javaClasses() { 132 | return this.javaClasses; 133 | } 134 | 135 | public Collection excludedClasses() { 136 | return this.excludedClasses; 137 | } 138 | 139 | public static Configuration defaultConfiguration() { 140 | return new Configuration(null, Namespace.IMPORT.WITHOUT_TESTS, null, null); 141 | } 142 | 143 | public static Configuration of(String namespace) { 144 | return new Configuration(namespace, Namespace.IMPORT.WITHOUT_TESTS, null, null); 145 | } 146 | 147 | public static Configuration of(String namespace, Collection excludedClasses) { 148 | return new Configuration(namespace, Namespace.IMPORT.WITHOUT_TESTS, null, excludedClasses); 149 | } 150 | 151 | public static Configuration of(Namespace.IMPORT namespaceImport) { 152 | return new Configuration(null, namespaceImport, null, null); 153 | } 154 | 155 | public static Configuration of(Namespace.IMPORT namespaceImport, 156 | Collection excludedClasses) { 157 | return new Configuration(null, namespaceImport, null, excludedClasses); 158 | } 159 | 160 | public static Configuration of(String namespace, Namespace.IMPORT namespaceImport) { 161 | return new Configuration(namespace, namespaceImport, null, null); 162 | } 163 | 164 | public static Configuration of(String namespace, Namespace.IMPORT namespaceImport, 165 | Collection excludedClasses) { 166 | return new Configuration(namespace, namespaceImport, null, excludedClasses); 167 | } 168 | 169 | public static Configuration of(JavaClasses javaClasses) { 170 | return new Configuration(null, null, javaClasses, null); 171 | } 172 | 173 | public static Configuration of(JavaClasses javaClasses, Collection excludedClasses) { 174 | return new Configuration(null, null, javaClasses, excludedClasses); 175 | } 176 | 177 | public static Configuration of(Collection excludedClasses) { 178 | return new Configuration(null, null, null, excludedClasses); 179 | } 180 | } 181 | 182 | private static final class ExcludeJavaClassDescribedPredicate extends 183 | DescribedPredicate { 184 | 185 | private final Collection allExcludedClassPatterns; 186 | 187 | ExcludeJavaClassDescribedPredicate(Collection allExcludedClasses) { 188 | super("exclude classes"); 189 | this.allExcludedClassPatterns = allExcludedClasses.stream() 190 | .map(Pattern::compile) 191 | .toList(); 192 | } 193 | 194 | @Override 195 | public boolean test(JavaClass javaClass) { 196 | return this.allExcludedClassPatterns.stream() 197 | .noneMatch(pattern -> pattern.matcher(javaClass.getFullName()).matches()); 198 | } 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /src/main/java/com/enofex/taikai/configures/AbstractConfigurer.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai.configures; 2 | 3 | import static java.util.Objects.requireNonNull; 4 | 5 | import com.enofex.taikai.TaikaiRule; 6 | import java.util.ArrayList; 7 | import java.util.Collection; 8 | import java.util.function.Supplier; 9 | 10 | public abstract class AbstractConfigurer implements Configurer { 11 | 12 | private final ConfigurerContext configurerContext; 13 | private final Collection rules; 14 | 15 | protected AbstractConfigurer(ConfigurerContext configurerContext) { 16 | this.configurerContext = requireNonNull(configurerContext); 17 | this.rules = new ArrayList<>(); 18 | } 19 | 20 | protected ConfigurerContext configurerContext() { 21 | return this.configurerContext; 22 | } 23 | 24 | protected T addRule(TaikaiRule rule) { 25 | this.rules.add(rule); 26 | return (T) this; 27 | } 28 | 29 | protected void disable(Class clazz) { 30 | if (clazz != null) { 31 | Configurer configurer = this.configurerContext.configurers().get(clazz); 32 | 33 | if (configurer != null) { 34 | configurer.clear(); 35 | } 36 | } 37 | } 38 | 39 | protected C customizer(Customizer customizer, 40 | Supplier supplier) { 41 | requireNonNull(customizer); 42 | requireNonNull(supplier); 43 | 44 | customizer.customize(this.configurerContext 45 | .configurers() 46 | .getOrApply(supplier.get())); 47 | 48 | return (C) this; 49 | } 50 | 51 | @Override 52 | public Collection rules() { 53 | return this.rules; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/com/enofex/taikai/configures/Configurer.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai.configures; 2 | 3 | import com.enofex.taikai.TaikaiRule; 4 | import java.util.Collection; 5 | 6 | public interface Configurer { 7 | 8 | default void clear() { 9 | rules().clear(); 10 | } 11 | 12 | Collection rules(); 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/enofex/taikai/configures/ConfigurerContext.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai.configures; 2 | 3 | public final class ConfigurerContext { 4 | 5 | private final String namespace; 6 | private final Configurers configurers; 7 | 8 | public ConfigurerContext(String namespace, Configurers configurers) { 9 | this.namespace = namespace; 10 | this.configurers = configurers; 11 | } 12 | 13 | public String namespace() { 14 | return this.namespace; 15 | } 16 | 17 | public Configurers configurers() { 18 | return this.configurers; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/enofex/taikai/configures/Configurers.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai.configures; 2 | 3 | import static java.util.Objects.requireNonNull; 4 | 5 | import java.util.Collection; 6 | import java.util.Iterator; 7 | import java.util.LinkedHashMap; 8 | import java.util.Map; 9 | 10 | public final class Configurers implements Iterable { 11 | 12 | private final Map, Configurer> configurers; 13 | 14 | public Configurers() { 15 | this.configurers = new LinkedHashMap<>(); 16 | } 17 | 18 | public C getOrApply(C configurer) { 19 | requireNonNull(configurer); 20 | 21 | C existingConfigurer = (C) this.get(configurer.getClass()); 22 | return existingConfigurer != null ? existingConfigurer : this.apply(configurer); 23 | } 24 | 25 | private C apply(C configurer) { 26 | this.add(configurer); 27 | return configurer; 28 | } 29 | 30 | private void add(C configurer) { 31 | Class clazz = configurer.getClass(); 32 | this.configurers.putIfAbsent(clazz, configurer); 33 | } 34 | 35 | public C get(Class clazz) { 36 | return (C) this.configurers.get(clazz); 37 | } 38 | 39 | public Collection all() { 40 | return this.configurers.values(); 41 | } 42 | 43 | @Override 44 | public Iterator iterator() { 45 | return this.configurers.values().iterator(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/com/enofex/taikai/configures/Customizer.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai.configures; 2 | 3 | @FunctionalInterface 4 | public interface Customizer { 5 | 6 | void customize(T t); 7 | 8 | } -------------------------------------------------------------------------------- /src/main/java/com/enofex/taikai/configures/DisableableConfigurer.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai.configures; 2 | 3 | public interface DisableableConfigurer extends Configurer { 4 | 5 | T disable(); 6 | 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/enofex/taikai/internal/ArchConditions.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai.internal; 2 | 3 | import static com.enofex.taikai.internal.Modifiers.isFieldPublic; 4 | import static com.enofex.taikai.internal.Modifiers.isFieldStatic; 5 | 6 | import com.tngtech.archunit.core.domain.JavaClass; 7 | import com.tngtech.archunit.core.domain.JavaField; 8 | import com.tngtech.archunit.core.domain.JavaMethod; 9 | import com.tngtech.archunit.core.domain.JavaModifier; 10 | import com.tngtech.archunit.lang.ArchCondition; 11 | import com.tngtech.archunit.lang.ConditionEvents; 12 | import com.tngtech.archunit.lang.SimpleConditionEvent; 13 | import java.util.Collection; 14 | import java.util.stream.Collectors; 15 | 16 | /** 17 | * Internal utility class for defining general ArchCondition used in architectural rules. 18 | *

19 | * This class is intended for internal use only and is not part of the public API. Developers should 20 | * not rely on this class for any public API usage. 21 | */ 22 | public final class ArchConditions { 23 | 24 | private ArchConditions() { 25 | } 26 | 27 | /** 28 | * Creates a condition that checks if a method does not declare thrown exceptions. 29 | * 30 | * @return an architectural condition for checking thrown exceptions in methods 31 | */ 32 | public static ArchCondition notDeclareThrownExceptions() { 33 | return new ArchCondition<>("not declare thrown exceptions") { 34 | @Override 35 | public void check(JavaMethod method, ConditionEvents events) { 36 | if (!method.getThrowsClause().isEmpty()) { 37 | events.add(SimpleConditionEvent.violated(method, 38 | "Method %s declares thrown exceptions".formatted( 39 | method.getFullName()))); 40 | } 41 | } 42 | }; 43 | } 44 | 45 | /** 46 | * Creates a condition that checks if a field is not public and not static. 47 | * 48 | * @return an architectural condition for checking public except static fields 49 | */ 50 | public static ArchCondition notBePublicUnlessStatic() { 51 | return new ArchCondition<>("not be public") { 52 | @Override 53 | public void check(JavaField field, ConditionEvents events) { 54 | if (!isFieldStatic(field) && isFieldPublic(field)) { 55 | events.add(SimpleConditionEvent.violated(field, 56 | "Field %s in class %s is public".formatted( 57 | field.getName(), 58 | field.getOwner().getFullName()))); 59 | } 60 | } 61 | }; 62 | } 63 | 64 | /** 65 | * Creates a condition that checks if a class has a field of the specified type. 66 | * 67 | * @param typeName the name of the type to check for in the fields of the class 68 | * @return an architectural condition for checking if a class has a field of the specified type 69 | */ 70 | public static ArchCondition haveFieldOfType(String typeName) { 71 | return new ArchCondition<>("have a field of type %s".formatted(typeName)) { 72 | @Override 73 | public void check(JavaClass item, ConditionEvents events) { 74 | boolean hasFieldOfType = item.getAllFields().stream() 75 | .anyMatch(field -> field.getRawType().getName().equals(typeName)); 76 | 77 | if (!hasFieldOfType) { 78 | events.add(SimpleConditionEvent.violated(item, 79 | "%s does not have a field of type %s".formatted( 80 | item.getName(), 81 | typeName))); 82 | } 83 | } 84 | }; 85 | } 86 | 87 | /** 88 | * Creates a condition that checks if a field contains all the specified modifiers. 89 | * 90 | * @param requiredModifiers the collection of modifiers that the field is required to have 91 | * @return an architectural condition for checking if a field has the required modifiers 92 | */ 93 | public static ArchCondition hasFieldModifiers( 94 | Collection requiredModifiers) { 95 | return new ArchCondition<>("has field modifiers") { 96 | @Override 97 | public void check(JavaField field, ConditionEvents events) { 98 | if (!field.getModifiers().containsAll(requiredModifiers)) { 99 | events.add(SimpleConditionEvent.violated(field, 100 | "Field %s in class %s is missing one of this %s modifier".formatted( 101 | field.getName(), 102 | field.getOwner().getFullName(), 103 | requiredModifiers.stream().map(Enum::name).collect(Collectors.joining(", "))))); 104 | } 105 | } 106 | }; 107 | } 108 | 109 | /** 110 | * Creates a condition that checks if a method contains all the specified modifiers. 111 | * 112 | * @param requiredModifiers the collection of modifiers that the method is required to have 113 | * @return an architectural condition for checking if a method has the required modifiers 114 | */ 115 | public static ArchCondition hasMethodsModifiers( 116 | Collection requiredModifiers) { 117 | return new ArchCondition<>("has method modifiers") { 118 | @Override 119 | public void check(JavaMethod method, ConditionEvents events) { 120 | if (!method.getModifiers().containsAll(requiredModifiers)) { 121 | events.add(SimpleConditionEvent.violated(method, 122 | "Method %s in class %s is missing one of this %s modifier".formatted( 123 | method.getName(), 124 | method.getOwner().getFullName(), 125 | requiredModifiers.stream().map(Enum::name).collect(Collectors.joining(", "))))); 126 | } 127 | } 128 | }; 129 | } 130 | 131 | /** 132 | * Creates a condition that checks if a class contains all the specified modifiers. 133 | * 134 | * @param requiredModifiers the collection of modifiers that the class is required to have 135 | * @return an architectural condition for checking if a class has the required modifiers 136 | */ 137 | public static ArchCondition hasClassModifiers( 138 | Collection requiredModifiers) { 139 | return new ArchCondition<>("has class modifiers") { 140 | @Override 141 | public void check(JavaClass clazz, ConditionEvents events) { 142 | if (!clazz.getModifiers().containsAll(requiredModifiers)) { 143 | events.add(SimpleConditionEvent.violated(clazz, 144 | "Class %s is missing one of this %s modifier".formatted( 145 | clazz.getName(), 146 | requiredModifiers.stream().map(Enum::name).collect(Collectors.joining(", "))))); 147 | } 148 | } 149 | }; 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/main/java/com/enofex/taikai/internal/DescribedPredicates.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai.internal; 2 | 3 | import static com.enofex.taikai.internal.Modifiers.isClassFinal; 4 | 5 | import com.tngtech.archunit.base.DescribedPredicate; 6 | import com.tngtech.archunit.core.domain.JavaClass; 7 | import com.tngtech.archunit.core.domain.properties.CanBeAnnotated; 8 | import java.util.Collection; 9 | 10 | /** 11 | * Internal utility class for defining general DescribedPredicate used in architectural rules. 12 | *

13 | * This class is intended for internal use only and is not part of the public API. Developers should 14 | * not rely on this class for any public API usage. 15 | */ 16 | public final class DescribedPredicates { 17 | 18 | private DescribedPredicates() { 19 | } 20 | 21 | /** 22 | * Creates a predicate that checks if an element is annotated with a specific annotation. 23 | * 24 | * @param annotation the annotation to check for 25 | * @param isMetaAnnotated true if the annotation should be meta-annotated, false otherwise 26 | * @return a described predicate for the annotation check 27 | */ 28 | public static DescribedPredicate annotatedWith(String annotation, 29 | boolean isMetaAnnotated) { 30 | return new DescribedPredicate<>("annotated with %s".formatted(annotation)) { 31 | @Override 32 | public boolean test(CanBeAnnotated canBeAnnotated) { 33 | return isMetaAnnotated ? canBeAnnotated.isMetaAnnotatedWith(annotation) 34 | : canBeAnnotated.isAnnotatedWith(annotation); 35 | } 36 | }; 37 | } 38 | 39 | /** 40 | * Creates a predicate that checks if an element is annotated with all the specified annotations. 41 | * 42 | * @param annotations the collection of annotations to check for 43 | * @param isMetaAnnotated true if the annotations should be meta-annotated, false otherwise 44 | * @return a described predicate for the annotation check 45 | */ 46 | public static DescribedPredicate annotatedWithAll(Collection annotations, 47 | boolean isMetaAnnotated) { 48 | return new DescribedPredicate<>("annotated with all of %s".formatted(annotations)) { 49 | @Override 50 | public boolean test(CanBeAnnotated canBeAnnotated) { 51 | return annotations.stream().allMatch(annotation -> 52 | isMetaAnnotated ? canBeAnnotated.isMetaAnnotatedWith(annotation) 53 | : canBeAnnotated.isAnnotatedWith(annotation)); 54 | } 55 | }; 56 | } 57 | 58 | /** 59 | * Creates a predicate that checks if a class is final. 60 | * 61 | * @return a described predicate for the final modifier check 62 | */ 63 | public static DescribedPredicate areFinal() { 64 | return new DescribedPredicate<>("are final") { 65 | @Override 66 | public boolean test(JavaClass javaClass) { 67 | return isClassFinal(javaClass); 68 | } 69 | }; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/main/java/com/enofex/taikai/internal/Modifiers.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai.internal; 2 | 3 | import com.tngtech.archunit.core.domain.JavaClass; 4 | import com.tngtech.archunit.core.domain.JavaConstructor; 5 | import com.tngtech.archunit.core.domain.JavaField; 6 | import com.tngtech.archunit.core.domain.JavaMethod; 7 | import com.tngtech.archunit.core.domain.JavaModifier; 8 | 9 | /** 10 | * This class provides utility methods for checking Java modifiers. 11 | *

12 | * This class is intended for internal use only and is not part of the public API. Developers should 13 | * not rely on this class for any public API usage. 14 | */ 15 | public final class Modifiers { 16 | 17 | private Modifiers() { 18 | } 19 | 20 | /** 21 | * Checks if a class is final. 22 | * 23 | * @param javaClass the Java class to check 24 | * @return true if the class is final, false otherwise 25 | */ 26 | public static boolean isClassFinal(JavaClass javaClass) { 27 | return javaClass.getModifiers().contains(JavaModifier.FINAL); 28 | } 29 | 30 | /** 31 | * Checks if a constructor is private. 32 | * 33 | * @param constructor the Java constructor to check 34 | * @return true if the constructor is private, false otherwise 35 | */ 36 | public static boolean isConstructorPrivate(JavaConstructor constructor) { 37 | return constructor.getModifiers().contains(JavaModifier.PRIVATE); 38 | } 39 | 40 | /** 41 | * Checks if a method is protected. 42 | * 43 | * @param method the Java method to check 44 | * @return true if the method is protected, false otherwise 45 | */ 46 | public static boolean isMethodProtected(JavaMethod method) { 47 | return method.getModifiers().contains(JavaModifier.PROTECTED); 48 | } 49 | 50 | /** 51 | * Checks if a method is static. 52 | * 53 | * @param method the Java method to check 54 | * @return true if the method is static, false otherwise 55 | */ 56 | public static boolean isMethodStatic(JavaMethod method) { 57 | return method.getModifiers().contains(JavaModifier.STATIC); 58 | } 59 | 60 | /** 61 | * Checks if a field is static. 62 | * 63 | * @param field the Java field to check 64 | * @return true if the field is static, false otherwise 65 | */ 66 | public static boolean isFieldStatic(JavaField field) { 67 | return field.getModifiers().contains(JavaModifier.STATIC); 68 | } 69 | 70 | /** 71 | * Checks if a field is public. 72 | * 73 | * @param field the Java field to check 74 | * @return true if the field is public, false otherwise 75 | */ 76 | public static boolean isFieldPublic(JavaField field) { 77 | return field.getModifiers().contains(JavaModifier.PUBLIC); 78 | } 79 | 80 | /** 81 | * Checks if a field is protected. 82 | * 83 | * @param field the Java field to check 84 | * @return true if the field is protected, false otherwise 85 | */ 86 | public static boolean isFieldProtected(JavaField field) { 87 | return field.getModifiers().contains(JavaModifier.PROTECTED); 88 | } 89 | 90 | /** 91 | * Checks if a field is final. 92 | * 93 | * @param field the Java field to check 94 | * @return true if the field is final, false otherwise 95 | */ 96 | public static boolean isFieldFinal(JavaField field) { 97 | return field.getModifiers().contains(JavaModifier.FINAL); 98 | } 99 | 100 | /** 101 | * Checks if a field is synthetic. 102 | * 103 | * @param field the Java field to check 104 | * @return true if the field is synthetic, false otherwise 105 | */ 106 | public static boolean isFieldSynthetic(JavaField field) { 107 | return field.getModifiers().contains(JavaModifier.SYNTHETIC); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/main/java/com/enofex/taikai/java/ConstantNaming.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai.java; 2 | 3 | import static com.enofex.taikai.internal.Modifiers.isFieldSynthetic; 4 | 5 | import com.tngtech.archunit.core.domain.JavaField; 6 | import com.tngtech.archunit.lang.ArchCondition; 7 | import com.tngtech.archunit.lang.ConditionEvents; 8 | import com.tngtech.archunit.lang.SimpleConditionEvent; 9 | import java.util.regex.Pattern; 10 | 11 | final class ConstantNaming { 12 | 13 | private static final Pattern CONSTANT_NAME_PATTERN = Pattern.compile("^[A-Z][A-Z0-9_]*$"); 14 | 15 | private ConstantNaming() { 16 | } 17 | 18 | static ArchCondition shouldFollowConstantNamingConventions() { 19 | return new ArchCondition<>("follow constant naming convention") { 20 | @Override 21 | public void check(JavaField field, ConditionEvents events) { 22 | if (!isFieldSynthetic(field) 23 | && !"serialVersionUID".equals(field.getName()) 24 | && !CONSTANT_NAME_PATTERN.matcher(field.getName()).matches()) { 25 | events.add(SimpleConditionEvent.violated(field, 26 | "Constant %s in class %s does not follow the naming convention".formatted( 27 | field.getName(), 28 | field.getOwner().getName()))); 29 | } 30 | } 31 | }; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/enofex/taikai/java/Deprecations.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai.java; 2 | 3 | import com.tngtech.archunit.core.domain.JavaClass; 4 | import com.tngtech.archunit.core.domain.JavaType; 5 | import com.tngtech.archunit.lang.ArchCondition; 6 | import com.tngtech.archunit.lang.ConditionEvents; 7 | import com.tngtech.archunit.lang.SimpleConditionEvent; 8 | 9 | final class Deprecations { 10 | 11 | private Deprecations() { 12 | } 13 | 14 | static ArchCondition notUseDeprecatedAPIs() { 15 | return new ArchCondition("not use deprecated APIs") { 16 | @Override 17 | public void check(JavaClass javaClass, ConditionEvents events) { 18 | javaClass.getFieldAccessesFromSelf().stream() 19 | .filter(access -> access.getTarget().isAnnotatedWith(Deprecated.class)) 20 | .forEach(access -> events.add(SimpleConditionEvent.violated(access.getTarget(), 21 | "Field %s in class %s is deprecated and is being accessed by %s".formatted( 22 | access.getTarget().getName(), 23 | access.getTarget().getOwner().getName(), 24 | javaClass.getName())))); 25 | 26 | javaClass.getMethodCallsFromSelf().stream() 27 | .filter(method -> !method.getTarget().getName().equals(Object.class.getName())) 28 | .filter(method -> !method.getTarget().getName().equals(Enum.class.getName())) 29 | .filter(method -> method.getTarget().isAnnotatedWith(Deprecated.class) || 30 | method.getTarget().getRawReturnType().isAnnotatedWith(Deprecated.class) || 31 | method.getTarget().getParameterTypes().stream() 32 | .anyMatch(Deprecations::isDeprecated)) 33 | .forEach(method -> events.add(SimpleConditionEvent.violated(method, 34 | "Method %s used in class %s is deprecated".formatted( 35 | method.getName(), 36 | javaClass.getName())))); 37 | 38 | javaClass.getConstructorCallsFromSelf().stream() 39 | .filter(constructor -> constructor.getTarget().isAnnotatedWith(Deprecated.class) || 40 | constructor.getTarget().getParameterTypes().stream() 41 | .anyMatch(Deprecations::isDeprecated)) 42 | .forEach(constructor -> events.add(SimpleConditionEvent.violated(constructor, 43 | "Constructor %s in class %s uses deprecated APIs".formatted( 44 | constructor.getTarget().getFullName(), 45 | javaClass.getName())))); 46 | 47 | javaClass.getDirectDependenciesFromSelf().stream() 48 | .filter(dependency -> dependency.getTargetClass().isAnnotatedWith(Deprecated.class)) 49 | .forEach(dependency -> events.add( 50 | SimpleConditionEvent.violated(dependency.getTargetClass(), 51 | "Class %s depends on deprecated class %s".formatted( 52 | javaClass.getName(), 53 | dependency.getTargetClass().getName())))); 54 | } 55 | }.as("no usage of deprecated APIs"); 56 | } 57 | 58 | private static boolean isDeprecated(JavaType javaType) { 59 | return javaType.toErasure().isAnnotatedWith(Deprecated.class); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/com/enofex/taikai/java/HashCodeAndEquals.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai.java; 2 | 3 | import com.tngtech.archunit.core.domain.JavaClass; 4 | import com.tngtech.archunit.lang.ArchCondition; 5 | import com.tngtech.archunit.lang.ConditionEvents; 6 | import com.tngtech.archunit.lang.SimpleConditionEvent; 7 | 8 | final class HashCodeAndEquals { 9 | 10 | private HashCodeAndEquals() { 11 | } 12 | 13 | static ArchCondition implementHashCodeAndEquals() { 14 | return new ArchCondition<>("implement both equals() and hashCode()") { 15 | @Override 16 | public void check(JavaClass javaClass, ConditionEvents events) { 17 | boolean hasEquals = hasEquals(javaClass); 18 | boolean hasHashCode = hasHashCode(javaClass); 19 | 20 | if (hasEquals && !hasHashCode) { 21 | events.add(SimpleConditionEvent.violated(javaClass, 22 | "Class %s implements equals() but not hashCode()".formatted( 23 | javaClass.getName()))); 24 | } else if (!hasEquals && hasHashCode) { 25 | events.add(SimpleConditionEvent.violated(javaClass, 26 | "Class %s implements hashCode() but not equals()".formatted( 27 | javaClass.getName()))); 28 | } 29 | } 30 | 31 | private static boolean hasHashCode(JavaClass javaClass) { 32 | return javaClass.getMethods().stream() 33 | .anyMatch(method -> "hashCode".equals(method.getName()) && 34 | method.getRawParameterTypes().isEmpty()); 35 | } 36 | 37 | private static boolean hasEquals(JavaClass javaClass) { 38 | return javaClass.getMethods().stream() 39 | .anyMatch(method -> "equals".equals(method.getName()) && 40 | method.getRawParameterTypes().size() == 1 && 41 | method.getRawParameterTypes().get(0).getName().equals(Object.class.getName())); 42 | } 43 | }; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/com/enofex/taikai/java/ImportsConfigurer.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai.java; 2 | 3 | import static com.enofex.taikai.TaikaiRule.Configuration.defaultConfiguration; 4 | import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses; 5 | import static com.tngtech.archunit.library.dependencies.SlicesRuleDefinition.slices; 6 | 7 | import com.enofex.taikai.TaikaiException; 8 | import com.enofex.taikai.TaikaiRule; 9 | import com.enofex.taikai.TaikaiRule.Configuration; 10 | import com.enofex.taikai.configures.AbstractConfigurer; 11 | import com.enofex.taikai.configures.ConfigurerContext; 12 | import com.enofex.taikai.configures.DisableableConfigurer; 13 | 14 | public class ImportsConfigurer extends AbstractConfigurer { 15 | 16 | ImportsConfigurer(ConfigurerContext configurerContext) { 17 | super(configurerContext); 18 | } 19 | 20 | public ImportsConfigurer shouldNotImport(String packageIdentifier) { 21 | return shouldNotImport(packageIdentifier, defaultConfiguration()); 22 | } 23 | 24 | public ImportsConfigurer shouldNotImport(String packageIdentifier, Configuration configuration) { 25 | return addRule(TaikaiRule.of(noClasses() 26 | .should().accessClassesThat() 27 | .resideInAPackage(packageIdentifier) 28 | .as("No classes should have imports from package %s".formatted(packageIdentifier)), 29 | configuration)); 30 | } 31 | 32 | public ImportsConfigurer shouldNotImport(String regex, String notImportClassesRegex) { 33 | return shouldNotImport(regex, notImportClassesRegex, defaultConfiguration()); 34 | } 35 | 36 | public ImportsConfigurer shouldNotImport(String regex, String notImportClassesRegex, 37 | Configuration configuration) { 38 | return addRule(TaikaiRule.of(noClasses() 39 | .that().haveNameMatching(regex) 40 | .should().accessClassesThat() 41 | .haveNameMatching(notImportClassesRegex) 42 | .as("No classes that have name matching %s should have imports %s".formatted( 43 | regex, notImportClassesRegex)), configuration)); 44 | } 45 | 46 | public ImportsConfigurer shouldHaveNoCycles() { 47 | return shouldHaveNoCycles(null); 48 | } 49 | 50 | public ImportsConfigurer shouldHaveNoCycles(Configuration configuration) { 51 | String namespace = configuration != null 52 | ? configuration.namespace() 53 | : configurerContext() != null 54 | ? configurerContext().namespace() 55 | : null; 56 | 57 | if (namespace == null) { 58 | throw new TaikaiException("Namespace is not set"); 59 | } 60 | 61 | return addRule(TaikaiRule.of(slices() 62 | .matching(namespace + ".(*)..") 63 | .should().beFreeOfCycles() 64 | .as("Namespace %s should be free of cycles".formatted(namespace)), configuration)); 65 | } 66 | 67 | public static final class Disableable extends ImportsConfigurer implements DisableableConfigurer { 68 | 69 | public Disableable(ConfigurerContext configurerContext) { 70 | super(configurerContext); 71 | } 72 | 73 | @Override 74 | public ImportsConfigurer disable() { 75 | disable(ImportsConfigurer.class); 76 | 77 | return this; 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/main/java/com/enofex/taikai/java/NamingConfigurer.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai.java; 2 | 3 | import static com.enofex.taikai.TaikaiRule.Configuration.defaultConfiguration; 4 | import static com.enofex.taikai.java.ConstantNaming.shouldFollowConstantNamingConventions; 5 | import static com.enofex.taikai.java.PackageNaming.resideInPackageWithProperNamingConvention; 6 | import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes; 7 | import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.fields; 8 | import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.methods; 9 | import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses; 10 | import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noFields; 11 | import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noMethods; 12 | 13 | import com.enofex.taikai.TaikaiRule; 14 | import com.enofex.taikai.TaikaiRule.Configuration; 15 | import com.enofex.taikai.configures.AbstractConfigurer; 16 | import com.enofex.taikai.configures.ConfigurerContext; 17 | import com.enofex.taikai.configures.DisableableConfigurer; 18 | import com.tngtech.archunit.core.domain.JavaClass; 19 | import com.tngtech.archunit.lang.ArchCondition; 20 | import com.tngtech.archunit.lang.ConditionEvents; 21 | import com.tngtech.archunit.lang.SimpleConditionEvent; 22 | import java.lang.annotation.Annotation; 23 | 24 | public class NamingConfigurer extends AbstractConfigurer { 25 | 26 | private static final String PACKAGE_NAME_REGEX = "^[a-z_]+(\\.[a-z_][a-z0-9_]*)*$"; 27 | 28 | NamingConfigurer(ConfigurerContext configurerContext) { 29 | super(configurerContext); 30 | } 31 | 32 | public NamingConfigurer packagesShouldMatchDefault() { 33 | return packagesShouldMatch(PACKAGE_NAME_REGEX, defaultConfiguration()); 34 | } 35 | 36 | public NamingConfigurer packagesShouldMatchDefault(Configuration configuration) { 37 | return packagesShouldMatch(PACKAGE_NAME_REGEX, configuration); 38 | } 39 | 40 | public NamingConfigurer packagesShouldMatch(String regex) { 41 | return packagesShouldMatch(regex, defaultConfiguration()); 42 | } 43 | 44 | public NamingConfigurer packagesShouldMatch(String regex, Configuration configuration) { 45 | return addRule(TaikaiRule.of(classes() 46 | .should(resideInPackageWithProperNamingConvention(regex)) 47 | .as("Packages should have names matching %s".formatted(regex)), configuration)); 48 | } 49 | 50 | public NamingConfigurer classesShouldNotMatch(String regex) { 51 | return classesShouldNotMatch(regex, defaultConfiguration()); 52 | } 53 | 54 | public NamingConfigurer classesShouldNotMatch(String regex, Configuration configuration) { 55 | return addRule(TaikaiRule.of(noClasses() 56 | .should().haveNameMatching(regex) 57 | .as("Classes should not have names matching %s".formatted(regex)), configuration)); 58 | } 59 | 60 | public NamingConfigurer classesAnnotatedWithShouldMatch( 61 | Class annotationType, String regex) { 62 | return classesAnnotatedWithShouldMatch(annotationType.getName(), regex, defaultConfiguration()); 63 | } 64 | 65 | public NamingConfigurer classesAnnotatedWithShouldMatch( 66 | Class annotationType, String regex, Configuration configuration) { 67 | return classesAnnotatedWithShouldMatch(annotationType.getName(), regex, configuration); 68 | } 69 | 70 | public NamingConfigurer classesAnnotatedWithShouldMatch(String annotationType, String regex) { 71 | return classesAnnotatedWithShouldMatch(annotationType, regex, defaultConfiguration()); 72 | } 73 | 74 | public NamingConfigurer classesAnnotatedWithShouldMatch(String annotationType, String regex, 75 | Configuration configuration) { 76 | return addRule(TaikaiRule.of(classes() 77 | .that().areMetaAnnotatedWith(annotationType) 78 | .should().haveNameMatching(regex) 79 | .as("Classes annotated with %s should have names matching %s".formatted( 80 | annotationType, regex)), configuration)); 81 | } 82 | 83 | public NamingConfigurer classesImplementingShouldMatch(Class clazz, String regex) { 84 | return classesImplementingShouldMatch(clazz.getName(), regex, defaultConfiguration()); 85 | } 86 | 87 | public NamingConfigurer classesImplementingShouldMatch(Class clazz, String regex, 88 | Configuration configuration) { 89 | return classesImplementingShouldMatch(clazz.getName(), regex, configuration); 90 | } 91 | 92 | public NamingConfigurer classesImplementingShouldMatch(String typeName, String regex) { 93 | return classesImplementingShouldMatch(typeName, regex, defaultConfiguration()); 94 | } 95 | 96 | public NamingConfigurer classesImplementingShouldMatch(String typeName, String regex, 97 | Configuration configuration) { 98 | return addRule(TaikaiRule.of(classes() 99 | .that().implement(typeName) 100 | .should().haveNameMatching(regex) 101 | .as("Classes implementing %s should have names matching %s".formatted( 102 | typeName, regex)), configuration)); 103 | } 104 | 105 | public NamingConfigurer classesAssignableToShouldMatch(Class clazz, String regex) { 106 | return classesAssignableToShouldMatch(clazz.getName(), regex, defaultConfiguration()); 107 | } 108 | 109 | public NamingConfigurer classesAssignableToShouldMatch(Class clazz, String regex, 110 | Configuration configuration) { 111 | return classesAssignableToShouldMatch(clazz.getName(), regex, configuration); 112 | } 113 | 114 | public NamingConfigurer classesAssignableToShouldMatch(String typeName, String regex) { 115 | return classesAssignableToShouldMatch(typeName, regex, defaultConfiguration()); 116 | } 117 | 118 | public NamingConfigurer classesAssignableToShouldMatch(String typeName, String regex, 119 | Configuration configuration) { 120 | return addRule(TaikaiRule.of(classes() 121 | .that().areAssignableTo(typeName) 122 | .should().haveNameMatching(regex) 123 | .as("Classes assignable to %s should have names matching %s".formatted( 124 | typeName, regex)), configuration)); 125 | } 126 | 127 | public NamingConfigurer methodsAnnotatedWithShouldMatch( 128 | Class annotationType, String regex) { 129 | return methodsAnnotatedWithShouldMatch(annotationType.getName(), regex, defaultConfiguration()); 130 | } 131 | 132 | public NamingConfigurer methodsAnnotatedWithShouldMatch( 133 | Class annotationType, String regex, Configuration configuration) { 134 | return methodsAnnotatedWithShouldMatch(annotationType.getName(), regex, configuration); 135 | } 136 | 137 | public NamingConfigurer methodsAnnotatedWithShouldMatch( 138 | String annotationType, String regex) { 139 | return methodsAnnotatedWithShouldMatch(annotationType, regex, defaultConfiguration()); 140 | } 141 | 142 | public NamingConfigurer methodsAnnotatedWithShouldMatch(String annotationType, String regex, 143 | Configuration configuration) { 144 | return addRule(TaikaiRule.of(methods() 145 | .that().areMetaAnnotatedWith(annotationType) 146 | .should().haveNameMatching(regex) 147 | .as("Methods annotated with %s should have names matching %s".formatted( 148 | annotationType, regex)), configuration)); 149 | } 150 | 151 | public NamingConfigurer methodsShouldNotMatch(String regex) { 152 | return methodsShouldNotMatch(regex, defaultConfiguration()); 153 | } 154 | 155 | public NamingConfigurer methodsShouldNotMatch(String regex, Configuration configuration) { 156 | return addRule(TaikaiRule.of(noMethods() 157 | .should().haveNameMatching(regex) 158 | .as("Methods should not have names matching %s".formatted(regex)), configuration)); 159 | } 160 | 161 | public NamingConfigurer fieldsAnnotatedWithShouldMatch( 162 | Class annotationType, String regex) { 163 | return fieldsAnnotatedWithShouldMatch(annotationType.getName(), regex, defaultConfiguration()); 164 | } 165 | 166 | public NamingConfigurer fieldsAnnotatedWithShouldMatch( 167 | Class annotationType, String regex, Configuration configuration) { 168 | return fieldsAnnotatedWithShouldMatch(annotationType.getName(), regex, configuration); 169 | } 170 | 171 | public NamingConfigurer fieldsAnnotatedWithShouldMatch(String annotationType, String regex) { 172 | return fieldsAnnotatedWithShouldMatch(annotationType, regex, defaultConfiguration()); 173 | } 174 | 175 | public NamingConfigurer fieldsAnnotatedWithShouldMatch(String annotationType, String regex, 176 | Configuration configuration) { 177 | return addRule(TaikaiRule.of(fields() 178 | .that().areMetaAnnotatedWith(annotationType) 179 | .should().haveNameMatching(regex) 180 | .as("Fields annotated with %s should have names matching %s".formatted( 181 | annotationType, regex)), configuration)); 182 | } 183 | 184 | public NamingConfigurer fieldsShouldMatch(String typeName, String regex) { 185 | return fieldsShouldMatch(typeName, regex, defaultConfiguration()); 186 | } 187 | 188 | public NamingConfigurer fieldsShouldMatch(Class clazz, String regex) { 189 | return fieldsShouldMatch(clazz.getName(), regex, defaultConfiguration()); 190 | } 191 | 192 | public NamingConfigurer fieldsShouldMatch(Class clazz, String regex, 193 | Configuration configuration) { 194 | return fieldsShouldMatch(clazz.getName(), regex, configuration); 195 | } 196 | 197 | public NamingConfigurer fieldsShouldMatch(String typeName, String regex, 198 | Configuration configuration) { 199 | return addRule(TaikaiRule.of(fields() 200 | .that().haveRawType(typeName) 201 | .should().haveNameMatching(regex) 202 | .as("Fields of type %s should have names matching %s".formatted(typeName, regex)), 203 | configuration)); 204 | } 205 | 206 | public NamingConfigurer fieldsShouldNotMatch(String regex) { 207 | return fieldsShouldNotMatch(regex, defaultConfiguration()); 208 | } 209 | 210 | public NamingConfigurer fieldsShouldNotMatch(String regex, Configuration configuration) { 211 | return addRule(TaikaiRule.of(noFields() 212 | .should().haveNameMatching(regex) 213 | .as("Fields should not have names matching %s".formatted(regex)), configuration)); 214 | } 215 | 216 | public NamingConfigurer interfacesShouldNotHavePrefixI() { 217 | return interfacesShouldNotHavePrefixI(defaultConfiguration()); 218 | } 219 | 220 | public NamingConfigurer interfacesShouldNotHavePrefixI(Configuration configuration) { 221 | return addRule(TaikaiRule.of(classes() 222 | .that().areInterfaces() 223 | .should(notBePrefixedWithI()) 224 | .as("Interfaces should not be prefixed with I"), configuration)); 225 | } 226 | 227 | private static ArchCondition notBePrefixedWithI() { 228 | return new ArchCondition<>("not be prefixed with I") { 229 | @Override 230 | public void check(JavaClass javaClass, ConditionEvents events) { 231 | if (javaClass.getSimpleName().startsWith("I") && Character.isUpperCase( 232 | javaClass.getSimpleName().charAt(1))) { 233 | events.add(SimpleConditionEvent.violated(javaClass, javaClass.getSimpleName())); 234 | } 235 | } 236 | }; 237 | } 238 | 239 | public NamingConfigurer constantsShouldFollowConventions() { 240 | return constantsShouldFollowConventions(defaultConfiguration()); 241 | } 242 | 243 | public NamingConfigurer constantsShouldFollowConventions(Configuration configuration) { 244 | return addRule(TaikaiRule.of(fields() 245 | .that().areFinal().and().areStatic() 246 | .should(shouldFollowConstantNamingConventions()) 247 | .as("Constants should follow constant naming conventions"), configuration)); 248 | } 249 | 250 | public static final class Disableable extends NamingConfigurer implements DisableableConfigurer { 251 | 252 | public Disableable(ConfigurerContext configurerContext) { 253 | super(configurerContext); 254 | } 255 | 256 | @Override 257 | public NamingConfigurer disable() { 258 | disable(NamingConfigurer.class); 259 | 260 | return this; 261 | } 262 | } 263 | } 264 | -------------------------------------------------------------------------------- /src/main/java/com/enofex/taikai/java/NoSystemOutOrErr.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai.java; 2 | 3 | import com.tngtech.archunit.core.domain.JavaClass; 4 | import com.tngtech.archunit.lang.ArchCondition; 5 | import com.tngtech.archunit.lang.ConditionEvents; 6 | import com.tngtech.archunit.lang.SimpleConditionEvent; 7 | 8 | final class NoSystemOutOrErr { 9 | 10 | private NoSystemOutOrErr() { 11 | } 12 | 13 | static ArchCondition notUseSystemOutOrErr() { 14 | return new ArchCondition<>("not call System.out or System.err") { 15 | @Override 16 | public void check(JavaClass javaClass, ConditionEvents events) { 17 | javaClass.getFieldAccessesFromSelf().stream() 18 | .filter(fieldAccess -> fieldAccess.getTargetOwner().isEquivalentTo(System.class)) 19 | .forEach(fieldAccess -> { 20 | String fieldName = fieldAccess.getTarget().getName(); 21 | 22 | if ("out".equals(fieldName) || "err".equals(fieldName)) { 23 | events.add(SimpleConditionEvent.violated(fieldAccess, 24 | "Method %s calls %s.%s".formatted( 25 | fieldAccess.getOrigin().getFullName(), 26 | fieldAccess.getTargetOwner().getName(), 27 | fieldAccess.getTarget().getName()))); 28 | } 29 | }); 30 | } 31 | }; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/enofex/taikai/java/PackageNaming.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai.java; 2 | 3 | import com.tngtech.archunit.core.domain.JavaClass; 4 | import com.tngtech.archunit.lang.ArchCondition; 5 | import com.tngtech.archunit.lang.ConditionEvents; 6 | import com.tngtech.archunit.lang.SimpleConditionEvent; 7 | import java.util.regex.Pattern; 8 | 9 | final class PackageNaming { 10 | 11 | private PackageNaming() { 12 | } 13 | 14 | static ArchCondition resideInPackageWithProperNamingConvention(String regex) { 15 | return new ArchCondition<>("reside in package with proper naming convention") { 16 | private final Pattern pattern = Pattern.compile(regex); 17 | 18 | @Override 19 | public void check(JavaClass javaClass, ConditionEvents events) { 20 | String packageName = javaClass.getPackageName(); 21 | if (!this.pattern.matcher(packageName).matches()) { 22 | events.add(SimpleConditionEvent.violated(javaClass, 23 | "Package '%s' does not follow the naming convention".formatted( 24 | packageName))); 25 | } 26 | } 27 | }; 28 | } 29 | } -------------------------------------------------------------------------------- /src/main/java/com/enofex/taikai/java/ProtectedMembers.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai.java; 2 | 3 | import static com.enofex.taikai.internal.Modifiers.isFieldProtected; 4 | import static com.enofex.taikai.internal.Modifiers.isMethodProtected; 5 | 6 | import com.tngtech.archunit.core.domain.JavaClass; 7 | import com.tngtech.archunit.core.domain.JavaField; 8 | import com.tngtech.archunit.core.domain.JavaMethod; 9 | import com.tngtech.archunit.lang.ArchCondition; 10 | import com.tngtech.archunit.lang.ConditionEvents; 11 | import com.tngtech.archunit.lang.SimpleConditionEvent; 12 | 13 | final class ProtectedMembers { 14 | 15 | private ProtectedMembers() { 16 | } 17 | 18 | static ArchCondition notHaveProtectedMembers() { 19 | return new ArchCondition<>("not have protected members") { 20 | @Override 21 | public void check(JavaClass javaClass, ConditionEvents events) { 22 | for (JavaField field : javaClass.getFields()) { 23 | if (isFieldProtected(field)) { 24 | events.add(SimpleConditionEvent.violated(field, 25 | "Field %s in final class %s is protected".formatted( 26 | field.getName(), 27 | javaClass.getName()))); 28 | } 29 | } 30 | 31 | for (JavaMethod method : javaClass.getMethods()) { 32 | if (isMethodProtected(method)) { 33 | events.add(SimpleConditionEvent.violated(method, 34 | "Method %s in final class %s is protected".formatted( 35 | method.getName(), 36 | javaClass.getName()))); 37 | } 38 | } 39 | } 40 | }; 41 | } 42 | } -------------------------------------------------------------------------------- /src/main/java/com/enofex/taikai/java/SerialVersionUID.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai.java; 2 | 3 | import static com.enofex.taikai.internal.Modifiers.isFieldFinal; 4 | import static com.enofex.taikai.internal.Modifiers.isFieldStatic; 5 | 6 | import com.tngtech.archunit.base.DescribedPredicate; 7 | import com.tngtech.archunit.core.domain.JavaField; 8 | import com.tngtech.archunit.lang.ArchCondition; 9 | import com.tngtech.archunit.lang.ConditionEvents; 10 | import com.tngtech.archunit.lang.SimpleConditionEvent; 11 | 12 | final class SerialVersionUID { 13 | 14 | private SerialVersionUID() { 15 | } 16 | 17 | static ArchCondition beStaticFinalLong() { 18 | return new ArchCondition<>("be static final long") { 19 | @Override 20 | public void check(JavaField javaField, ConditionEvents events) { 21 | if (!isFieldStatic(javaField) || !isFieldFinal(javaField) || !isLong(javaField)) { 22 | events.add(SimpleConditionEvent.violated(javaField, 23 | "Field %s in class %s is not static final long".formatted( 24 | javaField.getName(), 25 | javaField.getOwner().getName()))); 26 | } 27 | } 28 | 29 | private static boolean isLong(JavaField javaField) { 30 | return javaField.getRawType().isEquivalentTo(long.class); 31 | } 32 | }; 33 | } 34 | 35 | static DescribedPredicate namedSerialVersionUID() { 36 | return new DescribedPredicate<>("named serialVersionUID") { 37 | @Override 38 | public boolean test(JavaField javaField) { 39 | return "serialVersionUID".equals(javaField.getName()); 40 | } 41 | }; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/com/enofex/taikai/java/UtilityClasses.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai.java; 2 | 3 | import static com.enofex.taikai.internal.Modifiers.isMethodStatic; 4 | import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes; 5 | 6 | import com.enofex.taikai.internal.Modifiers; 7 | import com.tngtech.archunit.base.DescribedPredicate; 8 | import com.tngtech.archunit.core.domain.JavaClass; 9 | import com.tngtech.archunit.lang.ArchCondition; 10 | import com.tngtech.archunit.lang.ConditionEvents; 11 | import com.tngtech.archunit.lang.SimpleConditionEvent; 12 | import com.tngtech.archunit.lang.syntax.elements.GivenClassesConjunction; 13 | 14 | final class UtilityClasses { 15 | 16 | private UtilityClasses() { 17 | } 18 | 19 | static GivenClassesConjunction utilityClasses() { 20 | return classes().that(haveOnlyStaticMethods()); 21 | } 22 | 23 | private static DescribedPredicate haveOnlyStaticMethods() { 24 | return new DescribedPredicate<>("have only static methods") { 25 | @Override 26 | public boolean test(JavaClass javaClass) { 27 | return !javaClass.getMethods().isEmpty() && javaClass.getMethods().stream() 28 | .allMatch(method -> isMethodStatic(method) && !"main".equals(method.getName())); 29 | } 30 | }; 31 | } 32 | 33 | static ArchCondition havePrivateConstructor() { 34 | return new ArchCondition<>("have a private constructor") { 35 | @Override 36 | public void check(JavaClass javaClass, ConditionEvents events) { 37 | if (hasNoPrivateConstructor(javaClass)) { 38 | events.add(SimpleConditionEvent.violated(javaClass, 39 | "Class %s does not have a private constructor".formatted( 40 | javaClass.getName()))); 41 | } 42 | } 43 | 44 | private static boolean hasNoPrivateConstructor(JavaClass javaClass) { 45 | return javaClass.getConstructors().stream().noneMatch(Modifiers::isConstructorPrivate); 46 | } 47 | }; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/com/enofex/taikai/logging/LoggerConventions.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai.logging; 2 | 3 | import com.tngtech.archunit.core.domain.JavaClass; 4 | import com.tngtech.archunit.core.domain.JavaField; 5 | import com.tngtech.archunit.core.domain.JavaModifier; 6 | import com.tngtech.archunit.lang.ArchCondition; 7 | import com.tngtech.archunit.lang.ConditionEvents; 8 | import com.tngtech.archunit.lang.SimpleConditionEvent; 9 | import java.util.Collection; 10 | 11 | final class LoggerConventions { 12 | 13 | private LoggerConventions() { 14 | } 15 | 16 | static ArchCondition followLoggerConventions(String typeName, String regex, 17 | Collection requiredModifiers) { 18 | return new ArchCondition<>( 19 | "have a logger field of type %s with name pattern %s and modifiers %s".formatted( 20 | typeName, regex, requiredModifiers)) { 21 | @Override 22 | public void check(JavaClass javaClass, ConditionEvents events) { 23 | for (JavaField field : javaClass.getAllFields()) { 24 | if (field.getRawType().isAssignableTo(typeName)) { 25 | if (!field.getName().matches(regex)) { 26 | events.add(SimpleConditionEvent.violated(field, 27 | "Field '%s' in class %s does not match the naming pattern '%s'".formatted( 28 | field.getName(), 29 | javaClass.getName(), regex))); 30 | } 31 | 32 | if (!field.getModifiers().containsAll(requiredModifiers)) { 33 | events.add(SimpleConditionEvent.violated(field, 34 | "Field '%s' in class %s does not have the required modifiers %s".formatted( 35 | field.getName(), 36 | javaClass.getName(), 37 | requiredModifiers))); 38 | } 39 | } 40 | } 41 | } 42 | }; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/com/enofex/taikai/logging/LoggingConfigurer.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai.logging; 2 | 3 | import static com.enofex.taikai.TaikaiRule.Configuration.defaultConfiguration; 4 | import static com.enofex.taikai.internal.ArchConditions.haveFieldOfType; 5 | import static com.enofex.taikai.logging.LoggerConventions.followLoggerConventions; 6 | import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes; 7 | 8 | import com.enofex.taikai.TaikaiRule; 9 | import com.enofex.taikai.TaikaiRule.Configuration; 10 | import com.enofex.taikai.configures.AbstractConfigurer; 11 | import com.enofex.taikai.configures.ConfigurerContext; 12 | import com.enofex.taikai.configures.DisableableConfigurer; 13 | import com.tngtech.archunit.core.domain.JavaModifier; 14 | import java.util.Collection; 15 | import java.util.List; 16 | 17 | public class LoggingConfigurer extends AbstractConfigurer { 18 | 19 | public LoggingConfigurer(ConfigurerContext configurerContext) { 20 | super(configurerContext); 21 | } 22 | 23 | public LoggingConfigurer classesShouldUseLogger(String typeName, String regex) { 24 | return classesShouldUseLogger(typeName, regex, defaultConfiguration()); 25 | } 26 | 27 | public LoggingConfigurer classesShouldUseLogger(Class clazz, String regex) { 28 | return classesShouldUseLogger(clazz.getName(), regex, defaultConfiguration()); 29 | } 30 | 31 | public LoggingConfigurer classesShouldUseLogger(Class clazz, String regex, 32 | Configuration configuration) { 33 | return classesShouldUseLogger(clazz.getName(), regex, configuration); 34 | } 35 | 36 | public LoggingConfigurer classesShouldUseLogger(String typeName, String regex, 37 | Configuration configuration) { 38 | return addRule(TaikaiRule.of(classes() 39 | .that().haveNameMatching(regex) 40 | .should(haveFieldOfType(typeName)) 41 | .as("Classes with names matching %s should use a logger of type %s".formatted(regex, 42 | typeName)), 43 | configuration)); 44 | } 45 | 46 | public LoggingConfigurer loggersShouldFollowConventions(String typeName, String regex) { 47 | return loggersShouldFollowConventions(typeName, regex, List.of(), 48 | defaultConfiguration()); 49 | } 50 | 51 | public LoggingConfigurer loggersShouldFollowConventions(String typeName, String regex, 52 | Configuration configuration) { 53 | return loggersShouldFollowConventions(typeName, regex, List.of(), 54 | configuration); 55 | } 56 | 57 | public LoggingConfigurer loggersShouldFollowConventions(Class clazz, String regex) { 58 | return loggersShouldFollowConventions(clazz.getName(), regex, List.of(), 59 | defaultConfiguration()); 60 | } 61 | 62 | public LoggingConfigurer loggersShouldFollowConventions(Class clazz, String regex, 63 | Configuration configuration) { 64 | return loggersShouldFollowConventions(clazz.getName(), regex, List.of(), 65 | configuration); 66 | } 67 | 68 | public LoggingConfigurer loggersShouldFollowConventions(String typeName, String regex, 69 | Collection requiredModifiers) { 70 | return loggersShouldFollowConventions(typeName, regex, requiredModifiers, 71 | defaultConfiguration()); 72 | } 73 | 74 | public LoggingConfigurer loggersShouldFollowConventions(Class clazz, String regex, 75 | Collection requiredModifiers) { 76 | return loggersShouldFollowConventions(clazz.getName(), regex, requiredModifiers, 77 | defaultConfiguration()); 78 | } 79 | 80 | 81 | public LoggingConfigurer loggersShouldFollowConventions(Class clazz, String regex, 82 | Collection requiredModifiers, Configuration configuration) { 83 | return loggersShouldFollowConventions(clazz.getName(), regex, requiredModifiers, 84 | configuration); 85 | } 86 | 87 | public LoggingConfigurer loggersShouldFollowConventions(String typeName, String regex, 88 | Collection requiredModifiers, Configuration configuration) { 89 | return addRule(TaikaiRule.of(classes() 90 | .should(followLoggerConventions(typeName, regex, requiredModifiers)) 91 | .as("Loggers in classes matching %s should follow conventions and be of type %s with required modifiers %s".formatted( 92 | regex, typeName, requiredModifiers)), 93 | configuration)); 94 | } 95 | 96 | public static final class Disableable extends LoggingConfigurer implements DisableableConfigurer { 97 | 98 | public Disableable(ConfigurerContext configurerContext) { 99 | super(configurerContext); 100 | } 101 | 102 | @Override 103 | public LoggingConfigurer disable() { 104 | disable(LoggingConfigurer.class); 105 | 106 | return this; 107 | } 108 | } 109 | } -------------------------------------------------------------------------------- /src/main/java/com/enofex/taikai/spring/BootConfigurer.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai.spring; 2 | 3 | import static com.enofex.taikai.TaikaiRule.Configuration.defaultConfiguration; 4 | import static com.enofex.taikai.spring.SpringDescribedPredicates.ANNOTATION_SPRING_BOOT_APPLICATION; 5 | import static com.enofex.taikai.spring.SpringDescribedPredicates.annotatedWithSpringBootApplication; 6 | import static com.tngtech.archunit.lang.conditions.ArchPredicates.are; 7 | import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes; 8 | import static java.util.Objects.requireNonNull; 9 | 10 | import com.enofex.taikai.TaikaiRule; 11 | import com.enofex.taikai.TaikaiRule.Configuration; 12 | import com.enofex.taikai.configures.AbstractConfigurer; 13 | import com.enofex.taikai.configures.ConfigurerContext; 14 | import com.enofex.taikai.configures.DisableableConfigurer; 15 | 16 | public class BootConfigurer extends AbstractConfigurer { 17 | 18 | BootConfigurer(ConfigurerContext configurerContext) { 19 | super(configurerContext); 20 | } 21 | 22 | public BootConfigurer springBootApplicationShouldBeIn(String packageIdentifier) { 23 | requireNonNull(packageIdentifier); 24 | 25 | return springBootApplicationShouldBeIn(packageIdentifier, defaultConfiguration()); 26 | } 27 | 28 | public BootConfigurer springBootApplicationShouldBeIn(String packageIdentifier, Configuration configuration) { 29 | return addRule(TaikaiRule.of(classes() 30 | .that(are(annotatedWithSpringBootApplication(true))) 31 | .should().resideInAPackage(packageIdentifier) 32 | .allowEmptyShould(false) 33 | .as("Classes annotated with %s should be located in %s".formatted( 34 | ANNOTATION_SPRING_BOOT_APPLICATION, packageIdentifier)), configuration)); 35 | } 36 | 37 | public static final class Disableable extends BootConfigurer implements DisableableConfigurer { 38 | 39 | public Disableable(ConfigurerContext configurerContext) { 40 | super(configurerContext); 41 | } 42 | 43 | @Override 44 | public BootConfigurer disable() { 45 | disable(BootConfigurer.class); 46 | 47 | return this; 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/com/enofex/taikai/spring/ConfigurationsConfigurer.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai.spring; 2 | 3 | import static com.enofex.taikai.TaikaiRule.Configuration.defaultConfiguration; 4 | import static com.enofex.taikai.spring.SpringDescribedPredicates.annotatedWithConfiguration; 5 | import static com.enofex.taikai.spring.SpringDescribedPredicates.annotatedWithSpringBootApplication; 6 | import static com.tngtech.archunit.base.DescribedPredicate.not; 7 | import static com.tngtech.archunit.lang.conditions.ArchPredicates.are; 8 | import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes; 9 | 10 | import com.enofex.taikai.TaikaiRule; 11 | import com.enofex.taikai.TaikaiRule.Configuration; 12 | import com.enofex.taikai.configures.AbstractConfigurer; 13 | import com.enofex.taikai.configures.ConfigurerContext; 14 | import com.enofex.taikai.configures.DisableableConfigurer; 15 | 16 | public class ConfigurationsConfigurer extends AbstractConfigurer { 17 | 18 | private static final String DEFAULT_CONFIGURATION_NAME_MATCHING = ".+Configuration"; 19 | 20 | ConfigurationsConfigurer(ConfigurerContext configurerContext) { 21 | super(configurerContext); 22 | } 23 | 24 | public ConfigurationsConfigurer namesShouldEndWithConfiguration() { 25 | return namesShouldMatch(DEFAULT_CONFIGURATION_NAME_MATCHING, defaultConfiguration()); 26 | } 27 | 28 | public ConfigurationsConfigurer namesShouldEndWithConfiguration(Configuration configuration) { 29 | return namesShouldMatch(DEFAULT_CONFIGURATION_NAME_MATCHING, configuration); 30 | } 31 | 32 | public ConfigurationsConfigurer namesShouldMatch(String regex) { 33 | return namesShouldMatch(regex, defaultConfiguration()); 34 | } 35 | 36 | public ConfigurationsConfigurer namesShouldMatch(String regex, Configuration configuration) { 37 | return addRule(TaikaiRule.of(classes() 38 | .that(are(annotatedWithConfiguration(true) 39 | .and(not(annotatedWithSpringBootApplication(true)))) 40 | ) 41 | .should().haveNameMatching(regex) 42 | .as("Configurations should have name ending %s".formatted(regex)), configuration)); 43 | } 44 | 45 | public static final class Disableable extends ConfigurationsConfigurer implements 46 | DisableableConfigurer { 47 | 48 | public Disableable(ConfigurerContext configurerContext) { 49 | super(configurerContext); 50 | } 51 | 52 | @Override 53 | public ConfigurationsConfigurer disable() { 54 | disable(ConfigurationsConfigurer.class); 55 | 56 | return this; 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/com/enofex/taikai/spring/ControllersConfigurer.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai.spring; 2 | 3 | import static com.enofex.taikai.TaikaiRule.Configuration.defaultConfiguration; 4 | import static com.enofex.taikai.spring.ValidatedController.beAnnotatedWithValidated; 5 | import static com.enofex.taikai.spring.SpringDescribedPredicates.ANNOTATION_CONTROLLER; 6 | import static com.enofex.taikai.spring.SpringDescribedPredicates.ANNOTATION_REST_CONTROLLER; 7 | import static com.enofex.taikai.spring.SpringDescribedPredicates.ANNOTATION_VALIDATED; 8 | import static com.enofex.taikai.spring.SpringDescribedPredicates.annotatedWithController; 9 | import static com.enofex.taikai.spring.SpringDescribedPredicates.annotatedWithControllerOrRestController; 10 | import static com.enofex.taikai.spring.SpringDescribedPredicates.annotatedWithRestController; 11 | import static com.tngtech.archunit.lang.conditions.ArchConditions.be; 12 | import static com.tngtech.archunit.lang.conditions.ArchConditions.dependOnClassesThat; 13 | import static com.tngtech.archunit.lang.conditions.ArchConditions.not; 14 | import static com.tngtech.archunit.lang.conditions.ArchPredicates.are; 15 | import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes; 16 | 17 | import com.enofex.taikai.TaikaiRule; 18 | import com.enofex.taikai.TaikaiRule.Configuration; 19 | import com.enofex.taikai.configures.AbstractConfigurer; 20 | import com.enofex.taikai.configures.ConfigurerContext; 21 | import com.enofex.taikai.configures.DisableableConfigurer; 22 | 23 | public class ControllersConfigurer extends AbstractConfigurer { 24 | 25 | private static final String DEFAULT_CONTROLLER_NAME_MATCHING = ".+Controller"; 26 | 27 | ControllersConfigurer(ConfigurerContext configurerContext) { 28 | super(configurerContext); 29 | } 30 | 31 | public ControllersConfigurer namesShouldEndWithController() { 32 | return namesShouldMatch(DEFAULT_CONTROLLER_NAME_MATCHING, defaultConfiguration()); 33 | } 34 | 35 | public ControllersConfigurer namesShouldEndWithController(Configuration configuration) { 36 | return namesShouldMatch(DEFAULT_CONTROLLER_NAME_MATCHING, configuration); 37 | } 38 | 39 | public ControllersConfigurer namesShouldMatch(String regex) { 40 | return namesShouldMatch(regex, defaultConfiguration()); 41 | } 42 | 43 | public ControllersConfigurer namesShouldMatch(String regex, Configuration configuration) { 44 | return addRule(TaikaiRule.of(classes() 45 | .that(are(annotatedWithControllerOrRestController(true))) 46 | .should().haveNameMatching(regex) 47 | .as("Controllers should have name ending %s".formatted(regex)), configuration)); 48 | } 49 | 50 | public ControllersConfigurer shouldBeAnnotatedWithRestController() { 51 | return shouldBeAnnotatedWithRestController(DEFAULT_CONTROLLER_NAME_MATCHING, 52 | defaultConfiguration()); 53 | } 54 | 55 | public ControllersConfigurer shouldBeAnnotatedWithRestController(Configuration configuration) { 56 | return shouldBeAnnotatedWithRestController(DEFAULT_CONTROLLER_NAME_MATCHING, configuration); 57 | } 58 | 59 | public ControllersConfigurer shouldBeAnnotatedWithRestController(String regex) { 60 | return shouldBeAnnotatedWithRestController(regex, defaultConfiguration()); 61 | } 62 | 63 | public ControllersConfigurer shouldBeAnnotatedWithRestController(String regex, 64 | Configuration configuration) { 65 | return addRule(TaikaiRule.of(classes() 66 | .that().haveNameMatching(regex) 67 | .should(be(annotatedWithRestController(true))) 68 | .as("Controllers should be annotated with %s".formatted(ANNOTATION_REST_CONTROLLER)), 69 | configuration)); 70 | } 71 | 72 | public ControllersConfigurer shouldBeAnnotatedWithController() { 73 | return shouldBeAnnotatedWithController(DEFAULT_CONTROLLER_NAME_MATCHING, 74 | defaultConfiguration()); 75 | } 76 | 77 | public ControllersConfigurer shouldBeAnnotatedWithController(Configuration configuration) { 78 | return shouldBeAnnotatedWithController(DEFAULT_CONTROLLER_NAME_MATCHING, configuration); 79 | } 80 | 81 | public ControllersConfigurer shouldBeAnnotatedWithController(String regex) { 82 | return shouldBeAnnotatedWithController(regex, defaultConfiguration()); 83 | } 84 | 85 | public ControllersConfigurer shouldBeAnnotatedWithController(String regex, 86 | Configuration configuration) { 87 | return addRule(TaikaiRule.of(classes() 88 | .that().haveNameMatching(regex) 89 | .should(be(annotatedWithController(true))) 90 | .as("Controllers should be annotated with %s".formatted(ANNOTATION_CONTROLLER)), 91 | configuration)); 92 | } 93 | 94 | public ControllersConfigurer shouldBePackagePrivate() { 95 | return shouldBePackagePrivate(defaultConfiguration()); 96 | } 97 | 98 | public ControllersConfigurer shouldBePackagePrivate(Configuration configuration) { 99 | return addRule(TaikaiRule.of(classes() 100 | .that(are(annotatedWithControllerOrRestController(true))) 101 | .should().bePackagePrivate() 102 | .as("Controllers should be package-private"), configuration)); 103 | } 104 | 105 | public ControllersConfigurer shouldNotDependOnOtherControllers() { 106 | return shouldNotDependOnOtherControllers(defaultConfiguration()); 107 | } 108 | 109 | public ControllersConfigurer shouldNotDependOnOtherControllers(Configuration configuration) { 110 | return addRule(TaikaiRule.of(classes() 111 | .that(are(annotatedWithControllerOrRestController(true))) 112 | .should(not(dependOnClassesThat(are(annotatedWithControllerOrRestController(true))))) 113 | .as("Controllers should not be depend on other Controllers"), configuration)); 114 | } 115 | 116 | public ControllersConfigurer shouldBeAnnotatedWithValidated(String regex) { 117 | return shouldBeAnnotatedWithValidated(regex, defaultConfiguration()); 118 | } 119 | 120 | public ControllersConfigurer shouldBeAnnotatedWithValidated(String regex, Configuration configuration) { 121 | return addRule(TaikaiRule.of(classes() 122 | .that().haveNameMatching(regex) 123 | .should(beAnnotatedWithValidated()) 124 | .as("Validation annotations on @RequestParam or @PathVariable require the controller to be annotated with %s." 125 | .formatted(ANNOTATION_VALIDATED)), 126 | configuration)); 127 | } 128 | 129 | public ControllersConfigurer shouldBeAnnotatedWithValidated() { 130 | return shouldBeAnnotatedWithValidated(defaultConfiguration()); 131 | } 132 | 133 | public ControllersConfigurer shouldBeAnnotatedWithValidated(Configuration configuration) { 134 | return addRule(TaikaiRule.of(classes() 135 | .that(are(annotatedWithControllerOrRestController(true))) 136 | .should(beAnnotatedWithValidated()) 137 | .as("Validation annotations on @RequestParam or @PathVariable require the controller to be annotated with %s." 138 | .formatted(ANNOTATION_VALIDATED)), 139 | configuration)); 140 | } 141 | 142 | public static final class Disableable extends ControllersConfigurer implements 143 | DisableableConfigurer { 144 | 145 | public Disableable(ConfigurerContext configurerContext) { 146 | super(configurerContext); 147 | } 148 | 149 | @Override 150 | public ControllersConfigurer disable() { 151 | disable(ControllersConfigurer.class); 152 | 153 | return this; 154 | } 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/main/java/com/enofex/taikai/spring/PropertiesConfigurer.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai.spring; 2 | 3 | import static com.enofex.taikai.TaikaiRule.Configuration.defaultConfiguration; 4 | import static com.enofex.taikai.spring.SpringDescribedPredicates.ANNOTATION_CONFIGURATION_PROPERTIES; 5 | import static com.enofex.taikai.spring.SpringDescribedPredicates.ANNOTATION_VALIDATED; 6 | import static com.enofex.taikai.spring.SpringDescribedPredicates.annotatedWithConfigurationProperties; 7 | import static com.tngtech.archunit.lang.conditions.ArchConditions.be; 8 | import static com.tngtech.archunit.lang.conditions.ArchPredicates.are; 9 | import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes; 10 | 11 | import com.enofex.taikai.TaikaiRule; 12 | import com.enofex.taikai.TaikaiRule.Configuration; 13 | import com.enofex.taikai.configures.AbstractConfigurer; 14 | import com.enofex.taikai.configures.ConfigurerContext; 15 | import com.enofex.taikai.configures.DisableableConfigurer; 16 | 17 | public class PropertiesConfigurer extends AbstractConfigurer { 18 | 19 | private static final String DEFAULT_PROPERTIES_NAME_MATCHING = ".+Properties"; 20 | 21 | PropertiesConfigurer(ConfigurerContext configurerContext) { 22 | super(configurerContext); 23 | } 24 | 25 | public PropertiesConfigurer namesShouldEndWithProperties() { 26 | return namesShouldMatch(DEFAULT_PROPERTIES_NAME_MATCHING, defaultConfiguration()); 27 | } 28 | 29 | public PropertiesConfigurer namesShouldEndWithProperties(Configuration configuration) { 30 | return namesShouldMatch(DEFAULT_PROPERTIES_NAME_MATCHING, configuration); 31 | } 32 | 33 | public PropertiesConfigurer namesShouldMatch(String regex) { 34 | return namesShouldMatch(regex, defaultConfiguration()); 35 | } 36 | 37 | public PropertiesConfigurer namesShouldMatch(String regex, Configuration configuration) { 38 | return addRule(TaikaiRule.of(classes() 39 | .that(are(annotatedWithConfigurationProperties(true))) 40 | .should().haveNameMatching(regex) 41 | .as("Properties should have name ending %s".formatted(regex)), configuration)); 42 | } 43 | 44 | public PropertiesConfigurer shouldBeAnnotatedWithValidated() { 45 | return shouldBeAnnotatedWithValidated(defaultConfiguration()); 46 | } 47 | 48 | public PropertiesConfigurer shouldBeAnnotatedWithValidated(Configuration configuration) { 49 | return addRule(TaikaiRule.of(classes() 50 | .that(are(annotatedWithConfigurationProperties(true))) 51 | .should().beMetaAnnotatedWith(ANNOTATION_VALIDATED) 52 | .as("Configuration properties annotated with %s should be annotated with %s as well".formatted( 53 | ANNOTATION_CONFIGURATION_PROPERTIES, ANNOTATION_VALIDATED)), 54 | configuration)); 55 | } 56 | 57 | public PropertiesConfigurer shouldBeAnnotatedWithConfigurationProperties() { 58 | return shouldBeAnnotatedWithConfigurationProperties(DEFAULT_PROPERTIES_NAME_MATCHING, 59 | defaultConfiguration()); 60 | } 61 | 62 | public PropertiesConfigurer shouldBeAnnotatedWithConfigurationProperties( 63 | Configuration configuration) { 64 | return shouldBeAnnotatedWithConfigurationProperties(DEFAULT_PROPERTIES_NAME_MATCHING, 65 | configuration); 66 | } 67 | 68 | public PropertiesConfigurer shouldBeAnnotatedWithConfigurationProperties(String regex) { 69 | return shouldBeAnnotatedWithConfigurationProperties(regex, defaultConfiguration()); 70 | } 71 | 72 | public PropertiesConfigurer shouldBeAnnotatedWithConfigurationProperties(String regex, 73 | Configuration configuration) { 74 | return addRule(TaikaiRule.of(classes() 75 | .that().haveNameMatching(regex) 76 | .should(be(annotatedWithConfigurationProperties(true))) 77 | .as("Configuration properties should be annotated with %s".formatted( 78 | ANNOTATION_CONFIGURATION_PROPERTIES)), 79 | configuration)); 80 | } 81 | 82 | public static final class Disableable extends PropertiesConfigurer implements 83 | DisableableConfigurer { 84 | 85 | public Disableable(ConfigurerContext configurerContext) { 86 | super(configurerContext); 87 | } 88 | 89 | @Override 90 | public PropertiesConfigurer disable() { 91 | disable(PropertiesConfigurer.class); 92 | 93 | return this; 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/main/java/com/enofex/taikai/spring/RepositoriesConfigurer.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai.spring; 2 | 3 | import static com.enofex.taikai.TaikaiRule.Configuration.defaultConfiguration; 4 | import static com.enofex.taikai.spring.SpringDescribedPredicates.ANNOTATION_REPOSITORY; 5 | import static com.enofex.taikai.spring.SpringDescribedPredicates.annotatedWithRepository; 6 | import static com.enofex.taikai.spring.SpringDescribedPredicates.annotatedWithService; 7 | import static com.tngtech.archunit.lang.conditions.ArchConditions.be; 8 | import static com.tngtech.archunit.lang.conditions.ArchConditions.dependOnClassesThat; 9 | import static com.tngtech.archunit.lang.conditions.ArchConditions.not; 10 | import static com.tngtech.archunit.lang.conditions.ArchPredicates.are; 11 | import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes; 12 | 13 | import com.enofex.taikai.TaikaiRule; 14 | import com.enofex.taikai.TaikaiRule.Configuration; 15 | import com.enofex.taikai.configures.AbstractConfigurer; 16 | import com.enofex.taikai.configures.ConfigurerContext; 17 | import com.enofex.taikai.configures.DisableableConfigurer; 18 | 19 | public class RepositoriesConfigurer extends AbstractConfigurer { 20 | 21 | private static final String DEFAULT_REPOSITORY_NAME_MATCHING = ".+Repository"; 22 | 23 | RepositoriesConfigurer(ConfigurerContext configurerContext) { 24 | super(configurerContext); 25 | } 26 | 27 | public RepositoriesConfigurer namesShouldEndWithRepository() { 28 | return namesShouldMatch(DEFAULT_REPOSITORY_NAME_MATCHING, defaultConfiguration()); 29 | } 30 | 31 | public RepositoriesConfigurer namesShouldEndWithRepository(Configuration configuration) { 32 | return namesShouldMatch(DEFAULT_REPOSITORY_NAME_MATCHING, configuration); 33 | } 34 | 35 | public RepositoriesConfigurer namesShouldMatch(String regex) { 36 | return namesShouldMatch(regex, defaultConfiguration()); 37 | } 38 | 39 | public RepositoriesConfigurer namesShouldMatch(String regex, Configuration configuration) { 40 | return addRule(TaikaiRule.of(classes() 41 | .that(are(annotatedWithRepository(true))) 42 | .should().haveNameMatching(regex) 43 | .as("Repositories should have name ending %s".formatted(regex)), configuration)); 44 | } 45 | 46 | public RepositoriesConfigurer shouldBeAnnotatedWithRepository() { 47 | return shouldBeAnnotatedWithRepository(DEFAULT_REPOSITORY_NAME_MATCHING, 48 | defaultConfiguration()); 49 | } 50 | 51 | public RepositoriesConfigurer shouldBeAnnotatedWithRepository(Configuration configuration) { 52 | return shouldBeAnnotatedWithRepository(DEFAULT_REPOSITORY_NAME_MATCHING, configuration); 53 | } 54 | 55 | public RepositoriesConfigurer shouldBeAnnotatedWithRepository(String regex) { 56 | return shouldBeAnnotatedWithRepository(regex, defaultConfiguration()); 57 | } 58 | 59 | public RepositoriesConfigurer shouldBeAnnotatedWithRepository(String regex, 60 | Configuration configuration) { 61 | return addRule(TaikaiRule.of(classes() 62 | .that().haveNameMatching(regex) 63 | .should(be(annotatedWithRepository(true))) 64 | .as("Repositories should be annotated with %s".formatted(ANNOTATION_REPOSITORY)), 65 | configuration)); 66 | } 67 | 68 | public RepositoriesConfigurer shouldNotDependOnServices() { 69 | return shouldNotDependOnServices(defaultConfiguration()); 70 | } 71 | 72 | public RepositoriesConfigurer shouldNotDependOnServices(Configuration configuration) { 73 | return addRule(TaikaiRule.of(classes() 74 | .that(are(annotatedWithRepository(true))) 75 | .should(not(dependOnClassesThat(annotatedWithService(true)))) 76 | .as("Repositories should not depend on Services"), 77 | configuration)); 78 | } 79 | 80 | public static final class Disableable extends RepositoriesConfigurer implements 81 | DisableableConfigurer { 82 | 83 | public Disableable(ConfigurerContext configurerContext) { 84 | super(configurerContext); 85 | } 86 | 87 | @Override 88 | public RepositoriesConfigurer disable() { 89 | disable(RepositoriesConfigurer.class); 90 | 91 | return this; 92 | } 93 | } 94 | } 95 | 96 | -------------------------------------------------------------------------------- /src/main/java/com/enofex/taikai/spring/ServicesConfigurer.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai.spring; 2 | 3 | import static com.enofex.taikai.TaikaiRule.Configuration.defaultConfiguration; 4 | import static com.enofex.taikai.spring.SpringDescribedPredicates.ANNOTATION_SERVICE; 5 | import static com.enofex.taikai.spring.SpringDescribedPredicates.annotatedWithControllerOrRestController; 6 | import static com.enofex.taikai.spring.SpringDescribedPredicates.annotatedWithService; 7 | import static com.tngtech.archunit.lang.conditions.ArchConditions.be; 8 | import static com.tngtech.archunit.lang.conditions.ArchConditions.dependOnClassesThat; 9 | import static com.tngtech.archunit.lang.conditions.ArchConditions.not; 10 | import static com.tngtech.archunit.lang.conditions.ArchPredicates.are; 11 | import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes; 12 | 13 | import com.enofex.taikai.TaikaiRule; 14 | import com.enofex.taikai.TaikaiRule.Configuration; 15 | import com.enofex.taikai.configures.AbstractConfigurer; 16 | import com.enofex.taikai.configures.ConfigurerContext; 17 | import com.enofex.taikai.configures.DisableableConfigurer; 18 | 19 | public class ServicesConfigurer extends AbstractConfigurer { 20 | 21 | private static final String DEFAULT_SERVICE_NAME_MATCHING = ".+Service"; 22 | 23 | ServicesConfigurer(ConfigurerContext configurerContext) { 24 | super(configurerContext); 25 | } 26 | 27 | public ServicesConfigurer namesShouldEndWithService() { 28 | return namesShouldMatch(DEFAULT_SERVICE_NAME_MATCHING, defaultConfiguration()); 29 | } 30 | 31 | public ServicesConfigurer namesShouldEndWithService(Configuration configuration) { 32 | return namesShouldMatch(DEFAULT_SERVICE_NAME_MATCHING, configuration); 33 | } 34 | 35 | public ServicesConfigurer namesShouldMatch(String regex) { 36 | return namesShouldMatch(regex, defaultConfiguration()); 37 | } 38 | 39 | public ServicesConfigurer namesShouldMatch(String regex, Configuration configuration) { 40 | return addRule(TaikaiRule.of(classes() 41 | .that(are(annotatedWithService(true))) 42 | .should().haveNameMatching(regex) 43 | .as("Services should have name ending %s".formatted(regex)), configuration)); 44 | } 45 | 46 | public ServicesConfigurer shouldBeAnnotatedWithService() { 47 | return shouldBeAnnotatedWithService(DEFAULT_SERVICE_NAME_MATCHING, defaultConfiguration()); 48 | } 49 | 50 | public ServicesConfigurer shouldBeAnnotatedWithService(Configuration configuration) { 51 | return shouldBeAnnotatedWithService(DEFAULT_SERVICE_NAME_MATCHING, configuration); 52 | } 53 | 54 | public ServicesConfigurer shouldBeAnnotatedWithService(String regex) { 55 | return shouldBeAnnotatedWithService(regex, defaultConfiguration()); 56 | } 57 | 58 | public ServicesConfigurer shouldBeAnnotatedWithService(String regex, 59 | Configuration configuration) { 60 | return addRule(TaikaiRule.of(classes() 61 | .that().haveNameMatching(regex) 62 | .should(be(annotatedWithService(true))) 63 | .as("Services should be annotated with %s".formatted(ANNOTATION_SERVICE)), 64 | configuration)); 65 | } 66 | 67 | public ServicesConfigurer shouldNotDependOnControllers() { 68 | return shouldNotDependOnControllers(defaultConfiguration()); 69 | } 70 | 71 | public ServicesConfigurer shouldNotDependOnControllers(Configuration configuration) { 72 | return addRule(TaikaiRule.of(classes() 73 | .that(are(annotatedWithService(true))) 74 | .should(not(dependOnClassesThat(annotatedWithControllerOrRestController(true)))) 75 | .as("Services should not depend on Controllers or RestControllers"), 76 | configuration)); 77 | } 78 | 79 | public static final class Disableable extends ServicesConfigurer implements 80 | DisableableConfigurer { 81 | 82 | public Disableable(ConfigurerContext configurerContext) { 83 | super(configurerContext); 84 | } 85 | 86 | @Override 87 | public ServicesConfigurer disable() { 88 | disable(ServicesConfigurer.class); 89 | 90 | return this; 91 | } 92 | } 93 | } 94 | 95 | -------------------------------------------------------------------------------- /src/main/java/com/enofex/taikai/spring/SpringConfigurer.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai.spring; 2 | 3 | import static com.enofex.taikai.TaikaiRule.Configuration.defaultConfiguration; 4 | import static com.enofex.taikai.spring.SpringDescribedPredicates.ANNOTATION_AUTOWIRED; 5 | import static com.enofex.taikai.spring.SpringDescribedPredicates.annotatedWithAutowired; 6 | import static com.tngtech.archunit.lang.conditions.ArchConditions.be; 7 | import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noFields; 8 | 9 | import com.enofex.taikai.TaikaiRule; 10 | import com.enofex.taikai.TaikaiRule.Configuration; 11 | import com.enofex.taikai.configures.AbstractConfigurer; 12 | import com.enofex.taikai.configures.ConfigurerContext; 13 | import com.enofex.taikai.configures.Customizer; 14 | import com.enofex.taikai.configures.DisableableConfigurer; 15 | 16 | public class SpringConfigurer extends AbstractConfigurer { 17 | 18 | public SpringConfigurer(ConfigurerContext configurerContext) { 19 | super(configurerContext); 20 | } 21 | 22 | public Disableable properties(Customizer customizer) { 23 | return customizer(customizer, () -> new PropertiesConfigurer.Disableable(configurerContext())); 24 | } 25 | 26 | public Disableable configurations( 27 | Customizer customizer) { 28 | return customizer(customizer, () -> new ConfigurationsConfigurer.Disableable(configurerContext())); 29 | } 30 | 31 | public Disableable controllers( 32 | Customizer customizer) { 33 | return customizer(customizer, () -> new ControllersConfigurer.Disableable(configurerContext())); 34 | } 35 | 36 | public Disableable services(Customizer customizer) { 37 | return customizer(customizer, () -> new ServicesConfigurer.Disableable(configurerContext())); 38 | } 39 | 40 | public Disableable repositories( 41 | Customizer customizer) { 42 | return customizer(customizer, () -> new RepositoriesConfigurer.Disableable(configurerContext())); 43 | } 44 | 45 | public Disableable boot(Customizer customizer) { 46 | return customizer(customizer, () -> new BootConfigurer.Disableable(configurerContext())); 47 | } 48 | 49 | public SpringConfigurer noAutowiredFields() { 50 | return noAutowiredFields(defaultConfiguration()); 51 | } 52 | 53 | public SpringConfigurer noAutowiredFields(Configuration configuration) { 54 | return addRule(TaikaiRule.of(noFields() 55 | .should(be(annotatedWithAutowired(true))) 56 | .as("No fields should be annotated with %s, use constructor injection".formatted( 57 | ANNOTATION_AUTOWIRED)), configuration)); 58 | } 59 | 60 | public static final class Disableable extends SpringConfigurer implements DisableableConfigurer { 61 | 62 | public Disableable(ConfigurerContext configurerContext) { 63 | super(configurerContext); 64 | } 65 | 66 | @Override 67 | public SpringConfigurer disable() { 68 | disable(SpringConfigurer.class); 69 | disable(PropertiesConfigurer.class); 70 | disable(ConfigurationsConfigurer.class); 71 | disable(ControllersConfigurer.class); 72 | disable(ServicesConfigurer.class); 73 | disable(RepositoriesConfigurer.class); 74 | disable(BootConfigurer.class); 75 | 76 | return this; 77 | } 78 | } 79 | 80 | } -------------------------------------------------------------------------------- /src/main/java/com/enofex/taikai/spring/SpringDescribedPredicates.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai.spring; 2 | 3 | import static com.enofex.taikai.internal.DescribedPredicates.annotatedWith; 4 | 5 | import com.tngtech.archunit.base.DescribedPredicate; 6 | import com.tngtech.archunit.core.domain.properties.CanBeAnnotated; 7 | 8 | final class SpringDescribedPredicates { 9 | 10 | static final String ANNOTATION_CONFIGURATION = "org.springframework.context.annotation.Configuration"; 11 | static final String ANNOTATION_CONFIGURATION_PROPERTIES = "org.springframework.boot.context.properties.ConfigurationProperties"; 12 | static final String ANNOTATION_CONTROLLER = "org.springframework.stereotype.Controller"; 13 | static final String ANNOTATION_REST_CONTROLLER = "org.springframework.web.bind.annotation.RestController"; 14 | static final String ANNOTATION_SERVICE = "org.springframework.stereotype.Service"; 15 | static final String ANNOTATION_REPOSITORY = "org.springframework.stereotype.Repository"; 16 | static final String ANNOTATION_SPRING_BOOT_APPLICATION = "org.springframework.boot.autoconfigure.SpringBootApplication"; 17 | static final String ANNOTATION_AUTOWIRED = "org.springframework.beans.factory.annotation.Autowired"; 18 | static final String ANNOTATION_VALIDATED = "org.springframework.validation.annotation.Validated"; 19 | 20 | private SpringDescribedPredicates() { 21 | } 22 | 23 | static DescribedPredicate annotatedWithControllerOrRestController( 24 | boolean isMetaAnnotated) { 25 | 26 | return annotatedWith(ANNOTATION_CONTROLLER, isMetaAnnotated) 27 | .or(annotatedWith(ANNOTATION_REST_CONTROLLER, isMetaAnnotated)); 28 | } 29 | 30 | static DescribedPredicate annotatedWithConfiguration( 31 | boolean isMetaAnnotated) { 32 | return annotatedWith(ANNOTATION_CONFIGURATION, isMetaAnnotated); 33 | } 34 | 35 | static DescribedPredicate annotatedWithConfigurationProperties( 36 | boolean isMetaAnnotated) { 37 | return annotatedWith(ANNOTATION_CONFIGURATION_PROPERTIES, isMetaAnnotated); 38 | } 39 | 40 | static DescribedPredicate annotatedWithRestController(boolean isMetaAnnotated) { 41 | return annotatedWith(ANNOTATION_REST_CONTROLLER, isMetaAnnotated); 42 | } 43 | 44 | static DescribedPredicate annotatedWithController(boolean isMetaAnnotated) { 45 | return annotatedWith(ANNOTATION_CONTROLLER, isMetaAnnotated); 46 | } 47 | 48 | static DescribedPredicate annotatedWithService(boolean isMetaAnnotated) { 49 | return annotatedWith(ANNOTATION_SERVICE, isMetaAnnotated); 50 | } 51 | 52 | static DescribedPredicate annotatedWithRepository(boolean isMetaAnnotated) { 53 | return annotatedWith(ANNOTATION_REPOSITORY, isMetaAnnotated); 54 | } 55 | 56 | static DescribedPredicate annotatedWithSpringBootApplication( 57 | boolean isMetaAnnotated) { 58 | return annotatedWith(ANNOTATION_SPRING_BOOT_APPLICATION, isMetaAnnotated); 59 | } 60 | 61 | static DescribedPredicate annotatedWithAutowired(boolean isMetaAnnotated) { 62 | return annotatedWith(ANNOTATION_AUTOWIRED, isMetaAnnotated); 63 | } 64 | 65 | static DescribedPredicate annotatedWithValidated(boolean isMetaAnnotated) { 66 | return annotatedWith(ANNOTATION_VALIDATED, isMetaAnnotated); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/com/enofex/taikai/spring/ValidatedController.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai.spring; 2 | 3 | import static com.enofex.taikai.spring.SpringDescribedPredicates.ANNOTATION_VALIDATED; 4 | 5 | import com.tngtech.archunit.core.domain.JavaClass; 6 | import com.tngtech.archunit.core.domain.JavaMethod; 7 | import com.tngtech.archunit.core.domain.JavaParameter; 8 | import com.tngtech.archunit.lang.ArchCondition; 9 | import com.tngtech.archunit.lang.ConditionEvents; 10 | import com.tngtech.archunit.lang.SimpleConditionEvent; 11 | 12 | final class ValidatedController { 13 | 14 | private static final String REQUEST_PARAM = "org.springframework.web.bind.annotation.RequestParam"; 15 | private static final String PATH_VARIABLE = "org.springframework.web.bind.annotation.PathVariable"; 16 | 17 | private static final String JAVAX_VALIDATION_NOT_NULL = "javax.validation.constraints.NotNull"; 18 | private static final String JAVAX_VALIDATION_MIN = "javax.validation.constraints.Min"; 19 | private static final String JAVAX_VALIDATION_MAX = "javax.validation.constraints.Max"; 20 | private static final String JAVAX_VALIDATION_SIZE = "javax.validation.constraints.Size"; 21 | private static final String JAVAX_VALIDATION_NOT_BLANK = "javax.validation.constraints.NotBlank"; 22 | private static final String JAVAX_VALIDATION_PATTERN = "javax.validation.constraints.Pattern"; 23 | 24 | private static final String JAKARTA_VALIDATION_NOT_NULL = "jakarta.validation.constraints.NotNull"; 25 | private static final String JAKARTA_VALIDATION_MIN = "jakarta.validation.constraints.Min"; 26 | private static final String JAKARTA_VALIDATION_MAX = "jakarta.validation.constraints.Max"; 27 | private static final String JAKARTA_VALIDATION_SIZE = "jakarta.validation.constraints.Size"; 28 | private static final String JAKARTA_VALIDATION_NOT_BLANK = "jakarta.validation.constraints.NotBlank"; 29 | private static final String JAKARTA_VALIDATION_PATTERN = "jakarta.validation.constraints.Pattern"; 30 | 31 | private ValidatedController() { 32 | } 33 | 34 | static ArchCondition beAnnotatedWithValidated() { 35 | return new ArchCondition<>( 36 | "be annotated with @Validated if @RequestParam or @PathVariable has validation annotations") { 37 | @Override 38 | public void check(JavaClass controllerClass, ConditionEvents events) { 39 | boolean hasValidatedAnnotation = controllerClass.isMetaAnnotatedWith(ANNOTATION_VALIDATED); 40 | 41 | for (JavaMethod method : controllerClass.getMethods()) { 42 | for (JavaParameter parameter : method.getParameters()) { 43 | if ((parameter.isMetaAnnotatedWith(REQUEST_PARAM) 44 | || parameter.isMetaAnnotatedWith(PATH_VARIABLE)) 45 | && (hasJavaXValidationAnnotations(parameter) 46 | || hasJakartaValidationAnnotations(parameter)) 47 | ) { 48 | 49 | if (!hasValidatedAnnotation) { 50 | events.add(SimpleConditionEvent.violated(controllerClass, 51 | "Controller %s is missing @Validated but has a method parameter in %s annotated with @PathVariable or @RequestParam and a validation annotation.".formatted( 52 | controllerClass.getName(), 53 | method.getFullName()))); 54 | } 55 | } 56 | } 57 | } 58 | } 59 | 60 | private boolean hasJavaXValidationAnnotations(JavaParameter parameter) { 61 | return parameter.isMetaAnnotatedWith(JAVAX_VALIDATION_NOT_NULL) 62 | || parameter.isMetaAnnotatedWith(JAVAX_VALIDATION_MIN) 63 | || parameter.isMetaAnnotatedWith(JAVAX_VALIDATION_MAX) 64 | || parameter.isMetaAnnotatedWith(JAVAX_VALIDATION_SIZE) 65 | || parameter.isMetaAnnotatedWith(JAVAX_VALIDATION_NOT_BLANK) 66 | || parameter.isMetaAnnotatedWith(JAVAX_VALIDATION_PATTERN); 67 | } 68 | 69 | private boolean hasJakartaValidationAnnotations(JavaParameter parameter) { 70 | return parameter.isMetaAnnotatedWith(JAKARTA_VALIDATION_NOT_NULL) 71 | || parameter.isMetaAnnotatedWith(JAKARTA_VALIDATION_MIN) 72 | || parameter.isMetaAnnotatedWith(JAKARTA_VALIDATION_MAX) 73 | || parameter.isMetaAnnotatedWith(JAKARTA_VALIDATION_SIZE) 74 | || parameter.isMetaAnnotatedWith(JAKARTA_VALIDATION_NOT_BLANK) 75 | || parameter.isMetaAnnotatedWith(JAKARTA_VALIDATION_PATTERN); 76 | } 77 | }; 78 | } 79 | } 80 | 81 | -------------------------------------------------------------------------------- /src/main/java/com/enofex/taikai/test/ContainAssertionsOrVerifications.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai.test; 2 | 3 | import com.tngtech.archunit.core.domain.JavaMethod; 4 | import com.tngtech.archunit.core.domain.JavaMethodCall; 5 | import com.tngtech.archunit.lang.ArchCondition; 6 | import com.tngtech.archunit.lang.ConditionEvents; 7 | import com.tngtech.archunit.lang.SimpleConditionEvent; 8 | 9 | final class ContainAssertionsOrVerifications { 10 | 11 | private ContainAssertionsOrVerifications() { 12 | } 13 | 14 | static ArchCondition containAssertionsOrVerifications() { 15 | return new ArchCondition<>("a unit test should assert or verify something") { 16 | @Override 17 | public void check(JavaMethod item, ConditionEvents events) { 18 | for (JavaMethodCall call : item.getMethodCallsFromSelf()) { 19 | if (jUnit5(call) || 20 | mockito(call) || 21 | hamcrest(call) || 22 | assertJ(call) || 23 | truth(call) || 24 | cucumber(call) || 25 | springMockMvc(call) || 26 | archRule(call) || 27 | taikai(call) 28 | ) { 29 | return; 30 | } 31 | } 32 | events.add(SimpleConditionEvent.violated( 33 | item, 34 | "%s does not assert or verify anything".formatted(item.getDescription())) 35 | ); 36 | } 37 | 38 | private boolean jUnit5(JavaMethodCall call) { 39 | return "org.junit.jupiter.api.Assertions".equals(call.getTargetOwner().getName()); 40 | } 41 | 42 | private boolean mockito(JavaMethodCall call) { 43 | return "org.mockito.Mockito".equals(call.getTargetOwner().getName()) 44 | && (call.getName().startsWith("verify") 45 | || "inOrder".equals(call.getName()) 46 | || "capture".equals(call.getName())); 47 | } 48 | 49 | private boolean hamcrest(JavaMethodCall call) { 50 | return "org.hamcrest.MatcherAssert".equals(call.getTargetOwner().getName()); 51 | } 52 | 53 | private boolean assertJ(JavaMethodCall call) { 54 | return "org.assertj.core.api.Assertions".equals(call.getTargetOwner().getName()); 55 | } 56 | 57 | private boolean truth(JavaMethodCall call) { 58 | return "com.google.common.truth.Truth".equals(call.getTargetOwner().getName()); 59 | } 60 | 61 | private boolean cucumber(JavaMethodCall call) { 62 | return "io.cucumber.java.en.Then".equals(call.getTargetOwner().getName()) || 63 | "io.cucumber.java.en.Given".equals(call.getTargetOwner().getName()); 64 | } 65 | 66 | private boolean springMockMvc(JavaMethodCall call) { 67 | return 68 | "org.springframework.test.web.servlet.ResultActions".equals(call.getTargetOwner().getName()) 69 | && ("andExpect".equals(call.getName()) || "andExpectAll".equals(call.getName())); 70 | } 71 | 72 | private boolean archRule(JavaMethodCall call) { 73 | return "com.tngtech.archunit.lang.ArchRule".equals(call.getTargetOwner().getName()) 74 | && "check".equals(call.getName()); 75 | } 76 | 77 | private boolean taikai(JavaMethodCall call) { 78 | return "com.enofex.taikai.Taikai".equals(call.getTargetOwner().getName()) 79 | && ("check".equals(call.getName()) || "checkAll".equals(call.getName())) ; 80 | } 81 | }; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/main/java/com/enofex/taikai/test/JUnit5Configurer.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai.test; 2 | 3 | import static com.enofex.taikai.internal.ArchConditions.notDeclareThrownExceptions; 4 | import static com.enofex.taikai.test.ContainAssertionsOrVerifications.containAssertionsOrVerifications; 5 | import static com.enofex.taikai.test.JUnit5DescribedPredicates.ANNOTATION_DISABLED; 6 | import static com.enofex.taikai.test.JUnit5DescribedPredicates.ANNOTATION_DISPLAY_NAME; 7 | import static com.enofex.taikai.test.JUnit5DescribedPredicates.ANNOTATION_PARAMETRIZED_TEST; 8 | import static com.enofex.taikai.test.JUnit5DescribedPredicates.ANNOTATION_TEST; 9 | import static com.enofex.taikai.test.JUnit5DescribedPredicates.annotatedWithTestOrParameterizedTest; 10 | import static com.tngtech.archunit.lang.conditions.ArchPredicates.are; 11 | import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes; 12 | import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.methods; 13 | import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses; 14 | import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noMethods; 15 | 16 | import com.enofex.taikai.Namespace.IMPORT; 17 | import com.enofex.taikai.TaikaiRule; 18 | import com.enofex.taikai.TaikaiRule.Configuration; 19 | import com.enofex.taikai.configures.AbstractConfigurer; 20 | import com.enofex.taikai.configures.ConfigurerContext; 21 | import com.enofex.taikai.configures.DisableableConfigurer; 22 | 23 | public class JUnit5Configurer extends AbstractConfigurer { 24 | 25 | private static final Configuration CONFIGURATION = Configuration.of(IMPORT.ONLY_TESTS); 26 | 27 | JUnit5Configurer(ConfigurerContext configurerContext) { 28 | super(configurerContext); 29 | } 30 | 31 | public JUnit5Configurer methodsShouldMatch(String regex) { 32 | return methodsShouldMatch(regex, CONFIGURATION); 33 | } 34 | 35 | public JUnit5Configurer methodsShouldMatch(String regex, Configuration configuration) { 36 | return addRule(TaikaiRule.of(methods() 37 | .that(are(annotatedWithTestOrParameterizedTest(true))) 38 | .should().haveNameMatching(regex) 39 | .as("Methods annotated with %s or %s should have names matching %s".formatted( 40 | ANNOTATION_TEST, ANNOTATION_PARAMETRIZED_TEST, regex)), 41 | configuration)); 42 | } 43 | 44 | public JUnit5Configurer methodsShouldNotDeclareExceptions() { 45 | return methodsShouldNotDeclareExceptions(CONFIGURATION); 46 | } 47 | 48 | public JUnit5Configurer methodsShouldNotDeclareExceptions(Configuration configuration) { 49 | return addRule(TaikaiRule.of(methods() 50 | .that(are(annotatedWithTestOrParameterizedTest(true))) 51 | .should(notDeclareThrownExceptions()) 52 | .as("Methods annotated with %s or %s should not declare thrown Exceptions".formatted( 53 | ANNOTATION_TEST, ANNOTATION_PARAMETRIZED_TEST)), 54 | configuration)); 55 | } 56 | 57 | public JUnit5Configurer methodsShouldBeAnnotatedWithDisplayName() { 58 | return methodsShouldBeAnnotatedWithDisplayName(CONFIGURATION); 59 | } 60 | 61 | public JUnit5Configurer methodsShouldBeAnnotatedWithDisplayName(Configuration configuration) { 62 | return addRule(TaikaiRule.of(methods() 63 | .that(are(annotatedWithTestOrParameterizedTest(true))) 64 | .should().beMetaAnnotatedWith(ANNOTATION_DISPLAY_NAME) 65 | .as("Methods annotated with %s or %s should be annotated with %s".formatted(ANNOTATION_TEST, 66 | ANNOTATION_PARAMETRIZED_TEST, ANNOTATION_DISPLAY_NAME)), 67 | configuration)); 68 | } 69 | 70 | public JUnit5Configurer methodsShouldBePackagePrivate() { 71 | return methodsShouldBePackagePrivate(CONFIGURATION); 72 | } 73 | 74 | public JUnit5Configurer methodsShouldBePackagePrivate(Configuration configuration) { 75 | return addRule(TaikaiRule.of(methods() 76 | .that(are(annotatedWithTestOrParameterizedTest(true))) 77 | .should().bePackagePrivate() 78 | .as("Methods annotated with %s or %s should be package-private".formatted(ANNOTATION_TEST, 79 | ANNOTATION_PARAMETRIZED_TEST)), 80 | configuration)); 81 | } 82 | 83 | public JUnit5Configurer methodsShouldNotBeAnnotatedWithDisabled() { 84 | return methodsShouldNotBeAnnotatedWithDisabled(CONFIGURATION); 85 | } 86 | 87 | public JUnit5Configurer methodsShouldNotBeAnnotatedWithDisabled(Configuration configuration) { 88 | return addRule(TaikaiRule.of(noMethods() 89 | .should().beMetaAnnotatedWith(ANNOTATION_DISABLED) 90 | .as("Methods should not be annotated with %s".formatted(ANNOTATION_DISABLED)), 91 | configuration)); 92 | } 93 | 94 | public JUnit5Configurer methodsShouldContainAssertionsOrVerifications() { 95 | return methodsShouldContainAssertionsOrVerifications(CONFIGURATION); 96 | } 97 | 98 | public JUnit5Configurer methodsShouldContainAssertionsOrVerifications( 99 | Configuration configuration) { 100 | return addRule(TaikaiRule.of(methods() 101 | .that(are(annotatedWithTestOrParameterizedTest(true))) 102 | .should(containAssertionsOrVerifications()) 103 | .as("Methods annotated with %s or %s should contain assertions or verifications".formatted( 104 | ANNOTATION_TEST, ANNOTATION_PARAMETRIZED_TEST)), 105 | configuration)); 106 | } 107 | 108 | public JUnit5Configurer classesShouldNotBeAnnotatedWithDisabled() { 109 | return classesShouldNotBeAnnotatedWithDisabled(CONFIGURATION); 110 | } 111 | 112 | public JUnit5Configurer classesShouldBePackagePrivate(String regex) { 113 | return classesShouldBePackagePrivate(regex, CONFIGURATION); 114 | } 115 | 116 | public JUnit5Configurer classesShouldBePackagePrivate(String regex, Configuration configuration) { 117 | return addRule(TaikaiRule.of(classes() 118 | .that().areNotInterfaces().and().haveNameMatching(regex) 119 | .should().bePackagePrivate() 120 | .as("Classes with names matching %s should be package-private".formatted(regex)), 121 | configuration)); 122 | } 123 | 124 | 125 | public JUnit5Configurer classesShouldNotBeAnnotatedWithDisabled(Configuration configuration) { 126 | return addRule(TaikaiRule.of(noClasses() 127 | .should().beMetaAnnotatedWith(ANNOTATION_DISABLED) 128 | .as("Classes should not be annotated with %s".formatted(ANNOTATION_DISABLED)), 129 | configuration)); 130 | } 131 | 132 | public static final class Disableable extends JUnit5Configurer implements DisableableConfigurer { 133 | 134 | public Disableable(ConfigurerContext configurerContext) { 135 | super(configurerContext); 136 | } 137 | 138 | @Override 139 | public JUnit5Configurer disable() { 140 | disable(JUnit5Configurer.class); 141 | 142 | return this; 143 | } 144 | } 145 | } -------------------------------------------------------------------------------- /src/main/java/com/enofex/taikai/test/JUnit5DescribedPredicates.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai.test; 2 | 3 | import static com.enofex.taikai.internal.DescribedPredicates.annotatedWith; 4 | 5 | import com.tngtech.archunit.base.DescribedPredicate; 6 | import com.tngtech.archunit.core.domain.properties.CanBeAnnotated; 7 | 8 | final class JUnit5DescribedPredicates { 9 | 10 | static final String ANNOTATION_TEST = "org.junit.jupiter.api.Test"; 11 | static final String ANNOTATION_PARAMETRIZED_TEST = "org.junit.jupiter.params.ParameterizedTest"; 12 | static final String ANNOTATION_DISABLED = "org.junit.jupiter.api.Disabled"; 13 | static final String ANNOTATION_DISPLAY_NAME = "org.junit.jupiter.api.DisplayName"; 14 | 15 | private JUnit5DescribedPredicates() { 16 | } 17 | 18 | static DescribedPredicate annotatedWithTestOrParameterizedTest( 19 | boolean isMetaAnnotated) { 20 | 21 | return annotatedWith(ANNOTATION_TEST, isMetaAnnotated) 22 | .or(annotatedWith(ANNOTATION_PARAMETRIZED_TEST, isMetaAnnotated)); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/enofex/taikai/test/TestConfigurer.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai.test; 2 | 3 | import com.enofex.taikai.configures.AbstractConfigurer; 4 | import com.enofex.taikai.configures.ConfigurerContext; 5 | import com.enofex.taikai.configures.Customizer; 6 | import com.enofex.taikai.configures.DisableableConfigurer; 7 | 8 | public class TestConfigurer extends AbstractConfigurer { 9 | 10 | public TestConfigurer(ConfigurerContext configurerContext) { 11 | super(configurerContext); 12 | } 13 | 14 | public Disableable junit5(Customizer customizer) { 15 | return customizer(customizer, () -> new JUnit5Configurer.Disableable(configurerContext())); 16 | } 17 | 18 | public static final class Disableable extends TestConfigurer implements DisableableConfigurer { 19 | 20 | public Disableable(ConfigurerContext configurerContext) { 21 | super(configurerContext); 22 | } 23 | 24 | @Override 25 | public TestConfigurer disable() { 26 | disable(TestConfigurer.class); 27 | disable(JUnit5Configurer.class); 28 | 29 | return this; 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /src/test/java/com/enofex/taikai/ArchitectureTest.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai; 2 | 3 | import static com.tngtech.archunit.core.domain.JavaModifier.FINAL; 4 | import static com.tngtech.archunit.core.domain.JavaModifier.STATIC; 5 | 6 | import java.text.SimpleDateFormat; 7 | import java.util.Calendar; 8 | import java.util.Date; 9 | import java.util.List; 10 | import org.junit.jupiter.api.Test; 11 | 12 | class ArchitectureTest { 13 | 14 | @Test 15 | void shouldFulfilConstrains() { 16 | Taikai.builder() 17 | .namespace("com.enofex.taikai") 18 | .java(java -> java 19 | .noUsageOfDeprecatedAPIs() 20 | .noUsageOfSystemOutOrErr() 21 | .noUsageOf(Date.class) 22 | .noUsageOf(Calendar.class) 23 | .noUsageOf(SimpleDateFormat.class) 24 | .fieldsShouldHaveModifiers("^[A-Z][A-Z0-9_]*$", List.of(STATIC, FINAL)) 25 | .classesShouldImplementHashCodeAndEquals() 26 | .finalClassesShouldNotHaveProtectedMembers() 27 | .utilityClassesShouldBeFinalAndHavePrivateConstructor() 28 | .methodsShouldNotDeclareGenericExceptions() 29 | .fieldsShouldNotBePublic() 30 | .serialVersionUIDFieldsShouldBeStaticFinalLong() 31 | .classesShouldResideInPackage("com.enofex.taikai..") 32 | .imports(imports -> imports 33 | .shouldHaveNoCycles() 34 | .shouldNotImport("..shaded..") 35 | .shouldNotImport("..lombok..") 36 | .shouldNotImport("org.junit..")) 37 | .naming(naming -> naming 38 | .packagesShouldMatchDefault() 39 | .fieldsShouldNotMatch(".*(List|Set|Map)$") 40 | .classesShouldNotMatch(".*Impl") 41 | .interfacesShouldNotHavePrefixI() 42 | .constantsShouldFollowConventions())) 43 | .test(test -> test 44 | .junit5(junit5 -> junit5 45 | .classesShouldNotBeAnnotatedWithDisabled() 46 | .classesShouldBePackagePrivate(".*Test") 47 | .methodsShouldNotBeAnnotatedWithDisabled() 48 | .methodsShouldMatch("should.*") 49 | .methodsShouldBePackagePrivate() 50 | .methodsShouldNotDeclareExceptions() 51 | .methodsShouldContainAssertionsOrVerifications())) 52 | .build() 53 | .check(); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/test/java/com/enofex/taikai/NamespaceTest.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai; 2 | 3 | import com.tngtech.archunit.core.domain.JavaClasses; 4 | import com.tngtech.archunit.core.importer.ClassFileImporter; 5 | import com.tngtech.archunit.core.importer.ImportOption; 6 | import org.junit.jupiter.api.Test; 7 | 8 | import static org.junit.jupiter.api.Assertions.*; 9 | 10 | class NamespaceTest { 11 | 12 | private static final String VALID_NAMESPACE = "com.enofex.taikai"; 13 | 14 | @Test 15 | void shouldReturnJavaClassesWithoutTests() { 16 | JavaClasses result = Namespace.from(VALID_NAMESPACE, Namespace.IMPORT.WITHOUT_TESTS); 17 | 18 | assertNotNull(result); 19 | assertDoesNotThrow(() -> new ClassFileImporter() 20 | .withImportOption(new ImportOption.DoNotIncludeTests()) 21 | .withImportOption(new ImportOption.DoNotIncludeJars()) 22 | .importPackages(VALID_NAMESPACE)); 23 | } 24 | 25 | @Test 26 | void shouldReturnJavaClassesWithTests() { 27 | JavaClasses result = Namespace.from(VALID_NAMESPACE, Namespace.IMPORT.WITH_TESTS); 28 | 29 | assertNotNull(result); 30 | assertDoesNotThrow(() -> new ClassFileImporter() 31 | .withImportOption(new ImportOption.DoNotIncludeJars()) 32 | .importPackages(VALID_NAMESPACE)); 33 | } 34 | 35 | @Test 36 | void shouldReturnJavaClassesOnlyTests() { 37 | JavaClasses result = Namespace.from(VALID_NAMESPACE, Namespace.IMPORT.ONLY_TESTS); 38 | 39 | assertNotNull(result); 40 | assertDoesNotThrow(() -> new ClassFileImporter() 41 | .withImportOption(new ImportOption.OnlyIncludeTests()) 42 | .withImportOption(new ImportOption.DoNotIncludeJars()) 43 | .importPackages(VALID_NAMESPACE)); 44 | } 45 | 46 | @Test 47 | void shouldThrowExceptionForNullNamespace() { 48 | assertThrows(NullPointerException.class, 49 | () -> Namespace.from(null, Namespace.IMPORT.WITHOUT_TESTS)); 50 | } 51 | 52 | @Test 53 | void shouldThrowExceptionForNullImportOption() { 54 | assertThrows(NullPointerException.class, () -> Namespace.from(VALID_NAMESPACE, null)); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/test/java/com/enofex/taikai/TaikaiRuleTest.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | import static org.junit.jupiter.api.Assertions.assertNotNull; 5 | import static org.junit.jupiter.api.Assertions.assertSame; 6 | import static org.junit.jupiter.api.Assertions.assertThrows; 7 | import static org.mockito.Mockito.mock; 8 | import static org.mockito.Mockito.verify; 9 | 10 | import com.enofex.taikai.TaikaiRule.Configuration; 11 | import com.tngtech.archunit.core.domain.JavaClasses; 12 | import com.tngtech.archunit.lang.ArchRule; 13 | import org.junit.jupiter.api.Test; 14 | 15 | class TaikaiRuleTest { 16 | 17 | @Test 18 | void shouldConstructWithArchRuleAndDefaultConfiguration() { 19 | ArchRule archRule = mock(ArchRule.class); 20 | TaikaiRule taikaiRule = TaikaiRule.of(archRule); 21 | 22 | assertNotNull(taikaiRule); 23 | assertEquals(archRule, taikaiRule.archRule()); 24 | assertNotNull(taikaiRule.configuration()); 25 | } 26 | 27 | @Test 28 | void shouldConstructWithArchRuleAndGivenConfiguration() { 29 | ArchRule archRule = mock(ArchRule.class); 30 | Configuration configuration = Configuration.of("com.example"); 31 | TaikaiRule taikaiRule = TaikaiRule.of(archRule, configuration); 32 | 33 | assertNotNull(taikaiRule); 34 | assertEquals(archRule, taikaiRule.archRule()); 35 | assertSame(configuration, taikaiRule.configuration()); 36 | } 37 | 38 | @Test 39 | void shouldThrowNullPointerExceptionWhenConstructedWithNullArchRule() { 40 | assertThrows(NullPointerException.class, () -> TaikaiRule.of(null)); 41 | } 42 | 43 | @Test 44 | void shouldReturnConfigurationNamespace() { 45 | Configuration configuration = Configuration.of("com.example"); 46 | 47 | assertEquals("com.example", configuration.namespace()); 48 | } 49 | 50 | @Test 51 | void shouldReturnConfigurationNamespaceImport() { 52 | Configuration configuration = Configuration.of(Namespace.IMPORT.ONLY_TESTS); 53 | 54 | assertEquals(Namespace.IMPORT.ONLY_TESTS, configuration.namespaceImport()); 55 | } 56 | 57 | @Test 58 | void shouldReturnConfigurationJavaClasses() { 59 | JavaClasses javaClasses = mock(JavaClasses.class); 60 | Configuration configuration = Configuration.of(javaClasses); 61 | 62 | assertEquals(javaClasses, configuration.javaClasses()); 63 | } 64 | 65 | @Test 66 | void shouldCheckUsingGivenJavaClasses() { 67 | JavaClasses javaClasses = mock(JavaClasses.class); 68 | ArchRule archRule = mock(ArchRule.class); 69 | TaikaiRule.of(archRule, Configuration.of(javaClasses)).check(null); 70 | 71 | verify(archRule).check(javaClasses); 72 | } 73 | 74 | @Test 75 | void shouldCheckUsingNamespaceFromConfiguration() { 76 | ArchRule archRule = mock(ArchRule.class); 77 | TaikaiRule.of(archRule, Configuration.of("com.example", Namespace.IMPORT.WITH_TESTS)) 78 | .check(null); 79 | 80 | verify(archRule).check(Namespace.from("com.example", Namespace.IMPORT.WITH_TESTS)); 81 | } 82 | 83 | @Test 84 | void shouldCheckUsingGlobalNamespace() { 85 | ArchRule archRule = mock(ArchRule.class); 86 | TaikaiRule.of(archRule).check("com.example"); 87 | 88 | verify(archRule).check(Namespace.from("com.example", Namespace.IMPORT.WITHOUT_TESTS)); 89 | } 90 | 91 | @Test 92 | void shouldThrowTaikaiExceptionWhenNoNamespaceProvided() { 93 | ArchRule archRule = mock(ArchRule.class); 94 | TaikaiRule taikaiRule = TaikaiRule.of(archRule, Configuration.defaultConfiguration()); 95 | 96 | assertThrows(TaikaiException.class, () -> taikaiRule.check(null)); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/test/java/com/enofex/taikai/TaikaiTest.java: -------------------------------------------------------------------------------- 1 | 2 | package com.enofex.taikai; 3 | 4 | import static java.util.Collections.emptyList; 5 | import static org.junit.jupiter.api.Assertions.assertEquals; 6 | import static org.junit.jupiter.api.Assertions.assertFalse; 7 | import static org.junit.jupiter.api.Assertions.assertNotNull; 8 | import static org.junit.jupiter.api.Assertions.assertNull; 9 | import static org.junit.jupiter.api.Assertions.assertThrows; 10 | import static org.junit.jupiter.api.Assertions.assertTrue; 11 | import static org.mockito.Mockito.any; 12 | import static org.mockito.Mockito.mock; 13 | import static org.mockito.Mockito.times; 14 | import static org.mockito.Mockito.verify; 15 | 16 | import com.enofex.taikai.configures.Customizer; 17 | import com.enofex.taikai.java.JavaConfigurer; 18 | import com.enofex.taikai.spring.SpringConfigurer.Disableable; 19 | import com.enofex.taikai.test.TestConfigurer; 20 | import com.tngtech.archunit.ArchConfiguration; 21 | import com.tngtech.archunit.core.importer.ClassFileImporter; 22 | import java.util.Collection; 23 | import java.util.Collections; 24 | import org.junit.jupiter.api.BeforeEach; 25 | import org.junit.jupiter.api.Test; 26 | 27 | class TaikaiTest { 28 | 29 | private static final String VALID_NAMESPACE = "com.enofex.taikai"; 30 | 31 | @BeforeEach 32 | void setUp() { 33 | ArchConfiguration.get().reset(); 34 | } 35 | 36 | @Test 37 | void shouldBuildTaikaiWithDefaultValues() { 38 | Taikai taikai = Taikai.builder() 39 | .namespace(VALID_NAMESPACE) 40 | .build(); 41 | 42 | assertFalse(taikai.failOnEmpty()); 43 | assertEquals(VALID_NAMESPACE, taikai.namespace()); 44 | assertNull(taikai.classes()); 45 | assertTrue(taikai.rules().isEmpty()); 46 | assertTrue(taikai.excludedClasses().isEmpty()); 47 | } 48 | 49 | @Test 50 | void shouldBuildTaikaiWithCustomValues() { 51 | TaikaiRule mockRule = mock(TaikaiRule.class); 52 | Collection rules = Collections.singletonList(mockRule); 53 | 54 | Taikai taikai = Taikai.builder() 55 | .classes(new ClassFileImporter().importClasses(TaikaiTest.class)) 56 | .excludeClasses("com.enofex.taikai.foo.ClassToExclude", "com.enofex.taikai.bar.ClassToExclude") 57 | .failOnEmpty(true) 58 | .addRules(rules) 59 | .build(); 60 | 61 | assertTrue(taikai.failOnEmpty()); 62 | assertNull(taikai.namespace()); 63 | assertNotNull(taikai.classes()); 64 | assertEquals(1, taikai.rules().size()); 65 | assertTrue(taikai.rules().contains(mockRule)); 66 | assertEquals(2, taikai.excludedClasses().size()); 67 | } 68 | 69 | @Test 70 | void shouldAddSingleRule() { 71 | TaikaiRule mockRule = mock(TaikaiRule.class); 72 | 73 | Taikai taikai = Taikai.builder() 74 | .namespace(VALID_NAMESPACE) 75 | .addRule(mockRule) 76 | .build(); 77 | 78 | assertEquals(1, taikai.rules().size()); 79 | assertTrue(taikai.rules().contains(mockRule)); 80 | } 81 | 82 | @Test 83 | void shouldConfigureJavaCustomizer() { 84 | Customizer customizer = mock(Customizer.class); 85 | 86 | Taikai.builder() 87 | .namespace(VALID_NAMESPACE) 88 | .java(customizer) 89 | .build(); 90 | 91 | verify(customizer, times(1)).customize(any(JavaConfigurer.Disableable.class)); 92 | } 93 | 94 | @Test 95 | void shouldConfigureSpringCustomizer() { 96 | Customizer customizer = mock(Customizer.class); 97 | 98 | Taikai.builder() 99 | .namespace(VALID_NAMESPACE) 100 | .spring(customizer) 101 | .build(); 102 | 103 | verify(customizer, times(1)).customize(any(Disableable.class)); 104 | } 105 | 106 | @Test 107 | void shouldConfigureTestCustomizer() { 108 | Customizer customizer = mock(Customizer.class); 109 | 110 | Taikai.builder() 111 | .namespace(VALID_NAMESPACE) 112 | .test(customizer) 113 | .build(); 114 | 115 | verify(customizer, times(1)).customize(any(TestConfigurer.Disableable.class)); 116 | } 117 | 118 | @Test 119 | void shouldThrowExceptionForNullCustomizer() { 120 | assertThrows(NullPointerException.class, () -> Taikai.builder().java(null)); 121 | assertThrows(NullPointerException.class, () -> Taikai.builder().spring(null)); 122 | assertThrows(NullPointerException.class, () -> Taikai.builder().test(null)); 123 | } 124 | 125 | @Test 126 | void shouldCheckRules() { 127 | TaikaiRule mockRule = mock(TaikaiRule.class); 128 | 129 | Taikai.builder() 130 | .namespace(VALID_NAMESPACE) 131 | .addRule(mockRule) 132 | .build() 133 | .check(); 134 | 135 | verify(mockRule, times(1)).check(VALID_NAMESPACE, null, emptyList()); 136 | } 137 | 138 | @Test 139 | void shouldRebuildTaikaiWithNewValues() { 140 | Taikai taikai = Taikai.builder() 141 | .namespace(VALID_NAMESPACE) 142 | .excludeClasses("com.enofex.taikai.ClassToExclude") 143 | .failOnEmpty(true) 144 | .java(java -> java 145 | .fieldsShouldNotBePublic()) 146 | .build(); 147 | 148 | Taikai modifiedTaikai = taikai.toBuilder() 149 | .namespace("com.enofex.newnamespace") 150 | .excludeClasses("com.enofex.taikai.AnotherClassToExclude") 151 | .failOnEmpty(false) 152 | .java(java -> java 153 | .classesShouldImplementHashCodeAndEquals() 154 | .finalClassesShouldNotHaveProtectedMembers()) 155 | .build(); 156 | 157 | assertFalse(modifiedTaikai.failOnEmpty()); 158 | assertEquals("com.enofex.newnamespace", modifiedTaikai.namespace()); 159 | assertEquals(2, modifiedTaikai.excludedClasses().size()); 160 | assertEquals(3, modifiedTaikai.rules().size()); 161 | assertTrue(modifiedTaikai.excludedClasses().contains("com.enofex.taikai.ClassToExclude")); 162 | assertTrue( 163 | modifiedTaikai.excludedClasses().contains("com.enofex.taikai.AnotherClassToExclude")); 164 | } 165 | 166 | @Test 167 | void shouldThrowExceptionIfNamespaceAndClasses() { 168 | assertThrows(IllegalArgumentException.class, () -> Taikai.builder() 169 | .namespace(VALID_NAMESPACE) 170 | .classes(new ClassFileImporter().importClasses(TaikaiTest.class)) 171 | .build()); 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /src/test/java/com/enofex/taikai/configures/ConfigurerContextTest.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai.configures; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | import static org.junit.jupiter.api.Assertions.assertNull; 5 | import static org.junit.jupiter.api.Assertions.assertSame; 6 | 7 | import org.junit.jupiter.api.Test; 8 | 9 | class ConfigurerContextTest { 10 | 11 | private static final String VALID_NAMESPACE = "com.example"; 12 | private static final Configurers VALID_CONFIGURERS = new Configurers(); 13 | 14 | @Test 15 | void shouldReturnNamespace() { 16 | ConfigurerContext context = new ConfigurerContext(VALID_NAMESPACE, VALID_CONFIGURERS); 17 | 18 | assertEquals(VALID_NAMESPACE, context.namespace()); 19 | } 20 | 21 | @Test 22 | void shouldReturnConfigurers() { 23 | ConfigurerContext context = new ConfigurerContext(VALID_NAMESPACE, VALID_CONFIGURERS); 24 | 25 | assertSame(VALID_CONFIGURERS, context.configurers()); 26 | } 27 | 28 | @Test 29 | void shouldHandleNullNamespace() { 30 | ConfigurerContext context = new ConfigurerContext(null, VALID_CONFIGURERS); 31 | 32 | assertNull(context.namespace()); 33 | } 34 | 35 | @Test 36 | void shouldHandleNullConfigurers() { 37 | ConfigurerContext context = new ConfigurerContext(VALID_NAMESPACE, null); 38 | 39 | assertNull(context.configurers()); 40 | } 41 | 42 | @Test 43 | void shouldHandleNullParameters() { 44 | ConfigurerContext context = new ConfigurerContext(null, null); 45 | 46 | assertNull(context.namespace()); 47 | assertNull(context.configurers()); 48 | } 49 | } -------------------------------------------------------------------------------- /src/test/java/com/enofex/taikai/configures/ConfigurersTest.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai.configures; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | import static org.junit.jupiter.api.Assertions.assertFalse; 5 | import static org.junit.jupiter.api.Assertions.assertNull; 6 | import static org.junit.jupiter.api.Assertions.assertSame; 7 | import static org.junit.jupiter.api.Assertions.assertThrows; 8 | import static org.junit.jupiter.api.Assertions.assertTrue; 9 | 10 | import com.enofex.taikai.TaikaiRule; 11 | import java.util.Collection; 12 | import java.util.Iterator; 13 | import org.junit.jupiter.api.Test; 14 | 15 | class ConfigurersTest { 16 | 17 | private final Configurers configurers = new Configurers(); 18 | 19 | @Test 20 | void shouldThrowNullPointerExceptionWhenGetOrApplyWithNull() { 21 | assertThrows(NullPointerException.class, () -> this.configurers.getOrApply(null)); 22 | } 23 | 24 | @Test 25 | void shouldGetOrApplyReturnExistingConfigurer() { 26 | TestConfigurer testConfigurer = new TestConfigurer(); 27 | this.configurers.getOrApply(testConfigurer); 28 | TestConfigurer retrievedConfigurer = this.configurers.getOrApply(new TestConfigurer()); 29 | 30 | assertSame(testConfigurer, retrievedConfigurer); 31 | } 32 | 33 | @Test 34 | void shouldGetOrApplyApplyNewConfigurer() { 35 | TestConfigurer testConfigurer = new TestConfigurer(); 36 | TestConfigurer retrievedConfigurer = this.configurers.getOrApply(testConfigurer); 37 | 38 | assertSame(testConfigurer, retrievedConfigurer); 39 | assertEquals(1, this.configurers.all().size()); 40 | } 41 | 42 | @Test 43 | void shouldGetReturnConfigurerByClass() { 44 | TestConfigurer testConfigurer = new TestConfigurer(); 45 | this.configurers.getOrApply(testConfigurer); 46 | TestConfigurer retrievedConfigurer = this.configurers.get(TestConfigurer.class); 47 | 48 | assertSame(testConfigurer, retrievedConfigurer); 49 | } 50 | 51 | @Test 52 | void shouldGetReturnNullForUnknownClass() { 53 | assertNull(this.configurers.get(TestConfigurer.class)); 54 | } 55 | 56 | @Test 57 | void shouldAllReturnAllConfigurers() { 58 | TestConfigurer testConfigurer1 = new TestConfigurer(); 59 | AnotherTestConfigurer testConfigurer2 = new AnotherTestConfigurer(); 60 | this.configurers.getOrApply(testConfigurer1); 61 | this.configurers.getOrApply(testConfigurer2); 62 | 63 | Collection allConfigurers = this.configurers.all(); 64 | 65 | assertEquals(2, allConfigurers.size()); 66 | assertTrue(allConfigurers.contains(testConfigurer1)); 67 | assertTrue(allConfigurers.contains(testConfigurer2)); 68 | } 69 | 70 | @Test 71 | void shouldIteratorIterateOverAllConfigurers() { 72 | TestConfigurer testConfigurer1 = new TestConfigurer(); 73 | AnotherTestConfigurer testConfigurer2 = new AnotherTestConfigurer(); 74 | this.configurers.getOrApply(testConfigurer1); 75 | this.configurers.getOrApply(testConfigurer2); 76 | 77 | Iterator iterator = this.configurers.iterator(); 78 | assertTrue(iterator.hasNext()); 79 | assertSame(testConfigurer1, iterator.next()); 80 | assertSame(testConfigurer2, iterator.next()); 81 | assertFalse(iterator.hasNext()); 82 | } 83 | 84 | private static class TestConfigurer implements Configurer { 85 | 86 | @Override 87 | public Collection rules() { 88 | return null; 89 | } 90 | } 91 | 92 | private static class AnotherTestConfigurer implements Configurer { 93 | 94 | @Override 95 | public Collection rules() { 96 | return null; 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/test/java/com/enofex/taikai/internal/ArchConditionsTest.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai.internal; 2 | 3 | 4 | import static org.junit.jupiter.api.Assertions.assertEquals; 5 | import static org.mockito.Mockito.any; 6 | import static org.mockito.Mockito.mock; 7 | import static org.mockito.Mockito.never; 8 | import static org.mockito.Mockito.verify; 9 | import static org.mockito.Mockito.when; 10 | 11 | import com.tngtech.archunit.core.domain.JavaClass; 12 | import com.tngtech.archunit.core.domain.JavaField; 13 | import com.tngtech.archunit.core.domain.JavaMethod; 14 | import com.tngtech.archunit.core.domain.JavaModifier; 15 | import com.tngtech.archunit.core.domain.ThrowsClause; 16 | import com.tngtech.archunit.lang.ConditionEvents; 17 | import com.tngtech.archunit.lang.SimpleConditionEvent; 18 | import java.util.Collections; 19 | import java.util.EnumSet; 20 | import java.util.Set; 21 | import org.junit.jupiter.api.Test; 22 | import org.junit.jupiter.api.extension.ExtendWith; 23 | import org.mockito.ArgumentCaptor; 24 | import org.mockito.Mock; 25 | import org.mockito.junit.jupiter.MockitoExtension; 26 | 27 | @ExtendWith(MockitoExtension.class) 28 | class ArchConditionsTest { 29 | 30 | @Mock 31 | private JavaMethod mockMethod; 32 | @Mock 33 | private JavaField mockField; 34 | @Mock 35 | private JavaClass mockClass; 36 | @Mock 37 | private ConditionEvents events; 38 | @Mock 39 | private ThrowsClause mockThrowsClause; 40 | 41 | 42 | @Test 43 | void shouldNotDeclareThrownExceptions() { 44 | when(this.mockMethod.getThrowsClause()).thenReturn(this.mockThrowsClause); 45 | when(this.mockThrowsClause.isEmpty()).thenReturn(true); 46 | 47 | ArchConditions.notDeclareThrownExceptions().check(this.mockMethod, this.events); 48 | 49 | verify(this.events, never()).add(any(SimpleConditionEvent.class)); 50 | } 51 | 52 | @Test 53 | void shouldDeclareThrownExceptions() { 54 | when(this.mockMethod.getThrowsClause()).thenReturn(this.mockThrowsClause); 55 | when(this.mockThrowsClause.isEmpty()).thenReturn(false); 56 | 57 | ArchConditions.notDeclareThrownExceptions().check(this.mockMethod, this.events); 58 | 59 | verify(this.events).add(any(SimpleConditionEvent.class)); 60 | } 61 | 62 | @Test 63 | void shouldBePublicButNotStatic() { 64 | when(this.mockField.getName()).thenReturn("publicField"); 65 | when(this.mockField.getOwner()).thenReturn(this.mockClass); 66 | when(this.mockField.getModifiers()).thenReturn(EnumSet.of(JavaModifier.PUBLIC)); 67 | 68 | ArchConditions.notBePublicUnlessStatic().check(this.mockField, this.events); 69 | 70 | ArgumentCaptor eventCaptor = ArgumentCaptor.forClass( 71 | SimpleConditionEvent.class); 72 | verify(this.events).add(eventCaptor.capture()); 73 | assertEquals("Field %s in class %s is public".formatted( 74 | this.mockField.getName(), this.mockClass.getFullName()), 75 | eventCaptor.getValue().getDescriptionLines().get(0)); 76 | } 77 | 78 | 79 | @Test 80 | void shouldHaveRequiredModifiers() { 81 | Set requiredModifiers = EnumSet.of(JavaModifier.PRIVATE, JavaModifier.FINAL); 82 | when(this.mockField.getModifiers()).thenReturn(requiredModifiers); 83 | 84 | ArchConditions.hasFieldModifiers(requiredModifiers).check(this.mockField, this.events); 85 | 86 | verify(this.events, never()).add(any(SimpleConditionEvent.class)); 87 | } 88 | 89 | @Test 90 | void shouldNotHaveRequiredModifiers() { 91 | Set requiredModifiers = EnumSet.of(JavaModifier.PRIVATE, JavaModifier.FINAL); 92 | when(this.mockField.getModifiers()).thenReturn(EnumSet.of(JavaModifier.PUBLIC)); 93 | when(this.mockField.getName()).thenReturn("field"); 94 | when(this.mockField.getOwner()).thenReturn(this.mockClass); 95 | 96 | ArchConditions.hasFieldModifiers(requiredModifiers).check(this.mockField, this.events); 97 | 98 | ArgumentCaptor eventCaptor = ArgumentCaptor.forClass( 99 | SimpleConditionEvent.class); 100 | verify(this.events).add(eventCaptor.capture()); 101 | assertEquals("Field %s in class %s is missing one of this %s modifier".formatted( 102 | this.mockField.getName(), 103 | this.mockClass.getFullName(), 104 | "PRIVATE, FINAL"), 105 | eventCaptor.getValue().getDescriptionLines().get(0)); 106 | } 107 | 108 | @Test 109 | void shouldHaveFieldOfType() { 110 | String typeName = "com.example.MyType"; 111 | JavaClass mockRawType = mock(JavaClass.class); 112 | 113 | when(mockRawType.getName()).thenReturn(typeName); 114 | when(this.mockField.getRawType()).thenReturn(mockRawType); 115 | when(this.mockClass.getAllFields()).thenReturn(Collections.singleton(this.mockField)); 116 | 117 | ArchConditions.haveFieldOfType(typeName).check(this.mockClass, this.events); 118 | 119 | verify(this.events, never()).add(any(SimpleConditionEvent.class)); 120 | } 121 | 122 | @Test 123 | void shouldNotHaveFieldOfType() { 124 | String typeName = "com.example.MyType"; 125 | JavaClass mockRawType = mock(JavaClass.class); 126 | 127 | when(mockRawType.getName()).thenReturn("com.example.AnotherType"); 128 | when(this.mockField.getRawType()).thenReturn(mockRawType); 129 | when(this.mockClass.getAllFields()).thenReturn(Collections.singleton(this.mockField)); 130 | 131 | ArchConditions.haveFieldOfType(typeName).check(this.mockClass, this.events); 132 | 133 | verify(this.events).add(any(SimpleConditionEvent.class)); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/test/java/com/enofex/taikai/internal/DescribedPredicatesTest.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai.internal; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertFalse; 4 | import static org.junit.jupiter.api.Assertions.assertTrue; 5 | import static org.mockito.Mockito.when; 6 | 7 | import com.tngtech.archunit.core.domain.JavaClass; 8 | import com.tngtech.archunit.core.domain.properties.CanBeAnnotated; 9 | import java.util.Collections; 10 | import java.util.Set; 11 | import org.junit.jupiter.api.Test; 12 | import org.junit.jupiter.api.extension.ExtendWith; 13 | import org.mockito.Mock; 14 | import org.mockito.junit.jupiter.MockitoExtension; 15 | import org.mockito.junit.jupiter.MockitoSettings; 16 | import org.mockito.quality.Strictness; 17 | 18 | @ExtendWith(MockitoExtension.class) 19 | @MockitoSettings(strictness = Strictness.LENIENT) 20 | class DescribedPredicatesTest { 21 | 22 | @Mock 23 | private CanBeAnnotated canBeAnnotated; 24 | @Mock 25 | private JavaClass javaClass; 26 | 27 | @Test 28 | void shouldReturnTrueWhenAnnotatedWithSpecificAnnotation() { 29 | String annotation = "MyAnnotation"; 30 | when(this.canBeAnnotated.isAnnotatedWith(annotation)).thenReturn(true); 31 | 32 | assertTrue(DescribedPredicates.annotatedWith(annotation, false).test(this.canBeAnnotated)); 33 | } 34 | 35 | @Test 36 | void shouldReturnFalseWhenNotAnnotatedWithSpecificAnnotation() { 37 | String annotation = "MyAnnotation"; 38 | when(this.canBeAnnotated.isAnnotatedWith(annotation)).thenReturn(false); 39 | 40 | assertFalse(DescribedPredicates.annotatedWith(annotation, false).test(this.canBeAnnotated)); 41 | } 42 | 43 | @Test 44 | void shouldReturnTrueWhenAnnotatedWithAllAnnotations() { 45 | Set annotations = Set.of("MyAnnotation1", "MyAnnotation2"); 46 | 47 | when(this.canBeAnnotated.isAnnotatedWith("MyAnnotation1")).thenReturn(true); 48 | when(this.canBeAnnotated.isAnnotatedWith("MyAnnotation2")).thenReturn(true); 49 | 50 | assertTrue(DescribedPredicates.annotatedWithAll(annotations, false).test(this.canBeAnnotated)); 51 | } 52 | 53 | @Test 54 | void shouldReturnFalseWhenNotAnnotatedWithAllAnnotations() { 55 | Set annotations = Set.of("MyAnnotation1", "MyAnnotation2"); 56 | 57 | when(this.canBeAnnotated.isAnnotatedWith("MyAnnotation1")).thenReturn(true); 58 | when(this.canBeAnnotated.isAnnotatedWith("MyAnnotation2")).thenReturn(false); 59 | 60 | assertFalse(DescribedPredicates.annotatedWithAll(annotations, false).test(this.canBeAnnotated)); 61 | } 62 | 63 | @Test 64 | void shouldReturnTrueWhenClassIsFinal() { 65 | when(this.javaClass.getModifiers()).thenReturn( 66 | Set.of(com.tngtech.archunit.core.domain.JavaModifier.FINAL)); 67 | 68 | assertTrue(DescribedPredicates.areFinal().test(this.javaClass)); 69 | } 70 | 71 | @Test 72 | void shouldReturnFalseWhenClassIsNotFinal() { 73 | when(this.javaClass.getModifiers()).thenReturn(Collections.emptySet()); 74 | 75 | assertFalse(DescribedPredicates.areFinal().test(this.javaClass)); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/test/java/com/enofex/taikai/internal/ModifiersTest.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai.internal; 2 | 3 | 4 | import static org.junit.jupiter.api.Assertions.assertFalse; 5 | import static org.junit.jupiter.api.Assertions.assertTrue; 6 | import static org.mockito.Mockito.when; 7 | 8 | import com.tngtech.archunit.core.domain.JavaClass; 9 | import com.tngtech.archunit.core.domain.JavaConstructor; 10 | import com.tngtech.archunit.core.domain.JavaField; 11 | import com.tngtech.archunit.core.domain.JavaMethod; 12 | import com.tngtech.archunit.core.domain.JavaModifier; 13 | import java.util.Collections; 14 | import java.util.Set; 15 | import org.junit.jupiter.api.Test; 16 | import org.junit.jupiter.api.extension.ExtendWith; 17 | import org.mockito.Mock; 18 | import org.mockito.junit.jupiter.MockitoExtension; 19 | 20 | @ExtendWith(MockitoExtension.class) 21 | class ModifiersTest { 22 | 23 | @Mock 24 | private JavaClass javaClass; 25 | @Mock 26 | private JavaConstructor constructor; 27 | @Mock 28 | private JavaField field; 29 | @Mock 30 | private JavaMethod method; 31 | 32 | 33 | @Test 34 | void shouldReturnTrueWhenClassIsFinal() { 35 | when(this.javaClass.getModifiers()).thenReturn(Set.of(JavaModifier.FINAL)); 36 | 37 | assertTrue(Modifiers.isClassFinal(this.javaClass)); 38 | } 39 | 40 | @Test 41 | void shouldReturnFalseWhenClassIsNotFinal() { 42 | when(this.javaClass.getModifiers()).thenReturn(Collections.emptySet()); 43 | 44 | assertFalse(Modifiers.isClassFinal(this.javaClass)); 45 | } 46 | 47 | @Test 48 | void shouldReturnTrueWhenConstructorIsPrivate() { 49 | when(this.constructor.getModifiers()).thenReturn(Set.of(JavaModifier.PRIVATE)); 50 | 51 | assertTrue(Modifiers.isConstructorPrivate(this.constructor)); 52 | } 53 | 54 | @Test 55 | void shouldReturnFalseWhenConstructorIsNotPrivate() { 56 | when(this.constructor.getModifiers()).thenReturn(Collections.emptySet()); 57 | 58 | assertFalse(Modifiers.isConstructorPrivate(this.constructor)); 59 | } 60 | 61 | @Test 62 | void shouldReturnTrueWhenMethodIsProtected() { 63 | when(this.method.getModifiers()).thenReturn(Set.of(JavaModifier.PROTECTED)); 64 | 65 | assertTrue(Modifiers.isMethodProtected(this.method)); 66 | } 67 | 68 | @Test 69 | void shouldReturnFalseWhenMethodIsNotProtected() { 70 | when(this.method.getModifiers()).thenReturn(Collections.emptySet()); 71 | 72 | assertFalse(Modifiers.isMethodProtected(this.method)); 73 | } 74 | 75 | @Test 76 | void shouldReturnTrueWhenMethodIsStatic() { 77 | when(this.method.getModifiers()).thenReturn(Set.of(JavaModifier.STATIC)); 78 | 79 | assertTrue(Modifiers.isMethodStatic(this.method)); 80 | } 81 | 82 | @Test 83 | void shouldReturnFalseWhenMethodIsNotStatic() { 84 | when(this.method.getModifiers()).thenReturn(Collections.emptySet()); 85 | 86 | assertFalse(Modifiers.isMethodStatic(this.method)); 87 | } 88 | 89 | @Test 90 | void shouldReturnTrueWhenFieldIsStatic() { 91 | when(this.field.getModifiers()).thenReturn(Set.of(JavaModifier.STATIC)); 92 | 93 | assertTrue(Modifiers.isFieldStatic(this.field)); 94 | } 95 | 96 | @Test 97 | void shouldReturnFalseWhenFieldIsNotStatic() { 98 | when(this.field.getModifiers()).thenReturn(Collections.emptySet()); 99 | 100 | assertFalse(Modifiers.isFieldStatic(this.field)); 101 | } 102 | 103 | @Test 104 | void shouldReturnTrueWhenFieldIsPublic() { 105 | when(this.field.getModifiers()).thenReturn(Set.of(JavaModifier.PUBLIC)); 106 | 107 | assertTrue(Modifiers.isFieldPublic(this.field)); 108 | } 109 | 110 | @Test 111 | void shouldReturnFalseWhenFieldIsNotPublic() { 112 | when(this.field.getModifiers()).thenReturn(Collections.emptySet()); 113 | 114 | assertFalse(Modifiers.isFieldPublic(this.field)); 115 | } 116 | 117 | @Test 118 | void shouldReturnTrueWhenFieldIsProtected() { 119 | when(this.field.getModifiers()).thenReturn(Set.of(JavaModifier.PROTECTED)); 120 | 121 | assertTrue(Modifiers.isFieldProtected(this.field)); 122 | } 123 | 124 | @Test 125 | void shouldReturnFalseWhenFieldIsNotProtected() { 126 | when(this.field.getModifiers()).thenReturn(Collections.emptySet()); 127 | 128 | assertFalse(Modifiers.isFieldProtected(this.field)); 129 | } 130 | 131 | @Test 132 | void shouldReturnTrueWhenFieldIsFinal() { 133 | when(this.field.getModifiers()).thenReturn(Set.of(JavaModifier.FINAL)); 134 | 135 | assertTrue(Modifiers.isFieldFinal(this.field)); 136 | } 137 | 138 | @Test 139 | void shouldReturnFalseWhenFieldIsNotFinal() { 140 | when(this.field.getModifiers()).thenReturn(Collections.emptySet()); 141 | 142 | assertFalse(Modifiers.isFieldFinal(this.field)); 143 | } 144 | 145 | @Test 146 | void shouldReturnTrueWhenFieldIsSynthetic() { 147 | when(this.field.getModifiers()).thenReturn(Set.of(JavaModifier.SYNTHETIC)); 148 | 149 | assertTrue(Modifiers.isFieldSynthetic(this.field)); 150 | } 151 | 152 | @Test 153 | void shouldReturnFalseWhenFieldIsNotSynthetic() { 154 | when(this.field.getModifiers()).thenReturn(Collections.emptySet()); 155 | 156 | assertFalse(Modifiers.isFieldSynthetic(this.field)); 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/test/java/com/enofex/taikai/logging/LoggingConfigurerTest.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai.logging; 2 | 3 | import static com.tngtech.archunit.core.domain.JavaModifier.FINAL; 4 | import static com.tngtech.archunit.core.domain.JavaModifier.PRIVATE; 5 | import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; 6 | import static org.junit.jupiter.api.Assertions.assertThrows; 7 | 8 | import com.enofex.taikai.Taikai; 9 | import com.tngtech.archunit.core.importer.ClassFileImporter; 10 | import java.util.List; 11 | import java.util.logging.Logger; 12 | import org.junit.jupiter.api.Test; 13 | 14 | class LoggingConfigurerTest { 15 | 16 | @Test 17 | void shouldApplyLoggerConventionsWithClass() { 18 | Taikai taikai = Taikai.builder() 19 | .classes(new ClassFileImporter().importClasses(LoggerConventionsFollowed.class)) 20 | .logging(logging -> logging.loggersShouldFollowConventions(Logger.class, "logger", 21 | List.of(PRIVATE, FINAL))) 22 | .build(); 23 | 24 | assertDoesNotThrow(taikai::check); 25 | } 26 | 27 | @Test 28 | void shouldApplyLoggerConventionsWithTypeName() { 29 | Taikai taikai = Taikai.builder() 30 | .classes(new ClassFileImporter().importClasses(LoggerConventionsFollowed.class)) 31 | .logging(logging -> logging 32 | .loggersShouldFollowConventions("java.util.logging.Logger", "logger", 33 | List.of(PRIVATE, FINAL))) 34 | .build(); 35 | 36 | assertDoesNotThrow(taikai::check); 37 | } 38 | 39 | @Test 40 | void shouldThrowLoggerConventionsWithClassNaming() { 41 | Taikai taikai = Taikai.builder() 42 | .classes(new ClassFileImporter().importClasses(LoggerConventionsNotFollowedNaming.class)) 43 | .logging(logging -> logging.loggersShouldFollowConventions(Logger.class, "logger", 44 | List.of(PRIVATE, FINAL))) 45 | .build(); 46 | 47 | assertThrows(AssertionError.class, taikai::check); 48 | } 49 | 50 | @Test 51 | void shouldThrowLoggerConventionsWithClassModifier() { 52 | Taikai taikai = Taikai.builder() 53 | .classes(new ClassFileImporter().importClasses(LoggerConventionsPartiallyModifier.class)) 54 | .logging(logging -> logging.loggersShouldFollowConventions(Logger.class, "logger", 55 | List.of(PRIVATE, FINAL))) 56 | .build(); 57 | 58 | assertThrows(AssertionError.class, taikai::check); 59 | } 60 | 61 | private static class LoggerConventionsFollowed { 62 | private static final Logger logger = Logger.getLogger( 63 | LoggerConventionsFollowed.class.getName()); 64 | } 65 | 66 | private static class LoggerConventionsNotFollowedNaming { 67 | public static Logger LOGGER = Logger.getLogger( 68 | LoggerConventionsNotFollowedNaming.class.getName()); 69 | } 70 | 71 | private static class LoggerConventionsPartiallyModifier { 72 | private Logger logger = Logger.getLogger( 73 | LoggerConventionsPartiallyModifier.class.getName()); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/test/java/com/enofex/taikai/test/JUnit5DescribedPredicatesTest.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai.test; 2 | 3 | import static com.enofex.taikai.test.JUnit5DescribedPredicates.ANNOTATION_PARAMETRIZED_TEST; 4 | import static com.enofex.taikai.test.JUnit5DescribedPredicates.ANNOTATION_TEST; 5 | import static com.enofex.taikai.test.JUnit5DescribedPredicates.annotatedWithTestOrParameterizedTest; 6 | import static com.tngtech.archunit.lang.conditions.ArchConditions.beAnnotatedWith; 7 | import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.methods; 8 | import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; 9 | import static org.junit.jupiter.api.Assertions.assertTrue; 10 | 11 | import com.tngtech.archunit.core.domain.JavaClasses; 12 | import com.tngtech.archunit.core.importer.ClassFileImporter; 13 | import com.tngtech.archunit.lang.ArchRule; 14 | import com.tngtech.archunit.lang.conditions.ArchConditions; 15 | import org.junit.jupiter.api.Test; 16 | import org.junit.jupiter.params.ParameterizedTest; 17 | import org.junit.jupiter.params.provider.EmptySource; 18 | 19 | class JUnit5DescribedPredicatesTest { 20 | 21 | @Test 22 | void shouldIdentifyClassesAnnotatedWithTestOrParameterizedTest() { 23 | JavaClasses importedClasses = new ClassFileImporter().importClasses( 24 | TestExample.class, ParameterizedTestExample.class); 25 | 26 | ArchRule rule = methods().that(annotatedWithTestOrParameterizedTest(false)) 27 | .should(beAnnotatedWith(ANNOTATION_TEST) 28 | .or(beAnnotatedWith(ANNOTATION_PARAMETRIZED_TEST))); 29 | 30 | assertDoesNotThrow(() -> rule.check(importedClasses)); 31 | } 32 | 33 | @Test 34 | void shouldIdentifyClassesMetaAnnotatedWithTestOrParameterizedTest() { 35 | JavaClasses importedClasses = new ClassFileImporter().importClasses( 36 | MetaTestExample.class, MetaParameterizedTestExample.class); 37 | 38 | ArchRule rule = methods().that(annotatedWithTestOrParameterizedTest(true)) 39 | .should(ArchConditions.beMetaAnnotatedWith(ANNOTATION_TEST) 40 | .or(ArchConditions.beMetaAnnotatedWith(ANNOTATION_PARAMETRIZED_TEST))); 41 | 42 | assertDoesNotThrow(() -> rule.check(importedClasses)); 43 | } 44 | 45 | private static final class TestExample { 46 | 47 | @Test 48 | void should() { 49 | assertTrue(true); 50 | } 51 | } 52 | 53 | private static final class ParameterizedTestExample { 54 | 55 | @ParameterizedTest 56 | @EmptySource 57 | void should(String empty) { 58 | assertTrue(true); 59 | } 60 | } 61 | 62 | private static class MetaTestExample { 63 | 64 | @TestAnnotation 65 | void should() { 66 | assertTrue(true); 67 | } 68 | } 69 | 70 | private static final class MetaParameterizedTestExample { 71 | 72 | @ParameterizedTestAnnotation 73 | @EmptySource 74 | void should(String empty) { 75 | assertTrue(true); 76 | } 77 | } 78 | 79 | @Test 80 | private @interface TestAnnotation { 81 | 82 | } 83 | 84 | @ParameterizedTest 85 | private @interface ParameterizedTestAnnotation { 86 | 87 | } 88 | } 89 | --------------------------------------------------------------------------------