diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 70a86f88..448e307e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,6 +1,8 @@ name: build -on: [pull_request] +on: + push: + branches-ignore: [main] jobs: build: @@ -11,8 +13,5 @@ jobs: with: { java-version: 17, distribution: temurin } - name: Build and Verify run: | - # Build the plugin first and check its contents - ./gradlew :openapi-generator-plugin:build - - # Now run the full build - ./gradlew clean build + # With convention plugins, everything is self-contained + ./gradlew -PisSnapshot=true clean build diff --git a/.github/workflows/pr-snapshot.yml b/.github/workflows/pr-snapshot.yml new file mode 100644 index 00000000..bb92062f --- /dev/null +++ b/.github/workflows/pr-snapshot.yml @@ -0,0 +1,45 @@ +name: PR Snapshot + +on: + push: + branches-ignore: + - main + +jobs: + build-and-publish-snapshot: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-java@v4 + with: + java-version: 17 + distribution: temurin + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 + + - name: Build and Publish Snapshot + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_ACTOR: ${{ github.actor }} + # Sonatype credentials + OSSRH_USERNAME: ${{ secrets.MAVEN_CENTRAL_CREDS_TOKEN_USER }} + OSSRH_PASSWORD: ${{ secrets.MAVEN_CENTRAL_CREDS_TOKEN_PASS }} + # Optional signing environment variables for consistency with publish workflow + SIGNING_KEY: ${{ secrets.ORG_GRADLE_PROJECT_signingKey || '' }} + SIGNING_PASSWORD: ${{ secrets.ORG_GRADLE_PROJECT_signingPassword || '' }} + run: | + # Build the plugin first + ./gradlew -PisSnapshot=true :openapi-generator-plugin:build :openapi-generator-plugin:publishToMavenLocal + + # Set up parameters based on available secrets + GRADLE_PARAMS="-PisSnapshot=true" + if [ -n "$SIGNING_KEY" ] && [ -n "$SIGNING_PASSWORD" ]; then + GRADLE_PARAMS="$GRADLE_PARAMS -P signingKey=$SIGNING_KEY -P signingPassword=$SIGNING_PASSWORD" + fi + + # Publish snapshots to GitHub Packages + ./gradlew -PisSnapshot=true \ + -P ossrhUsername=${{ secrets.MAVEN_CENTRAL_CREDS_TOKEN_USER }} \ + -P ossrhPassword=${{ secrets.MAVEN_CENTRAL_CREDS_TOKEN_PASS }} \ + $GRADLE_PARAMS :api-client-library:publishToSonatype diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 2f66e133..00ca3981 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -35,8 +35,8 @@ jobs: ./gradlew \ -P signingKey=${{ secrets.ORG_GRADLE_PROJECT_signingKey }} \ -P signingPassword=${{ secrets.ORG_GRADLE_PROJECT_signingPassword }} \ - -P ossrhUsername=${{ secrets.OSSRH_USERNAME }} \ - -P ossrhPassword=${{ secrets.OSSRH_PASSWORD }} \ + -P ossrhUsername=${{ secrets.MAVEN_CENTRAL_CREDS_TOKEN_USER }} \ + -P ossrhPassword=${{ secrets.MAVEN_CENTRAL_CREDS_TOKEN_PASS }} \ :api-client-library:publishToSonatype \ - :api-client-library:closeAndReleaseSonatypeStagingRepository + closeAndReleaseSonatypeStagingRepository ./scripts/publish.sh diff --git a/.github/workflows/version-check.yml b/.github/workflows/version-check.yml new file mode 100644 index 00000000..9698c6a7 --- /dev/null +++ b/.github/workflows/version-check.yml @@ -0,0 +1,111 @@ +name: Version Check + +on: + pull_request: + branches: [ main ] + +jobs: + version-check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 # Fetch full history to compare branches + + - uses: actions/setup-java@v4 + with: + java-version: 17 + distribution: temurin + + - name: Check Version Increment + run: | + # Extract current version from build.gradle + CURRENT_VERSION=$(grep "def projectVersion" build.gradle | sed -E "s/.*def projectVersion = ['\"]([^'\"]*)['\"].*/\1/") + echo "Current version: $CURRENT_VERSION" + + # Validate that we extracted current version correctly + if [ -z "$CURRENT_VERSION" ]; then + echo "❌ Failed to extract current version from build.gradle" + exit 1 + fi + + # Get the latest Git tag from the target branch + git fetch origin ${{ github.base_ref }} + git checkout origin/${{ github.base_ref }} + + LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "") + + if [ -z "$LATEST_TAG" ]; then + echo "No existing tags found. Assuming this is the first release." + LATEST_TAG="0.0.0" + fi + + # Remove 'v' prefix if present + TARGET_VERSION=$(echo "$LATEST_TAG" | sed 's/^v//') + echo "Latest tag version: $TARGET_VERSION" + + # Switch back to PR branch + git checkout ${{ github.head_ref }} + + # Function to normalize version (ensure 3 parts) + normalize_version() { + local version=$1 + IFS='.' read -ra V <<< "$version" + + # Ensure we have 3 parts + while [ ${#V[@]} -lt 3 ]; do V+=(0); done + + echo "${V[0]}.${V[1]}.${V[2]}" + } + + # Function to compare semantic versions + version_compare() { + local version1=$1 + local version2=$2 + + # Normalize both versions + version1=$(normalize_version "$version1") + version2=$(normalize_version "$version2") + + # Split versions into arrays + IFS='.' read -ra V1 <<< "$version1" + IFS='.' read -ra V2 <<< "$version2" + + # Compare major version + if [ "${V1[0]}" -gt "${V2[0]}" ]; then + return 0 # version1 > version2 + elif [ "${V1[0]}" -lt "${V2[0]}" ]; then + return 1 # version1 < version2 + fi + + # Compare minor version + if [ "${V1[1]}" -gt "${V2[1]}" ]; then + return 0 + elif [ "${V1[1]}" -lt "${V2[1]}" ]; then + return 1 + fi + + # Compare patch version + if [ "${V1[2]}" -gt "${V2[2]}" ]; then + return 0 + elif [ "${V1[2]}" -lt "${V2[2]}" ]; then + return 1 + fi + + # Versions are equal + return 2 + } + + # Compare versions + if version_compare "$CURRENT_VERSION" "$TARGET_VERSION"; then + echo "✅ Version increment detected: $TARGET_VERSION → $CURRENT_VERSION" + exit 0 + elif [ $? -eq 2 ]; then + echo "❌ Version not incremented: $CURRENT_VERSION is the same as $TARGET_VERSION" + echo "Please increment the version in build.gradle" + exit 1 + else + echo "❌ Version decreased: $TARGET_VERSION → $CURRENT_VERSION" + echo "Version should only increase, not decrease" + exit 1 + fi diff --git a/MULTI_MODULE_README.md b/MULTI_MODULE_README.md index 8138fd60..b965b1a1 100644 --- a/MULTI_MODULE_README.md +++ b/MULTI_MODULE_README.md @@ -1,54 +1,26 @@ -# Vertex API Client Java - Multi-Module Project +# Vertex API Client Java - Project Architecture -This project has been restructured as a multi-module Gradle project with the following modules: +This project uses a modern Gradle multi-module structure with convention plugins for clean, maintainable builds. -## Modules +## Project Structure -### 1. `openapi-generator-plugin` -Custom Gradle plugin for generating Vertex API client code using OpenAPI Generator. +### Core Modules -**Location**: `openapi-generator-plugin/` - -**Purpose**: -- Contains custom OpenAPI code generation logic -- Provides a reusable Gradle plugin for generating the API client -- Includes any custom templates or generators specific to Vertex API - -**Build**: -```bash -./gradlew :openapi-generator-plugin:build -./gradlew :openapi-generator-plugin:publishToMavenLocal -``` - -### 2. `api-client-library` +#### `api-client-library/` The main API client library generated from the OpenAPI specification. -**Location**: `api-client-library/` - **Purpose**: - Contains the generated API client code -- Includes authentication utilities -- Provides the core SDK functionality - -**Build**: -```bash -./gradlew :api-client-library:build -``` +- Includes authentication utilities and models +- Provides the core SDK functionality that gets published to Maven Central -**Generate API Client**: -```bash -./gradlew :api-client-library:openApiGenerate -``` - -### 3. `examples` +#### `examples/` Example applications demonstrating how to use the API client library. -**Location**: `examples/` - **Purpose**: -- Contains example code showing API usage +- Contains example code showing API usage patterns - Includes command-line utilities for common operations -- Demonstrates best practices for using the API client +- Demonstrates best practices for authentication and API calls **Run Examples**: ```bash @@ -57,50 +29,46 @@ Example applications demonstrating how to use the API client library. ./gradlew :examples:listExamples ``` -## Build Order - -The modules have dependencies on each other and should be built in this order: +### Build Infrastructure -1. `openapi-generator-plugin` (standalone) -2. `api-client-library` (uses the plugin) -3. `examples` (depends on the library) +#### `buildSrc/` +Contains the build logic and custom OpenAPI generator. -## Building the Entire Project +**Purpose**: +- Houses convention plugins (`vertex.java-conventions`, `vertex.openapi-generator`) +- Contains the custom `VertexJavaClientCodegen` generator +- Provides shared build configuration across all modules +- Eliminates circular dependencies and external plugin publishing -To build all modules: -```bash -./gradlew build -``` +**Key Files**: +- `src/main/java/com/vertexvis/codegen/VertexJavaClientCodegen.java` - Custom OpenAPI generator +- `src/main/groovy/vertex.java-conventions.gradle` - Common Java build settings +- `src/main/groovy/vertex.openapi-generator.gradle` - OpenAPI generation configuration -To clean and rebuild everything: -```bash -./gradlew clean build -``` +## Architecture Benefits -## Publishing +### Convention Plugins +- **Self-contained**: No need to publish plugins separately +- **Consistent**: Shared configuration across all modules +- **Maintainable**: Build logic lives with the code it serves -The library can be published to Maven repositories: -```bash -./gradlew :api-client-library:publishToMavenLocal -./gradlew publish -``` +### Simplified Dependencies +- **No circular dependencies**: buildSrc → modules (not modules → plugins → modules) +- **Faster builds**: No separate plugin build/publish cycle +- **Easier development**: Make changes and build immediately ## Development Workflow -1. Make changes to the OpenAPI generator plugin if needed -2. Build and publish the plugin locally: `./gradlew :openapi-generator-plugin:publishToMavenLocal` -3. Regenerate the API client: `./gradlew :api-client-library:openApiGenerate` -4. Build the library: `./gradlew :api-client-library:build` -5. Test with examples: `./gradlew :examples:run` +The build is completely self-contained: -## Migration Notes +```bash +# Everything needed is built automatically +./gradlew build -This project was converted from a single-module to a multi-module structure: +# Test examples +./gradlew :examples:run -- **Original structure**: All code in `src/main/java/com/vertexvis/` -- **New structure**: - - Code generation logic → `openapi-generator-plugin/` - - Core API client → `api-client-library/` - - Examples → `examples/` +# Publish locally for testing +./gradlew :api-client-library:publishToMavenLocal +``` -The functionality remains the same, but the code is now better organized and the generator can be reused across projects. diff --git a/README.md b/README.md index 45801d1d..a6812836 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ The client can be used with Java 1.8+ and pulled into Maven or Gradle projects. com.vertexvis api-client-java - 0.10.0 + 0.11.0 compile ``` @@ -25,13 +25,13 @@ The client can be used with Java 1.8+ and pulled into Maven or Gradle projects. ### Gradle ```groovy -compile "com.vertexvis:api-client-java:0.10.0" +compile "com.vertexvis:api-client-java:0.11.0" ``` ### Sbt ```sbt -libraryDependencies += "com.vertexvis" % "api-client-java" % "0.10.0" +libraryDependencies += "com.vertexvis" % "api-client-java" % "0.11.0" ``` ### Others @@ -44,7 +44,7 @@ mvn clean package Then manually install the following JARs. -- `target/api-client-java-0.10.0.jar` +- `target/api-client-java-0.11.0.jar` - `target/lib/*.jar` ## Usage @@ -63,37 +63,64 @@ Then, check out our [sample applications](./src/main/java/com/vertexvis/example) ## Local Development -This project uses a multi-module Gradle structure. For detailed information about the modules and their purposes, refer to the [Multi-Module README](./MULTI_MODULE_README.md). +This project uses a multi-module Gradle structure with convention plugins. For detailed information about the modules and their purposes, refer to the [Multi-Module README](./MULTI_MODULE_README.md). -### Build Order +### Quick Start -1. Build the OpenAPI Generator Plugin: -```bash -./gradlew :openapi-generator-plugin:build -./gradlew :openapi-generator-plugin:publishToMavenLocal -``` +The project now uses convention plugins in `buildSrc/` which makes the build completely self-contained: -2. Generate the API Client: ```bash -./gradlew :api-client-library:openApiGenerate -``` - -3. Build the API Client Library: -```bash -./gradlew :api-client-library:build -``` +# Build everything (no separate plugin publishing needed) +./gradlew build -4. Run Example Applications: -```bash +# Run example applications ./gradlew :examples:run ./gradlew :examples:listExamples ``` -### Building the Entire Project +### Development Workflow -To build all modules: -```bash -./gradlew build +1. **Make changes** to the API client or custom generator +2. **Build and test** with `./gradlew build` +3. **Test locally** with `./gradlew :api-client-library:publishToMavenLocal` + +### Using Snapshot Versions + +To consume published snapshot versions in other projects, add the snapshot repository to your build configuration: + +#### Maven + +```xml + + + central-snapshots + https://central.sonatype.com/repository/maven-snapshots/ + + true + + + + + + com.vertexvis + api-client-java + 0.11.0-SNAPSHOT + +``` + +#### Gradle + +```groovy +repositories { + mavenCentral() + maven { + url 'https://central.sonatype.com/repository/maven-snapshots/' + } +} + +dependencies { + implementation 'com.vertexvis:api-client-java:0.11.0-SNAPSHOT' +} ``` ### Versioning @@ -105,12 +132,9 @@ To bump the version of all modules: ### Publishing -To publish to Maven Local: +To publish to Maven Local for testing: ```bash ./gradlew :api-client-library:publishToMavenLocal ``` -To publish to Maven Central: -```bash -./gradlew publish -``` +Snapshots are automatically published to Maven Central on pushes to the `main` branch. Releases are created when a new version tag is pushed. diff --git a/api-client-library/build.gradle b/api-client-library/build.gradle index c2bbde60..ae7d3b12 100644 --- a/api-client-library/build.gradle +++ b/api-client-library/build.gradle @@ -1,13 +1,6 @@ -buildscript{ - dependencies { - classpath files("$rootDir/openapi-generator-plugin/build/libs/openapi-generator-plugin-0.11.0.jar") - } -} plugins { - id 'java-library' - id 'maven-publish' - id 'signing' - id 'org.openapi.generator' version '7.14.0' + id 'vertex.java-conventions' + id 'vertex.openapi-generator' } description = 'Vertex API Client Library for Java' @@ -29,108 +22,26 @@ dependencies { testImplementation("com.squareup.okhttp3:mockwebserver:4.12.0") } -openApiGenerate { - verbose = false - generatorName = 'vertex-java' // Use our custom generator - generateModelTests = false - generateApiTests = false - generateModelDocumentation = false - remoteInputSpec = 'https://platform.vertexvis.com/spec' - outputDir = "${buildDir}/generated/" - invokerPackage = 'com.vertexvis' - modelPackage = 'com.vertexvis.model' - apiPackage = 'com.vertexvis.api' - templateDir = "${project(':openapi-generator-plugin').projectDir}/src/main/resources/vertex-java" - configOptions = [ - openApiNullable: "false", - dateLibrary: "java8", - hideGenerationTimestamp: "true", - useRuntimeException: "true", - ] - additionalProperties = [ - skipValidationFor: "Part,PartData,PartDataAttributes,QueuedJobData,QueuedJob,QueuedJobDataAttributes" // Comma-separated list of models to skip validation for - ] - ignoreFileOverride = "${projectDir}/.openapi-generator-ignore" -} - -sourceSets { - main { - java { - srcDirs += [ - "${buildDir}/generated/src/main/java" - ] - } - } -} -tasks.named("openApiGenerate").configure { - dependsOn(":openapi-generator-plugin:build") -} - -compileJava.dependsOn tasks.openApiGenerate -compileTestJava.dependsOn tasks.openApiGenerate - -// Ensure our custom generator plugin is built before we generate -tasks.openApiGenerate.dependsOn ':openapi-generator-plugin:build' - -java { - withJavadocJar() - withSourcesJar() -} - -tasks.named('sourcesJar') { - dependsOn tasks.openApiGenerate - from sourceSets.main.allJava - duplicatesStrategy = DuplicatesStrategy.EXCLUDE -} - -jar { - from sourceSets.main.allSource - duplicatesStrategy = DuplicatesStrategy.EXCLUDE -} - +// Override publication configuration for this specific library publishing { publications { maven(MavenPublication) { artifactId = 'api-client-java' - from components.java pom { name = 'com.vertexvis:api-client-java' description = 'The Vertex REST API client for Java.' - url = 'https://github.com/Vertexvis/vertex-api-client-java' - licenses { - license { - name = 'MIT' - url = 'https://github.com/Vertexvis/vertex-api-client-java/blob/main/LICENSE' - } - } - developers { - developer { - email = 'support@vertexvis.com' - name = 'Vertex Developers' - organizationUrl = 'https://developer.vertexvis.com/' - } - } - scm { - connection = 'scm:git:git@github.com:vertexvis/vertex-api-client-java.git' - developerConnection = 'scm:git:git@github.com:vertexvis/vertex-api-client-java.git' - url = 'https://github.com/Vertexvis/vertex-api-client-java' - } } } } } -signing { - def hasSigningKey = project.hasProperty("signingKey") - def hasSigningPassword = project.hasProperty("signingPassword") - required { hasSigningKey && hasSigningPassword && !project.version.endsWith("-SNAPSHOT") } - if (hasSigningKey && hasSigningPassword) { - def base64Decode = { prop -> - return new String(Base64.getDecoder().decode(project.findProperty(prop).toString())).trim() - } - useInMemoryPgpKeys(base64Decode("signingKey"), base64Decode("signingPassword")) - } - sign publishing.publications.maven +// Ensure proper task dependencies for generated sources +tasks.named('sourcesJar') { + dependsOn tasks.openApiGenerate +} + +tasks.named('javadocJar') { + dependsOn tasks.openApiGenerate } javadoc { @@ -140,12 +51,3 @@ javadoc { } dependsOn tasks.openApiGenerate } - -tasks.withType(Sign) { - dependsOn tasks.withType(GenerateModuleMetadata) - dependsOn tasks.withType(Jar) -} - -tasks.withType(PublishToMavenLocal) { - dependsOn tasks.withType(Sign) -} diff --git a/build.gradle b/build.gradle index 1c1adca4..daa9f223 100644 --- a/build.gradle +++ b/build.gradle @@ -1,48 +1,27 @@ plugins { id "idea" - id "io.github.gradle-nexus.publish-plugin" version "1.1.0" + id "io.github.gradle-nexus.publish-plugin" } -group = 'com.vertexvis' -version = '0.11.0' +def projectVersion = '0.11.0' +def isSnapshot = project.hasProperty('isSnapshot') && project.isSnapshot.toBoolean() +version = isSnapshot ? "${projectVersion}-SNAPSHOT" : projectVersion allprojects { group = 'com.vertexvis' - version = '0.11.0' - - repositories { - mavenCentral() - } -} + version = rootProject.version -subprojects { - apply plugin: 'java-library' - - java { - toolchain { - languageVersion.set(JavaLanguageVersion.of(17)) - } - } - - test { - useJUnitPlatform() - testLogging { - events "passed", "skipped", "failed" - } - } + // Make project version accessible as a project property for buildscript blocks + ext.projectVersion = projectVersion } nexusPublishing { repositories { sonatype { - nexusUrl.set(uri("https://s01.oss.sonatype.org/service/local/")) - snapshotRepositoryUrl.set(uri("https://s01.oss.sonatype.org/content/repositories/snapshots/")) - username = project.hasProperty("ossrhUsername") ? project.ossrhUsername : "" - password = project.hasProperty("ossrhPassword") ? project.ossrhPassword : "" + nexusUrl.set(uri("https://ossrh-staging-api.central.sonatype.com/service/local/")) + snapshotRepositoryUrl.set(uri("https://central.sonatype.com/repository/maven-snapshots/")) + username = project.findProperty("ossrhUsername") ?: System.getenv("OSSRH_USERNAME") + password = project.findProperty("ossrhPassword") ?: System.getenv("OSSRH_PASSWORD") } } } - -def base64Decode(prop) { - return new String(Base64.getDecoder().decode(project.findProperty(prop).toString())).trim() -} diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle new file mode 100644 index 00000000..d3ce4e01 --- /dev/null +++ b/buildSrc/build.gradle @@ -0,0 +1,15 @@ +plugins { + id 'groovy-gradle-plugin' +} + +repositories { + gradlePluginPortal() + mavenCentral() +} + +dependencies { + implementation 'org.openapitools:openapi-generator-gradle-plugin:7.14.0' + implementation 'org.openapitools:openapi-generator:7.14.0' + implementation 'io.swagger.core.v3:swagger-models:2.2.31' + implementation 'io.github.gradle-nexus:publish-plugin:2.0.0' +} diff --git a/buildSrc/src/main/groovy/vertex.java-conventions.gradle b/buildSrc/src/main/groovy/vertex.java-conventions.gradle new file mode 100644 index 00000000..ad82efb2 --- /dev/null +++ b/buildSrc/src/main/groovy/vertex.java-conventions.gradle @@ -0,0 +1,79 @@ +plugins { + id 'java-library' + id 'maven-publish' + id 'signing' +} + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(17) + } + withJavadocJar() + withSourcesJar() +} + +test { + useJUnitPlatform() +} + +repositories { + mavenLocal() + mavenCentral() +} + +publishing { + repositories { + maven { + name = 'GitHubPackages' + url = uri("https://maven.pkg.github.com/Vertexvis/vertex-api-client-java") + credentials { + username = System.getenv("GITHUB_ACTOR") ?: project.findProperty("gpr.user") ?: "" + password = System.getenv("GITHUB_TOKEN") ?: project.findProperty("gpr.key") ?: "" + } + } + } + publications { + maven(MavenPublication) { + from components.java + pom { + url = 'https://github.com/Vertexvis/vertex-api-client-java' + licenses { + license { + name = 'MIT' + url = 'https://github.com/Vertexvis/vertex-api-client-java/blob/main/LICENSE' + } + } + developers { + developer { + email = 'support@vertexvis.com' + name = 'Vertex Developers' + organizationUrl = 'https://developer.vertexvis.com/' + } + } + scm { + connection = 'scm:git:git@github.com:vertexvis/vertex-api-client-java.git' + developerConnection = 'scm:git:git@github.com:vertexvis/vertex-api-client-java.git' + url = 'https://github.com/Vertexvis/vertex-api-client-java' + } + } + } + } +} + +signing { + def hasSigningKey = project.hasProperty("signingKey") + def hasSigningPassword = project.hasProperty("signingPassword") + required { hasSigningKey && hasSigningPassword && !project.version.endsWith("-SNAPSHOT") } + if (hasSigningKey && hasSigningPassword) { + def base64Decode = { prop -> + return new String(Base64.getDecoder().decode(project.findProperty(prop).toString())).trim() + } + useInMemoryPgpKeys(base64Decode("signingKey"), base64Decode("signingPassword")) + } + sign publishing.publications.maven +} + +tasks.withType(Sign) { + dependsOn tasks.withType(GenerateModuleMetadata) + dependsOn tasks.withType(Jar) +} diff --git a/buildSrc/src/main/groovy/vertex.openapi-generator.gradle b/buildSrc/src/main/groovy/vertex.openapi-generator.gradle new file mode 100644 index 00000000..20b0530c --- /dev/null +++ b/buildSrc/src/main/groovy/vertex.openapi-generator.gradle @@ -0,0 +1,44 @@ +plugins { + id 'org.openapi.generator' +} + +repositories { + mavenLocal() + mavenCentral() +} + +openApiGenerate { + verbose = false + generatorName = 'vertex-java' // Use our custom generator + generateModelTests = false + generateApiTests = false + generateModelDocumentation = false + remoteInputSpec = 'https://platform.vertexvis.com/spec' + outputDir = "${buildDir}/generated/" + invokerPackage = 'com.vertexvis' + modelPackage = 'com.vertexvis.model' + apiPackage = 'com.vertexvis.api' + templateDir = "${project.rootDir}/buildSrc/src/main/resources/vertex-java" + configOptions = [ + openApiNullable: "false", + dateLibrary: "java8", + hideGenerationTimestamp: "true", + useRuntimeException: "true", + ] + additionalProperties = [ + skipValidationFor: "Part,PartData,PartDataAttributes,QueuedJobData,QueuedJob,QueuedJobDataAttributes" // Comma-separated list of models to skip validation for + ] + ignoreFileOverride = "${projectDir}/.openapi-generator-ignore" +} + +// Ensure generated sources are included in the source sets +sourceSets { + main { + java { + srcDir "${buildDir}/generated/src/main/java" + } + } +} + +// Make sure compilation depends on code generation +compileJava.dependsOn tasks.openApiGenerate diff --git a/buildSrc/src/main/java/com/vertexvis/codegen/VertexJavaClientCodegen.java b/buildSrc/src/main/java/com/vertexvis/codegen/VertexJavaClientCodegen.java new file mode 100644 index 00000000..b7ee2605 --- /dev/null +++ b/buildSrc/src/main/java/com/vertexvis/codegen/VertexJavaClientCodegen.java @@ -0,0 +1,99 @@ +package com.vertexvis.codegen; + +import org.openapitools.codegen.CodegenModel; +import org.openapitools.codegen.CodegenProperty; +import org.openapitools.codegen.languages.JavaClientCodegen; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.openapitools.codegen.CodegenModel; +import org.openapitools.codegen.CodegenConfig; +import org.openapitools.codegen.model.ModelsMap; +import org.openapitools.codegen.model.ModelMap; +import io.swagger.v3.oas.models.media.Schema; +import java.util.*; + +/** + * Custom Java client codegen that supports conditional validation skipping. + */ +public class VertexJavaClientCodegen extends JavaClientCodegen { + private static final Logger LOGGER = LoggerFactory.getLogger(VertexJavaClientCodegen.class); + + /** Configuration option key for specifying models to skip validation for. */ + public static final String SKIP_VALIDATION_FOR = "skipValidationFor"; + /** Vendor extension key used to mark models that should skip validation. */ + public static final String X_SKIP_VALIDATION = "x-skip-validation"; + + private Set skipValidationModels = new HashSet<>(); + + /** + * Default constructor that sets up the custom CLI options. + */ + public VertexJavaClientCodegen() { + super(); + + // Add custom CLI option + cliOptions.add(org.openapitools.codegen.CliOption.newString(SKIP_VALIDATION_FOR, + "Comma-separated list of model class names to skip validation for (e.g., Scene,SceneMetadata)")); + } + + @Override + public String getName() { + return "vertex-java"; + } + + @Override + public String getHelp() { + return "Generates a Vertex-customized Java client library."; + } + + @Override + public void processOpts() { + super.processOpts(); + + // Parse the skipValidationFor option + if (additionalProperties.containsKey(SKIP_VALIDATION_FOR)) { + String skipValidationForValue = (String) additionalProperties.get(SKIP_VALIDATION_FOR); + if (skipValidationForValue != null && !skipValidationForValue.trim().isEmpty()) { + String[] modelNames = skipValidationForValue.split(","); + for (String modelName : modelNames) { + skipValidationModels.add(modelName.trim()); + } + LOGGER.info("Models to skip validation: {}", skipValidationModels); + } + } + } + + @Override + public CodegenModel fromModel(String name, Schema schema) { + CodegenModel model = super.fromModel(name, schema); + + // Check if this model should skip validation + if (skipValidationModels.contains(model.classname)) { + LOGGER.info("Adding skip validation extension for model: {}", model.classname); + model.vendorExtensions.put(X_SKIP_VALIDATION, true); + } + + return model; + } + + @Override + public ModelsMap postProcessModels(ModelsMap objs) { + var result = super.postProcessModels(objs); + + // Additional processing can be done here if needed + @SuppressWarnings("unchecked") + List models = (List) objs.get("models"); + + if (models != null) { + for (ModelMap modelMap : models) { + CodegenModel model = modelMap.getModel(); + + if (model.vendorExtensions.containsKey(X_SKIP_VALIDATION)) { + LOGGER.debug("Model {} will skip validation", model.classname); + } + } + } + + return result; + } +} diff --git a/buildSrc/src/main/resources/META-INF/services/org.openapitools.codegen.CodegenConfig b/buildSrc/src/main/resources/META-INF/services/org.openapitools.codegen.CodegenConfig new file mode 100644 index 00000000..953e26e4 --- /dev/null +++ b/buildSrc/src/main/resources/META-INF/services/org.openapitools.codegen.CodegenConfig @@ -0,0 +1 @@ +com.vertexvis.codegen.VertexJavaClientCodegen diff --git a/buildSrc/src/main/resources/vertex-java/pojo.mustache b/buildSrc/src/main/resources/vertex-java/pojo.mustache new file mode 100644 index 00000000..e1f16c3b --- /dev/null +++ b/buildSrc/src/main/resources/vertex-java/pojo.mustache @@ -0,0 +1,601 @@ +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonArray; +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.TypeAdapterFactory; +import com.google.gson.reflect.TypeToken; +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; +import java.io.IOException; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import {{invokerPackage}}.JSON; + +/** + * {{description}}{{^description}}{{classname}}{{/description}}{{#isDeprecated}} + * @deprecated{{/isDeprecated}} + */{{#isDeprecated}} +@Deprecated{{/isDeprecated}} +{{#swagger1AnnotationLibrary}} +{{#description}} +@ApiModel(description = "{{{.}}}") +{{/description}} +{{/swagger1AnnotationLibrary}} +{{#swagger2AnnotationLibrary}} +{{#description}} +@Schema(description = "{{{.}}}") +{{/description}} +{{/swagger2AnnotationLibrary}} +{{>additionalModelTypeAnnotations}}{{>generatedAnnotation}}{{#discriminator}}{{>typeInfoAnnotation}}{{/discriminator}}{{>xmlAnnotation}} +{{#vendorExtensions.x-class-extra-annotation}} +{{{vendorExtensions.x-class-extra-annotation}}} +{{/vendorExtensions.x-class-extra-annotation}} +public class {{classname}} {{#parent}}extends {{{.}}} {{/parent}}{{#vendorExtensions.x-implements}}{{#-first}}implements {{{.}}}{{/-first}}{{^-first}}, {{{.}}}{{/-first}}{{#-last}} {{/-last}}{{/vendorExtensions.x-implements}}{ +{{#serializableModel}} + private static final long serialVersionUID = 1L; + +{{/serializableModel}} + {{#vars}} + {{#isEnum}} + {{^isContainer}} +{{>modelInnerEnum}} + + {{/isContainer}} + {{#isContainer}} + {{#mostInnerItems}} +{{>modelInnerEnum}} + + {{/mostInnerItems}} + {{/isContainer}} + {{/isEnum}} + public static final String SERIALIZED_NAME_{{nameInSnakeCase}} = "{{baseName}}"; + {{#withXml}} + @Xml{{#isXmlAttribute}}Attribute{{/isXmlAttribute}}{{^isXmlAttribute}}Element{{/isXmlAttribute}}(name = "{{items.xmlName}}{{^items.xmlName}}{{xmlName}}{{^xmlName}}{{baseName}}{{/xmlName}}{{/items.xmlName}}"{{#xmlNamespace}}, namespace = "{{.}}"{{/xmlNamespace}}) + {{#isXmlWrapped}} + @XmlElementWrapper(name = "{{xmlName}}{{^xmlName}}{{baseName}}{{/xmlName}}"{{#xmlNamespace}}, namespace = "{{.}}"{{/xmlNamespace}}) + {{/isXmlWrapped}} + {{/withXml}} + {{#deprecated}} + @Deprecated + {{/deprecated}} + @SerializedName(SERIALIZED_NAME_{{nameInSnakeCase}}) + {{#vendorExtensions.x-field-extra-annotation}} + {{{vendorExtensions.x-field-extra-annotation}}} + {{/vendorExtensions.x-field-extra-annotation}} + {{>nullable_var_annotations}}{{! prevent indent}} + {{#isDiscriminator}}protected{{/isDiscriminator}}{{^isDiscriminator}}private{{/isDiscriminator}} {{{datatypeWithEnum}}} {{name}}{{#defaultValue}} = {{{.}}}{{/defaultValue}}; + + {{/vars}} + public {{classname}}() { + {{#parent}} + {{#parcelableModel}} + super(); + {{/parcelableModel}} + {{/parent}} + {{#discriminator}} + {{#discriminator.isEnum}} +{{#readWriteVars}}{{#isDiscriminator}}{{#defaultValue}} + this.{{name}} = {{defaultValue}}; +{{/defaultValue}}{{/isDiscriminator}}{{/readWriteVars}} + {{/discriminator.isEnum}} + {{^discriminator.isEnum}} + this.{{{discriminatorName}}} = this.getClass().getSimpleName(); + {{/discriminator.isEnum}} + {{/discriminator}} + } + {{#vendorExtensions.x-has-readonly-properties}} + {{^withXml}} + + public {{classname}}( + {{#readOnlyVars}} + {{{datatypeWithEnum}}} {{name}}{{^-last}}, {{/-last}} + {{/readOnlyVars}} + ) { + this(); + {{#readOnlyVars}} + this.{{name}} = {{name}}; + {{/readOnlyVars}} + } + {{/withXml}} + {{/vendorExtensions.x-has-readonly-properties}} + {{#vars}} + + {{^isReadOnly}} + {{#deprecated}} + @Deprecated + {{/deprecated}} + public {{classname}} {{name}}({{>nullable_var_annotations}} {{{datatypeWithEnum}}} {{name}}) { + this.{{name}} = {{name}}; + return this; + } + {{#isArray}} + + public {{classname}} add{{nameInPascalCase}}Item({{{items.datatypeWithEnum}}} {{name}}Item) { + if (this.{{name}} == null) { + this.{{name}} = {{{defaultValue}}}{{^defaultValue}}new {{#uniqueItems}}LinkedHashSet{{/uniqueItems}}{{^uniqueItems}}ArrayList{{/uniqueItems}}<>(){{/defaultValue}}; + } + this.{{name}}.add({{name}}Item); + return this; + } + {{/isArray}} + {{#isMap}} + + public {{classname}} put{{nameInPascalCase}}Item(String key, {{{items.datatypeWithEnum}}} {{name}}Item) { + if (this.{{name}} == null) { + this.{{name}} = {{{defaultValue}}}{{^defaultValue}}new HashMap<>(){{/defaultValue}}; + } + this.{{name}}.put(key, {{name}}Item); + return this; + } + {{/isMap}} + + {{/isReadOnly}} + /** + {{#description}} + * {{.}} + {{/description}} + {{^description}} + * Get {{name}} + {{/description}} + {{#minimum}} + * minimum: {{.}} + {{/minimum}} + {{#maximum}} + * maximum: {{.}} + {{/maximum}} + * @return {{name}} + {{#deprecated}} + * @deprecated + {{/deprecated}} + */ +{{#deprecated}} + @Deprecated +{{/deprecated}} + {{>nullable_var_annotations}}{{! prevent indent}} +{{#useBeanValidation}} +{{>beanValidation}} + +{{/useBeanValidation}} +{{#swagger1AnnotationLibrary}} + @ApiModelProperty({{#example}}example = "{{{.}}}", {{/example}}{{#required}}required = {{required}}, {{/required}}value = "{{{description}}}") +{{/swagger1AnnotationLibrary}} +{{#swagger2AnnotationLibrary}} + @Schema({{#example}}example = "{{{.}}}", {{/example}}requiredMode = {{#required}}Schema.RequiredMode.REQUIRED{{/required}}{{^required}}Schema.RequiredMode.NOT_REQUIRED{{/required}}, description = "{{{description}}}") +{{/swagger2AnnotationLibrary}} +{{#vendorExtensions.x-extra-annotation}} + {{{vendorExtensions.x-extra-annotation}}} +{{/vendorExtensions.x-extra-annotation}} + public {{{datatypeWithEnum}}} {{getter}}() { + return {{name}}; + } + + {{^isReadOnly}} +{{#vendorExtensions.x-setter-extra-annotation}} {{{vendorExtensions.x-setter-extra-annotation}}} +{{/vendorExtensions.x-setter-extra-annotation}}{{#deprecated}} @Deprecated +{{/deprecated}} public void {{setter}}({{>nullable_var_annotations}} {{{datatypeWithEnum}}} {{name}}) { + this.{{name}} = {{name}}; + } + {{/isReadOnly}} + + {{/vars}} +{{>libraries/okhttp-gson/additional_properties}} + + + @Override + public boolean equals(Object o) { + {{#useReflectionEqualsHashCode}} + return EqualsBuilder.reflectionEquals(this, o, false, null, true); + {{/useReflectionEqualsHashCode}} + {{^useReflectionEqualsHashCode}} + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + }{{#hasVars}} + {{classname}} {{classVarName}} = ({{classname}}) o; + return {{#vars}}{{#isByteArray}}Arrays{{/isByteArray}}{{^isByteArray}}Objects{{/isByteArray}}.equals(this.{{name}}, {{classVarName}}.{{name}}){{^-last}} && + {{/-last}}{{/vars}}{{#isAdditionalPropertiesTrue}}&& + Objects.equals(this.additionalProperties, {{classVarName}}.additionalProperties){{/isAdditionalPropertiesTrue}}{{#parent}} && + super.equals(o){{/parent}};{{/hasVars}}{{^hasVars}} + return {{#parent}}super.equals(o){{/parent}}{{^parent}}true{{/parent}};{{/hasVars}} + {{/useReflectionEqualsHashCode}} + }{{#vendorExtensions.x-jackson-optional-nullable-helpers}} + + private static boolean equalsNullable(JsonNullable a, JsonNullable b) { + return a == b || (a != null && b != null && a.isPresent() && b.isPresent() && Objects.deepEquals(a.get(), b.get())); + }{{/vendorExtensions.x-jackson-optional-nullable-helpers}} + + @Override + public int hashCode() { + {{#useReflectionEqualsHashCode}} + return HashCodeBuilder.reflectionHashCode(this); + {{/useReflectionEqualsHashCode}} + {{^useReflectionEqualsHashCode}} + return Objects.hash({{#vars}}{{^isByteArray}}{{name}}{{/isByteArray}}{{#isByteArray}}Arrays.hashCode({{name}}){{/isByteArray}}{{^-last}}, {{/-last}}{{/vars}}{{#parent}}{{#hasVars}}, {{/hasVars}}super.hashCode(){{/parent}}{{#isAdditionalPropertiesTrue}}{{#hasVars}}, {{/hasVars}}{{^hasVars}}{{#parent}}, {{/parent}}{{/hasVars}}additionalProperties{{/isAdditionalPropertiesTrue}}); + {{/useReflectionEqualsHashCode}} + }{{#vendorExtensions.x-jackson-optional-nullable-helpers}} + + private static int hashCodeNullable(JsonNullable a) { + if (a == null) { + return 1; + } + return a.isPresent() ? Arrays.deepHashCode(new Object[]{a.get()}) : 31; + }{{/vendorExtensions.x-jackson-optional-nullable-helpers}} + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class {{classname}} {\n"); + {{#parent}} + sb.append(" ").append(toIndentedString(super.toString())).append("\n"); + {{/parent}} + {{#vars}} + sb.append(" {{name}}: ").append({{#isPassword}}"*"{{/isPassword}}{{^isPassword}}toIndentedString({{name}}){{/isPassword}}).append("\n"); + {{/vars}} +{{#isAdditionalPropertiesTrue}} + sb.append(" additionalProperties: ").append(toIndentedString(additionalProperties)).append("\n"); +{{/isAdditionalPropertiesTrue}} + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + +{{#parcelableModel}} + + public void writeToParcel(Parcel out, int flags) { +{{#model}} +{{#isArray}} + out.writeList(this); +{{/isArray}} +{{^isArray}} +{{#parent}} + super.writeToParcel(out, flags); +{{/parent}} +{{#vars}} + out.writeValue({{name}}); +{{/vars}} +{{/isArray}} +{{/model}} + } + + {{classname}}(Parcel in) { +{{#isArray}} + in.readTypedList(this, {{arrayModelType}}.CREATOR); +{{/isArray}} +{{^isArray}} +{{#parent}} + super(in); +{{/parent}} +{{#vars}} +{{#isPrimitiveType}} + {{name}} = ({{{datatypeWithEnum}}})in.readValue(null); +{{/isPrimitiveType}} +{{^isPrimitiveType}} + {{name}} = ({{{datatypeWithEnum}}})in.readValue({{complexType}}.class.getClassLoader()); +{{/isPrimitiveType}} +{{/vars}} +{{/isArray}} + } + + public int describeContents() { + return 0; + } + + public static final Parcelable.Creator<{{classname}}> CREATOR = new Parcelable.Creator<{{classname}}>() { + public {{classname}} createFromParcel(Parcel in) { +{{#model}} +{{#isArray}} + {{classname}} result = new {{classname}}(); + result.addAll(in.readArrayList({{arrayModelType}}.class.getClassLoader())); + return result; +{{/isArray}} +{{^isArray}} + return new {{classname}}(in); +{{/isArray}} +{{/model}} + } + public {{classname}}[] newArray(int size) { + return new {{classname}}[size]; + } + }; +{{/parcelableModel}} + + public static HashSet openapiFields; + public static HashSet openapiRequiredFields; + + static { + // a set of all properties/fields (JSON key names) + {{#hasVars}} + openapiFields = new HashSet(Arrays.asList({{#allVars}}"{{baseName}}"{{^-last}}, {{/-last}}{{/allVars}})); + {{/hasVars}} + {{^hasVars}} + openapiFields = new HashSet(0); + {{/hasVars}} + + // a set of required properties/fields (JSON key names) + {{#hasRequired}} + openapiRequiredFields = new HashSet(Arrays.asList({{#requiredVars}}"{{baseName}}"{{^-last}}, {{/-last}}{{/requiredVars}})); + {{/hasRequired}} + {{^hasRequired}} + openapiRequiredFields = new HashSet(0); + {{/hasRequired}} + } + + /** + * Validates the JSON Element and throws an exception if issues found + * + * @param jsonElement JSON Element + * @throws IOException if the JSON Element is invalid with respect to {{classname}} + */ + public static void validateJsonElement(JsonElement jsonElement) throws IOException { + if (jsonElement == null) { + if (!{{classname}}.openapiRequiredFields.isEmpty()) { // has required fields but JSON element is null + throw new IllegalArgumentException(String.format("The required field(s) %s in {{{classname}}} is not found in the empty JSON string", {{classname}}.openapiRequiredFields.toString())); + } + } + {{^hasChildren}} + {{^isAdditionalPropertiesTrue}} + + Set> entries = jsonElement.getAsJsonObject().entrySet(); + // check to see if the JSON string contains additional fields + for (Map.Entry entry : entries) { + if (!{{classname}}.openapiFields.contains(entry.getKey())) { + throw new IllegalArgumentException(String.format("The field `%s` in the JSON string is not defined in the `{{classname}}` properties. JSON: %s", entry.getKey(), jsonElement.toString())); + } + } + {{/isAdditionalPropertiesTrue}} + {{#requiredVars}} + {{#-first}} + + // check to make sure all required properties/fields are present in the JSON string + for (String requiredField : {{classname}}.openapiRequiredFields) { + if (jsonElement.getAsJsonObject().get(requiredField) == null) { + throw new IllegalArgumentException(String.format("The required field `%s` is not found in the JSON string: %s", requiredField, jsonElement.toString())); + } + } + {{/-first}} + {{/requiredVars}} + {{/hasChildren}} + {{^discriminator}} + {{#hasVars}} + JsonObject jsonObj = jsonElement.getAsJsonObject(); + {{/hasVars}} + {{#vars}} + {{#isArray}} + {{#items.isModel}} + {{#required}} + // ensure the json data is an array + if (!jsonObj.get("{{{baseName}}}").isJsonArray()) { + throw new IllegalArgumentException(String.format("Expected the field `{{{baseName}}}` to be an array in the JSON string but got `%s`", jsonObj.get("{{{baseName}}}").toString())); + } + + JsonArray jsonArray{{name}} = jsonObj.getAsJsonArray("{{{baseName}}}"); + // validate the required field `{{{baseName}}}` (array) + for (int i = 0; i < jsonArray{{name}}.size(); i++) { + {{{items.dataType}}}.validateJsonElement(jsonArray{{name}}.get(i)); + }; + {{/required}} + {{^required}} + if (jsonObj.get("{{{baseName}}}") != null && !jsonObj.get("{{{baseName}}}").isJsonNull()) { + JsonArray jsonArray{{name}} = jsonObj.getAsJsonArray("{{{baseName}}}"); + if (jsonArray{{name}} != null) { + // ensure the json data is an array + if (!jsonObj.get("{{{baseName}}}").isJsonArray()) { + throw new IllegalArgumentException(String.format("Expected the field `{{{baseName}}}` to be an array in the JSON string but got `%s`", jsonObj.get("{{{baseName}}}").toString())); + } + + // validate the optional field `{{{baseName}}}` (array) + for (int i = 0; i < jsonArray{{name}}.size(); i++) { + {{{items.dataType}}}.validateJsonElement(jsonArray{{name}}.get(i)); + }; + } + } + {{/required}} + {{/items.isModel}} + {{^items.isModel}} + {{^required}} + // ensure the optional json data is an array if present + if (jsonObj.get("{{{baseName}}}") != null && !jsonObj.get("{{{baseName}}}").isJsonNull() && !jsonObj.get("{{{baseName}}}").isJsonArray()) { + throw new IllegalArgumentException(String.format("Expected the field `{{{baseName}}}` to be an array in the JSON string but got `%s`", jsonObj.get("{{{baseName}}}").toString())); + } + {{/required}} + {{#required}} + // ensure the required json array is present + if (jsonObj.get("{{{baseName}}}") == null) { + throw new IllegalArgumentException("Expected the field `linkedContent` to be an array in the JSON string but got `null`"); + } else if (!jsonObj.get("{{{baseName}}}").isJsonArray()) { + throw new IllegalArgumentException(String.format("Expected the field `{{{baseName}}}` to be an array in the JSON string but got `%s`", jsonObj.get("{{{baseName}}}").toString())); + } + {{/required}} + {{/items.isModel}} + {{/isArray}} + {{^isContainer}} + {{#isString}} + if ({{#notRequiredOrIsNullable}}(jsonObj.get("{{{baseName}}}") != null && !jsonObj.get("{{{baseName}}}").isJsonNull()) && {{/notRequiredOrIsNullable}}!jsonObj.get("{{{baseName}}}").isJsonPrimitive()) { + throw new IllegalArgumentException(String.format("Expected the field `{{{baseName}}}` to be a primitive type in the JSON string but got `%s`", jsonObj.get("{{{baseName}}}").toString())); + } + {{/isString}} + {{#isModel}} + {{#required}} + // validate the required field `{{{baseName}}}` + {{{dataType}}}.validateJsonElement(jsonObj.get("{{{baseName}}}")); + {{/required}} + {{^required}} + // validate the optional field `{{{baseName}}}` + if (jsonObj.get("{{{baseName}}}") != null && !jsonObj.get("{{{baseName}}}").isJsonNull()) { + {{{dataType}}}.validateJsonElement(jsonObj.get("{{{baseName}}}")); + } + {{/required}} + {{/isModel}} + {{#isEnum}} + {{#required}} + // validate the required field `{{{baseName}}}` + {{{datatypeWithEnum}}}.validateJsonElement(jsonObj.get("{{{baseName}}}")); + {{/required}} + {{^required}} + // validate the optional field `{{{baseName}}}` + if (jsonObj.get("{{{baseName}}}") != null && !jsonObj.get("{{{baseName}}}").isJsonNull()) { + {{{datatypeWithEnum}}}.validateJsonElement(jsonObj.get("{{{baseName}}}")); + } + {{/required}} + {{/isEnum}} + {{#isEnumRef}} + {{#required}} + // validate the required field `{{{baseName}}}` + {{{dataType}}}.validateJsonElement(jsonObj.get("{{{baseName}}}")); + {{/required}} + {{^required}} + // validate the optional field `{{{baseName}}}` + if (jsonObj.get("{{{baseName}}}") != null && !jsonObj.get("{{{baseName}}}").isJsonNull()) { + {{{dataType}}}.validateJsonElement(jsonObj.get("{{{baseName}}}")); + } + {{/required}} + {{/isEnumRef}} + {{/isContainer}} + {{/vars}} + {{/discriminator}} + {{#hasChildren}} + {{#discriminator}} + + String discriminatorValue = jsonElement.getAsJsonObject().get("{{{propertyBaseName}}}").getAsString(); + switch (discriminatorValue) { + {{#mappedModels}} + case "{{mappingName}}": + {{modelName}}.validateJsonElement(jsonElement); + break; + {{/mappedModels}} + default: + throw new IllegalArgumentException(String.format("The value of the `{{{propertyBaseName}}}` field `%s` does not match any key defined in the discriminator's mapping.", discriminatorValue)); + } + {{/discriminator}} + {{/hasChildren}} + } + +{{^hasChildren}} + public static class CustomTypeAdapterFactory implements TypeAdapterFactory { + @SuppressWarnings("unchecked") + @Override + public TypeAdapter create(Gson gson, TypeToken type) { + if (!{{classname}}.class.isAssignableFrom(type.getRawType())) { + return null; // this class only serializes '{{classname}}' and its subtypes + } + final TypeAdapter elementAdapter = gson.getAdapter(JsonElement.class); + final TypeAdapter<{{classname}}> thisAdapter + = gson.getDelegateAdapter(this, TypeToken.get({{classname}}.class)); + + return (TypeAdapter) new TypeAdapter<{{classname}}>() { + @Override + public void write(JsonWriter out, {{classname}} value) throws IOException { + JsonObject obj = thisAdapter.toJsonTree(value).getAsJsonObject(); + {{#isAdditionalPropertiesTrue}} + obj.remove("additionalProperties"); + // serialize additional properties + if (value.getAdditionalProperties() != null) { + for (Map.Entry entry : value.getAdditionalProperties().entrySet()) { + if (entry.getValue() instanceof String) + obj.addProperty(entry.getKey(), (String) entry.getValue()); + else if (entry.getValue() instanceof Number) + obj.addProperty(entry.getKey(), (Number) entry.getValue()); + else if (entry.getValue() instanceof Boolean) + obj.addProperty(entry.getKey(), (Boolean) entry.getValue()); + else if (entry.getValue() instanceof Character) + obj.addProperty(entry.getKey(), (Character) entry.getValue()); + else { + JsonElement jsonElement = gson.toJsonTree(entry.getValue()); + if (jsonElement.isJsonArray()) { + obj.add(entry.getKey(), jsonElement.getAsJsonArray()); + } else { + obj.add(entry.getKey(), jsonElement.getAsJsonObject()); + } + } + } + } + {{/isAdditionalPropertiesTrue}} + elementAdapter.write(out, obj); + } + + @Override + public {{classname}} read(JsonReader in) throws IOException { + JsonElement jsonElement = elementAdapter.read(in); +{{^vendorExtensions.x-skip-validation}} + validateJsonElement(jsonElement); +{{/vendorExtensions.x-skip-validation}} +{{#vendorExtensions.x-skip-validation}} + // vertex:TODO: Skipping validation during deserialization of the JSON element based on configuration + // validateJsonElement(jsonElement); +{{/vendorExtensions.x-skip-validation}} + {{#isAdditionalPropertiesTrue}} + JsonObject jsonObj = jsonElement.getAsJsonObject(); + // store additional fields in the deserialized instance + {{classname}} instance = thisAdapter.fromJsonTree(jsonObj); + for (Map.Entry entry : jsonObj.entrySet()) { + if (!openapiFields.contains(entry.getKey())) { + if (entry.getValue().isJsonPrimitive()) { // primitive type + if (entry.getValue().getAsJsonPrimitive().isString()) + instance.putAdditionalProperty(entry.getKey(), entry.getValue().getAsString()); + else if (entry.getValue().getAsJsonPrimitive().isNumber()) + instance.putAdditionalProperty(entry.getKey(), entry.getValue().getAsNumber()); + else if (entry.getValue().getAsJsonPrimitive().isBoolean()) + instance.putAdditionalProperty(entry.getKey(), entry.getValue().getAsBoolean()); + else + throw new IllegalArgumentException(String.format("The field `%s` has unknown primitive type. Value: %s", entry.getKey(), entry.getValue().toString())); + } else if (entry.getValue().isJsonArray()) { + instance.putAdditionalProperty(entry.getKey(), gson.fromJson(entry.getValue(), List.class)); + } else { // JSON object + instance.putAdditionalProperty(entry.getKey(), gson.fromJson(entry.getValue(), HashMap.class)); + } + } + } + return instance; + {{/isAdditionalPropertiesTrue}} + {{^isAdditionalPropertiesTrue}} + return thisAdapter.fromJsonTree(jsonElement); + {{/isAdditionalPropertiesTrue}} + } + + }.nullSafe(); + } + } +{{/hasChildren}} + + /** + * Create an instance of {{classname}} given an JSON string + * + * @param jsonString JSON string + * @return An instance of {{classname}} + * @throws IOException if the JSON string is invalid with respect to {{classname}} + */ + public static {{{classname}}} fromJson(String jsonString) throws IOException { + return JSON.getGson().fromJson(jsonString, {{{classname}}}.class); + } + + /** + * Convert an instance of {{classname}} to an JSON string + * + * @return JSON string + */ + public String toJson() { + return JSON.getGson().toJson(this); + } +} \ No newline at end of file diff --git a/examples/build.gradle b/examples/build.gradle index 851083a5..8032d755 100644 --- a/examples/build.gradle +++ b/examples/build.gradle @@ -1,6 +1,7 @@ plugins { id 'java' id 'application' + id 'vertex.java-conventions' } description = 'Example applications using the Vertex API Client Library' diff --git a/examples/src/main/java/com/vertexvis/example/CreateAssemblyFromRevisionsExample.java b/examples/src/main/java/com/vertexvis/example/CreateAssemblyFromRevisionsExample.java index ba8841db..8a363d64 100644 --- a/examples/src/main/java/com/vertexvis/example/CreateAssemblyFromRevisionsExample.java +++ b/examples/src/main/java/com/vertexvis/example/CreateAssemblyFromRevisionsExample.java @@ -86,7 +86,7 @@ public void run() { } catch (InterruptedException e) { logger.severe(e.getMessage()); // Restore interrupted state... - Thread.currentThread().interrupt(); + java.lang.Thread.currentThread().interrupt(); } } diff --git a/examples/src/main/java/com/vertexvis/example/CreatePartRevisionsWithMetadataExample.java b/examples/src/main/java/com/vertexvis/example/CreatePartRevisionsWithMetadataExample.java index 3798f009..88dc2ea9 100644 --- a/examples/src/main/java/com/vertexvis/example/CreatePartRevisionsWithMetadataExample.java +++ b/examples/src/main/java/com/vertexvis/example/CreatePartRevisionsWithMetadataExample.java @@ -87,7 +87,7 @@ public void run() { } catch (InterruptedException e) { logger.severe(e.getMessage()); // Restore interrupted state... - Thread.currentThread().interrupt(); + java.lang.Thread.currentThread().interrupt(); } } diff --git a/openapi-generator-plugin/build.gradle b/openapi-generator-plugin/build.gradle index 18d4e68a..ca9b6e61 100644 --- a/openapi-generator-plugin/build.gradle +++ b/openapi-generator-plugin/build.gradle @@ -6,6 +6,11 @@ plugins { description = 'Custom OpenAPI Generator Plugin for Vertex API Client' +repositories { + mavenLocal() + mavenCentral() +} + dependencies { implementation 'org.openapitools:openapi-generator:7.14.0' implementation 'org.openapitools:openapi-generator-gradle-plugin:7.14.0' @@ -21,6 +26,16 @@ java { } publishing { + repositories { + maven { + name = 'GitHubPackages' + url = uri("https://maven.pkg.github.com/Vertexvis/vertex-api-client-java") + credentials { + username = System.getenv("GITHUB_ACTOR") ?: project.findProperty("gpr.user") ?: "" + password = System.getenv("GITHUB_TOKEN") ?: project.findProperty("gpr.key") ?: "" + } + } + } publications { maven(MavenPublication) { artifactId = 'openapi-generator-plugin' @@ -70,9 +85,9 @@ tasks.withType(Sign) { dependsOn tasks.withType(Jar) } -tasks.withType(PublishToMavenLocal) { - dependsOn tasks.withType(Sign) -} +// tasks.withType(PublishToMavenLocal) { +// dependsOn tasks.withType(Sign) +// } // Debug task to check if the service file exists task checkServiceFile { @@ -85,3 +100,16 @@ task checkServiceFile { } } } + +// Debug task to check version +task printVersion { + doLast { + println "Project version: ${project.version}" + println "Root project version: ${rootProject.version}" + println "isSnapshot property: ${project.hasProperty('isSnapshot')}" + if (project.hasProperty('isSnapshot')) { + println "isSnapshot value: ${project.isSnapshot}" + } + println "Version ends with SNAPSHOT: ${project.version.endsWith('-SNAPSHOT')}" + } +} diff --git a/openapi-generator-plugin/src/main/java/com/vertexvis/codegen/VertexJavaClientCodegen.java b/openapi-generator-plugin/src/main/java/com/vertexvis/codegen/VertexJavaClientCodegen.java index e3a99c52..b7ee2605 100644 --- a/openapi-generator-plugin/src/main/java/com/vertexvis/codegen/VertexJavaClientCodegen.java +++ b/openapi-generator-plugin/src/main/java/com/vertexvis/codegen/VertexJavaClientCodegen.java @@ -15,7 +15,7 @@ /** * Custom Java client codegen that supports conditional validation skipping. */ -public class VertexJavaClientCodegen extends JavaClientCodegen implements CodegenConfig { +public class VertexJavaClientCodegen extends JavaClientCodegen { private static final Logger LOGGER = LoggerFactory.getLogger(VertexJavaClientCodegen.class); /** Configuration option key for specifying models to skip validation for. */ diff --git a/scripts/bump-version.sh b/scripts/bump-version.sh index 3cebd15f..49420d0b 100755 --- a/scripts/bump-version.sh +++ b/scripts/bump-version.sh @@ -17,8 +17,9 @@ main() { new=$(_bump_version "$old" "$@") echo "Updating version from $old to $new" - sed -i "s|version = '$old'|version = '$new'|" build.gradle - sed -i "s|$old|$new|" README.md + # Use portable sed approach that works on both macOS and Linux + sed "s|def projectVersion = '$old'|def projectVersion = '$new'|" build.gradle > build.gradle.tmp && mv build.gradle.tmp build.gradle + sed "s|$old|$new|g" README.md > README.md.tmp && mv README.md.tmp README.md } main "$@" diff --git a/scripts/version-lib.sh b/scripts/version-lib.sh index 9c38564f..b688f6c9 100644 --- a/scripts/version-lib.sh +++ b/scripts/version-lib.sh @@ -46,8 +46,8 @@ _die() { # # Returns version. _get_version() { - local prefix="version = " - grep "$prefix" build.gradle | tr -d "$prefix" | tr -d "'" + local prefix="def projectVersion = " + grep "$prefix" build.gradle | sed "s/$prefix//" | tr -d "'" } # Internal: Bump API client version.