├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── dependabot.yml └── workflows │ ├── dockerhub-description.yml │ └── release.yml ├── .gitignore ├── Dockerfile ├── README.md ├── build.gradle ├── docs └── images │ └── apply-without-saving.png ├── example ├── Configuration.h ├── Configuration_adv.h ├── multi-printer.md └── profile.yaml ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── lombok.config ├── settings.gradle └── src ├── main ├── java │ └── fr │ │ └── chuckame │ │ └── marlinfw │ │ └── configurator │ │ ├── MarlinConfigurator.java │ │ ├── change │ │ ├── BadLineChangeException.java │ │ ├── LineChange.java │ │ ├── LineChangeFormatter.java │ │ ├── LineChangeManager.java │ │ └── LineChangeValidator.java │ │ ├── command │ │ ├── ApplyCommand.java │ │ ├── Command.java │ │ ├── CommandRunner.java │ │ ├── DiffCommand.java │ │ ├── GenerateProfileCommand.java │ │ ├── HelpCommand.java │ │ ├── InvalidUseException.java │ │ └── ManuallyStoppedException.java │ │ ├── config │ │ └── JCommanderConfig.java │ │ ├── constant │ │ ├── Constant.java │ │ ├── ConstantLineDetails.java │ │ ├── ConstantLineInterpreter.java │ │ └── ProfileAdapter.java │ │ ├── profile │ │ ├── ConstantHelper.java │ │ ├── ProfileProperties.java │ │ └── ProfilePropertiesParser.java │ │ └── util │ │ ├── ConsoleHelper.java │ │ ├── ExceptionUtils.java │ │ └── FileHelper.java └── resources │ └── application.yml └── test ├── java └── fr │ └── chuckame │ └── marlinfw │ └── configurator │ ├── change │ ├── LineChangeFormatterTest.java │ ├── LineChangeManagerTest.java │ ├── LineChangeTest.java │ └── LineChangeValidatorTest.java │ ├── constant │ ├── ConstantLineInterpreterTest.java │ └── ProfileAdapterTest.java │ └── profile │ └── ProfilePropertiesParserTest.java └── resources ├── file.h ├── file2.h └── profile.yaml /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '[bug] ' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. `cd...` 16 | 2. `marlin-console-configurator command with bug` 17 | 4. See error 18 | 19 | **Expected behavior** 20 | A clear and concise description of what you expected to happen. 21 | 22 | **Screenshots** 23 | If applicable, add screenshots to help explain your problem. 24 | 25 | **Environnement (please complete the following information):** 26 | - OS: [e.g. Ubuntu] 27 | - Execution from docker/release/source code 28 | - Version [e.g. 1.1.42] 29 | 30 | **Additional context** 31 | Add any other context about the problem here, like Marlin files used, profiles... 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[feature] " 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context, Marlin files, profiles or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # Maintain dependencies for GitHub Actions 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "daily" 8 | -------------------------------------------------------------------------------- /.github/workflows/dockerhub-description.yml: -------------------------------------------------------------------------------- 1 | name: Update Docker Hub Description 2 | on: 3 | push: 4 | branches: 5 | - main 6 | paths: 7 | - README.md 8 | - .github/workflows/dockerhub-description.yml 9 | jobs: 10 | dockerHubDescription: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | 15 | - name: Docker Hub Description 16 | uses: peter-evans/dockerhub-description@v2 17 | with: 18 | username: ${{ secrets.DOCKERHUB_USERNAME }} 19 | password: ${{ secrets.DOCKERHUB_PASSWORD }} 20 | repository: chuckame/marlin-console-configurator 21 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Java project with Gradle 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-gradle 3 | 4 | name: Release 5 | 6 | on: 7 | push: 8 | tags: 9 | - 'v*.*.*' 10 | 11 | jobs: 12 | release: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout repository 16 | uses: actions/checkout@v2 17 | - name: Set up JDK 18 | uses: actions/setup-java@v1 19 | with: 20 | java-version: 11 21 | - name: Grant permissions for gradle 22 | run: chmod +x gradlew 23 | - name: Build and test 24 | run: ./gradlew test 25 | - name: Prepare packages 26 | run: ./gradlew bootDistZip 27 | - name: Create Release 28 | id: create_release 29 | uses: actions/create-release@v1 30 | env: 31 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 32 | with: 33 | tag_name: ${{ github.ref }} 34 | release_name: Release ${{ github.ref }} 35 | draft: false 36 | prerelease: false 37 | - name: Upload Release Asset 38 | id: upload-release-asset 39 | uses: actions/upload-release-asset@v1 40 | env: 41 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 42 | with: 43 | upload_url: ${{ steps.create_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above 44 | asset_path: ./build/distributions/marlin-console-configurator.zip 45 | asset_name: marlin-console-configurator.zip 46 | asset_content_type: application/zip 47 | 48 | - name: Push to DockerHub 49 | uses: docker/build-push-action@v1 50 | with: 51 | username: ${{ secrets.DOCKERHUB_USERNAME }} 52 | password: ${{ secrets.DOCKERHUB_TOKEN }} 53 | repository: chuckame/marlin-console-configurator 54 | tag_with_ref: true 55 | - name: Push to GitHub Packages 56 | uses: docker/build-push-action@v1 57 | with: 58 | username: ${{ github.actor }} 59 | password: ${{ secrets.GITHUB_TOKEN }} 60 | registry: docker.pkg.github.com 61 | repository: chuckame/marlin-console-configurator/marlin-console-configurator 62 | tag_with_ref: true 63 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/gradle,java,intellij 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=gradle,java,intellij 4 | 5 | ### Intellij ### 6 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 7 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 8 | 9 | # User-specific stuff 10 | .idea/**/workspace.xml 11 | .idea/**/tasks.xml 12 | .idea/**/usage.statistics.xml 13 | .idea/**/dictionaries 14 | .idea/**/shelf 15 | 16 | # Generated files 17 | .idea/**/contentModel.xml 18 | 19 | # Sensitive or high-churn files 20 | .idea/**/dataSources/ 21 | .idea/**/dataSources.ids 22 | .idea/**/dataSources.local.xml 23 | .idea/**/sqlDataSources.xml 24 | .idea/**/dynamic.xml 25 | .idea/**/uiDesigner.xml 26 | .idea/**/dbnavigator.xml 27 | 28 | # Gradle 29 | .idea/**/gradle.xml 30 | .idea/**/libraries 31 | 32 | # Gradle and Maven with auto-import 33 | # When using Gradle or Maven with auto-import, you should exclude module files, 34 | # since they will be recreated, and may cause churn. Uncomment if using 35 | # auto-import. 36 | # .idea/artifacts 37 | # .idea/compiler.xml 38 | # .idea/jarRepositories.xml 39 | # .idea/modules.xml 40 | # .idea/*.iml 41 | # .idea/modules 42 | # *.iml 43 | # *.ipr 44 | 45 | # CMake 46 | cmake-build-*/ 47 | 48 | # Mongo Explorer plugin 49 | .idea/**/mongoSettings.xml 50 | 51 | # File-based project format 52 | *.iws 53 | 54 | # IntelliJ 55 | out/ 56 | 57 | # mpeltonen/sbt-idea plugin 58 | .idea_modules/ 59 | 60 | # JIRA plugin 61 | atlassian-ide-plugin.xml 62 | 63 | # Cursive Clojure plugin 64 | .idea/replstate.xml 65 | 66 | # Crashlytics plugin (for Android Studio and IntelliJ) 67 | com_crashlytics_export_strings.xml 68 | crashlytics.properties 69 | crashlytics-build.properties 70 | fabric.properties 71 | 72 | # Editor-based Rest Client 73 | .idea/httpRequests 74 | 75 | # Android studio 3.1+ serialized cache file 76 | .idea/caches/build_file_checksums.ser 77 | 78 | ### Intellij Patch ### 79 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 80 | 81 | # *.iml 82 | # modules.xml 83 | # .idea/misc.xml 84 | # *.ipr 85 | 86 | # Sonarlint plugin 87 | # https://plugins.jetbrains.com/plugin/7973-sonarlint 88 | .idea/**/sonarlint/ 89 | 90 | # SonarQube Plugin 91 | # https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin 92 | .idea/**/sonarIssues.xml 93 | 94 | # Markdown Navigator plugin 95 | # https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced 96 | .idea/**/markdown-navigator.xml 97 | .idea/**/markdown-navigator-enh.xml 98 | .idea/**/markdown-navigator/ 99 | 100 | # Cache file creation bug 101 | # See https://youtrack.jetbrains.com/issue/JBR-2257 102 | .idea/$CACHE_FILE$ 103 | 104 | # CodeStream plugin 105 | # https://plugins.jetbrains.com/plugin/12206-codestream 106 | .idea/codestream.xml 107 | 108 | ### Java ### 109 | # Compiled class file 110 | *.class 111 | 112 | # Log file 113 | *.log 114 | 115 | # BlueJ files 116 | *.ctxt 117 | 118 | # Mobile Tools for Java (J2ME) 119 | .mtj.tmp/ 120 | 121 | # Package Files # 122 | *.jar 123 | *.war 124 | *.nar 125 | *.ear 126 | *.zip 127 | *.tar.gz 128 | *.rar 129 | 130 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 131 | hs_err_pid* 132 | 133 | ### Gradle ### 134 | .gradle 135 | build/ 136 | 137 | # Ignore Gradle GUI config 138 | gradle-app.setting 139 | 140 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 141 | !gradle-wrapper.jar 142 | 143 | # Cache of project 144 | .gradletasknamecache 145 | 146 | # # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 147 | # gradle/wrapper/gradle-wrapper.properties 148 | 149 | ### Gradle Patch ### 150 | **/build/ 151 | 152 | # End of https://www.toptal.com/developers/gitignore/api/gradle,java,intellij 153 | 154 | .idea/ 155 | 156 | ### MacOS ### 157 | .DS_Store 158 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM adoptopenjdk/openjdk11:alpine-jre 2 | ADD build/bootScripts/* /app/bin/ 3 | ADD build/libs/* /app/lib/ 4 | WORKDIR /app/files 5 | ENTRYPOINT ["/app/bin/marlin-console-configurator"] 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Release](https://github.com/Chuckame/marlin-console-configurator/workflows/Release/badge.svg) 2 | [![Buy me a beer](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=CQ6WPNYRBSWUU&item_name=Buy+me+a+beer¤cy_code=EUR) 3 | 4 | # Marlin Console Configurator 5 | 6 | *The life is too short to configure Marlin* 7 | 8 | If you want to modify and share easily your Marlin configuration, Marlin Console Configurator is for **YOU**. 9 | 10 | This tool will help you to disable, enable and changes values into your Marlin firmware (currently Configuration.h & Configuration_adv.h) with just only a minimalistic wanted config. 11 | 12 | - [Marlin Console Configurator](#marlin-console-configurator) 13 | * [Go quickly with docker](#go-quickly-with-docker) 14 | * [Docker alternative: Downloading binaries](#docker-alternative-downloading-binaries) 15 | + [Linux, MacOS, unix-like](#linux-macos-unix-like) 16 | + [Windows](#windows) 17 | * [Execute from sources](#execute-from-sources) 18 | * [How to use it](#how-to-use-it) 19 | + [Concrete example: show all changes without saving (just output to console)](#concrete-example-show-all-changes-without-saving-just-output-to-console) 20 | + [Concrete example: show all changes and save modifications to Marlin firmware files](#concrete-example-show-all-changes-and-save-modifications-to-marlin-firmware-files) 21 | + [Concrete example: Create a profile from your current config](#concrete-example-create-a-profile-from-your-current-config) 22 | * [Usage](#usage) 23 | * [A problem?](#a-problem) 24 | * [Credits](#credits) 25 | 26 | Cool things: 27 | - No need to fork Marlin repository, and have a git pull-rebase-conflicts-f**k. Just download/clone last Marlin sources and run marlin-console-configuration onto wanted profile. 28 | - No more Ctrl+F to find the constant to change 29 | - Same Marlin version/code, when you can apply the wanted profile of targeted printer 30 | - Share profile with friends, or on a tutorial. 31 | - It is not doing anything else than modifying C/C++ header files (`.h`), so it don't care about Marlin version ! 32 | - It will not add/remove/reorder constants, but just modifying only what you wanted into your profile :) 33 | 34 | Bad things: 35 | - This tool is so quick that you have no time to take a coffee while it is running :D 36 | - Complex guys that loves complex things won't love it... Too simple ! 37 | 38 | 39 | ## Go quickly with docker 40 | Just run the following command. 41 | 42 | ```shell script 43 | cd /path/to/Marlin 44 | docker run --rm -it -v ${PWD}:/app/files chuckame/marlin-console-configurator help 45 | ``` 46 | 47 | Since docker need to access to your Marlin configuration folder/files AND your profile, this is why there is `${PWD}:/app/files` volume. 48 | But, because of this volume, you cannot go trought the current folder's parents using `../` from marlin-console-configurator parameters. 49 | 50 | Example for `apply` command with the following folder architecture: 51 | ``` 52 | 3d-printing/ 53 | ├── profiles/ 54 | │ ├── ender-3-pro-base.yaml 55 | │ ├── ender-3-pro-mbl.yaml 56 | │ ├── ender-3-pro-runout-sensor.yaml 57 | │ └── ender-3-pro-abl.yaml 58 | └── MarlinFirmware/ 59 | └── Marlin/ 60 | ├── Configuration.h 61 | └── Configuration_adv.h 62 | ``` 63 | Execute the command: 64 | ```shell script 65 | cd 3d-printing/ 66 | docker run --rm -it -v ${PWD}:/app/files chuckame/marlin-console-configurator apply ./MarlinFirmware/Marlin -p ./profiles/ender-3-pro.yaml 67 | ``` 68 | 69 | > Actually only compatible with amd64 architectures. If you want to execute on other arch, like armv7 for raspberry pi, you can directly [use binaries](#downloading-binaries), while you can create an issue if you really want marlin-console-configurator on your arch. 70 | 71 | ## Docker alternative: Downloading binaries 72 | Download `marlin-console-configurator.zip` from [Releases](https://github.com/Chuckame/marlin-console-configurator/releases) and extract it. 73 | 74 | The unzipped folder will contain those files: 75 | ``` 76 | marlin-console-configurator/ 77 | ├── bin/ 78 | │ ├── marlin-console-configurator # Entrypoint script for unix-like OS 79 | │ └── marlin-console-configurator.bat # Entrypoint script for windows OS 80 | └── lib/ 81 | └── marlin-console-configurator.jar # The marlin-console-configurator 82 | ``` 83 | 84 | How to use it: 85 | 86 | ### Linux, MacOS, unix-like 87 | ```shell script 88 | cd ./marlin-console-configurator/bin 89 | ./marlin-console-configurator help 90 | ``` 91 | 92 | ### Windows 93 | ```shell script 94 | cd ./marlin-console-configurator/bin 95 | marlin-console-configurator.bat help 96 | ``` 97 | 98 | ## Execute from sources 99 | ```shell script 100 | ./gradlew bootRun help 101 | ``` 102 | 103 | ## How to use it 104 | 105 | ### Concrete example: show all changes without saving (just output to console) 106 | ```shell script 107 | marlin-console-configurator apply ./Marlin -p ./ender-3-base.yml ./ender-3-abl.yml 108 | ``` 109 | You will see something like this: 110 | ![apply-without-saving](docs/images/apply-without-saving.png) 111 | 112 | 113 | ### Concrete example: show all changes and save modifications to Marlin firmware files 114 | ```shell script 115 | marlin-console-configurator apply ./Marlin -p ./ender-3-abl.yml --save 116 | ``` 117 | > Only needed modifications will be saved. 118 | 119 | ### Concrete example: Create a profile from your current config 120 | ```shell script 121 | marlin-console-configurator generate-profile ./Marlin -o ./my-new-profile.yml 122 | ``` 123 | 124 | ## Usage 125 | 126 | ``` 127 | Usage: marlin-console-configurator [command] [command options] 128 | Commands: 129 | apply Apply the given profile to marlin constants files, that will enable, change value or disable constants into marlin 130 | configuration files 131 | Usage: apply [options] /path1 /path2 ... File or directory path(s) where all changes will be applied 132 | Options: 133 | * --profiles, -p 134 | Profile's path(s) (space separated) containing changes to apply. Format: yaml 135 | --save, -s 136 | When is present, will save changes to files. Else, just display changes without saving 137 | Default: false 138 | --verbose, -v 139 | when present, all non-changed line are printed 140 | Default: false 141 | --yes, -y 142 | when present, the changes will be saved without prompting the user 143 | Default: false 144 | 145 | diff Display differences between marlin configuration files 146 | Usage: diff [options] 147 | Options: 148 | * --left 149 | marlin configuration folder or files paths for the base of diff 150 | * --right 151 | marlin configuration folder or files paths to know what was changed since --source paths 152 | 153 | generate-profile Generate a profile from given marlin constants files 154 | Usage: generate-profile [options] /path1 /path2 ... The marlin constants folder or files paths 155 | Options: 156 | --diff-from 157 | The marlin constants folder or files paths from where you want to make a diff. If gathered, the generated profile will contains 158 | only the diff between those files and the command files 159 | * --output, -o 160 | The output profile path, will be overwritten if already existing file. If 'console' is specified, the profile will just be 161 | printed to the console 162 | 163 | help Display this help message 164 | Usage: help 165 | ``` 166 | 167 | ## A problem? 168 | Please go to [Issues](/issues), find a similar issue, or create a new one with your problem. 169 | 170 | ## Credits 171 | Made with :heart: by Chuckame 172 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | id 'org.springframework.boot' version '2.3.4.RELEASE' 4 | id 'application' 5 | } 6 | 7 | group 'fr.chuckame.marlinfw' 8 | version '1.1.0' 9 | sourceCompatibility = '11' 10 | 11 | repositories { 12 | mavenCentral() 13 | } 14 | 15 | apply plugin: 'io.spring.dependency-management' 16 | dependencies { 17 | implementation 'org.springframework.boot:spring-boot-starter' 18 | implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-yaml' 19 | compile 'io.projectreactor:reactor-core' 20 | implementation 'com.beust:jcommander:1.78' 21 | implementation 'com.google.guava:guava:30.0-jre' 22 | 23 | compileOnly 'org.projectlombok:lombok' 24 | annotationProcessor 'org.projectlombok:lombok' 25 | 26 | testCompile('org.springframework.boot:spring-boot-starter-test') { 27 | exclude group: 'junit', module: 'junit' 28 | exclude group: 'org.junit.vintage', module: 'junit-vintage-engine' 29 | } 30 | testCompile 'io.projectreactor:reactor-test' 31 | } 32 | 33 | application { 34 | mainClass = 'fr.chuckame.marlinfw.configurator.MarlinConfigurator' 35 | applicationDefaultJvmArgs = ['-Dapp-console-name=marlin-console-configurator'] 36 | } 37 | 38 | test { 39 | useJUnitPlatform() 40 | } 41 | 42 | bootDistZip.setArchiveFileName('marlin-console-configurator.zip') 43 | -------------------------------------------------------------------------------- /docs/images/apply-without-saving.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Chuckame/marlin-console-configurator/a49b2fe857768275e5bf4ed6ed3e92489beea5f6/docs/images/apply-without-saving.png -------------------------------------------------------------------------------- /example/Configuration.h: -------------------------------------------------------------------------------- 1 | #define CONSTANT_WITH_VALUE_STRING "a string value that requires quotes" 2 | #define CONSTANT_WITH_VALUE_COMPLEX_STRING "a complex value that requires quotes and\nreturns" 3 | #define CONSTANT_WITHOUT_VALUE 4 | #define WeiRd_conStANt hey 5 | 6 | //#define DISABLED_CONSTANT 7 | -------------------------------------------------------------------------------- /example/Configuration_adv.h: -------------------------------------------------------------------------------- 1 | #define CONSTANT_WITH_VALUE_ARRAY { 14, 15, 28 } 2 | #define CONSTANT_WITH_VALUE_NUMBER 1 3 | 4 | //#define DiSaBLeD_CoNsTANt2 5 | -------------------------------------------------------------------------------- /example/multi-printer.md: -------------------------------------------------------------------------------- 1 | # How to use marlin-console-configurator with multiple printers ? 2 | 3 | - 1. Locate your profiles folder, here we will use `/data/printing/profiles` containing profiles for each printers. 4 | 5 | - 2. get the wanted Marlin code with `git clone https://github.com/MarlinFirmware/Marlin.git /data/printing/Marlin` 6 | 7 | - 3. go to root folder: `cd /data/printing` 8 | 9 | - 4. Apply the ender3 profile with ABL : `docker run --rm -it -v ${PWD}:/app/files chuckame/marlin-console-configurator apply Marlin/Marlin -p profiles/ender3/base.yml profiles/ender3/abl.yml --save` 10 | 11 | - 5. You will be prompted for saving the displayed configuration, just type `y` if you're okay 12 | 13 | - 6. Build Marlin and send the firmware to the current configured printer. 14 | 15 | - 7. Exec `cd /data/printing/Marlin && git reset --hard` to reset Marlin to default config 16 | 17 | - 8. Go to `3.` and target another printer config 18 | -------------------------------------------------------------------------------- /example/profile.yaml: -------------------------------------------------------------------------------- 1 | enabled: 2 | CONSTANT_WITH_VALUE_ARRAY: '{ 14, 15, 28 }' 3 | CONSTANT_WITH_VALUE_NUMBER: 1 4 | CONSTANT_WITH_VALUE_COMPLEX_STRING: '"a complex value that requires quotes and\nreturns"' 5 | CONSTANT_WITHOUT_VALUE: # ':' is mandatory, even if no value !! 6 | CONSTANT_WITH_VALUE_STRING: '"a string value that requires quotes"' 7 | WeiRd_conStANt: hey 8 | disabled: 9 | - DISABLED_CONSTANT 10 | - DiSaBLeD_CoNsTANt2 11 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Chuckame/marlin-console-configurator/a49b2fe857768275e5bf4ed6ed3e92489beea5f6/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Oct 07 14:18:54 CEST 2020 2 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip 3 | distributionBase=GRADLE_USER_HOME 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | # Determine the Java command to use to start the JVM. 86 | if [ -n "$JAVA_HOME" ] ; then 87 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 88 | # IBM's JDK on AIX uses strange locations for the executables 89 | JAVACMD="$JAVA_HOME/jre/sh/java" 90 | else 91 | JAVACMD="$JAVA_HOME/bin/java" 92 | fi 93 | if [ ! -x "$JAVACMD" ] ; then 94 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 95 | 96 | Please set the JAVA_HOME variable in your environment to match the 97 | location of your Java installation." 98 | fi 99 | else 100 | JAVACMD="java" 101 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 102 | 103 | Please set the JAVA_HOME variable in your environment to match the 104 | location of your Java installation." 105 | fi 106 | 107 | # Increase the maximum file descriptors if we can. 108 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 109 | MAX_FD_LIMIT=`ulimit -H -n` 110 | if [ $? -eq 0 ] ; then 111 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 112 | MAX_FD="$MAX_FD_LIMIT" 113 | fi 114 | ulimit -n $MAX_FD 115 | if [ $? -ne 0 ] ; then 116 | warn "Could not set maximum file descriptor limit: $MAX_FD" 117 | fi 118 | else 119 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 120 | fi 121 | fi 122 | 123 | # For Darwin, add options to specify how the application appears in the dock 124 | if $darwin; then 125 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 126 | fi 127 | 128 | # For Cygwin or MSYS, switch paths to Windows format before running java 129 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 130 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 131 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 132 | JAVACMD=`cygpath --unix "$JAVACMD"` 133 | 134 | # We build the pattern for arguments to be converted via cygpath 135 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 136 | SEP="" 137 | for dir in $ROOTDIRSRAW ; do 138 | ROOTDIRS="$ROOTDIRS$SEP$dir" 139 | SEP="|" 140 | done 141 | OURCYGPATTERN="(^($ROOTDIRS))" 142 | # Add a user-defined pattern to the cygpath arguments 143 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 144 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 145 | fi 146 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 147 | i=0 148 | for arg in "$@" ; do 149 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 150 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 151 | 152 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 153 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 154 | else 155 | eval `echo args$i`="\"$arg\"" 156 | fi 157 | i=`expr $i + 1` 158 | done 159 | case $i in 160 | 0) set -- ;; 161 | 1) set -- "$args0" ;; 162 | 2) set -- "$args0" "$args1" ;; 163 | 3) set -- "$args0" "$args1" "$args2" ;; 164 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 165 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 166 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 167 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 168 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 169 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 170 | esac 171 | fi 172 | 173 | # Escape application args 174 | save () { 175 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 176 | echo " " 177 | } 178 | APP_ARGS=`save "$@"` 179 | 180 | # Collect all arguments for the java command, following the shell quoting and substitution rules 181 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 182 | 183 | exec "$JAVACMD" "$@" 184 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 33 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 34 | 35 | @rem Find java.exe 36 | if defined JAVA_HOME goto findJavaFromJavaHome 37 | 38 | set JAVA_EXE=java.exe 39 | %JAVA_EXE% -version >NUL 2>&1 40 | if "%ERRORLEVEL%" == "0" goto init 41 | 42 | echo. 43 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 44 | echo. 45 | echo Please set the JAVA_HOME variable in your environment to match the 46 | echo location of your Java installation. 47 | 48 | goto fail 49 | 50 | :findJavaFromJavaHome 51 | set JAVA_HOME=%JAVA_HOME:"=% 52 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 53 | 54 | if exist "%JAVA_EXE%" goto init 55 | 56 | echo. 57 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 58 | echo. 59 | echo Please set the JAVA_HOME variable in your environment to match the 60 | echo location of your Java installation. 61 | 62 | goto fail 63 | 64 | :init 65 | @rem Get command-line arguments, handling Windows variants 66 | 67 | if not "%OS%" == "Windows_NT" goto win9xME_args 68 | 69 | :win9xME_args 70 | @rem Slurp the command line arguments. 71 | set CMD_LINE_ARGS= 72 | set _SKIP=2 73 | 74 | :win9xME_args_slurp 75 | if "x%~1" == "x" goto execute 76 | 77 | set CMD_LINE_ARGS=%* 78 | 79 | :execute 80 | @rem Setup the command line 81 | 82 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 83 | 84 | @rem Execute Gradle 85 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 86 | 87 | :end 88 | @rem End local scope for the variables with windows NT shell 89 | if "%ERRORLEVEL%"=="0" goto mainEnd 90 | 91 | :fail 92 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 93 | rem the _cmd.exe /c_ return code! 94 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 95 | exit /b 1 96 | 97 | :mainEnd 98 | if "%OS%"=="Windows_NT" endlocal 99 | 100 | :omega 101 | -------------------------------------------------------------------------------- /lombok.config: -------------------------------------------------------------------------------- 1 | lombok.anyconstructor.addconstructorproperties=true 2 | lombok.copyableannotations += com.fasterxml.jackson.annotation.JsonProperty 3 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'marlin-console-configurator' 2 | 3 | -------------------------------------------------------------------------------- /src/main/java/fr/chuckame/marlinfw/configurator/MarlinConfigurator.java: -------------------------------------------------------------------------------- 1 | package fr.chuckame.marlinfw.configurator; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class MarlinConfigurator { 8 | public static void main(final String[] args) { 9 | SpringApplication.run(MarlinConfigurator.class, args); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/fr/chuckame/marlinfw/configurator/change/BadLineChangeException.java: -------------------------------------------------------------------------------- 1 | package fr.chuckame.marlinfw.configurator.change; 2 | 3 | import lombok.Getter; 4 | import lombok.RequiredArgsConstructor; 5 | 6 | @RequiredArgsConstructor 7 | public class BadLineChangeException extends RuntimeException { 8 | private final String message; 9 | @Getter 10 | private final LineChange lineChange; 11 | 12 | @Override 13 | public String getMessage() { 14 | return String.format("Line[%s]: %s", lineChange.getLineNumber(), message); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/fr/chuckame/marlinfw/configurator/change/LineChange.java: -------------------------------------------------------------------------------- 1 | package fr.chuckame.marlinfw.configurator.change; 2 | 3 | import fr.chuckame.marlinfw.configurator.constant.ConstantLineDetails; 4 | import lombok.AccessLevel; 5 | import lombok.AllArgsConstructor; 6 | import lombok.Builder; 7 | import lombok.Data; 8 | import lombok.Setter; 9 | import org.springframework.lang.Nullable; 10 | 11 | @Data 12 | @Builder(access = AccessLevel.PACKAGE) 13 | @AllArgsConstructor(access = AccessLevel.PRIVATE) 14 | public class LineChange { 15 | private final String line; 16 | private final int lineNumber; 17 | private final DiffEnum diff; 18 | @Nullable 19 | private final LineChangeConstant constant; 20 | @Nullable 21 | private final ConstantLineDetails constantLineDetails; 22 | @Nullable 23 | @Setter(AccessLevel.PACKAGE) 24 | private String violation; 25 | 26 | LineChange(final String line, final int lineNumber) { 27 | this.line = line; 28 | this.lineNumber = lineNumber; 29 | diff = DiffEnum.DO_NOTHING; 30 | constant = null; 31 | constantLineDetails = null; 32 | violation = null; 33 | } 34 | 35 | @Data 36 | @Builder 37 | public static class LineChangeConstant { 38 | private final String name; 39 | private final String currentValue; 40 | @Nullable 41 | private String wantedValue; 42 | } 43 | 44 | public enum DiffEnum { 45 | DO_NOTHING, 46 | TO_ENABLE, 47 | TO_ENABLE_AND_CHANGE_VALUE, 48 | CHANGE_VALUE, 49 | TO_DISABLE, 50 | ERROR 51 | } 52 | 53 | public boolean isConstant() { 54 | return constant != null; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/fr/chuckame/marlinfw/configurator/change/LineChangeFormatter.java: -------------------------------------------------------------------------------- 1 | package fr.chuckame.marlinfw.configurator.change; 2 | 3 | import org.springframework.lang.Nullable; 4 | import org.springframework.stereotype.Component; 5 | import org.springframework.util.StringUtils; 6 | 7 | import java.util.Map; 8 | 9 | @Component 10 | public class LineChangeFormatter { 11 | private static final String DISABLE = 12 | "Disable "; 13 | private static final String ENABLE = 14 | "Enable "; 15 | private static final String CHANGE = 16 | "Change "; 17 | private static final String ERROR = 18 | "Error "; 19 | private static final String ENABLE_AND_CHANGE = 20 | "Enable & Change"; 21 | 22 | private static final Map FORMAT_DIFF_MAPPING = Map.of( 23 | LineChange.DiffEnum.TO_DISABLE, DISABLE, 24 | LineChange.DiffEnum.TO_ENABLE, ENABLE, 25 | LineChange.DiffEnum.TO_ENABLE_AND_CHANGE_VALUE, ENABLE_AND_CHANGE, 26 | LineChange.DiffEnum.CHANGE_VALUE, CHANGE, 27 | LineChange.DiffEnum.ERROR, ERROR 28 | ); 29 | 30 | /** 31 | * To format like CONSTANT: E&C 15 → 18
32 | */ 33 | public String format(final LineChange lineChange) { 34 | if (!lineChange.isConstant()) { 35 | return null; 36 | } 37 | final var output = new StringBuilder(); 38 | if (FORMAT_DIFF_MAPPING.containsKey(lineChange.getDiff())) { 39 | output.append(FORMAT_DIFF_MAPPING.get(lineChange.getDiff())); 40 | output.append(" "); 41 | } 42 | output.append(lineChange.getConstant().getName()); 43 | switch (lineChange.getDiff()) { 44 | case DO_NOTHING: 45 | output.append(": Nothing to do"); 46 | break; 47 | case TO_DISABLE: 48 | case TO_ENABLE: 49 | break; 50 | case TO_ENABLE_AND_CHANGE_VALUE: 51 | case CHANGE_VALUE: 52 | output.append(": "); 53 | formatValue(lineChange.getConstant().getCurrentValue(), output); 54 | output.append(" → "); 55 | formatValue(lineChange.getConstant().getWantedValue(), output); 56 | break; 57 | case ERROR: 58 | output.append(": "); 59 | output.append(lineChange.getViolation()); 60 | break; 61 | default: 62 | throw new UnsupportedOperationException("Unknown diff: " + lineChange.getDiff()); 63 | } 64 | return output.toString(); 65 | } 66 | 67 | private void formatValue(@Nullable final String value, final StringBuilder output) { 68 | if (StringUtils.hasText(value)) { 69 | output.append(value); 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/main/java/fr/chuckame/marlinfw/configurator/change/LineChangeManager.java: -------------------------------------------------------------------------------- 1 | package fr.chuckame.marlinfw.configurator.change; 2 | 3 | import fr.chuckame.marlinfw.configurator.constant.Constant; 4 | import fr.chuckame.marlinfw.configurator.constant.ConstantLineDetails; 5 | import fr.chuckame.marlinfw.configurator.constant.ConstantLineInterpreter; 6 | import lombok.RequiredArgsConstructor; 7 | import org.springframework.lang.Nullable; 8 | import org.springframework.stereotype.Component; 9 | import reactor.core.publisher.Flux; 10 | import reactor.core.publisher.Mono; 11 | 12 | import java.util.Collection; 13 | import java.util.List; 14 | import java.util.Map; 15 | import java.util.Objects; 16 | import java.util.Optional; 17 | import java.util.function.Predicate; 18 | import java.util.stream.Collectors; 19 | 20 | @Component 21 | @RequiredArgsConstructor 22 | public class LineChangeManager { 23 | private final ConstantLineInterpreter constantLineInterpreter; 24 | private final LineChangeValidator lineChangeValidator; 25 | 26 | public Mono prepareChange(final String line, final int lineNumber, final Map wantedConstants) { 27 | return constantLineInterpreter.parseLine(line) 28 | .map(parsedConstant -> toLineChange(line, lineNumber, parsedConstant, wantedConstants 29 | .get(parsedConstant.getConstant().getName()))) 30 | .switchIfEmpty(Mono.fromSupplier(() -> new LineChange(line, lineNumber))); 31 | } 32 | 33 | public Mono applyChange(final LineChange change) { 34 | if (!change.isConstant()) { 35 | return Mono.fromSupplier(change::getLine); 36 | } 37 | switch (change.getDiff()) { 38 | case ERROR: 39 | return Mono.error(() -> new BadLineChangeException(change.getViolation(), change)); 40 | case CHANGE_VALUE: 41 | return constantLineInterpreter.changeValue(change.getConstantLineDetails(), change.getConstant().getWantedValue()); 42 | case TO_ENABLE_AND_CHANGE_VALUE: 43 | return constantLineInterpreter.enableLineAndChangeValue(change.getConstantLineDetails(), change.getConstant().getWantedValue()); 44 | case TO_ENABLE: 45 | return constantLineInterpreter.enableLine(change.getConstantLineDetails()); 46 | case TO_DISABLE: 47 | return constantLineInterpreter.disableLine(change.getConstantLineDetails()); 48 | case DO_NOTHING: 49 | return Mono.just(change.getLine()); 50 | default: 51 | throw new UnsupportedOperationException("Unknown diff: " + change.getDiff()); 52 | } 53 | } 54 | 55 | public Flux getUnusedWantedConstants(final Collection changes, final Map wantedConstants) { 56 | final List constantsFound = changes.stream() 57 | .filter(LineChange::isConstant) 58 | .map(LineChange::getConstant) 59 | .map(LineChange.LineChangeConstant::getName) 60 | .collect(Collectors.toList()); 61 | return Flux.fromIterable(wantedConstants.keySet()) 62 | .filter(Predicate.not(constantsFound::contains)); 63 | } 64 | 65 | public LineChange toLineChange(final String line, final int lineNumber, final Constant parsedConstant, 66 | @Nullable final Constant wantedConstant, @Nullable final ConstantLineDetails lineDetails) { 67 | final var violation = lineChangeValidator.getViolation(parsedConstant, wantedConstant); 68 | return LineChange.builder() 69 | .line(line) 70 | .lineNumber(lineNumber) 71 | .constant(LineChange.LineChangeConstant.builder() 72 | .name(parsedConstant.getName()) 73 | .currentValue(parsedConstant.getValue()) 74 | .wantedValue(Optional.ofNullable(wantedConstant).map(Constant::getValue).orElse(null)) 75 | .build()) 76 | .diff(violation != null ? LineChange.DiffEnum.ERROR : computeDiff(parsedConstant, wantedConstant)) 77 | .violation(violation) 78 | .constantLineDetails(lineDetails) 79 | .build(); 80 | } 81 | 82 | private LineChange toLineChange(final String line, final int lineNumber, final ConstantLineInterpreter.ParsedConstant parsedConstant, @Nullable final Constant wantedConstant) { 83 | return toLineChange(line, lineNumber, parsedConstant.getConstant(), wantedConstant, parsedConstant.getConstantLineDetails()); 84 | } 85 | 86 | private LineChange.DiffEnum computeDiff(final Constant parsedConstant, @Nullable final Constant wantedConstant) { 87 | if (parsedConstant == null || wantedConstant == null) { 88 | return LineChange.DiffEnum.DO_NOTHING; 89 | } 90 | if (!parsedConstant.isEnabled() && wantedConstant.isEnabled() && !valueEquals(parsedConstant, wantedConstant)) { 91 | return LineChange.DiffEnum.TO_ENABLE_AND_CHANGE_VALUE; 92 | } 93 | if (!parsedConstant.isEnabled() && wantedConstant.isEnabled() && valueEquals(parsedConstant, wantedConstant)) { 94 | return LineChange.DiffEnum.TO_ENABLE; 95 | } 96 | if (parsedConstant.isEnabled() && wantedConstant.isEnabled() && !valueEquals(parsedConstant, wantedConstant)) { 97 | return LineChange.DiffEnum.CHANGE_VALUE; 98 | } 99 | if (parsedConstant.isEnabled() && !wantedConstant.isEnabled()) { 100 | return LineChange.DiffEnum.TO_DISABLE; 101 | } 102 | return LineChange.DiffEnum.DO_NOTHING; 103 | } 104 | 105 | private boolean valueEquals(final Constant parsedConstant, final Constant wantedConstant) { 106 | return Objects.equals(parsedConstant.getValue(), wantedConstant.getValue()); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/main/java/fr/chuckame/marlinfw/configurator/change/LineChangeValidator.java: -------------------------------------------------------------------------------- 1 | package fr.chuckame.marlinfw.configurator.change; 2 | 3 | import fr.chuckame.marlinfw.configurator.constant.Constant; 4 | import org.springframework.stereotype.Component; 5 | 6 | @Component 7 | public class LineChangeValidator { 8 | private static final String MISSING_WANTED_VALUE_MESSAGE = "Wanted value should be defined"; 9 | private static final String MISSING_PARSED_VALUE_MESSAGE = "Wanted value should not be defined"; 10 | 11 | public String getViolation(final Constant parsed, final Constant wanted) { 12 | if (parsed == null || wanted == null) { 13 | return null; 14 | } 15 | if (wanted.isEnabled()) { 16 | if (parsed.getValue() != null && wanted.getValue() == null) { 17 | return MISSING_WANTED_VALUE_MESSAGE; 18 | } 19 | if (parsed.getValue() == null && wanted.getValue() != null) { 20 | return MISSING_PARSED_VALUE_MESSAGE; 21 | } 22 | } 23 | return null; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/fr/chuckame/marlinfw/configurator/command/ApplyCommand.java: -------------------------------------------------------------------------------- 1 | package fr.chuckame.marlinfw.configurator.command; 2 | 3 | import com.beust.jcommander.Parameter; 4 | import com.beust.jcommander.Parameters; 5 | import fr.chuckame.marlinfw.configurator.change.LineChange; 6 | import fr.chuckame.marlinfw.configurator.change.LineChangeFormatter; 7 | import fr.chuckame.marlinfw.configurator.change.LineChangeManager; 8 | import fr.chuckame.marlinfw.configurator.constant.Constant; 9 | import fr.chuckame.marlinfw.configurator.constant.ProfileAdapter; 10 | import fr.chuckame.marlinfw.configurator.profile.ProfilePropertiesParser; 11 | import fr.chuckame.marlinfw.configurator.util.ConsoleHelper; 12 | import fr.chuckame.marlinfw.configurator.util.FileHelper; 13 | import lombok.RequiredArgsConstructor; 14 | import org.springframework.stereotype.Component; 15 | import reactor.core.publisher.Flux; 16 | import reactor.core.publisher.Mono; 17 | import reactor.util.function.Tuple2; 18 | import reactor.util.function.Tuples; 19 | 20 | import java.nio.file.Path; 21 | import java.util.Collection; 22 | import java.util.List; 23 | import java.util.Map; 24 | import java.util.function.Predicate; 25 | import java.util.stream.Collectors; 26 | 27 | @Component 28 | @Parameters(commandNames = "apply", commandDescription = "Apply the given profile to marlin constants files, that will enable, change value or disable constants into marlin configuration files") 29 | @RequiredArgsConstructor 30 | public class ApplyCommand implements Command { 31 | @Parameter(required = true, description = "/path1 /path2 ...\tFile or directory path(s) where all changes will be applied") 32 | private List filesPath; 33 | @Parameter(names = {"--profiles", "-p"}, variableArity = true, required = true, description = "Profile's path(s) (space separated) containing changes to apply. Format: yaml") 34 | private List profilePaths; 35 | @Parameter(names = {"--save", "-s"}, description = "When is present, will save changes to files. Else, just display changes without saving") 36 | private boolean doSave; 37 | @Parameter(names = {"--yes", "-y"}, description = "when present, the changes will be saved without prompting the user") 38 | private boolean applyWithoutPrompt; 39 | @Parameter(names = {"--verbose", "-v"}, description = "when present, all non-changed line are printed") 40 | private boolean verbose; 41 | 42 | private final ProfileAdapter profileAdapter; 43 | private final LineChangeManager lineChangeManager; 44 | private final LineChangeFormatter lineChangeFormatter; 45 | private final ProfilePropertiesParser profilePropertiesParser; 46 | private final FileHelper fileHelper; 47 | private final ConsoleHelper consoleHelper; 48 | 49 | @Override 50 | public Mono run() { 51 | return profilePropertiesParser 52 | .parseFromFiles(profilePaths) 53 | .map(profileAdapter::profileToConstants) 54 | .flatMap(wantedConstants -> 55 | prepareChanges(wantedConstants) 56 | .flatMap(changes -> printChanges(changes) 57 | .then(printUnusedConstants(changes, wantedConstants)) 58 | .then(applyAndSaveChangesIfNeeded(changes))) 59 | ); 60 | } 61 | 62 | private Mono>> prepareChanges(final Map wantedConstants) { 63 | return fileHelper.listFiles(filesPath) 64 | .flatMap(filePath -> fileHelper.lines(filePath) 65 | .index() 66 | .concatMap(line -> lineChangeManager.prepareChange(line.getT2(), line.getT1().intValue(), wantedConstants)) 67 | .collectList() 68 | .filter(changes -> changes.stream().anyMatch(LineChange::isConstant)) 69 | .map(changes -> Tuples.of(filePath, changes))) 70 | .collectMap(Tuple2::getT1, Tuple2::getT2); 71 | } 72 | 73 | private Mono printChanges(final Map> changes) { 74 | return Flux.fromIterable(changes.entrySet()) 75 | .concatMap(fileChanges -> Flux.concat( 76 | Flux.fromIterable(fileChanges.getValue()) 77 | .filter(this::isModifyingChange) 78 | .count() 79 | .filter(count -> verbose || count > 0) 80 | .doOnNext(count -> consoleHelper.writeLine(String.format("%s change(s) to apply for file %s:", count, fileChanges 81 | .getKey()), ConsoleHelper.FormatterEnum.UNDERLINED, ConsoleHelper.FormatterEnum.BOLD, ConsoleHelper.ForegroundColorEnum.GREEN)), 82 | Flux.fromIterable(fileChanges.getValue()) 83 | .filter(LineChange::isConstant) 84 | .filter(this::isModifyingChange) 85 | .doOnNext(change -> consoleHelper.writeLine(lineChangeFormatter.format(change), getChangeColor(change))), 86 | Flux.fromIterable(fileChanges.getValue()) 87 | .filter(LineChange::isConstant) 88 | .filter(c -> verbose && !isModifyingChange(c)) 89 | .doOnNext(change -> consoleHelper.writeLine(lineChangeFormatter.format(change), getChangeColor(change))), 90 | Mono.fromRunnable(consoleHelper::newLine) 91 | )) 92 | .then() 93 | ; 94 | } 95 | 96 | private ConsoleHelper.ConsoleStyle getChangeColor(final LineChange change) { 97 | switch (change.getDiff()) { 98 | case ERROR: 99 | return ConsoleHelper.ForegroundColorEnum.RED; 100 | case CHANGE_VALUE: 101 | return ConsoleHelper.ForegroundColorEnum.LIGHT_BLUE; 102 | case TO_DISABLE: 103 | return ConsoleHelper.ForegroundColorEnum.LIGHT_YELLOW; 104 | case TO_ENABLE: 105 | return ConsoleHelper.ForegroundColorEnum.LIGHT_CYAN; 106 | case TO_ENABLE_AND_CHANGE_VALUE: 107 | return ConsoleHelper.ForegroundColorEnum.LIGHT_MAGENTA; 108 | case DO_NOTHING: 109 | default: 110 | return ConsoleHelper.ForegroundColorEnum.DARK_GRAY; 111 | } 112 | } 113 | 114 | private Mono printUnusedConstants(final Map> changes, final Map wantedConstants) { 115 | return lineChangeManager.getUnusedWantedConstants(changes.values().stream().flatMap(List::stream).collect(Collectors.toList()), wantedConstants) 116 | .collectList() 117 | .filter(Predicate.not(List::isEmpty)) 118 | .doOnNext(unusedConstants -> consoleHelper.writeLine(String.format("Still some unused constants: %s", unusedConstants))) 119 | .then(); 120 | } 121 | 122 | private Mono applyAndSaveChangesIfNeeded(final Map> changes) { 123 | if (!doSave) { 124 | return Mono.empty(); 125 | } 126 | return checkIfUserAgree().then(applyAndSaveChanges(changes)); 127 | } 128 | 129 | private Mono checkIfUserAgree() { 130 | if (applyWithoutPrompt) { 131 | return Mono.empty(); 132 | } 133 | return Mono.fromRunnable(() -> consoleHelper.writeLine("Apply changes ? type 'y' to apply changes, or everything else to cancel")) 134 | .then(Mono.fromSupplier(consoleHelper::readLine)) 135 | .filter("y"::equals) 136 | .switchIfEmpty(Mono.error(() -> new ManuallyStoppedException("User refused to apply"))) 137 | .then(); 138 | } 139 | 140 | private Mono applyAndSaveChanges(final Map> changes) { 141 | return Flux.fromIterable(changes.entrySet()) 142 | .filter(e -> onlyChangedFile(e.getValue())) 143 | .groupBy(Map.Entry::getKey, Map.Entry::getValue) 144 | .flatMap(fileChanges -> fileHelper.write(fileChanges.key(), true, fileChanges.flatMap(this::applyChanges))) 145 | .then(); 146 | } 147 | 148 | private boolean onlyChangedFile(final List changes) { 149 | return changes.stream().anyMatch(this::isModifyingChange); 150 | } 151 | 152 | private boolean isModifyingChange(final LineChange change) { 153 | return !LineChange.DiffEnum.DO_NOTHING.equals(change.getDiff()); 154 | } 155 | 156 | private Flux applyChanges(final Collection changes) { 157 | return Flux.fromIterable(changes).flatMap(lineChangeManager::applyChange); 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/main/java/fr/chuckame/marlinfw/configurator/command/Command.java: -------------------------------------------------------------------------------- 1 | package fr.chuckame.marlinfw.configurator.command; 2 | 3 | import reactor.core.publisher.Mono; 4 | 5 | public interface Command { 6 | Mono run(); 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/fr/chuckame/marlinfw/configurator/command/CommandRunner.java: -------------------------------------------------------------------------------- 1 | package fr.chuckame.marlinfw.configurator.command; 2 | 3 | import com.beust.jcommander.JCommander; 4 | import com.beust.jcommander.ParameterException; 5 | import fr.chuckame.marlinfw.configurator.util.ConsoleHelper; 6 | import lombok.RequiredArgsConstructor; 7 | import lombok.extern.slf4j.Slf4j; 8 | import org.springframework.boot.CommandLineRunner; 9 | import org.springframework.stereotype.Component; 10 | 11 | import java.nio.file.FileAlreadyExistsException; 12 | import java.nio.file.NoSuchFileException; 13 | import java.nio.file.Path; 14 | 15 | @Slf4j 16 | @Component 17 | @RequiredArgsConstructor 18 | public class CommandRunner implements CommandLineRunner { 19 | private final JCommander jCommander; 20 | private final ConsoleHelper consoleHelper; 21 | 22 | @Override 23 | public void run(final String[] args) throws Exception { 24 | try { 25 | findCommandByAlias(parseAlias(args)).run().blockOptional(); 26 | } catch (final ManuallyStoppedException e) { 27 | consoleHelper.writeErrorLine(e.getMessage()); 28 | System.exit(e.getExitCode()); 29 | } catch (final Exception e) { 30 | if (e.getCause() instanceof NoSuchFileException) { 31 | consoleHelper.writeErrorLine("File not found: " + Path.of(e.getCause().getMessage()).toAbsolutePath()); 32 | System.exit(4); 33 | } else if (e.getCause() instanceof FileAlreadyExistsException) { 34 | consoleHelper.writeErrorLine("File already present: " + Path.of(e.getCause().getMessage()).toAbsolutePath()); 35 | System.exit(6); 36 | } else { 37 | consoleHelper.writeErrorLine(e.getMessage()); 38 | System.exit(5); 39 | } 40 | } 41 | } 42 | 43 | private String parseAlias(final String[] args) { 44 | String errorMessage = null; 45 | try { 46 | jCommander.parse(args); 47 | } catch (final ParameterException e) { 48 | errorMessage = e.getMessage(); 49 | } 50 | if (errorMessage == null && jCommander.getParsedAlias() != null) { 51 | return jCommander.getParsedAlias(); 52 | } 53 | 54 | if (errorMessage == null) { 55 | consoleHelper.writeErrorLine("No argument passed"); 56 | } else { 57 | consoleHelper.writeErrorLine("Bad argument: " + errorMessage); 58 | } 59 | consoleHelper.writeLine(jCommander.getUsageFormatter()::usage); 60 | System.exit(InvalidUseException.EXIT_CODE); 61 | return null; 62 | } 63 | 64 | private Command findCommandByAlias(final String alias) { 65 | return (Command) jCommander.findCommandByAlias(alias).getObjects().get(0); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/fr/chuckame/marlinfw/configurator/command/DiffCommand.java: -------------------------------------------------------------------------------- 1 | package fr.chuckame.marlinfw.configurator.command; 2 | 3 | import com.beust.jcommander.Parameter; 4 | import com.beust.jcommander.Parameters; 5 | import com.google.common.collect.MapDifference; 6 | import com.google.common.collect.Maps; 7 | import fr.chuckame.marlinfw.configurator.change.LineChangeFormatter; 8 | import fr.chuckame.marlinfw.configurator.change.LineChangeManager; 9 | import fr.chuckame.marlinfw.configurator.constant.Constant; 10 | import fr.chuckame.marlinfw.configurator.profile.ConstantHelper; 11 | import fr.chuckame.marlinfw.configurator.util.ConsoleHelper; 12 | import lombok.RequiredArgsConstructor; 13 | import org.springframework.stereotype.Component; 14 | import reactor.core.publisher.Flux; 15 | import reactor.core.publisher.Mono; 16 | 17 | import java.nio.file.Path; 18 | import java.util.List; 19 | 20 | @Component 21 | @Parameters(commandNames = "diff", commandDescription = "Display differences between marlin configuration files") 22 | @RequiredArgsConstructor 23 | public class DiffCommand implements Command { 24 | @Parameter(names = {"--left"}, variableArity = true, required = true, description = "marlin configuration folder or files paths for the base of diff") 25 | private List leftFiles; 26 | @Parameter(names = {"--right"}, variableArity = true, required = true, description = "marlin configuration folder or files paths to know what was changed since --source paths") 27 | private List rightFiles; 28 | 29 | private final LineChangeManager lineChangeManager; 30 | private final LineChangeFormatter lineChangeFormatter; 31 | private final ConstantHelper constantHelper; 32 | private final ConsoleHelper consoleHelper; 33 | 34 | @Override 35 | public Mono run() { 36 | return Mono.zip(constantHelper.getConstants(leftFiles).collectMap(Constant::getName), constantHelper.getConstants(rightFiles).collectMap(Constant::getName)) 37 | .map(t -> Maps.difference(t.getT1(), t.getT2())) 38 | .flatMap(this::printDiff) 39 | .then(); 40 | } 41 | 42 | private Mono printDiff(final MapDifference diff) { 43 | final var added = Flux.fromIterable(diff.entriesOnlyOnRight().values()) 44 | .map(addedConstant -> { 45 | if (addedConstant.isEnabled() && addedConstant.getValue() != null) { 46 | return addedConstant.getName() + ": " + addedConstant.getValue(); 47 | } 48 | return addedConstant.getName(); 49 | }) 50 | .doOnNext(msg -> consoleHelper.writeLine(msg, ConsoleHelper.ForegroundColorEnum.GREEN)) 51 | .then(); 52 | final var removed = Flux.fromIterable(diff.entriesOnlyOnLeft().keySet()) 53 | .doOnNext(removedConstant -> consoleHelper.writeLine(removedConstant, ConsoleHelper.ForegroundColorEnum.RED)) 54 | .then(); 55 | final var modified = Flux.fromIterable(diff.entriesDiffering().values()) 56 | .map(modifiedConstant -> lineChangeManager.toLineChange("", 1, modifiedConstant.leftValue(), modifiedConstant.rightValue(), null)) 57 | .map(lineChangeFormatter::format) 58 | .doOnNext(msg -> consoleHelper.writeLine(msg, ConsoleHelper.ForegroundColorEnum.CYAN)) 59 | .then(); 60 | return Flux.concat( 61 | Mono.fromRunnable(() -> consoleHelper 62 | .writeLine("Present in right (or absent in left):", ConsoleHelper.ForegroundColorEnum.GREEN, ConsoleHelper.FormatterEnum.UNDERLINED)), 63 | added, 64 | Mono.fromRunnable(() -> consoleHelper.writeLine("Modified:", ConsoleHelper.ForegroundColorEnum.CYAN, ConsoleHelper.FormatterEnum.UNDERLINED)), 65 | modified, 66 | Mono.fromRunnable(() -> consoleHelper 67 | .writeLine("Absent in right (or present in left):", ConsoleHelper.ForegroundColorEnum.RED, ConsoleHelper.FormatterEnum.UNDERLINED)), 68 | removed 69 | ).then(); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/main/java/fr/chuckame/marlinfw/configurator/command/GenerateProfileCommand.java: -------------------------------------------------------------------------------- 1 | package fr.chuckame.marlinfw.configurator.command; 2 | 3 | import com.beust.jcommander.Parameter; 4 | import com.beust.jcommander.Parameters; 5 | import com.google.common.collect.MapDifference; 6 | import com.google.common.collect.Maps; 7 | import fr.chuckame.marlinfw.configurator.constant.Constant; 8 | import fr.chuckame.marlinfw.configurator.profile.ConstantHelper; 9 | import fr.chuckame.marlinfw.configurator.profile.ProfilePropertiesParser; 10 | import fr.chuckame.marlinfw.configurator.util.ConsoleHelper; 11 | import lombok.RequiredArgsConstructor; 12 | import org.springframework.stereotype.Component; 13 | import org.springframework.util.CollectionUtils; 14 | import reactor.core.publisher.Flux; 15 | import reactor.core.publisher.Mono; 16 | 17 | import java.nio.file.Path; 18 | import java.util.List; 19 | 20 | @Component 21 | @Parameters(commandNames = "generate-profile", commandDescription = "Generate a profile from given marlin constants files") 22 | @RequiredArgsConstructor 23 | public class GenerateProfileCommand implements Command { 24 | @Parameter(required = true, description = "/path1 /path2 ...\tThe marlin constants folder or files paths") 25 | private List filesPath; 26 | @Parameter(names = {"--diff-from"}, description = "The marlin constants folder or files paths from where you want to make a diff. If gathered, the generated profile will contains only the diff between those files and the command files") 27 | private List filesPathBase; 28 | @Parameter(names = {"--output", "-o"}, required = true, description = "The output profile path, will be overwritten if already existing file. If 'console' is specified, the profile will just be printed to the console") 29 | private Path profilePath; 30 | 31 | private static final Path CONSOLE_OUTPUT = Path.of("console"); 32 | 33 | private final ProfilePropertiesParser profilePropertiesParser; 34 | private final ConstantHelper constantHelper; 35 | private final ConsoleHelper consoleHelper; 36 | 37 | @Override 38 | public Mono run() { 39 | final Flux constants = CollectionUtils.isEmpty(filesPathBase) ? constantHelper.getConstants(filesPath) : getConstantsFromDiff(); 40 | return constantHelper.constantsToProfile(constants) 41 | .flatMap(profile -> profilePath.equals(CONSOLE_OUTPUT) ? 42 | profilePropertiesParser.writeToString(profile).doOnNext(consoleHelper::writeLine).then() 43 | : profilePropertiesParser.writeToFile(profile, profilePath)); 44 | } 45 | 46 | /** 47 | * @return only constants that are not present from {@link #filesPathBase}, and only modified constants present on both sides 48 | */ 49 | private Flux getConstantsFromDiff() { 50 | return Mono.zip(constantHelper.getConstants(filesPathBase).collectMap(Constant::getName), 51 | constantHelper.getConstants(filesPath).collectMap(Constant::getName)) 52 | .map(t -> Maps.difference(t.getT1(), t.getT2())) 53 | .flatMapMany(diff -> Flux.fromIterable(diff.entriesDiffering().values()) 54 | .map(MapDifference.ValueDifference::leftValue) 55 | .mergeWith(Flux.fromIterable(diff.entriesOnlyOnRight().values()))); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/fr/chuckame/marlinfw/configurator/command/HelpCommand.java: -------------------------------------------------------------------------------- 1 | package fr.chuckame.marlinfw.configurator.command; 2 | 3 | import com.beust.jcommander.JCommander; 4 | import com.beust.jcommander.Parameters; 5 | import fr.chuckame.marlinfw.configurator.util.ConsoleHelper; 6 | import lombok.RequiredArgsConstructor; 7 | import org.springframework.stereotype.Component; 8 | import reactor.core.publisher.Mono; 9 | 10 | @Component 11 | @Parameters(commandNames = "help", commandDescription = "Display this help message") 12 | @RequiredArgsConstructor 13 | public class HelpCommand implements Command { 14 | private final ConsoleHelper consoleHelper; 15 | private final JCommander jCommander; 16 | 17 | @Override 18 | public Mono run() { 19 | return Mono.fromSupplier(StringBuilder::new) 20 | .doOnNext(jCommander.getUsageFormatter()::usage) 21 | .map(StringBuilder::toString) 22 | .doOnNext(consoleHelper::writeLine) 23 | .then(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/fr/chuckame/marlinfw/configurator/command/InvalidUseException.java: -------------------------------------------------------------------------------- 1 | package fr.chuckame.marlinfw.configurator.command; 2 | 3 | import org.springframework.boot.ExitCodeGenerator; 4 | 5 | public class InvalidUseException extends RuntimeException implements ExitCodeGenerator { 6 | public static final int EXIT_CODE = 2; 7 | 8 | public InvalidUseException(final String format, final Object... args) { 9 | super(String.format(format, args)); 10 | } 11 | 12 | @Override 13 | public int getExitCode() { 14 | return EXIT_CODE; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/fr/chuckame/marlinfw/configurator/command/ManuallyStoppedException.java: -------------------------------------------------------------------------------- 1 | package fr.chuckame.marlinfw.configurator.command; 2 | 3 | import org.springframework.boot.ExitCodeGenerator; 4 | 5 | public class ManuallyStoppedException extends RuntimeException implements ExitCodeGenerator { 6 | public static final int EXIT_CODE = 1; 7 | 8 | public ManuallyStoppedException(final String format, final Object... args) { 9 | super(String.format(format, args)); 10 | } 11 | 12 | @Override 13 | public int getExitCode() { 14 | return EXIT_CODE; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/fr/chuckame/marlinfw/configurator/config/JCommanderConfig.java: -------------------------------------------------------------------------------- 1 | package fr.chuckame.marlinfw.configurator.config; 2 | 3 | import com.beust.jcommander.JCommander; 4 | import fr.chuckame.marlinfw.configurator.command.Command; 5 | import org.springframework.beans.factory.ObjectProvider; 6 | import org.springframework.beans.factory.annotation.Value; 7 | import org.springframework.context.annotation.Bean; 8 | import org.springframework.context.annotation.Configuration; 9 | 10 | import java.util.List; 11 | 12 | @Configuration 13 | public class JCommanderConfig { 14 | @Value("${app-console-name}") 15 | private String commandUsage; 16 | 17 | @Bean 18 | public List commands(final ObjectProvider> commandsProvider) { 19 | final var commands = commandsProvider.getObject(jCommander()); 20 | commands.forEach(jCommander()::addCommand); 21 | return commands; 22 | } 23 | 24 | @Bean 25 | public JCommander jCommander() { 26 | final var jcmd = new JCommander(); 27 | jcmd.setProgramName(commandUsage); 28 | jcmd.setColumnSize(140); 29 | return jcmd; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/fr/chuckame/marlinfw/configurator/constant/Constant.java: -------------------------------------------------------------------------------- 1 | package fr.chuckame.marlinfw.configurator.constant; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Builder; 5 | import lombok.Data; 6 | import org.springframework.lang.Nullable; 7 | 8 | @Data 9 | @Builder 10 | @AllArgsConstructor 11 | public class Constant { 12 | private boolean enabled; 13 | private String name; 14 | @Nullable 15 | private String value; 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/fr/chuckame/marlinfw/configurator/constant/ConstantLineDetails.java: -------------------------------------------------------------------------------- 1 | package fr.chuckame.marlinfw.configurator.constant; 2 | 3 | import lombok.Builder; 4 | import lombok.Data; 5 | 6 | @Data 7 | @Builder 8 | public class ConstantLineDetails { 9 | private final String line; 10 | private final MatchIndex disabledMatchIndex; 11 | private final MatchIndex valueMatchIndex; 12 | 13 | @Data 14 | @Builder 15 | public static class MatchIndex { 16 | private final int start; 17 | private final int end; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/fr/chuckame/marlinfw/configurator/constant/ConstantLineInterpreter.java: -------------------------------------------------------------------------------- 1 | package fr.chuckame.marlinfw.configurator.constant; 2 | 3 | import lombok.Builder; 4 | import lombok.Data; 5 | import org.springframework.lang.Nullable; 6 | import org.springframework.stereotype.Component; 7 | import org.springframework.util.StringUtils; 8 | import reactor.core.publisher.Mono; 9 | 10 | import java.util.regex.Matcher; 11 | import java.util.regex.Pattern; 12 | 13 | @Component 14 | public class ConstantLineInterpreter { 15 | private static final Pattern CONSTANT_REGEX = Pattern.compile("^(?:\\h*(//))?\\h*#define\\h+(\\w+)(?:\\h+([^/\\h]+(?:\\h+[^/\\h]+)*))?\\h*(?://\\h*(?:.*))?$"); 16 | 17 | @Data 18 | @Builder 19 | public static class ParsedConstant { 20 | private final Constant constant; 21 | private final ConstantLineDetails constantLineDetails; 22 | } 23 | 24 | public Mono parseLine(final String line) { 25 | return Mono.just(line) 26 | .filter(this::isNotMultilineValue) 27 | .map(CONSTANT_REGEX::matcher) 28 | .filter(Matcher::matches) 29 | .map(this::toConstant); 30 | } 31 | 32 | private boolean isNotMultilineValue(final String line) { 33 | return !line.endsWith("\\"); 34 | } 35 | 36 | public Mono disableLine(final ConstantLineDetails constantLineDetails) { 37 | return Mono.just(constantLineDetails) 38 | .map(ConstantLineDetails::getLine) 39 | .map("//"::concat); 40 | } 41 | 42 | public Mono enableLine(final ConstantLineDetails constantLineDetails) { 43 | return Mono.fromSupplier(() -> replace(constantLineDetails.getLine(), constantLineDetails.getDisabledMatchIndex(), "")); 44 | } 45 | 46 | public Mono enableLineAndChangeValue(final ConstantLineDetails constantLineDetails, final String newValue) { 47 | return Mono.fromSupplier(() -> replace(constantLineDetails.getLine(), 48 | constantLineDetails.getDisabledMatchIndex(), "", 49 | constantLineDetails.getValueMatchIndex(), newValue)); 50 | } 51 | 52 | public Mono changeValue(final ConstantLineDetails constantLineDetails, final String newValue) { 53 | return Mono.fromSupplier(() -> replace(constantLineDetails.getLine(), constantLineDetails.getValueMatchIndex(), newValue)); 54 | } 55 | 56 | private ParsedConstant toConstant(final Matcher matcher) { 57 | return ParsedConstant.builder() 58 | .constant(Constant.builder() 59 | .enabled(matcher.group(1) == null) 60 | .name(matcher.group(2)) 61 | .value(trim(matcher.group(3))) 62 | .build()) 63 | .constantLineDetails(ConstantLineDetails.builder() 64 | .line(matcher.group(0)) 65 | .disabledMatchIndex(matchIndex(matcher, 1)) 66 | .valueMatchIndex(matchIndex(matcher, 3)) 67 | .build()) 68 | .build(); 69 | } 70 | 71 | private ConstantLineDetails.MatchIndex matchIndex(final Matcher matcher, final int group) { 72 | if (matcher.group(group) == null) { 73 | return null; 74 | } 75 | return ConstantLineDetails.MatchIndex.builder() 76 | .start(matcher.start(group)) 77 | .end(matcher.end(group)) 78 | .build(); 79 | } 80 | 81 | private String replace(final String line, final ConstantLineDetails.MatchIndex matchIndex, final String newValue) { 82 | return new StringBuilder(line).replace(matchIndex.getStart(), matchIndex.getEnd(), newValue).toString(); 83 | } 84 | 85 | private String replace(final String line, final ConstantLineDetails.MatchIndex matchIndex1, final String newValue1, final ConstantLineDetails.MatchIndex matchIndex2, 86 | final String newValue2) { 87 | return new StringBuilder(line) 88 | .replace(matchIndex2.getStart(), matchIndex2.getEnd(), newValue2) 89 | .replace(matchIndex1.getStart(), matchIndex1.getEnd(), newValue1) 90 | .toString(); 91 | } 92 | 93 | private String trim(@Nullable final String value) { 94 | if (!StringUtils.hasText(value)) { 95 | return null; 96 | } 97 | return value.trim(); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/main/java/fr/chuckame/marlinfw/configurator/constant/ProfileAdapter.java: -------------------------------------------------------------------------------- 1 | package fr.chuckame.marlinfw.configurator.constant; 2 | 3 | import fr.chuckame.marlinfw.configurator.profile.ProfileProperties; 4 | import org.springframework.stereotype.Component; 5 | 6 | import java.util.HashMap; 7 | import java.util.Map; 8 | 9 | @Component 10 | public class ProfileAdapter { 11 | public Map profileToConstants(final ProfileProperties profileProperties) { 12 | final var wantedChanges = new HashMap(); 13 | profileProperties.getDisabled().forEach(constantName -> wantedChanges.put(constantName, disabledConstant(constantName))); 14 | profileProperties.getEnabled().forEach((constantName, constantValue) -> wantedChanges.put(constantName, enabledConstant(constantName, constantValue))); 15 | return wantedChanges; 16 | } 17 | 18 | private Constant disabledConstant(final String name) { 19 | return Constant.builder().name(name).enabled(false).build(); 20 | } 21 | 22 | private Constant enabledConstant(final String name, final String value) { 23 | return Constant.builder().name(name).value(value).enabled(true).build(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/fr/chuckame/marlinfw/configurator/profile/ConstantHelper.java: -------------------------------------------------------------------------------- 1 | package fr.chuckame.marlinfw.configurator.profile; 2 | 3 | import fr.chuckame.marlinfw.configurator.constant.Constant; 4 | import fr.chuckame.marlinfw.configurator.constant.ConstantLineInterpreter; 5 | import fr.chuckame.marlinfw.configurator.util.FileHelper; 6 | import lombok.RequiredArgsConstructor; 7 | import org.springframework.stereotype.Component; 8 | import reactor.core.publisher.Flux; 9 | import reactor.core.publisher.Mono; 10 | 11 | import java.nio.file.Path; 12 | import java.util.ArrayList; 13 | import java.util.LinkedHashMap; 14 | import java.util.List; 15 | 16 | @Component 17 | @RequiredArgsConstructor 18 | public class ConstantHelper { 19 | private final FileHelper fileHelper; 20 | private final ConstantLineInterpreter constantLineInterpreter; 21 | 22 | public Mono constantsToProfile(final Flux constants) { 23 | return constants.reduceWith(this::initEmptyProfile, this::addConstantToProfile); 24 | } 25 | 26 | private ProfileProperties initEmptyProfile() { 27 | return ProfileProperties.builder().disabled(new ArrayList<>()).enabled(new LinkedHashMap<>()).build(); 28 | } 29 | 30 | private ProfileProperties addConstantToProfile(final ProfileProperties profile, final Constant constant) { 31 | if (constant.isEnabled()) { 32 | profile.getEnabled().put(constant.getName(), constant.getValue()); 33 | } else { 34 | profile.getDisabled().add(constant.getName()); 35 | } 36 | return profile; 37 | } 38 | 39 | public Flux getConstants(final List files) { 40 | return fileHelper.listFiles(files) 41 | .flatMap(fileHelper::lines) 42 | .flatMap(constantLineInterpreter::parseLine) 43 | .map(ConstantLineInterpreter.ParsedConstant::getConstant); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/fr/chuckame/marlinfw/configurator/profile/ProfileProperties.java: -------------------------------------------------------------------------------- 1 | package fr.chuckame.marlinfw.configurator.profile; 2 | 3 | import com.fasterxml.jackson.annotation.JsonSetter; 4 | import com.fasterxml.jackson.annotation.Nulls; 5 | import lombok.AllArgsConstructor; 6 | import lombok.Builder; 7 | import lombok.Data; 8 | import lombok.NoArgsConstructor; 9 | 10 | import java.util.ArrayList; 11 | import java.util.LinkedHashMap; 12 | import java.util.List; 13 | import java.util.Map; 14 | 15 | @Data 16 | @Builder 17 | @NoArgsConstructor 18 | @AllArgsConstructor 19 | public class ProfileProperties { 20 | @JsonSetter(nulls = Nulls.AS_EMPTY) 21 | private Map enabled; 22 | @JsonSetter(nulls = Nulls.AS_EMPTY) 23 | private List disabled; 24 | 25 | public static ProfileProperties merge(final ProfileProperties a, final ProfileProperties b) { 26 | final var props = new ProfileProperties(new LinkedHashMap<>(), new ArrayList<>()); 27 | if (a.enabled != null) { 28 | props.enabled.putAll(a.enabled); 29 | } 30 | if (a.disabled != null) { 31 | props.disabled.addAll(a.disabled); 32 | } 33 | if (b.enabled != null) { 34 | props.enabled.putAll(b.enabled); 35 | } 36 | if (b.disabled != null) { 37 | props.disabled.addAll(b.disabled); 38 | } 39 | return props; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/fr/chuckame/marlinfw/configurator/profile/ProfilePropertiesParser.java: -------------------------------------------------------------------------------- 1 | package fr.chuckame.marlinfw.configurator.profile; 2 | 3 | import com.fasterxml.jackson.core.io.IOContext; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; 6 | import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator; 7 | import fr.chuckame.marlinfw.configurator.util.ExceptionUtils; 8 | import fr.chuckame.marlinfw.configurator.util.FileHelper; 9 | import lombok.RequiredArgsConstructor; 10 | import org.springframework.stereotype.Component; 11 | import org.yaml.snakeyaml.DumperOptions; 12 | import reactor.core.publisher.Mono; 13 | 14 | import java.io.IOException; 15 | import java.io.Writer; 16 | import java.nio.file.Path; 17 | import java.util.List; 18 | 19 | import static com.fasterxml.jackson.dataformat.yaml.YAMLGenerator.Feature.INDENT_ARRAYS; 20 | import static com.fasterxml.jackson.dataformat.yaml.YAMLGenerator.Feature.MINIMIZE_QUOTES; 21 | import static com.fasterxml.jackson.dataformat.yaml.YAMLGenerator.Feature.SPLIT_LINES; 22 | import static com.fasterxml.jackson.dataformat.yaml.YAMLGenerator.Feature.WRITE_DOC_START_MARKER; 23 | 24 | @Component 25 | @RequiredArgsConstructor 26 | public class ProfilePropertiesParser { 27 | private final ObjectMapper yamlParser = prepareYamlMapper(); 28 | private final FileHelper fileHelper; 29 | 30 | public Mono parseFromFiles(final List profileFilePaths) { 31 | return fileHelper.listFiles(profileFilePaths) 32 | .flatMap(this::parseFromFile) 33 | .reduceWith(ProfileProperties::new, ProfileProperties::merge); 34 | } 35 | 36 | public Mono parseFromFile(final Path profileFilePath) { 37 | return fileHelper.read(profileFilePath) 38 | .map(ExceptionUtils.wrap(bytes -> yamlParser.readValue(bytes, ProfileProperties.class))); 39 | } 40 | 41 | public Mono writeToFile(final ProfileProperties profile, final Path outputFilePath) { 42 | return Mono.fromCallable(() -> yamlParser.writeValueAsBytes(profile)) 43 | .flatMap(bytes -> fileHelper.write(bytes, outputFilePath)) 44 | .then(); 45 | } 46 | 47 | public Mono writeToString(final ProfileProperties profile) { 48 | return Mono.fromCallable(() -> yamlParser.writeValueAsString(profile)); 49 | } 50 | 51 | private ObjectMapper prepareYamlMapper() { 52 | return new ObjectMapper(newYamlFactoryWithCustomIndentationForArrays(2) 53 | .enable(MINIMIZE_QUOTES) 54 | .enable(INDENT_ARRAYS) 55 | .disable(SPLIT_LINES) 56 | .disable(WRITE_DOC_START_MARKER)); 57 | } 58 | 59 | private YAMLFactory newYamlFactoryWithCustomIndentationForArrays(final int indentation) { 60 | return new YAMLFactory() { 61 | @Override 62 | protected YAMLGenerator _createGenerator(final Writer out, final IOContext ctxt) throws IOException { 63 | return new YAMLGenerator(ctxt, _generatorFeatures, _yamlGeneratorFeatures, 64 | _objectCodec, out, _version) { 65 | @Override 66 | protected DumperOptions buildDumperOptions(final int jsonFeatures, final int yamlFeatures, final DumperOptions.Version version) { 67 | final var opts = super.buildDumperOptions(jsonFeatures, yamlFeatures, version); 68 | opts.setIndicatorIndent(indentation); 69 | return opts; 70 | } 71 | 72 | @Override 73 | public void writeNull() throws IOException { 74 | _verifyValueWrite("write null value"); 75 | // no real type for this, is there? 76 | _writeScalar("", "object", DumperOptions.ScalarStyle.PLAIN); 77 | } 78 | }; 79 | } 80 | }; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/main/java/fr/chuckame/marlinfw/configurator/util/ConsoleHelper.java: -------------------------------------------------------------------------------- 1 | package fr.chuckame.marlinfw.configurator.util; 2 | 3 | import lombok.Getter; 4 | import lombok.RequiredArgsConstructor; 5 | import org.springframework.stereotype.Service; 6 | 7 | import java.io.InputStream; 8 | import java.io.PrintStream; 9 | import java.util.Scanner; 10 | import java.util.function.Consumer; 11 | import java.util.stream.Collectors; 12 | import java.util.stream.Stream; 13 | 14 | @Service 15 | @SuppressWarnings("unused")//don't want to remove unused colors for the moment 16 | public class ConsoleHelper { 17 | private static final String RESET_COLOR = "\u001B[0m"; 18 | 19 | private final PrintStream consoleOutput; 20 | private final PrintStream consoleErrorOutput; 21 | private final InputStream consoleInput; 22 | 23 | @SuppressWarnings("java:S106")// This is wanted to use serr/sout 24 | public ConsoleHelper() { 25 | consoleInput = System.in; 26 | consoleOutput = System.out; 27 | consoleErrorOutput = System.err; 28 | } 29 | 30 | public void writeLine(final String line, final ConsoleStyle... styles) { 31 | consoleOutput.println(String.join("", Stream.of(styles).map(ConsoleStyle::getCode).collect(Collectors.joining("")), line, RESET_COLOR)); 32 | } 33 | 34 | public void newLine() { 35 | consoleOutput.println(); 36 | } 37 | 38 | public void writeLine(final Consumer lineBuilder, final ConsoleStyle... styles) { 39 | final var line = new StringBuilder(); 40 | lineBuilder.accept(line); 41 | writeLine(line.toString(), styles); 42 | } 43 | 44 | public void writeErrorLine(final String line) { 45 | consoleErrorOutput.println(line); 46 | } 47 | 48 | public String readLine() { 49 | return new Scanner(consoleInput).next(); 50 | } 51 | 52 | 53 | public interface ConsoleStyle { 54 | String getCode(); 55 | } 56 | 57 | @RequiredArgsConstructor 58 | public enum ForegroundColorEnum implements ConsoleStyle { 59 | DEFAULT("\u001B[39m"), 60 | BLACK("\u001B[30m"), 61 | RED("\u001B[31m"), 62 | GREEN("\u001B[32m"), 63 | YELLOW("\u001B[33m"), 64 | BLUE("\u001B[34m"), 65 | MAGENTA("\u001B[35m"), 66 | CYAN("\u001B[36m"), 67 | LIGHT_GRAY("\u001B[37m"), 68 | DARK_GRAY("\u001B[90m"), 69 | LIGHT_RED("\u001B[91m"), 70 | LIGHT_GREEN("\u001B[92m"), 71 | LIGHT_YELLOW("\u001B[93m"), 72 | LIGHT_BLUE("\u001B[94m"), 73 | LIGHT_MAGENTA("\u001B[95m"), 74 | LIGHT_CYAN("\u001B[96m"), 75 | ; 76 | @Getter 77 | private final String code; 78 | } 79 | 80 | @RequiredArgsConstructor 81 | public enum FormatterEnum implements ConsoleStyle { 82 | BOLD("\u001B[1m"), 83 | UNDERLINED("\u001B[4m"), 84 | DIM("\u001B[2m"), 85 | ; 86 | @Getter 87 | private final String code; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/main/java/fr/chuckame/marlinfw/configurator/util/ExceptionUtils.java: -------------------------------------------------------------------------------- 1 | package fr.chuckame.marlinfw.configurator.util; 2 | 3 | import reactor.core.Exceptions; 4 | 5 | import java.util.concurrent.Callable; 6 | import java.util.function.Function; 7 | import java.util.function.Supplier; 8 | 9 | public class ExceptionUtils { 10 | public static Function wrap(final CheckedFunction function) { 11 | return in -> { 12 | try { 13 | return function.apply(in); 14 | } catch (final Exception e) { 15 | throw Exceptions.propagate(e); 16 | } 17 | }; 18 | } 19 | 20 | public static Supplier wrap(final Callable callable) { 21 | return () -> { 22 | try { 23 | return callable.call(); 24 | } catch (final Exception e) { 25 | throw Exceptions.propagate(e); 26 | } 27 | }; 28 | } 29 | 30 | public interface CheckedFunction { 31 | O apply(I in) throws Exception; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/fr/chuckame/marlinfw/configurator/util/FileHelper.java: -------------------------------------------------------------------------------- 1 | package fr.chuckame.marlinfw.configurator.util; 2 | 3 | import org.springframework.stereotype.Component; 4 | import reactor.core.publisher.Flux; 5 | import reactor.core.publisher.Mono; 6 | 7 | import java.io.File; 8 | import java.io.FileInputStream; 9 | import java.io.IOException; 10 | import java.nio.charset.StandardCharsets; 11 | import java.nio.file.Files; 12 | import java.nio.file.Path; 13 | import java.nio.file.StandardOpenOption; 14 | import java.util.List; 15 | import java.util.Spliterator; 16 | import java.util.function.Supplier; 17 | import java.util.stream.StreamSupport; 18 | 19 | @Component 20 | public class FileHelper { 21 | public Flux listFiles(final List paths) { 22 | return Flux.fromIterable(paths) 23 | .flatMap(path -> { 24 | if (Files.isDirectory(path)) { 25 | return toFlux(ExceptionUtils.wrap(() -> Files.newDirectoryStream(path).spliterator())); 26 | } 27 | return Mono.just(path); 28 | }) 29 | .filter(Files::isRegularFile); 30 | } 31 | 32 | private Flux toFlux(final Supplier> iterator) { 33 | return Flux.fromStream(() -> StreamSupport.stream(iterator.get(), false)); 34 | 35 | } 36 | 37 | public Flux lines(final Path file) { 38 | return Flux.fromStream(ExceptionUtils.wrap(() -> Files.lines(file))); 39 | } 40 | 41 | public Mono read(final Path file) { 42 | return Mono.fromCallable(() -> Files.readAllBytes(file)); 43 | } 44 | 45 | public Mono write(final byte[] bytes, final Path file) { 46 | return Mono.fromSupplier(ExceptionUtils.wrap(() -> Files.write(file, bytes, StandardOpenOption.CREATE_NEW))).then(); 47 | } 48 | 49 | public Mono write(final Path file, final boolean override, final Flux lines) { 50 | return lines.collectList() 51 | .defaultIfEmpty(List.of()) 52 | .flatMap(l -> (override && Files.exists(file) ? detectLineSeparator(file) : Mono.empty()).defaultIfEmpty(System.lineSeparator()) 53 | .map(lineSeparator -> String.join(lineSeparator, l) 54 | .concat(lineSeparator))) 55 | .flatMap(linesList -> 56 | Mono.fromSupplier(ExceptionUtils.wrap(() -> 57 | Files.write(file, linesList 58 | .getBytes(StandardCharsets.UTF_8), override ? StandardOpenOption.TRUNCATE_EXISTING : StandardOpenOption.CREATE_NEW)))) 59 | .then(); 60 | } 61 | 62 | public Mono detectLineSeparator(final Path file) { 63 | return Mono.fromCallable(() -> retrieveLineSeparator(file.toFile())); 64 | } 65 | 66 | private static String retrieveLineSeparator(final File file) throws IOException { 67 | char current; 68 | final StringBuilder lineSeparator = new StringBuilder(); 69 | try (final FileInputStream fis = new FileInputStream(file)) { 70 | while (fis.available() > 0) { 71 | current = (char) fis.read(); 72 | if ((current == '\n') || (current == '\r')) { 73 | lineSeparator.append(current); 74 | if (fis.available() > 0) { 75 | final char next = (char) fis.read(); 76 | if ((next != current) 77 | && ((next == '\r') || (next == '\n'))) { 78 | lineSeparator.append(next); 79 | } 80 | } 81 | return lineSeparator.toString(); 82 | } 83 | } 84 | } 85 | return null; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring.main.web-application-type: none 2 | spring.main.banner-mode: off 3 | app-console-name: ./gradlew bootRun 4 | spring: 5 | main: 6 | log-startup-info: false 7 | -------------------------------------------------------------------------------- /src/test/java/fr/chuckame/marlinfw/configurator/change/LineChangeFormatterTest.java: -------------------------------------------------------------------------------- 1 | package fr.chuckame.marlinfw.configurator.change; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static org.assertj.core.api.Assertions.assertThat; 6 | 7 | class LineChangeFormatterTest { 8 | private static final String CONSTANT_NAME = "constant"; 9 | private static final String CONSTANT_VALUE = "value"; 10 | private static final String LINE = "a line"; 11 | private static final int LINE_NUMBER = 15; 12 | private final LineChangeFormatter formatter = new LineChangeFormatter(); 13 | 14 | @Test 15 | void formatShouldReturnNull() { 16 | final var lineChange = new LineChange(LINE, LINE_NUMBER); 17 | 18 | final var formatted = formatter.format(lineChange); 19 | 20 | assertThat(formatted).isNull(); 21 | } 22 | 23 | @Test 24 | void formatShouldReturnDoNothingWhenWantedConstantIsNull() { 25 | final var lineChange = lineChange(LineChange.DiffEnum.DO_NOTHING, CONSTANT_VALUE, null); 26 | 27 | final var formatted = formatter.format(lineChange); 28 | 29 | assertThat(formatted).isEqualTo(CONSTANT_NAME + ": Nothing to do"); 30 | } 31 | 32 | @Test 33 | void formatShouldReturnDoNothingWhenEnabledAndValuesAreSame() { 34 | final var lineChange = lineChange(LineChange.DiffEnum.DO_NOTHING, CONSTANT_VALUE, CONSTANT_VALUE); 35 | 36 | final var formatted = formatter.format(lineChange); 37 | 38 | assertThat(formatted).isEqualTo(CONSTANT_NAME + ": Nothing to do"); 39 | } 40 | 41 | @Test 42 | void formatShouldReturnDoNothingWhenEnabledAndValuesAreNull() { 43 | final var lineChange = lineChange(LineChange.DiffEnum.DO_NOTHING, null, null); 44 | 45 | final var formatted = formatter.format(lineChange); 46 | 47 | assertThat(formatted).isEqualTo(CONSTANT_NAME + ": Nothing to do"); 48 | } 49 | 50 | @Test 51 | void formatShouldReturnEnableOnlyWhenParsedDisabledAndWantedEnabled() { 52 | final var lineChange = lineChange(LineChange.DiffEnum.TO_ENABLE, CONSTANT_VALUE, CONSTANT_VALUE); 53 | 54 | final var formatted = formatter.format(lineChange); 55 | 56 | assertThat(formatted).isEqualTo("Enable constant"); 57 | } 58 | 59 | @Test 60 | void formatShouldReturnDisableOnlyWhenParsedEnabledAndWantedDisabled() { 61 | final var lineChange = lineChange(LineChange.DiffEnum.TO_DISABLE, CONSTANT_VALUE, CONSTANT_VALUE); 62 | 63 | final var formatted = formatter.format(lineChange); 64 | 65 | assertThat(formatted).isEqualTo("Disable constant"); 66 | } 67 | 68 | @Test 69 | void formatShouldReturnEnableAndChangeWhenParsedDisabledAndWantedEnabledWithOtherValue() { 70 | final var lineChange = lineChange(LineChange.DiffEnum.TO_ENABLE_AND_CHANGE_VALUE, CONSTANT_VALUE, "new value"); 71 | 72 | final var formatted = formatter.format(lineChange); 73 | 74 | assertThat(formatted).isEqualTo("Enable & Change constant: value → new value"); 75 | } 76 | 77 | @Test 78 | void formatShouldReturnExpectedTextWhenChangeValue() { 79 | final var lineChange = lineChange(LineChange.DiffEnum.CHANGE_VALUE, CONSTANT_VALUE, "new value"); 80 | 81 | final var formatted = formatter.format(lineChange); 82 | 83 | assertThat(formatted).isEqualTo("Change constant: value → new value"); 84 | } 85 | 86 | @Test 87 | void formatShouldReturnExpectedTextWhenError() { 88 | final var lineChange = LineChange.builder() 89 | .line(LINE) 90 | .lineNumber(LINE_NUMBER) 91 | .diff(LineChange.DiffEnum.ERROR) 92 | .violation("a violation") 93 | .constant(LineChange.LineChangeConstant.builder() 94 | .name(CONSTANT_NAME) 95 | .currentValue(CONSTANT_VALUE) 96 | .build()) 97 | .build(); 98 | 99 | final var formatted = formatter.format(lineChange); 100 | 101 | assertThat(formatted).isEqualTo("Error constant: a violation"); 102 | } 103 | 104 | private LineChange lineChange(final LineChange.DiffEnum diff, final String oldValue, final String wantedValue) { 105 | return LineChange.builder() 106 | .line(LINE) 107 | .lineNumber(LINE_NUMBER) 108 | .diff(diff) 109 | .constant(LineChange.LineChangeConstant.builder() 110 | .name(CONSTANT_NAME) 111 | .currentValue(oldValue) 112 | .wantedValue(wantedValue) 113 | .build()) 114 | .build(); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/test/java/fr/chuckame/marlinfw/configurator/change/LineChangeManagerTest.java: -------------------------------------------------------------------------------- 1 | package fr.chuckame.marlinfw.configurator.change; 2 | 3 | import fr.chuckame.marlinfw.configurator.constant.Constant; 4 | import fr.chuckame.marlinfw.configurator.constant.ConstantLineDetails; 5 | import fr.chuckame.marlinfw.configurator.constant.ConstantLineInterpreter; 6 | import org.junit.jupiter.api.Test; 7 | import org.junit.jupiter.api.extension.ExtendWith; 8 | import org.mockito.InjectMocks; 9 | import org.mockito.Mock; 10 | import org.mockito.junit.jupiter.MockitoExtension; 11 | import reactor.core.publisher.Mono; 12 | import reactor.test.StepVerifier; 13 | 14 | import java.util.List; 15 | import java.util.Map; 16 | import java.util.stream.Collectors; 17 | 18 | import static org.assertj.core.api.Assertions.assertThat; 19 | import static org.mockito.Mockito.verify; 20 | import static org.mockito.Mockito.when; 21 | 22 | @ExtendWith(MockitoExtension.class) 23 | class LineChangeManagerTest { 24 | private static final String CONSTANT_NAME = "name"; 25 | private static final String CONSTANT_VALUE = "value"; 26 | private static final String CONSTANT_OTHER_VALUE = "other value"; 27 | private static final int LINE_NUMBER = 15; 28 | private static final String INPUT_LINE = "input"; 29 | private static final String OUTPUT_LINE = "output"; 30 | 31 | @Mock 32 | private ConstantLineInterpreter constantLineInterpreterMock; 33 | @Mock 34 | private LineChangeValidator lineChangeValidatorMock; 35 | @InjectMocks 36 | private LineChangeManager lineChangeManager; 37 | 38 | @Test 39 | void getUnusedWantedConstantsShouldReturnEmptyWhenAllUsed() { 40 | final var changes = List.of( 41 | lineChange("c1", LineChange.DiffEnum.TO_DISABLE, CONSTANT_VALUE, CONSTANT_VALUE).build(), 42 | lineChange("c2", LineChange.DiffEnum.TO_ENABLE, CONSTANT_VALUE, CONSTANT_VALUE).build(), 43 | lineChange("c3", LineChange.DiffEnum.TO_ENABLE, CONSTANT_VALUE, CONSTANT_VALUE).build() 44 | ); 45 | final var wantedConstants = changes.stream().collect(Collectors.toMap(c -> c.getConstant().getName(), this::lineChangeToWantedConstant)); 46 | 47 | final var unusedWantedConstants = lineChangeManager.getUnusedWantedConstants(changes, wantedConstants); 48 | 49 | StepVerifier.create(unusedWantedConstants) 50 | .expectComplete() 51 | .verify(); 52 | } 53 | 54 | private Constant lineChangeToWantedConstant(final LineChange change) { 55 | return Constant.builder() 56 | .enabled(true) 57 | .name(change.getConstant().getName()) 58 | .value(change.getConstant().getWantedValue()) 59 | .build(); 60 | } 61 | 62 | @Test 63 | void getUnusedWantedConstantsShouldReturnEmptyWhenAllUsedAndMoreChangesThanWantedConstants() { 64 | final var changes = List.of( 65 | lineChange("c1", LineChange.DiffEnum.TO_DISABLE, CONSTANT_VALUE, CONSTANT_VALUE).build(), 66 | lineChange("c2", LineChange.DiffEnum.TO_ENABLE, CONSTANT_VALUE, CONSTANT_VALUE).build(), 67 | lineChange("c3", LineChange.DiffEnum.TO_ENABLE, CONSTANT_VALUE, CONSTANT_VALUE).build() 68 | ); 69 | final var wantedConstants = Map.of("c1", lineChangeToWantedConstant(changes.get(0))); 70 | 71 | final var unusedWantedConstants = lineChangeManager.getUnusedWantedConstants(changes, wantedConstants); 72 | 73 | StepVerifier.create(unusedWantedConstants) 74 | .expectComplete() 75 | .verify(); 76 | } 77 | 78 | @Test 79 | void getUnusedWantedConstantsShouldReturnOneElement() { 80 | final var changes = List.of( 81 | lineChange("c1", LineChange.DiffEnum.TO_DISABLE, CONSTANT_VALUE, CONSTANT_VALUE).build(), 82 | lineChange("c2", LineChange.DiffEnum.TO_DISABLE, CONSTANT_VALUE, CONSTANT_VALUE).build() 83 | ); 84 | final var wantedConstants = Map.of("c2", lineChangeToWantedConstant(changes.get(1)), 85 | "c3", Constant.builder().name("c3").enabled(true).value("value").build()); 86 | 87 | final var unusedWantedConstants = lineChangeManager.getUnusedWantedConstants(changes, wantedConstants); 88 | 89 | StepVerifier.create(unusedWantedConstants) 90 | .expectNext("c3") 91 | .expectComplete() 92 | .verify(); 93 | } 94 | 95 | @Test 96 | void getUnusedWantedConstantsShouldReturnTwoElements() { 97 | final var changes = List.of( 98 | lineChange("c1", LineChange.DiffEnum.TO_DISABLE, CONSTANT_VALUE, CONSTANT_VALUE).build() 99 | ); 100 | final var wantedConstants = Map.of( 101 | "c2", Constant.builder().name("c2").enabled(false).build(), 102 | "c3", Constant.builder().name("c3").enabled(true).value("value").build() 103 | ); 104 | 105 | final var unusedWantedConstants = lineChangeManager.getUnusedWantedConstants(changes, wantedConstants); 106 | 107 | assertThat(unusedWantedConstants.collectList().block()).containsExactlyInAnyOrder("c2", "c3"); 108 | } 109 | 110 | @Test 111 | void prepareChangeShouldReturnNonConstantLineChangeWhenNothingParsed() { 112 | when(constantLineInterpreterMock.parseLine(INPUT_LINE)).thenReturn(Mono.empty()); 113 | final Map wantedConstants = Map.of(); 114 | final var expectedChange = LineChange.builder() 115 | .diff(LineChange.DiffEnum.DO_NOTHING) 116 | .lineNumber(LINE_NUMBER) 117 | .line(INPUT_LINE) 118 | .build(); 119 | 120 | final var change = lineChangeManager.prepareChange(INPUT_LINE, LINE_NUMBER, wantedConstants); 121 | 122 | StepVerifier.create(change) 123 | .expectNext(expectedChange) 124 | .expectComplete() 125 | .verify(); 126 | } 127 | 128 | @Test 129 | void prepareChangeShouldReturnDoNothingLineChangeWhenNoWantedConstant() { 130 | final var parsedConstant = parsedConstant(Constant.builder().name(CONSTANT_NAME).enabled(false).value("value").build()); 131 | when(constantLineInterpreterMock.parseLine(INPUT_LINE)).thenReturn(Mono.just(parsedConstant)); 132 | final Map wantedConstants = Map.of(); 133 | final var expectedChange = lineChange(LineChange.DiffEnum.DO_NOTHING, "value", null).build(); 134 | 135 | final var change = lineChangeManager.prepareChange(INPUT_LINE, LINE_NUMBER, wantedConstants); 136 | 137 | StepVerifier.create(change) 138 | .expectNext(expectedChange) 139 | .expectComplete() 140 | .verify(); 141 | } 142 | 143 | @Test 144 | void prepareChangeShouldReturnDoNothingLineChange() { 145 | prepareChangeShouldReturnExpectedLineChange(true, "value", true, "value", LineChange.DiffEnum.DO_NOTHING); 146 | } 147 | 148 | @Test 149 | void prepareChangeShouldReturnDoNothingLineChange_nullValue() { 150 | prepareChangeShouldReturnExpectedLineChange(true, null, true, null, LineChange.DiffEnum.DO_NOTHING); 151 | } 152 | 153 | @Test 154 | void prepareChangeShouldReturnToEnableLineChange() { 155 | prepareChangeShouldReturnExpectedLineChange(false, "value", true, "value", LineChange.DiffEnum.TO_ENABLE); 156 | } 157 | 158 | @Test 159 | void prepareChangeShouldReturnToEnableAndChangeLineChange() { 160 | prepareChangeShouldReturnExpectedLineChange(false, "value", true, "other", LineChange.DiffEnum.TO_ENABLE_AND_CHANGE_VALUE); 161 | } 162 | 163 | @Test 164 | void prepareChangeShouldReturnToChangeLineChange() { 165 | prepareChangeShouldReturnExpectedLineChange(true, "value", true, "other", LineChange.DiffEnum.CHANGE_VALUE); 166 | } 167 | 168 | @Test 169 | void prepareChangeShouldReturnToDisableLineChange() { 170 | prepareChangeShouldReturnExpectedLineChange(true, "value", false, "other", LineChange.DiffEnum.TO_DISABLE); 171 | } 172 | 173 | void prepareChangeShouldReturnExpectedLineChange(final boolean parsedEnabled, final String parsedValue, final boolean wantedEnabled, final String wantedValue, 174 | final LineChange.DiffEnum expectedDiff) { 175 | final var parsedConstant = Constant.builder().name(CONSTANT_NAME).enabled(parsedEnabled).value(parsedValue).build(); 176 | final var wantedConstant = Constant.builder().name(CONSTANT_NAME).enabled(wantedEnabled).value(wantedValue).build(); 177 | when(constantLineInterpreterMock.parseLine(INPUT_LINE)).thenReturn(Mono.just(parsedConstant(parsedConstant))); 178 | final Map wantedConstants = Map.of(CONSTANT_NAME, wantedConstant); 179 | final var expectedChange = lineChange(expectedDiff, parsedValue, wantedValue).build(); 180 | 181 | final var change = lineChangeManager.prepareChange(INPUT_LINE, LINE_NUMBER, wantedConstants); 182 | 183 | assertThat(change.blockOptional()).hasValue(expectedChange); 184 | verify(lineChangeValidatorMock).getViolation(parsedConstant, wantedConstant); 185 | } 186 | 187 | @Test 188 | void prepareChangeShouldReturnErrorLineChange() { 189 | final var parsedConstant = Constant.builder().name(CONSTANT_NAME).build(); 190 | final var wantedConstant = Constant.builder().name(CONSTANT_NAME).build(); 191 | when(constantLineInterpreterMock.parseLine(INPUT_LINE)).thenReturn(Mono.just(parsedConstant(parsedConstant))); 192 | when(lineChangeValidatorMock.getViolation(parsedConstant, wantedConstant)).thenReturn("an error"); 193 | final Map wantedConstants = Map.of(CONSTANT_NAME, wantedConstant); 194 | final var expectedChange = lineChange(LineChange.DiffEnum.ERROR, null, null) 195 | .violation("an error").build(); 196 | 197 | final var change = lineChangeManager.prepareChange(INPUT_LINE, LINE_NUMBER, wantedConstants); 198 | 199 | assertThat(change.blockOptional()).hasValue(expectedChange); 200 | } 201 | 202 | @Test 203 | void applyChangeShouldReturnSameLineChangeWhenNotConstant() { 204 | final var lineChange = LineChange.builder() 205 | .diff(LineChange.DiffEnum.DO_NOTHING) 206 | .lineNumber(LINE_NUMBER) 207 | .line(INPUT_LINE) 208 | .build(); 209 | 210 | final var change = lineChangeManager.applyChange(lineChange); 211 | 212 | StepVerifier.create(change) 213 | .expectNext(INPUT_LINE) 214 | .expectComplete() 215 | .verify(); 216 | } 217 | 218 | @Test 219 | void applyChangeShouldThrowBadLineChangeExceptionWhenHasViolation() { 220 | final var lineChange = lineChange(LineChange.DiffEnum.ERROR, "value", "value").build(); 221 | final var expectedExceptionMessage = "too bad"; 222 | lineChange.setViolation(expectedExceptionMessage); 223 | 224 | StepVerifier.create(lineChangeManager.applyChange(lineChange)) 225 | .expectErrorSatisfies(e -> assertThat(e).isExactlyInstanceOf(BadLineChangeException.class) 226 | .hasMessageContaining(expectedExceptionMessage) 227 | .extracting(ee -> ((BadLineChangeException) ee).getLineChange()) 228 | .isEqualTo(lineChange)) 229 | .verify() 230 | ; 231 | } 232 | 233 | @Test 234 | void applyChangeShouldReturnSameLineWhenEnabledWithSameValue() { 235 | final var lineChange = lineChange(LineChange.DiffEnum.DO_NOTHING, CONSTANT_VALUE, CONSTANT_VALUE).build(); 236 | 237 | final var outputLine = lineChangeManager.applyChange(lineChange); 238 | 239 | StepVerifier.create(outputLine) 240 | .expectNext(INPUT_LINE) 241 | .expectComplete() 242 | .verify(); 243 | } 244 | 245 | @Test 246 | void applyChangeShouldReturnExpectedLineWhenWantedToBeDisabled() { 247 | final var lineChange = lineChange(LineChange.DiffEnum.TO_DISABLE, CONSTANT_VALUE, null).build(); 248 | when(constantLineInterpreterMock.disableLine(lineChange.getConstantLineDetails())).thenReturn(Mono.just(OUTPUT_LINE)); 249 | 250 | final var outputLine = lineChangeManager.applyChange(lineChange); 251 | 252 | StepVerifier.create(outputLine) 253 | .expectNext(OUTPUT_LINE) 254 | .expectComplete() 255 | .verify(); 256 | } 257 | 258 | @Test 259 | void applyChangeShouldReturnExpectedLineWhenWantedToBeEnabled() { 260 | final var lineChange = lineChange(LineChange.DiffEnum.TO_ENABLE, CONSTANT_VALUE, CONSTANT_VALUE).build(); 261 | when(constantLineInterpreterMock.enableLine(lineChange.getConstantLineDetails())).thenReturn(Mono.just(OUTPUT_LINE)); 262 | 263 | final var outputLine = lineChangeManager.applyChange(lineChange); 264 | 265 | StepVerifier.create(outputLine) 266 | .expectNext(OUTPUT_LINE) 267 | .expectComplete() 268 | .verify(); 269 | } 270 | 271 | @Test 272 | void applyChangeShouldReturnExpectedLineWhenWantedToBeEnabledWithOtherValue() { 273 | final var lineChange = lineChange(LineChange.DiffEnum.TO_ENABLE_AND_CHANGE_VALUE, CONSTANT_VALUE, CONSTANT_OTHER_VALUE).build(); 274 | when(constantLineInterpreterMock.enableLineAndChangeValue(lineChange.getConstantLineDetails(), CONSTANT_OTHER_VALUE)).thenReturn(Mono.just(OUTPUT_LINE)); 275 | 276 | final var outputLine = lineChangeManager.applyChange(lineChange); 277 | 278 | StepVerifier.create(outputLine) 279 | .expectNext(OUTPUT_LINE) 280 | .expectComplete() 281 | .verify(); 282 | } 283 | 284 | @Test 285 | void applyChangeShouldReturnExpectedLineWhenAlreadyEnabledButOtherValue() { 286 | final var lineChange = lineChange(LineChange.DiffEnum.CHANGE_VALUE, CONSTANT_VALUE, CONSTANT_OTHER_VALUE).build(); 287 | when(constantLineInterpreterMock.changeValue(lineChange.getConstantLineDetails(), CONSTANT_OTHER_VALUE)).thenReturn(Mono.just(OUTPUT_LINE)); 288 | 289 | final var outputLine = lineChangeManager.applyChange(lineChange); 290 | 291 | StepVerifier.create(outputLine) 292 | .expectNext(OUTPUT_LINE) 293 | .expectComplete() 294 | .verify(); 295 | } 296 | 297 | private LineChange.LineChangeBuilder lineChange(final LineChange.DiffEnum diff, final String oldValue, final String wantedValue) { 298 | return lineChange(CONSTANT_NAME, diff, oldValue, wantedValue); 299 | } 300 | 301 | private LineChange.LineChangeBuilder lineChange(final String constantName, final LineChange.DiffEnum diff, final String oldValue, final String wantedValue) { 302 | return LineChange.builder() 303 | .line(INPUT_LINE) 304 | .lineNumber(LINE_NUMBER) 305 | .diff(diff) 306 | .constant(LineChange.LineChangeConstant.builder() 307 | .name(constantName) 308 | .currentValue(oldValue) 309 | .wantedValue(wantedValue) 310 | .build()) 311 | .constantLineDetails(constantLineDetails()) 312 | ; 313 | } 314 | 315 | private ConstantLineInterpreter.ParsedConstant parsedConstant(final Constant constant) { 316 | return ConstantLineInterpreter.ParsedConstant.builder() 317 | .constant(constant) 318 | .constantLineDetails(constantLineDetails()) 319 | .build(); 320 | } 321 | 322 | private static ConstantLineDetails constantLineDetails() { 323 | return ConstantLineDetails.builder() 324 | .line(INPUT_LINE) 325 | .build(); 326 | } 327 | } 328 | -------------------------------------------------------------------------------- /src/test/java/fr/chuckame/marlinfw/configurator/change/LineChangeTest.java: -------------------------------------------------------------------------------- 1 | package fr.chuckame.marlinfw.configurator.change; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static org.assertj.core.api.Assertions.assertThat; 6 | 7 | class LineChangeTest { 8 | private static final String CONSTANT_VALUE = "value"; 9 | 10 | @Test 11 | void isConstantShouldReturnFalseWhenParsedConstantIsNull() { 12 | final var lineChange = lineChange() 13 | .constant(null) 14 | .build(); 15 | 16 | final var isConstant = lineChange.isConstant(); 17 | 18 | assertThat(isConstant).isFalse(); 19 | } 20 | 21 | @Test 22 | void isConstantShouldReturnTrueWhenParsedConstantIsNotNull() { 23 | final var lineChange = lineChange() 24 | .build(); 25 | 26 | final var isConstant = lineChange.isConstant(); 27 | 28 | assertThat(isConstant).isTrue(); 29 | } 30 | 31 | private LineChange.LineChangeBuilder lineChange() { 32 | return LineChange.builder() 33 | .line("a line") 34 | .lineNumber(15) 35 | .diff(LineChange.DiffEnum.DO_NOTHING) 36 | .constant(LineChange.LineChangeConstant.builder() 37 | .currentValue(CONSTANT_VALUE) 38 | .wantedValue(CONSTANT_VALUE) 39 | .build()); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/test/java/fr/chuckame/marlinfw/configurator/change/LineChangeValidatorTest.java: -------------------------------------------------------------------------------- 1 | package fr.chuckame.marlinfw.configurator.change; 2 | 3 | import fr.chuckame.marlinfw.configurator.constant.Constant; 4 | import org.junit.jupiter.api.Test; 5 | import org.junit.jupiter.params.ParameterizedTest; 6 | import org.junit.jupiter.params.provider.Arguments; 7 | import org.junit.jupiter.params.provider.MethodSource; 8 | 9 | import java.util.Optional; 10 | import java.util.stream.Stream; 11 | 12 | import static org.assertj.core.api.Assertions.assertThat; 13 | 14 | /** 15 | * E/v -> E/NO_VALUE : ERROR
16 | * E/NO_VALUE -> E/v : ERROR
17 | * D/v -> E/NO_VALUE : ERROR
18 | * D/NO_VALUE -> E/v : ERROR
19 | * All other cases are good: no violation 20 | */ 21 | class LineChangeValidatorTest { 22 | private static final String CONSTANT_NAME = "constant"; 23 | private static final String CONSTANT_VALUE = "value1"; 24 | 25 | private final LineChangeValidator lineChangeValidator = new LineChangeValidator(); 26 | 27 | @Test 28 | void getViolationShouldReturnNullWhenGivenLineHasNoWantedConstant() { 29 | final Constant parsed = Constant.builder() 30 | .value(CONSTANT_VALUE) 31 | .build(); 32 | final Constant wanted = null; 33 | 34 | final var violation = lineChangeValidator.getViolation(parsed, wanted); 35 | 36 | assertThat(violation).isNull(); 37 | } 38 | 39 | @Test 40 | void getViolationShouldReturnNullWhenGivenLineIsNotAConstant() { 41 | final Constant parsed = null; 42 | final Constant wanted = null; 43 | 44 | final var violation = lineChangeValidator.getViolation(parsed, wanted); 45 | 46 | assertThat(violation).isNull(); 47 | } 48 | 49 | @Test 50 | void getViolationShouldReturnViolationWhenEnabledButCurrentWithValueAndWantedWithoutValue() { 51 | // E/v -> E/NO_VALUE : ERROR 52 | final Constant parsed = Constant.builder() 53 | .enabled(true) 54 | .value(CONSTANT_VALUE) 55 | .build(); 56 | final Constant wanted = Constant.builder() 57 | .enabled(true) 58 | .build(); 59 | 60 | final var violation = lineChangeValidator.getViolation(parsed, wanted); 61 | 62 | assertThat(violation).contains("Wanted value should be defined"); 63 | } 64 | 65 | @Test 66 | void getViolationShouldReturnViolationWhenCurrentDisabledWithValueAndWantedEnabledWithoutValue() { 67 | // D/v -> E/NO_VALUE : ERROR 68 | final Constant parsed = Constant.builder() 69 | .enabled(false) 70 | .value(CONSTANT_VALUE) 71 | .build(); 72 | final Constant wanted = Constant.builder() 73 | .enabled(true) 74 | .build(); 75 | 76 | final var violation = lineChangeValidator.getViolation(parsed, wanted); 77 | 78 | assertThat(violation).contains("Wanted value should be defined"); 79 | } 80 | 81 | @Test 82 | void getViolationShouldReturnViolationWhenEnabledButCurrentWithoutValueAndWantedWithValue() { 83 | // E/NO_VALUE -> E/v : ERROR 84 | final Constant parsed = Constant.builder() 85 | .enabled(true) 86 | .build(); 87 | final Constant wanted = Constant.builder() 88 | .enabled(true) 89 | .value(CONSTANT_VALUE) 90 | .build(); 91 | 92 | final var violation = lineChangeValidator.getViolation(parsed, wanted); 93 | 94 | assertThat(violation).contains("Wanted value should not be defined"); 95 | } 96 | 97 | @Test 98 | void getViolationShouldReturnViolationWhenDisabledCurrentWithoutValueAndEnabledWantedWithValue() { 99 | // D/NO_VALUE -> E/v : ERROR 100 | final Constant parsed = Constant.builder() 101 | .enabled(false) 102 | .build(); 103 | final Constant wanted = Constant.builder() 104 | .enabled(true) 105 | .value(CONSTANT_VALUE) 106 | .build(); 107 | 108 | final var violation = lineChangeValidator.getViolation(parsed, wanted); 109 | 110 | assertThat(violation).contains("Wanted value should not be defined"); 111 | } 112 | 113 | @ParameterizedTest 114 | @MethodSource("getViolationShouldReturnNullArguments") 115 | void getViolationShouldReturnNull(final Constant.ConstantBuilder currentConstant, final Constant.ConstantBuilder wantedConstant) { 116 | final var violation = lineChangeValidator.getViolation(Optional.ofNullable(currentConstant).map(Constant.ConstantBuilder::build).orElse(null), 117 | Optional.ofNullable(wantedConstant).map(Constant.ConstantBuilder::build).orElse(null)); 118 | 119 | assertThat(violation).isNull(); 120 | } 121 | 122 | private static Stream getViolationShouldReturnNullArguments() { 123 | return Stream.of( 124 | // SameEnabledConstantsIgnoringComment 125 | Arguments.arguments(Constant.builder().name(CONSTANT_NAME).enabled(true).value(CONSTANT_VALUE), 126 | Constant.builder().name(CONSTANT_NAME).enabled(true).value(CONSTANT_VALUE)), 127 | // SameDisabledConstantsIgnoringComment 128 | Arguments.arguments(Constant.builder().name(CONSTANT_NAME).enabled(false).value(CONSTANT_VALUE), 129 | Constant.builder().name(CONSTANT_NAME).enabled(false).value(CONSTANT_VALUE)), 130 | // E/v -> D/v 131 | Arguments.arguments(Constant.builder().name(CONSTANT_NAME).enabled(true).value(CONSTANT_VALUE), 132 | Constant.builder().name(CONSTANT_NAME).enabled(false).value(CONSTANT_VALUE)), 133 | // D/v -> E/v 134 | Arguments.arguments(Constant.builder().name(CONSTANT_NAME).enabled(false).value(CONSTANT_VALUE), 135 | Constant.builder().name(CONSTANT_NAME).enabled(true).value(CONSTANT_VALUE)), 136 | // E/NO_VALUE -> D/NO_VALUE 137 | Arguments.arguments(Constant.builder().name(CONSTANT_NAME).enabled(true), 138 | Constant.builder().name(CONSTANT_NAME).enabled(false)), 139 | // D/NO_VALUE -> E/NO_VALUE 140 | Arguments.arguments(Constant.builder().name(CONSTANT_NAME).enabled(false), 141 | Constant.builder().name(CONSTANT_NAME).enabled(true)) 142 | ); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/test/java/fr/chuckame/marlinfw/configurator/constant/ConstantLineInterpreterTest.java: -------------------------------------------------------------------------------- 1 | package fr.chuckame.marlinfw.configurator.constant; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.junit.jupiter.params.ParameterizedTest; 5 | import org.junit.jupiter.params.provider.CsvSource; 6 | import org.junit.jupiter.params.provider.ValueSource; 7 | 8 | import static org.assertj.core.api.Assertions.assertThat; 9 | 10 | class ConstantLineInterpreterTest { 11 | private final ConstantLineInterpreter constantLineInterpreter = new ConstantLineInterpreter(); 12 | 13 | // fixme cannot parse following lines when first line ending with backslash '\': https://github.com/Chuckame/marlin-console-configurator/issues/1 14 | @Test 15 | void TEMPORARY_FIX_parseLineShouldReturnNothingWhenValueEndsWithBackslash() { 16 | final var line = "#define CONSTANT value \\"; 17 | 18 | final var constant = constantLineInterpreter.parseLine(line); 19 | 20 | assertThat(constant.blockOptional()).isEmpty(); 21 | } 22 | 23 | @ParameterizedTest 24 | @CsvSource({ 25 | "'#define coNsTANT ',coNsTANT,", 26 | "'#define HEADER azert',HEADER,azert", 27 | "'#define A_VALUE 123',A_VALUE,123", 28 | "'#define coNsTANT 123 ',coNsTANT,123", 29 | "' #define cool_CONSTANT 123 ',cool_CONSTANT,123", 30 | "' #define cool_CONSTANT { 15, 23, 88 } ',cool_CONSTANT,'{ 15, 23, 88 }'", 31 | "' #define coNsTANT \"quoted text\" ',coNsTANT,\"quoted text\"", 32 | "' #define coNsTANT \"quoted\\n text\" ',coNsTANT,\"quoted\\n text\"", 33 | }) 34 | void parseLineShouldReturnExpectedNameAndValueAndEnabled(final String line, final String expectedName, final String expectedValue) { 35 | final var expectedConstant = Constant.builder().enabled(true).name(expectedName).value(expectedValue).build(); 36 | 37 | final var constant = constantLineInterpreter.parseLine(line); 38 | 39 | assertThat(constant.blockOptional()).map(ConstantLineInterpreter.ParsedConstant::getConstant).hasValue(expectedConstant); 40 | } 41 | 42 | @ParameterizedTest 43 | @CsvSource({ 44 | "'//#define coNsTANT ',coNsTANT,", 45 | "' // #define HEADER azert',HEADER,azert", 46 | "'// #define A_VALUE 123',A_VALUE,123", 47 | "' // #define coNsTANT 123 ',coNsTANT,123", 48 | "' // #define cool_CONSTANT { 15, 23, 88 } ',cool_CONSTANT,'{ 15, 23, 88 }'", 49 | "' // #define cool_CONSTANT 123 ',cool_CONSTANT,123", 50 | "' //#define coNsTANT \"quoted\\n text\" ',coNsTANT,\"quoted\\n text\"", 51 | }) 52 | void parseLineShouldReturnExpectedNameAndValueAndDisabled(final String line, final String expectedName, final String expectedValue) { 53 | final var expectedConstant = Constant.builder().enabled(false).name(expectedName).value(expectedValue).build(); 54 | 55 | final var constant = constantLineInterpreter.parseLine(line); 56 | 57 | assertThat(constant.blockOptional()).map(ConstantLineInterpreter.ParsedConstant::getConstant).hasValue(expectedConstant); 58 | } 59 | 60 | @ParameterizedTest 61 | @ValueSource(strings = { 62 | " * #define coNsTANT ", 63 | "", 64 | " some text ", 65 | }) 66 | void parseLineShouldReturnNothing(final String line) { 67 | final var constant = constantLineInterpreter.parseLine(line); 68 | 69 | assertThat(constant.blockOptional()).isEmpty(); 70 | } 71 | 72 | @ParameterizedTest 73 | @ValueSource(strings = { 74 | "#define coNsTANT ", 75 | "#define HEADER azert", 76 | "#define A_VALUE 123", 77 | "#define coNsTANT 123 ", 78 | " #define cool_CONSTANT 123 ", 79 | " #define cool_CONSTANT { 15, 23, 88 } ", 80 | " #define coNsTANT \"quoted text\" ", 81 | " #define coNsTANT \"quoted\\n text\" ", 82 | }) 83 | void disableLine(final String line) { 84 | final var lineDetails = ConstantLineDetails.builder() 85 | .line(line) 86 | .build(); 87 | 88 | final var constant = constantLineInterpreter.disableLine(lineDetails); 89 | 90 | assertThat(constant.blockOptional()).hasValue("//" + line); 91 | } 92 | 93 | @ParameterizedTest 94 | @CsvSource({ 95 | "'// #define coNsTANT ',0,2,' #define coNsTANT '", 96 | "' //#define HEADER azert',3,5,' #define HEADER azert'", 97 | "' // #define A_VALUE 123',2,4,' #define A_VALUE 123'", 98 | "'// #define coNsTANT 123 ',0,2,' #define coNsTANT 123 '", 99 | "' // #define cool_CONSTANT 123 ',9,11,' #define cool_CONSTANT 123 '", 100 | "' // #define cool_CONSTANT { 15, 23, 88 } ',5,7,' #define cool_CONSTANT { 15, 23, 88 } '", 101 | "' //#define coNsTANT \"quoted\\n text\" ',8,10,' #define coNsTANT \"quoted\\n text\" '", 102 | }) 103 | void enableLine(final String line, final int startIndex, final int endIndex, final String expectedLine) { 104 | final var lineDetails = ConstantLineDetails.builder() 105 | .line(line) 106 | .disabledMatchIndex(ConstantLineDetails.MatchIndex.builder() 107 | .start(startIndex) 108 | .end(endIndex) 109 | .build()) 110 | .build(); 111 | 112 | final var constant = constantLineInterpreter.enableLine(lineDetails); 113 | 114 | assertThat(constant.blockOptional()).hasValue(expectedLine); 115 | } 116 | 117 | @ParameterizedTest 118 | @CsvSource({ 119 | "' //#define HEADER azert',3,5,23,28,toto,' #define HEADER toto'", 120 | "' // #define A_VALUE 123',2,4,23,26,OH YEAH,' #define A_VALUE OH YEAH'", 121 | "'// #define coNsTANT 123 ',0,2,33,36,Some value,' #define coNsTANT Some value '", 122 | "' // #define cool_CONSTANT 123 ',9,11,40,43,azerty1234§è!çà,' #define cool_CONSTANT azerty1234§è!çà '", 123 | "' // #define cool_CONSTANT { 15, 23, 88 } ',5,7,40,54,'{ 111, 222, 333 }',' #define cool_CONSTANT { 111, 222, 333 } '", 124 | "' //#define coNsTANT \"quoted\\n text\" ',8,10,33,48,\"other quoted\\n text cool\",' #define coNsTANT \"other quoted\\n text cool\" '", 125 | }) 126 | void enableLineAndChangeValue(final String line, final int disabledStart, final int disabledEnd, final int valueStart, final int valueEnd, final String newValue, 127 | final String expectedLine) { 128 | final var lineDetails = ConstantLineDetails.builder() 129 | .line(line) 130 | .disabledMatchIndex(ConstantLineDetails.MatchIndex.builder() 131 | .start(disabledStart) 132 | .end(disabledEnd) 133 | .build()) 134 | .valueMatchIndex(ConstantLineDetails.MatchIndex.builder() 135 | .start(valueStart) 136 | .end(valueEnd) 137 | .build()) 138 | .build(); 139 | 140 | final var constant = constantLineInterpreter.enableLineAndChangeValue(lineDetails, newValue); 141 | 142 | assertThat(constant.blockOptional()).hasValue(expectedLine); 143 | } 144 | 145 | @ParameterizedTest 146 | @CsvSource({ 147 | "' #define HEADER azert',21,26,toto,' #define HEADER toto'", 148 | "' #define A_VALUE 123',21,24,OH YEAH,' #define A_VALUE OH YEAH'", 149 | "' #define coNsTANT 123 ',31,34,Some value,' #define coNsTANT Some value '", 150 | "' #define cool_CONSTANT 123 ',38,41,azerty1234§è!çà,' #define cool_CONSTANT azerty1234§è!çà '", 151 | "' #define cool_CONSTANT { 15, 23, 88 } ',38,52,'{ 111, 222, 333 }',' #define cool_CONSTANT { 111, 222, 333 } '", 152 | "' #define coNsTANT \"quoted\\n text\" ',31,46,\"other quoted\\n text cool\",' #define coNsTANT \"other quoted\\n text cool\" '", 153 | }) 154 | void changeValue(final String line, final int startIndex, final int endIndex, final String newValue, final String expectedLine) { 155 | final var lineDetails = ConstantLineDetails.builder() 156 | .line(line) 157 | .valueMatchIndex(ConstantLineDetails.MatchIndex.builder() 158 | .start(startIndex) 159 | .end(endIndex) 160 | .build()) 161 | .build(); 162 | 163 | final var constant = constantLineInterpreter.changeValue(lineDetails, newValue); 164 | 165 | assertThat(constant.blockOptional()).hasValue(expectedLine); 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/test/java/fr/chuckame/marlinfw/configurator/constant/ProfileAdapterTest.java: -------------------------------------------------------------------------------- 1 | package fr.chuckame.marlinfw.configurator.constant; 2 | 3 | import fr.chuckame.marlinfw.configurator.profile.ProfileProperties; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import java.util.HashMap; 7 | import java.util.List; 8 | import java.util.Map; 9 | 10 | import static org.assertj.core.api.Assertions.assertThat; 11 | 12 | class ProfileAdapterTest { 13 | private final ProfileAdapter profileAdapter = new ProfileAdapter(); 14 | 15 | @Test 16 | void getWantedConstantsShouldReturnEmptyMapWhenEmptyEnabledAndEmptyDisabled() { 17 | final var profile = ProfileProperties.builder() 18 | .enabled(Map.of()) 19 | .disabled(List.of()) 20 | .build(); 21 | 22 | final var wantedConstants = profileAdapter.profileToConstants(profile); 23 | 24 | assertThat(wantedConstants).isEmpty(); 25 | } 26 | 27 | @Test 28 | void getWantedConstantsShouldReturnExpectedConstants() { 29 | final var profile = ProfileProperties.builder() 30 | .enabled(withNullValue("c1", Map.of("c2", "v2"))) 31 | .disabled(List.of("c3", "c4")) 32 | .build(); 33 | 34 | final var wantedConstants = profileAdapter.profileToConstants(profile); 35 | 36 | assertThat(wantedConstants) 37 | .containsEntry("c1", Constant.builder().enabled(true).name("c1").value(null).build()) 38 | .containsEntry("c2", Constant.builder().enabled(true).name("c2").value("v2").build()) 39 | .containsEntry("c3", Constant.builder().enabled(false).name("c3").value(null).build()) 40 | .containsEntry("c4", Constant.builder().enabled(false).name("c4").value(null).build()); 41 | } 42 | 43 | private Map withNullValue(final String key, final Map map) { 44 | final var output = new HashMap<>(map); 45 | output.put(key, null); 46 | return output; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/test/java/fr/chuckame/marlinfw/configurator/profile/ProfilePropertiesParserTest.java: -------------------------------------------------------------------------------- 1 | package fr.chuckame.marlinfw.configurator.profile; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; 5 | import fr.chuckame.marlinfw.configurator.util.FileHelper; 6 | import org.junit.jupiter.api.Test; 7 | import org.junit.jupiter.api.extension.ExtendWith; 8 | import org.mockito.ArgumentCaptor; 9 | import org.mockito.ArgumentMatchers; 10 | import org.mockito.InjectMocks; 11 | import org.mockito.Mock; 12 | import org.mockito.Mockito; 13 | import org.mockito.junit.jupiter.MockitoExtension; 14 | import reactor.core.publisher.Mono; 15 | import reactor.test.StepVerifier; 16 | 17 | import java.io.File; 18 | import java.io.IOException; 19 | import java.nio.file.Files; 20 | import java.nio.file.Path; 21 | import java.util.ArrayList; 22 | import java.util.HashMap; 23 | import java.util.LinkedHashMap; 24 | import java.util.List; 25 | import java.util.Map; 26 | import java.util.Objects; 27 | 28 | import static org.assertj.core.api.Assertions.assertThat; 29 | 30 | @ExtendWith(MockitoExtension.class) 31 | class ProfilePropertiesParserTest { 32 | private final Path FILE_PATH = resourceToPath("profile.yaml"); 33 | 34 | @Mock 35 | private FileHelper fileHelperMock; 36 | @InjectMocks 37 | private ProfilePropertiesParser profilePropertiesParser; 38 | 39 | @Test 40 | void parseFromFileShouldReturnExpectedProfile() throws IOException { 41 | Mockito.when(fileHelperMock.read(FILE_PATH)).thenReturn(Mono.just(Files.readAllBytes(FILE_PATH))); 42 | 43 | final var profileProperties = profilePropertiesParser.parseFromFile(FILE_PATH); 44 | 45 | StepVerifier.create(profileProperties) 46 | .expectNext(profileProperties()) 47 | .expectComplete() 48 | .verify(); 49 | } 50 | 51 | @Test 52 | void parseFromFileShouldReturnNotReturnNullValues() throws IOException { 53 | Mockito.when(fileHelperMock.read(FILE_PATH)).thenReturn(Mono.just("enabled:\n".getBytes())); 54 | 55 | final var profileProperties = profilePropertiesParser.parseFromFile(FILE_PATH); 56 | 57 | StepVerifier.create(profileProperties) 58 | .expectNext(ProfileProperties.builder() 59 | .enabled(Map.of()) 60 | .disabled(List.of()) 61 | .build()) 62 | .expectComplete() 63 | .verify(); 64 | } 65 | 66 | @Test 67 | void writeToFileShouldWriteExpectedContent() throws IOException { 68 | final var yamlMapper = new ObjectMapper(new YAMLFactory()); 69 | final var captor = ArgumentCaptor.forClass(byte[].class); 70 | Mockito.when(fileHelperMock.write(captor.capture(), ArgumentMatchers.same(FILE_PATH))).thenReturn(Mono.empty()); 71 | 72 | final var writerMono = profilePropertiesParser.writeToFile(profileProperties(), FILE_PATH); 73 | 74 | StepVerifier.create(writerMono) 75 | .expectComplete() 76 | .verify(); 77 | 78 | final ProfileProperties writtenProfileProperties = yamlMapper.readValue(captor.getValue(), ProfileProperties.class); 79 | assertThat(writtenProfileProperties).isEqualTo(profileProperties()); 80 | } 81 | 82 | private Map enabledConstants() { 83 | final Map enabledConstants = new HashMap<>(Map.of( 84 | "CONSTANT_WITH_VALUE_NUMBER", "1", 85 | "CONSTANT_WITH_VALUE_STRING", "\"a string value that requires quotes\"", 86 | "CONSTANT_WITH_VALUE_COMPLEX_STRING", "\"a complex value that requires quotes and\\nreturns\"", 87 | "CONSTANT_WITH_VALUE_ARRAY", "{ 14, 15, 28 }", 88 | "WeiRd_conStANt", "hey" 89 | )); 90 | enabledConstants.put("CONSTANT_WITHOUT_VALUE", null); 91 | return enabledConstants; 92 | } 93 | 94 | private Map disabledConstants() { 95 | final var constants = new LinkedHashMap(); 96 | constants.put("DISABLED_CONSTANT", null); 97 | constants.put("DiSaBLeD_CoNsTANt2", null); 98 | return constants; 99 | } 100 | 101 | private ProfileProperties profileProperties() { 102 | return ProfileProperties.builder() 103 | .enabled(enabledConstants()) 104 | .disabled(new ArrayList<>(disabledConstants().keySet())) 105 | .build(); 106 | } 107 | 108 | private Path resourceToPath(final String resourcePath) { 109 | return new File(Objects.requireNonNull(getClass().getClassLoader().getResource(resourcePath), resourcePath + " not found").getFile()).toPath(); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/test/resources/file.h: -------------------------------------------------------------------------------- 1 | #define CONSTANT_WITH_VALUE_STRING "a string value that requires quotes" // hello 2 | #define CONSTANT_WITH_VALUE_COMPLEX_STRING "a complex value that requires quotes and\nreturns"// 3 | #define CONSTANT_WITHOUT_VALUE // another comment 4 | #define WeiRd_conStANt hey//comment 5 | 6 | //#define DISABLED_CONSTANT // cool thing on disabled comment 7 | 8 | #define CONSTANT_WITH_VALUE_ARRAY { 14, 15, 28 } // a comment 9 | #define CONSTANT_WITH_VALUE_NUMBER 1 10 | 11 | //#define DiSaBLeD_CoNsTANt2 12 | -------------------------------------------------------------------------------- /src/test/resources/file2.h: -------------------------------------------------------------------------------- 1 | #define OTHER_WeiRd_conStANt hey//comment 2 | 3 | //#define OTHER_DISABLED_CONSTANT // cool thing on disabled comment 4 | 5 | #define CONSTANT_WITH_VALUE_ARRAY { 14, 15, 42 } // a comment 6 | -------------------------------------------------------------------------------- /src/test/resources/profile.yaml: -------------------------------------------------------------------------------- 1 | enabled: 2 | CONSTANT_WITH_VALUE_ARRAY: '{ 14, 15, 28 }' 3 | CONSTANT_WITH_VALUE_NUMBER: 1 4 | CONSTANT_WITH_VALUE_COMPLEX_STRING: '"a complex value that requires quotes and\nreturns"' 5 | CONSTANT_WITHOUT_VALUE: # ':' is mandatory, even if no value !! 6 | CONSTANT_WITH_VALUE_STRING: '"a string value that requires quotes"' 7 | WeiRd_conStANt: hey 8 | disabled: 9 | - DISABLED_CONSTANT 10 | - DiSaBLeD_CoNsTANt2 11 | --------------------------------------------------------------------------------