diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index d2d9762e..548e5055 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -3,6 +3,9 @@ on: pull_request: branches: - develop + paths-ignore: + - '**README.md' + - '**README_INTERNAL.md' jobs: develop: @@ -17,5 +20,6 @@ jobs: java-version: '17' distribution: 'temurin' architecture: x64 + - name: run maven tests run: mvn clean verify \ No newline at end of file diff --git a/.github/workflows/maven-publish.yml b/.github/workflows/maven-publish.yml index d3fbcea8..1fafc176 100644 --- a/.github/workflows/maven-publish.yml +++ b/.github/workflows/maven-publish.yml @@ -3,16 +3,20 @@ name: Release on: push: branches: - - 'main' + - develop + - main paths-ignore: - '**pom.xml' - + - '**README.md' + - '**README_INTERNAL.md' jobs: - build: + develop_release: runs-on: ubuntu-latest + if: ${{ github.ref == 'refs/heads/develop' }} + environment: dev permissions: - contents: read + contents: write packages: write steps: @@ -20,13 +24,83 @@ jobs: name: Checkout uses: actions/checkout@v4 + - name: Verify SNAPSHOT version + run: | + VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout) + echo "Detected version: $VERSION" + if [[ "$VERSION" != *-SNAPSHOT ]]; then + echo "ERROR: Version is not a SNAPSHOT version! Aborting deploy." + exit 1 + fi + + - id: gpg-install + name: Install gpg secret key + run: | + # Install gpg secret key + cat <(echo -e "${{ secrets.MAVEN_GPG_PRIVATE_KEY }}") | gpg --batch --import + # Verify gpg secret key + gpg --list-secret-keys --keyid-format=long || echo "❌ No secret keys found" + + - id: ssh-setup + name: Set SSH key + uses: webfactory/ssh-agent@v0.9.0 + with: + ssh-private-key: ${{ secrets.TAG_SSH_KEY }} + + - id: git-config + name: Set GIT user name and email + uses: fregante/setup-git-user@v2 + + - id: jdk-setup + name: Setup JDK + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'temurin' + cache: 'maven' + settings-path: ${{ github.workspace }} + + - id: server-setup + name: Setup deployment server + uses: s4u/maven-settings-action@v3.0.0 + with: + servers: | + [{ + "id": "central", + "username": "${{ secrets.DISTRIBUTION_REPOSITORY_RELEASE_USERNAME }}", + "password": "${{ secrets.DISTRIBUTION_REPOSITORY_RELEASE_PASSWORD }}" + }] + properties: '[{"distribution.repository.snapshot.id": "central"}, {"distribution.repository.snapshot.url": "https://central.sonatype.com/repository/maven-snapshots/"}]' + + - name: Deploy SNAPSHOT to Maven Repo + run: mvn clean deploy -DskipTests=true -Prelease + + + production_release: + runs-on: ubuntu-latest + if: ${{ github.ref == 'refs/heads/main' }} + environment: dev + permissions: + contents: write + packages: write + + steps: + - id: checkout + name: Checkout + uses: actions/checkout@v4 + with: + ref: main + fetch-depth: 0 # Required for Maven Release Plugin + persist-credentials: false + - id: gpg-install name: Install gpg secret key run: | # Install gpg secret key cat <(echo -e "${{ secrets.MAVEN_GPG_PRIVATE_KEY }}") | gpg --batch --import # Verify gpg secret key - gpg --list-secret-keys --keyid-format LONG + gpg --list-secret-keys --keyid-format=long || echo "❌ No secret keys found" + echo "Installed gpg secret key" - id: ssh-setup name: Set SSH key @@ -53,16 +127,17 @@ jobs: with: servers: | [{ - "id": "ossrh", + "id": "central", "username": "${{ secrets.DISTRIBUTION_REPOSITORY_RELEASE_USERNAME }}", "password": "${{ secrets.DISTRIBUTION_REPOSITORY_RELEASE_PASSWORD }}" }] - properties: '[{"distribution.repository.release.id": "ossrh"}, {"distribution.repository.release.url": "https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/"}]' + properties: '[{"distribution.repository.release.id": "central"}, {"distribution.repository.release.url": "https://central.sonatype.com"}]' - id: release name: Release to production - run: mvn clean release:clean release:prepare release:perform -B -Darguments="-Ddevelop.api.key=${{ secrets.DEVELOP_SERVER_API_KEY }} -Dprod.eu1.api.key=${{ secrets.PROD_EU1_SERVER_API_KEY }} -Dprod.na1.api.key=${{ secrets.PROD_NA1_SERVER_API_KEY }}" -DpreparationGoals=install + run: mvn clean release:clean release:prepare release:perform -B -DskipTests=true -DpreparationGoals=install + # run: mvn clean release:clean release:prepare release:perform -B -Darguments="-Ddevelop.api.key=${{ secrets.DEVELOP_SERVER_API_KEY }} -Dprod.eu1.api.key=${{ secrets.PROD_EU1_SERVER_API_KEY }} -Dprod.na1.api.key=${{ secrets.PROD_NA1_SERVER_API_KEY }}" -DskipTests=true -X -DpreparationGoals=install - id: rollback name: Rollback diff --git a/.github/workflows/polyapi-update-pre-built-java-image.yml b/.github/workflows/polyapi-update-pre-built-java-image.yml index 21aad9cf..673910c9 100644 --- a/.github/workflows/polyapi-update-pre-built-java-image.yml +++ b/.github/workflows/polyapi-update-pre-built-java-image.yml @@ -21,7 +21,7 @@ jobs: - name: Hit endpoint and build Java server function pre-built image from scratch run: | curl -X POST \ - https://develop-k8s.polyapi.io/functions/server/prebuilt-base-image \ + https://dev.polyapi.io/functions/server/prebuilt-base-image \ -H "Authorization: Bearer ${{ secrets.POLY_SUPER_ADMIN_USER_KEY }}" \ -H "Content-Type: application/json" \ -d '{"language": "java"}' diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b59cdd1..64ee4ccc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,31 +1,38 @@ # Changelog ---- - -## [0.15.5] - 2024-11-20 +## +## [0.15.5] - 2025-06-04 ### Added -- +- `classFqn` helper to compute fully-qualified names +- import-filter to drop any illegal import strings +- `typeRef` helper to pick simple name or FQN and avoid duplicates +- `lastSegment` helper to filter out a context’s own class from imports ### Changed -- +- Updated `ResolvedContext.hbs` so that all generated-type references go through `typeRef` (simple names only when safe) +- Updated constructor loops in `ResolvedContext.hbs` to call proxy methods with `classFqn` (true FQNs) ### Fixed -- Command `mvn clean compile polyapi:deploy-functions` failed issue resolved ---- +- “already defined in this compilation unit” compile errors—SDK now builds without name collisions +## ## [0.15.4] - 2024-11-15 ### Added -- Only source code pass to the poly function with a separate `sourceSode` field +- Passing unaltered sourceCode to PolyAPI for display within Canopy application. + +- Fixed duplicate-field collisions by preprocessing JSON schemas to inject suffixed properties (e.g. order_id_1) into both properties and required before code generation, and enhanced NameHelper to preserve suffixes and special‐character mappings, eliminating “path not present” and “same field twice” errors. ### Changed -- +- PolyApiService.java: Replaced full-body buffering and Commons-IO parsing with a single BufferedInputStream using mark/reset for unified JSON/text/stream parsing and 1 KB error-preview logging. + +- Added lombok dependency and plugin to parent-pom ### Fixed diff --git a/README.md b/README.md index 2b931986..88cbc32a 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ # Java Client Library * Latest released version 0.15.5 -* Latest snapshot version 0.15.5-SNAPSHOT +* Latest snapshot version 0.15.6-SNAPSHOT ## Introduction -Welcome my friends! This is the PolyAPI Java client GitHub page. If you are here, then it means you're familiar with what we do at Poly. If you aren't, you can always check [here](https://github.com/polyapi/poly-alpha). +This is the PolyAPI Java client GitHub page. If you are here, then it means you're familiar with what we do at Poly. If you aren't, you can always check [here](https://github.com/polyapi/poly-alpha). In here you'll find instructions for both developers and customers to work with Poly through our Java clients and maven plugin. We hope that you find this documentation useful and easy to understand. If you don't please let us know. @@ -13,10 +13,10 @@ We hope that you find this documentation useful and easy to understand. If you d Here you'll find the minimum requirements of software that you'll need to work with this Java client. 2. [Project setup:](#project-setup) This section will get you the steps you need to do to setup this Poly client. -3. [Project description:](#project-description) -This is the boring section that describes the structure of this project and its components. -4. [Usage:](#usage) +3. [Usage:](#usage) In here you will find common usages of this client. +4. [Project description:](#project-description) +This section describes the structure of this project and its components. 5. [Changelog:](#changelog) This last (but not least) section shows the list of changes per version of this client. @@ -25,144 +25,258 @@ This last (but not least) section shows the list of changes per version of this This is the list of requirements for the usage of this client: - Java 17+ - Maven 3.6.3+ (or Gradle 7.2+) -- PolyAPI key +- PolyAPI API Key +- PolyAPI Host URL (ex. `https://na1.polyapi.io`) ## Setting up project -### I'm looking to dive into the source code! -Welcome fellow Poly dev! As you will be modifying the code here (and maybe even this documentation), you'll need to download and make sure that this project compiles for you. -So, the steps to follow are these: -1. **Setup an SSH key in your computer.** -To do so, follow [this GitHub tutorial](https://docs.github.com/en/authentication/connecting-to-github-with-ssh/adding-a-new-ssh-key-to-your-github-account). -2. **Clone this beautiful project.** To do so, run wherever you want to set the project.: -``` -git clone git@github.com:polyapi/polyapi-java.git -``` -3. **Install the project.** For this you'll run: +1. **Create a new Java Maven project.** + + There are many ways to achieve this. Most likely you already have a project where you want to run this. If you don't, you can follow [this tutorial](https://maven.apache.org/guides/getting-started/maven-in-five-minutes.html). Just have in mind to update the Java version to 17. + +2. **Add extension for project-specific settings.** + + **NOTE: If you're using Maven v4.0.0 or higher then you do not need to use an extension in order to have a project-specific settings and can skip this step.** + + Maven versions below v4.0.0 only support global and user-account specific `settings.xml` file, but this extension will allow you to have a `settings.xml` file just for this PolyAPI project which will give you a good place to keep your PolyAPI credentials securely out of your project's `pom.xml` file. + + Create or update your `PROJECT/.mvn/extensions.xml` file to enable setting up project-specific settings for maven: + ```xml + + + com.github.gzm55.maven + project-settings-extension + 0.3.5 + + + ``` + +3. **Create or update your project settings.xml file.** + + Update your `PROJECT/.mvn/settings.xml` file if you already have one, or create one and paste in the following (be sure to replace the `POLYAPI_HOST_URL` and `POLYAPI_API_KEY` with the actual values): + ```xml + + + + my-profile + + POLYAPI_HOST_URL + POLYAPI_API_KEY + + + + + my-profile + + + ``` + + **WARNING: If you're using a git repository for development, be sure to add `.mvn/settings.xml` file to your `.gitignore` file. Otherwise you could leak your PolyAPI credentials by commiting this file!** + +4. **Update the project pom.xml file.** + + Add the following to your project's `pom.xml` (be sure to replace `POLYAPI_VERSION` with the actual version you wish to use): + ```xml + + POLYAPI_VERSION + + + + io.polyapi + library + ${poly.version} + + + + + + target/generated-resources + + + + + io.polyapi + polyapi-maven-plugin + ${poly.version} + + ${poly.hostUrl} + 443 + ${poly.apiKey} + + + + generate-sources + + generate-sources + + + ${poly.hostUrl} + 443 + ${poly.apiKey} + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.13.0 + + true + + + + org.codehaus.mojo + build-helper-maven-plugin + 3.2.0 + + + add-source + generate-sources + + add-source + + + + target/generated-sources + + + + + + + + ``` + Make sure you replace `{API_KEY}` with valid API key to access PolyAPI. + If you work on Windows, remember to replace the '/' bar in the resources for '\'. + +5. **Compile the project.** + + To generate the Poly functions and compile the project (this needs to be done everytime you update your Poly functions) run this command: + ``` + mvn clean compile + ``` + +6. **And Poly is ready to use in your Java project!** + + +## Usage + +### Poly Functions +To use the Poly functions you can import `import io.polyapi.Poly;` and traverse through it to find the function you want to use. For example: +```java +var result = Poly.yourApi.context.reallyCoolPolyFunction("https://really.cool.polyfunction.net", "param"); + +System.out.println(result); +``` + +### Webhook handlers +```java +Poly.myWebhooks.onCoolEvent((event, headers, params) -> { + System.out.println(event.getPrice()); +}); ``` -mvn clean install + +### Error handlers +```java +Poly.onError("poly.context", errorEvent -> { + System.out.println(errorEvent.getMessage()); +}); ``` -From the folder of the project. -**There! You got the project running my dear, go make things better!** +### Auth functions +```java +var clientId = "..."; +var clientSecret = "..."; +var scopes = new String[]{"offline_access"}; + +Poly.auth0.getToken(clientId, clientSecret, scopes, (token, url, error) -> { + System.out.println(token); + System.out.println(url); + System.out.println(error); + + if (token != null) { + ... + // revoke token (optional, if you want to revoke the token after you are done with it) + Poly.auth0.revokeToken(token); + } +}); +``` -If you want to run the project functionalities, follow the steps for customer. Just bear in mind to update the version to your local ones below. +### Poly Variables +To use Poly variables you can import `import io.polyapi.Vari;` and traverse through it to find the variable you want to use. For example: +```java +var clientId = Vari.auth.clientId.get(); +System.out.println(clientId); +``` +You can update variable using the following code: +```java +Vari.auth.clientId.update("newClientId"); +``` +You can listen for update events: +```java +Vari.auth.clientId.onUpdate((event) -> { + System.out.println("Previous value: " + event.getPreviousValue()+", currentValue: " + event.getCurrentValue()); +}); +``` -### I'm looking to use Poly to simplify my life! -Nice to have some customers looking around here! So, you'll need to run the following steps: -1. **Create a new Java Maven project.** There are many ways to achieve this. Most likely you already have a project where you want to run this. If you don't, you can follow [this tutorial](https://maven.apache.org/guides/getting-started/maven-in-five-minutes.html). Just have in mind to update the Java version to 17. -2. **Update the project.** Add the following to your project's `pom.xml`: +### Poly server functions +It is possible to deploy server functions that can be used in Poly. To do so, you need to create a class with desired function. For example: +```java +public class CustomFunction { + public String sayHello(String name) { + return "Hello " + name; + } +} +``` +Then, it is required that you have a setup project. In this project, you need to have the PolyAPI commons library installed as a dependency. ```xml - - 0.15.5 - - - + io.polyapi - library + commons ${poly.version} - - - - - - target/generated-resources - - - - - io.polyapi - polyapi-maven-plugin - ${poly.version} - - https://na1.polyapi.io - 443 - {API_KEY} - - - - generate-sources - - generate-sources - - - https://na1.polyapi.io - 443 - {API_KEY} - - - - - - org.apache.maven.plugins - maven-compiler-plugin - 3.13.0 - - true - - - - org.codehaus.mojo - build-helper-maven-plugin - 3.2.0 - - - add-source - generate-sources - - add-source - - - - target/generated-sources - - - - - - - + ``` -Make sure you replace `{API_KEY}` with valid API key to access PolyAPI. -If you work on Windows, remember to replace the '/' bar in the resources for '\'. - -3. **Compile the project.** To generate the Poly functions and compile the project (this needs to be done everytime you update your Poly functions) run this beautiful command: +Then, annotate the function you want to upload with `@PolyServerFunction`. +And finally, just run: +```bash +mvn polyapi:deploy-functions ``` -mvn clean compile +### Poly client functions +To create a Poly client Function you need to follow the same steps as with a server function, but instead using the `@PolyClientFunction` annotation. +### Vari server variables +To create a Poly server variable you need to just run the following command: +``` bash +mvn polyapi:create-server-variable -Dname=myVariable -Dvalue=myValue -Dcontext=myContext ``` -**And Poly is ready to use in your Java project!** +## Limitations +Comparing to its Typescript counterpart, the Java library is still missing the following features: +- Error handlers +- Fetching multiple Poly Variables from context + +These features will be added in the future releases. ## Project description -This is the boring section that describes this project. If you are not into the technical details, you'll are likely to find this boring and not very useful. - -You have been warned. - -This project has the following components: -1. **Multimodule project** -This is the core multimodule project that will wrap every project here and compile them together. -2. **Parent pom** -This provides the common maven configuration for to the rest of the Java projects. -3. **Commons library** -This library is one containing all objects that are common to all Java libraries used, such as the HttpClient or common model classes. -4. **The Library** -This library is the one that is injected into the customer's project and contains the basic functionality used by all the generated code. -5. **Maven plugin** -This plugin contains the goals to add functions to the Poly server as well as downloading and generating the Poly code to run the Poly functions. - -### Multimodule project -This project works both as a multimodule project and as a parent pom. The purpose of this project is to unify the development of all common Java client components in one. - -### Parent pom + +This a multimodule project containing several packages to make development easier: + +- parent-pom +- commons +- library +- polyapi-maven-plugin + +### Parent pom (/parent-pom) This project, a single POM file, works as a parent pom and provides the basic Maven configuration for all projects that inherit it. At this time it provides the common test libraries, as well as the logging ones. It also provids the versioning for all children projects. -### Commons library +### Commons library (/commons) This library is used by both the Maven plugin as well as the client library as it serves common functionality to both. -It provides 3 main functionalities: + +It provides several functionalities: #### The HTTP client The HTTP client provides a generic interface to execute HTTP requests. It is used by the internal services. It uses [OkHttp](https://square.github.io/okhttp/) as a client by default, but by implementing the interface and using a different client in the back (such as [Jersey](https://eclipse-ee4j.github.io/jersey/)) requires to just implement the interface and use instead. @@ -178,7 +292,7 @@ This basic functionality is used to write files into the FileSystem. There isn't This parser uses [Jackson](https://github.com/FasterXML/jackson) as default client, but provides an interface [JsonParser](https://github.com/polyapi/polyapi-java/blob/develop/commons/src/main/java/io/polyapi/commons/api/json/JsonParser.java). To use different clients, you just need to replace the implementation but implement the interface. -### The Library +### The Library (/library) This library is the PolyAPI client itself. It provides the common functionality for the usage of Poly functions in Java. It relies in the existence of generated code using the Maven plugin to provide the interface. Normally you wouldn't have to access it's contents, as they work through the generated classes. @@ -186,7 +300,7 @@ It relies in the existence of generated code using the Maven plugin to provide t #### Proxy factory One of the key classes in this library is the proxy factory. As most of the generated code is interfaces, the implementation by default is proxied to channel all of them through the API calls. This hides the implementation from the developers so this works in a black box kind of way. -### Maven plugin +### Maven plugin (/polyapi-maven-plugin) This maven plugin provides the following MOJOs: #### generate-sources @@ -266,104 +380,6 @@ Here's the list of parameters: - **secret:** Whether or not the variable contents will be revealed. - **type:** The type of the variable being set. This field is case insensitive. Valid inputs are `string`, `java.lang.String`, `integer`, `int`, `java.lang.Integer`, `double`, `java.lang.Double`, `long`, `java.lang.Long`, `float`, `java.lang.Float`, `byte`, `java.lang.Byte`, `short`, `java.lang.Short`, `boolean`, `java.lang.Boolean`. The content of the `value` field will be cast to this type before upload. If not set, the type will be auto-detected from the `value` content. - -## Usage - -### Poly Functions -To use the Poly functions you can import `import io.polyapi.Poly;` and traverse through it to find the function you want to use. For example: -```java -var result = Poly.yourApi.context.reallyCoolPolyFunction("https://really.cool.polyfunction.net", "param"); - -System.out.println(result); -``` - -### Webhook handlers -```java -Poly.myWebhooks.onCoolEvent((event, headers, params) -> { - System.out.println(event.getPrice()); -}); -``` - -### Error handlers -```java -Poly.onError("poly.context", errorEvent -> { - System.out.println(errorEvent.getMessage()); -}); -``` - -### Auth functions -```java -var clientId = "..."; -var clientSecret = "..."; -var scopes = new String[]{"offline_access"}; - -Poly.auth0.getToken(clientId, clientSecret, scopes, (token, url, error) -> { - System.out.println(token); - System.out.println(url); - System.out.println(error); - - if (token != null) { - ... - // revoke token (optional, if you want to revoke the token after you are done with it) - Poly.auth0.revokeToken(token); - } -}); -``` - -### Poly Variables -To use Poly variables you can import `import io.polyapi.Vari;` and traverse through it to find the variable you want to use. For example: -```java -var clientId = Vari.auth.clientId.get(); -System.out.println(clientId); -``` -You can update variable using the following code: -```java -Vari.auth.clientId.update("newClientId"); -``` -You can listen for update events: -```java -Vari.auth.clientId.onUpdate((event) -> { - System.out.println("Previous value: " + event.getPreviousValue()+", currentValue: " + event.getCurrentValue()); -}); -``` - -### Poly server functions -It is possible to deploy server functions that can be used in Poly. To do so, you need to create a class with desired function. For example: -```java -public class CustomFunction { - public String sayHello(String name) { - return "Hello " + name; - } -} -``` -Then, it is required that you have a setup project. In this project, you need to have the PolyAPI commons library installed as a dependency. -```xml - - io.polyapi - commons - ${poly.version} - -``` -Then, annotate the function you want to upload with `@PolyServerFunction`. -And finally, just run: -```bash -mvn polyapi:deploy-functions -``` -### Poly client functions -To create a Poly client Function you need to follow the same steps as with a server function, but instead using the `@PolyClientFunction` annotation. -### Vari server variables -To create a Poly server variable you need to just run the following command: -``` bash -mvn polyapi:create-server-variable -Dname=myVariable -Dvalue=myValue -Dcontext=myContext -``` - -## Limitations -Comparing to its Typescript counterpart, the Java library is still missing the following features: -- Error handlers -- Fetching multiple Poly Variables from context - -These features will be added in the future releases. - ## Changelog See the Changelog **[here](https://github.com/polyapi/polyapi-java/releases)**. \ No newline at end of file diff --git a/README_INTERNAL.md b/README_INTERNAL.md new file mode 100644 index 00000000..37991d21 --- /dev/null +++ b/README_INTERNAL.md @@ -0,0 +1,88 @@ +# Java Client Library (INTERNAL) + +This is the README for developers of PolyAPI or contributors to this project. + +## Testing SNAPSHOT versions + +You can test the development releases of the Java Client by either: + +A. pulling this repo and building/installing locally to get the version in your local maven repository + +OR + +B. you can update your testing project's pom.xml to allow you to install SNAPSHOT versions that have already been published to the Maven Central Repository. + +### A. Using a local build + +Once you've cloned the repository locally: run `mvn clean install` from the project directory to install all dependencies, build the project and put it into your local repository. + +At this point you can use it within local projects in order to test it by setting the built version within the `` property in your `pom.xml` + +### B. Using published SNAPSHOT versions + +Within your local Java project where you want to use the PolyAPI java client: update your project's `pom.xml` to enable pulling a released SNAPSHOT version from the Maven Central Repository: +```xml + + + Central Portal Snapshots + central-portal-snapshots + https://central.sonatype.com/repository/maven-snapshots/ + + false + + + true + + + + + + central-portal-snapshots + https://central.sonatype.com/repository/maven-snapshots/ + + false + + + true + + + +``` + +You should now be able to use SNAPSHOT versions within the `` property in your `pom.xml` + +## Versioning + +Maven uses SNAPSHOT versions to indicate development versions. So a version like `0.13.5-SNAPSHOT` is considered a development version. SNAPSHOT versions can be modified and redeployed under the same version number multiple times in order to update it without needing to increment a version number. + + +## How to deploy development versions + +We've setup a github workflow which should automatically publish the SNAPSHOT version of the client to the Maven Central Repository whenever code changes are pushed to the `develop` branch. + +1. Branch off of `develop` and make your changes as normal. Test locally until you are satisfied with the changeset. + +2. Make sure there's changelog entries for your snapshot version. Remember that as we develop we don't need to increment version numbers, so if there's already a changelong entry for the current snapshot version then modify it, else make a new entry. + +3. Push your changes and open a PR against `develop`. + +4. After approval of your changes: merge into `develop` to trigger the Release workflow in Github which should build and deploy the SNAPSHOT version release. Make sure it succeeds! + + +## How to release to production + +1. Checkout `develop` and bump the minor version within the README.md: + ``` + - * Latest released version 0.15.3 + - * Latest snapshot version 0.15.4-SNAPSHOT + + * Latest released version 0.15.4 + + * Latest snapshot version 0.15.5-SNAPSHOT + ```` + +2. Commit and push. + +3. Open a Release PR from `develop` into `main`. + +4. After approval of the release: merge into `main` to trigger the Release workflow in Github which should build and deploy the production version release. Make sure it succeeds! You should see two commits from the `[maven-release-plugin]`, the first which prepares the release by removing the "-SNAPSHOT" part of the client version and then releasing that version, and then the second which prepares the next "-SNAPSHOT" version and updates all the pom.xml files accordingly. + +5. Merge `main` back into `develop`. \ No newline at end of file diff --git a/commons/pom.xml b/commons/pom.xml index a769195b..141d4256 100644 --- a/commons/pom.xml +++ b/commons/pom.xml @@ -4,7 +4,7 @@ io.polyapi parent-pom - 0.15.5-SNAPSHOT + 0.15.6-SNAPSHOT ../parent-pom @@ -73,11 +73,6 @@ commons-text 1.10.0 - - org.projectlombok - lombok - 1.18.30 - com.kjetland mbknor-jackson-jsonschema_2.12 @@ -159,16 +154,21 @@ + + + --pinentry-mode + loopback + + - org.sonatype.plugins - nexus-staging-maven-plugin - 1.6.13 + org.sonatype.central + central-publishing-maven-plugin + 0.7.0 true - ossrh - https://s01.oss.sonatype.org/ - true + central + true diff --git a/commons/src/main/java/io/polyapi/commons/api/model/LifecycleState.java b/commons/src/main/java/io/polyapi/commons/api/model/LifecycleState.java new file mode 100644 index 00000000..9f707c80 --- /dev/null +++ b/commons/src/main/java/io/polyapi/commons/api/model/LifecycleState.java @@ -0,0 +1,5 @@ +package io.polyapi.commons.api.model; + +public enum LifecycleState { + ALPHA, BETA, STABLE, DEPRECATED, DISABLED +} \ No newline at end of file diff --git a/commons/src/main/java/io/polyapi/commons/api/service/PolyApiService.java b/commons/src/main/java/io/polyapi/commons/api/service/PolyApiService.java index 228730fe..7d09b969 100644 --- a/commons/src/main/java/io/polyapi/commons/api/service/PolyApiService.java +++ b/commons/src/main/java/io/polyapi/commons/api/service/PolyApiService.java @@ -8,8 +8,7 @@ import io.polyapi.commons.api.http.Response; import io.polyapi.commons.api.json.JsonParser; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.io.IOUtils; - +import java.io.BufferedInputStream; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Type; @@ -24,7 +23,7 @@ import static java.util.function.Predicate.not; /** - * Parent implementation class for all services that connec to the PolyAPI service. + * Parent implementation class for all services that connect to the PolyAPI service. */ @Slf4j public class PolyApiService { @@ -44,7 +43,10 @@ public O get(String relativePath, Type expectedResponseType) { return get(relativePath, new HashMap<>(), new HashMap<>(), expectedResponseType); } - public O get(String relativePath, Map> headers, Map> queryParams, Type expectedResponseType) { + public O get(String relativePath, + Map> headers, + Map> queryParams, + Type expectedResponseType) { return parsedCall(GET, relativePath, headers, queryParams, null, expectedResponseType); } @@ -52,7 +54,11 @@ public O post(String relativePath, I body, Type expectedResponseType) { return post(relativePath, new HashMap<>(), new HashMap<>(), body, expectedResponseType); } - public O post(String relativePath, Map> headers, Map> queryParams, I body, Type expectedResponseType) { + public O post(String relativePath, + Map> headers, + Map> queryParams, + I body, + Type expectedResponseType) { return parsedCall(POST, relativePath, headers, queryParams, body, expectedResponseType); } @@ -60,7 +66,10 @@ public void patch(String relativePath, I body) { parsedCall(PATCH, relativePath, new HashMap<>(), new HashMap<>(), body, Void.TYPE); } - public void patch(String relativePath, Map> headers, Map> queryParams, I body) { + public void patch(String relativePath, + Map> headers, + Map> queryParams, + I body) { parsedCall(PATCH, relativePath, headers, queryParams, body, Void.TYPE); } @@ -68,62 +77,124 @@ public void delete(String relativePath) { delete(relativePath, new HashMap<>(), new HashMap<>(), null); } - - public void delete(String relativePath, Map> headers, Map> queryParams, I body) { + public void delete(String relativePath, + Map> headers, + Map> queryParams, + I body) { parsedCall(DELETE, relativePath, headers, queryParams, body, Void.TYPE); } - private O parsedCall(HttpMethod method, String relativePath, Map> headers, Map> queryParams, I body, Type expectedResponseType) { + private O parsedCall(HttpMethod method, + String relativePath, + Map> headers, + Map> queryParams, + I body, + Type expectedResponseType) { + Map> allHeaders = new HashMap<>(); - //allHeaders.put("Accept", List.of("application/json")); allHeaders.put("Content-type", List.of("application/json")); headers.forEach((key, value) -> allHeaders.put(key, value.stream().toList())); - Response response = callApi(method, relativePath, allHeaders, queryParams, jsonParser.toJsonInputStream(body)); + Response response = callApi( + method, + relativePath, + allHeaders, + queryParams, + jsonParser.toJsonInputStream(body) + ); + log.debug("Response is successful. Status code is {}.", response.statusCode()); log.debug("Parsing response."); - O result = Optional.of(expectedResponseType) - .filter(not(Void.TYPE::equals)) - .map(type ->{ - String contentType = response.headers().get("Content-type").stream().findFirst().orElseThrow(); - O parsedResult; - log.debug("Content type is {}.", contentType); - log.debug("Type class is {}.", type.getClass()); - log.debug("Type is {}.", type); - if (contentType.startsWith("application/json")) { - parsedResult = jsonParser.parseInputStream(response.body(), TypeVariable.class.isAssignableFrom(type.getClass()) ? Object.class : type); - } else { + + final int PREVIEW = 1024; + byte[] previewBuf = new byte[PREVIEW]; + int previewLen = 0; + BufferedInputStream bodyStream = new BufferedInputStream(response.body()); + + // mark & read first PREVIEW bytes for potential error logging + bodyStream.mark(PREVIEW); + try { + previewLen = bodyStream.read(previewBuf); + } catch (IOException ignored) { + } + try { + bodyStream.reset(); + } catch (IOException ignored) { + } + + try { + O parsed = Optional.of(expectedResponseType) + .filter(not(Void.TYPE::equals)) + .map(type -> { + String contentType = response.headers() + .get("Content-type") + .stream() + .findFirst() + .orElse("application/json"); + + if (contentType.startsWith("application/json")) { + return jsonParser.parseInputStream( + bodyStream, + TypeVariable.class.isAssignableFrom(type.getClass()) + ? Object.class + : type + ); + } + if (checkType(type, String.class) && contentType.startsWith("text/")) { try { - parsedResult = (O)IOUtils.toString(response.body(), defaultCharset()); - } catch (IOException e) { - throw new ParsingException("An error occurred while parsing the response.", e); - } - } else { - if (checkType(type, InputStream.class)) { - parsedResult = (O)response.body(); - } else { - throw new UnsupportedContentTypeException(contentType, type); + @SuppressWarnings("unchecked") + O result = (O) new String( + bodyStream.readAllBytes(), + defaultCharset() + ); + return result; + } catch (IOException ioe) { + throw new ParsingException("Could not read text response", ioe); } } - } - return parsedResult; - }) - .orElse(null); - log.debug("Response parsed successfully."); - return result; + + if (checkType(type, InputStream.class)) { + @SuppressWarnings("unchecked") + O result = (O) bodyStream; + return result; + } + + throw new UnsupportedContentTypeException(contentType, type); + }) + .orElse(null); + + log.debug("Response parsed successfully."); + return parsed; + + } catch (RuntimeException ex) { + String snippet = previewLen > 0 + ? new String(previewBuf, 0, previewLen, defaultCharset()) + : ""; + log.error("Failed to parse response from {} {} (first {} bytes):\n{}", + method, relativePath, previewLen, snippet); + throw ex; + } } private boolean checkType(Type type, Class expectedClass) { - return (type.getClass().isAssignableFrom(Class.class) && Class.class.cast(type).isAssignableFrom(expectedClass)) || TypeVariable.class.isAssignableFrom(type.getClass()); + return (type.getClass().isAssignableFrom(Class.class) + && ((Class) type).isAssignableFrom(expectedClass)) + || TypeVariable.class.isAssignableFrom(type.getClass()); } - private Response callApi(HttpMethod method, String relativePath, Map> headers, Map> queryParams, InputStream body) { + private Response callApi(HttpMethod method, + String relativePath, + Map> headers, + Map> queryParams, + InputStream body) { + Request request = client.prepareAuthenticatedRequest(host, port, method, relativePath) .withHeaders(headers) .withQueryParams(queryParams) .withBody(body) .build(); + log.debug("Executing authenticated {} request with target {}", method, request.getUrl()); return client.send(request); } diff --git a/library/pom.xml b/library/pom.xml index 983cbb06..17a60586 100644 --- a/library/pom.xml +++ b/library/pom.xml @@ -4,7 +4,7 @@ io.polyapi parent-pom - 0.15.5-SNAPSHOT + 0.15.6-SNAPSHOT ../parent-pom library @@ -57,12 +57,6 @@ 3.9.0 provided - - org.projectlombok - lombok - 1.18.30 - provided - com.google.code.gson gson @@ -110,16 +104,21 @@ + + + --pinentry-mode + loopback + + - org.sonatype.plugins - nexus-staging-maven-plugin - 1.6.13 + org.sonatype.central + central-publishing-maven-plugin + 0.7.0 true - ossrh - https://s01.oss.sonatype.org/ - true + central + true diff --git a/parent-pom/pom.xml b/parent-pom/pom.xml index 2de2484b..b7e6bc64 100644 --- a/parent-pom/pom.xml +++ b/parent-pom/pom.xml @@ -3,7 +3,7 @@ 4.0.0 io.polyapi parent-pom - 0.15.5-SNAPSHOT + 0.15.6-SNAPSHOT pom PolyAPI Java parent POM https://polyapi.io @@ -28,8 +28,8 @@ scm:git:git@github.com:polyapi/polyapi-java.git scm:git:git@github.com:polyapi/polyapi-java.git https://github.com/polyapi/polyapi-java - HEAD - + HEAD + 17 17 @@ -37,6 +37,7 @@ 5.10.0 2.0.9 2.0.9 + 1.18.38 @@ -97,6 +98,12 @@ ${slf4j.version} test + + org.projectlombok + lombok + ${lombok.version} + provided + @@ -121,16 +128,21 @@ + + + --pinentry-mode + loopback + + - org.sonatype.plugins - nexus-staging-maven-plugin - 1.6.13 + org.sonatype.central + central-publishing-maven-plugin + 0.7.0 true - ossrh - https://s01.oss.sonatype.org/ - true + central + true @@ -161,6 +173,20 @@ + + org.apache.maven.plugins + maven-compiler-plugin + 3.14.0 + + + + org.projectlombok + lombok + ${lombok.version} + + + + @@ -175,4 +201,4 @@ - + \ No newline at end of file diff --git a/polyapi-maven-plugin/pom.xml b/polyapi-maven-plugin/pom.xml index 6f35072a..449bc724 100644 --- a/polyapi-maven-plugin/pom.xml +++ b/polyapi-maven-plugin/pom.xml @@ -4,7 +4,7 @@ io.polyapi parent-pom - 0.15.5-SNAPSHOT + 0.15.6-SNAPSHOT ../parent-pom polyapi-maven-plugin @@ -68,12 +68,6 @@ jsonschema2pojo-core 1.2.1 - - org.projectlombok - lombok - 1.18.30 - provided - io.socket socket.io-client @@ -100,6 +94,11 @@ json 20231013 + + com.github.erosb + everit-json-schema + 1.14.6 + @@ -127,9 +126,6 @@ false - - MAVEN_GPG_KEY - @@ -145,16 +141,21 @@ + + + --pinentry-mode + loopback + + - org.sonatype.plugins - nexus-staging-maven-plugin - 1.6.13 + org.sonatype.central + central-publishing-maven-plugin + 0.7.0 true - ossrh - https://s01.oss.sonatype.org/ - true + central + true diff --git a/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/model/function/PolyFunction.java b/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/model/function/PolyFunction.java index 95950b07..ff5b33be 100644 --- a/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/model/function/PolyFunction.java +++ b/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/model/function/PolyFunction.java @@ -1,8 +1,8 @@ package io.polyapi.plugin.model.function; - -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import io.polyapi.commons.api.model.LifecycleState; import io.polyapi.commons.api.model.Visibility; +import io.polyapi.plugin.model.specification.SchemaRef; import lombok.Getter; import lombok.Setter; import lombok.ToString; @@ -20,21 +20,26 @@ @ToString public class PolyFunction { private String id; + private String context; private String name; + private String contextName; private String description; - private String context; + private String sourceCode; // Only used when deploying a function private String code; - private String sourceCode; private String language = "java"; private String returnType; private Visibility visibility; + private LifecycleState state; private Boolean logsEnabled; - @JsonDeserialize(using = RequirementsDeserializer.class) private List requirements; + private String ownerUserId; private Map returnTypeSchema; private List arguments; + private List otherReturnTypes; + private List unresolvedReturnTypePolySchemaRefs; + /** * Gets the signature of this function in the form of functionName(ArgClass, ArgClass2...). * diff --git a/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/model/function/PolyFunctionArgument.java b/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/model/function/PolyFunctionArgument.java index bf1f2953..f328b8d4 100644 --- a/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/model/function/PolyFunctionArgument.java +++ b/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/model/function/PolyFunctionArgument.java @@ -1,5 +1,7 @@ package io.polyapi.plugin.model.function; +import io.polyapi.plugin.model.specification.SchemaRef; +import java.util.List; import lombok.Getter; import lombok.Setter; @@ -8,8 +10,10 @@ public class PolyFunctionArgument { private String key; private String name; + private String description; private String type; private String typeSchema; private Boolean required; private Boolean secure; + private List unresolvedPolySchemaRefs; } diff --git a/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/model/function/RequirementsDeserializer.java b/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/model/function/RequirementsDeserializer.java deleted file mode 100644 index 0a2ca47e..00000000 --- a/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/model/function/RequirementsDeserializer.java +++ /dev/null @@ -1,39 +0,0 @@ -package io.polyapi.plugin.model.function; - -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JsonDeserializer; -import com.fasterxml.jackson.databind.JsonNode; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -public class RequirementsDeserializer extends JsonDeserializer> { - - @Override - public List deserialize(JsonParser jsonParser, DeserializationContext context) throws IOException { - JsonNode node = jsonParser.getCodec().readTree(jsonParser); - - if (node.isArray() && node.isEmpty()) { - return Collections.emptyList(); - } - - if (node.isArray()) { - List requirements = new ArrayList<>(); - for (JsonNode item : node) { - if (item.isTextual()) { - requirements.add(item.asText()); - } - } - return requirements; - } - - if (node.isTextual() && "[]".equals(node.asText())) { - return Collections.emptyList(); - } - - return Collections.emptyList(); - } -} diff --git a/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/model/function/ReturnType.java b/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/model/function/ReturnType.java new file mode 100644 index 00000000..fc58e6dd --- /dev/null +++ b/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/model/function/ReturnType.java @@ -0,0 +1,14 @@ +package io.polyapi.plugin.model.function; + +import java.util.Map; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class ReturnType { + private Integer statusCode; + private String type; + private Map schema; +} diff --git a/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/model/specification/SchemaRef.java b/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/model/specification/SchemaRef.java new file mode 100644 index 00000000..9a7cb313 --- /dev/null +++ b/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/model/specification/SchemaRef.java @@ -0,0 +1,11 @@ +package io.polyapi.plugin.model.specification; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class SchemaRef { + private String publicNamespace = ""; + private String path; +} diff --git a/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/service/DeploymentServiceImpl.java b/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/service/DeploymentServiceImpl.java index 67d6b17b..878b5c72 100644 --- a/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/service/DeploymentServiceImpl.java +++ b/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/service/DeploymentServiceImpl.java @@ -87,6 +87,7 @@ public List deployFunctions(List functionFilters, boolean try (FileInputStream fileInputStream = new FileInputStream(sourceCodePath)) { String code = IOUtils.toString(fileInputStream, defaultCharset()); codeObject.setCode(code); + polyFunction.setSourceCode(code); if (annotation.contextAwareness().equals(PolyServerFunction.AUTO_DETECT_CONTEXT)) { Set matches = Pattern.compile("(Vari|Poly)\\.[.a-zA-Z0-9_\\s]+[^.a-zA-Z0-9_\\s]") .matcher(code) diff --git a/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/service/generation/PolyObjectResolverService.java b/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/service/generation/PolyObjectResolverService.java index fc5190ad..7fb74f16 100644 --- a/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/service/generation/PolyObjectResolverService.java +++ b/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/service/generation/PolyObjectResolverService.java @@ -5,6 +5,7 @@ import java.util.ArrayList; import java.util.HashSet; import java.util.List; +import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.function.Function; @@ -43,6 +44,8 @@ @Slf4j public class PolyObjectResolverService { private final JsonSchemaParser jsonSchemaParser; + private static final Pattern VALID_IMPORT = + Pattern.compile("^[a-zA-Z_][a-zA-Z0-9_]*(\\.[a-zA-Z_][a-zA-Z0-9_]*)+$"); public PolyObjectResolverService(JsonSchemaParser jsonSchemaParser) { this.jsonSchemaParser = jsonSchemaParser; @@ -92,12 +95,21 @@ public ResolvedServerVariableSpecification resolve(ServerVariableSpecification s public ResolvedContext resolve(Context context) { Set imports = new HashSet<>(); - context.getSubcontexts().stream().map(subcontext -> format("%s.%s", subcontext.getPackageName(), subcontext.getClassName())).forEach(imports::add); + context.getSubcontexts().stream() + .map(subcontext -> format("%s.%s", subcontext.getPackageName(), subcontext.getClassName())) + .filter(s -> !s.isBlank()) + .forEach(imports::add); context.getSpecifications().forEach(specification -> { ImportsCollectorVisitor importsCollectorVisitor = new ImportsCollectorVisitor(specification.getPackageName(), specification.getClassName(), jsonSchemaParser); importsCollectorVisitor.doVisit(specification); - imports.addAll(importsCollectorVisitor.getImports()); + importsCollectorVisitor.getImports().stream() + .filter(Objects::nonNull) + .filter(s -> !s.isBlank()) + .filter(s -> VALID_IMPORT.matcher(s).matches()) + .filter(s -> !s.substring(s.lastIndexOf('.') + 1).equals(context.getClassName())) + .forEach(imports::add); }); + return new ResolvedContext(context.getName(), context.getPackageName(), imports, context.getClassName(), context.getSubcontexts().stream().map(this::resolve).toList(), context.getSpecifications().stream().map(specification -> { PolyObjectResolverVisitor visitor = new PolyObjectResolverVisitor(this); visitor.doVisit(specification); diff --git a/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/service/schema/JsonSchemaNameHelper.java b/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/service/schema/JsonSchemaNameHelper.java index 31dac47c..2c5de797 100644 --- a/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/service/schema/JsonSchemaNameHelper.java +++ b/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/service/schema/JsonSchemaNameHelper.java @@ -1,15 +1,49 @@ package io.polyapi.plugin.service.schema; +import com.fasterxml.jackson.databind.JsonNode; import org.jsonschema2pojo.GenerationConfig; import org.jsonschema2pojo.util.NameHelper; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +/** + * A NameHelper that preserves trailing numeric suffixes on JSON property names + * and maps '+' to 'Plus' and '-' to 'Minus'. + */ public class JsonSchemaNameHelper extends NameHelper { - public JsonSchemaNameHelper(GenerationConfig generationConfig) { - super(generationConfig); + + private static final Pattern SUFFIX_PATTERN = Pattern.compile("^(.*?)(_\\d+)$"); + + public JsonSchemaNameHelper(GenerationConfig config) { + super(config); } @Override public String replaceIllegalCharacters(String name) { - return super.replaceIllegalCharacters(name.replace("+", "Plus").replace("-", "Minus")); + String mapped = name.replace("+", "Plus").replace("-", "Minus"); + return super.replaceIllegalCharacters(mapped); + } + + @Override + public String getPropertyName(String jsonName, JsonNode node) { + String mapped = jsonName.replace("+", "Plus").replace("-", "Minus"); + Matcher m = SUFFIX_PATTERN.matcher(mapped); + String base = mapped; + String suffix = ""; + if (m.matches()) { + base = m.group(1); + suffix = m.group(2).replaceAll("[^0-9]", ""); + } + String javaBase = super.getPropertyName(base, node); + if (!suffix.isEmpty()) { + javaBase = javaBase + suffix; + } + return javaBase; + } + + @Override + public String getClassName(String raw, JsonNode node) { + String mapped = raw.replace("+", "Plus").replace("-", "Minus"); + return super.getClassName(mapped, node); } } diff --git a/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/service/schema/JsonSchemaParser.java b/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/service/schema/JsonSchemaParser.java index fa2a11a1..e1a52774 100644 --- a/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/service/schema/JsonSchemaParser.java +++ b/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/service/schema/JsonSchemaParser.java @@ -2,6 +2,11 @@ import com.sun.codemodel.JClass; import com.sun.codemodel.JCodeModel; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import com.fasterxml.jackson.databind.node.ObjectNode; import io.polyapi.plugin.error.PolyApiMavenPluginException; import io.polyapi.plugin.model.ParsedType; import io.polyapi.plugin.model.generation.CustomType; @@ -12,60 +17,132 @@ import org.jsonschema2pojo.SchemaMapper; import java.io.IOException; -import java.net.URLEncoder; -import java.nio.charset.Charset; import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; import java.util.List; +import java.util.Locale; import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; @Slf4j public class JsonSchemaParser { - // FIXME: This whole JSON schema parsing needs: - // FIXME: 1. More automated testing. - // FIXME: 2. A refactor. public List parse(String defaultName, String packageName, String schema) { try { - var codeModel = new JCodeModel(); - log.trace("Generating Java code from JSON schema {}.", schema); + ObjectMapper om = new ObjectMapper(); + JsonNode root = om.readTree(Optional.ofNullable(schema).orElse("")); - // This cannot be put as an attribute of this class as it does not take well when being reused and has many errors. + int dupCount = patchDuplicates(root); + if (dupCount > 0) { + log.warn("⚠️ [{}] injected {} duplicate-field suffix(es)", defaultName, dupCount); + } + + String patched = om.writeValueAsString(root); + + // generate Java code from the patched schema + JCodeModel codeModel = new JCodeModel(); new SchemaMapper(new PolyRuleFactory(new PolyGenerationConfig()), new SchemaGenerator()) - .generate(codeModel, defaultName, packageName, Optional.ofNullable(schema).orElse("")); + .generate(codeModel, defaultName, packageName, patched); + log.debug("Code generated. Writing to string."); try (var codeWriter = new PolyCodeWriter()) { codeModel.build(codeWriter); var result = codeWriter.getClasses(); if (log.isTraceEnabled()) { - result.forEach((String name, String code) -> log.trace("Generated code for {} is: {}", name, code)); + result.forEach((name, code) -> log.trace("Generated code for {} is: {}", name, code)); } return result.entrySet().stream() .map(entry -> new CustomType(packageName, entry.getKey(), entry.getValue())) - .toList(); + .collect(Collectors.toList()); } } catch (IOException e) { - //FIXME: Throw the appropriate exception throw new PolyApiMavenPluginException(e); } } public ParsedType getType(String defaultName, String packageName, String schema) { - // This cannot be put as an attribute of this class as it does not take well when being reused and has many errors. try { - return getType(new SchemaMapper(new PolyRuleFactory(new PolyGenerationConfig()), new SchemaGenerator()) - .generate(new JCodeModel(), defaultName, packageName, Optional.ofNullable(schema).orElse("")) - .boxify()); + ObjectMapper om = new ObjectMapper(); + JsonNode root = om.readTree(Optional.ofNullable(schema).orElse("")); + patchDuplicates(root); + String patched = om.writeValueAsString(root); + + JClass jClass = new SchemaMapper(new PolyRuleFactory(new PolyGenerationConfig()), new SchemaGenerator()) + .generate(new JCodeModel(), defaultName, packageName, patched) + .boxify(); + return getType(jClass); } catch (IOException e) { - //FIXME: Throw the appropriate exception throw new PolyApiMavenPluginException(e); } } private ParsedType getType(JClass jClass) { - return new ParsedType(jClass.erasure().fullName(), Optional.ofNullable(jClass.getTypeParameters()) - .orElseGet(ArrayList::new) - .stream() - .map(this::getType) - .toList()); + return new ParsedType( + jClass.erasure().fullName(), + Optional.ofNullable(jClass.getTypeParameters()).orElseGet(ArrayList::new).stream() + .map(this::getType) + .collect(Collectors.toList()) + ); + } + + /** + * Recursively mutates the JSON schema tree, injecting any properties whose + * JSON names collapse to the same Java identifier under a suffix, + * into both "properties" and "required". + */ + private int patchDuplicates(JsonNode root) { + if (!root.isObject()) return 0; + int injectedCount = 0; + ObjectNode obj = (ObjectNode) root; + + if (obj.has("properties")) { + ObjectNode props = (ObjectNode) obj.get("properties"); + ArrayNode reqs = obj.has("required") + ? (ArrayNode) obj.get("required") + : new ArrayNode(JsonNodeFactory.instance); + + Set seen = new HashSet<>(); + List collisions = new ArrayList<>(); + Iterator it = props.fieldNames(); + while (it.hasNext()) { + String jsonName = it.next(); + String normalized = jsonName.replaceAll("[^A-Za-z0-9]+","") + .toLowerCase(Locale.ROOT); + log.trace(" – jsonName='{}' → normalized='{}'", jsonName, normalized); + if (!seen.add(normalized)) { + collisions.add(jsonName); + } + } + + int suffix = 1; + for (String dup : collisions) { + injectedCount++; + String injected = dup + "_" + suffix++; + JsonNode originalNode = props.get(dup); + props.remove(dup); + + // re-insert under the suffixed name + props.set(injected, originalNode); + + // now fix up "required": swap dup -> injected + if (obj.has("required")) { + ArrayNode reqsArray = (ArrayNode) obj.get("required"); + for (int i = 0; i < reqsArray.size(); i++) { + if (reqsArray.get(i).asText().equals(dup)) { + reqsArray.set(i, JsonNodeFactory.instance.textNode(injected)); + } + } + } + } + } + + Iterator fieldsIt = obj.fieldNames(); + while (fieldsIt.hasNext()) { + patchDuplicates(obj.get(fieldsIt.next())); + } + + return injectedCount; } } diff --git a/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/service/template/PolyHandlebars.java b/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/service/template/PolyHandlebars.java index 5d812e8a..5781e0ba 100644 --- a/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/service/template/PolyHandlebars.java +++ b/polyapi-maven-plugin/src/main/java/io/polyapi/plugin/service/template/PolyHandlebars.java @@ -7,6 +7,7 @@ import java.util.function.BiPredicate; import java.util.function.Function; +import java.lang.reflect.Method; public class PolyHandlebars extends Handlebars { @@ -15,6 +16,50 @@ public PolyHandlebars() { registerSimpleHelper("toCamelCase", StringUtils::toCamelCase); registerSimpleHelper("toPascalCase", StringUtils::toCamelCase); registerConditionalHelper("ifIsType", (object, options) -> object.getClass().getSimpleName().equals(options.param(0))); + registerSimpleHelper("lastSegment", (Object fqn) -> { + if (fqn == null) { + return ""; + } + String s = fqn.toString(); + int idx = s.lastIndexOf('.'); + return idx == -1 ? s : s.substring(idx + 1); + }); + registerConditionalHelper("eq", + (obj, opts) -> { + Object other = opts.param(0, ""); + return obj != null && obj.toString().equals(other == null ? "" : other.toString()); + }); + registerHelper("typeRef", (Object ctx, Options opts) -> { + if (ctx == null) { + return ""; + } + String fqn = ctx.toString(); + String parentSimple = opts.param(0, "").toString(); + String simple = fqn.substring(fqn.lastIndexOf('.') + 1); + return simple.equals(parentSimple) ? fqn : simple; + }); + registerHelper("classFqn", (Object ctx, Options o) -> { + if (ctx == null) return ""; + + for (String m : new String[]{"getFullClassName", "getFullName"}) { + try { + Method mm = ctx.getClass().getMethod(m); + Object val = mm.invoke(ctx); + if (val != null) return val.toString(); + } catch (ReflectiveOperationException ignored) {} + } + + try { + Method pm = ctx.getClass().getMethod("getPackageName"); + Method cm = ctx.getClass().getMethod("getClassName"); + Object pkg = pm.invoke(ctx); + Object cls = cm.invoke(ctx); + if (pkg != null && cls != null) return pkg + "." + cls; + if (cls != null) return cls.toString(); + } catch (ReflectiveOperationException ignored) {} + + return ""; + }); } private void registerSimpleHelper(String name, Function helper) { diff --git a/polyapi-maven-plugin/src/main/resources/templates/ResolvedContext.hbs b/polyapi-maven-plugin/src/main/resources/templates/ResolvedContext.hbs index 9765b8fd..92eff32b 100644 --- a/polyapi-maven-plugin/src/main/resources/templates/ResolvedContext.hbs +++ b/polyapi-maven-plugin/src/main/resources/templates/ResolvedContext.hbs @@ -8,68 +8,74 @@ import io.polyapi.client.api.model.PolyEntity; import io.polyapi.client.api.AuthTokenOptions; import io.polyapi.commons.api.model.PolyGeneratedClass; {{~#each this.imports}} +{{~#unless (eq (lastSegment this) ../className)}} import {{{this}}}; +{{~/unless}} {{~/each}} @PolyGeneratedClass public class {{className}} extends PolyContext { {{~#each functionSpecifications}} - private final {{this.className}} {{this.name}}; + private final {{typeRef (classFqn this) ../className}} {{this.name}}; {{~/each}} {{~#each standardAuthFunctionSpecifications}} - private final {{this.className}} {{this.name}}; + private final {{typeRef (classFqn this) ../className}} {{this.name}}; {{~/each}} {{~#each subresourceAuthFunctionSpecifications}} - private final {{this.className}} {{this.name}}; + private final {{typeRef (classFqn this) ../className}} {{this.name}}; {{~/each}} {{~#each serverVariableSpecifications}} - public final {{this.className}} {{this.name}}; + private final {{typeRef (classFqn this) ../className}} {{this.name}}; {{~/each}} {{~#each webhookHandlerSpecifications}} - private final {{this.className}} {{this.name}}; + private final {{typeRef (classFqn this) ../className}} {{this.name}}; {{~/each}} {{#each subcontexts}} - public final {{this.className}} {{this.name}}; + private final {{typeRef (classFqn this) ../className}} {{this.name}}; {{~/each}} - public {{className}}(PolyProxyFactory proxyFactory, WebSocketClient webSocketClient) { - super(proxyFactory, webSocketClient); +public {{className}}(PolyProxyFactory proxyFactory, WebSocketClient webSocketClient) { +super(proxyFactory, webSocketClient); {{~#each serverFunctionSpecifications}} - this.{{this.name}} = createServerFunctionProxy({{this.className}}.class); + this.{{this.name}} = + createServerFunctionProxy({{classFqn this}}.class); {{~/each}} {{~#each customFunctionSpecifications}} - this.{{this.name}} = createCustomFunctionProxy({{this.className}}.class); + this.{{this.name}} = + createCustomFunctionProxy({{classFqn this}}.class); {{~/each}} {{~#each apiFunctionSpecifications}} - this.{{this.name}} = createApiFunctionProxy({{this.className}}.class); + this.{{this.name}} = + createApiFunctionProxy({{classFqn this}}.class); {{~/each}} {{~#each subresourceAuthFunctionSpecifications}} - this.{{this.name}} = createSubresourceAuthFunction({{this.className}}.class); + this.{{this.name}} = + createSubresourceAuthFunction({{classFqn this}}.class); {{~/each}} {{~#each standardAuthFunctionSpecifications}} - this.{{this.name}} = create{{#if audienceRequired}}Audience{{/if}}TokenAuthFunction({{this.className}}.class); + this.{{this.name}} = + create{{#if audienceRequired}}Audience{{/if}}TokenAuthFunction({{classFqn this}}.class); {{~/each}} {{~#each serverVariableSpecifications}} - this.{{this.name}} = createServerVariableHandler({{this.className}}.class); + this.{{this.name}} = + createServerVariableHandler({{classFqn this}}.class); {{~/each}} {{~#each webhookHandlerSpecifications}} - this.{{this.name}} = createPolyTriggerProxy({{this.className}}.class); + this.{{this.name}} = + createPolyTriggerProxy({{classFqn this}}.class); {{~/each}} {{#each subcontexts}} - this.{{this.name}} = new {{this.className}}(proxyFactory, webSocketClient); + this.{{this.name}} = new {{typeRef (classFqn this) ../className}}(proxyFactory, webSocketClient); {{~/each}} } {{~#each functionSpecifications}} public {{{this.returnType}}} {{{this.methodSignature}}} { - {{~#if this.returnsValue}} - return - {{~else}} - {{~/if}} this.{{this.name}}.{{this.name}}({{this.paramVariableNames}}); + {{#if this.returnsValue}}return {{/if}}this.{{this.name}}.{{this.name}}({{this.paramVariableNames}}); } - public {{{this.className}}} get{{{this.className}}}Function() { - return this.{{{this.name}}}; + public {{typeRef (classFqn this) ../className}} get{{this.className}}Function() { + return this.{{this.name}}; } {{~/each}} @@ -110,6 +116,9 @@ public class {{className}} extends PolyContext { {{~#each specifications}} {{~#ifIsType this "AuthFunctionSpecification"}} + public {{typeRef (classFqn this) ../className}} get{{this.className}}AuthFunction() { + return this.{{this.name}}; + } {{~#if subResource}} public void {{name}}(String token) { this.{{name}}.{{name}}(token); @@ -127,8 +136,8 @@ public class {{className}} extends PolyContext { } {{~/if}} - public {{{this.className}}} get{{{this.className}}}AuthFunction() { - return this.{{{this.name}}}; + public {{typeRef (classFqn this) ../className}} get{{this.className}}AuthFunction() { + return this.{{this.name}}; } {{~/ifIsType}} {{~/each}} diff --git a/polyapi-maven-plugin/src/test/java/io/polyapi/plugin/service/JsonSchemaParserTest.java b/polyapi-maven-plugin/src/test/java/io/polyapi/plugin/service/JsonSchemaParserTest.java index c0a73ffa..c29fe997 100644 --- a/polyapi-maven-plugin/src/test/java/io/polyapi/plugin/service/JsonSchemaParserTest.java +++ b/polyapi-maven-plugin/src/test/java/io/polyapi/plugin/service/JsonSchemaParserTest.java @@ -40,24 +40,31 @@ public static Stream generateSource() { createArguments(13, "Schema with different types that have the same enum.", "Identifier", DEFAULT_RESPONSE_NAME, "Data"), createArguments(14, "Schema that is an Integer."), createArguments(15, "Schema with multiple enums with the same name and properties.", DEFAULT_RESPONSE_NAME, "DashMinusstyle", "DashMinusstyle_", "Other"), - createArguments(16, "Schema with json property yhat has a space in it.", "Data", DEFAULT_RESPONSE_NAME)); + createArguments(16, "Schema with json property yhat has a space in it.", "Data", DEFAULT_RESPONSE_NAME), + createArguments(17, "Schema with both order_id and orderId to force a collision", "Case17SchemaWithDuplicateFields")); } public static Stream getTypeSource() { - return Stream.of(Arguments.of(1, "Simple recursive schema with no base type.", createClassName(DEFAULT_RESPONSE_NAME)), + return Stream.of( + Arguments.of(1, "Simple recursive schema with no base type.", createClassName(DEFAULT_RESPONSE_NAME)), Arguments.of(2, "Recursive schema with base type.", createClassName(DEFAULT_RESPONSE_NAME)), - Arguments.of(3, "Schema that has a text value evaluated to null.", createClassName(DEFAULT_RESPONSE_NAME)), + Arguments.of(3, "Schema that has a text value evaluated to null.", + createClassName(DEFAULT_RESPONSE_NAME)), Arguments.of(4, "Schema with base type and no definitions.", createClassName(DEFAULT_RESPONSE_NAME)), Arguments.of(5, "Schema for array of numbers.", createListClassName(Double.class.getName())), Arguments.of(6, "Schema for array of integers.", createListClassName(Long.class.getName())), Arguments.of(7, "Simple schema with attribute.", createClassName(DEFAULT_RESPONSE_NAME)), - Arguments.of(8, "Schema with duplicate fields.", createListClassName(createClassName("ResponseTypeElement"))), + Arguments.of(8, "Schema with duplicate fields.", + createListClassName(createClassName("ResponseTypeElement"))), Arguments.of(9, "Schema with enum.", createClassName(DEFAULT_RESPONSE_NAME)), Arguments.of(10, "Schema that is a String.", String.class.getName()), Arguments.of(11, "Schema that uses allof.", Object.class.getName()), - Arguments.of(12, "Schema with enum with '-' in one of the options.", createClassName(DEFAULT_RESPONSE_NAME)), - Arguments.of(13, "Schema with different types that have the same enum.", createClassName(DEFAULT_RESPONSE_NAME)), - Arguments.of(14, "Schema that is an integer.", Long.class.getName())); + Arguments.of(12, "Schema with enum with '-' in one of the options.", + createClassName(DEFAULT_RESPONSE_NAME)), + Arguments.of(13, "Schema with different types that have the same enum.", + createClassName(DEFAULT_RESPONSE_NAME)), + Arguments.of(14, "Schema that is an integer.", Long.class.getName()), + Arguments.of(17, "Schema with both order_id and orderId to force a collision", createClassName("Case17SchemaWithDuplicateFields"))); } private static String createClassName(String className) { @@ -81,6 +88,12 @@ public void generateTest(Integer caseNumber, String description, List ex var customTypes = jsonSchemaParser.parse("TestResponse", specification.getPackageName(), getSchema(caseNumber)); assertThat(customTypes, notNullValue()); assertThat(customTypes.size(), equalTo(expectedNames.size())); + if (caseNumber.equals(17)) { + CustomType ct = customTypes.get(0); + String code = ct.getCode(); + // check that we injected the suffixed field into the class + assertTrue(code.contains("orderId1"), "Expected generated code to contain field 'orderId1'"); + } var customTypeNames = customTypes.stream().map(CustomType::getName).toList(); expectedNames.forEach(expectedName -> assertTrue(customTypeNames.contains(expectedName), format("Result should contain object with name %s. Result contains %s.", expectedName, customTypeNames))); customTypes.forEach(customType -> assertTrue(customType.getCode().contains(format("public class %s {", customType.getName())) || customType.getCode().contains(format("public enum %s {", customType.getName())))); @@ -89,12 +102,18 @@ public void generateTest(Integer caseNumber, String description, List ex @ParameterizedTest(name = "Case {0}: {1}") @MethodSource("getTypeSource") public void getTypeTest(Integer caseNumber, String description, String expectedType) { - assertThat(jsonSchemaParser.getType(DEFAULT_RESPONSE_NAME, JsonSchemaParserTest.class.getPackageName(), getSchema(caseNumber)).getFullName(), equalTo(expectedType)); + assertThat(jsonSchemaParser + .getType(DEFAULT_RESPONSE_NAME, JsonSchemaParserTest.class.getPackageName(), getSchema(caseNumber)) + .getFullName(), equalTo(expectedType)); } private String getSchema(Integer caseNumber) { try { - return IOUtils.toString(JsonSchemaParser.class.getResourceAsStream(format("/%s/cases/Case %s.schema.json", JsonSchemaParser.class.getPackageName().replace(".", "/"), caseNumber)), defaultCharset()); + return IOUtils + .toString( + JsonSchemaParser.class.getResourceAsStream(format("/%s/cases/Case %s.schema.json", + JsonSchemaParser.class.getPackageName().replace(".", "/"), caseNumber)), + defaultCharset()); } catch (IOException e) { throw new RuntimeException(e); } diff --git a/polyapi-maven-plugin/src/test/resources/io/polyapi/plugin/service/schema/cases/Case 17.schema.json b/polyapi-maven-plugin/src/test/resources/io/polyapi/plugin/service/schema/cases/Case 17.schema.json new file mode 100644 index 00000000..3180c84b --- /dev/null +++ b/polyapi-maven-plugin/src/test/resources/io/polyapi/plugin/service/schema/cases/Case 17.schema.json @@ -0,0 +1,17 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Case 17: Schema with duplicate fields", + "type": "object", + "properties": { + "order_id": { + "type": "string" + }, + "orderId": { + "type": "string" + } + }, + "required": [ + "order_id", + "orderId" + ] +} \ No newline at end of file diff --git a/pom.xml b/pom.xml index 1611ed06..60c23271 100644 --- a/pom.xml +++ b/pom.xml @@ -1,9 +1,10 @@ + 4.0.0 io.polyapi polyapi-java - 0.15.5-SNAPSHOT + 0.15.6-SNAPSHOT pom parent-pom @@ -34,8 +35,8 @@ scm:git:git@github.com:polyapi/polyapi-java.git scm:git:git@github.com:polyapi/polyapi-java.git https://github.com/polyapi/polyapi-java - HEAD - + HEAD + @@ -65,7 +66,7 @@ maven-invoker-plugin 3.7.0 - verify + true true @@ -79,7 +80,7 @@ ${project.build.directory}/it/develop - https://develop-k8s.polyapi.io + https://dev.polyapi.io ${develop.api.key} 443 @@ -104,7 +105,7 @@ maven-invoker-plugin 3.7.0 - verify + true true @@ -143,7 +144,7 @@ maven-invoker-plugin 3.7.0 - verify + true true @@ -169,7 +170,7 @@ - + release @@ -190,16 +191,21 @@ + + + --pinentry-mode + loopback + + - org.sonatype.plugins - nexus-staging-maven-plugin - 1.6.13 + org.sonatype.central + central-publishing-maven-plugin + 0.7.0 true - ossrh - https://s01.oss.sonatype.org/ - true + central + true @@ -244,4 +250,4 @@ - + \ No newline at end of file diff --git a/src/it/deploy-function-it/pom.xml b/src/it/deploy-function-it/pom.xml index f01d7c9e..72f9b682 100644 --- a/src/it/deploy-function-it/pom.xml +++ b/src/it/deploy-function-it/pom.xml @@ -62,7 +62,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.12.1 + 3.14.0 true