From 75c47d4bfe1fba4c87828fc080d57341ef8a3b64 Mon Sep 17 00:00:00 2001 From: Fadelis Date: Fri, 16 Jul 2021 17:01:04 +0200 Subject: [PATCH] Initial commit --- .github/workflows/build_pipeline.yml | 31 +++ .github/workflows/release_pipeline.yml | 74 ++++++ .gitignore | 3 + LICENSE | 222 ++++++++++++++-- README.md | 87 +++++++ pom.xml | 245 ++++++++++++++++++ protoc-gen-java-optional-test/pom.xml | 120 +++++++++ .../src/test/java/DataTypesTest.java | 162 ++++++++++++ .../src/test/resources/data-types.proto | 73 ++++++ .../dependency-reduced-pom.xml | 120 +++++++++ protoc-gen-java-optional/pom.xml | 70 +++++ .../protoc/plugin/OptionalGenerator.java | 233 +++++++++++++++++ .../grpcmock/protoc/plugin/Parameters.java | 81 ++++++ .../resources/templates/optionalGet.mustache | 7 + .../templates/optionalSetOrClear.mustache | 8 + .../resources/templates/setOrClear.mustache | 8 + .../protoc/plugin/ParametersTest.java | 34 +++ 17 files changed, 1557 insertions(+), 21 deletions(-) create mode 100644 .github/workflows/build_pipeline.yml create mode 100644 .github/workflows/release_pipeline.yml create mode 100644 .gitignore create mode 100644 README.md create mode 100644 pom.xml create mode 100644 protoc-gen-java-optional-test/pom.xml create mode 100644 protoc-gen-java-optional-test/src/test/java/DataTypesTest.java create mode 100644 protoc-gen-java-optional-test/src/test/resources/data-types.proto create mode 100644 protoc-gen-java-optional/dependency-reduced-pom.xml create mode 100644 protoc-gen-java-optional/pom.xml create mode 100644 protoc-gen-java-optional/src/main/java/org/grpcmock/protoc/plugin/OptionalGenerator.java create mode 100644 protoc-gen-java-optional/src/main/java/org/grpcmock/protoc/plugin/Parameters.java create mode 100644 protoc-gen-java-optional/src/main/resources/templates/optionalGet.mustache create mode 100644 protoc-gen-java-optional/src/main/resources/templates/optionalSetOrClear.mustache create mode 100644 protoc-gen-java-optional/src/main/resources/templates/setOrClear.mustache create mode 100644 protoc-gen-java-optional/src/test/java/org/grpcmock/protoc/plugin/ParametersTest.java diff --git a/.github/workflows/build_pipeline.yml b/.github/workflows/build_pipeline.yml new file mode 100644 index 0000000..04f1113 --- /dev/null +++ b/.github/workflows/build_pipeline.yml @@ -0,0 +1,31 @@ +name: Build pipeline + +on: + push: + branches: + - '**' + +jobs: + build: + runs-on: ubuntu-latest + timeout-minutes: 20 + + steps: + - name: Checkout code + uses: actions/checkout@v1 + + - name: Set up JDK 8 + uses: actions/setup-java@v1 + with: + java-version: 1.8 + + - name: Restore Maven repository + uses: actions/cache@v1 + with: + path: ~/.m2/repository + key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} + + - name: mvn verify + run: | + mvn install -pl protoc-gen-java-optional -DskipTests + mvn --batch-mode -T1C verify diff --git a/.github/workflows/release_pipeline.yml b/.github/workflows/release_pipeline.yml new file mode 100644 index 0000000..5268068 --- /dev/null +++ b/.github/workflows/release_pipeline.yml @@ -0,0 +1,74 @@ +name: Release pipeline + +on: + push: + tags: + - '*' + +jobs: + release: + runs-on: ubuntu-latest + timeout-minutes: 20 + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Set up JDK 8 + uses: actions/setup-java@v1 + with: + java-version: 1.8 + + - name: Restore Maven repository + uses: actions/cache@v1 + with: + path: ~/.m2/repository + key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} + + - name: Set version + id: version + run: | + export GITHUB_TAG=${GITHUB_REF##*/} + mvn versions:set -DnewVersion=$GITHUB_TAG + sed -i 's/HEAD<\/tag>/$GITHUB_TAG<\/tag>/' pom.xml + echo ::set-output name=github_tag::${CURRENT_TAG} + + - name: Perform release + uses: samuelmeuli/action-maven-publish@v1 + with: + directory: protoc-gen-java-optional + maven_args: '-DstagingProgressTimeoutMinutes=60' + gpg_private_key: ${{ secrets.gpg_private_key }} + gpg_passphrase: ${{ secrets.gpg_passphrase }} + nexus_username: ${{ secrets.nexus_username }} + nexus_password: ${{ secrets.nexus_password }} + + - name: Github Release + uses: softprops/action-gh-release@v1 + with: + files: | + ./protoc-gen-java-optional/target/protoc-gen-java-optional-${{ steps.version.outputs.github_tag }}-linux-aarch_64.exe + ./protoc-gen-java-optional/target/protoc-gen-java-optional-${{ steps.version.outputs.github_tag }}-linux-x86_64.exe + ./protoc-gen-java-optional/target/protoc-gen-java-optional-${{ steps.version.outputs.github_tag }}-osx-aarch_64.exe + ./protoc-gen-java-optional/target/protoc-gen-java-optional-${{ steps.version.outputs.github_tag }}-osx-x86_64.exe + ./protoc-gen-java-optional/target/protoc-gen-java-optional-${{ steps.version.outputs.github_tag }}-windows-x86_64.exe + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Clean release changes + run: git add . && git stash && git stash drop + + - name: Bump snapshot version + run: | + export GITHUB_TAG=${GITHUB_REF##*/} + export NEW_SNAPSHOT_VERSION=${GITHUB_TAG%%${GITHUB_TAG##*[!0-9]}}$((${GITHUB_TAG##*[!0-9]} + 1))-SNAPSHOT + sed -i "s/[0-9.]*<\/version>/$GITHUB_TAG<\/version>/" README.md + mvn versions:set -DgenerateBackupPoms=false -DnewVersion=$NEW_SNAPSHOT_VERSION + + - name: Commit snapshot version bump + uses: EndBug/add-and-commit@v7 + with: + branch: 'master' + message: 'bump snapshot version' + author_name: github-actions + author_email: 41898282+github-actions[bot]@users.noreply.github.com diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2125173 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +**/.idea +**/*.iml +**/target \ No newline at end of file diff --git a/LICENSE b/LICENSE index 78333a6..261eeb9 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,201 @@ -MIT License - -Copyright (c) 2021 Paulius Paplauskas - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..87407ef --- /dev/null +++ b/README.md @@ -0,0 +1,87 @@ +# Protoc Generator Java Optional ![Build pipeline](https://github.com/Fadelis/protoc-gen-java-optional/workflows/Build%20pipeline/badge.svg) + +A Java Protoc plugin extending generated java classes with null safe `setOrClear` and `getOptional` methods. Also works for +protobuf primitive variables with the [optional](https://github.com/protocolbuffers/protobuf/blob/v3.12.0/docs/field_presence.md) +keyword. + +## Quick usage + +### Using `protoc` binary + +You must have `protoc` binary installed in your system and have to download `protoc-gen-java-optional` executable based on +platform from GitHub [releases](https://github.com/Fadelis/protoc-gen-java-optional/releases) +or [maven-central](). + +With defaults: + +``` +protoc --proto-path=./protos/ --java_out=./out/directory --plugin=protoc-gen-java-optional=./path/to/plugin/protoc-gen-java-optional.exe --java-optional_out=./out/directory ./protos/sample.proto +``` + +With some parameters: + +``` +protoc --proto-path=./protos/ --java_out=./out/directory --plugin=protoc-gen-java-optional=./path/to/plugin/protoc-gen-java-optional.exe --java-optional_out=setter_optional=true,use_primitive_optionals=true:./out/directory ./protos/sample.proto +``` + +### Using [protobuf-maven-plugin](https://github.com/xolstice/protobuf-maven-plugin) + +```xml + + + + + kr.motd.maven + os-maven-plugin + ${os-maven-plugin.version} + + + + + org.xolstice.maven.plugins + protobuf-maven-plugin + ${protobuf-maven-plugin.version} + + + protobuf + + compile + + generate-sources + + com.google.protobuf:protoc:${protoc.version}:exe:${os.detected.classifier} + ${project.basedir}/src/test/resources/ + true + + + java-optional + org.grpcmock + protoc-gen-java-optional + 1.0.0 + org.grpcmock.protoc.plugin.OptionalGenerator + + + + + + + + +``` + +This plugin does not support passing java protoc plugin parameters +until https://github.com/xolstice/protobuf-maven-plugin/issues/56 is solved, so default options will be used. + +### Using [protoc-jar-maven-plugin](https://github.com/os72/protoc-jar-maven-plugin) + +This maven plugin is not supported as it does not support running multiple output types as a single protoc command. This protoc +plugin needs to be run together with the `java_out` target in order to extend generated classes. + +## Configuration parameters + +- `setter_object` - boolean flag indicating whether to add setter methods with the nullable object itself as argument. + Default `true`. +- `setter_optional` - boolean flag indicating whether to add setter methods with `Optional` as an argument. Default `false`. +- `getter_optional` - boolean flag indicating whether to add getter methods returning {@link java.util.Optional}. Default `true`. +- `use_primitive_optionals` - boolean flag indicating whether to use primitive + optionals (`OptionalInt/OptionalLong/OptionalDouble`) for `optional` protobuf primitive's setters and getters. Default `false`. diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..18efc24 --- /dev/null +++ b/pom.xml @@ -0,0 +1,245 @@ + + + 4.0.0 + + org.grpcmock + protoc + 1.0.0-SNAPSHOT + pom + + + protoc-gen-java-optional + protoc-gen-java-optional-test + + + Protoc Generator + Protoc generator plugin for java adding support for null safe extra methods + https://github.com/Fadelis/protoc-gen-java-optional + + + UTF-8 + UTF-8 + 1.8 + + 1.1.0 + 1.1.0 + 3.2.0 + 3.2.4 + 3.15.8 + 1.6.2 + 0.6.1 + 3.11.4 + 5.7.0 + 3.19.0 + 3.7.0 + 3.14.0 + 3.8.1 + 2.22.2 + 3.0.0 + 1.6.8 + 3.0.0 + 2.8.2 + 2.5.3 + 3.1.0 + 3.1.0 + 1.6 + + + + + org.junit.jupiter + junit-jupiter-api + ${junit-jupiter.version} + test + + + org.junit.jupiter + junit-jupiter-params + ${junit-jupiter.version} + test + + + org.junit.jupiter + junit-jupiter-engine + ${junit-jupiter.version} + test + + + org.assertj + assertj-core + ${assertj.version} + test + + + org.mockito + mockito-inline + ${mockito.version} + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + ${maven-compiler-plugin.version} + + true + ${java.version} + ${java.version} + ${java.version} + + + + + org.apache.maven.plugins + maven-pmd-plugin + ${pmd.version} + + true + true + false + + ${project.build.directory}/generated-sources + + + + + pmd-check + verify + + check + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven-surefire-plugin.version} + + + + org.sonatype.plugins + nexus-staging-maven-plugin + ${nexus-staging-maven-plugin.version} + true + + ossrh + https://oss.sonatype.org/ + true + + + + + + + ${basedir}/src/test/resources + + + + + + + + deploy + + + + + org.apache.maven.plugins + maven-source-plugin + ${maven-source-plugin.version} + + + attach-sources + + jar-no-fork + + + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + ${maven-javadoc-plugin.version} + + ${java.version} + none + + + + attach-javadocs + + jar + + + + + + + + org.apache.maven.plugins + maven-gpg-plugin + ${maven-gpg-plugin.version} + + + sign-artifacts + verify + + sign + + + + + --pinentry-mode + loopback + + + + + + + + + + + + + Apache License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + https://github.com/Fadelis/grpcmock + scm:git:git://github.com/Fadelis/grpcmock.git + scm:git:git@github.com/Fadelis/grpcmock.git + HEAD + + + + Fadelis + https://github.com/Fadelis + + + + + + + ossrh + https://oss.sonatype.org/content/repositories/snapshots + + + ossrh + https://oss.sonatype.org/service/local/staging/deploy/maven2/ + + + + diff --git a/protoc-gen-java-optional-test/pom.xml b/protoc-gen-java-optional-test/pom.xml new file mode 100644 index 0000000..4212012 --- /dev/null +++ b/protoc-gen-java-optional-test/pom.xml @@ -0,0 +1,120 @@ + + + + protoc + org.grpcmock + 1.0.0-SNAPSHOT + + 4.0.0 + + protoc-gen-java-optional-test + + Protoc Generator Java Optional Test + + + + com.google.protobuf + protobuf-java + ${protoc.version} + + + + + + + kr.motd.maven + os-maven-plugin + ${os-maven-plugin.version} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + org.xolstice.maven.plugins + protobuf-maven-plugin + ${protobuf-maven-plugin.version} + + + protobuf + + compile + + generate-sources + + com.google.protobuf:protoc:${protoc.version}:exe:${os.detected.classifier} + ${project.basedir}/src/test/resources/ + ${project.build.directory}/generated-sources/protobuf + true + + + java-optional + ${project.groupId} + protoc-gen-java-optional + ${project.version} + org.grpcmock.protoc.plugin.OptionalGenerator + + + + + + + + + diff --git a/protoc-gen-java-optional-test/src/test/java/DataTypesTest.java b/protoc-gen-java-optional-test/src/test/java/DataTypesTest.java new file mode 100644 index 0000000..0215b27 --- /dev/null +++ b/protoc-gen-java-optional-test/src/test/java/DataTypesTest.java @@ -0,0 +1,162 @@ +import static org.assertj.core.api.Assertions.assertThat; + +import com.google.protobuf.ByteString; +import org.junit.jupiter.api.Test; +import org.test.datatypes.TestEnum; +import org.test.datatypes.TestMessage; +import org.test.datatypes.TestSubMessage; + +class DataTypesTest { + + @Test + void should_clear_field_data_on_null() { + TestMessage.Builder builder = populatedTestMessage(); + + builder.setOrClearOneofString(null); + assertThat(builder.hasOneofString()).isFalse(); + + builder.setOrClearOptionalBool(null); + assertThat(builder.hasOptionalBool()).isFalse(); + + builder.setOrClearOptionalString(null); + assertThat(builder.hasOptionalString()).isFalse(); + + builder.setOrClearOptionalBytes(null); + assertThat(builder.hasOptionalBytes()).isFalse(); + + builder.setOrClearOptionalEnum(null); + assertThat(builder.hasOptionalEnum()).isFalse(); + + builder.setOrClearOptionalFloat(null); + assertThat(builder.hasOptionalFloat()).isFalse(); + + builder.setOrClearOptionalDouble(null); + assertThat(builder.hasOptionalDouble()).isFalse(); + + builder.setOrClearOptionalFixed32(null); + assertThat(builder.hasOptionalFixed32()).isFalse(); + + builder.setOrClearOptionalFixed64(null); + assertThat(builder.hasOptionalFixed64()).isFalse(); + + builder.setOrClearOptionalInt32(null); + assertThat(builder.hasOptionalInt32()).isFalse(); + + builder.setOrClearOptionalInt64(null); + assertThat(builder.hasOptionalInt64()).isFalse(); + + builder.setOrClearOptionalUint32(null); + assertThat(builder.hasOptionalUint32()).isFalse(); + + builder.setOrClearOptionalUint64(null); + assertThat(builder.hasOptionalUint64()).isFalse(); + + builder.setOrClearSubMessage(null); + assertThat(builder.hasSubMessage()).isFalse(); + } + + @Test + void should_set_field_data_on_non_null() { + TestMessage.Builder builder = populatedTestMessage(); + + builder.setOrClearOneofString("one-of"); + assertThat(builder.hasOneofString()).isTrue(); + + builder.setOrClearOptionalBool(true); + assertThat(builder.hasOptionalBool()).isTrue(); + + builder.setOrClearOptionalString("opt-string"); + assertThat(builder.hasOptionalString()).isTrue(); + + builder.setOrClearOptionalBytes(ByteString.copyFromUtf8("opt-bytes")); + assertThat(builder.hasOptionalBytes()).isTrue(); + + builder.setOrClearOptionalEnum(TestEnum.value1); + assertThat(builder.hasOptionalEnum()).isTrue(); + + builder.setOrClearOptionalFloat(3.5F); + assertThat(builder.hasOptionalFloat()).isTrue(); + + builder.setOrClearOptionalDouble(3.5D); + assertThat(builder.hasOptionalDouble()).isTrue(); + + builder.setOrClearOptionalFixed32(10); + assertThat(builder.hasOptionalFixed32()).isTrue(); + + builder.setOrClearOptionalFixed64(10L); + assertThat(builder.hasOptionalFixed64()).isTrue(); + + builder.setOrClearOptionalInt32(10); + assertThat(builder.hasOptionalInt32()).isTrue(); + + builder.setOrClearOptionalInt64(10L); + assertThat(builder.hasOptionalInt64()).isTrue(); + + builder.setOrClearOptionalUint32(10); + assertThat(builder.hasOptionalUint32()).isTrue(); + + builder.setOrClearOptionalUint64(10L); + assertThat(builder.hasOptionalUint64()).isTrue(); + + builder.setOrClearSubMessage(TestSubMessage.newBuilder().setString("value").build()); + assertThat(builder.hasSubMessage()).isTrue(); + } + + @Test + void should_have_optional_populated_with_field_data() { + TestMessage message = populatedTestMessage().build(); + + assertThat(message.getOptionalOneofString()).contains("one-of"); + assertThat(message.getOptionalOptionalBool()).contains(true); + assertThat(message.getOptionalOptionalString()).contains("opt-string"); + assertThat(message.getOptionalOptionalBytes()).contains(ByteString.copyFromUtf8("opt-bytes")); + assertThat(message.getOptionalOptionalEnum()).contains(TestEnum.value1); + assertThat(message.getOptionalOptionalFloat()).contains(3.5F); + assertThat(message.getOptionalOptionalDouble()).contains(3.5D); + assertThat(message.getOptionalOptionalFixed32()).contains(10); + assertThat(message.getOptionalOptionalFixed64()).contains(10L); + assertThat(message.getOptionalOptionalInt32()).contains(10); + assertThat(message.getOptionalOptionalInt64()).contains(10L); + assertThat(message.getOptionalOptionalUint32()).contains(10); + assertThat(message.getOptionalOptionalUint64()).contains(10L); + assertThat(message.getOptionalSubMessage()).contains(TestSubMessage.newBuilder().setString("value").build()); + } + + @Test + void should_have_empty_optional_on_no_data() { + TestMessage message = TestMessage.getDefaultInstance(); + + assertThat(message.getOptionalOneofString()).isEmpty(); + assertThat(message.getOptionalOptionalBool()).isEmpty(); + assertThat(message.getOptionalOptionalString()).isEmpty(); + assertThat(message.getOptionalOptionalBytes()).isEmpty(); + assertThat(message.getOptionalOptionalEnum()).isEmpty(); + assertThat(message.getOptionalOptionalFloat()).isEmpty(); + assertThat(message.getOptionalOptionalDouble()).isEmpty(); + assertThat(message.getOptionalOptionalFixed32()).isEmpty(); + assertThat(message.getOptionalOptionalFixed64()).isEmpty(); + assertThat(message.getOptionalOptionalInt32()).isEmpty(); + assertThat(message.getOptionalOptionalInt64()).isEmpty(); + assertThat(message.getOptionalOptionalUint32()).isEmpty(); + assertThat(message.getOptionalOptionalUint64()).isEmpty(); + assertThat(message.getOptionalSubMessage()).isEmpty(); + } + + private static TestMessage.Builder populatedTestMessage() { + return TestMessage.newBuilder() + .setOneofString("one-of") + .setOptionalBool(true) + .setOptionalString("opt-string") + .setOptionalBytes(ByteString.copyFromUtf8("opt-bytes")) + .setOptionalEnum(TestEnum.value1) + .setOptionalFloat(3.5F) + .setOptionalDouble(3.5D) + .setOptionalFixed32(10) + .setOptionalFixed64(10L) + .setOptionalInt32(10) + .setOptionalInt64(10L) + .setOptionalUint32(10) + .setOptionalUint64(10L) + .setSubMessage(TestSubMessage.newBuilder().setString("value").build()); + } +} diff --git a/protoc-gen-java-optional-test/src/test/resources/data-types.proto b/protoc-gen-java-optional-test/src/test/resources/data-types.proto new file mode 100644 index 0000000..2417217 --- /dev/null +++ b/protoc-gen-java-optional-test/src/test/resources/data-types.proto @@ -0,0 +1,73 @@ +syntax = "proto3"; + +package test.datatypes; + +option java_multiple_files = true; +option java_package = "org.test.datatypes"; + +enum TestEnum { + value0 = 0; + value1 = 1; + value2 = 2; + VALUE3 = 3; +} + +message TestSubMessage {string string = 1;} + +message TestMessage { + enum TestSubEnum { + value0 = 0; + value1 = 1; + value2 = 2; + } + + string string = 1; + int32 int32 = 2; + bool bool = 3; + TestSubMessage subMessage = 5; + double double = 6; + float float = 7; + bytes bytes = 8; + int64 int64 = 9; + TestEnum enum = 10; + fixed32 fixed32 = 11; + fixed64 fixed64 = 12; + uint32 uint32 = 13; + uint64 uint64 = 14; + optional bytes optional_bytes = 15; + optional string optional_string = 16; + optional int32 optional_int32 = 17; + optional bool optional_bool = 18; + optional double optional_double = 19; + optional float optional_float = 20; + optional int64 optional_int64 = 21; + optional TestEnum optional_enum = 22; + optional fixed32 optional_fixed32 = 23; + optional fixed64 optional_fixed64 = 24; + optional uint32 optional_uint32 = 25; + optional uint64 optional_uint64 = 26; + map map_string_string = 27; + map map_int64_sub = 28; + map map_bool_string = 29; + + oneof oneof { + string oneof_string = 30; + TestSubEnum oneof_enum = 31; + } +} + +message RepeatedTestMessage { + repeated string string = 1; + repeated int32 int32 = 2; + repeated bool bool = 3; + repeated TestSubMessage subMessage = 5; + repeated double double = 6; + repeated float float = 7; + repeated bytes bytes = 8; + repeated int64 int64 = 9; + repeated TestEnum enum = 10; + repeated fixed32 fixed32 = 11; + repeated fixed64 fixed64 = 12; + repeated uint32 uint32 = 13; + repeated uint64 uint64 = 14; +} diff --git a/protoc-gen-java-optional/dependency-reduced-pom.xml b/protoc-gen-java-optional/dependency-reduced-pom.xml new file mode 100644 index 0000000..cca7d96 --- /dev/null +++ b/protoc-gen-java-optional/dependency-reduced-pom.xml @@ -0,0 +1,120 @@ + + + + protoc + org.grpcmock + 1.0.0-SNAPSHOT + + 4.0.0 + protoc-gen-java-optional + Protoc Generator Java Optional + Protoc generator plugin for java adding support for null safe extra methods + + + + maven-shade-plugin + ${maven-shade-plugin.version} + + + package + + shade + + + + + + maven-jar-plugin + ${maven-jar-plugin.version} + + + + true + org.grpcmock.protoc.OptionalGenerator + + + + + + com.salesforce.servicelibs + canteen-maven-plugin + ${canteen.version} + + + + bootstrap + + + + + + + + + org.junit.jupiter + junit-jupiter-api + 5.7.0 + test + + + apiguardian-api + org.apiguardian + + + opentest4j + org.opentest4j + + + junit-platform-commons + org.junit.platform + + + + + org.junit.jupiter + junit-jupiter-params + 5.7.0 + test + + + apiguardian-api + org.apiguardian + + + + + org.junit.jupiter + junit-jupiter-engine + 5.7.0 + test + + + junit-platform-engine + org.junit.platform + + + apiguardian-api + org.apiguardian + + + + + org.assertj + assertj-core + 3.19.0 + test + + + org.mockito + mockito-inline + 3.7.0 + test + + + mockito-core + org.mockito + + + + + diff --git a/protoc-gen-java-optional/pom.xml b/protoc-gen-java-optional/pom.xml new file mode 100644 index 0000000..241b5c6 --- /dev/null +++ b/protoc-gen-java-optional/pom.xml @@ -0,0 +1,70 @@ + + + + org.grpcmock + protoc + 1.0.0-SNAPSHOT + + 4.0.0 + protoc-gen-java-optional + + Protoc Generator Java Optional + Protoc generator plugin for java adding support for null safe extra methods + + + + com.salesforce.servicelibs + jprotoc + ${jprotoc.version} + + + + + + + + org.apache.maven.plugins + maven-shade-plugin + ${maven-shade-plugin.version} + + + package + + shade + + + + + + + + org.apache.maven.plugins + maven-jar-plugin + ${maven-jar-plugin.version} + + + + true + org.grpcmock.protoc.OptionalGenerator + + + + + + + + com.salesforce.servicelibs + canteen-maven-plugin + ${canteen.version} + + + + bootstrap + + + + + + + diff --git a/protoc-gen-java-optional/src/main/java/org/grpcmock/protoc/plugin/OptionalGenerator.java b/protoc-gen-java-optional/src/main/java/org/grpcmock/protoc/plugin/OptionalGenerator.java new file mode 100644 index 0000000..811c5d2 --- /dev/null +++ b/protoc-gen-java-optional/src/main/java/org/grpcmock/protoc/plugin/OptionalGenerator.java @@ -0,0 +1,233 @@ +package org.grpcmock.protoc.plugin; + +import com.google.common.collect.ImmutableMap; +import com.google.protobuf.ByteString; +import com.google.protobuf.DescriptorProtos.DescriptorProto; +import com.google.protobuf.DescriptorProtos.FieldDescriptorProto; +import com.google.protobuf.DescriptorProtos.FieldDescriptorProto.Label; +import com.google.protobuf.DescriptorProtos.FieldDescriptorProto.Type; +import com.google.protobuf.DescriptorProtos.FileDescriptorProto; +import com.google.protobuf.Descriptors.FieldDescriptor; +import com.google.protobuf.Descriptors.FieldDescriptor.JavaType; +import com.google.protobuf.compiler.PluginProtos.CodeGeneratorRequest; +import com.google.protobuf.compiler.PluginProtos.CodeGeneratorResponse.Feature; +import com.google.protobuf.compiler.PluginProtos.CodeGeneratorResponse.File; +import com.salesforce.jprotoc.Generator; +import com.salesforce.jprotoc.GeneratorException; +import com.salesforce.jprotoc.ProtoTypeMap; +import com.salesforce.jprotoc.ProtocPlugin; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import java.util.OptionalDouble; +import java.util.OptionalInt; +import java.util.OptionalLong; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class OptionalGenerator extends Generator { + + private static final String DELIMITER = "\n"; + private static final String JAVA_EXTENSION = ".java"; + private static final String DIR_SEPARATOR = java.io.File.separator; + private static final String TEMPLATES_DIRECTORY = "templates" + DIR_SEPARATOR; + private static final String METHOD_NAME = "javaMethodName"; + private static final String FIELD_TYPE = "javaFieldType"; + private static final String OPTIONAL_CLASS = "optionalClass"; + private static final String PRIMITIVE_OPTIONAL = "primitiveOptional"; + private static final String OPTIONAL_GETTER_METHOD = "optionalGetterMethod"; + private static final String BUILDER_SCOPE = "builder_scope:"; + private static final String CLASS_SCOPE = "class_scope:"; + private static final String DEFAULT_OPTIONAL_CLASS = Optional.class.getName(); + private static final String DEFAULT_OPTIONAL_GETTER_METHOD = "get"; + private static final Map PRIMITIVE_CLASSES = ImmutableMap.builder() + .put(JavaType.INT, Integer.class.getSimpleName()) + .put(JavaType.LONG, Long.class.getSimpleName()) + .put(JavaType.FLOAT, Float.class.getSimpleName()) + .put(JavaType.DOUBLE, Double.class.getSimpleName()) + .put(JavaType.BOOLEAN, Boolean.class.getSimpleName()) + .put(JavaType.STRING, String.class.getSimpleName()) + .put(JavaType.BYTE_STRING, ByteString.class.getName()) + .build(); + private static final Map PRIMITIVE_OPTIONALS = ImmutableMap.builder() + .put(Integer.class.getSimpleName(), OptionalInt.class.getName()) + .put(Long.class.getSimpleName(), OptionalLong.class.getName()) + .put(Double.class.getSimpleName(), OptionalDouble.class.getName()) + .build(); + private static final Map PRIMITIVE_OPTIONAL_GETTER_METHODS = ImmutableMap.builder() + .put(Integer.class.getSimpleName(), "getAsInt") + .put(Long.class.getSimpleName(), "getAsLong") + .put(Double.class.getSimpleName(), "getAsDouble") + .build(); + + public static void main(String[] args) { + ProtocPlugin.generate(new OptionalGenerator()); + } + + private Parameters parameters; + private ProtoTypeMap protoTypeMap; + + @Override + public List generateFiles(CodeGeneratorRequest request) throws GeneratorException { + // create a map from proto types to java types + this.protoTypeMap = ProtoTypeMap.of(request.getProtoFileList()); + this.parameters = Parameters.from(request.getParameter()); + + return request.getProtoFileList().stream() + .filter(file -> request.getFileToGenerateList().contains(file.getName())) + .flatMap(this::handleProtoFile) + .collect(Collectors.toList()); + } + + @Override + protected List supportedFeatures() { + return Collections.singletonList(Feature.FEATURE_PROTO3_OPTIONAL); + } + + private Stream handleProtoFile(FileDescriptorProto fileDescriptor) { + String protoPackage = fileDescriptor.getPackage(); + String javaPackage = fileDescriptor.getOptions().hasJavaPackage() + ? fileDescriptor.getOptions().getJavaPackage() + : protoPackage; + + return fileDescriptor.getMessageTypeList().stream() + .flatMap(messageDescriptor -> handleMessage(messageDescriptor, protoPackage, javaPackage)); + } + + private Stream handleMessage(DescriptorProto messageDescriptor, String protoPackage, String javaPackage) { + String fileName = javaPackage.replace(".", DIR_SEPARATOR) + DIR_SEPARATOR + messageDescriptor.getName() + JAVA_EXTENSION; + String fullMethodName = protoPackage + "." + messageDescriptor.getName(); + + return Stream.of( + createFile(messageDescriptor, fileName, fullMethodName, BUILDER_SCOPE, this::createBuilderMethods), + createFile(messageDescriptor, fileName, fullMethodName, CLASS_SCOPE, this::createClassMethods)) + .filter(Optional::isPresent) + .map(Optional::get); + } + + private Optional createFile( + DescriptorProto messageDescriptor, + String fileName, + String fullMethodName, + String scopeType, + Function> createMethods + ) { + return messageDescriptor.getFieldList().stream() + .map(createMethods) + .filter(Optional::isPresent) + .map(Optional::get) + .collect(Collectors.collectingAndThen(Collectors.joining(DELIMITER), Optional::of)) + .filter(value -> !value.isEmpty()) + .map(methodsContent -> File.newBuilder() + .setName(fileName) + .setContent(methodsContent + DELIMITER) + .setInsertionPoint(scopeType + fullMethodName) + .build()); + } + + private Optional createBuilderMethods(FieldDescriptorProto fieldDescriptor) { + if (hasFieldPresence(fieldDescriptor)) { + return Stream.of(setOrClearMethod(fieldDescriptor), optionalSetOrClearMethod(fieldDescriptor)) + .filter(Optional::isPresent) + .map(Optional::get) + .collect(Collectors.collectingAndThen(Collectors.joining(DELIMITER), Optional::of)); + } + return Optional.empty(); + } + + private Optional createClassMethods(FieldDescriptorProto fieldDescriptor) { + if (hasFieldPresence(fieldDescriptor)) { + return optionalGet(fieldDescriptor); + } + return Optional.empty(); + } + + private Optional setOrClearMethod(FieldDescriptorProto fieldDescriptor) { + if (!parameters.isSetterObject()) { + return Optional.empty(); + } + Map context = ImmutableMap.builder() + .put(METHOD_NAME, getJavaMethodName(fieldDescriptor)) + .put(FIELD_TYPE, getJavaTypeName(fieldDescriptor)) + .build(); + return Optional.of(applyTemplate(templatePath("setOrClear.mustache"), context)); + } + + private Optional optionalSetOrClearMethod(FieldDescriptorProto fieldDescriptor) { + if (!parameters.isSetterOptional()) { + return Optional.empty(); + } + + String javaTypeName = getJavaTypeName(fieldDescriptor); + Map context = ImmutableMap.builder() + .put(METHOD_NAME, getJavaMethodName(fieldDescriptor)) + .put(FIELD_TYPE, javaTypeName) + .put(OPTIONAL_CLASS, getOptionalClassName(javaTypeName)) + .put(PRIMITIVE_OPTIONAL, isPrimitiveOptional(javaTypeName)) + .put(OPTIONAL_GETTER_METHOD, getOptionalGetterMethod(javaTypeName)) + .build(); + return Optional.of(applyTemplate(templatePath("optionalSetOrClear.mustache"), context)); + } + + private Optional optionalGet(FieldDescriptorProto fieldDescriptor) { + if (!parameters.isGetterOptional()) { + return Optional.empty(); + } + + String javaTypeName = getJavaTypeName(fieldDescriptor); + Map context = ImmutableMap.builder() + .put(METHOD_NAME, getJavaMethodName(fieldDescriptor)) + .put(FIELD_TYPE, javaTypeName) + .put(OPTIONAL_CLASS, getOptionalClassName(javaTypeName)) + .put(PRIMITIVE_OPTIONAL, isPrimitiveOptional(javaTypeName)) + .build(); + return Optional.of(applyTemplate(templatePath("optionalGet.mustache"), context)); + } + + private String getJavaMethodName(FieldDescriptorProto fieldDescriptor) { + return fieldDescriptor.getJsonName().substring(0, 1).toUpperCase(Locale.ROOT) + fieldDescriptor.getJsonName().substring(1); + } + + private String getJavaTypeName(FieldDescriptorProto fieldDescriptor) { + String protoTypeName = fieldDescriptor.getTypeName(); + if (protoTypeName.isEmpty()) { + return Optional.of(fieldDescriptor.getType()) + .map(FieldDescriptor.Type::valueOf) + .map(FieldDescriptor.Type::getJavaType) + .map(PRIMITIVE_CLASSES::get) + .orElseThrow(() -> new IllegalArgumentException("Failed to find java type for field:\n" + fieldDescriptor)); + } + return Optional.ofNullable(protoTypeMap.toJavaTypeName(protoTypeName)) + .orElseThrow(() -> new IllegalArgumentException("Failed to find java type for prototype '" + protoTypeName + "'")); + } + + private String getOptionalClassName(String javaTypeName) { + return parameters.isUsePrimitiveOptionals() + ? PRIMITIVE_OPTIONALS.getOrDefault(javaTypeName, DEFAULT_OPTIONAL_CLASS) + : DEFAULT_OPTIONAL_CLASS; + } + + private String getOptionalGetterMethod(String javaTypeName) { + return parameters.isUsePrimitiveOptionals() + ? PRIMITIVE_OPTIONAL_GETTER_METHODS.getOrDefault(javaTypeName, DEFAULT_OPTIONAL_GETTER_METHOD) + : DEFAULT_OPTIONAL_GETTER_METHOD; + } + + private boolean isPrimitiveOptional(String javaTypeName) { + return parameters.isUsePrimitiveOptionals() && PRIMITIVE_OPTIONALS.containsKey(javaTypeName); + } + + private static boolean hasFieldPresence(FieldDescriptorProto fieldDescriptor) { + return fieldDescriptor.getLabel() != Label.LABEL_REPEATED + && (fieldDescriptor.getProto3Optional() + || fieldDescriptor.getType() == Type.TYPE_MESSAGE + || fieldDescriptor.hasOneofIndex()); + } + + private static String templatePath(String path) { + return TEMPLATES_DIRECTORY + path; + } +} diff --git a/protoc-gen-java-optional/src/main/java/org/grpcmock/protoc/plugin/Parameters.java b/protoc-gen-java-optional/src/main/java/org/grpcmock/protoc/plugin/Parameters.java new file mode 100644 index 0000000..116a285 --- /dev/null +++ b/protoc-gen-java-optional/src/main/java/org/grpcmock/protoc/plugin/Parameters.java @@ -0,0 +1,81 @@ +package org.grpcmock.protoc.plugin; + +import java.util.Collections; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +final class Parameters { + + private static final String DELIMITER = ","; + private static final String ASSIGMENT = "="; + private static final String SETTER_OBJECT_KEY = "setter_object"; + private static final String SETTER_OPTIONAL_KEY = "setter_optional"; + private static final String GETTER_OPTIONAL_KEY = "getter_optional"; + private static final String USE_PRIMITIVE_OPTIONALS_KEY = "use_primitive_optionals"; + private static final boolean DEFAULT_SETTER_OBJECT = true; + private static final boolean DEFAULT_SETTER_OPTIONAL = false; + private static final boolean DEFAULT_GETTER_OPTIONAL = true; + private static final boolean DEFAULT_USE_PRIMITIVE_OPTIONALS = false; + + /** + * Flag indicating whether to add setter methods with the nullable object itself as an argument. Default {@code true}. + */ + private final boolean setterObject; + /** + * Flag indicating whether to add setter methods with {@link java.util.Optional} as an argument. Default {@code false}. + */ + private final boolean setterOptional; + /** + * Flag indicating whether to add getter methods returning {@link java.util.Optional}. Default {@code true}. + */ + private final boolean getterOptional; + /** + * Flag indicating whether to use primitive optionals ({@link java.util.OptionalInt} and similar) for {@code optional} protobuf + * primitive's setters and getters. Default {@code false}. + */ + private final boolean usePrimitiveOptionals; + + Parameters(boolean setterObject, boolean setterOptional, boolean getterOptional, boolean usePrimitiveOptionals) { + this.setterObject = setterObject; + this.setterOptional = setterOptional; + this.getterOptional = getterOptional; + this.usePrimitiveOptionals = usePrimitiveOptionals; + } + + public static Parameters from(String parametersRaw) { + Map values = Optional.ofNullable(parametersRaw) + .map(parameters -> Stream.of(parameters.split(DELIMITER)) + .map(parameterRaw -> parameterRaw.split(ASSIGMENT)) + .collect(Collectors.toMap(split -> split[0], Parameters::safeParseBoolean))) + .orElseGet(Collections::emptyMap); + + return new Parameters( + values.getOrDefault(SETTER_OBJECT_KEY, DEFAULT_SETTER_OBJECT), + values.getOrDefault(SETTER_OPTIONAL_KEY, DEFAULT_SETTER_OPTIONAL), + values.getOrDefault(GETTER_OPTIONAL_KEY, DEFAULT_GETTER_OPTIONAL), + values.getOrDefault(USE_PRIMITIVE_OPTIONALS_KEY, DEFAULT_USE_PRIMITIVE_OPTIONALS) + ); + } + + public boolean isSetterObject() { + return setterObject; + } + + public boolean isSetterOptional() { + return setterOptional; + } + + public boolean isGetterOptional() { + return getterOptional; + } + + public boolean isUsePrimitiveOptionals() { + return usePrimitiveOptionals; + } + + private static boolean safeParseBoolean(String[] input) { + return input.length == 2 && Boolean.parseBoolean(input[1]); + } +} diff --git a/protoc-gen-java-optional/src/main/resources/templates/optionalGet.mustache b/protoc-gen-java-optional/src/main/resources/templates/optionalGet.mustache new file mode 100644 index 0000000..4712e09 --- /dev/null +++ b/protoc-gen-java-optional/src/main/resources/templates/optionalGet.mustache @@ -0,0 +1,7 @@ +public {{optionalClass}}{{#primitiveOptional}}{{/primitiveOptional}}{{^primitiveOptional}}<{{javaFieldType}}>{{/primitiveOptional}} getOptional{{javaMethodName}}() { + if (has{{javaMethodName}}()) { + return {{optionalClass}}.of(get{{javaMethodName}}()); + } else { + return {{optionalClass}}.empty(); + } +} diff --git a/protoc-gen-java-optional/src/main/resources/templates/optionalSetOrClear.mustache b/protoc-gen-java-optional/src/main/resources/templates/optionalSetOrClear.mustache new file mode 100644 index 0000000..0b7b791 --- /dev/null +++ b/protoc-gen-java-optional/src/main/resources/templates/optionalSetOrClear.mustache @@ -0,0 +1,8 @@ +public Builder setOrClear{{javaMethodName}}({{optionalClass}}{{#primitiveOptional}}{{/primitiveOptional}}{{^primitiveOptional}}<{{javaFieldType}}>{{/primitiveOptional}} value) { + if (value.isPresent()) { + return set{{javaMethodName}}(value.{{optionalGetterMethod}}()); + } else { + clear{{javaMethodName}}(); + } + return this; +} diff --git a/protoc-gen-java-optional/src/main/resources/templates/setOrClear.mustache b/protoc-gen-java-optional/src/main/resources/templates/setOrClear.mustache new file mode 100644 index 0000000..65eaf2b --- /dev/null +++ b/protoc-gen-java-optional/src/main/resources/templates/setOrClear.mustache @@ -0,0 +1,8 @@ +public Builder setOrClear{{javaMethodName}}({{javaFieldType}} value) { + if (value != null) { + return set{{javaMethodName}}(value); + } else { + clear{{javaMethodName}}(); + } + return this; +} diff --git a/protoc-gen-java-optional/src/test/java/org/grpcmock/protoc/plugin/ParametersTest.java b/protoc-gen-java-optional/src/test/java/org/grpcmock/protoc/plugin/ParametersTest.java new file mode 100644 index 0000000..ca52e80 --- /dev/null +++ b/protoc-gen-java-optional/src/test/java/org/grpcmock/protoc/plugin/ParametersTest.java @@ -0,0 +1,34 @@ +package org.grpcmock.protoc.plugin; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.stream.Stream; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class ParametersTest { + + @ParameterizedTest + @MethodSource("parametersParseProvider") + void should_correctly_parse_parameters_string(String parametersInput, Parameters expected) { + Parameters parsed = Parameters.from(parametersInput); + + assertThat(parsed.isSetterObject()).isEqualTo(expected.isSetterObject()); + assertThat(parsed.isSetterOptional()).isEqualTo(expected.isSetterOptional()); + assertThat(parsed.isGetterOptional()).isEqualTo(expected.isGetterOptional()); + assertThat(parsed.isUsePrimitiveOptionals()).isEqualTo(expected.isUsePrimitiveOptionals()); + } + + static Stream parametersParseProvider() { + return Stream.of( + Arguments.of("", new Parameters(true, false, true, false)), + Arguments.of("setter_object=false", new Parameters(false, false, true, false)), + Arguments.of("setter_object=false,setter_optional=true", new Parameters(false, true, true, false)), + Arguments.of("setter_object=false,getter_optional=false", new Parameters(false, false, false, false)), + Arguments.of("setter_object=true,setter_optional=true,getter_optional=true,use_primitive_optionals=true", + new Parameters(true, true, true, true)), + Arguments.of("use_primitive_optionals=true", new Parameters(true, false, true, true)) + ); + } +}