diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 261b2d93..6186d52d 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -168,3 +168,56 @@ jobs: GITHUB_ORG_ID: ${{ secrets.PYTHON_SDK_GITHUB_ORG_ID }} GITHUB_REPO_ID: ${{ secrets.PYTHON_SDK_GITHUB_REPO_ID }} SSH_KEY: ${{ secrets.PYTHON_SDK_SSH_KEY }} + + build-and-test-java-sdk: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Set up JDK + uses: actions/setup-java@v2 + with: + java-version: 11 + distribution: 'temurin' + cache: 'gradle' + + - name: Setup git + run: ./scripts/setup_git.sh + env: + GIT_USER_NAME: ${{ secrets.GIT_USER_NAME }} + GIT_USER_EMAIL: ${{ secrets.GIT_USER_EMAIL }} + + # - name: Clone the existing SDK + # run: ./scripts/clone_sdk.sh + # env: + # GITHUB_ORG_ID: ${{ secrets.JAVA_SDK_GITHUB_ORG_ID }} + # GITHUB_REPO_ID: ${{ secrets.JAVA_SDK_GITHUB_REPO_ID }} + # SSH_KEY: ${{ secrets.JAVA_SDK_SSH_KEY }} + # SDK_PATH: clients/fga-java-sdk + # KNOWN_HOSTS: ${{secrets.KNOWN_HOSTS}} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + + # TODO: Don't run this twice. For some reason, the initial run of + # `make build-client-java` doesn't work, but subsequent runs do. + - name: Build Java SDK + continue-on-error: true + run: |- + mkdir -p clients/fga-java-sdk + make build-client-java + + - name: Run All Tests + run: |- + make test-integration-client-java + + # - name: Check for SDK changes + # run: ./scripts/commit_push_changes.sh + # env: + # SDK_PATH: clients/fga-java-sdk + # DRY_RUN: 1 + # TAGGING_DISABLE: 1 + # GITHUB_ORG_ID: ${{ secrets.JAVA_SDK_GITHUB_ORG_ID }} + # GITHUB_REPO_ID: ${{ secrets.JAVA_SDK_GITHUB_REPO_ID }} + # SSH_KEY: ${{ secrets.JAVA_SDK_SSH_KEY }} diff --git a/Makefile b/Makefile index 6183a73b..8280e7c6 100644 --- a/Makefile +++ b/Makefile @@ -6,6 +6,7 @@ GO_DOCKER_TAG = 1 DOTNET_DOCKER_TAG = 6.0 GOLINT_DOCKER_TAG = v1.51-alpine BUSYBOX_DOCKER_TAG = 1 +GRADLE_DOCKER_TAG = 8.2 PYTHON_DOCKER_TAG = 3.10 # Other config CONFIG_DIR = ${PWD}/config @@ -28,6 +29,7 @@ pull-docker-images: docker pull golangci/golangci-lint:${GOLINT_DOCKER_TAG} docker pull mcr.microsoft.com/dotnet/sdk:${DOTNET_DOCKER_TAG} docker pull busybox:${BUSYBOX_DOCKER_TAG} + docker pull gradle:${GRADLE_DOCKER_TAG} ## Publishing publish-client-dotnet: build-client-dotnet @@ -42,10 +44,10 @@ test: test-all-clients build: build-all-clients .PHONY: test-all-clients -test-all-clients: test-client-js test-client-go test-client-dotnet test-client-python +test-all-clients: test-client-js test-client-go test-client-dotnet test-client-python test-client-java .PHONY: build-all-clients -build-all-clients: build-client-js build-client-go build-client-dotnet build-client-python +build-all-clients: build-client-js build-client-go build-client-dotnet build-client-python build-client-java ### JavaScript .PHONY: tag-client-js @@ -129,6 +131,7 @@ run-in-docker: -v "${CLIENTS_OUTPUT_DIR}/fga-${sdk_language}-sdk":/module \ -v ${CONFIG_DIR}:/config \ -w /module \ + --net="host" \ ${image} \ ${command} @@ -202,3 +205,19 @@ shellcheck: .PHONY: setup-new-sdk setup-new-sdk: ./scripts/setup_new_sdk.sh + +.PHONY: build-client-java +build-client-java: + make build-client sdk_language=java tmpdir=${TMP_DIR} + make run-in-docker sdk_language=java image=gradle:${GRADLE_DOCKER_TAG} command="/bin/sh -c 'chmod +x ./gradlew && gradle fmt build'" + +.PHONY: test-integration-client-java +test-integration-client-java: build-client-java + docker container rm --force openfga-for-java-client || true + docker run --detach --name openfga-for-java-client -p 8080:8080 openfga/openfga run + make run-in-docker sdk_language=java image=gradle:${GRADLE_DOCKER_TAG} command="/bin/sh -c './gradlew test-integration'" + docker container rm --force openfga-for-java-client + +.PHONY: test-client-java +test-client-java: build-client-java + make run-in-docker sdk_language=java image=gradle:${GRADLE_DOCKER_TAG} command="/bin/sh -c 'gradle test'" diff --git a/config/clients/java/.openapi-generator-ignore b/config/clients/java/.openapi-generator-ignore new file mode 100644 index 00000000..e3549f0b --- /dev/null +++ b/config/clients/java/.openapi-generator-ignore @@ -0,0 +1,16 @@ +# Used but at a different location +**/client/Configuration.java +**/client/Pair.java +**/client/ApiException.java + +# Unused +**/ServerConfiguration.java +**/ServerVariable.java +**/JSON.java +**/RFC3339DateFormat.java +src/main/AndroidManifest.xml +build.sbt +pom.xml +.github/workflows/maven.yml +git_push.sh +.travis.yml diff --git a/config/clients/java/CHANGELOG.md.mustache b/config/clients/java/CHANGELOG.md.mustache new file mode 100644 index 00000000..e69de29b diff --git a/config/clients/java/config.overrides.json b/config/clients/java/config.overrides.json new file mode 100644 index 00000000..57180b2c --- /dev/null +++ b/config/clients/java/config.overrides.json @@ -0,0 +1,105 @@ +{ + "sdkId": "java", + "gitRepoId": "java-sdk", + "packageName": "dev.openfga:openfga-sdk", + "artifactId": "openfga-sdk", + "groupId": "dev.openfga", + "artifactVersion": "0.0.1", + "packageVersion": "0.0.1", + "apiPackage": "dev.openfga.sdk.api", + "invokerPackage": "dev.openfga.sdk.api.client", + "modelPackage": "dev.openfga.sdk.api.model", + "snapshotVersion": false, + "packageDescription": "Java SDK for OpenFGA", + "artifactDescription": "Java SDK for OpenFGA", + "packageDetailedDescription": "This is an autogenerated Java SDK for OpenFGA. It provides a wrapper around the [OpenFGA API definition](https://openfga.dev/api).", + "developerEmail": "community@openfga.dev", + "developerName": "OpenFGA Contributors", + "developerOrganization": "OpenFGA", + "developerOrganizationUrl": "https://openfga.dev", + "licenseName": "Apache-2.0", + "licenseUrl": "https://github.com/openfga/java-sdk/blob/main/LICENSE", + "scmConnection": "scm:git:git@github.com:openfga/java-sdk.git", + "scmDeveloperConnection": "scm:git:git@github.com:openfga/java-sdk.git", + "scmUrl": "https://github.com/openfga/java-sdk", + "library": "native", + "asyncNative": true, + "disallowAdditionalPropertiesIfNotPresent": false, + "enumUnknownDefaultCase": true, + "allowUnicodeIdentifiers": true, + "caseInsensitiveResponseHeaders": true, + "files": { + "build.gradle.mustache" : { + "destinationFilename": "build.gradle", + "templateType": "SupportingFiles" + }, + ".github/workflows/main.yml.mustache" : { + "destinationFilename": ".github/workflows/main.yml", + "templateType": "SupportingFiles" + }, + "creds-CredentialsMethod.java.mustache" : { + "destinationFilename": "src/main/java/dev/openfga/sdk/api/auth/CredentialsMethod.java", + "templateType": "SupportingFiles" + }, + "creds-ApiBearerToken.java.mustache" : { + "destinationFilename": "src/main/java/dev/openfga/sdk/api/auth/ApiBearerToken.java", + "templateType": "SupportingFiles" + }, + "creds-ClientCredentials.java.mustache" : { + "destinationFilename": "src/main/java/dev/openfga/sdk/api/auth/ClientCredentials.java", + "templateType": "SupportingFiles" + }, + "creds-ClientCredentialsTest.java.mustache" : { + "destinationFilename": "src/test/java/dev/openfga/sdk/api/auth/ClientCredentialsTest.java", + "templateType": "SupportingFiles" + }, + "config-BaseConfiguration.java.mustache" : { + "destinationFilename": "src/main/java/dev/openfga/sdk/api/configuration/BaseConfiguration.java", + "templateType": "SupportingFiles" + }, + "config-Configuration.java.mustache" : { + "destinationFilename": "src/main/java/dev/openfga/sdk/api/configuration/Configuration.java", + "templateType": "SupportingFiles" + }, + "config-ConfigurationOverride.java.mustache" : { + "destinationFilename": "src/main/java/dev/openfga/sdk/api/configuration/ConfigurationOverride.java", + "templateType": "SupportingFiles" + }, + "config-ConfigurationTest.java.mustache" : { + "destinationFilename": "src/test/java/dev/openfga/sdk/api/configuration/ConfigurationTest.java", + "templateType": "SupportingFiles" + }, + "Pair.mustache" : { + "destinationFilename": "src/main/java/dev/openfga/util/Pair.java", + "templateType": "SupportingFiles" + }, + "util-StringUtil.java.mustache" : { + "destinationFilename": "src/main/java/dev/openfga/util/StringUtil.java", + "templateType": "SupportingFiles" + }, + "util-StringUtilTest.java.mustache" : { + "destinationFilename": "src/test/java/dev/openfga/util/StringUtilTest.java", + "templateType": "SupportingFiles" + }, + "libraries/native/apiException.mustache" : { + "destinationFilename": "src/main/java/dev/openfga/sdk/errors/ApiException.java", + "templateType": "SupportingFiles" + }, + "FgaInvalidParameterException.java.mustache" : { + "destinationFilename": "src/main/java/dev/openfga/sdk/errors/FgaInvalidParameterException.java", + "templateType": "SupportingFiles" + }, + "OpenFgaApiTest.java.mustache" : { + "destinationFilename": "src/test/java/dev/openfga/sdk/api/OpenFgaApiTest.java", + "templateType": "SupportingFiles" + }, + "OpenFgaApiIntegrationTest.java.mustache" : { + "destinationFilename": "src/test-integration/java/dev/openfga/sdk/api/OpenFgaApiIntegrationTest.java", + "templateType": "SupportingFiles" + }, + "gradle-wrapper.properties.mustache" : { + "destinationFilename": "gradle/wrapper/gradle-wrapper.properties", + "templateType": "SupportingFiles" + } + } +} diff --git a/config/clients/java/generator.txt b/config/clients/java/generator.txt new file mode 100644 index 00000000..f3d360b1 --- /dev/null +++ b/config/clients/java/generator.txt @@ -0,0 +1 @@ +java diff --git a/config/clients/java/template-source.json b/config/clients/java/template-source.json new file mode 100644 index 00000000..47ae7a41 --- /dev/null +++ b/config/clients/java/template-source.json @@ -0,0 +1,7 @@ +{ + "repo": "https://github.com/OpenAPITools/openapi-generator", + "branch": "master", + "commit": "90eacb685c57308e67c65e436b19d160091f91e1", + "url": "https://github.com/OpenAPITools/openapi-generator/tree/master/modules/openapi-generator/src/main/resources/Java", + "docs": "https://github.com/OpenAPITools/openapi-generator/blob/master/docs/generators/java.md" +} diff --git a/config/clients/java/template/.github/workflows/main.yml.mustache b/config/clients/java/template/.github/workflows/main.yml.mustache new file mode 100644 index 00000000..f23ec3a8 --- /dev/null +++ b/config/clients/java/template/.github/workflows/main.yml.mustache @@ -0,0 +1,27 @@ +name: Java CI with Gradle + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + build: + name: Build {{{appName}}} + runs-on: ubuntu-latest + strategy: + matrix: + java: [ '11', '17', '20' ] + steps: + - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.2 + - name: Set up JDK + uses: actions/setup-java@cd89f46ac9d01407894225f350157564c9c7cee2 # v3.12.0 + with: + {{=< >=}} + java-version: ${{ matrix.java }} + distribution: 'temurin' + cache: gradle + - name: Build with Gradle + run: | + ./gradlew build diff --git a/config/clients/java/template/CustomInstantDeserializer.mustache b/config/clients/java/template/CustomInstantDeserializer.mustache new file mode 100644 index 00000000..5ebea810 --- /dev/null +++ b/config/clients/java/template/CustomInstantDeserializer.mustache @@ -0,0 +1,232 @@ +package {{invokerPackage}}; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonTokenId; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.datatype.threetenbp.DecimalUtils; +import com.fasterxml.jackson.datatype.threetenbp.deser.ThreeTenDateTimeDeserializerBase; +import com.fasterxml.jackson.datatype.threetenbp.function.BiFunction; +import com.fasterxml.jackson.datatype.threetenbp.function.Function; +import org.threeten.bp.DateTimeException; +import org.threeten.bp.DateTimeUtils; +import org.threeten.bp.Instant; +import org.threeten.bp.OffsetDateTime; +import org.threeten.bp.ZoneId; +import org.threeten.bp.ZonedDateTime; +import org.threeten.bp.format.DateTimeFormatter; +import org.threeten.bp.temporal.Temporal; +import org.threeten.bp.temporal.TemporalAccessor; + +import java.io.IOException; +import java.math.BigDecimal; + +/** + * Deserializer for ThreeTen temporal {@link Instant}s, {@link OffsetDateTime}, and {@link ZonedDateTime}s. + * Adapted from the jackson threetenbp InstantDeserializer to add support for deserializing rfc822 format. + * + * @author Nick Williams + */ +public class CustomInstantDeserializer + extends ThreeTenDateTimeDeserializerBase { + private static final long serialVersionUID = 1L; + + public static final CustomInstantDeserializer INSTANT = new CustomInstantDeserializer( + Instant.class, DateTimeFormatter.ISO_INSTANT, + new Function() { + @Override + public Instant apply(TemporalAccessor temporalAccessor) { + return Instant.from(temporalAccessor); + } + }, + new Function() { + @Override + public Instant apply(FromIntegerArguments a) { + return Instant.ofEpochMilli(a.value); + } + }, + new Function() { + @Override + public Instant apply(FromDecimalArguments a) { + return Instant.ofEpochSecond(a.integer, a.fraction); + } + }, + null + ); + + public static final CustomInstantDeserializer OFFSET_DATE_TIME = new CustomInstantDeserializer( + OffsetDateTime.class, DateTimeFormatter.ISO_OFFSET_DATE_TIME, + new Function() { + @Override + public OffsetDateTime apply(TemporalAccessor temporalAccessor) { + return OffsetDateTime.from(temporalAccessor); + } + }, + new Function() { + @Override + public OffsetDateTime apply(FromIntegerArguments a) { + return OffsetDateTime.ofInstant(Instant.ofEpochMilli(a.value), a.zoneId); + } + }, + new Function() { + @Override + public OffsetDateTime apply(FromDecimalArguments a) { + return OffsetDateTime.ofInstant(Instant.ofEpochSecond(a.integer, a.fraction), a.zoneId); + } + }, + new BiFunction() { + @Override + public OffsetDateTime apply(OffsetDateTime d, ZoneId z) { + return d.withOffsetSameInstant(z.getRules().getOffset(d.toLocalDateTime())); + } + } + ); + + public static final CustomInstantDeserializer ZONED_DATE_TIME = new CustomInstantDeserializer( + ZonedDateTime.class, DateTimeFormatter.ISO_ZONED_DATE_TIME, + new Function() { + @Override + public ZonedDateTime apply(TemporalAccessor temporalAccessor) { + return ZonedDateTime.from(temporalAccessor); + } + }, + new Function() { + @Override + public ZonedDateTime apply(FromIntegerArguments a) { + return ZonedDateTime.ofInstant(Instant.ofEpochMilli(a.value), a.zoneId); + } + }, + new Function() { + @Override + public ZonedDateTime apply(FromDecimalArguments a) { + return ZonedDateTime.ofInstant(Instant.ofEpochSecond(a.integer, a.fraction), a.zoneId); + } + }, + new BiFunction() { + @Override + public ZonedDateTime apply(ZonedDateTime zonedDateTime, ZoneId zoneId) { + return zonedDateTime.withZoneSameInstant(zoneId); + } + } + ); + + protected final Function fromMilliseconds; + + protected final Function fromNanoseconds; + + protected final Function parsedToValue; + + protected final BiFunction adjust; + + protected CustomInstantDeserializer(Class supportedType, + DateTimeFormatter parser, + Function parsedToValue, + Function fromMilliseconds, + Function fromNanoseconds, + BiFunction adjust) { + super(supportedType, parser); + this.parsedToValue = parsedToValue; + this.fromMilliseconds = fromMilliseconds; + this.fromNanoseconds = fromNanoseconds; + this.adjust = adjust == null ? new BiFunction() { + @Override + public T apply(T t, ZoneId zoneId) { + return t; + } + } : adjust; + } + + @SuppressWarnings("unchecked") + protected CustomInstantDeserializer(CustomInstantDeserializer base, DateTimeFormatter f) { + super((Class) base.handledType(), f); + parsedToValue = base.parsedToValue; + fromMilliseconds = base.fromMilliseconds; + fromNanoseconds = base.fromNanoseconds; + adjust = base.adjust; + } + + @Override + protected JsonDeserializer withDateFormat(DateTimeFormatter dtf) { + if (dtf == _formatter) { + return this; + } + return new CustomInstantDeserializer(this, dtf); + } + + @Override + public T deserialize(JsonParser parser, DeserializationContext context) throws IOException { + //NOTE: Timestamps contain no timezone info, and are always in configured TZ. Only + //string values have to be adjusted to the configured TZ. + switch (parser.getCurrentTokenId()) { + case JsonTokenId.ID_NUMBER_FLOAT: { + BigDecimal value = parser.getDecimalValue(); + long seconds = value.longValue(); + int nanoseconds = DecimalUtils.extractNanosecondDecimal(value, seconds); + return fromNanoseconds.apply(new FromDecimalArguments( + seconds, nanoseconds, getZone(context))); + } + + case JsonTokenId.ID_NUMBER_INT: { + long timestamp = parser.getLongValue(); + if (context.isEnabled(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS)) { + return this.fromNanoseconds.apply(new FromDecimalArguments( + timestamp, 0, this.getZone(context) + )); + } + return this.fromMilliseconds.apply(new FromIntegerArguments( + timestamp, this.getZone(context) + )); + } + + case JsonTokenId.ID_STRING: { + String string = parser.getText().trim(); + if (string.length() == 0) { + return null; + } + if (string.endsWith("+0000")) { + string = string.substring(0, string.length() - 5) + "Z"; + } + T value; + try { + TemporalAccessor acc = _formatter.parse(string); + value = parsedToValue.apply(acc); + if (context.isEnabled(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE)) { + return adjust.apply(value, this.getZone(context)); + } + } catch (DateTimeException e) { + throw _peelDTE(e); + } + return value; + } + } + throw context.mappingException("Expected type float, integer, or string."); + } + + private ZoneId getZone(DeserializationContext context) { + // Instants are always in UTC, so don't waste compute cycles + return (_valueClass == Instant.class) ? null : DateTimeUtils.toZoneId(context.getTimeZone()); + } + + private static class FromIntegerArguments { + public final long value; + public final ZoneId zoneId; + + private FromIntegerArguments(long value, ZoneId zoneId) { + this.value = value; + this.zoneId = zoneId; + } + } + + private static class FromDecimalArguments { + public final long integer; + public final int fraction; + public final ZoneId zoneId; + + private FromDecimalArguments(long integer, int fraction, ZoneId zoneId) { + this.integer = integer; + this.fraction = fraction; + this.zoneId = zoneId; + } + } +} diff --git a/config/clients/java/template/FgaInvalidParameterException.java.mustache b/config/clients/java/template/FgaInvalidParameterException.java.mustache new file mode 100644 index 00000000..a553425f --- /dev/null +++ b/config/clients/java/template/FgaInvalidParameterException.java.mustache @@ -0,0 +1,14 @@ +package dev.openfga.sdk.errors; + +public class FgaInvalidParameterException extends Exception { + public FgaInvalidParameterException(String paramName, String functionName) { + super(message(paramName, functionName)); + } + public FgaInvalidParameterException(String paramName, String functionName, Throwable cause) { + super(message(paramName, functionName), cause); + } + + private static String message(String paramName, String functionName) { + return String.format("Required parameter %s was invalid when calling %s.", paramName, functionName); + } +} diff --git a/config/clients/java/template/JavaTimeFormatter.mustache b/config/clients/java/template/JavaTimeFormatter.mustache new file mode 100644 index 00000000..569ce80a --- /dev/null +++ b/config/clients/java/template/JavaTimeFormatter.mustache @@ -0,0 +1,52 @@ +{{>licenseInfo}} +package {{invokerPackage}}; + +import java.time.OffsetDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; + +/** + * Class that add parsing/formatting support for Java 8+ {@code OffsetDateTime} class. + * It's generated for java clients when {@code AbstractJavaCodegen#dateLibrary} specified as {@code java8}. + */ +public class JavaTimeFormatter { + + private DateTimeFormatter offsetDateTimeFormatter = DateTimeFormatter.ISO_OFFSET_DATE_TIME; + + /** + * Get the date format used to parse/format {@code OffsetDateTime} parameters. + * @return DateTimeFormatter + */ + public DateTimeFormatter getOffsetDateTimeFormatter() { + return offsetDateTimeFormatter; + } + + /** + * Set the date format used to parse/format {@code OffsetDateTime} parameters. + * @param offsetDateTimeFormatter {@code DateTimeFormatter} + */ + public void setOffsetDateTimeFormatter(DateTimeFormatter offsetDateTimeFormatter) { + this.offsetDateTimeFormatter = offsetDateTimeFormatter; + } + + /** + * Parse the given string into {@code OffsetDateTime} object. + * @param str String + * @return {@code OffsetDateTime} + */ + public OffsetDateTime parseOffsetDateTime(String str) { + try { + return OffsetDateTime.parse(str, offsetDateTimeFormatter); + } catch (DateTimeParseException e) { + throw new RuntimeException(e); + } + } + /** + * Format the given {@code OffsetDateTime} object into string. + * @param offsetDateTime {@code OffsetDateTime} + * @return {@code OffsetDateTime} in string format + */ + public String formatOffsetDateTime(OffsetDateTime offsetDateTime) { + return offsetDateTimeFormatter.format(offsetDateTime); + } +} diff --git a/config/clients/java/template/OpenFgaApiIntegrationTest.java.mustache b/config/clients/java/template/OpenFgaApiIntegrationTest.java.mustache new file mode 100644 index 00000000..f360a063 --- /dev/null +++ b/config/clients/java/template/OpenFgaApiIntegrationTest.java.mustache @@ -0,0 +1,315 @@ +{{>licenseInfo}} + +package {{apiPackage}}; + +import static org.junit.jupiter.api.Assertions.*; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import {{invokerPackage}}.*; +import {{modelPackage}}.*; +import {{apiPackage}}.configuration.*; +import dev.openfga.errors.ApiException; +import java.net.http.HttpClient; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class OpenFgaApiIntegrationTest { + private static final ObjectMapper mapper = new ObjectMapper().findAndRegisterModules(); + private static final String DEFAULT_AUTH_MODEL = + "{\"schema_version\":\"1.1\",\"type_definitions\":[{\"type\":\"user\"},{\"type\":\"document\",\"relations\":{\"reader\":{\"this\":{}},\"writer\":{\"this\":{}},\"owner\":{\"this\":{}}},\"metadata\":{\"relations\":{\"reader\":{\"directly_related_user_types\":[{\"type\":\"user\"}]},\"writer\":{\"directly_related_user_types\":[{\"type\":\"user\"}]},\"owner\":{\"directly_related_user_types\":[{\"type\":\"user\"}]}}}}]}"; + private static final String DEFAULT_USER = "user:81684243-9356-4421-8fbf-a4f8d36aa31b"; + private static final String DEFAULT_DOC = "document:2021-budget"; + public static final TupleKey DEFAULT_TUPLE_KEY = + new TupleKey().user(DEFAULT_USER).relation("reader")._object(DEFAULT_DOC); + public static final List DEFAULT_TUPLE_KEYS = List.of(DEFAULT_TUPLE_KEY); + + private OpenFgaApi api; + + @BeforeEach + public void initializeApi() { + Configuration apiConfig = new Configuration("http://localhost:8080"); + ApiClient apiClient = new ApiClient(HttpClient.newBuilder(), mapper); + api = new OpenFgaApi(apiClient, apiConfig); + } + + @Test + public void createStore() throws Exception { + // Given + String storeName = thisTestName(); + CreateStoreRequest createStoreRequest = new CreateStoreRequest().name(storeName); + + // When + CreateStoreResponse response = api.createStore(createStoreRequest).get(); + + // Then + assertEquals("OpenFgaApiIntegrationTest.createStore", response.getName()); + } + + @Test + public void deleteStore() throws Exception { + // Given + String storeName = thisTestName(); + String storeId = createStore(storeName); + + // When + api.deleteStore(storeId).get(); + + // Then + ListStoresResponse response = api.listStores(100, null).get(); + boolean itWasDeleted = response.getStores().stream().map(Store::getId).noneMatch(storeId::equals); + assertTrue(itWasDeleted, String.format("No stores should remain with the id %s.", storeId)); + } + + @Test + public void getStore() throws Exception { + // Given + String storeName = thisTestName(); + String storeId = createStore(storeName); + + // When + GetStoreResponse response = api.getStore(storeId).get(); + + // Then + assertEquals(storeName, response.getName()); + } + + @Test + public void listStores() throws Exception { + // Given + String testName = thisTestName(); + String store1 = testName + "-store1"; + String store2 = testName + "-store2"; + String store3 = testName + "-store3"; + List stores = List.of(store1, store2, store3); + for (String store : stores) { + createStore(store); + } + + // When + ListStoresResponse response = api.listStores(100, null).get(); + + // Then + for (String store : stores) { + boolean exists = response.getStores().stream().map(Store::getName).anyMatch(store::equals); + assertTrue(exists, String.format("Store %s should be in listStores response", store)); + } + } + + @Test + public void readAuthModel() throws Exception { + // Given + String storeName = thisTestName(); + String storeId = createStore(storeName); + String authModelId = writeAuthModel(storeId); + + // When + ReadAuthorizationModelResponse response = api.readAuthorizationModel(storeId, authModelId).get(); + + // Then + AuthorizationModel authModel = response.getAuthorizationModel(); + assertEquals(authModelId, authModel.getId()); + String typeDefsJson = mapper.writeValueAsString(authModel.getTypeDefinitions()); + assertEquals( + "[{\"type\":\"user\",\"relations\":{},\"metadata\":null},{\"type\":\"document\",\"relations\":{\"owner\":{\"this\":{},\"computedUserset\":null,\"tupleToUserset\":null,\"union\":null,\"intersection\":null,\"difference\":null},\"reader\":{\"this\":{},\"computedUserset\":null,\"tupleToUserset\":null,\"union\":null,\"intersection\":null,\"difference\":null},\"writer\":{\"this\":{},\"computedUserset\":null,\"tupleToUserset\":null,\"union\":null,\"intersection\":null,\"difference\":null}},\"metadata\":{\"relations\":{\"owner\":{\"directly_related_user_types\":[{\"type\":\"user\",\"relation\":null,\"wildcard\":null}]},\"reader\":{\"directly_related_user_types\":[{\"type\":\"user\",\"relation\":null,\"wildcard\":null}]},\"writer\":{\"directly_related_user_types\":[{\"type\":\"user\",\"relation\":null,\"wildcard\":null}]}}}}]", + typeDefsJson); + } + + @Test + public void readAuthModels() throws Exception { + // Given + String storeName = thisTestName(); + String storeId = createStore(storeName); + String authModelId = writeAuthModel(storeId); + + // When + ReadAuthorizationModelsResponse response = api.readAuthorizationModels(storeId, 100, null).get(); + + // Then + response.getAuthorizationModels().stream() + .filter(authModel -> authModel.getId().equals(authModelId)) + .forEach(authModel -> { + assertEquals(authModelId, authModel.getId()); + try { + String typeDefsJson = mapper.writeValueAsString(authModel.getTypeDefinitions()); + + assertEquals( + "[{\"type\":\"user\",\"relations\":{},\"metadata\":null},{\"type\":\"document\",\"relations\":{\"owner\":{\"this\":{},\"computedUserset\":null,\"tupleToUserset\":null,\"union\":null,\"intersection\":null,\"difference\":null},\"reader\":{\"this\":{},\"computedUserset\":null,\"tupleToUserset\":null,\"union\":null,\"intersection\":null,\"difference\":null},\"writer\":{\"this\":{},\"computedUserset\":null,\"tupleToUserset\":null,\"union\":null,\"intersection\":null,\"difference\":null}},\"metadata\":{\"relations\":{\"owner\":{\"directly_related_user_types\":[{\"type\":\"user\",\"relation\":null,\"wildcard\":null}]},\"reader\":{\"directly_related_user_types\":[{\"type\":\"user\",\"relation\":null,\"wildcard\":null}]},\"writer\":{\"directly_related_user_types\":[{\"type\":\"user\",\"relation\":null,\"wildcard\":null}]}}}}]", + typeDefsJson); + } catch (JsonProcessingException ex) { + assertNull(ex); + } + }); + } + + @Test + public void writeAuthModel() throws Exception { + // Given + String storeName = thisTestName(); + String storeId = createStore(storeName); + WriteAuthorizationModelRequest request = + mapper.readValue(DEFAULT_AUTH_MODEL, WriteAuthorizationModelRequest.class); + + // When + WriteAuthorizationModelResponse response = api.writeAuthorizationModel(storeId, request).get(); + + // Then + assertNotNull(response); + assertNotNull(response.getAuthorizationModelId()); + assertNotEquals("", response.getAuthorizationModelId()); + } + + @Test + public void write_and_read() throws Exception { + // Given + String storeName = thisTestName(); + String storeId = createStore(storeName); + String _authModelId = writeAuthModel(storeId); + WriteRequest writeRequest = new WriteRequest().writes(new TupleKeys().tupleKeys(List.of(DEFAULT_TUPLE_KEY))); + ReadRequest readRequest = + new ReadRequest().tupleKey(new TupleKey().user(DEFAULT_USER)._object(DEFAULT_DOC)); + + // When + api.write(storeId, writeRequest).get(); + ReadResponse response = api.read(storeId, readRequest).get(); + + // Then + TupleKey key = response.getTuples().get(0).getKey(); + assertEquals(DEFAULT_USER, key.getUser()); + assertEquals("reader", key.getRelation()); + assertEquals(DEFAULT_DOC, key.getObject()); + } + + @Test + public void write_and_check() throws Exception { + // Given + String storeName = thisTestName(); + String storeId = createStore(storeName); + String _authModelId = writeAuthModel(storeId); + WriteRequest writeRequest = new WriteRequest().writes(new TupleKeys().tupleKeys(DEFAULT_TUPLE_KEYS)); + CheckRequest checkRequest = new CheckRequest() + .tupleKey(new TupleKey().user(DEFAULT_USER).relation("reader")._object(DEFAULT_DOC)); + + // When + api.write(storeId, writeRequest).get(); + CheckResponse response = api.check(storeId, checkRequest).get(); + + // Then + assertTrue(response.getAllowed()); + } + + @Test + public void write_and_expand() throws Exception { + // Given + String storeName = thisTestName(); + String storeId = createStore(storeName); + String _authModelId = writeAuthModel(storeId); + WriteRequest writeRequest = new WriteRequest().writes(new TupleKeys().tupleKeys(DEFAULT_TUPLE_KEYS)); + ExpandRequest expandRequest = + new ExpandRequest().tupleKey(new TupleKey()._object(DEFAULT_DOC).relation("reader")); + + // When + api.write(storeId, writeRequest).get(); + ExpandResponse response = api.expand(storeId, expandRequest).get(); + + // Then + assertNotNull(response.getTree()); + String responseJson = mapper.writeValueAsString(response); + assertEquals( + "{\"tree\":{\"root\":{\"name\":\"document:2021-budget#reader\",\"leaf\":{\"users\":{\"users\":[\"user:81684243-9356-4421-8fbf-a4f8d36aa31b\"]},\"computed\":null,\"tupleToUserset\":null},\"difference\":null,\"union\":null,\"intersection\":null}}}", + responseJson); + } + + @Test + public void write_and_listObjects() throws Exception { + // Given + String storeName = thisTestName(); + String storeId = createStore(storeName); + String _authModelId = writeAuthModel(storeId); + WriteRequest writeRequest = new WriteRequest().writes(new TupleKeys().tupleKeys(DEFAULT_TUPLE_KEYS)); + ListObjectsRequest listObjectsRequest = + new ListObjectsRequest().user(DEFAULT_USER).relation("reader").type("document"); + + // When + api.write(storeId, writeRequest).get(); + ListObjectsResponse response = api.listObjects(storeId, listObjectsRequest).get(); + + // Then + assertEquals(1, response.getObjects().size()); + assertEquals(DEFAULT_DOC, response.getObjects().get(0)); + } + + @Test + public void write_and_readChanges() throws Exception { + // Given + String storeName = thisTestName(); + String storeId = createStore(storeName); + String _authModelId = writeAuthModel(storeId); + WriteRequest writeRequest = new WriteRequest().writes(new TupleKeys().tupleKeys(DEFAULT_TUPLE_KEYS)); + + // When + api.write(storeId, writeRequest).get(); + ReadChangesResponse response = api.readChanges(storeId, null, null, null).get(); + + // Then + assertEquals(1, response.getChanges().size()); + String firstTupleKeyJson = + mapper.writeValueAsString(response.getChanges().get(0).getTupleKey()); + assertEquals( + "{\"object\":\"document:2021-budget\",\"relation\":\"reader\",\"user\":\"user:81684243-9356-4421-8fbf-a4f8d36aa31b\"}", + firstTupleKeyJson); + } + + @Test + public void write_readAssertions() throws Exception { + // Given + String storeName = thisTestName(); + String storeId = createStore(storeName); + String authModelId = writeAuthModel(storeId); + WriteAssertionsRequest writeRequest = new WriteAssertionsRequest() + .assertions(List.of(new Assertion().tupleKey(DEFAULT_TUPLE_KEY).expectation(true))); + + // When + api.writeAssertions(storeId, authModelId, writeRequest).get(); + ReadAssertionsResponse response = api.readAssertions(storeId, authModelId).get(); + + // Then + String responseJson = mapper.writeValueAsString(response.getAssertions()); + assertEquals( + "[{\"tuple_key\":{\"object\":\"document:2021-budget\",\"relation\":\"reader\",\"user\":\"user:81684243-9356-4421-8fbf-a4f8d36aa31b\"},\"expectation\":true}]", + responseJson); + } + + /** + * Create a store for a given name. If tests fail here, troubleshoot with the no-arguments + * test method createStore(). + * @return The created Store ID + */ + private String createStore(String storeName) throws Exception { + CreateStoreResponse response = api.createStore(new CreateStoreRequest().name(storeName)).get(); + return response.getId(); + } + + /** + * Add a default authorization model to a store. If tests fail here, troubleshoot with the + * no-arguments @Test writeAuthModel() method. + * @return The created Authorization Model ID + */ + private String writeAuthModel(String storeId) throws Exception { + WriteAuthorizationModelRequest request = + mapper.readValue(DEFAULT_AUTH_MODEL, WriteAuthorizationModelRequest.class); + WriteAuthorizationModelResponse response = api.writeAuthorizationModel(storeId, request).get(); + return response.getAuthorizationModelId(); + } + + /** Get the name of the test that invokes this function. Returned in the form: "$class.$fn" */ + private String thisTestName() { + // Tracing the stack gives an array of: + // 0: getStackTrace(), 1: getThisFunctionName(), 2: , 3: ... + StackTraceElement callingFn = Thread.currentThread().getStackTrace()[2]; + String callingClass = callingFn.getClassName().replace("dev.openfga.sdk.api.", ""); + + return String.format("%s.%s", callingClass, callingFn.getMethodName()); + } +} diff --git a/config/clients/java/template/OpenFgaApiTest.java.mustache b/config/clients/java/template/OpenFgaApiTest.java.mustache new file mode 100644 index 00000000..6636997b --- /dev/null +++ b/config/clients/java/template/OpenFgaApiTest.java.mustache @@ -0,0 +1,1753 @@ +{{>licenseInfo}} + +package {{apiPackage}}; + +import static org.hamcrest.Matchers.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.pgssoft.httpclient.HttpClientMock; +import {{invokerPackage}}.*; +import {{modelPackage}}.*; +import {{apiPackage}}.configuration.*; +import dev.openfga.sdk.errors.*; +import java.time.Duration; +import java.util.List; +import java.util.concurrent.ExecutionException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * API tests for OpenFgaApi + */ +public class OpenFgaApiTest { + private static final String DEFAULT_STORE_ID = "01YCP46JKYM8FJCQ37NMBYHE5X"; + private static final String DEFAULT_STORE_NAME = "test_store"; + private static final String DEFAULT_AUTH_MODEL_ID = "01G5JAVJ41T49E9TT3SKVS7X1J"; + private static final String DEFAULT_USER = "user:81684243-9356-4421-8fbf-a4f8d36aa31b"; + private static final String DEFAULT_RELATION = "reader"; + private static final String DEFAULT_TYPE = "document"; + private static final String DEFAULT_OBJECT = "document:budget"; + private static final String DEFAULT_SCHEMA_VERSION = "1.1"; + public static final String EMPTY_RESPONSE_BODY = "{}"; + + private final ObjectMapper mapper = new ObjectMapper(); + private OpenFgaApi fga; + private Configuration mockConfiguration; + private ApiClient mockApiClient; + private HttpClientMock mockHttpClient; + + @BeforeEach + public void beforeEachTest() { + mockHttpClient = new HttpClientMock(); + + mockConfiguration = mock(Configuration.class); + when(mockConfiguration.getApiUrl()).thenReturn("https://localhost"); + when(mockConfiguration.getReadTimeout()).thenReturn(Duration.ofMillis(250)); + + mockApiClient = mock(ApiClient.class); + when(mockApiClient.getObjectMapper()).thenReturn(mapper); + when(mockApiClient.getHttpClient()).thenReturn(mockHttpClient); + + fga = new OpenFgaApi(mockApiClient, mockConfiguration); + } + + /** + * List all stores. + */ + @Test + public void listStoresTest() throws Exception { + // Given + String responseBody = + String.format("{\"stores\":[{\"id\":\"%s\",\"name\":\"%s\"}]}", DEFAULT_STORE_ID, DEFAULT_STORE_NAME); + mockHttpClient.onGet("https://localhost/stores").doReturn(200, responseBody); + Integer pageSize = null; // Input is optional + String continuationToken = null; // Input is optional + + // When + ListStoresResponse response = + fga.listStores(pageSize, continuationToken).get(); + + // Then + mockHttpClient.verify().get("https://localhost/stores").called(1); + assertNotNull(response.getStores()); + assertEquals(1, response.getStores().size()); + assertEquals(DEFAULT_STORE_ID, response.getStores().get(0).getId()); + assertEquals(DEFAULT_STORE_NAME, response.getStores().get(0).getName()); + } + + @Test + public void listStores_400() { + // Given + mockHttpClient + .onGet("https://localhost/stores") + .doReturn(400, "{\"code\":\"validation_error\",\"message\":\"Generic validation error\"}"); + Integer pageSize = null; // Input is optional + String continuationToken = null; // Input is optional + + // When + ExecutionException execException = + assertThrows(ExecutionException.class, () -> fga.listStores(pageSize, continuationToken) + .get()); + + // Then + mockHttpClient.verify().get("https://localhost/stores").called(1); + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals(400, exception.getCode()); + assertEquals( + "{\"code\":\"validation_error\",\"message\":\"Generic validation error\"}", + exception.getResponseBody()); + } + + @Test + public void listStores_404() throws Exception { + // Given + mockHttpClient + .onGet("https://localhost/stores") + .doReturn(404, "{\"code\":\"undefined_endpoint\",\"message\":\"Endpoint not enabled\"}"); + Integer pageSize = null; // Input is optional + String continuationToken = null; // Input is optional + + // When + ExecutionException execException = + assertThrows(ExecutionException.class, () -> fga.listStores(pageSize, continuationToken) + .get()); + + // Then + mockHttpClient.verify().get("https://localhost/stores").called(1); + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals(404, exception.getCode()); + assertEquals( + "{\"code\":\"undefined_endpoint\",\"message\":\"Endpoint not enabled\"}", exception.getResponseBody()); + } + + @Test + public void listStores_500() throws Exception { + // Given + mockHttpClient + .onGet("https://localhost/stores") + .doReturn(500, "{\"code\":\"internal_error\",\"message\":\"Internal Server Error\"}"); + Integer pageSize = null; // Input is optional + String continuationToken = null; // Input is optional + + // When + ExecutionException execException = + assertThrows(ExecutionException.class, () -> fga.listStores(pageSize, continuationToken) + .get()); + + // Then + mockHttpClient.verify().get("https://localhost/stores").called(1); + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals(500, exception.getCode()); + assertEquals( + "{\"code\":\"internal_error\",\"message\":\"Internal Server Error\"}", exception.getResponseBody()); + } + + /** + * Create a store. + */ + @Test + public void createStoreTest() throws Exception { + // Given + String expectedBody = String.format("{\"name\":\"%s\"}", DEFAULT_STORE_NAME); + String requestBody = String.format("{\"id\":\"%s\",\"name\":\"%s\"}", DEFAULT_STORE_ID, DEFAULT_STORE_NAME); + mockHttpClient + .onPost("https://localhost/stores") + .withBody(is(expectedBody)) + .doReturn(201, requestBody); + CreateStoreRequest request = new CreateStoreRequest().name(DEFAULT_STORE_NAME); + + // When + CreateStoreResponse response = fga.createStore(request).get(); + + // Then + mockHttpClient + .verify() + .post("https://localhost/stores") + .withBody(is(expectedBody)) + .called(1); + assertEquals(DEFAULT_STORE_ID, response.getId()); + assertEquals(DEFAULT_STORE_NAME, response.getName()); + } + + @Test + public void createStore_bodyRequired() { + // When + ExecutionException execException = assertThrows( + ExecutionException.class, () -> fga.createStore(null).get()); + + // Then + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals("Missing the required parameter 'body' when calling createStore", exception.getMessage()); + } + + @Test + public void createStore_400() throws Exception { + // Given + mockHttpClient + .onPost("https://localhost/stores") + .doReturn(400, "{\"code\":\"validation_error\",\"message\":\"Generic validation error\"}"); + + // When + ExecutionException execException = + assertThrows(ExecutionException.class, () -> fga.createStore(new CreateStoreRequest()) + .get()); + + // Then + mockHttpClient.verify().post("https://localhost/stores").called(1); + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals(400, exception.getCode()); + assertEquals( + "{\"code\":\"validation_error\",\"message\":\"Generic validation error\"}", + exception.getResponseBody()); + } + + @Test + public void createStore_404() throws Exception { + // Given + mockHttpClient + .onPost("https://localhost/stores") + .doReturn(404, "{\"code\":\"undefined_endpoint\",\"message\":\"Endpoint not enabled\"}"); + + // When + ExecutionException execException = + assertThrows(ExecutionException.class, () -> fga.createStore(new CreateStoreRequest()) + .get()); + + // Then + mockHttpClient.verify().post("https://localhost/stores").called(1); + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals(404, exception.getCode()); + assertEquals( + "{\"code\":\"undefined_endpoint\",\"message\":\"Endpoint not enabled\"}", exception.getResponseBody()); + } + + @Test + public void createStore_500() throws Exception { + // Given + mockHttpClient + .onPost("https://localhost/stores") + .doReturn(500, "{\"code\":\"internal_error\",\"message\":\"Internal Server Error\"}"); + + // When + ExecutionException execException = + assertThrows(ExecutionException.class, () -> fga.createStore(new CreateStoreRequest()) + .get()); + + // Then + mockHttpClient.verify().post("https://localhost/stores").called(1); + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals(500, exception.getCode()); + assertEquals( + "{\"code\":\"internal_error\",\"message\":\"Internal Server Error\"}", exception.getResponseBody()); + } + + /** + * Get a store. + */ + @Test + public void getStoreTest() throws Exception { + // Given + String getUrl = "https://localhost/stores/01YCP46JKYM8FJCQ37NMBYHE5X"; + String responseBody = String.format("{\"id\":\"%s\",\"name\":\"%s\"}", DEFAULT_STORE_ID, DEFAULT_STORE_NAME); + mockHttpClient.onGet(getUrl).doReturn(200, responseBody); + + // When + GetStoreResponse response = fga.getStore(DEFAULT_STORE_ID).get(); + + // Then + mockHttpClient.verify().get(getUrl).called(1); + assertEquals(DEFAULT_STORE_ID, response.getId()); + assertEquals(DEFAULT_STORE_NAME, response.getName()); + } + + @Test + public void getStore_storeIdRequired() { + // When + ExecutionException execException = + assertThrows(ExecutionException.class, () -> fga.getStore(null).get()); + + // Then + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals("Missing the required parameter 'storeId' when calling getStore", exception.getMessage()); + } + + @Test + public void getStore_400() throws Exception { + // Given + String getUrl = "https://localhost/stores/01YCP46JKYM8FJCQ37NMBYHE5X"; + mockHttpClient + .onGet(getUrl) + .doReturn(400, "{\"code\":\"validation_error\",\"message\":\"Generic validation error\"}"); + + // When + ExecutionException execException = assertThrows( + ExecutionException.class, () -> fga.getStore(DEFAULT_STORE_ID).get()); + + // Then + mockHttpClient.verify().get(getUrl).called(1); + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals(400, exception.getCode()); + assertEquals( + "{\"code\":\"validation_error\",\"message\":\"Generic validation error\"}", + exception.getResponseBody()); + } + + @Test + public void getStore_404() throws Exception { + // Given + String getUrl = "https://localhost/stores/01YCP46JKYM8FJCQ37NMBYHE5X"; + mockHttpClient + .onGet(getUrl) + .doReturn(404, "{\"code\":\"undefined_endpoint\",\"message\":\"Endpoint not enabled\"}"); + + // When + ExecutionException execException = assertThrows( + ExecutionException.class, () -> fga.getStore(DEFAULT_STORE_ID).get()); + + // Then + mockHttpClient.verify().get(getUrl).called(1); + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals(404, exception.getCode()); + assertEquals( + "{\"code\":\"undefined_endpoint\",\"message\":\"Endpoint not enabled\"}", exception.getResponseBody()); + } + + @Test + public void getStore_500() throws Exception { + // Given + String getUrl = "https://localhost/stores/01YCP46JKYM8FJCQ37NMBYHE5X"; + mockHttpClient + .onGet(getUrl) + .doReturn(500, "{\"code\":\"internal_error\",\"message\":\"Internal Server Error\"}"); + + // When + ExecutionException execException = assertThrows( + ExecutionException.class, () -> fga.getStore(DEFAULT_STORE_ID).get()); + + // Then + mockHttpClient.verify().get(getUrl).called(1); + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals(500, exception.getCode()); + assertEquals( + "{\"code\":\"internal_error\",\"message\":\"Internal Server Error\"}", exception.getResponseBody()); + } + + /** + * Delete a store. + */ + @Test + public void deleteStoreTest() throws Exception { + // Given + String deleteUrl = "https://localhost/stores/01YCP46JKYM8FJCQ37NMBYHE5X"; + mockHttpClient.onDelete(deleteUrl).doReturn(204, EMPTY_RESPONSE_BODY); + + // When + fga.deleteStore(DEFAULT_STORE_ID); + + // Then + mockHttpClient.verify().delete(deleteUrl).called(1); + } + + @Test + public void deleteStore_storeIdRequired() { + // When + ExecutionException execException = assertThrows( + ExecutionException.class, () -> fga.deleteStore(null).get()); + + // Then + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals("Missing the required parameter 'storeId' when calling deleteStore", exception.getMessage()); + } + + @Test + public void deleteStore_400() throws Exception { + // Given + String deleteUrl = "https://localhost/stores/01YCP46JKYM8FJCQ37NMBYHE5X"; + mockHttpClient + .onDelete(deleteUrl) + .doReturn(400, "{\"code\":\"validation_error\",\"message\":\"Generic validation error\"}"); + + // When + ExecutionException execException = + assertThrows(ExecutionException.class, () -> fga.deleteStore(DEFAULT_STORE_ID) + .get()); + + // Then + mockHttpClient.verify().delete(deleteUrl).called(1); + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals(400, exception.getCode()); + assertEquals( + "{\"code\":\"validation_error\",\"message\":\"Generic validation error\"}", + exception.getResponseBody()); + } + + @Test + public void deleteStore_404() throws Exception { + // Given + String deleteUrl = "https://localhost/stores/01YCP46JKYM8FJCQ37NMBYHE5X"; + mockHttpClient + .onDelete(deleteUrl) + .doReturn(404, "{\"code\":\"undefined_endpoint\",\"message\":\"Endpoint not enabled\"}"); + + // When + ExecutionException execException = + assertThrows(ExecutionException.class, () -> fga.deleteStore(DEFAULT_STORE_ID) + .get()); + + // Then + mockHttpClient.verify().delete(deleteUrl).called(1); + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals(404, exception.getCode()); + assertEquals( + "{\"code\":\"undefined_endpoint\",\"message\":\"Endpoint not enabled\"}", exception.getResponseBody()); + } + + @Test + public void deleteStore_500() throws Exception { + // Given + String deleteUrl = "https://localhost/stores/01YCP46JKYM8FJCQ37NMBYHE5X"; + mockHttpClient + .onDelete(deleteUrl) + .doReturn(500, "{\"code\":\"internal_error\",\"message\":\"Internal Server Error\"}"); + + // When + ExecutionException execException = + assertThrows(ExecutionException.class, () -> fga.deleteStore(DEFAULT_STORE_ID) + .get()); + + // Then + mockHttpClient.verify().delete(deleteUrl).called(1); + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals(500, exception.getCode()); + assertEquals( + "{\"code\":\"internal_error\",\"message\":\"Internal Server Error\"}", exception.getResponseBody()); + } + + /** + * Return all the authorization models for a particular store. + */ + @Test + public void readAuthorizationModelsTest() throws Exception { + // Given + String getUrl = "https://localhost/stores/01YCP46JKYM8FJCQ37NMBYHE5X/authorization-models"; + String responseBody = String.format( + "{\"authorization_models\":[{\"id\":\"%s\",\"schema_version\":\"%s\"}]}", + DEFAULT_AUTH_MODEL_ID, DEFAULT_SCHEMA_VERSION); + mockHttpClient.onGet(getUrl).doReturn(200, responseBody); + Integer pageSize = null; // Input is optional + String continuationToken = null; // Input is optional + + // When + ReadAuthorizationModelsResponse response = fga.readAuthorizationModels( + DEFAULT_STORE_ID, pageSize, continuationToken) + .get(); + + // Then + mockHttpClient.verify().get(getUrl).called(1); + assertNotNull(response.getAuthorizationModels()); + assertEquals(1, response.getAuthorizationModels().size()); + AuthorizationModel authModel = response.getAuthorizationModels().get(0); + assertEquals(DEFAULT_AUTH_MODEL_ID, authModel.getId()); + assertEquals(DEFAULT_SCHEMA_VERSION, authModel.getSchemaVersion()); + } + + @Test + public void readAuthorizationModels_storeIdRequired() { + // When + ExecutionException execException = + assertThrows(ExecutionException.class, () -> fga.readAuthorizationModels(null, null, null) + .get()); + + // Then + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals( + "Missing the required parameter 'storeId' when calling readAuthorizationModels", + exception.getMessage()); + } + + @Test + public void readAuthorizationModels_400() throws Exception { + // Given + String getUrl = "https://localhost/stores/01YCP46JKYM8FJCQ37NMBYHE5X/authorization-models"; + mockHttpClient + .onGet(getUrl) + .doReturn(400, "{\"code\":\"validation_error\",\"message\":\"Generic validation error\"}"); + Integer pageSize = null; // Input is optional + String continuationToken = null; // Input is optional + + // When + ExecutionException execException = assertThrows(ExecutionException.class, () -> fga.readAuthorizationModels( + DEFAULT_STORE_ID, pageSize, continuationToken) + .get()); + + // Then + mockHttpClient.verify().get(getUrl).called(1); + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals(400, exception.getCode()); + assertEquals( + "{\"code\":\"validation_error\",\"message\":\"Generic validation error\"}", + exception.getResponseBody()); + } + + @Test + public void readAuthorizationModels_404() throws Exception { + // Given + String getUrl = "https://localhost/stores/01YCP46JKYM8FJCQ37NMBYHE5X/authorization-models"; + mockHttpClient + .onGet(getUrl) + .doReturn(404, "{\"code\":\"undefined_endpoint\",\"message\":\"Endpoint not enabled\"}"); + Integer pageSize = null; // Input is optional + String continuationToken = null; // Input is optional + + // When + ExecutionException execException = assertThrows(ExecutionException.class, () -> fga.readAuthorizationModels( + DEFAULT_STORE_ID, pageSize, continuationToken) + .get()); + + // Then + mockHttpClient.verify().get(getUrl).called(1); + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals(404, exception.getCode()); + assertEquals( + "{\"code\":\"undefined_endpoint\",\"message\":\"Endpoint not enabled\"}", exception.getResponseBody()); + } + + @Test + public void readAuthorizationModels_500() throws Exception { + // Given + String getUrl = "https://localhost/stores/01YCP46JKYM8FJCQ37NMBYHE5X/authorization-models"; + mockHttpClient + .onGet(getUrl) + .doReturn(500, "{\"code\":\"internal_error\",\"message\":\"Internal Server Error\"}"); + Integer pageSize = null; // Input is optional + String continuationToken = null; // Input is optional + + // When + ExecutionException execException = assertThrows(ExecutionException.class, () -> fga.readAuthorizationModels( + DEFAULT_STORE_ID, pageSize, continuationToken) + .get()); + + // Then + mockHttpClient.verify().get(getUrl).called(1); + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals(500, exception.getCode()); + assertEquals( + "{\"code\":\"internal_error\",\"message\":\"Internal Server Error\"}", exception.getResponseBody()); + } + + /** + * Create a new authorization model. + */ + @Test + public void writeAuthorizationModelTest() throws Exception { + // Given + String postUrl = "https://localhost/stores/01YCP46JKYM8FJCQ37NMBYHE5X/authorization-models"; + String expectedBody = + "{\"type_definitions\":[{\"type\":\"document\",\"relations\":{},\"metadata\":null}],\"schema_version\":\"1.1\"}"; + String responseBody = String.format("{\"authorization_model_id\":\"%s\"}", DEFAULT_AUTH_MODEL_ID); + mockHttpClient.onPost(postUrl).withBody(is(expectedBody)).doReturn(201, responseBody); + WriteAuthorizationModelRequest request = new WriteAuthorizationModelRequest() + .schemaVersion(DEFAULT_SCHEMA_VERSION) + .typeDefinitions(List.of(new TypeDefinition().type(DEFAULT_TYPE))); + + // When + WriteAuthorizationModelResponse response = + fga.writeAuthorizationModel(DEFAULT_STORE_ID, request).get(); + + // Then + mockHttpClient.verify().post(postUrl).withBody(is(expectedBody)).called(1); + assertEquals(DEFAULT_AUTH_MODEL_ID, response.getAuthorizationModelId()); + } + + @Test + public void writeAuthorizationModel_storeIdRequired() { + // When + ExecutionException execException = assertThrows( + ExecutionException.class, () -> fga.writeAuthorizationModel(null, new WriteAuthorizationModelRequest()) + .get()); + + // Then + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals( + "Missing the required parameter 'storeId' when calling writeAuthorizationModel", + exception.getMessage()); + } + + @Test + public void writeAuthorizationModel_bodyRequired() { + // When + ExecutionException execException = + assertThrows(ExecutionException.class, () -> fga.writeAuthorizationModel(DEFAULT_STORE_ID, null) + .get()); + + // Then + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals( + "Missing the required parameter 'body' when calling writeAuthorizationModel", exception.getMessage()); + } + + @Test + public void writeAuthorizationModel_400() throws Exception { + // Given + String postUrl = "https://localhost/stores/01YCP46JKYM8FJCQ37NMBYHE5X/authorization-models"; + mockHttpClient + .onPost(postUrl) + .doReturn(400, "{\"code\":\"validation_error\",\"message\":\"Generic validation error\"}"); + + // When + ExecutionException execException = assertThrows(ExecutionException.class, () -> fga.writeAuthorizationModel( + DEFAULT_STORE_ID, new WriteAuthorizationModelRequest()) + .get()); + + // Then + mockHttpClient.verify().post(postUrl).called(1); + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals(400, exception.getCode()); + assertEquals( + "{\"code\":\"validation_error\",\"message\":\"Generic validation error\"}", + exception.getResponseBody()); + } + + @Test + public void writeAuthorizationModel_404() throws Exception { + // Given + String postUrl = "https://localhost/stores/01YCP46JKYM8FJCQ37NMBYHE5X/authorization-models"; + mockHttpClient + .onPost(postUrl) + .doReturn(404, "{\"code\":\"undefined_endpoint\",\"message\":\"Endpoint not enabled\"}"); + + // When + ExecutionException execException = assertThrows(ExecutionException.class, () -> fga.writeAuthorizationModel( + DEFAULT_STORE_ID, new WriteAuthorizationModelRequest()) + .get()); + + // Then + mockHttpClient.verify().post(postUrl).called(1); + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals(404, exception.getCode()); + assertEquals( + "{\"code\":\"undefined_endpoint\",\"message\":\"Endpoint not enabled\"}", exception.getResponseBody()); + } + + @Test + public void writeAuthorizationModel_500() throws Exception { + // Given + String postUrl = "https://localhost/stores/01YCP46JKYM8FJCQ37NMBYHE5X/authorization-models"; + mockHttpClient + .onPost(postUrl) + .doReturn(500, "{\"code\":\"internal_error\",\"message\":\"Internal Server Error\"}"); + + // When + ExecutionException execException = assertThrows(ExecutionException.class, () -> fga.writeAuthorizationModel( + DEFAULT_STORE_ID, new WriteAuthorizationModelRequest()) + .get()); + + // Then + mockHttpClient.verify().post(postUrl).called(1); + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals(500, exception.getCode()); + assertEquals( + "{\"code\":\"internal_error\",\"message\":\"Internal Server Error\"}", exception.getResponseBody()); + } + + /** + * Return a particular version of an authorization model. + */ + @Test + public void readAuthorizationModelTest() throws Exception { + // Given + String getUrl = + "https://localhost/stores/01YCP46JKYM8FJCQ37NMBYHE5X/authorization-models/01G5JAVJ41T49E9TT3SKVS7X1J"; + String getResponse = String.format( + "{\"authorization_model\":{\"id\":\"%s\",\"schema_version\":\"%s\"}}", + DEFAULT_AUTH_MODEL_ID, DEFAULT_SCHEMA_VERSION); + mockHttpClient.onGet(getUrl).doReturn(200, getResponse); + + // When + ReadAuthorizationModelResponse response = fga.readAuthorizationModel(DEFAULT_STORE_ID, DEFAULT_AUTH_MODEL_ID) + .get(); + + // Then + mockHttpClient.verify().get(getUrl).called(1); + assertNotNull(response.getAuthorizationModel()); + assertEquals(DEFAULT_AUTH_MODEL_ID, response.getAuthorizationModel().getId()); + assertEquals(DEFAULT_SCHEMA_VERSION, response.getAuthorizationModel().getSchemaVersion()); + } + + @Test + public void readAuthorizationModel_storeIdRequired() { + // When + ExecutionException execException = + assertThrows(ExecutionException.class, () -> fga.readAuthorizationModel(null, DEFAULT_AUTH_MODEL_ID) + .get()); + + // Then + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals( + "Missing the required parameter 'storeId' when calling readAuthorizationModel", exception.getMessage()); + } + + @Test + public void readAuthorizationModel_idRequired() { + // When + ExecutionException execException = + assertThrows(ExecutionException.class, () -> fga.readAuthorizationModel(DEFAULT_STORE_ID, null) + .get()); + + // Then + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals("Missing the required parameter 'id' when calling readAuthorizationModel", exception.getMessage()); + } + + @Test + public void readAuthorizationModel_400() throws Exception { + // Given + String getUrl = + "https://localhost/stores/01YCP46JKYM8FJCQ37NMBYHE5X/authorization-models/01G5JAVJ41T49E9TT3SKVS7X1J"; + mockHttpClient + .onGet(getUrl) + .doReturn(400, "{\"code\":\"validation_error\",\"message\":\"Generic validation error\"}"); + + // When + ExecutionException execException = assertThrows( + ExecutionException.class, () -> fga.readAuthorizationModel(DEFAULT_STORE_ID, DEFAULT_AUTH_MODEL_ID) + .get()); + + // Then + mockHttpClient.verify().get(getUrl).called(1); + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals(400, exception.getCode()); + assertEquals( + "{\"code\":\"validation_error\",\"message\":\"Generic validation error\"}", + exception.getResponseBody()); + } + + @Test + public void readAuthorizationModel_404() throws Exception { + // Given + String getUrl = + "https://localhost/stores/01YCP46JKYM8FJCQ37NMBYHE5X/authorization-models/01G5JAVJ41T49E9TT3SKVS7X1J"; + mockHttpClient + .onGet(getUrl) + .doReturn(404, "{\"code\":\"undefined_endpoint\",\"message\":\"Endpoint not enabled\"}"); + + // When + ExecutionException execException = assertThrows( + ExecutionException.class, () -> fga.readAuthorizationModel(DEFAULT_STORE_ID, DEFAULT_AUTH_MODEL_ID) + .get()); + + // Then + mockHttpClient.verify().get(getUrl).called(1); + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals(404, exception.getCode()); + assertEquals( + "{\"code\":\"undefined_endpoint\",\"message\":\"Endpoint not enabled\"}", exception.getResponseBody()); + } + + @Test + public void readAuthorizationModel_500() throws Exception { + // Given + String getUrl = + "https://localhost/stores/01YCP46JKYM8FJCQ37NMBYHE5X/authorization-models/01G5JAVJ41T49E9TT3SKVS7X1J"; + mockHttpClient + .onGet(getUrl) + .doReturn(500, "{\"code\":\"internal_error\",\"message\":\"Internal Server Error\"}"); + + // When + ExecutionException execException = assertThrows( + ExecutionException.class, () -> fga.readAuthorizationModel(DEFAULT_STORE_ID, DEFAULT_AUTH_MODEL_ID) + .get()); + + // Then + mockHttpClient.verify().get(getUrl).called(1); + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals(500, exception.getCode()); + assertEquals( + "{\"code\":\"internal_error\",\"message\":\"Internal Server Error\"}", exception.getResponseBody()); + } + + /** + * Return a list of all the tuple changes. + */ + @Test + public void readChangesTest() throws Exception { + // Given + String getPath = "https://localhost/stores/01YCP46JKYM8FJCQ37NMBYHE5X/changes"; + String responseBody = String.format( + "{\"changes\":[{\"tuple_key\":{\"object\":\"%s\",\"relation\":\"%s\",\"user\":\"%s\"}}]}", + DEFAULT_OBJECT, DEFAULT_RELATION, DEFAULT_USER); + mockHttpClient.onGet(getPath).doReturn(200, responseBody); + String type = null; // Input is optional + Integer pageSize = null; // Input is optional + String continuationToken = null; // Input is optional + + // When + ReadChangesResponse response = fga.readChanges(DEFAULT_STORE_ID, type, pageSize, continuationToken) + .get(); + + // Then + mockHttpClient.verify().get(getPath).called(1); + assertNotNull(response.getChanges()); + assertEquals(1, response.getChanges().size()); + TupleChange change = response.getChanges().get(0); + assertNotNull(change.getTupleKey()); + assertEquals(DEFAULT_OBJECT, change.getTupleKey().getObject()); + assertEquals(DEFAULT_RELATION, change.getTupleKey().getRelation()); + assertEquals(DEFAULT_USER, change.getTupleKey().getUser()); + } + + @Test + public void readChanges_storeIdRequired() throws Exception { + // When + ExecutionException execException = + assertThrows(ExecutionException.class, () -> fga.readChanges(null, null, null, null) + .get()); + + // Then + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals("Missing the required parameter 'storeId' when calling readChanges", exception.getMessage()); + } + + @Test + public void readChanges_400() throws Exception { + // Given + String getUrl = "https://localhost/stores/01YCP46JKYM8FJCQ37NMBYHE5X/changes"; + mockHttpClient + .onGet(getUrl) + .doReturn(400, "{\"code\":\"validation_error\",\"message\":\"Generic validation error\"}"); + String type = null; // Input is optional + Integer pageSize = null; // Input is optional + String continuationToken = null; // Input is optional + + // When + ExecutionException execException = assertThrows( + ExecutionException.class, () -> fga.readChanges(DEFAULT_STORE_ID, type, pageSize, continuationToken) + .get()); + + // Then + mockHttpClient.verify().get(getUrl).called(1); + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals(400, exception.getCode()); + assertEquals( + "{\"code\":\"validation_error\",\"message\":\"Generic validation error\"}", + exception.getResponseBody()); + } + + @Test + public void readChanges_404() throws Exception { + // Given + String getUrl = "https://localhost/stores/01YCP46JKYM8FJCQ37NMBYHE5X/changes"; + mockHttpClient + .onGet(getUrl) + .doReturn(404, "{\"code\":\"undefined_endpoint\",\"message\":\"Endpoint not enabled\"}"); + String type = null; // Input is optional + Integer pageSize = null; // Input is optional + String continuationToken = null; // Input is optional + + // When + ExecutionException execException = assertThrows( + ExecutionException.class, () -> fga.readChanges(DEFAULT_STORE_ID, type, pageSize, continuationToken) + .get()); + + // Then + mockHttpClient.verify().get(getUrl).called(1); + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals(404, exception.getCode()); + assertEquals( + "{\"code\":\"undefined_endpoint\",\"message\":\"Endpoint not enabled\"}", exception.getResponseBody()); + } + + @Test + public void readChanges_500() throws Exception { + // Given + String getUrl = "https://localhost/stores/01YCP46JKYM8FJCQ37NMBYHE5X/changes"; + mockHttpClient + .onGet(getUrl) + .doReturn(500, "{\"code\":\"internal_error\",\"message\":\"Internal Server Error\"}"); + String type = null; // Input is optional + Integer pageSize = null; // Input is optional + String continuationToken = null; // Input is optional + + // When + ExecutionException execException = assertThrows( + ExecutionException.class, () -> fga.readChanges(DEFAULT_STORE_ID, type, pageSize, continuationToken) + .get()); + + // Then + mockHttpClient.verify().get(getUrl).called(1); + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals(500, exception.getCode()); + assertEquals( + "{\"code\":\"internal_error\",\"message\":\"Internal Server Error\"}", exception.getResponseBody()); + } + + /** + * Get tuples from the store that matches a query, without following userset rewrite rules. + */ + @Test + public void readTest() throws Exception { + // Given + String postUrl = "https://localhost/stores/01YCP46JKYM8FJCQ37NMBYHE5X/read"; + String expectedBody = String.format( + "{\"tuple_key\":{\"object\":\"%s\",\"relation\":\"%s\",\"user\":\"%s\"},\"page_size\":null,\"continuation_token\":null}", + DEFAULT_OBJECT, DEFAULT_RELATION, DEFAULT_USER); + String responseBody = String.format( + "{\"tuples\":[{\"key\":{\"user\":\"%s\",\"relation\":\"%s\",\"object\":\"%s\"}}]}", + DEFAULT_USER, DEFAULT_RELATION, DEFAULT_OBJECT); + mockHttpClient.onPost(postUrl).withBody(is(expectedBody)).doReturn(200, responseBody); + ReadRequest request = new ReadRequest() + .tupleKey(new TupleKey() + ._object(DEFAULT_OBJECT) + .relation(DEFAULT_RELATION) + .user(DEFAULT_USER)); + + // When + ReadResponse response = fga.read(DEFAULT_STORE_ID, request).get(); + + // Then + mockHttpClient.verify().post(postUrl).withBody(is(expectedBody)).called(1); + assertNotNull(response.getTuples()); + assertEquals(1, response.getTuples().size()); + TupleKey key = response.getTuples().get(0).getKey(); + assertNotNull(key); + assertEquals(DEFAULT_USER, key.getUser()); + assertEquals(DEFAULT_RELATION, key.getRelation()); + assertEquals(DEFAULT_OBJECT, key.getObject()); + } + + @Test + public void read_storeIdRequired() { + // When + ExecutionException execException = + assertThrows(ExecutionException.class, () -> fga.read(null, new ReadRequest()) + .get()); + + // Then + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals("Missing the required parameter 'storeId' when calling read", exception.getMessage()); + } + + @Test + public void read_bodyRequired() { + // When + ExecutionException execException = assertThrows( + ExecutionException.class, () -> fga.read(DEFAULT_STORE_ID, null).get()); + + // Then + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals("Missing the required parameter 'body' when calling read", exception.getMessage()); + } + + @Test + public void read_400() throws Exception { + // Given + String postUrl = "https://localhost/stores/01YCP46JKYM8FJCQ37NMBYHE5X/read"; + mockHttpClient + .onPost(postUrl) + .doReturn(400, "{\"code\":\"validation_error\",\"message\":\"Generic validation error\"}"); + + // When + ExecutionException execException = + assertThrows(ExecutionException.class, () -> fga.read(DEFAULT_STORE_ID, new ReadRequest()) + .get()); + + // Then + mockHttpClient.verify().post(postUrl).called(1); + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals(400, exception.getCode()); + assertEquals( + "{\"code\":\"validation_error\",\"message\":\"Generic validation error\"}", + exception.getResponseBody()); + } + + @Test + public void read_404() throws Exception { + // Given + String postUrl = "https://localhost/stores/01YCP46JKYM8FJCQ37NMBYHE5X/read"; + mockHttpClient + .onPost(postUrl) + .doReturn(404, "{\"code\":\"undefined_endpoint\",\"message\":\"Endpoint not enabled\"}"); + + // When + ExecutionException execException = + assertThrows(ExecutionException.class, () -> fga.read(DEFAULT_STORE_ID, new ReadRequest()) + .get()); + + // Then + mockHttpClient.verify().post(postUrl).called(1); + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals(404, exception.getCode()); + assertEquals( + "{\"code\":\"undefined_endpoint\",\"message\":\"Endpoint not enabled\"}", exception.getResponseBody()); + } + + @Test + public void read_500() throws Exception { + // Given + String postUrl = "https://localhost/stores/01YCP46JKYM8FJCQ37NMBYHE5X/read"; + mockHttpClient + .onPost(postUrl) + .doReturn(500, "{\"code\":\"internal_error\",\"message\":\"Internal Server Error\"}"); + + // When + ExecutionException execException = + assertThrows(ExecutionException.class, () -> fga.read(DEFAULT_STORE_ID, new ReadRequest()) + .get()); + + // Then + mockHttpClient.verify().post(postUrl).called(1); + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals(500, exception.getCode()); + assertEquals( + "{\"code\":\"internal_error\",\"message\":\"Internal Server Error\"}", exception.getResponseBody()); + } + + /** + * Add or delete tuples from the store. + */ + @Test + public void writeTest_writes() throws Exception { + // Given + String postPath = "https://localhost/stores/01YCP46JKYM8FJCQ37NMBYHE5X/write"; + String expectedBody = String.format( + "{\"writes\":{\"tuple_keys\":[{\"object\":\"%s\",\"relation\":\"%s\",\"user\":\"%s\"}]},\"deletes\":null,\"authorization_model_id\":\"%s\"}", + DEFAULT_OBJECT, DEFAULT_RELATION, DEFAULT_USER, DEFAULT_AUTH_MODEL_ID); + mockHttpClient.onPost(postPath).withBody(is(expectedBody)).doReturn(200, EMPTY_RESPONSE_BODY); + WriteRequest request = new WriteRequest() + .authorizationModelId(DEFAULT_AUTH_MODEL_ID) + .writes(new TupleKeys() + .tupleKeys(List.of(new TupleKey() + ._object(DEFAULT_OBJECT) + .relation(DEFAULT_RELATION) + .user(DEFAULT_USER)))); + + // When + fga.write(DEFAULT_STORE_ID, request); + + // Then + mockHttpClient.verify().post(postPath).withBody(is(expectedBody)).called(1); + } + + /** + * Add or delete tuples from the store. + */ + @Test + public void writeTest_deletes() throws Exception { + // Given + String postPath = "https://localhost/stores/01YCP46JKYM8FJCQ37NMBYHE5X/write"; + String expectedBody = String.format( + "{\"writes\":null,\"deletes\":{\"tuple_keys\":[{\"object\":\"%s\",\"relation\":\"%s\",\"user\":\"%s\"}]},\"authorization_model_id\":\"%s\"}", + DEFAULT_OBJECT, DEFAULT_RELATION, DEFAULT_USER, DEFAULT_AUTH_MODEL_ID); + mockHttpClient.onPost(postPath).withBody(is(expectedBody)).doReturn(200, EMPTY_RESPONSE_BODY); + WriteRequest request = new WriteRequest() + .authorizationModelId(DEFAULT_AUTH_MODEL_ID) + .deletes(new TupleKeys() + .tupleKeys(List.of(new TupleKey() + ._object(DEFAULT_OBJECT) + .relation(DEFAULT_RELATION) + .user(DEFAULT_USER)))); + + // When + fga.write(DEFAULT_STORE_ID, request); + + // Then + mockHttpClient.verify().post(postPath).withBody(is(expectedBody)).called(1); + } + + @Test + public void write_storeIdRequired() { + // When + ExecutionException execException = + assertThrows(ExecutionException.class, () -> fga.write(null, new WriteRequest()) + .get()); + + // Then + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals("Missing the required parameter 'storeId' when calling write", exception.getMessage()); + } + + @Test + public void write_bodyRequired() { + // When + ExecutionException execException = + assertThrows(ExecutionException.class, () -> fga.write(DEFAULT_STORE_ID, null) + .get()); + + // Then + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals("Missing the required parameter 'body' when calling write", exception.getMessage()); + } + + @Test + public void write_400() throws Exception { + // Given + String postUrl = "https://localhost/stores/01YCP46JKYM8FJCQ37NMBYHE5X/write"; + mockHttpClient + .onPost(postUrl) + .doReturn(400, "{\"code\":\"validation_error\",\"message\":\"Generic validation error\"}"); + + // When + ExecutionException execException = + assertThrows(ExecutionException.class, () -> fga.write(DEFAULT_STORE_ID, new WriteRequest()) + .get()); + + // Then + mockHttpClient.verify().post(postUrl).called(1); + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals(400, exception.getCode()); + assertEquals( + "{\"code\":\"validation_error\",\"message\":\"Generic validation error\"}", + exception.getResponseBody()); + } + + @Test + public void write_404() throws Exception { + // Given + String postUrl = "https://localhost/stores/01YCP46JKYM8FJCQ37NMBYHE5X/write"; + mockHttpClient + .onPost(postUrl) + .doReturn(404, "{\"code\":\"undefined_endpoint\",\"message\":\"Endpoint not enabled\"}"); + + // When + ExecutionException execException = + assertThrows(ExecutionException.class, () -> fga.write(DEFAULT_STORE_ID, new WriteRequest()) + .get()); + + // Then + mockHttpClient.verify().post(postUrl).called(1); + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals(404, exception.getCode()); + assertEquals( + "{\"code\":\"undefined_endpoint\",\"message\":\"Endpoint not enabled\"}", exception.getResponseBody()); + } + + @Test + public void write_500() throws Exception { + // Given + String postUrl = "https://localhost/stores/01YCP46JKYM8FJCQ37NMBYHE5X/write"; + mockHttpClient + .onPost(postUrl) + .doReturn(500, "{\"code\":\"internal_error\",\"message\":\"Internal Server Error\"}"); + + // When + ExecutionException execException = + assertThrows(ExecutionException.class, () -> fga.write(DEFAULT_STORE_ID, new WriteRequest()) + .get()); + + // Then + mockHttpClient.verify().post(postUrl).called(1); + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals(500, exception.getCode()); + assertEquals( + "{\"code\":\"internal_error\",\"message\":\"Internal Server Error\"}", exception.getResponseBody()); + } + + /** + * Check whether a user is authorized to access an object. + */ + @Test + public void check() throws Exception { + // Given + String postPath = "https://localhost/stores/01YCP46JKYM8FJCQ37NMBYHE5X/check"; + String expectedBody = String.format( + "{\"tuple_key\":{\"object\":\"%s\",\"relation\":\"%s\",\"user\":\"%s\"},\"contextual_tuples\":{\"tuple_keys\":[]},\"authorization_model_id\":\"01G5JAVJ41T49E9TT3SKVS7X1J\",\"trace\":null}", + DEFAULT_OBJECT, DEFAULT_RELATION, DEFAULT_USER); + mockHttpClient.onPost(postPath).withBody(is(expectedBody)).doReturn(200, "{\"allowed\":true}"); + CheckRequest request = new CheckRequest() + .tupleKey(new TupleKey() + ._object(DEFAULT_OBJECT) + .relation(DEFAULT_RELATION) + .user(DEFAULT_USER)) + .contextualTuples(new ContextualTupleKeys()) + .authorizationModelId(DEFAULT_AUTH_MODEL_ID); + + // When + CheckResponse response = fga.check(DEFAULT_STORE_ID, request).get(); + + // Then + verify(mockConfiguration).getApiUrl(); + verify(mockConfiguration).getReadTimeout(); + mockHttpClient.verify().post(postPath).withBody(is(expectedBody)).called(1); + assertEquals(Boolean.TRUE, response.getAllowed()); + } + + @Test + public void check_storeIdRequired() { + // When + ExecutionException execException = + assertThrows(ExecutionException.class, () -> fga.check(null, new CheckRequest()) + .get()); + + // Then + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals("Missing the required parameter 'storeId' when calling check", exception.getMessage()); + } + + @Test + public void check_bodyRequired() { + // When + ExecutionException execException = + assertThrows(ExecutionException.class, () -> fga.check(DEFAULT_STORE_ID, null) + .get()); + + // Then + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals("Missing the required parameter 'body' when calling check", exception.getMessage()); + } + + @Test + public void check_400() throws Exception { + // Given + String postUrl = "https://localhost/stores/01YCP46JKYM8FJCQ37NMBYHE5X/check"; + mockHttpClient + .onPost(postUrl) + .doReturn(400, "{\"code\":\"validation_error\",\"message\":\"Generic validation error\"}"); + + // When + ExecutionException execException = + assertThrows(ExecutionException.class, () -> fga.check(DEFAULT_STORE_ID, new CheckRequest()) + .get()); + + // Then + mockHttpClient.verify().post(postUrl).called(1); + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals(400, exception.getCode()); + assertEquals( + "{\"code\":\"validation_error\",\"message\":\"Generic validation error\"}", + exception.getResponseBody()); + } + + @Test + public void check_404() throws Exception { + // Given + String postUrl = "https://localhost/stores/01YCP46JKYM8FJCQ37NMBYHE5X/check"; + mockHttpClient + .onPost(postUrl) + .doReturn(404, "{\"code\":\"undefined_endpoint\",\"message\":\"Endpoint not enabled\"}"); + + // When + ExecutionException execException = + assertThrows(ExecutionException.class, () -> fga.check(DEFAULT_STORE_ID, new CheckRequest()) + .get()); + + // Then + mockHttpClient.verify().post(postUrl).called(1); + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals(404, exception.getCode()); + assertEquals( + "{\"code\":\"undefined_endpoint\",\"message\":\"Endpoint not enabled\"}", exception.getResponseBody()); + } + + @Test + public void check_500() throws Exception { + // Given + String postUrl = "https://localhost/stores/01YCP46JKYM8FJCQ37NMBYHE5X/check"; + mockHttpClient + .onPost(postUrl) + .doReturn(500, "{\"code\":\"internal_error\",\"message\":\"Internal Server Error\"}"); + + // When + ExecutionException execException = + assertThrows(ExecutionException.class, () -> fga.check(DEFAULT_STORE_ID, new CheckRequest()) + .get()); + + // Then + mockHttpClient.verify().post(postUrl).called(1); + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals(500, exception.getCode()); + assertEquals( + "{\"code\":\"internal_error\",\"message\":\"Internal Server Error\"}", exception.getResponseBody()); + } + + /** + * Expand all relationships in userset tree format, and following userset rewrite rules. Useful to reason about and debug a certain relationship. + */ + @Test + public void expandTest() throws Exception { + // Given + String postPath = "https://localhost/stores/01YCP46JKYM8FJCQ37NMBYHE5X/expand"; + String expectedBody = String.format( + "{\"tuple_key\":{\"object\":\"%s\",\"relation\":\"%s\",\"user\":\"%s\"},\"authorization_model_id\":\"%s\"}", + DEFAULT_OBJECT, DEFAULT_RELATION, DEFAULT_USER, DEFAULT_AUTH_MODEL_ID); + String responseBody = String.format( + "{\"tree\":{\"root\":{\"union\":{\"nodes\":[{\"leaf\":{\"users\":{\"users\":[\"%s\"]}}}]}}}}", + DEFAULT_USER); + mockHttpClient.onPost(postPath).withBody(is(expectedBody)).doReturn(200, responseBody); + ExpandRequest request = new ExpandRequest() + .authorizationModelId(DEFAULT_AUTH_MODEL_ID) + .tupleKey(new TupleKey() + ._object(DEFAULT_OBJECT) + .relation(DEFAULT_RELATION) + .user(DEFAULT_USER)); + + // When + ExpandResponse response = fga.expand(DEFAULT_STORE_ID, request).get(); + + // Then + mockHttpClient.verify().post(postPath).withBody(is(expectedBody)).called(1); + assertNotNull(response.getTree()); + assertNotNull(response.getTree().getRoot()); + assertNotNull(response.getTree().getRoot().getUnion()); + assertNotNull(response.getTree().getRoot().getUnion().getNodes()); + assertEquals(1, response.getTree().getRoot().getUnion().getNodes().size()); + assertNotNull(response.getTree().getRoot().getUnion().getNodes().get(0)); + Node node = response.getTree().getRoot().getUnion().getNodes().get(0); + assertNotNull(node.getLeaf()); + assertNotNull(node.getLeaf().getUsers()); + assertNotNull(node.getLeaf().getUsers().getUsers()); + assertEquals(1, node.getLeaf().getUsers().getUsers().size()); + assertEquals(DEFAULT_USER, node.getLeaf().getUsers().getUsers().get(0)); + } + + @Test + public void expand_storeIdRequired() { + // When + ExecutionException execException = + assertThrows(ExecutionException.class, () -> fga.expand(null, new ExpandRequest()) + .get()); + + // Then + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals("Missing the required parameter 'storeId' when calling expand", exception.getMessage()); + } + + @Test + public void expand_bodyRequired() { + // When + ExecutionException execException = + assertThrows(ExecutionException.class, () -> fga.expand(DEFAULT_STORE_ID, null) + .get()); + + // Then + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals("Missing the required parameter 'body' when calling expand", exception.getMessage()); + } + + @Test + public void expand_400() throws Exception { + // Given + String postUrl = "https://localhost/stores/01YCP46JKYM8FJCQ37NMBYHE5X/expand"; + mockHttpClient + .onPost(postUrl) + .doReturn(400, "{\"code\":\"validation_error\",\"message\":\"Generic validation error\"}"); + + // When + ExecutionException execException = + assertThrows(ExecutionException.class, () -> fga.expand(DEFAULT_STORE_ID, new ExpandRequest()) + .get()); + + // Then + mockHttpClient.verify().post(postUrl).called(1); + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals(400, exception.getCode()); + assertEquals( + "{\"code\":\"validation_error\",\"message\":\"Generic validation error\"}", + exception.getResponseBody()); + } + + @Test + public void expand_404() throws Exception { + // Given + String postUrl = "https://localhost/stores/01YCP46JKYM8FJCQ37NMBYHE5X/expand"; + mockHttpClient + .onPost(postUrl) + .doReturn(404, "{\"code\":\"undefined_endpoint\",\"message\":\"Endpoint not enabled\"}"); + + // When + ExecutionException execException = + assertThrows(ExecutionException.class, () -> fga.expand(DEFAULT_STORE_ID, new ExpandRequest()) + .get()); + + // Then + mockHttpClient.verify().post(postUrl).called(1); + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals(404, exception.getCode()); + assertEquals( + "{\"code\":\"undefined_endpoint\",\"message\":\"Endpoint not enabled\"}", exception.getResponseBody()); + } + + @Test + public void expand_500() throws Exception { + // Given + String postUrl = "https://localhost/stores/01YCP46JKYM8FJCQ37NMBYHE5X/expand"; + mockHttpClient + .onPost(postUrl) + .doReturn(500, "{\"code\":\"internal_error\",\"message\":\"Internal Server Error\"}"); + + // When + ExecutionException execException = + assertThrows(ExecutionException.class, () -> fga.expand(DEFAULT_STORE_ID, new ExpandRequest()) + .get()); + + // Then + mockHttpClient.verify().post(postUrl).called(1); + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals(500, exception.getCode()); + assertEquals( + "{\"code\":\"internal_error\",\"message\":\"Internal Server Error\"}", exception.getResponseBody()); + } + + /** + * List all objects of the given type that the user has a relation with. + */ + @Test + public void listObjectsTest() throws Exception { + // Given + String postPath = "https://localhost/stores/01YCP46JKYM8FJCQ37NMBYHE5X/list-objects"; + String expectedBody = String.format( + "{\"authorization_model_id\":\"%s\",\"type\":null,\"relation\":\"%s\",\"user\":\"%s\",\"contextual_tuples\":null}", + DEFAULT_AUTH_MODEL_ID, DEFAULT_RELATION, DEFAULT_USER); + mockHttpClient + .onPost(postPath) + .withBody(is(expectedBody)) + .doReturn(200, String.format("{\"objects\":[\"%s\"]}", DEFAULT_OBJECT)); + ListObjectsRequest request = new ListObjectsRequest() + .authorizationModelId(DEFAULT_AUTH_MODEL_ID) + .relation(DEFAULT_RELATION) + .user(DEFAULT_USER); + + // When + ListObjectsResponse response = + fga.listObjects(DEFAULT_STORE_ID, request).get(); + + // Then + mockHttpClient.verify().post(postPath).withBody(is(expectedBody)).called(1); + assertEquals(List.of(DEFAULT_OBJECT), response.getObjects()); + } + + @Test + public void listObjects_storeIdRequired() { + // When + ExecutionException execException = + assertThrows(ExecutionException.class, () -> fga.listObjects(null, new ListObjectsRequest()) + .get()); + + // Then + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals("Missing the required parameter 'storeId' when calling listObjects", exception.getMessage()); + } + + @Test + public void listObjects_bodyRequired() { + // When + ExecutionException execException = + assertThrows(ExecutionException.class, () -> fga.listObjects(DEFAULT_STORE_ID, null) + .get()); + + // Then + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals("Missing the required parameter 'body' when calling listObjects", exception.getMessage()); + } + + @Test + public void listObjects_400() throws Exception { + // Given + String postUrl = "https://localhost/stores/01YCP46JKYM8FJCQ37NMBYHE5X/list-objects"; + mockHttpClient + .onPost(postUrl) + .doReturn(400, "{\"code\":\"validation_error\",\"message\":\"Generic validation error\"}"); + + // When + ExecutionException execException = + assertThrows(ExecutionException.class, () -> fga.listObjects(DEFAULT_STORE_ID, new ListObjectsRequest()) + .get()); + + // Then + mockHttpClient.verify().post(postUrl).called(1); + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals(400, exception.getCode()); + assertEquals( + "{\"code\":\"validation_error\",\"message\":\"Generic validation error\"}", + exception.getResponseBody()); + } + + @Test + public void listObjects_404() throws Exception { + // Given + String postUrl = "https://localhost/stores/01YCP46JKYM8FJCQ37NMBYHE5X/list-objects"; + mockHttpClient + .onPost(postUrl) + .doReturn(404, "{\"code\":\"undefined_endpoint\",\"message\":\"Endpoint not enabled\"}"); + + // When + ExecutionException execException = + assertThrows(ExecutionException.class, () -> fga.listObjects(DEFAULT_STORE_ID, new ListObjectsRequest()) + .get()); + + // Then + mockHttpClient.verify().post(postUrl).called(1); + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals(404, exception.getCode()); + assertEquals( + "{\"code\":\"undefined_endpoint\",\"message\":\"Endpoint not enabled\"}", exception.getResponseBody()); + } + + @Test + public void listObjects_500() throws Exception { + // Given + String postUrl = "https://localhost/stores/01YCP46JKYM8FJCQ37NMBYHE5X/list-objects"; + mockHttpClient + .onPost(postUrl) + .doReturn(500, "{\"code\":\"internal_error\",\"message\":\"Internal Server Error\"}"); + + // When + ExecutionException execException = + assertThrows(ExecutionException.class, () -> fga.listObjects(DEFAULT_STORE_ID, new ListObjectsRequest()) + .get()); + + // Then + mockHttpClient.verify().post(postUrl).called(1); + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals(500, exception.getCode()); + assertEquals( + "{\"code\":\"internal_error\",\"message\":\"Internal Server Error\"}", exception.getResponseBody()); + } + + /** + * Read assertions for an authorization model ID. + */ + @Test + public void readAssertionsTest() throws Exception { + // Given + String getUrl = "https://localhost/stores/01YCP46JKYM8FJCQ37NMBYHE5X/assertions/01G5JAVJ41T49E9TT3SKVS7X1J"; + String responseBody = String.format( + "{\"assertions\":[{\"tuple_key\":{\"object\":\"%s\",\"relation\":\"%s\",\"user\":\"%s\"},\"expectation\":true}]}", + DEFAULT_OBJECT, DEFAULT_RELATION, DEFAULT_USER); + mockHttpClient.onGet(getUrl).doReturn(200, responseBody); + + // When + ReadAssertionsResponse response = + fga.readAssertions(DEFAULT_STORE_ID, DEFAULT_AUTH_MODEL_ID).get(); + + // Then + mockHttpClient.verify().get(getUrl).called(1); + assertNotNull(response.getAssertions()); + assertEquals(1, response.getAssertions().size()); + Assertion assertion = response.getAssertions().get(0); + assertNotNull(assertion); + assertTrue(assertion.getExpectation()); + assertEquals(DEFAULT_OBJECT, assertion.getTupleKey().getObject()); + assertEquals(DEFAULT_RELATION, assertion.getTupleKey().getRelation()); + assertEquals(DEFAULT_USER, assertion.getTupleKey().getUser()); + } + + @Test + public void readAssertions_storeIdRequired() { + // When + ExecutionException execException = + assertThrows(ExecutionException.class, () -> fga.readAssertions(null, DEFAULT_AUTH_MODEL_ID) + .get()); + + // Then + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals("Missing the required parameter 'storeId' when calling readAssertions", exception.getMessage()); + } + + @Test + public void readAssertions_authModelIdRequired() { + // When + ExecutionException execException = + assertThrows(ExecutionException.class, () -> fga.readAssertions(DEFAULT_STORE_ID, null) + .get()); + + // Then + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals( + "Missing the required parameter 'authorizationModelId' when calling readAssertions", + exception.getMessage()); + } + + @Test + public void readAssertions_400() throws Exception { + // Given + String getUrl = "https://localhost/stores/01YCP46JKYM8FJCQ37NMBYHE5X/assertions/01G5JAVJ41T49E9TT3SKVS7X1J"; + mockHttpClient + .onGet(getUrl) + .doReturn(400, "{\"code\":\"validation_error\",\"message\":\"Generic validation error\"}"); + + // When + ExecutionException execException = + assertThrows(ExecutionException.class, () -> fga.readAssertions(DEFAULT_STORE_ID, DEFAULT_AUTH_MODEL_ID) + .get()); + + // Then + mockHttpClient.verify().get(getUrl).called(1); + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals(400, exception.getCode()); + assertEquals( + "{\"code\":\"validation_error\",\"message\":\"Generic validation error\"}", + exception.getResponseBody()); + } + + @Test + public void readAssertions_404() throws Exception { + // Given + String getUrl = "https://localhost/stores/01YCP46JKYM8FJCQ37NMBYHE5X/assertions/01G5JAVJ41T49E9TT3SKVS7X1J"; + mockHttpClient + .onGet(getUrl) + .doReturn(404, "{\"code\":\"undefined_endpoint\",\"message\":\"Endpoint not enabled\"}"); + + // When + ExecutionException execException = + assertThrows(ExecutionException.class, () -> fga.readAssertions(DEFAULT_STORE_ID, DEFAULT_AUTH_MODEL_ID) + .get()); + + // Then + mockHttpClient.verify().get(getUrl).called(1); + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals(404, exception.getCode()); + assertEquals( + "{\"code\":\"undefined_endpoint\",\"message\":\"Endpoint not enabled\"}", exception.getResponseBody()); + } + + @Test + public void readAssertions_500() throws Exception { + // Given + String getUrl = "https://localhost/stores/01YCP46JKYM8FJCQ37NMBYHE5X/assertions/01G5JAVJ41T49E9TT3SKVS7X1J"; + mockHttpClient + .onGet(getUrl) + .doReturn(500, "{\"code\":\"internal_error\",\"message\":\"Internal Server Error\"}"); + + // When + ExecutionException execException = + assertThrows(ExecutionException.class, () -> fga.readAssertions(DEFAULT_STORE_ID, DEFAULT_AUTH_MODEL_ID) + .get()); + + // Then + mockHttpClient.verify().get(getUrl).called(1); + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals(500, exception.getCode()); + assertEquals( + "{\"code\":\"internal_error\",\"message\":\"Internal Server Error\"}", exception.getResponseBody()); + } + + /** + * Upsert assertions for an authorization model ID. + */ + @Test + public void writeAssertionsTest() throws Exception { + // Given + String putUrl = "https://localhost/stores/01YCP46JKYM8FJCQ37NMBYHE5X/assertions/01G5JAVJ41T49E9TT3SKVS7X1J"; + String expectedBody = String.format( + "{\"assertions\":[{\"tuple_key\":{\"object\":\"%s\",\"relation\":\"%s\",\"user\":\"%s\"},\"expectation\":true}]}", + DEFAULT_OBJECT, DEFAULT_RELATION, DEFAULT_USER); + mockHttpClient.onPut(putUrl).withBody(is(expectedBody)).doReturn(200, EMPTY_RESPONSE_BODY); + WriteAssertionsRequest request = new WriteAssertionsRequest() + .assertions(List.of(new Assertion() + .tupleKey(new TupleKey() + ._object(DEFAULT_OBJECT) + .relation(DEFAULT_RELATION) + .user(DEFAULT_USER)) + .expectation(true))); + + // When + fga.writeAssertions(DEFAULT_STORE_ID, DEFAULT_AUTH_MODEL_ID, request); + + // Then + mockHttpClient.verify().put(putUrl).withBody(is(expectedBody)).called(1); + } + + @Test + public void writeAssertions_storeIdRequired() { + // When + ExecutionException execException = assertThrows(ExecutionException.class, () -> fga.writeAssertions( + null, DEFAULT_AUTH_MODEL_ID, new WriteAssertionsRequest()) + .get()); + + // Then + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals("Missing the required parameter 'storeId' when calling writeAssertions", exception.getMessage()); + } + + @Test + public void writeAssertions_authModelIdRequired() { + // When + ExecutionException execException = assertThrows(ExecutionException.class, () -> fga.writeAssertions( + DEFAULT_STORE_ID, null, new WriteAssertionsRequest()) + .get()); + + // Then + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals( + "Missing the required parameter 'authorizationModelId' when calling writeAssertions", + exception.getMessage()); + } + + @Test + public void writeAssertions_bodyRequired() { + // When + ExecutionException execException = assertThrows( + ExecutionException.class, () -> fga.writeAssertions(DEFAULT_STORE_ID, DEFAULT_AUTH_MODEL_ID, null) + .get()); + + // Then + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals("Missing the required parameter 'body' when calling writeAssertions", exception.getMessage()); + } + + @Test + public void writeAssertions_400() throws Exception { + // Given + String putUrl = "https://localhost/stores/01YCP46JKYM8FJCQ37NMBYHE5X/assertions/01G5JAVJ41T49E9TT3SKVS7X1J"; + mockHttpClient + .onPut(putUrl) + .doReturn(400, "{\"code\":\"validation_error\",\"message\":\"Generic validation error\"}"); + + // When + ExecutionException execException = assertThrows(ExecutionException.class, () -> fga.writeAssertions( + DEFAULT_STORE_ID, DEFAULT_AUTH_MODEL_ID, new WriteAssertionsRequest()) + .get()); + + // Then + mockHttpClient.verify().put(putUrl).called(1); + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals(400, exception.getCode()); + assertEquals( + "{\"code\":\"validation_error\",\"message\":\"Generic validation error\"}", + exception.getResponseBody()); + } + + @Test + public void writeAssertions_404() throws Exception { + // Given + String putUrl = "https://localhost/stores/01YCP46JKYM8FJCQ37NMBYHE5X/assertions/01G5JAVJ41T49E9TT3SKVS7X1J"; + mockHttpClient + .onPut(putUrl) + .doReturn(404, "{\"code\":\"undefined_endpoint\",\"message\":\"Endpoint not enabled\"}"); + + // When + ExecutionException execException = assertThrows(ExecutionException.class, () -> fga.writeAssertions( + DEFAULT_STORE_ID, DEFAULT_AUTH_MODEL_ID, new WriteAssertionsRequest()) + .get()); + + // Then + mockHttpClient.verify().put(putUrl).called(1); + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals(404, exception.getCode()); + assertEquals( + "{\"code\":\"undefined_endpoint\",\"message\":\"Endpoint not enabled\"}", exception.getResponseBody()); + } + + @Test + public void writeAssertions_500() throws Exception { + // Given + String putUrl = "https://localhost/stores/01YCP46JKYM8FJCQ37NMBYHE5X/assertions/01G5JAVJ41T49E9TT3SKVS7X1J"; + mockHttpClient + .onPut(putUrl) + .doReturn(500, "{\"code\":\"internal_error\",\"message\":\"Internal Server Error\"}"); + + // When + ExecutionException execException = assertThrows(ExecutionException.class, () -> fga.writeAssertions( + DEFAULT_STORE_ID, DEFAULT_AUTH_MODEL_ID, new WriteAssertionsRequest()) + .get()); + + // Then + mockHttpClient.verify().put(putUrl).called(1); + ApiException exception = assertInstanceOf(ApiException.class, execException.getCause()); + assertEquals(500, exception.getCode()); + assertEquals( + "{\"code\":\"internal_error\",\"message\":\"Internal Server Error\"}", exception.getResponseBody()); + } +} diff --git a/config/clients/java/template/Pair.mustache b/config/clients/java/template/Pair.mustache new file mode 100644 index 00000000..bc3e3e67 --- /dev/null +++ b/config/clients/java/template/Pair.mustache @@ -0,0 +1,44 @@ +{{>licenseInfo}} +package dev.openfga.util; + +public class Pair { + private String name = ""; + private String value = ""; + + public Pair (String name, String value) { + setName(name); + setValue(value); + } + + private void setName(String name) { + if (!isValidString(name)) { + return; + } + + this.name = name; + } + + private void setValue(String value) { + if (!isValidString(value)) { + return; + } + + this.value = value; + } + + public String getName() { + return this.name; + } + + public String getValue() { + return this.value; + } + + private boolean isValidString(String arg) { + if (arg == null) { + return false; + } + + return true; + } +} diff --git a/config/clients/java/template/README_api_endpoints.mustache b/config/clients/java/template/README_api_endpoints.mustache new file mode 100644 index 00000000..e69de29b diff --git a/config/clients/java/template/README_calling_api.mustache b/config/clients/java/template/README_calling_api.mustache new file mode 100644 index 00000000..81f97fee --- /dev/null +++ b/config/clients/java/template/README_calling_api.mustache @@ -0,0 +1,191 @@ +#### Stores + +##### List Stores + +Get a paginated list of stores. + +[API Documentation]({{apiDocsUrl}}/docs/api#/Stores/ListStores) + +```java +``` + +##### Create Store + +Initialize a store. + +[API Documentation]({{apiDocsUrl}}/docs/api#/Stores/CreateStore) + +```java +``` + +##### Get Store + +Get information about the current store. + +[API Documentation]({{apiDocsUrl}}/docs/api#/Stores/GetStore) + +> Requires a client initialized with a storeId + +```java +``` + +##### Delete Store + +Delete a store. + +[API Documentation]({{apiDocsUrl}}/docs/api#/Stores/DeleteStore) + +> Requires a client initialized with a storeId + +```java +``` + +#### Authorization Models + +##### Read Authorization Models + +Read all authorization models in the store. + +[API Documentation]({{apiDocsUrl}}#/Authorization%20Models/ReadAuthorizationModels) + +```java +``` + +##### Write Authorization Model + +Create a new authorization model. + +[API Documentation]({{apiDocsUrl}}#/Authorization%20Models/WriteAuthorizationModel) + +> Note: To learn how to build your authorization model, check the Docs at {{docsUrl}}. + +> Learn more about [the {{appTitleCaseName}} configuration language]({{docsUrl}}/configuration-language). + +> You can use the OpenFGA [CLI](https://github.com/openfga/cli) or [Syntax Transformer](https://github.com/openfga/syntax-transformer) to convert between the OpenFGA DSL and the JSON authorization model. + +```java +``` + +#### Read a Single Authorization Model + +Read a particular authorization model. + +[API Documentation]({{apiDocsUrl}}#/Authorization%20Models/ReadAuthorizationModel) + +```java +``` + +##### Read the Latest Authorization Model + +Reads the latest authorization model (note: this ignores the model id in configuration). + +[API Documentation]({{apiDocsUrl}}#/Authorization%20Models/ReadAuthorizationModel) + +```java +``` + +#### Relationship Tuples + +##### Read Relationship Tuple Changes (Watch) + +Reads the list of historical relationship tuple writes and deletes. + +[API Documentation]({{apiDocsUrl}}#/Relationship%20Tuples/ReadChanges) + +```java +``` + +##### Read Relationship Tuples + +Reads the relationship tuples stored in the database. It does not evaluate nor exclude invalid tuples according to the authorization model. + +[API Documentation]({{apiDocsUrl}}#/Relationship%20Tuples/Read) + +```java +``` + +##### Write (Create and Delete) Relationship Tuples + +Create and/or delete relationship tuples to update the system state. + +[API Documentation]({{apiDocsUrl}}#/Relationship%20Tuples/Write) + +###### Transaction mode (default) + +By default, write runs in a transaction mode where any invalid operation (deleting a non-existing tuple, creating an existing tuple, one of the tuples was invalid) or a server error will fail the entire operation. + +```java +``` + +Convenience `WriteTuples` and `DeleteTuples` methods are also available. + +###### Non-transaction mode + +The SDK will split the writes into separate requests and send them sequentially to avoid violating rate limits. + +```java +``` + +#### Relationship Queries + +##### Check + +Check if a user has a particular relation with an object. + +[API Documentation]({{apiDocsUrl}}#/Relationship%20Queries/Check) + +```java +``` + +##### Batch Check + +Run a set of [checks](#check). Batch Check will return `allowed: false` if it encounters an error, and will return the error in the body. +If 429s or 5xxs are encountered, the underlying check will retry up to {{defaultMaxRetry}} times before giving up. + +```java +``` + +##### Expand + +Expands the relationships in userset tree format. + +[API Documentation]({{apiDocsUrl}}#/Relationship%20Queries/Expand) + +```java +``` + +##### List Objects + +List the objects of a particular type a user has access to. + +[API Documentation]({{apiDocsUrl}}#/Relationship%20Queries/ListObjects) + +```java +``` + +##### List Relations + +List the relations a user has on an object. + +```java +``` + +#### Assertions + +##### Read Assertions + +Read assertions for a particular authorization model. + +[API Documentation]({{apiDocsUrl}}#/Assertions/Read%20Assertions) + +```java +``` + +##### Write Assertions + +Update the assertions for a particular authorization model. + +[API Documentation]({{apiDocsUrl}}#/Assertions/Write%20Assertions) + +```java +``` diff --git a/config/clients/java/template/README_custom_badges.mustache b/config/clients/java/template/README_custom_badges.mustache new file mode 100644 index 00000000..e69de29b diff --git a/config/clients/java/template/README_initializing.mustache b/config/clients/java/template/README_initializing.mustache new file mode 100644 index 00000000..6b5f2e67 --- /dev/null +++ b/config/clients/java/template/README_initializing.mustache @@ -0,0 +1,14 @@ +#### No Credentials + +```cjava +``` + +#### API Token + +```java +``` + +#### Client Credentials + +```java +``` diff --git a/config/clients/java/template/README_installation.mustache b/config/clients/java/template/README_installation.mustache new file mode 100644 index 00000000..e69de29b diff --git a/config/clients/java/template/README_license_disclaimer.mustache b/config/clients/java/template/README_license_disclaimer.mustache new file mode 100644 index 00000000..a297302a --- /dev/null +++ b/config/clients/java/template/README_license_disclaimer.mustache @@ -0,0 +1 @@ +The code in this repo was auto generated by [OpenAPI Generator](https://github.com/OpenAPITools/openapi-generator) from a template based on the [Java template](https://github.com/OpenAPITools/openapi-generator/tree/master/modules/openapi-generator/src/main/resources/Java), licensed under the [Apache License 2.0](https://github.com/OpenAPITools/openapi-generator/blob/master/LICENSE). diff --git a/config/clients/java/template/README_models.mustache b/config/clients/java/template/README_models.mustache new file mode 100644 index 00000000..7ea24660 --- /dev/null +++ b/config/clients/java/template/README_models.mustache @@ -0,0 +1,3 @@ +{{#models}}{{#model}} +- [{{{classname}}}](https://{{gitHost}}/{{gitUserId}}/{{gitRepoId}}/blob/main/docs/{{{classname}}}.md) +{{/model}}{{/models}} diff --git a/config/clients/java/template/additionalEnumTypeAnnotations.mustache b/config/clients/java/template/additionalEnumTypeAnnotations.mustache new file mode 100644 index 00000000..aa524798 --- /dev/null +++ b/config/clients/java/template/additionalEnumTypeAnnotations.mustache @@ -0,0 +1,2 @@ +{{#additionalEnumTypeAnnotations}}{{{.}}} +{{/additionalEnumTypeAnnotations}} \ No newline at end of file diff --git a/config/clients/java/template/additionalModelTypeAnnotations.mustache b/config/clients/java/template/additionalModelTypeAnnotations.mustache new file mode 100644 index 00000000..f4871c02 --- /dev/null +++ b/config/clients/java/template/additionalModelTypeAnnotations.mustache @@ -0,0 +1,2 @@ +{{#additionalModelTypeAnnotations}}{{{.}}} +{{/additionalModelTypeAnnotations}} \ No newline at end of file diff --git a/config/clients/java/template/additionalOneOfTypeAnnotations.mustache b/config/clients/java/template/additionalOneOfTypeAnnotations.mustache new file mode 100644 index 00000000..283f8f91 --- /dev/null +++ b/config/clients/java/template/additionalOneOfTypeAnnotations.mustache @@ -0,0 +1,2 @@ +{{#additionalOneOfTypeAnnotations}}{{{.}}} +{{/additionalOneOfTypeAnnotations}} \ No newline at end of file diff --git a/config/clients/java/template/apiOperation.mustache b/config/clients/java/template/apiOperation.mustache new file mode 100644 index 00000000..97adb0ea --- /dev/null +++ b/config/clients/java/template/apiOperation.mustache @@ -0,0 +1,28 @@ +{{>licenseInfo}} +package {{invokerPackage}}; + +import io.swagger.v3.oas.models.Operation; + +public class ApiOperation { + private final String path; + private final String method; + private final Operation operation; + + public ApiOperation(String path, String method, Operation operation) { + this.path = path; + this.method = method; + this.operation = operation; + } + + public Operation getOperation() { + return operation; + } + + public String getPath() { + return path; + } + + public String getMethod() { + return method; + } +} diff --git a/config/clients/java/template/build.gradle.mustache b/config/clients/java/template/build.gradle.mustache new file mode 100644 index 00000000..4e69cf3a --- /dev/null +++ b/config/clients/java/template/build.gradle.mustache @@ -0,0 +1,137 @@ +plugins { + id 'java' + id 'jvm-test-suite' + id 'maven-publish' + id 'idea' + id 'eclipse' + id 'com.diffplug.spotless' version '6.20.0' +} + +group = '{{groupId}}' +version = '{{artifactVersion}}' + +repositories { + mavenCentral() +} + +publishing { + publications { + maven(MavenPublication) { + artifactId = '{{artifactId}}' + from components.java + } + } +} + +java { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + + withJavadocJar() + withSourcesJar() +} + +javadoc { + // Ignore warnings. + options.addStringOption('Xdoclint:none', '-quiet') +} + +ext { + {{#swagger1AnnotationLibrary}} + swagger_annotations_version = "1.6.9" + {{/swagger1AnnotationLibrary}} + {{#swagger2AnnotationLibrary}} + swagger_annotations_version = "2.2.9" + {{/swagger2AnnotationLibrary}} + jackson_version = "2.14.1" + junit_version = "5.7.1" + {{#hasFormParamsInSpec}} + httpmime_version = "4.5.13" + {{/hasFormParamsInSpec}} +} + +dependencies { + {{#swagger1AnnotationLibrary}} + implementation "io.swagger:swagger-annotations:$swagger_annotations_version" + {{/swagger1AnnotationLibrary}} + {{#swagger2AnnotationLibrary}} + implementation "io.swagger.core.v3:swagger-annotations:$swagger_annotations_version" + {{/swagger2AnnotationLibrary}} + implementation "com.google.code.findbugs:jsr305:3.0.2" + implementation "com.fasterxml.jackson.core:jackson-core:$jackson_version" + implementation "com.fasterxml.jackson.core:jackson-annotations:$jackson_version" + implementation "com.fasterxml.jackson.core:jackson-databind:$jackson_version" + implementation "com.fasterxml.jackson.datatype:jackson-datatype-jsr310:$jackson_version" + implementation "org.openapitools:jackson-databind-nullable:0.2.1" + {{#hasFormParamsInSpec}} + implementation "org.apache.httpcomponents:httpmime:$httpmime_version" + {{/hasFormParamsInSpec}} +} + +testing { + suites { + test { + useJUnitJupiter() + + dependencies { + implementation project() + implementation "org.junit.jupiter:junit-jupiter:$junit_version" + implementation "org.mockito:mockito-core:3.+" + runtimeOnly "org.junit.platform:junit-platform-launcher" + + // This test-only dependency is convenient but not widely used. + // Review project activity before updating the version here. + // See also: https://github.com/PGSSoft/HttpClientMock/issues/3 + implementation "com.pgs-soft:HttpClientMock:1.0.0" + } + } + integration(JvmTestSuite) { + testType = TestSuiteType.INTEGRATION_TEST + useJUnitJupiter() + + dependencies { + implementation "com.fasterxml.jackson.core:jackson-core:$jackson_version" + implementation "com.fasterxml.jackson.core:jackson-databind:$jackson_version" + implementation project() + } + + sources { + java { + srcDirs = ['src/test-integration/java'] + } + } + } + } +} + +// Use spotless plugin to automatically format code, remove unused import, etc +// To apply changes directly to the file, run `gradlew spotlessApply` +// Ref: https://github.com/diffplug/spotless/tree/main/plugin-gradle +spotless { + // comment out below to run spotless as part of the `check` task + enforceCheck false + format 'misc', { + // define the files (e.g. '*.gradle', '*.md') to apply `misc` to + target '.gitignore' + // define the steps to apply to those files + trimTrailingWhitespace() + indentWithSpaces() // Takes an integer argument if you don't like 4 + endWithNewline() + } + java { + palantirJavaFormat() + removeUnusedImports() + importOrder() + } +} + +// Use spotless plugin to automatically format code, remove unused import, etc +// To apply changes directly to the file, run `gradlew spotlessApply` +// Ref: https://github.com/diffplug/spotless/tree/main/plugin-gradle +tasks.register('fmt') { + dependsOn 'spotlessApply' +} + +tasks.register('test-integration') { + dependsOn testing.suites.integration +} diff --git a/config/clients/java/template/config-BaseConfiguration.java.mustache b/config/clients/java/template/config-BaseConfiguration.java.mustache new file mode 100644 index 00000000..eb6689ad --- /dev/null +++ b/config/clients/java/template/config-BaseConfiguration.java.mustache @@ -0,0 +1,15 @@ +{{>licenseInfo}} +package {{apiPackage}}.configuration; + +import dev.openfga.sdk.errors.FgaInvalidParameterException; +import java.time.Duration; + +public interface BaseConfiguration { + String getApiUrl(); + + String getUserAgent(); + + Duration getReadTimeout(); + + Duration getConnectTimeout(); +} diff --git a/config/clients/java/template/config-Configuration.java.mustache b/config/clients/java/template/config-Configuration.java.mustache new file mode 100644 index 00000000..5c7843bb --- /dev/null +++ b/config/clients/java/template/config-Configuration.java.mustache @@ -0,0 +1,198 @@ +{{>licenseInfo}} +package {{apiPackage}}.configuration; + +import static dev.openfga.util.StringUtil.isNullOrWhitespace; + +import dev.openfga.sdk.errors.FgaInvalidParameterException; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URL; +import java.net.http.HttpClient; +import java.net.http.HttpConnectTimeoutException; +import java.net.http.HttpRequest; +import java.time.Duration; + +/** + * Configurations for an api client. + */ +public class Configuration implements BaseConfiguration { + public static final String VERSION = "{{packageVersion}}"; + + private static final String DEFAULT_API_URL = "http://localhost:8080"; + private static final String DEFAULT_USER_AGENT = "{{{userAgent}}}"; + private static final Duration DEFAULT_READ_TIMEOUT = Duration.ofSeconds(10); + private static final Duration DEFAULT_CONNECT_TIMEOUT = Duration.ofSeconds(10); + + private String apiUrl; + private String userAgent; + private Duration readTimeout; + private Duration connectTimeout; + + public Configuration() { + this.apiUrl = DEFAULT_API_URL; + this.userAgent = DEFAULT_USER_AGENT; + this.readTimeout = DEFAULT_READ_TIMEOUT; + this.connectTimeout = DEFAULT_CONNECT_TIMEOUT; + } + + public Configuration(String apiUrl) { + this.apiUrl = apiUrl; + this.userAgent = DEFAULT_USER_AGENT; + this.readTimeout = DEFAULT_READ_TIMEOUT; + this.connectTimeout = DEFAULT_CONNECT_TIMEOUT; + } + + /** + * Assert that the configuration is valid. + */ + public void assertValid() throws FgaInvalidParameterException { + // If apiUrl is null/empty/whitespace it will resolve to + // DEFAULT_API_URL when getApiUrl is called. + if (!isNullOrWhitespace(apiUrl)) { + URI uri; + + try { + uri = URI.create(apiUrl); + URL _url = uri.toURL(); + } catch (MalformedURLException | IllegalArgumentException cause) { + throw new FgaInvalidParameterException("apiUrl", "Configuration", cause); + } + + if (isNullOrWhitespace(uri.getScheme())) { + throw new FgaInvalidParameterException("scheme", "Configuration"); + } + + if (isNullOrWhitespace(uri.getHost())) { + throw new FgaInvalidParameterException("hostname", "Configuration"); + } + } + } + + /** + * Construct a new {@link Configuration} with any non-null values of a {@link ConfigurationOverride} and remaining values from this {@link Configuration}. + * + * @param configurationOverride The values to override + * @return A new {@link Configuration} with values of this Configuration mixed with non-null values of configurationOverride + */ + public Configuration override(ConfigurationOverride configurationOverride) { + Configuration result = new Configuration(); + + String overrideApiUrl = configurationOverride.getApiUrl(); + result.apiUrl(overrideApiUrl != null ? overrideApiUrl : apiUrl); + + String overrideUserAgent = configurationOverride.getUserAgent(); + result.userAgent(overrideUserAgent != null ? overrideUserAgent : userAgent); + + Duration overrideReadTimeout = configurationOverride.getReadTimeout(); + result.readTimeout(overrideReadTimeout != null ? overrideReadTimeout : readTimeout); + + Duration overrideConnectTimeout = configurationOverride.getConnectTimeout(); + result.connectTimeout(overrideConnectTimeout != null ? overrideConnectTimeout : connectTimeout); + + return result; + } + + /** + * Set the API URL for the http client. + * + * @param apiUrl The URL. + * @return This object. + */ + public Configuration apiUrl(String apiUrl) { + this.apiUrl = apiUrl; + return this; + } + + /** + * Get the API URL that was set. + * + * @return The url. + */ + @Override + public String getApiUrl() { + if (isNullOrWhitespace(apiUrl)) { + return DEFAULT_API_URL; + } + + return apiUrl; + } + + /** + * Set the user agent. + * + * @param userAgent The user agent. + * @return This object. + */ + public Configuration userAgent(String userAgent) { + this.userAgent = userAgent; + return this; + } + + /** + * Get the user agent. + * + * @return The user agent. + */ + @Override + public String getUserAgent() { + return userAgent; + } + + /** + * Set the read timeout for the http client. + * + *

This is the value used by default for each request, though it can be + * overridden on a per-request basis with a request interceptor.

+ * + * @param readTimeout The read timeout used by default by the http client. + * Setting this value to null resets the timeout to an + * effectively infinite value. + * @return This object. + */ + public Configuration readTimeout(Duration readTimeout) { + this.readTimeout = readTimeout; + return this; + } + + /** + * Get the read timeout that was set. + * + * @return The read timeout, or null if no timeout was set. Null represents + * an infinite wait time. + */ + @Override + public Duration getReadTimeout() { + return readTimeout; + } + + /** + * Sets the connect timeout (in milliseconds) for the http client. + * + *

In the case where a new connection needs to be established, if + * the connection cannot be established within the given {@code + * duration}, then {@link HttpClient#send(HttpRequest, BodyHandler) + * HttpClient::send} throws an {@link HttpConnectTimeoutException}, or + * {@link HttpClient#sendAsync(HttpRequest, BodyHandler) + * HttpClient::sendAsync} completes exceptionally with an + * {@code HttpConnectTimeoutException}. If a new connection does not + * need to be established, for example if a connection can be reused + * from a previous request, then this timeout duration has no effect. + * + * @param connectTimeout connection timeout in milliseconds + * @return This object. + */ + public Configuration connectTimeout(Duration connectTimeout) { + this.connectTimeout = connectTimeout; + return this; + } + + /** + * Get connection timeout (in milliseconds). + * + * @return Timeout in milliseconds + */ + @Override + public Duration getConnectTimeout() { + return connectTimeout; + } +} diff --git a/config/clients/java/template/config-ConfigurationOverride.java.mustache b/config/clients/java/template/config-ConfigurationOverride.java.mustache new file mode 100644 index 00000000..2b9ac4a9 --- /dev/null +++ b/config/clients/java/template/config-ConfigurationOverride.java.mustache @@ -0,0 +1,127 @@ +{{>licenseInfo}} +package {{apiPackage}}.configuration; + +import java.net.http.HttpClient; +import java.net.http.HttpConnectTimeoutException; +import java.net.http.HttpRequest; +import java.time.Duration; + +/** + * Configuration overrides for an api client. Values are initialized to null, and any values unset are intended to fall + * through to the values of a {@link Configuration}. + *

+ * More details on intended usage of this class can be found in the documentation of the {@link Configuration#override(ConfigurationOverride)} method. + */ +public class ConfigurationOverride implements BaseConfiguration { + private String apiUrl; + private String userAgent; + private Duration readTimeout; + private Duration connectTimeout; + + public ConfigurationOverride() { + this.apiUrl = null; + this.userAgent = null; + this.readTimeout = null; + this.connectTimeout = null; + } + + /** + * Set the API URL for the http client. + * + * @param apiUrl The URL. + * @return This object. + */ + public ConfigurationOverride apiUrl(String apiUrl) { + this.apiUrl = apiUrl; + return this; + } + + /** + * Get the API URL that was set. + * + * @return The url. + */ + @Override + public String getApiUrl() { + return apiUrl; + } + + /** + * Set the user agent. + * + * @param userAgent The user agent. + * @return This object. + */ + public ConfigurationOverride userAgent(String userAgent) { + this.userAgent = userAgent; + return this; + } + + /** + * Get the user agent. + * + * @return The user agent. + */ + @Override + public String getUserAgent() { + return userAgent; + } + + /** + * Set the read timeout for the http client. + * + *

This is the value used by default for each request, though it can be + * overridden on a per-request basis with a request interceptor.

+ * + * @param readTimeout The read timeout used by default by the http client. + * Setting this value to null resets the timeout to an + * effectively infinite value. + * @return This object. + */ + public ConfigurationOverride readTimeout(Duration readTimeout) { + this.readTimeout = readTimeout; + return this; + } + + /** + * Get the read timeout that was set. + * + * @return The read timeout, or null if no timeout was set. Null represents + * an infinite wait time. + */ + @Override + public Duration getReadTimeout() { + return readTimeout; + } + + /** + * Sets the connect timeout (in milliseconds) for the http client. + * + *

In the case where a new connection needs to be established, if + * the connection cannot be established within the given {@code + * duration}, then {@link HttpClient#send(HttpRequest, BodyHandler) + * HttpClient::send} throws an {@link HttpConnectTimeoutException}, or + * {@link HttpClient#sendAsync(HttpRequest, BodyHandler) + * HttpClient::sendAsync} completes exceptionally with an + * {@code HttpConnectTimeoutException}. If a new connection does not + * need to be established, for example if a connection can be reused + * from a previous request, then this timeout duration has no effect. + * + * @param connectTimeout connection timeout in milliseconds + * @return This object. + */ + public ConfigurationOverride connectTimeout(Duration connectTimeout) { + this.connectTimeout = connectTimeout; + return this; + } + + /** + * Get connection timeout (in milliseconds). + * + * @return Timeout in milliseconds + */ + @Override + public Duration getConnectTimeout() { + return connectTimeout; + } +} diff --git a/config/clients/java/template/config-ConfigurationTest.java.mustache b/config/clients/java/template/config-ConfigurationTest.java.mustache new file mode 100644 index 00000000..8ad883a3 --- /dev/null +++ b/config/clients/java/template/config-ConfigurationTest.java.mustache @@ -0,0 +1,185 @@ +{{>licenseInfo}} +package {{apiPackage}}.configuration; + +import static org.junit.jupiter.api.Assertions.*; + +import dev.openfga.sdk.errors.*; +import org.junit.jupiter.api.Test; +import java.time.Duration; + +class ConfigurationTest { + private static final String DEFAULT_API_URL = "http://localhost:8080"; + private static final String DEFAULT_USER_AGENT = "{{{userAgent}}}"; + private static final Duration DEFAULT_READ_TIMEOUT = Duration.ofSeconds(10); + private static final Duration DEFAULT_CONNECT_TIMEOUT = Duration.ofSeconds(10); + + @Test + void apiUrl_nullDefaults() throws FgaInvalidParameterException { + // Given + String apiUrl = null; + var config = new Configuration(apiUrl); + + // When + config.assertValid(); + + // Then + assertEquals("http://localhost:8080", config.getApiUrl()); + } + + @Test + void apiUrl_emptyStringDefaults() throws FgaInvalidParameterException { + // Given + String apiUrl = ""; + var config = new Configuration(apiUrl); + + // When + config.assertValid(); + + // Then + assertEquals("http://localhost:8080", config.getApiUrl()); + } + + @Test + void apiUrl_whitespaceStringDefaults() throws FgaInvalidParameterException { + // Given + String apiUrl = " \t\r\n"; + var config = new Configuration(apiUrl); + + // When + config.assertValid(); + + // Then + assertEquals("http://localhost:8080", config.getApiUrl()); + } + + @Test + void apiUrl_stringNoProtocolFails() { + // Given + String apiUrl = "localhost:8080"; + + // When + FgaInvalidParameterException e = assertThrows(FgaInvalidParameterException.class, () -> { + var config = new Configuration(apiUrl); + config.assertValid(); + }); + + // Then + assertEquals("Required parameter apiUrl was invalid when calling Configuration.", e.getMessage()); + } + + @Test + void apiUrl_stringInvalidProtocolFails() { + // Given + String apiUrl = "zzz://localhost:8080"; + + // When + FgaInvalidParameterException e = assertThrows(FgaInvalidParameterException.class, () -> { + var config = new Configuration(apiUrl); + config.assertValid(); + }); + + // Then + assertEquals("Required parameter apiUrl was invalid when calling Configuration.", e.getMessage()); + } + + @Test + void apiUrl_stringNoHostFails() { + // Given + String apiUrl = "http://"; + + // When + FgaInvalidParameterException e = assertThrows(FgaInvalidParameterException.class, () -> { + var config = new Configuration(apiUrl); + config.assertValid(); + }); + + // Then + assertEquals("Required parameter apiUrl was invalid when calling Configuration.", e.getMessage()); + } + + @Test + void apiUrl_stringBadPortFails() { + // Given + String apiUrl = "http://localhost:abcd"; + + // When + FgaInvalidParameterException e = assertThrows(FgaInvalidParameterException.class, () -> { + var config = new Configuration(apiUrl); + config.assertValid(); + }); + + // Then + assertEquals("Required parameter apiUrl was invalid when calling Configuration.", e.getMessage()); + } + + @Test + void defaults() { + // Given + Configuration config = new Configuration(); + + // NOTE: Failures in this test indicate that default values in Configuration have changed. Changing + // the defaults of Configuration can be a suprising and breaking change for consumers. + + // Then + assertEquals(DEFAULT_API_URL, config.getApiUrl()); + assertEquals(DEFAULT_USER_AGENT, config.getUserAgent()); + assertEquals(DEFAULT_READ_TIMEOUT, config.getReadTimeout()); + assertEquals(DEFAULT_CONNECT_TIMEOUT, config.getConnectTimeout()); + } + + @Test + void override_apiUrl() { + // Given + Configuration original = new Configuration(); + ConfigurationOverride configOverride = new ConfigurationOverride().apiUrl("https://override.url"); + + // When + Configuration result = original.override(configOverride); + + // Then + assertEquals("https://override.url", result.getApiUrl()); + assertEquals(DEFAULT_API_URL, original.getApiUrl(), "The Configuration's default apiUrl should be unmodified."); + } + + @Test + void override_userAgent() { + // Given + Configuration original = new Configuration(); + ConfigurationOverride configOverride = new ConfigurationOverride().userAgent("override-agent"); + + // When + Configuration result = original.override(configOverride); + + // Then + assertEquals("override-agent", result.getUserAgent()); + assertEquals(DEFAULT_USER_AGENT, original.getUserAgent(), "The Configuration's default userAgent should be unmodified."); + } + + @Test + void override_readTimeout() { + // Given + Configuration original = new Configuration(); + ConfigurationOverride configOverride = new ConfigurationOverride().readTimeout(Duration.ofDays(7)); + + // When + Configuration result = original.override(configOverride); + + // Then + assertEquals(Duration.ofDays(7), result.getReadTimeout()); + assertEquals(DEFAULT_READ_TIMEOUT, original.getReadTimeout(), "The Configuration's default readTimeout should be unmodified."); + } + + @Test + void override_connectTimeout() { + // Given + Configuration original = new Configuration(); + ConfigurationOverride configOverride = new ConfigurationOverride().connectTimeout(Duration.ofDays(7)); + + // When + Configuration result = original.override(configOverride); + + // Then + assertEquals(Duration.ofDays(7), result.getConnectTimeout()); + assertEquals(DEFAULT_CONNECT_TIMEOUT, original.getConnectTimeout(), "The Configuration's default connectTimeout should be unmodified."); + } +} diff --git a/config/clients/java/template/creds-ApiBearerToken.java.mustache b/config/clients/java/template/creds-ApiBearerToken.java.mustache new file mode 100644 index 00000000..85978783 --- /dev/null +++ b/config/clients/java/template/creds-ApiBearerToken.java.mustache @@ -0,0 +1,18 @@ +{{>licenseInfo}} +package {{invokerPackage}}; + +public class ApiBearerToken { + private String token; + + public ApiBearerToken(String token) { + this.token = token; + } + + public void setToken(String token) { + this.token = token; + } + + public String getToken() { + return this.token; + } +} \ No newline at end of file diff --git a/config/clients/java/template/creds-ClientCredentials.java.mustache b/config/clients/java/template/creds-ClientCredentials.java.mustache new file mode 100644 index 00000000..7d7a62b6 --- /dev/null +++ b/config/clients/java/template/creds-ClientCredentials.java.mustache @@ -0,0 +1,69 @@ +{{>licenseInfo}} +package {{invokerPackage}}; + +import static dev.openfga.util.StringUtil.isNullOrWhitespace; + +import dev.openfga.sdk.errors.FgaInvalidParameterException; + +public class ClientCredentials { + private String clientId; + private String clientSecret; + private String apiTokenIssuer; + private String apiAudience; + + public ClientCredentials() { } + + public ClientCredentials clientId(String clientId) { + this.clientId = clientId; + return this; + } + + public void assertValid() throws FgaInvalidParameterException { + if (isNullOrWhitespace(clientId)) { + throw new FgaInvalidParameterException("clientId", "ClientCredentials"); + } + + if (isNullOrWhitespace(clientSecret)) { + throw new FgaInvalidParameterException("clientSecret", "ClientCredentials"); + } + + if (isNullOrWhitespace(apiTokenIssuer)) { + throw new FgaInvalidParameterException("apiTokenIssuer", "ClientCredentials"); + } + + if (isNullOrWhitespace(apiAudience)) { + throw new FgaInvalidParameterException("apiAudience", "ClientCredentials"); + } + } + + public String getClientId() { + return this.clientId; + } + + public ClientCredentials clientSecret(String clientSecret) { + this.clientSecret = clientSecret; + return this; + } + + public String getClientSecret() { + return this.clientSecret; + } + + public ClientCredentials apiTokenIssuer(String apiTokenIssuer) { + this.apiTokenIssuer = apiTokenIssuer; + return this; + } + + public String getApiTokenIssuer() { + return this.apiTokenIssuer; + } + + public ClientCredentials apiAudience(String apiAudience) { + this.apiAudience = apiAudience; + return this; + } + + public String getApiAudience() { + return this.apiAudience; + } +} \ No newline at end of file diff --git a/config/clients/java/template/creds-ClientCredentialsTest.java.mustache b/config/clients/java/template/creds-ClientCredentialsTest.java.mustache new file mode 100644 index 00000000..313ebc2c --- /dev/null +++ b/config/clients/java/template/creds-ClientCredentialsTest.java.mustache @@ -0,0 +1,101 @@ +{{>licenseInfo}} +package {{invokerPackage}}; + +import static org.junit.jupiter.api.Assertions.*; + +import dev.openfga.sdk.errors.FgaInvalidParameterException; +import java.util.Arrays; +import java.util.List; +import org.junit.jupiter.api.Test; + +public class ClientCredentialsTest { + private static final List INVALID_IDENTIFIERS = Arrays.asList(null, "", "\t\r\n"); + private static final String VALID_CLIENT_ID = "client"; + private static final String VALID_CLIENT_SECRET = "secret"; + private static final String VALID_API_TOKEN_ISSUER = "tokenissuer.fga.example"; + private static final String VALID_API_AUDIENCE = "audience"; + + @Test + public void assertValid_allValid() throws FgaInvalidParameterException { + // When + ClientCredentials creds = new ClientCredentials() + .clientId(VALID_CLIENT_ID) + .clientSecret(VALID_CLIENT_SECRET) + .apiTokenIssuer(VALID_API_TOKEN_ISSUER) + .apiAudience(VALID_API_AUDIENCE); + + // Then + assertEquals(VALID_CLIENT_ID, creds.getClientId()); + assertEquals(VALID_CLIENT_SECRET, creds.getClientSecret()); + assertEquals(VALID_API_TOKEN_ISSUER, creds.getApiTokenIssuer()); + assertEquals(VALID_API_AUDIENCE, creds.getApiAudience()); + } + + @Test + public void assertValid_invalidClientId() { + INVALID_IDENTIFIERS.stream() + // Given + .map(invalid -> new ClientCredentials() + .clientId(invalid) + .clientSecret(VALID_CLIENT_SECRET) + .apiTokenIssuer(VALID_API_TOKEN_ISSUER) + .apiAudience(VALID_API_AUDIENCE)) + // When + .map(creds -> assertThrows(FgaInvalidParameterException.class, creds::assertValid)) + // Then + .forEach(exception -> assertEquals( + "Required parameter clientId was invalid when calling ClientCredentials.", + exception.getMessage())); + } + + @Test + public void assertValid_invalidClientSecret() { + INVALID_IDENTIFIERS.stream() + // Given + .map(invalid -> new ClientCredentials() + .clientId(VALID_CLIENT_ID) + .clientSecret(invalid) + .apiTokenIssuer(VALID_API_TOKEN_ISSUER) + .apiAudience(VALID_API_AUDIENCE)) + // When + .map(creds -> assertThrows(FgaInvalidParameterException.class, creds::assertValid)) + // Then + .forEach(exception -> assertEquals( + "Required parameter clientSecret was invalid when calling ClientCredentials.", + exception.getMessage())); + } + + @Test + public void assertValid_invalidApiTokenIssuer() { + INVALID_IDENTIFIERS.stream() + // Given + .map(invalid -> new ClientCredentials() + .clientId(VALID_CLIENT_ID) + .clientSecret(VALID_CLIENT_SECRET) + .apiTokenIssuer(invalid) + .apiAudience(VALID_API_AUDIENCE)) + // When + .map(creds -> assertThrows(FgaInvalidParameterException.class, creds::assertValid)) + // Then + .forEach(exception -> assertEquals( + "Required parameter apiTokenIssuer was invalid when calling ClientCredentials.", + exception.getMessage())); + } + + @Test + public void assertValid_invalidApiAudience() { + INVALID_IDENTIFIERS.stream() + // Given + .map(invalid -> new ClientCredentials() + .clientId(VALID_CLIENT_ID) + .clientSecret(VALID_CLIENT_SECRET) + .apiTokenIssuer(VALID_API_TOKEN_ISSUER) + .apiAudience(invalid)) + // When + .map(creds -> assertThrows(FgaInvalidParameterException.class, creds::assertValid)) + // Then + .forEach(exception -> assertEquals( + "Required parameter apiAudience was invalid when calling ClientCredentials.", + exception.getMessage())); + } +} diff --git a/config/clients/java/template/creds-CredentialsMethod.java.mustache b/config/clients/java/template/creds-CredentialsMethod.java.mustache new file mode 100644 index 00000000..90a65f14 --- /dev/null +++ b/config/clients/java/template/creds-CredentialsMethod.java.mustache @@ -0,0 +1,30 @@ +{{>licenseInfo}} +package {{invokerPackage}}; + +/** + * + */ +public enum CredentialsMethod { + NONE, + API_BEARER_TOKEN, + CLIENT_CREDENTIALS; + + private ApiBearerToken apiBearerToken; + private ClientCredentials clientCredentials; + + public static CredentialsMethod none() { + return NONE; + } + + public static CredentialsMethod apiBearerToken(ApiBearerToken apiBearerToken) { + CredentialsMethod it = API_BEARER_TOKEN; + it.apiBearerToken = apiBearerToken; + return it; + } + + public static CredentialsMethod clientCredentials(ClientCredentials clientCredentials) { + CredentialsMethod it = CLIENT_CREDENTIALS; + it.clientCredentials = clientCredentials; + return it; + } +} \ No newline at end of file diff --git a/config/clients/java/template/enum_outer_doc.mustache b/config/clients/java/template/enum_outer_doc.mustache new file mode 100644 index 00000000..20c512aa --- /dev/null +++ b/config/clients/java/template/enum_outer_doc.mustache @@ -0,0 +1,7 @@ +# {{classname}} + +## Enum + +{{#allowableValues}}{{#enumVars}} +* `{{name}}` (value: `{{{value}}}`) +{{/enumVars}}{{/allowableValues}} diff --git a/config/clients/java/template/gitignore.mustache b/config/clients/java/template/gitignore.mustache new file mode 100644 index 00000000..f847fdbd --- /dev/null +++ b/config/clients/java/template/gitignore.mustache @@ -0,0 +1,25 @@ +*.class + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.ear + +# exclude jar for gradle wrapper +!gradle/wrapper/*.jar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + +# build files +**/target +target +.gradle +build + +# openapi files +/api/openapi.yaml +VERSION.txt diff --git a/config/clients/java/template/gitignore_custom.mustache b/config/clients/java/template/gitignore_custom.mustache new file mode 100644 index 00000000..e69de29b diff --git a/config/clients/java/template/gradle-wrapper.jar b/config/clients/java/template/gradle-wrapper.jar new file mode 100644 index 00000000..033e24c4 Binary files /dev/null and b/config/clients/java/template/gradle-wrapper.jar differ diff --git a/config/clients/java/template/gradle-wrapper.properties.mustache b/config/clients/java/template/gradle-wrapper.properties.mustache new file mode 100644 index 00000000..a1f2792d --- /dev/null +++ b/config/clients/java/template/gradle-wrapper.properties.mustache @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-all.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/config/clients/java/template/gradlew.bat.mustache b/config/clients/java/template/gradlew.bat.mustache new file mode 100644 index 00000000..6a68175e --- /dev/null +++ b/config/clients/java/template/gradlew.bat.mustache @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS=-Dfile.encoding=UTF-8 "-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/config/clients/java/template/gradlew.mustache b/config/clients/java/template/gradlew.mustache new file mode 100755 index 00000000..005bcde0 --- /dev/null +++ b/config/clients/java/template/gradlew.mustache @@ -0,0 +1,234 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='-Dfile.encoding=UTF-8 "-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/config/clients/java/template/jackson_annotations.mustache b/config/clients/java/template/jackson_annotations.mustache new file mode 100644 index 00000000..c7413447 --- /dev/null +++ b/config/clients/java/template/jackson_annotations.mustache @@ -0,0 +1,20 @@ +{{! + If this is map and items are nullable, make sure that nulls are included. + To determine what JsonInclude.Include method to use, consider the following: + * If the field is required, always include it, even if it is null. + * Else use custom behaviour, IOW use whatever is defined on the object mapper + }} + @JsonProperty(JSON_PROPERTY_{{nameInSnakeCase}}) + @JsonInclude({{#isMap}}{{#items.isNullable}}content = JsonInclude.Include.ALWAYS, {{/items.isNullable}}{{/isMap}}value = JsonInclude.Include.{{#required}}ALWAYS{{/required}}{{^required}}USE_DEFAULTS{{/required}}) + {{#withXml}} + {{^isContainer}} + @JacksonXmlProperty({{#isXmlAttribute}}isAttribute = true, {{/isXmlAttribute}}{{#xmlNamespace}}namespace="{{.}}", {{/xmlNamespace}}localName = "{{xmlName}}{{^xmlName}}{{baseName}}{{/xmlName}}") + {{/isContainer}} + {{#isContainer}} + {{#xmlName}} + // xmlName={{.}} + {{/xmlName}} + @JacksonXmlProperty({{#xmlNamespace}}namespace="{{.}}", {{/xmlNamespace}}localName = "{{#xmlName}}{{xmlName}}{{/xmlName}}{{^xmlName}}{{#xmlName}}{{xmlName}}{{/xmlName}}{{^xmlName}}{{baseName}}{{/xmlName}}{{/xmlName}}") + @JacksonXmlElementWrapper(useWrapping = {{isXmlWrapped}}{{#xmlNamespace}}, namespace="{{.}}"{{/xmlNamespace}}{{#isXmlWrapped}}, localName = "{{#xmlName}}{{xmlName}}{{/xmlName}}{{^xmlName}}{{baseName}}{{/xmlName}}"{{/isXmlWrapped}}) + {{/isContainer}} + {{/withXml}} \ No newline at end of file diff --git a/config/clients/java/template/libraries/native/AbstractOpenApiSchema.mustache b/config/clients/java/template/libraries/native/AbstractOpenApiSchema.mustache new file mode 100644 index 00000000..a9401a9f --- /dev/null +++ b/config/clients/java/template/libraries/native/AbstractOpenApiSchema.mustache @@ -0,0 +1,135 @@ +{{>licenseInfo}} + +package {{modelPackage}}; + +import java.util.Objects; +import java.lang.reflect.Type; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * Abstract class for oneOf,anyOf schemas defined in OpenAPI spec + */ +public abstract class AbstractOpenApiSchema { + + // store the actual instance of the schema/object + private Object instance; + + // is nullable + private Boolean isNullable; + + // schema type (e.g. oneOf, anyOf) + private final String schemaType; + + public AbstractOpenApiSchema(String schemaType, Boolean isNullable) { + this.schemaType = schemaType; + this.isNullable = isNullable; + } + + /** + * Get the list of oneOf/anyOf composed schemas allowed to be stored in this object + * + * @return an instance of the actual schema/object + */ + public abstract Map> getSchemas(); + + /** + * Get the actual instance + * + * @return an instance of the actual schema/object + */ + @JsonValue + public Object getActualInstance() {return instance;} + + /** + * Set the actual instance + * + * @param instance the actual instance of the schema/object + */ + public void setActualInstance(Object instance) {this.instance = instance;} + + /** + * Get the instant recursively when the schemas defined in oneOf/anyof happen to be oneOf/anyOf schema as well + * + * @return an instance of the actual schema/object + */ + public Object getActualInstanceRecursively() { + return getActualInstanceRecursively(this); + } + + private Object getActualInstanceRecursively(AbstractOpenApiSchema object) { + if (object.getActualInstance() == null) { + return null; + } else if (object.getActualInstance() instanceof AbstractOpenApiSchema) { + return getActualInstanceRecursively((AbstractOpenApiSchema)object.getActualInstance()); + } else { + return object.getActualInstance(); + } + } + + /** + * Get the schema type (e.g. anyOf, oneOf) + * + * @return the schema type + */ + public String getSchemaType() { + return schemaType; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class ").append(getClass()).append(" {\n"); + sb.append(" instance: ").append(toIndentedString(instance)).append("\n"); + sb.append(" isNullable: ").append(toIndentedString(isNullable)).append("\n"); + sb.append(" schemaType: ").append(toIndentedString(schemaType)).append("\n"); + 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 "); + } + + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + AbstractOpenApiSchema a = (AbstractOpenApiSchema) o; + return Objects.equals(this.instance, a.instance) && + Objects.equals(this.isNullable, a.isNullable) && + Objects.equals(this.schemaType, a.schemaType); + } + + @Override + public int hashCode() { + return Objects.hash(instance, isNullable, schemaType); + } + + /** + * Is nullable + * + * @return true if it's nullable + */ + public Boolean isNullable() { + if (Boolean.TRUE.equals(isNullable)) { + return Boolean.TRUE; + } else { + return Boolean.FALSE; + } + } + +{{>libraries/native/additional_properties}} + +} diff --git a/config/clients/java/template/libraries/native/ApiClient.mustache b/config/clients/java/template/libraries/native/ApiClient.mustache new file mode 100644 index 00000000..3d6bf71d --- /dev/null +++ b/config/clients/java/template/libraries/native/ApiClient.mustache @@ -0,0 +1,323 @@ +{{>licenseInfo}} +package {{invokerPackage}}; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +{{#openApiNullable}} +import org.openapitools.jackson.nullable.JsonNullableModule; +{{/openApiNullable}} + +import dev.openfga.util.Pair; +import java.io.InputStream; +import java.net.URI; +import java.net.URLEncoder; +import java.net.http.HttpClient; +import java.net.http.HttpConnectTimeoutException; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.Duration; +import java.time.OffsetDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.StringJoiner; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +import static java.nio.charset.StandardCharsets.UTF_8; + +/** + * Configuration and utility class for API clients. + * + *

This class can be constructed and modified, then used to instantiate the + * various API classes. The API classes use the settings in this class to + * configure themselves, but otherwise do not store a link to this class.

+ * + *

This class is mutable and not synchronized, so it is not thread-safe. + * The API classes generated from this are immutable and thread-safe.

+ * + *

The setter methods of this class return the current object to facilitate + * a fluent style of configuration.

+ */ +public class ApiClient { + + private HttpClient.Builder builder; + private ObjectMapper mapper; + private Consumer interceptor; + private Consumer> responseInterceptor; + private Consumer> asyncResponseInterceptor; + + /** + * Create an instance of ApiClient. + */ + public ApiClient() { + this.builder = createDefaultHttpClientBuilder(); + this.mapper = createDefaultObjectMapper(); + interceptor = null; + responseInterceptor = null; + asyncResponseInterceptor = null; + } + + /** + * Create an instance of ApiClient. + *

+ * In other contexts, note that any settings in a {@link Configuration} + * will take precedence over equivalent settings in the + * {@link HttpClient.Builder} here. + * + * @param builder Http client builder. + * @param mapper Object mapper. + */ + public ApiClient(HttpClient.Builder builder, ObjectMapper mapper) { + this.builder = builder; + this.mapper = mapper; + interceptor = null; + responseInterceptor = null; + asyncResponseInterceptor = null; + } + + private static String valueToString(Object value) { + if (value == null) { + return ""; + } + if (value instanceof OffsetDateTime) { + return ((OffsetDateTime) value).format(DateTimeFormatter.ISO_OFFSET_DATE_TIME); + } + return value.toString(); + } + + /** + * URL encode a string in the UTF-8 encoding. + * + * @param s String to encode. + * @return URL-encoded representation of the input string. + */ + public static String urlEncode(String s) { + return URLEncoder.encode(s, UTF_8).replaceAll("\\+", "%20"); + } + + /** + * Convert a URL query name/value parameter to a list of encoded {@link Pair} + * objects. + * + *

The value can be null, in which case an empty list is returned.

+ * + * @param name The query name parameter. + * @param value The query value, which may not be a collection but may be + * null. + * @return A singleton list of the {@link Pair} objects representing the input + * parameters, which is encoded for use in a URL. If the value is null, an + * empty list is returned. + */ + public static List parameterToPairs(String name, Object value) { + if (name == null || name.isEmpty() || value == null) { + return Collections.emptyList(); + } + return Collections.singletonList(new Pair(urlEncode(name), urlEncode(valueToString(value)))); + } + + /** + * Convert a URL query name/collection parameter to a list of encoded + * {@link Pair} objects. + * + * @param collectionFormat The swagger collectionFormat string (csv, tsv, etc). + * @param name The query name parameter. + * @param values A collection of values for the given query name, which may be + * null. + * @return A list of {@link Pair} objects representing the input parameters, + * which is encoded for use in a URL. If the values collection is null, an + * empty list is returned. + */ + public static List parameterToPairs( + String collectionFormat, String name, Collection values) { + if (name == null || name.isEmpty() || values == null || values.isEmpty()) { + return Collections.emptyList(); + } + + // get the collection format (default: csv) + String format = collectionFormat == null || collectionFormat.isEmpty() ? "csv" : collectionFormat; + + // create the params based on the collection format + if ("multi".equals(format)) { + return values.stream() + .map(value -> new Pair(urlEncode(name), urlEncode(valueToString(value)))) + .collect(Collectors.toList()); + } + + String delimiter; + switch(format) { + case "csv": + delimiter = urlEncode(","); + break; + case "ssv": + delimiter = urlEncode(" "); + break; + case "tsv": + delimiter = urlEncode("\t"); + break; + case "pipes": + delimiter = urlEncode("|"); + break; + default: + throw new IllegalArgumentException("Illegal collection format: " + collectionFormat); + } + + StringJoiner joiner = new StringJoiner(delimiter); + for (Object value : values) { + joiner.add(urlEncode(valueToString(value))); + } + + return Collections.singletonList(new Pair(urlEncode(name), joiner.toString())); + } + + protected ObjectMapper createDefaultObjectMapper() { + ObjectMapper mapper = new ObjectMapper(); + mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + mapper.configure(DeserializationFeature.FAIL_ON_INVALID_SUBTYPE, false); + mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + mapper.enable(SerializationFeature.WRITE_ENUMS_USING_TO_STRING); + mapper.enable(DeserializationFeature.READ_ENUMS_USING_TO_STRING); + mapper.disable(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE); + mapper.registerModule(new JavaTimeModule()); + {{#openApiNullable}} + mapper.registerModule(new JsonNullableModule()); + {{/openApiNullable}} + return mapper; + } + + protected String getDefaultBaseUri() { + return "{{{basePath}}}"; + } + + protected HttpClient.Builder createDefaultHttpClientBuilder() { + return HttpClient.newBuilder(); + } + + /** + * Set a custom {@link HttpClient.Builder} object to use when creating the + * {@link HttpClient} that is used by the API client. + *

+ * In other contexts, note that any settings in a {@link Configuration} + * will take precedence over equivalent settings in the + * {@link HttpClient.Builder} here. + * + * @param builder Custom client builder. + * @return This object. + */ + public ApiClient setHttpClientBuilder(HttpClient.Builder builder) { + this.builder = builder; + return this; + } + + /** + * Get an {@link HttpClient} based on the current {@link HttpClient.Builder}. + * + *

The returned object is immutable and thread-safe.

+ * + * @return The HTTP client. + */ + public HttpClient getHttpClient() { + return builder.build(); + } + + /** + * Set a custom {@link ObjectMapper} to serialize and deserialize the request + * and response bodies. + * + * @param mapper Custom object mapper. + * @return This object. + */ + public ApiClient setObjectMapper(ObjectMapper mapper) { + this.mapper = mapper; + return this; + } + + /** + * Get a copy of the current {@link ObjectMapper}. + * + * @return A copy of the current object mapper. + */ + public ObjectMapper getObjectMapper() { + return mapper.copy(); + } + + /** + * Set a custom request interceptor. + * + *

A request interceptor is a mechanism for altering each request before it + * is sent. After the request has been fully configured but not yet built, the + * request builder is passed into this function for further modification, + * after which it is sent out.

+ * + *

This is useful for altering the requests in a custom manner, such as + * adding headers. It could also be used for logging and monitoring.

+ * + * @param interceptor A function invoked before creating each request. A value + * of null resets the interceptor to a no-op. + * @return This object. + */ + public ApiClient setRequestInterceptor(Consumer interceptor) { + this.interceptor = interceptor; + return this; + } + + /** + * Get the custom interceptor. + * + * @return The custom interceptor that was set, or null if there isn't any. + */ + public Consumer getRequestInterceptor() { + return interceptor; + } + + /** + * Set a custom response interceptor. + * + *

This is useful for logging, monitoring or extraction of header variables

+ * + * @param interceptor A function invoked before creating each request. A value + * of null resets the interceptor to a no-op. + * @return This object. + */ + public ApiClient setResponseInterceptor(Consumer> interceptor) { + this.responseInterceptor = interceptor; + return this; + } + + /** + * Get the custom response interceptor. + * + * @return The custom interceptor that was set, or null if there isn't any. + */ + public Consumer> getResponseInterceptor() { + return responseInterceptor; + } + + /** + * Set a custom async response interceptor. Use this interceptor when asyncNative is set to 'true'. + * + *

This is useful for logging, monitoring or extraction of header variables

+ * + * @param interceptor A function invoked before creating each request. A value + * of null resets the interceptor to a no-op. + * @return This object. + */ + public ApiClient setAsyncResponseInterceptor(Consumer> interceptor) { + this.asyncResponseInterceptor = interceptor; + return this; + } + + /** + * Get the custom async response interceptor. Use this interceptor when asyncNative is set to 'true'. + * + * @return The custom interceptor that was set, or null if there isn't any. + */ + public Consumer> getAsyncResponseInterceptor() { + return asyncResponseInterceptor; + } +} diff --git a/config/clients/java/template/libraries/native/ApiResponse.mustache b/config/clients/java/template/libraries/native/ApiResponse.mustache new file mode 100644 index 00000000..1e277319 --- /dev/null +++ b/config/clients/java/template/libraries/native/ApiResponse.mustache @@ -0,0 +1,58 @@ +{{>licenseInfo}} + +package {{invokerPackage}}; + +import java.util.List; +import java.util.Map; +{{#caseInsensitiveResponseHeaders}} +import java.util.Map.Entry; +import java.util.TreeMap; +{{/caseInsensitiveResponseHeaders}} + +/** + * API response returned by API call. + * + * @param The type of data that is deserialized from response body + */ +public class ApiResponse { + final private int statusCode; + final private Map> headers; + final private T data; + + /** + * @param statusCode The status code of HTTP response + * @param headers The headers of HTTP response + */ + public ApiResponse(int statusCode, Map> headers) { + this(statusCode, headers, null); + } + + /** + * @param statusCode The status code of HTTP response + * @param headers The headers of HTTP response + * @param data The object deserialized from response bod + */ + public ApiResponse(int statusCode, Map> headers, T data) { + this.statusCode = statusCode; + {{#caseInsensitiveResponseHeaders}} + Map> responseHeaders = new TreeMap>(String.CASE_INSENSITIVE_ORDER); + for(Entry> entry : headers.entrySet()){ + responseHeaders.put(entry.getKey().toLowerCase(), entry.getValue()); + } + {{/caseInsensitiveResponseHeaders}} + this.headers = {{#caseInsensitiveResponseHeaders}}responseHeaders{{/caseInsensitiveResponseHeaders}}{{^caseInsensitiveResponseHeaders}}headers{{/caseInsensitiveResponseHeaders}}; + this.data = data; + } + + public int getStatusCode() { + return statusCode; + } + + public Map> getHeaders() { + return headers; + } + + public T getData() { + return data; + } +} diff --git a/config/clients/java/template/libraries/native/additional_properties.mustache b/config/clients/java/template/libraries/native/additional_properties.mustache new file mode 100644 index 00000000..8e718279 --- /dev/null +++ b/config/clients/java/template/libraries/native/additional_properties.mustache @@ -0,0 +1,45 @@ +{{#additionalPropertiesType}} + /** + * A container for additional, undeclared properties. + * This is a holder for any undeclared properties as specified with + * the 'additionalProperties' keyword in the OAS document. + */ + private Map additionalProperties; + + /** + * Set the additional (undeclared) property with the specified name and value. + * If the property does not already exist, create it otherwise replace it. + * @param key the name of the property + * @param value the value of the property + * @return self reference + */ + @JsonAnySetter + public {{classname}} putAdditionalProperty(String key, {{{.}}} value) { + if (this.additionalProperties == null) { + this.additionalProperties = new HashMap(); + } + this.additionalProperties.put(key, value); + return this; + } + + /** + * Return the additional (undeclared) properties. + * @return the additional (undeclared) properties + */ + @JsonAnyGetter + public Map getAdditionalProperties() { + return additionalProperties; + } + + /** + * Return the additional (undeclared) property with the specified name. + * @param key the name of the property + * @return the additional (undeclared) property with the specified name + */ + public {{{.}}} getAdditionalProperty(String key) { + if (this.additionalProperties == null) { + return null; + } + return this.additionalProperties.get(key); + } +{{/additionalPropertiesType}} diff --git a/config/clients/java/template/libraries/native/anyof_model.mustache b/config/clients/java/template/libraries/native/anyof_model.mustache new file mode 100644 index 00000000..7ce1c7a2 --- /dev/null +++ b/config/clients/java/template/libraries/native/anyof_model.mustache @@ -0,0 +1,360 @@ +import java.io.IOException; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; +import {{invokerPackage}}.JSON; + +{{>additionalModelTypeAnnotations}}{{>xmlAnnotation}} +@JsonDeserialize(using={{classname}}.{{classname}}Deserializer.class) +@JsonSerialize(using = {{classname}}.{{classname}}Serializer.class) +public class {{classname}} extends AbstractOpenApiSchema{{#vendorExtensions.x-implements}}, {{{.}}}{{/vendorExtensions.x-implements}} { + private static final Logger log = Logger.getLogger({{classname}}.class.getName()); + + public static class {{classname}}Serializer extends StdSerializer<{{classname}}> { + public {{classname}}Serializer(Class<{{classname}}> t) { + super(t); + } + + public {{classname}}Serializer() { + this(null); + } + + @Override + public void serialize({{classname}} value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonProcessingException { + jgen.writeObject(value.getActualInstance()); + } + } + + public static class {{classname}}Deserializer extends StdDeserializer<{{classname}}> { + public {{classname}}Deserializer() { + this({{classname}}.class); + } + + public {{classname}}Deserializer(Class vc) { + super(vc); + } + + @Override + public {{classname}} deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException { + JsonNode tree = jp.readValueAsTree(); + + Object deserialized = null; + {{#discriminator}} + Class cls = JSON.getClassForElement(tree, {{classname}}.class); + if (cls != null) { + // When the OAS schema includes a discriminator, use the discriminator value to + // discriminate the anyOf schemas. + // Get the discriminator mapping value to get the class. + deserialized = tree.traverse(jp.getCodec()).readValueAs(cls); + {{classname}} ret = new {{classname}}(); + ret.setActualInstance(deserialized); + return ret; + } + {{/discriminator}} + {{#anyOf}} + // deserialize {{{.}}} + try { + deserialized = tree.traverse(jp.getCodec()).readValueAs({{{.}}}.class); + {{classname}} ret = new {{classname}}(); + ret.setActualInstance(deserialized); + return ret; + } catch (Exception e) { + // deserialization failed, continue, log to help debugging + log.log(Level.FINER, "Input data does not match '{{classname}}'", e); + } + + {{/anyOf}} + throw new IOException(String.format("Failed deserialization for {{classname}}: no match found")); + } + + /** + * Handle deserialization of the 'null' value. + */ + @Override + public {{classname}} getNullValue(DeserializationContext ctxt) throws JsonMappingException { + {{#isNullable}} + return null; + {{/isNullable}} + {{^isNullable}} + throw new JsonMappingException(ctxt.getParser(), "{{classname}} cannot be null"); + {{/isNullable}} + } + } + + // store a list of schema names defined in anyOf + public static final Map> schemas = new HashMap>(); + + public {{classname}}() { + super("anyOf", {{#isNullable}}Boolean.TRUE{{/isNullable}}{{^isNullable}}Boolean.FALSE{{/isNullable}}); + } +{{> libraries/native/additional_properties }} + {{#additionalPropertiesType}} + /** + * Return true if this {{name}} object is equal to o. + */ + @Override + public boolean equals(Object o) { + return super.equals(o) && Objects.equals(this.additionalProperties, (({{classname}})o).additionalProperties); + } + + @Override + public int hashCode() { + return Objects.hash(getActualInstance(), isNullable(), getSchemaType(), additionalProperties); + } + {{/additionalPropertiesType}} + {{#anyOf}} + public {{classname}}({{{.}}} o) { + super("anyOf", {{#isNullable}}Boolean.TRUE{{/isNullable}}{{^isNullable}}Boolean.FALSE{{/isNullable}}); + setActualInstance(o); + } + + {{/anyOf}} + static { + {{#anyOf}} + schemas.put("{{{.}}}", {{{.}}}.class); + {{/anyOf}} + JSON.registerDescendants({{classname}}.class, Collections.unmodifiableMap(schemas)); + {{#discriminator}} + // Initialize and register the discriminator mappings. + Map> mappings = new HashMap>(); + {{#mappedModels}} + mappings.put("{{mappingName}}", {{modelName}}.class); + {{/mappedModels}} + mappings.put("{{name}}", {{classname}}.class); + JSON.registerDiscriminator({{classname}}.class, "{{propertyBaseName}}", mappings); + {{/discriminator}} + } + + @Override + public Map> getSchemas() { + return {{classname}}.schemas; + } + + /** + * Set the instance that matches the anyOf child schema, check + * the instance parameter is valid against the anyOf child schemas: + * {{#anyOf}}{{{.}}}{{^-last}}, {{/-last}}{{/anyOf}} + * + * It could be an instance of the 'anyOf' schemas. + * The anyOf child schemas may themselves be a composed schema (allOf, anyOf, anyOf). + */ + @Override + public void setActualInstance(Object instance) { + {{#isNullable}} + if (instance == null) { + super.setActualInstance(instance); + return; + } + + {{/isNullable}} + {{#anyOf}} + if (JSON.isInstanceOf({{{.}}}.class, instance, new HashSet>())) { + super.setActualInstance(instance); + return; + } + + {{/anyOf}} + throw new RuntimeException("Invalid instance type. Must be {{#anyOf}}{{{.}}}{{^-last}}, {{/-last}}{{/anyOf}}"); + } + + /** + * Get the actual instance, which can be the following: + * {{#anyOf}}{{{.}}}{{^-last}}, {{/-last}}{{/anyOf}} + * + * @return The actual instance ({{#anyOf}}{{{.}}}{{^-last}}, {{/-last}}{{/anyOf}}) + */ + @Override + public Object getActualInstance() { + return super.getActualInstance(); + } + + {{#anyOf}} + /** + * Get the actual instance of `{{{.}}}`. If the actual instance is not `{{{.}}}`, + * the ClassCastException will be thrown. + * + * @return The actual instance of `{{{.}}}` + * @throws ClassCastException if the instance is not `{{{.}}}` + */ + public {{{.}}} get{{{.}}}() throws ClassCastException { + return ({{{.}}})super.getActualInstance(); + } + + {{/anyOf}} + +{{#supportUrlQuery}} + + /** + * Convert the instance into URL query string. + * + * @return URL query string + */ + public String toUrlQueryString() { + return toUrlQueryString(null); + } + + /** + * Convert the instance into URL query string. + * + * @param prefix prefix of the query string + * @return URL query string + */ + public String toUrlQueryString(String prefix) { + String suffix = ""; + String containerSuffix = ""; + String containerPrefix = ""; + if (prefix == null) { + // style=form, explode=true, e.g. /pet?name=cat&type=manx + prefix = ""; + } else { + // deepObject style e.g. /pet?id[name]=cat&id[type]=manx + prefix = prefix + "["; + suffix = "]"; + containerSuffix = "]"; + containerPrefix = "["; + } + + StringJoiner joiner = new StringJoiner("&"); + + {{#composedSchemas.oneOf}} + if (getActualInstance() instanceof {{{dataType}}}) { + {{#isArray}} + {{#items.isPrimitiveType}} + {{#uniqueItems}} + if (getActualInstance() != null) { + int i = 0; + for ({{{items.dataType}}} _item : ({{{dataType}}})getActualInstance()) { + joiner.add(String.format("%s{{baseName}}%s%s=%s", prefix, suffix, + "".equals(suffix) ? "" : String.format("%s%d%s", containerPrefix, i, containerSuffix), + URLEncoder.encode(String.valueOf(_item), StandardCharsets.UTF_8).replaceAll("\\+", "%20"))); + } + i++; + } + {{/uniqueItems}} + {{^uniqueItems}} + if (getActualInstance() != null) { + for (int i = 0; i < (({{{dataType}}})getActualInstance()).size(); i++) { + joiner.add(String.format("%s{{baseName}}%s%s=%s", prefix, suffix, + "".equals(suffix) ? "" : String.format("%s%d%s", containerPrefix, i, containerSuffix), + URLEncoder.encode(String.valueOf(getActualInstance().get(i)), StandardCharsets.UTF_8).replaceAll("\\+", "%20"))); + } + } + {{/uniqueItems}} + {{/items.isPrimitiveType}} + {{^items.isPrimitiveType}} + {{#items.isModel}} + {{#uniqueItems}} + if (getActualInstance() != null) { + int i = 0; + for ({{{items.dataType}}} _item : ({{{dataType}}})getActualInstance()) { + if (_item != null) { + joiner.add(_item.toUrlQueryString(String.format("%s{{baseName}}%s%s", prefix, suffix, + "".equals(suffix) ? "" : String.format("%s%d%s", containerPrefix, i, containerSuffix)))); + } + } + i++; + } + {{/uniqueItems}} + {{^uniqueItems}} + if (getActualInstance() != null) { + for (int i = 0; i < (({{{dataType}}})getActualInstance()).size(); i++) { + if ((({{{dataType}}})getActualInstance()).get(i) != null) { + joiner.add((({{{items.dataType}}})getActualInstance()).get(i).toUrlQueryString(String.format("%s{{baseName}}%s%s", prefix, suffix, + "".equals(suffix) ? "" : String.format("%s%d%s", containerPrefix, i, containerSuffix)))); + } + } + } + {{/uniqueItems}} + {{/items.isModel}} + {{^items.isModel}} + {{#uniqueItems}} + if (getActualInstance() != null) { + int i = 0; + for ({{{items.dataType}}} _item : ({{{dataType}}})getActualInstance()) { + if (_item != null) { + joiner.add(String.format("%s{{baseName}}%s%s=%s", prefix, suffix, + "".equals(suffix) ? "" : String.format("%s%d%s", containerPrefix, i, containerSuffix), + URLEncoder.encode(String.valueOf(_item), StandardCharsets.UTF_8).replaceAll("\\+", "%20"))); + } + i++; + } + } + {{/uniqueItems}} + {{^uniqueItems}} + if (getActualInstance() != null) { + for (int i = 0; i < (({{{dataType}}})getActualInstance()).size(); i++) { + if (getActualInstance().get(i) != null) { + joiner.add(String.format("%s{{baseName}}%s%s=%s", prefix, suffix, + "".equals(suffix) ? "" : String.format("%s%d%s", containerPrefix, i, containerSuffix), + URLEncoder.encode(String.valueOf((({{{dataType}}})getActualInstance()).get(i)), StandardCharsets.UTF_8).replaceAll("\\+", "%20"))); + } + } + } + {{/uniqueItems}} + {{/items.isModel}} + {{/items.isPrimitiveType}} + {{/isArray}} + {{^isArray}} + {{#isMap}} + {{#items.isPrimitiveType}} + if (getActualInstance() != null) { + for (String _key : (({{{dataType}}})getActualInstance()).keySet()) { + joiner.add(String.format("%s{{baseName}}%s%s=%s", prefix, suffix, + "".equals(suffix) ? "" : String.format("%s%d%s", containerPrefix, _key, containerSuffix), + getActualInstance().get(_key), URLEncoder.encode(String.valueOf((({{{dataType}}})getActualInstance()).get(_key)), StandardCharsets.UTF_8).replaceAll("\\+", "%20"))); + } + } + {{/items.isPrimitiveType}} + {{^items.isPrimitiveType}} + if (getActualInstance() != null) { + for (String _key : (({{{dataType}}})getActualInstance()).keySet()) { + if ((({{{dataType}}})getActualInstance()).get(_key) != null) { + joiner.add((({{{items.dataType}}})getActualInstance()).get(_key).toUrlQueryString(String.format("%s{{baseName}}%s%s", prefix, suffix, + "".equals(suffix) ? "" : String.format("%s%d%s", containerPrefix, _key, containerSuffix)))); + } + } + } + {{/items.isPrimitiveType}} + {{/isMap}} + {{^isMap}} + {{#isPrimitiveType}} + if (getActualInstance() != null) { + joiner.add(String.format("%s{{{baseName}}}%s=%s", prefix, suffix, URLEncoder.encode(String.valueOf(getActualInstance()), StandardCharsets.UTF_8).replaceAll("\\+", "%20"))); + } + {{/isPrimitiveType}} + {{^isPrimitiveType}} + {{#isModel}} + if (getActualInstance() != null) { + joiner.add((({{{dataType}}})getActualInstance()).toUrlQueryString(prefix + "{{{baseName}}}" + suffix)); + } + {{/isModel}} + {{^isModel}} + if (getActualInstance() != null) { + joiner.add(String.format("%s{{{baseName}}}%s=%s", prefix, suffix, URLEncoder.encode(String.valueOf(getActualInstance()), StandardCharsets.UTF_8).replaceAll("\\+", "%20"))); + } + {{/isModel}} + {{/isPrimitiveType}} + {{/isMap}} + {{/isArray}} + return joiner.toString(); + } + {{/composedSchemas.oneOf}} + return null; + } +{{/supportUrlQuery}} + +} diff --git a/config/clients/java/template/libraries/native/api.mustache b/config/clients/java/template/libraries/native/api.mustache new file mode 100644 index 00000000..4414eb88 --- /dev/null +++ b/config/clients/java/template/libraries/native/api.mustache @@ -0,0 +1,655 @@ +{{>licenseInfo}} +package {{package}}; + +import {{apiPackage}}.configuration.Configuration; +import {{apiPackage}}.configuration.ConfigurationOverride; +import {{invokerPackage}}.ApiClient; +import {{invokerPackage}}.ApiResponse; +import dev.openfga.util.Pair; +import dev.openfga.sdk.errors.ApiException; +import dev.openfga.sdk.errors.FgaInvalidParameterException; + +{{#imports}} +import {{import}}; +{{/imports}} + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; + +{{#hasFormParamsInSpec}} +import org.apache.http.HttpEntity; +import org.apache.http.NameValuePair; +import org.apache.http.entity.mime.MultipartEntityBuilder; +import org.apache.http.message.BasicNameValuePair; +import org.apache.http.client.entity.UrlEncodedFormEntity; + +{{/hasFormParamsInSpec}} +import java.io.InputStream; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.net.http.HttpRequest; +import java.nio.channels.Channels; +import java.nio.channels.Pipe; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.Duration; + +import java.util.ArrayList; +import java.util.StringJoiner; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; +{{#asyncNative}} + +import java.util.concurrent.CompletableFuture; +{{/asyncNative}} + +{{#operations}} +public class {{classname}} { + private final HttpClient memberVarHttpClient; + private final ObjectMapper memberVarObjectMapper; + private final Configuration configuration; + private final Consumer memberVarInterceptor; + private final Consumer> memberVarResponseInterceptor; + private final Consumer> memberVarAsyncResponseInterceptor; + + public {{classname}}(ApiClient apiClient, Configuration configuration) { + memberVarHttpClient = apiClient.getHttpClient(); + memberVarObjectMapper = apiClient.getObjectMapper(); + this.configuration = configuration; + memberVarInterceptor = apiClient.getRequestInterceptor(); + memberVarResponseInterceptor = apiClient.getResponseInterceptor(); + memberVarAsyncResponseInterceptor = apiClient.getAsyncResponseInterceptor(); + } + {{#asyncNative}} + + private ApiException getApiException(String operationId, HttpResponse response) { + String message = formatExceptionMessage(operationId, response.statusCode(), response.body()); + return new ApiException(response.statusCode(), message, response.headers(), response.body()); + } + {{/asyncNative}} + {{^asyncNative}} + + protected ApiException getApiException(String operationId, HttpResponse response) throws IOException { + String body = response.body() == null ? null : new String(response.body().readAllBytes()); + String message = formatExceptionMessage(operationId, response.statusCode(), body); + return new ApiException(response.statusCode(), message, response.headers(), body); + } + {{/asyncNative}} + + private String formatExceptionMessage(String operationId, int statusCode, String body) { + if (body == null || body.isEmpty()) { + body = "[no body]"; + } + return operationId + " call failed with: " + statusCode + " - " + body; + } + + {{#operation}} + {{#vendorExtensions.x-group-parameters}} + {{#hasParams}} + /** + * {{summary}} + * {{notes}} + * @param apiRequest {@link API{{operationId}}Request} + {{#returnType}} + * @return {{#asyncNative}}CompletableFuture<{{/asyncNative}}{{returnType}}{{#asyncNative}}>{{/asyncNative}} + {{/returnType}} + {{^returnType}} + {{#asyncNative}} + * @return CompletableFuture<Void> + {{/asyncNative}} + {{/returnType}} + * @throws ApiException if fails to make API call + {{#isDeprecated}} + * @deprecated + {{/isDeprecated}} + {{#externalDocs}} + * {{description}} + * @see {{summary}} Documentation + {{/externalDocs}} + */ + {{#isDeprecated}} + @Deprecated + {{/isDeprecated}} + public {{#returnType}}{{#asyncNative}}CompletableFuture<{{{returnType}}}>{{/asyncNative}}{{^asyncNative}}{{{returnType}}}{{/asyncNative}}{{/returnType}}{{^returnType}}{{#asyncNative}}CompletableFuture{{/asyncNative}}{{^asyncNative}}void{{/asyncNative}}{{/returnType}} {{operationId}}(API{{operationId}}Request apiRequest) throws ApiException { + {{#allParams}} + {{{dataType}}} {{paramName}} = apiRequest.{{paramName}}(); + {{/allParams}} + {{#returnType}}return {{/returnType}}{{^returnType}}{{#asyncNative}}return {{/asyncNative}}{{/returnType}}{{operationId}}({{#allParams}}{{paramName}}{{^-last}}, {{/-last}}{{/allParams}}); + } + + /** + * {{summary}} + * {{notes}} + * @param apiRequest {@link API{{operationId}}Request} + * @return {{#asyncNative}}CompletableFuture<{{/asyncNative}}ApiResponse<{{returnType}}{{^returnType}}Void{{/returnType}}>{{#asyncNative}}>{{/asyncNative}} + * @throws ApiException if fails to make API call + {{#isDeprecated}} + * @deprecated + {{/isDeprecated}} + {{#externalDocs}} + * {{description}} + * @see {{summary}} Documentation + {{/externalDocs}} + */ + {{#isDeprecated}} + @Deprecated + {{/isDeprecated}} + public {{#asyncNative}}CompletableFuture<{{/asyncNative}}ApiResponse<{{{returnType}}}{{^returnType}}Void{{/returnType}}>{{#asyncNative}}>{{/asyncNative}} {{operationId}}WithHttpInfo(API{{operationId}}Request apiRequest) throws ApiException { + {{#allParams}} + {{{dataType}}} {{paramName}} = apiRequest.{{paramName}}(); + {{/allParams}} + return {{operationId}}WithHttpInfo({{#allParams}}{{paramName}}{{^-last}}, {{/-last}}{{/allParams}}); + } + + {{/hasParams}} + {{/vendorExtensions.x-group-parameters}} + + /** + * {{summary}} + * {{notes}} + {{#allParams}} + * @param {{paramName}} {{description}}{{#required}} (required){{/required}}{{^required}} (optional{{^isContainer}}{{#defaultValue}}, default to {{.}}{{/defaultValue}}){{/isContainer}}{{/required}} + {{/allParams}} + {{#returnType}} + * @return {{#asyncNative}}CompletableFuture<{{/asyncNative}}{{returnType}}{{#asyncNative}}>{{/asyncNative}} + {{/returnType}} + {{^returnType}} + {{#asyncNative}} + * @return CompletableFuture<Void> + {{/asyncNative}} + {{/returnType}} + * @throws ApiException if fails to make API call + {{#isDeprecated}} + * @deprecated + {{/isDeprecated}} + {{#externalDocs}} + * {{description}} + * @see {{summary}} Documentation + {{/externalDocs}} + */ + {{#isDeprecated}} + @Deprecated + {{/isDeprecated}} + public {{#returnType}}{{#asyncNative}}CompletableFuture<{{{returnType}}}>{{/asyncNative}}{{^asyncNative}}{{{returnType}}}{{/asyncNative}}{{/returnType}}{{^returnType}}{{#asyncNative}}CompletableFuture{{/asyncNative}}{{^asyncNative}}void{{/asyncNative}}{{/returnType}} {{operationId}}({{#allParams}}{{{dataType}}} {{paramName}}{{^-last}}, {{/-last}}{{/allParams}}) throws ApiException, FgaInvalidParameterException { + return {{operationId}}({{#allParams}}{{paramName}}{{^-last}}, {{/-last}}{{/allParams}}, this.configuration); + } + + /** + * {{summary}} + * {{notes}} + {{#allParams}} + * @param {{paramName}} {{description}}{{#required}} (required){{/required}}{{^required}} (optional{{^isContainer}}{{#defaultValue}}, default to {{.}}{{/defaultValue}}){{/isContainer}}{{/required}} + {{/allParams}} + * @param configurationOverride Override the {@link Configuration} this {{classname}} was constructed with + {{#returnType}} + * @return {{#asyncNative}}CompletableFuture<{{/asyncNative}}{{returnType}}{{#asyncNative}}>{{/asyncNative}} + {{/returnType}} + {{^returnType}} + {{#asyncNative}} + * @return CompletableFuture<Void> + {{/asyncNative}} + {{/returnType}} + * @throws ApiException if fails to make API call + {{#isDeprecated}} + * @deprecated + {{/isDeprecated}} + {{#externalDocs}} + * {{description}} + * @see {{summary}} Documentation + {{/externalDocs}} + */ + {{#isDeprecated}} + @Deprecated + {{/isDeprecated}} + public {{#returnType}}{{#asyncNative}}CompletableFuture<{{{returnType}}}>{{/asyncNative}}{{^asyncNative}}{{{returnType}}}{{/asyncNative}}{{/returnType}}{{^returnType}}{{#asyncNative}}CompletableFuture{{/asyncNative}}{{^asyncNative}}void{{/asyncNative}}{{/returnType}} {{operationId}}({{#allParams}}{{{dataType}}} {{paramName}}{{^-last}}, {{/-last}}{{/allParams}}, ConfigurationOverride configurationOverride) throws ApiException, FgaInvalidParameterException { + return {{operationId}}({{#allParams}}{{paramName}}{{^-last}}, {{/-last}}{{/allParams}}, this.configuration.override(configurationOverride)); + } + + private {{#returnType}}{{#asyncNative}}CompletableFuture<{{{returnType}}}>{{/asyncNative}}{{^asyncNative}}{{{returnType}}}{{/asyncNative}}{{/returnType}}{{^returnType}}{{#asyncNative}}CompletableFuture{{/asyncNative}}{{^asyncNative}}void{{/asyncNative}}{{/returnType}} {{operationId}}({{#allParams}}{{{dataType}}} {{paramName}}{{^-last}}, {{/-last}}{{/allParams}}, Configuration configuration) throws ApiException, FgaInvalidParameterException { + {{^asyncNative}} + {{#returnType}}ApiResponse<{{{.}}}> localVarResponse = {{/returnType}}{{operationId}}WithHttpInfo({{#allParams}}{{paramName}}{{^-last}}, {{/-last}}{{/allParams}}); + {{#returnType}} + return localVarResponse.getData(); + {{/returnType}} + {{/asyncNative}} + {{#asyncNative}} + try { + HttpRequest.Builder localVarRequestBuilder = {{operationId}}RequestBuilder({{#allParams}}{{paramName}}{{^-last}}, {{/-last}}{{/allParams}}, configuration); + return memberVarHttpClient.sendAsync( + localVarRequestBuilder.build(), + HttpResponse.BodyHandlers.ofString()).thenComposeAsync(localVarResponse -> { + if (localVarResponse.statusCode()/ 100 != 2) { + return CompletableFuture.failedFuture(getApiException("{{operationId}}", localVarResponse)); + } + {{#returnType}} + try { + String responseBody = localVarResponse.body(); + return CompletableFuture.completedFuture( + responseBody == null || responseBody.isBlank() ? null : memberVarObjectMapper.readValue(responseBody, new TypeReference<{{{returnType}}}>() {}) + ); + } catch (IOException e) { + return CompletableFuture.failedFuture(new ApiException(e)); + } + {{/returnType}} + {{^returnType}} + return CompletableFuture.completedFuture(null); + {{/returnType}} + }); + } + catch (ApiException e) { + return CompletableFuture.failedFuture(e); + } + {{/asyncNative}} + } + + /** + * {{summary}} + * {{notes}} + {{#allParams}} + * @param {{paramName}} {{description}}{{#required}} (required){{/required}}{{^required}} (optional{{^isContainer}}{{#defaultValue}}, default to {{.}}{{/defaultValue}}){{/isContainer}}{{/required}} + {{/allParams}} + * @return {{#asyncNative}}CompletableFuture<{{/asyncNative}}ApiResponse<{{returnType}}{{^returnType}}Void{{/returnType}}>{{#asyncNative}}>{{/asyncNative}} + * @throws ApiException if fails to make API call + {{#isDeprecated}} + * @deprecated + {{/isDeprecated}} + {{#externalDocs}} + * {{description}} + * @see {{summary}} Documentation + {{/externalDocs}} + */ + {{#isDeprecated}} + @Deprecated + {{/isDeprecated}} + public {{#asyncNative}}CompletableFuture<{{/asyncNative}}ApiResponse<{{{returnType}}}{{^returnType}}Void{{/returnType}}>{{#asyncNative}}>{{/asyncNative}} {{operationId}}WithHttpInfo({{#allParams}}{{{dataType}}} {{paramName}}{{^-last}}, {{/-last}}{{/allParams}}) throws ApiException, FgaInvalidParameterException { + return {{operationId}}WithHttpInfo({{#allParams}}{{paramName}}{{^-last}}, {{/-last}}{{/allParams}}, this.configuration); + } + + /** + * {{summary}} + * {{notes}} + {{#allParams}} + * @param {{paramName}} {{description}}{{#required}} (required){{/required}}{{^required}} (optional{{^isContainer}}{{#defaultValue}}, default to {{.}}{{/defaultValue}}){{/isContainer}}{{/required}} + {{/allParams}} + * @param configurationOverride Override the {@link Configuration} this {{classname}} was constructed with + * @return {{#asyncNative}}CompletableFuture<{{/asyncNative}}ApiResponse<{{returnType}}{{^returnType}}Void{{/returnType}}>{{#asyncNative}}>{{/asyncNative}} + * @throws ApiException if fails to make API call + {{#isDeprecated}} + * @deprecated + {{/isDeprecated}} + {{#externalDocs}} + * {{description}} + * @see {{summary}} Documentation + {{/externalDocs}} + */ + {{#isDeprecated}} + @Deprecated + {{/isDeprecated}} + public {{#asyncNative}}CompletableFuture<{{/asyncNative}}ApiResponse<{{{returnType}}}{{^returnType}}Void{{/returnType}}>{{#asyncNative}}>{{/asyncNative}} {{operationId}}WithHttpInfo({{#allParams}}{{{dataType}}} {{paramName}}{{^-last}}, {{/-last}}{{/allParams}}, ConfigurationOverride configurationOverride) throws ApiException, FgaInvalidParameterException { + return {{operationId}}WithHttpInfo({{#allParams}}{{paramName}}{{^-last}}, {{/-last}}{{/allParams}}, this.configuration.override(configurationOverride)); + } + + private {{#asyncNative}}CompletableFuture<{{/asyncNative}}ApiResponse<{{{returnType}}}{{^returnType}}Void{{/returnType}}>{{#asyncNative}}>{{/asyncNative}} {{operationId}}WithHttpInfo({{#allParams}}{{{dataType}}} {{paramName}}{{^-last}}, {{/-last}}{{/allParams}}, Configuration configuration) throws ApiException, FgaInvalidParameterException { + {{^asyncNative}} + HttpRequest.Builder localVarRequestBuilder = {{operationId}}RequestBuilder({{#allParams}}{{paramName}}{{^-last}}, {{/-last}}{{/allParams}}, configuration); + try { + HttpResponse localVarResponse = memberVarHttpClient.send( + localVarRequestBuilder.build(), + HttpResponse.BodyHandlers.ofInputStream()); + if (memberVarResponseInterceptor != null) { + memberVarResponseInterceptor.accept(localVarResponse); + } + try { + if (localVarResponse.statusCode()/ 100 != 2) { + throw getApiException("{{operationId}}", localVarResponse); + } + {{#vendorExtensions.x-java-text-plain-string}} + // for plain text response + if (localVarResponse.headers().map().containsKey("Content-Type") && + "text/plain".equalsIgnoreCase(localVarResponse.headers().map().get("Content-Type").get(0).split(";")[0].trim())) { + java.util.Scanner s = new java.util.Scanner(localVarResponse.body()).useDelimiter("\\A"); + String responseBodyText = s.hasNext() ? s.next() : ""; + return new ApiResponse( + localVarResponse.statusCode(), + localVarResponse.headers().map(), + responseBodyText + ); + } else { + throw new RuntimeException("Error! The response Content-Type is supposed to be `text/plain` but it's not: " + localVarResponse); + } + {{/vendorExtensions.x-java-text-plain-string}} + {{^vendorExtensions.x-java-text-plain-string}} + return new ApiResponse<{{{returnType}}}{{^returnType}}Void{{/returnType}}>( + localVarResponse.statusCode(), + localVarResponse.headers().map(), + {{#returnType}} + localVarResponse.body() == null ? null : memberVarObjectMapper.readValue(localVarResponse.body(), new TypeReference<{{{returnType}}}>() {}) // closes the InputStream + {{/returnType}} + {{^returnType}} + null + {{/returnType}} + ); + {{/vendorExtensions.x-java-text-plain-string}} + } finally { + {{^returnType}} + // Drain the InputStream + while (localVarResponse.body().read() != -1) { + // Ignore + } + localVarResponse.body().close(); + {{/returnType}} + } + } catch (IOException e) { + throw new ApiException(e); + } + catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new ApiException(e); + } + {{/asyncNative}} + {{#asyncNative}} + try { + HttpRequest.Builder localVarRequestBuilder = {{operationId}}RequestBuilder({{#allParams}}{{paramName}}{{^-last}}, {{/-last}}{{/allParams}}, configuration); + return memberVarHttpClient.sendAsync( + localVarRequestBuilder.build(), + HttpResponse.BodyHandlers.ofString()).thenComposeAsync(localVarResponse -> { + if (memberVarAsyncResponseInterceptor != null) { + memberVarAsyncResponseInterceptor.accept(localVarResponse); + } + if (localVarResponse.statusCode()/ 100 != 2) { + return CompletableFuture.failedFuture(getApiException("{{operationId}}", localVarResponse)); + } + {{#returnType}} + try { + String responseBody = localVarResponse.body(); + return CompletableFuture.completedFuture( + new ApiResponse<{{{returnType}}}>( + localVarResponse.statusCode(), + localVarResponse.headers().map(), + responseBody == null || responseBody.isBlank() ? null : memberVarObjectMapper.readValue(responseBody, new TypeReference<{{{returnType}}}>() {})) + ); + } catch (IOException e) { + return CompletableFuture.failedFuture(new ApiException(e)); + } + {{/returnType}} + {{^returnType}} + return CompletableFuture.completedFuture( + new ApiResponse(localVarResponse.statusCode(), localVarResponse.headers().map(), null) + ); + {{/returnType}} + } + ); + } + catch (ApiException e) { + return CompletableFuture.failedFuture(e); + } + {{/asyncNative}} + } + + private HttpRequest.Builder {{operationId}}RequestBuilder({{#allParams}}{{{dataType}}} {{paramName}}{{^-last}}, {{/-last}}{{/allParams}}, Configuration configuration) throws ApiException, FgaInvalidParameterException { + {{#allParams}} + {{#required}} + // verify the required parameter '{{paramName}}' is set + if ({{paramName}} == null) { + throw new ApiException(400, "Missing the required parameter '{{paramName}}' when calling {{operationId}}"); + } + {{/required}} + {{/allParams}} + + // verify the Configuration is valid + configuration.assertValid(); + + HttpRequest.Builder localVarRequestBuilder = HttpRequest.newBuilder(); + + {{! Switch delimiters for baseName so we can write constants like "{query}" }} + String localVarPath = "{{{path}}}"{{#pathParams}} + .replace({{=<% %>=}}"{<%baseName%>}"<%={{ }}=%>, ApiClient.urlEncode({{{paramName}}}.toString())){{/pathParams}}; + + {{#hasQueryParams}} + List localVarQueryParams = new ArrayList<>(); + StringJoiner localVarQueryStringJoiner = new StringJoiner("&"); + String localVarQueryParameterBaseName; + {{#queryParams}} + localVarQueryParameterBaseName = "{{{baseName}}}"; + {{#collectionFormat}} + localVarQueryParams.addAll(ApiClient.parameterToPairs("{{{collectionFormat}}}", "{{baseName}}", {{paramName}})); + {{/collectionFormat}} + {{^collectionFormat}} + {{#isDeepObject}} + if ({{paramName}} != null) { + {{#isArray}} + for (int i=0; i < {{paramName}}.size(); i++) { + localVarQueryStringJoiner.add({{paramName}}.get(i).toUrlQueryString(String.format("{{baseName}}[%d]", i))); + } + {{/isArray}} + {{^isArray}} + localVarQueryStringJoiner.add({{paramName}}.toUrlQueryString("{{baseName}}")); + {{/isArray}} + } + {{/isDeepObject}} + {{^isDeepObject}} + {{#isExplode}} + {{#hasVars}} + {{#vars}} + {{#isArray}} + localVarQueryParams.addAll(ApiClient.parameterToPairs("multi", "{{baseName}}", {{paramName}}.{{getter}}())); + {{/isArray}} + {{^isArray}} + localVarQueryParams.addAll(ApiClient.parameterToPairs("{{baseName}}", {{paramName}}.{{getter}}())); + {{/isArray}} + {{/vars}} + {{/hasVars}} + {{^hasVars}} + {{#isModel}} + localVarQueryStringJoiner.add({{paramName}}.toUrlQueryString()); + {{/isModel}} + {{^isModel}} + localVarQueryParams.addAll(ApiClient.parameterToPairs("{{baseName}}", {{paramName}})); + {{/isModel}} + {{/hasVars}} + {{/isExplode}} + {{^isExplode}} + localVarQueryParams.addAll(ApiClient.parameterToPairs("{{baseName}}", {{paramName}})); + {{/isExplode}} + {{/isDeepObject}} + {{/collectionFormat}} + {{/queryParams}} + + if (!localVarQueryParams.isEmpty() || localVarQueryStringJoiner.length() != 0) { + StringJoiner queryJoiner = new StringJoiner("&"); + localVarQueryParams.forEach(p -> queryJoiner.add(p.getName() + '=' + p.getValue())); + if (localVarQueryStringJoiner.length() != 0) { + queryJoiner.add(localVarQueryStringJoiner.toString()); + } + localVarRequestBuilder.uri(URI.create(configuration.getApiUrl() + localVarPath + '?' + queryJoiner.toString())); + } else { + localVarRequestBuilder.uri(URI.create(configuration.getApiUrl() + localVarPath)); + } + {{/hasQueryParams}} + {{^hasQueryParams}} + localVarRequestBuilder.uri(URI.create(configuration.getApiUrl() + localVarPath)); + {{/hasQueryParams}} + + {{#headerParams}} + if ({{paramName}} != null) { + localVarRequestBuilder.header("{{baseName}}", {{paramName}}.toString()); + } + {{/headerParams}} + {{#bodyParam}} + localVarRequestBuilder.header("Content-Type", "{{#hasConsumes}}{{#consumes}}{{#-first}}{{mediaType}}{{/-first}}{{/consumes}}{{/hasConsumes}}{{#hasConsumes}}{{^consumes}}application/json{{/consumes}}{{/hasConsumes}}{{^hasConsumes}}application/json{{/hasConsumes}}"); + {{/bodyParam}} + localVarRequestBuilder.header("Accept", "{{#hasProduces}}{{#produces}}{{mediaType}}{{^-last}}, {{/-last}}{{/produces}}{{/hasProduces}}{{#hasProduces}}{{^produces}}application/json{{/produces}}{{/hasProduces}}{{^hasProduces}}application/json{{/hasProduces}}"); + + {{#bodyParam}} + {{#isString}} + localVarRequestBuilder.method("{{httpMethod}}", HttpRequest.BodyPublishers.ofString({{paramName}})); + {{/isString}} + {{^isString}} + try { + byte[] localVarPostBody = memberVarObjectMapper.writeValueAsBytes({{paramName}}); + localVarRequestBuilder.method("{{httpMethod}}", HttpRequest.BodyPublishers.ofByteArray(localVarPostBody)); + } catch (IOException e) { + throw new ApiException(e); + } + {{/isString}} + {{/bodyParam}} + {{^bodyParam}} + {{#hasFormParams}} + {{#isMultipart}} + MultipartEntityBuilder multiPartBuilder = MultipartEntityBuilder.create(); + boolean hasFiles = false; + {{#formParams}} + {{#isArray}} + for (int i=0; i < {{paramName}}.size(); i++) { + {{#isFile}} + multiPartBuilder.addBinaryBody("{{{baseName}}}", {{paramName}}.get(i)); + hasFiles = true; + {{/isFile}} + {{^isFile}} + multiPartBuilder.addTextBody("{{{baseName}}}", {{paramName}}.get(i).toString()); + {{/isFile}} + } + {{/isArray}} + {{^isArray}} + {{#isFile}} + multiPartBuilder.addBinaryBody("{{{baseName}}}", {{paramName}}); + hasFiles = true; + {{/isFile}} + {{^isFile}} + multiPartBuilder.addTextBody("{{{baseName}}}", {{paramName}}.toString()); + {{/isFile}} + {{/isArray}} + {{/formParams}} + HttpEntity entity = multiPartBuilder.build(); + HttpRequest.BodyPublisher formDataPublisher; + if (hasFiles) { + Pipe pipe; + try { + pipe = Pipe.open(); + } catch (IOException e) { + throw new RuntimeException(e); + } + new Thread(() -> { + try (OutputStream outputStream = Channels.newOutputStream(pipe.sink())) { + entity.writeTo(outputStream); + } catch (IOException e) { + e.printStackTrace(); + } + }).start(); + formDataPublisher = HttpRequest.BodyPublishers.ofInputStream(() -> Channels.newInputStream(pipe.source())); + } else { + ByteArrayOutputStream formOutputStream = new ByteArrayOutputStream(); + try { + entity.writeTo(formOutputStream); + } catch (IOException e) { + throw new RuntimeException(e); + } + formDataPublisher = HttpRequest.BodyPublishers + .ofInputStream(() -> new ByteArrayInputStream(formOutputStream.toByteArray())); + } + localVarRequestBuilder + .header("Content-Type", entity.getContentType().getValue()) + .method("{{httpMethod}}", formDataPublisher); + {{/isMultipart}} + {{^isMultipart}} + List formValues = new ArrayList<>(); + {{#formParams}} + {{#isArray}} + for (int i=0; i < {{paramName}}.size(); i++) { + if ({{paramName}}.get(i) != null) { + formValues.add(new BasicNameValuePair("{{{baseName}}}", {{paramName}}.get(i).toString())); + } + } + {{/isArray}} + {{^isArray}} + if ({{paramName}} != null) { + formValues.add(new BasicNameValuePair("{{{baseName}}}", {{paramName}}.toString())); + } + {{/isArray}} + {{/formParams}} + HttpEntity entity = new UrlEncodedFormEntity(formValues, java.nio.charset.StandardCharsets.UTF_8); + ByteArrayOutputStream formOutputStream = new ByteArrayOutputStream(); + try { + entity.writeTo(formOutputStream); + } catch (IOException e) { + throw new RuntimeException(e); + } + localVarRequestBuilder + .header("Content-Type", entity.getContentType().getValue()) + .method("{{httpMethod}}", HttpRequest.BodyPublishers + .ofInputStream(() -> new ByteArrayInputStream(formOutputStream.toByteArray()))); + {{/isMultipart}} + {{/hasFormParams}} + {{^hasFormParams}} + localVarRequestBuilder.method("{{httpMethod}}", HttpRequest.BodyPublishers.noBody()); + {{/hasFormParams}} + {{/bodyParam}} + Duration readTimeout = configuration.getReadTimeout(); + if (readTimeout != null) { + localVarRequestBuilder.timeout(readTimeout); + } + if (memberVarInterceptor != null) { + memberVarInterceptor.accept(localVarRequestBuilder); + } + return localVarRequestBuilder; + } + {{#vendorExtensions.x-group-parameters}} + {{#hasParams}} + + public static final class API{{operationId}}Request { + {{#requiredParams}} + private {{{dataType}}} {{paramName}}; // {{description}} (required) + {{/requiredParams}} + {{#optionalParams}} + private {{{dataType}}} {{paramName}}; // {{description}} (optional{{^isContainer}}{{#defaultValue}}, default to {{.}}{{/defaultValue}}){{/isContainer}} + {{/optionalParams}} + + private API{{operationId}}Request(Builder builder) { + {{#requiredParams}} + this.{{paramName}} = builder.{{paramName}}; + {{/requiredParams}} + {{#optionalParams}} + this.{{paramName}} = builder.{{paramName}}; + {{/optionalParams}} + } + {{#allParams}} + public {{{dataType}}} {{paramName}}() { + return {{paramName}}; + } + {{/allParams}} + public static Builder newBuilder() { + return new Builder(); + } + + public static class Builder { + {{#requiredParams}} + private {{{dataType}}} {{paramName}}; + {{/requiredParams}} + {{#optionalParams}} + private {{{dataType}}} {{paramName}}; + {{/optionalParams}} + + {{#allParams}} + public Builder {{paramName}}({{{dataType}}} {{paramName}}) { + this.{{paramName}} = {{paramName}}; + return this; + } + {{/allParams}} + public API{{operationId}}Request build() { + return new API{{operationId}}Request(this); + } + } + } + + {{/hasParams}} + {{/vendorExtensions.x-group-parameters}} + {{/operation}} +} +{{/operations}} diff --git a/config/clients/java/template/libraries/native/apiException.mustache b/config/clients/java/template/libraries/native/apiException.mustache new file mode 100644 index 00000000..79cf57f4 --- /dev/null +++ b/config/clients/java/template/libraries/native/apiException.mustache @@ -0,0 +1,78 @@ +{{>licenseInfo}} + +package dev.openfga.sdk.errors; + +import java.net.http.HttpHeaders; + +public class ApiException extends{{#useRuntimeException}} RuntimeException {{/useRuntimeException}}{{^useRuntimeException}} Exception {{/useRuntimeException}}{ + private int code = 0; + private HttpHeaders responseHeaders = null; + private String responseBody = null; + + public ApiException() {} + + public ApiException(Throwable throwable) { + super(throwable); + } + + public ApiException(String message) { + super(message); + } + + public ApiException(String message, Throwable throwable, int code, HttpHeaders responseHeaders, String responseBody) { + super(message, throwable); + this.code = code; + this.responseHeaders = responseHeaders; + this.responseBody = responseBody; + } + + public ApiException(String message, int code, HttpHeaders responseHeaders, String responseBody) { + this(message, (Throwable) null, code, responseHeaders, responseBody); + } + + public ApiException(String message, Throwable throwable, int code, HttpHeaders responseHeaders) { + this(message, throwable, code, responseHeaders, null); + } + + public ApiException(int code, HttpHeaders responseHeaders, String responseBody) { + this((String) null, (Throwable) null, code, responseHeaders, responseBody); + } + + public ApiException(int code, String message) { + super(message); + this.code = code; + } + + public ApiException(int code, String message, HttpHeaders responseHeaders, String responseBody) { + this(code, message); + this.responseHeaders = responseHeaders; + this.responseBody = responseBody; + } + + /** + * Get the HTTP status code. + * + * @return HTTP status code + */ + public int getCode() { + return code; + } + + /** + * Get the HTTP response headers. + * + * @return Headers as an HttpHeaders object + */ + public HttpHeaders getResponseHeaders() { + return responseHeaders; + } + + /** + * Get the HTTP response body. + * + * @return Response body in the form of string + */ + public String getResponseBody() { + return responseBody; + } +} diff --git a/config/clients/java/template/libraries/native/api_doc.mustache b/config/clients/java/template/libraries/native/api_doc.mustache new file mode 100644 index 00000000..7857cf67 --- /dev/null +++ b/config/clients/java/template/libraries/native/api_doc.mustache @@ -0,0 +1,280 @@ +# {{classname}}{{#description}} + +{{.}}{{/description}} + +All URIs are relative to *{{basePath}}* + +| Method | HTTP request | Description | +|------------- | ------------- | -------------| +{{#operations}}{{#operation}}| [**{{operationId}}**]({{classname}}.md#{{operationId}}) | **{{httpMethod}}** {{path}} | {{summary}} | +| [**{{operationId}}WithHttpInfo**]({{classname}}.md#{{operationId}}WithHttpInfo) | **{{httpMethod}}** {{path}} | {{summary}} | +{{/operation}}{{/operations}} + +{{#operations}} +{{#operation}} + +## {{operationId}} + +{{^vendorExtensions.x-group-parameters}} +> {{#returnType}}{{#asyncNative}}CompletableFuture<{{{returnType}}}>{{/asyncNative}}{{^asyncNative}}{{{returnType}}}{{/asyncNative}}{{/returnType}}{{^returnType}}{{#asyncNative}}CompletableFuture{{/asyncNative}}{{^asyncNative}}void{{/asyncNative}}{{/returnType}} {{operationId}}({{#allParams}}{{{paramName}}}{{^-last}}, {{/-last}}{{/allParams}}) +{{/vendorExtensions.x-group-parameters}} +{{#vendorExtensions.x-group-parameters}} +> {{#returnType}}{{#asyncNative}}CompletableFuture<{{{returnType}}}>{{/asyncNative}}{{^asyncNative}}{{{returnType}}}{{/asyncNative}}{{/returnType}}{{^returnType}}{{#asyncNative}}CompletableFuture{{/asyncNative}}{{^asyncNative}}void{{/asyncNative}}{{/returnType}} {{operationId}}({{#hasParams}}{{operationId}}Request{{/hasParams}}) +{{/vendorExtensions.x-group-parameters}} + +{{summary}}{{#notes}} + +{{.}}{{/notes}} + +### Example + +```java +// Import classes: +import {{{invokerPackage}}}.ApiClient; +import {{{invokerPackage}}}.ApiException; +import {{{package}}}.configuration.Configuration;{{#hasAuthMethods}} +import {{{invokerPackage}}}.auth.*;{{/hasAuthMethods}} +import {{{invokerPackage}}}.models.*; +import {{{package}}}.{{{classname}}};{{#vendorExtensions.x-group-parameters}} +import {{{package}}}.{{{classname}}}.*;{{/vendorExtensions.x-group-parameters}} +{{#asyncNative}} +import java.util.concurrent.CompletableFuture; +{{/asyncNative}} + +public class Example { + public static void main(String[] args) { + ApiClient defaultClient = Configuration.getDefaultApiClient(); + defaultClient.setBasePath("{{{basePath}}}"); + {{#hasAuthMethods}} + {{#authMethods}}{{#isBasic}}{{#isBasicBasic}} + // Configure HTTP basic authorization: {{{name}}} + HttpBasicAuth {{{name}}} = (HttpBasicAuth) defaultClient.getAuthentication("{{{name}}}"); + {{{name}}}.setUsername("YOUR USERNAME"); + {{{name}}}.setPassword("YOUR PASSWORD");{{/isBasicBasic}}{{#isBasicBearer}} + // Configure HTTP bearer authorization: {{{name}}} + HttpBearerAuth {{{name}}} = (HttpBearerAuth) defaultClient.getAuthentication("{{{name}}}"); + {{{name}}}.setBearerToken("BEARER TOKEN");{{/isBasicBearer}}{{/isBasic}}{{#isApiKey}} + // Configure API key authorization: {{{name}}} + ApiKeyAuth {{{name}}} = (ApiKeyAuth) defaultClient.getAuthentication("{{{name}}}"); + {{{name}}}.setApiKey("YOUR API KEY"); + // Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null) + //{{{name}}}.setApiKeyPrefix("Token");{{/isApiKey}}{{#isOAuth}} + // Configure OAuth2 access token for authorization: {{{name}}} + OAuth {{{name}}} = (OAuth) defaultClient.getAuthentication("{{{name}}}"); + {{{name}}}.setAccessToken("YOUR ACCESS TOKEN");{{/isOAuth}} + {{/authMethods}} + {{/hasAuthMethods}} + + {{{classname}}} apiInstance = new {{{classname}}}(defaultClient); + {{#allParams}} + {{{dataType}}} {{{paramName}}} = {{{example}}}; // {{{dataType}}} | {{{description}}} + {{/allParams}} + try { + {{^vendorExtensions.x-group-parameters}} + {{#returnType}}{{#asyncNative}}CompletableFuture<{{{returnType}}}>{{/asyncNative}}{{^asyncNative}}{{{returnType}}}{{/asyncNative}} result = {{/returnType}}{{^returnType}}{{#asyncNative}}CompletableFuture result = {{/asyncNative}}{{/returnType}}apiInstance.{{{operationId}}}({{#allParams}}{{{paramName}}}{{^-last}}, {{/-last}}{{/allParams}}); + {{/vendorExtensions.x-group-parameters}} + {{#vendorExtensions.x-group-parameters}} + {{#hasParams}} + API{{operationId}}Request request = API{{operationId}}Request.newBuilder(){{#allParams}} + .{{paramName}}({{paramName}}){{/allParams}} + .build(); + {{/hasParams}} + {{#returnType}}{{#asyncNative}}CompletableFuture<{{{returnType}}}>{{/asyncNative}}{{^asyncNative}}{{{returnType}}}{{/asyncNative}} result = {{/returnType}}{{^returnType}}{{#asyncNative}}CompletableFuture result = {{/asyncNative}}{{/returnType}}apiInstance.{{operationId}}({{#hasParams}}request{{/hasParams}}); + {{/vendorExtensions.x-group-parameters}} + {{#returnType}} + System.out.println(result{{#asyncNative}}.get(){{/asyncNative}}); + {{/returnType}} + } catch (ApiException e) { + System.err.println("Exception when calling {{{classname}}}#{{{operationId}}}"); + System.err.println("Status code: " + e.getCode()); + System.err.println("Reason: " + e.getResponseBody()); + System.err.println("Response headers: " + e.getResponseHeaders()); + e.printStackTrace(); + } + } +} +``` + +### Parameters + +{{^allParams}}This endpoint does not need any parameter.{{/allParams}}{{^vendorExtensions.x-group-parameters}}{{#allParams}}{{#-last}} +| Name | Type | Description | Notes | +|------------- | ------------- | ------------- | -------------|{{/-last}}{{/allParams}} +{{#allParams}}| **{{paramName}}** | {{#isPrimitiveType}}**{{dataType}}**{{/isPrimitiveType}}{{^isPrimitiveType}}{{#isFile}}**{{dataType}}**{{/isFile}}{{^isFile}}[**{{dataType}}**]({{baseType}}.md){{/isFile}}{{/isPrimitiveType}}| {{description}} |{{^required}} [optional]{{/required}}{{^isContainer}}{{#defaultValue}} [default to {{.}}]{{/defaultValue}}{{/isContainer}}{{#allowableValues}} [enum: {{#values}}{{{.}}}{{^-last}}, {{/-last}}{{/values}}]{{/allowableValues}} | +{{/allParams}} +{{/vendorExtensions.x-group-parameters}} +{{#vendorExtensions.x-group-parameters}} +{{#hasParams}} +| Name | Type | Description | Notes | +|------------- | ------------- | ------------- | -------------| +| {{operationId}}Request | [**API{{operationId}}Request**]({{classname}}.md#API{{operationId}}Request)|-|-|{{/hasParams}} +{{/vendorExtensions.x-group-parameters}} + +### Return type + +{{#returnType}}{{#asyncNative}}CompletableFuture<{{/asyncNative}}{{#returnTypeIsPrimitive}}**{{returnType}}**{{/returnTypeIsPrimitive}}{{^returnTypeIsPrimitive}}[**{{returnType}}**]({{returnBaseType}}.md){{/returnTypeIsPrimitive}}{{#asyncNative}}>{{/asyncNative}}{{/returnType}} +{{^returnType}}{{#asyncNative}}CompletableFuture{{/asyncNative}}{{^asyncNative}}null{{/asyncNative}} (empty response body){{/returnType}} + +### Authorization + +{{^authMethods}}No authorization required{{/authMethods}}{{#authMethods}}[{{name}}](../README.md#{{name}}){{^-last}}, {{/-last}}{{/authMethods}} + +### HTTP request headers + +- **Content-Type**: {{#consumes}}{{{mediaType}}}{{^-last}}, {{/-last}}{{/consumes}}{{^consumes}}Not defined{{/consumes}} +- **Accept**: {{#produces}}{{{mediaType}}}{{^-last}}, {{/-last}}{{/produces}}{{^produces}}Not defined{{/produces}} + +{{#responses.0}} +### HTTP response details +| Status code | Description | Response headers | +|-------------|-------------|------------------| +{{#responses}} +| **{{code}}** | {{message}} | {{#headers}} * {{baseName}} - {{description}}
{{/headers}}{{^headers.0}} - {{/headers.0}} | +{{/responses}} +{{/responses.0}} + +## {{operationId}}WithHttpInfo + +{{^vendorExtensions.x-group-parameters}} +> {{#asyncNative}}CompletableFuture<{{/asyncNative}}ApiResponse<{{{returnType}}}{{^returnType}}Void{{/returnType}}>{{#asyncNative}}>{{/asyncNative}} {{operationId}} {{operationId}}WithHttpInfo({{#allParams}}{{{paramName}}}{{^-last}}, {{/-last}}{{/allParams}}) +{{/vendorExtensions.x-group-parameters}} +{{#vendorExtensions.x-group-parameters}} +> {{#asyncNative}}CompletableFuture<{{/asyncNative}}ApiResponse<{{{returnType}}}{{^returnType}}Void{{/returnType}}>{{#asyncNative}}>{{/asyncNative}} {{operationId}} {{operationId}}WithHttpInfo({{#hasParams}}{{operationId}}Request{{/hasParams}}) +{{/vendorExtensions.x-group-parameters}} + +{{summary}}{{#notes}} + +{{.}}{{/notes}} + +### Example + +```java +// Import classes: +import {{{invokerPackage}}}.ApiClient; +import {{{invokerPackage}}}.ApiException; +import {{{invokerPackage}}}.ApiResponse; +import {{{apiPackage}}}.configuration.Configuration;{{#hasAuthMethods}} +import {{{invokerPackage}}}.auth.*;{{/hasAuthMethods}} +import {{{invokerPackage}}}.models.*; +import {{{package}}}.{{{classname}}};{{#vendorExtensions.x-group-parameters}} +import {{{package}}}.{{{classname}}}.*;{{/vendorExtensions.x-group-parameters}} +{{#asyncNative}} +import java.util.concurrent.CompletableFuture; +{{/asyncNative}} + +public class Example { + public static void main(String[] args) { + ApiClient defaultClient = Configuration.getDefaultApiClient(); + defaultClient.setBasePath("{{{basePath}}}"); + {{#hasAuthMethods}} + {{#authMethods}}{{#isBasic}}{{#isBasicBasic}} + // Configure HTTP basic authorization: {{{name}}} + HttpBasicAuth {{{name}}} = (HttpBasicAuth) defaultClient.getAuthentication("{{{name}}}"); + {{{name}}}.setUsername("YOUR USERNAME"); + {{{name}}}.setPassword("YOUR PASSWORD");{{/isBasicBasic}}{{#isBasicBearer}} + // Configure HTTP bearer authorization: {{{name}}} + HttpBearerAuth {{{name}}} = (HttpBearerAuth) defaultClient.getAuthentication("{{{name}}}"); + {{{name}}}.setBearerToken("BEARER TOKEN");{{/isBasicBearer}}{{/isBasic}}{{#isApiKey}} + // Configure API key authorization: {{{name}}} + ApiKeyAuth {{{name}}} = (ApiKeyAuth) defaultClient.getAuthentication("{{{name}}}"); + {{{name}}}.setApiKey("YOUR API KEY"); + // Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null) + //{{{name}}}.setApiKeyPrefix("Token");{{/isApiKey}}{{#isOAuth}} + // Configure OAuth2 access token for authorization: {{{name}}} + OAuth {{{name}}} = (OAuth) defaultClient.getAuthentication("{{{name}}}"); + {{{name}}}.setAccessToken("YOUR ACCESS TOKEN");{{/isOAuth}} + {{/authMethods}} + {{/hasAuthMethods}} + + {{{classname}}} apiInstance = new {{{classname}}}(defaultClient); + {{#allParams}} + {{{dataType}}} {{{paramName}}} = {{{example}}}; // {{{dataType}}} | {{{description}}} + {{/allParams}} + try { + {{^vendorExtensions.x-group-parameters}} + {{#asyncNative}}CompletableFuture<{{/asyncNative}}ApiResponse<{{{returnType}}}{{^returnType}}Void{{/returnType}}>{{#asyncNative}}>{{/asyncNative}} response = apiInstance.{{{operationId}}}WithHttpInfo({{#allParams}}{{{paramName}}}{{^-last}}, {{/-last}}{{/allParams}}); + {{/vendorExtensions.x-group-parameters}} + {{#vendorExtensions.x-group-parameters}} + {{#hasParams}} + API{{operationId}}Request request = API{{operationId}}Request.newBuilder(){{#allParams}} + .{{paramName}}({{paramName}}){{/allParams}} + .build(); + {{/hasParams}} + {{#asyncNative}}CompletableFuture<{{/asyncNative}}ApiResponse<{{{returnType}}}{{^returnType}}Void{{/returnType}}>{{#asyncNative}}>{{/asyncNative}} response = apiInstance.{{{operationId}}}WithHttpInfo({{#hasParams}}request{{/hasParams}}); + {{/vendorExtensions.x-group-parameters}} + System.out.println("Status code: " + response{{#asyncNative}}.get(){{/asyncNative}}.getStatusCode()); + System.out.println("Response headers: " + response{{#asyncNative}}.get(){{/asyncNative}}.getHeaders()); + {{#returnType}} + System.out.println("Response body: " + response{{#asyncNative}}.get(){{/asyncNative}}.getData()); + {{/returnType}} + {{#asyncNative}} + } catch (InterruptedException | ExecutionException e) { + ApiException apiException = (ApiException)e.getCause(); + System.err.println("Exception when calling {{{classname}}}#{{{operationId}}}"); + System.err.println("Status code: " + apiException.getCode()); + System.err.println("Response headers: " + apiException.getResponseHeaders()); + System.err.println("Reason: " + apiException.getResponseBody()); + e.printStackTrace(); + {{/asyncNative}} + } catch (ApiException e) { + System.err.println("Exception when calling {{{classname}}}#{{{operationId}}}"); + System.err.println("Status code: " + e.getCode()); + System.err.println("Response headers: " + e.getResponseHeaders()); + System.err.println("Reason: " + e.getResponseBody()); + e.printStackTrace(); + } + } +} +``` + +### Parameters + +{{^allParams}}This endpoint does not need any parameter.{{/allParams}}{{^vendorExtensions.x-group-parameters}}{{#allParams}}{{#-last}} +| Name | Type | Description | Notes | +|------------- | ------------- | ------------- | -------------|{{/-last}}{{/allParams}} +{{#allParams}}| **{{paramName}}** | {{#isPrimitiveType}}**{{dataType}}**{{/isPrimitiveType}}{{^isPrimitiveType}}{{#isFile}}**{{dataType}}**{{/isFile}}{{^isFile}}[**{{dataType}}**]({{baseType}}.md){{/isFile}}{{/isPrimitiveType}}| {{description}} |{{^required}} [optional]{{/required}}{{^isContainer}}{{#defaultValue}} [default to {{.}}]{{/defaultValue}}{{/isContainer}}{{#allowableValues}} [enum: {{#values}}{{{.}}}{{^-last}}, {{/-last}}{{/values}}]{{/allowableValues}} | +{{/allParams}} +{{/vendorExtensions.x-group-parameters}} +{{#vendorExtensions.x-group-parameters}} +{{#hasParams}} +| Name | Type | Description | Notes | +|------------- | ------------- | ------------- | -------------| +| {{operationId}}Request | [**API{{operationId}}Request**]({{classname}}.md#API{{operationId}}Request)|-|-|{{/hasParams}} +{{/vendorExtensions.x-group-parameters}} + +### Return type + +{{#returnType}}{{#asyncNative}}CompletableFuture<{{/asyncNative}}ApiResponse<{{#returnTypeIsPrimitive}}**{{returnType}}**{{/returnTypeIsPrimitive}}{{^returnTypeIsPrimitive}}[**{{returnType}}**]({{returnBaseType}}.md){{/returnTypeIsPrimitive}}>{{#asyncNative}}>{{/asyncNative}}{{/returnType}} +{{^returnType}}{{#asyncNative}}CompletableFuture<{{/asyncNative}}ApiResponse{{#asyncNative}}>{{/asyncNative}}{{/returnType}} + +### Authorization + +{{^authMethods}}No authorization required{{/authMethods}}{{#authMethods}}[{{name}}](../README.md#{{name}}){{^-last}}, {{/-last}}{{/authMethods}} + +### HTTP request headers + +- **Content-Type**: {{#consumes}}{{{mediaType}}}{{^-last}}, {{/-last}}{{/consumes}}{{^consumes}}Not defined{{/consumes}} +- **Accept**: {{#produces}}{{{mediaType}}}{{^-last}}, {{/-last}}{{/produces}}{{^produces}}Not defined{{/produces}} + +{{#responses.0}} +### HTTP response details +| Status code | Description | Response headers | +|-------------|-------------|------------------| +{{#responses}} +| **{{code}}** | {{message}} | {{#headers}} * {{baseName}} - {{description}}
{{/headers}}{{^headers.0}} - {{/headers.0}} | +{{/responses}} +{{/responses.0}} +{{#vendorExtensions.x-group-parameters}}{{#hasParams}} + + +## API{{operationId}}Request +### Properties +{{#allParams}}{{#-last}} +| Name | Type | Description | Notes | +| ------------- | ------------- | ------------- | -------------|{{/-last}}{{/allParams}} +{{#allParams}}| **{{paramName}}** | {{#isPrimitiveType}}**{{dataType}}**{{/isPrimitiveType}}{{^isPrimitiveType}}{{#isFile}}**{{dataType}}**{{/isFile}}{{^isFile}}[**{{dataType}}**]({{baseType}}.md){{/isFile}}{{/isPrimitiveType}} | {{description}} |{{^required}} [optional]{{/required}}{{^isContainer}}{{#defaultValue}} [default to {{.}}]{{/defaultValue}}{{/isContainer}}{{#allowableValues}} [enum: {{#values}}{{{.}}}{{^-last}}, {{/-last}}{{/values}}]{{/allowableValues}} | +{{/allParams}} + +{{/hasParams}}{{/vendorExtensions.x-group-parameters}} +{{/operation}} +{{/operations}} diff --git a/config/clients/java/template/libraries/native/gradle.properties.mustache b/config/clients/java/template/libraries/native/gradle.properties.mustache new file mode 100644 index 00000000..e69de29b diff --git a/config/clients/java/template/libraries/native/model.mustache b/config/clients/java/template/libraries/native/model.mustache new file mode 100644 index 00000000..40d8a5a4 --- /dev/null +++ b/config/clients/java/template/libraries/native/model.mustache @@ -0,0 +1,68 @@ +{{>licenseInfo}} + +package {{package}}; + +{{#useReflectionEqualsHashCode}} +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; +{{/useReflectionEqualsHashCode}} +{{#models}} +{{#model}} +{{#additionalPropertiesType}} +import java.util.Map; +import java.util.HashMap; +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +{{/additionalPropertiesType}} +{{/model}} +{{/models}} +{{#supportUrlQuery}} +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.StringJoiner; +{{/supportUrlQuery}} +import java.util.Objects; +import java.util.Arrays; +import java.util.Map; +import java.util.HashMap; +{{#imports}} +import {{import}}; +{{/imports}} +{{#serializableModel}} +import java.io.Serializable; +{{/serializableModel}} +{{#jackson}} +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +{{#withXml}} +import com.fasterxml.jackson.dataformat.xml.annotation.*; +{{/withXml}} +{{#vendorExtensions.x-has-readonly-properties}} +import com.fasterxml.jackson.annotation.JsonCreator; +{{/vendorExtensions.x-has-readonly-properties}} +{{/jackson}} +{{#withXml}} +import {{javaxPackage}}.xml.bind.annotation.*; +{{/withXml}} +{{#parcelableModel}} +import android.os.Parcelable; +import android.os.Parcel; +{{/parcelableModel}} +{{#useBeanValidation}} +import {{javaxPackage}}.validation.constraints.*; +import {{javaxPackage}}.validation.Valid; +{{/useBeanValidation}} +{{#performBeanValidation}} +import org.hibernate.validator.constraints.*; +{{/performBeanValidation}} + +{{#models}} +{{#model}} +{{#oneOf}} +{{#-first}} +import com.fasterxml.jackson.core.type.TypeReference; +{{/-first}} +{{/oneOf}} + +{{#isEnum}}{{>modelEnum}}{{/isEnum}}{{^isEnum}}{{#oneOf}}{{#-first}}{{>oneof_model}}{{/-first}}{{/oneOf}}{{^oneOf}}{{#anyOf}}{{#-first}}{{>anyof_model}}{{/-first}}{{/anyOf}}{{^anyOf}}{{>pojo}}{{/anyOf}}{{/oneOf}}{{/isEnum}} +{{/model}} +{{/models}} diff --git a/config/clients/java/template/libraries/native/modelEnum.mustache b/config/clients/java/template/libraries/native/modelEnum.mustache new file mode 100644 index 00000000..b4cd2f41 --- /dev/null +++ b/config/clients/java/template/libraries/native/modelEnum.mustache @@ -0,0 +1,117 @@ +{{#jackson}} +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +{{/jackson}} +{{#gson}} +import java.io.IOException; +import com.google.gson.TypeAdapter; +import com.google.gson.annotations.JsonAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; +{{/gson}} + +/** + * {{description}}{{^description}}Gets or Sets {{{name}}}{{/description}} + */ +{{#gson}} +@JsonAdapter({{{datatypeWithEnum}}}{{^datatypeWithEnum}}{{{classname}}}{{/datatypeWithEnum}}.Adapter.class) +{{/gson}} +{{#jsonb}} +@JsonbTypeSerializer({{datatypeWithEnum}}.Serializer.class) +@JsonbTypeDeserializer({{datatypeWithEnum}}.Deserializer.class) +{{/jsonb}} +{{>additionalEnumTypeAnnotations}}public enum {{{datatypeWithEnum}}}{{^datatypeWithEnum}}{{{classname}}}{{/datatypeWithEnum}} { + {{#allowableValues}}{{#enumVars}} + {{#enumDescription}} + /** + * {{.}} + */ + {{/enumDescription}} + {{#withXml}} + @XmlEnumValue({{#isInteger}}"{{/isInteger}}{{#isDouble}}"{{/isDouble}}{{#isLong}}"{{/isLong}}{{#isFloat}}"{{/isFloat}}{{{value}}}{{#isInteger}}"{{/isInteger}}{{#isDouble}}"{{/isDouble}}{{#isLong}}"{{/isLong}}{{#isFloat}}"{{/isFloat}}) + {{/withXml}} + {{{name}}}({{{value}}}){{^-last}}, + {{/-last}}{{#-last}};{{/-last}}{{/enumVars}}{{/allowableValues}} + + private {{{dataType}}} value; + + {{{datatypeWithEnum}}}{{^datatypeWithEnum}}{{{classname}}}{{/datatypeWithEnum}}({{{dataType}}} value) { + this.value = value; + } + +{{#jackson}} + @JsonValue +{{/jackson}} + public {{{dataType}}} getValue() { + return value; + } + + @Override + public String toString() { + return String.valueOf(value); + } + +{{#jackson}} + @JsonCreator +{{/jackson}} + public static {{{datatypeWithEnum}}}{{^datatypeWithEnum}}{{{classname}}}{{/datatypeWithEnum}} fromValue({{{dataType}}} value) { + for ({{{datatypeWithEnum}}}{{^datatypeWithEnum}}{{{classname}}}{{/datatypeWithEnum}} b : {{{datatypeWithEnum}}}{{^datatypeWithEnum}}{{{classname}}}{{/datatypeWithEnum}}.values()) { + if (b.value.equals(value)) { + return b; + } + } + {{#isNullable}}return null;{{/isNullable}}{{^isNullable}}{{#enumUnknownDefaultCase}}{{#allowableValues}}{{#enumVars}}{{#-last}}return {{{name}}};{{/-last}}{{/enumVars}}{{/allowableValues}}{{/enumUnknownDefaultCase}}{{^enumUnknownDefaultCase}}throw new IllegalArgumentException("Unexpected value '" + value + "'");{{/enumUnknownDefaultCase}}{{/isNullable}} + } +{{#supportUrlQuery}} + + /** + * Convert the instance into URL query string. + * + * @param prefix prefix of the query string + * @return URL query string + */ + public String toUrlQueryString(String prefix) { + if (prefix == null) { + prefix = ""; + } + + return String.format("%s=%s", prefix, this.toString()); + } +{{/supportUrlQuery}} + +{{#gson}} + + public static class Adapter extends TypeAdapter<{{{datatypeWithEnum}}}{{^datatypeWithEnum}}{{{classname}}}{{/datatypeWithEnum}}> { + @Override + public void write(final JsonWriter jsonWriter, final {{{datatypeWithEnum}}}{{^datatypeWithEnum}}{{{classname}}}{{/datatypeWithEnum}} enumeration) throws IOException { + jsonWriter.value(enumeration.getValue()); + } + + @Override + public {{{datatypeWithEnum}}}{{^datatypeWithEnum}}{{{classname}}}{{/datatypeWithEnum}} read(final JsonReader jsonReader) throws IOException { + {{^isNumber}}{{{dataType}}}{{/isNumber}}{{#isNumber}}String{{/isNumber}} value = jsonReader.{{#isNumber}}nextString(){{/isNumber}}{{#isInteger}}nextInt(){{/isInteger}}{{^isNumber}}{{^isInteger}}next{{{dataType}}}(){{/isInteger}}{{/isNumber}}; + return {{{datatypeWithEnum}}}{{^datatypeWithEnum}}{{{classname}}}{{/datatypeWithEnum}}.fromValue({{#isNumber}}new BigDecimal({{/isNumber}}value{{#isNumber}}){{/isNumber}}); + } + } +{{/gson}} +{{#jsonb}} + public static final class Deserializer implements JsonbDeserializer<{{datatypeWithEnum}}> { + @Override + public {{datatypeWithEnum}} deserialize(JsonParser parser, DeserializationContext ctx, Type rtType) { + for ({{{datatypeWithEnum}}}{{^datatypeWithEnum}}{{{classname}}}{{/datatypeWithEnum}} b : {{{datatypeWithEnum}}}{{^datatypeWithEnum}}{{{classname}}}{{/datatypeWithEnum}}.values()) { + if (String.valueOf(b.value).equals(parser.getString())) { + return b; + } + } + {{#useNullForUnknownEnumValue}}return null;{{/useNullForUnknownEnumValue}}{{^useNullForUnknownEnumValue}}throw new IllegalArgumentException("Unexpected value '" + parser.getString() + "'");{{/useNullForUnknownEnumValue}} + } + } + + public static final class Serializer implements JsonbSerializer<{{datatypeWithEnum}}> { + @Override + public void serialize({{datatypeWithEnum}} obj, JsonGenerator generator, SerializationContext ctx) { + generator.write(obj.value); + } + } +{{/jsonb}} +} diff --git a/config/clients/java/template/libraries/native/model_anyof_doc.mustache b/config/clients/java/template/libraries/native/model_anyof_doc.mustache new file mode 100644 index 00000000..e360aa56 --- /dev/null +++ b/config/clients/java/template/libraries/native/model_anyof_doc.mustache @@ -0,0 +1,38 @@ +# {{classname}} + +{{#description}} +{{&description}} + +{{/description}} +## anyOf schemas +{{#anyOf}} +* [{{{.}}}]({{{.}}}.md) +{{/anyOf}} + +{{#isNullable}} +NOTE: this class is nullable. + +{{/isNullable}} +## Example +```java +// Import classes: +import {{{package}}}.{{{classname}}}; +{{#anyOf}} +import {{{package}}}.{{{.}}}; +{{/anyOf}} + +public class Example { + public static void main(String[] args) { + {{classname}} example{{classname}} = new {{classname}}(); + {{#anyOf}} + + // create a new {{{.}}} + {{{.}}} example{{{.}}} = new {{{.}}}(); + // set {{{classname}}} to {{{.}}} + example{{classname}}.setActualInstance(example{{{.}}}); + // to get back the {{{.}}} set earlier + {{{.}}} test{{{.}}} = ({{{.}}}) example{{classname}}.getActualInstance(); + {{/anyOf}} + } +} +``` diff --git a/config/clients/java/template/libraries/native/model_doc.mustache b/config/clients/java/template/libraries/native/model_doc.mustache new file mode 100644 index 00000000..be1aedcf --- /dev/null +++ b/config/clients/java/template/libraries/native/model_doc.mustache @@ -0,0 +1,19 @@ +{{#models}}{{#model}} + +{{#isEnum}} +{{>enum_outer_doc}} +{{/isEnum}} +{{^isEnum}} +{{^oneOf.isEmpty}} +{{>model_oneof_doc}} +{{/oneOf.isEmpty}} +{{^anyOf.isEmpty}} +{{>model_anyof_doc}} +{{/anyOf.isEmpty}} +{{^anyOf}} +{{^oneOf}} +{{>pojo_doc}} +{{/oneOf}} +{{/anyOf}} +{{/isEnum}} +{{/model}}{{/models}} diff --git a/config/clients/java/template/libraries/native/model_oneof_doc.mustache b/config/clients/java/template/libraries/native/model_oneof_doc.mustache new file mode 100644 index 00000000..5fff76c9 --- /dev/null +++ b/config/clients/java/template/libraries/native/model_oneof_doc.mustache @@ -0,0 +1,38 @@ +# {{classname}} + +{{#description}} +{{&description}} + +{{/description}} +## oneOf schemas +{{#oneOf}} +* [{{{.}}}]({{{.}}}.md) +{{/oneOf}} + +{{#isNullable}} +NOTE: this class is nullable. + +{{/isNullable}} +## Example +```java +// Import classes: +import {{{package}}}.{{{classname}}}; +{{#oneOf}} +import {{{package}}}.{{{.}}}; +{{/oneOf}} + +public class Example { + public static void main(String[] args) { + {{classname}} example{{classname}} = new {{classname}}(); + {{#oneOf}} + + // create a new {{{.}}} + {{{.}}} example{{{.}}} = new {{{.}}}(); + // set {{{classname}}} to {{{.}}} + example{{classname}}.setActualInstance(example{{{.}}}); + // to get back the {{{.}}} set earlier + {{{.}}} test{{{.}}} = ({{{.}}}) example{{classname}}.getActualInstance(); + {{/oneOf}} + } +} +``` diff --git a/config/clients/java/template/libraries/native/oneof_model.mustache b/config/clients/java/template/libraries/native/oneof_model.mustache new file mode 100644 index 00000000..72080a05 --- /dev/null +++ b/config/clients/java/template/libraries/native/oneof_model.mustache @@ -0,0 +1,393 @@ +import java.io.IOException; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; +import {{invokerPackage}}.JSON; + +{{>additionalModelTypeAnnotations}}{{>xmlAnnotation}} +@JsonDeserialize(using = {{classname}}.{{classname}}Deserializer.class) +@JsonSerialize(using = {{classname}}.{{classname}}Serializer.class) +public class {{classname}} extends AbstractOpenApiSchema{{#vendorExtensions.x-implements}}, {{{.}}}{{/vendorExtensions.x-implements}} { + private static final Logger log = Logger.getLogger({{classname}}.class.getName()); + + public static class {{classname}}Serializer extends StdSerializer<{{classname}}> { + public {{classname}}Serializer(Class<{{classname}}> t) { + super(t); + } + + public {{classname}}Serializer() { + this(null); + } + + @Override + public void serialize({{classname}} value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonProcessingException { + jgen.writeObject(value.getActualInstance()); + } + } + + public static class {{classname}}Deserializer extends StdDeserializer<{{classname}}> { + public {{classname}}Deserializer() { + this({{classname}}.class); + } + + public {{classname}}Deserializer(Class vc) { + super(vc); + } + + @Override + public {{classname}} deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException { + JsonNode tree = jp.readValueAsTree(); + Object deserialized = null; + {{#useOneOfDiscriminatorLookup}} + {{#discriminator}} + {{classname}} new{{classname}} = new {{classname}}(); + Map result2 = tree.traverse(jp.getCodec()).readValueAs(new TypeReference>() {}); + String discriminatorValue = (String)result2.get("{{{propertyBaseName}}}"); + switch (discriminatorValue) { + {{#mappedModels}} + case "{{{mappingName}}}": + deserialized = tree.traverse(jp.getCodec()).readValueAs({{{modelName}}}.class); + new{{classname}}.setActualInstance(deserialized); + return new{{classname}}; + {{/mappedModels}} + default: + log.log(Level.WARNING, String.format("Failed to lookup discriminator value `%s` for {{classname}}. Possible values:{{#mappedModels}} {{{mappingName}}}{{/mappedModels}}", discriminatorValue)); + } + + {{/discriminator}} + {{/useOneOfDiscriminatorLookup}} + boolean typeCoercion = ctxt.isEnabled(MapperFeature.ALLOW_COERCION_OF_SCALARS); + int match = 0; + JsonToken token = tree.traverse(jp.getCodec()).nextToken(); + {{#oneOf}} + // deserialize {{{.}}} + try { + boolean attemptParsing = true; + // ensure that we respect type coercion as set on the client ObjectMapper + if ({{{.}}}.class.equals(Integer.class) || {{{.}}}.class.equals(Long.class) || {{{.}}}.class.equals(Float.class) || {{{.}}}.class.equals(Double.class) || {{{.}}}.class.equals(Boolean.class) || {{{.}}}.class.equals(String.class)) { + attemptParsing = typeCoercion; + if (!attemptParsing) { + attemptParsing |= (({{{.}}}.class.equals(Integer.class) || {{{.}}}.class.equals(Long.class)) && token == JsonToken.VALUE_NUMBER_INT); + attemptParsing |= (({{{.}}}.class.equals(Float.class) || {{{.}}}.class.equals(Double.class)) && token == JsonToken.VALUE_NUMBER_FLOAT); + attemptParsing |= ({{{.}}}.class.equals(Boolean.class) && (token == JsonToken.VALUE_FALSE || token == JsonToken.VALUE_TRUE)); + attemptParsing |= ({{{.}}}.class.equals(String.class) && token == JsonToken.VALUE_STRING); + {{#isNullable}} + attemptParsing |= (token == JsonToken.VALUE_NULL); + {{/isNullable}} + } + } + if (attemptParsing) { + deserialized = tree.traverse(jp.getCodec()).readValueAs({{{.}}}.class); + // TODO: there is no validation against JSON schema constraints + // (min, max, enum, pattern...), this does not perform a strict JSON + // validation, which means the 'match' count may be higher than it should be. + match++; + log.log(Level.FINER, "Input data matches schema '{{{.}}}'"); + } + } catch (Exception e) { + // deserialization failed, continue + log.log(Level.FINER, "Input data does not match schema '{{{.}}}'", e); + } + + {{/oneOf}} + if (match == 1) { + {{classname}} ret = new {{classname}}(); + ret.setActualInstance(deserialized); + return ret; + } + throw new IOException(String.format("Failed deserialization for {{classname}}: %d classes match result, expected 1", match)); + } + + /** + * Handle deserialization of the 'null' value. + */ + @Override + public {{classname}} getNullValue(DeserializationContext ctxt) throws JsonMappingException { + {{#isNullable}} + return null; + {{/isNullable}} + {{^isNullable}} + throw new JsonMappingException(ctxt.getParser(), "{{classname}} cannot be null"); + {{/isNullable}} + } + } + + // store a list of schema names defined in oneOf + public static final Map> schemas = new HashMap<>(); + + public {{classname}}() { + super("oneOf", {{#isNullable}}Boolean.TRUE{{/isNullable}}{{^isNullable}}Boolean.FALSE{{/isNullable}}); + } +{{> libraries/native/additional_properties }} + {{#additionalPropertiesType}} + /** + * Return true if this {{name}} object is equal to o. + */ + @Override + public boolean equals(Object o) { + return super.equals(o) && Objects.equals(this.additionalProperties, (({{classname}})o).additionalProperties); + } + + @Override + public int hashCode() { + return Objects.hash(getActualInstance(), isNullable(), getSchemaType(), additionalProperties); + } + {{/additionalPropertiesType}} + {{#oneOf}} + public {{classname}}({{{.}}} o) { + super("oneOf", {{#isNullable}}Boolean.TRUE{{/isNullable}}{{^isNullable}}Boolean.FALSE{{/isNullable}}); + setActualInstance(o); + } + + {{/oneOf}} + static { + {{#oneOf}} + schemas.put("{{{.}}}", {{{.}}}.class); + {{/oneOf}} + JSON.registerDescendants({{classname}}.class, Collections.unmodifiableMap(schemas)); + {{#discriminator}} + // Initialize and register the discriminator mappings. + Map> mappings = new HashMap>(); + {{#mappedModels}} + mappings.put("{{mappingName}}", {{modelName}}.class); + {{/mappedModels}} + mappings.put("{{name}}", {{classname}}.class); + JSON.registerDiscriminator({{classname}}.class, "{{propertyBaseName}}", mappings); + {{/discriminator}} + } + + @Override + public Map> getSchemas() { + return {{classname}}.schemas; + } + + /** + * Set the instance that matches the oneOf child schema, check + * the instance parameter is valid against the oneOf child schemas: + * {{#oneOf}}{{{.}}}{{^-last}}, {{/-last}}{{/oneOf}} + * + * It could be an instance of the 'oneOf' schemas. + * The oneOf child schemas may themselves be a composed schema (allOf, anyOf, oneOf). + */ + @Override + public void setActualInstance(Object instance) { + {{#isNullable}} + if (instance == null) { + super.setActualInstance(instance); + return; + } + + {{/isNullable}} + {{#oneOf}} + if (JSON.isInstanceOf({{{.}}}.class, instance, new HashSet>())) { + super.setActualInstance(instance); + return; + } + + {{/oneOf}} + throw new RuntimeException("Invalid instance type. Must be {{#oneOf}}{{{.}}}{{^-last}}, {{/-last}}{{/oneOf}}"); + } + + /** + * Get the actual instance, which can be the following: + * {{#oneOf}}{{{.}}}{{^-last}}, {{/-last}}{{/oneOf}} + * + * @return The actual instance ({{#oneOf}}{{{.}}}{{^-last}}, {{/-last}}{{/oneOf}}) + */ + @Override + public Object getActualInstance() { + return super.getActualInstance(); + } + + {{#oneOf}} + /** + * Get the actual instance of `{{{.}}}`. If the actual instance is not `{{{.}}}`, + * the ClassCastException will be thrown. + * + * @return The actual instance of `{{{.}}}` + * @throws ClassCastException if the instance is not `{{{.}}}` + */ + public {{{.}}} get{{{.}}}() throws ClassCastException { + return ({{{.}}})super.getActualInstance(); + } + + {{/oneOf}} + +{{#supportUrlQuery}} + + /** + * Convert the instance into URL query string. + * + * @return URL query string + */ + public String toUrlQueryString() { + return toUrlQueryString(null); + } + + /** + * Convert the instance into URL query string. + * + * @param prefix prefix of the query string + * @return URL query string + */ + public String toUrlQueryString(String prefix) { + String suffix = ""; + String containerSuffix = ""; + String containerPrefix = ""; + if (prefix == null) { + // style=form, explode=true, e.g. /pet?name=cat&type=manx + prefix = ""; + } else { + // deepObject style e.g. /pet?id[name]=cat&id[type]=manx + prefix = prefix + "["; + suffix = "]"; + containerSuffix = "]"; + containerPrefix = "["; + } + + StringJoiner joiner = new StringJoiner("&"); + + {{#composedSchemas.oneOf}} + if (getActualInstance() instanceof {{{dataType}}}) { + {{#isArray}} + {{#items.isPrimitiveType}} + {{#uniqueItems}} + if (getActualInstance() != null) { + int i = 0; + for ({{{items.dataType}}} _item : ({{{dataType}}})getActualInstance()) { + joiner.add(String.format("%s{{baseName}}%s%s=%s", prefix, suffix, + "".equals(suffix) ? "" : String.format("%s%d%s", containerPrefix, i, containerSuffix), + URLEncoder.encode(String.valueOf(_item), StandardCharsets.UTF_8).replaceAll("\\+", "%20"))); + } + i++; + } + {{/uniqueItems}} + {{^uniqueItems}} + if (getActualInstance() != null) { + for (int i = 0; i < (({{{dataType}}})getActualInstance()).size(); i++) { + joiner.add(String.format("%s{{baseName}}%s%s=%s", prefix, suffix, + "".equals(suffix) ? "" : String.format("%s%d%s", containerPrefix, i, containerSuffix), + URLEncoder.encode(String.valueOf(getActualInstance().get(i)), StandardCharsets.UTF_8).replaceAll("\\+", "%20"))); + } + } + {{/uniqueItems}} + {{/items.isPrimitiveType}} + {{^items.isPrimitiveType}} + {{#items.isModel}} + {{#uniqueItems}} + if (getActualInstance() != null) { + int i = 0; + for ({{{items.dataType}}} _item : ({{{dataType}}})getActualInstance()) { + if (_item != null) { + joiner.add(_item.toUrlQueryString(String.format("%s{{baseName}}%s%s", prefix, suffix, + "".equals(suffix) ? "" : String.format("%s%d%s", containerPrefix, i, containerSuffix)))); + } + } + i++; + } + {{/uniqueItems}} + {{^uniqueItems}} + if (getActualInstance() != null) { + for (int i = 0; i < (({{{dataType}}})getActualInstance()).size(); i++) { + if ((({{{dataType}}})getActualInstance()).get(i) != null) { + joiner.add((({{{items.dataType}}})getActualInstance()).get(i).toUrlQueryString(String.format("%s{{baseName}}%s%s", prefix, suffix, + "".equals(suffix) ? "" : String.format("%s%d%s", containerPrefix, i, containerSuffix)))); + } + } + } + {{/uniqueItems}} + {{/items.isModel}} + {{^items.isModel}} + {{#uniqueItems}} + if (getActualInstance() != null) { + int i = 0; + for ({{{items.dataType}}} _item : ({{{dataType}}})getActualInstance()) { + if (_item != null) { + joiner.add(String.format("%s{{baseName}}%s%s=%s", prefix, suffix, + "".equals(suffix) ? "" : String.format("%s%d%s", containerPrefix, i, containerSuffix), + URLEncoder.encode(String.valueOf(_item), StandardCharsets.UTF_8).replaceAll("\\+", "%20"))); + } + i++; + } + } + {{/uniqueItems}} + {{^uniqueItems}} + if (getActualInstance() != null) { + for (int i = 0; i < (({{{dataType}}})getActualInstance()).size(); i++) { + if (getActualInstance().get(i) != null) { + joiner.add(String.format("%s{{baseName}}%s%s=%s", prefix, suffix, + "".equals(suffix) ? "" : String.format("%s%d%s", containerPrefix, i, containerSuffix), + URLEncoder.encode(String.valueOf((({{{dataType}}})getActualInstance()).get(i)), StandardCharsets.UTF_8).replaceAll("\\+", "%20"))); + } + } + } + {{/uniqueItems}} + {{/items.isModel}} + {{/items.isPrimitiveType}} + {{/isArray}} + {{^isArray}} + {{#isMap}} + {{#items.isPrimitiveType}} + if (getActualInstance() != null) { + for (String _key : (({{{dataType}}})getActualInstance()).keySet()) { + joiner.add(String.format("%s{{baseName}}%s%s=%s", prefix, suffix, + "".equals(suffix) ? "" : String.format("%s%d%s", containerPrefix, _key, containerSuffix), + getActualInstance().get(_key), URLEncoder.encode(String.valueOf((({{{dataType}}})getActualInstance()).get(_key)), StandardCharsets.UTF_8).replaceAll("\\+", "%20"))); + } + } + {{/items.isPrimitiveType}} + {{^items.isPrimitiveType}} + if (getActualInstance() != null) { + for (String _key : (({{{dataType}}})getActualInstance()).keySet()) { + if ((({{{dataType}}})getActualInstance()).get(_key) != null) { + joiner.add((({{{items.dataType}}})getActualInstance()).get(_key).toUrlQueryString(String.format("%s{{baseName}}%s%s", prefix, suffix, + "".equals(suffix) ? "" : String.format("%s%d%s", containerPrefix, _key, containerSuffix)))); + } + } + } + {{/items.isPrimitiveType}} + {{/isMap}} + {{^isMap}} + {{#isPrimitiveType}} + if (getActualInstance() != null) { + joiner.add(String.format("%s{{{baseName}}}%s=%s", prefix, suffix, URLEncoder.encode(String.valueOf(getActualInstance()), StandardCharsets.UTF_8).replaceAll("\\+", "%20"))); + } + {{/isPrimitiveType}} + {{^isPrimitiveType}} + {{#isModel}} + if (getActualInstance() != null) { + joiner.add((({{{dataType}}})getActualInstance()).toUrlQueryString(prefix + "{{{baseName}}}" + suffix)); + } + {{/isModel}} + {{^isModel}} + if (getActualInstance() != null) { + joiner.add(String.format("%s{{{baseName}}}%s=%s", prefix, suffix, URLEncoder.encode(String.valueOf(getActualInstance()), StandardCharsets.UTF_8).replaceAll("\\+", "%20"))); + } + {{/isModel}} + {{/isPrimitiveType}} + {{/isMap}} + {{/isArray}} + return joiner.toString(); + } + {{/composedSchemas.oneOf}} + return null; + } +{{/supportUrlQuery}} + +} diff --git a/config/clients/java/template/libraries/native/pojo.mustache b/config/clients/java/template/libraries/native/pojo.mustache new file mode 100644 index 00000000..9d7eab31 --- /dev/null +++ b/config/clients/java/template/libraries/native/pojo.mustache @@ -0,0 +1,596 @@ +{{#discriminator}} +import {{invokerPackage}}.JSON; +{{/discriminator}} +/** + * {{description}}{{^description}}{{classname}}{{/description}}{{#isDeprecated}} + * @deprecated{{/isDeprecated}} + */{{#isDeprecated}} +@Deprecated{{/isDeprecated}} +{{#swagger1AnnotationLibrary}} +{{#description}} +@ApiModel(description = "{{{.}}}") +{{/description}} +{{/swagger1AnnotationLibrary}} +{{#swagger2AnnotationLibrary}} +{{#description}} +@Schema(description = "{{{.}}}") +{{/description}} +{{/swagger2AnnotationLibrary}} +{{#jackson}} +@JsonPropertyOrder({ +{{#vars}} + {{classname}}.JSON_PROPERTY_{{nameInSnakeCase}}{{^-last}},{{/-last}} +{{/vars}} +}) +{{/jackson}} +{{>additionalModelTypeAnnotations}}{{#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}} + {{^vendorExtensions.x-enum-as-string}} +{{>modelInnerEnum}} + {{/vendorExtensions.x-enum-as-string}} + {{/isContainer}} + {{#isContainer}} + {{#mostInnerItems}} +{{>modelInnerEnum}} + {{/mostInnerItems}} + {{/isContainer}} + {{/isEnum}} + {{#gson}} + public static final String SERIALIZED_NAME_{{nameInSnakeCase}} = "{{baseName}}"; + {{/gson}} + {{#jackson}} + public static final String JSON_PROPERTY_{{nameInSnakeCase}} = "{{baseName}}"; + {{/jackson}} + {{#withXml}} + {{#isXmlAttribute}} + @XmlAttribute(name = "{{xmlName}}{{^xmlName}}{{baseName}}{{/xmlName}}") + {{/isXmlAttribute}} + {{^isXmlAttribute}} + {{^isContainer}} + @XmlElement({{#xmlNamespace}}namespace="{{.}}", {{/xmlNamespace}}name = "{{xmlName}}{{^xmlName}}{{baseName}}{{/xmlName}}") + {{/isContainer}} + {{#isContainer}} + // Is a container wrapped={{isXmlWrapped}} + {{#items}} + // items.name={{name}} items.baseName={{baseName}} items.xmlName={{xmlName}} items.xmlNamespace={{xmlNamespace}} + // items.example={{example}} items.type={{dataType}} + @XmlElement({{#xmlNamespace}}namespace="{{.}}", {{/xmlNamespace}}name = "{{xmlName}}{{^xmlName}}{{baseName}}{{/xmlName}}") + {{/items}} + {{#isXmlWrapped}} + @XmlElementWrapper({{#xmlNamespace}}namespace="{{.}}", {{/xmlNamespace}}name = "{{xmlName}}{{^xmlName}}{{baseName}}{{/xmlName}}") + {{/isXmlWrapped}} + {{/isContainer}} + {{/isXmlAttribute}} + {{/withXml}} + {{#gson}} + @SerializedName(SERIALIZED_NAME_{{nameInSnakeCase}}) + {{/gson}} + {{#vendorExtensions.x-field-extra-annotation}} + {{{vendorExtensions.x-field-extra-annotation}}} + {{/vendorExtensions.x-field-extra-annotation}} + {{#vendorExtensions.x-is-jackson-optional-nullable}} + {{#isContainer}} + private JsonNullable<{{{datatypeWithEnum}}}> {{name}} = JsonNullable.<{{{datatypeWithEnum}}}>undefined(); + {{/isContainer}} + {{^isContainer}} + private JsonNullable<{{{datatypeWithEnum}}}> {{name}} = JsonNullable.<{{{datatypeWithEnum}}}>{{#defaultValue}}of({{{.}}}){{/defaultValue}}{{^defaultValue}}undefined(){{/defaultValue}}; + {{/isContainer}} + {{/vendorExtensions.x-is-jackson-optional-nullable}} + {{^vendorExtensions.x-is-jackson-optional-nullable}} + private {{{datatypeWithEnum}}} {{name}}{{#defaultValue}} = {{{.}}}{{/defaultValue}}; + {{/vendorExtensions.x-is-jackson-optional-nullable}} + + {{/vars}} + public {{classname}}() { {{#parent}}{{#parcelableModel}} + super();{{/parcelableModel}}{{/parent}}{{#gson}}{{#discriminator}} + this.{{{discriminatorName}}} = this.getClass().getSimpleName();{{/discriminator}}{{/gson}} + }{{#vendorExtensions.x-has-readonly-properties}}{{^withXml}} + + {{#jackson}}@JsonCreator{{/jackson}} + public {{classname}}( + {{#readOnlyVars}} + @JsonProperty(JSON_PROPERTY_{{nameInSnakeCase}}) {{{datatypeWithEnum}}} {{name}}{{^-last}}, {{/-last}} + {{/readOnlyVars}} + ) { + this(); + {{#readOnlyVars}} + this.{{name}} = {{#vendorExtensions.x-is-jackson-optional-nullable}}{{name}} == null ? JsonNullable.<{{{datatypeWithEnum}}}>undefined() : JsonNullable.of({{name}}){{/vendorExtensions.x-is-jackson-optional-nullable}}{{^vendorExtensions.x-is-jackson-optional-nullable}}{{name}}{{/vendorExtensions.x-is-jackson-optional-nullable}}; + {{/readOnlyVars}} + }{{/withXml}}{{/vendorExtensions.x-has-readonly-properties}} + {{#vars}} + + {{^isReadOnly}} + {{#vendorExtensions.x-enum-as-string}} + public static final Set {{{nameInSnakeCase}}}_VALUES = new HashSet<>(Arrays.asList( + {{#allowableValues}}{{#enumVars}}{{{value}}}{{^-last}}, {{/-last}}{{/enumVars}}{{/allowableValues}} + )); + + {{/vendorExtensions.x-enum-as-string}} + public {{classname}} {{name}}({{{datatypeWithEnum}}} {{name}}) { + {{#vendorExtensions.x-enum-as-string}} + if (!{{{nameInSnakeCase}}}_VALUES.contains({{name}})) { + throw new IllegalArgumentException({{name}} + " is invalid. Possible values for {{name}}: " + String.join(", ", {{{nameInSnakeCase}}}_VALUES)); + } + + {{/vendorExtensions.x-enum-as-string}} + {{#vendorExtensions.x-is-jackson-optional-nullable}} + this.{{name}} = JsonNullable.<{{{datatypeWithEnum}}}>of({{name}}); + {{/vendorExtensions.x-is-jackson-optional-nullable}} + {{^vendorExtensions.x-is-jackson-optional-nullable}} + this.{{name}} = {{name}}; + {{/vendorExtensions.x-is-jackson-optional-nullable}} + return this; + } + {{#isArray}} + + public {{classname}} add{{nameInCamelCase}}Item({{{items.datatypeWithEnum}}} {{name}}Item) { + {{#vendorExtensions.x-is-jackson-optional-nullable}} + if (this.{{name}} == null || !this.{{name}}.isPresent()) { + this.{{name}} = JsonNullable.<{{{datatypeWithEnum}}}>of({{{defaultValue}}}{{^defaultValue}}new {{#uniqueItems}}LinkedHashSet{{/uniqueItems}}{{^uniqueItems}}ArrayList{{/uniqueItems}}<>(){{/defaultValue}}); + } + try { + this.{{name}}.get().add({{name}}Item); + } catch (java.util.NoSuchElementException e) { + // this can never happen, as we make sure above that the value is present + } + return this; + {{/vendorExtensions.x-is-jackson-optional-nullable}} + {{^vendorExtensions.x-is-jackson-optional-nullable}} + if (this.{{name}} == null) { + this.{{name}} = {{{defaultValue}}}{{^defaultValue}}new {{#uniqueItems}}LinkedHashSet{{/uniqueItems}}{{^uniqueItems}}ArrayList{{/uniqueItems}}<>(){{/defaultValue}}; + } + this.{{name}}.add({{name}}Item); + return this; + {{/vendorExtensions.x-is-jackson-optional-nullable}} + } + {{/isArray}} + {{#isMap}} + + public {{classname}} put{{nameInCamelCase}}Item(String key, {{{items.datatypeWithEnum}}} {{name}}Item) { + {{#vendorExtensions.x-is-jackson-optional-nullable}} + if (this.{{name}} == null || !this.{{name}}.isPresent()) { + this.{{name}} = JsonNullable.<{{{datatypeWithEnum}}}>of({{{defaultValue}}}{{^defaultValue}}new HashMap<>(){{/defaultValue}}); + } + try { + this.{{name}}.get().put(key, {{name}}Item); + } catch (java.util.NoSuchElementException e) { + // this can never happen, as we make sure above that the value is present + } + return this; + {{/vendorExtensions.x-is-jackson-optional-nullable}} + {{^vendorExtensions.x-is-jackson-optional-nullable}} + if (this.{{name}} == null) { + this.{{name}} = {{{defaultValue}}}{{^defaultValue}}new HashMap<>(){{/defaultValue}}; + } + this.{{name}}.put(key, {{name}}Item); + return this; + {{/vendorExtensions.x-is-jackson-optional-nullable}} + } + {{/isMap}} + + {{/isReadOnly}} + /** + {{#description}} + * {{.}} + {{/description}} + {{^description}} + * Get {{name}} + {{/description}} + {{#minimum}} + * minimum: {{.}} + {{/minimum}} + {{#maximum}} + * maximum: {{.}} + {{/maximum}} + * @return {{name}} + {{#deprecated}} + * @deprecated + {{/deprecated}} + **/ +{{#deprecated}} + @Deprecated +{{/deprecated}} +{{#required}} +{{#isNullable}} + @{{javaxPackage}}.annotation.Nullable +{{/isNullable}} +{{^isNullable}} + @{{javaxPackage}}.annotation.Nonnull +{{/isNullable}} +{{/required}} +{{^required}} + @{{javaxPackage}}.annotation.Nullable +{{/required}} +{{#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}} +{{#vendorExtensions.x-is-jackson-optional-nullable}} + {{!unannotated, Jackson would pick this up automatically and add it *in addition* to the _JsonNullable getter field}} + @JsonIgnore +{{/vendorExtensions.x-is-jackson-optional-nullable}} +{{^vendorExtensions.x-is-jackson-optional-nullable}}{{#jackson}}{{> jackson_annotations}}{{/jackson}}{{/vendorExtensions.x-is-jackson-optional-nullable}} + public {{{datatypeWithEnum}}} {{getter}}() { + {{#vendorExtensions.x-is-jackson-optional-nullable}} + {{#isReadOnly}}{{! A readonly attribute doesn't have setter => jackson will set null directly if explicitly returned by API, so make sure we have an empty JsonNullable}} + if ({{name}} == null) { + {{name}} = JsonNullable.<{{{datatypeWithEnum}}}>{{#defaultValue}}of({{{.}}}){{/defaultValue}}{{^defaultValue}}undefined(){{/defaultValue}}; + } + {{/isReadOnly}} + return {{name}}.orElse(null); + {{/vendorExtensions.x-is-jackson-optional-nullable}} + {{^vendorExtensions.x-is-jackson-optional-nullable}} + return {{name}}; + {{/vendorExtensions.x-is-jackson-optional-nullable}} + } + + {{#vendorExtensions.x-is-jackson-optional-nullable}} +{{> jackson_annotations}} + public JsonNullable<{{{datatypeWithEnum}}}> {{getter}}_JsonNullable() { + return {{name}}; + } + {{/vendorExtensions.x-is-jackson-optional-nullable}}{{#vendorExtensions.x-is-jackson-optional-nullable}} + @JsonProperty(JSON_PROPERTY_{{nameInSnakeCase}}) + {{#isReadOnly}}private{{/isReadOnly}}{{^isReadOnly}}public{{/isReadOnly}} void {{setter}}_JsonNullable(JsonNullable<{{{datatypeWithEnum}}}> {{name}}) { + {{! For getters/setters that have name differing from attribute name, we must include setter (albeit private) for jackson to be able to set the attribute}} + this.{{name}} = {{name}}; + } + {{/vendorExtensions.x-is-jackson-optional-nullable}} + + {{^isReadOnly}} +{{#vendorExtensions.x-setter-extra-annotation}} {{{vendorExtensions.x-setter-extra-annotation}}} +{{/vendorExtensions.x-setter-extra-annotation}}{{#jackson}}{{^vendorExtensions.x-is-jackson-optional-nullable}}{{> jackson_annotations}}{{/vendorExtensions.x-is-jackson-optional-nullable}}{{/jackson}} public void {{setter}}({{{datatypeWithEnum}}} {{name}}) { + {{#vendorExtensions.x-enum-as-string}} + if (!{{{nameInSnakeCase}}}_VALUES.contains({{name}})) { + throw new IllegalArgumentException({{name}} + " is invalid. Possible values for {{name}}: " + String.join(", ", {{{nameInSnakeCase}}}_VALUES)); + } + + {{/vendorExtensions.x-enum-as-string}} + {{#vendorExtensions.x-is-jackson-optional-nullable}} + this.{{name}} = JsonNullable.<{{{datatypeWithEnum}}}>of({{name}}); + {{/vendorExtensions.x-is-jackson-optional-nullable}} + {{^vendorExtensions.x-is-jackson-optional-nullable}} + this.{{name}} = {{name}}; + {{/vendorExtensions.x-is-jackson-optional-nullable}} + } + {{/isReadOnly}} + + {{/vars}} +{{>libraries/native/additional_properties}} + {{#parent}} + {{#allVars}} + {{#isOverridden}} + @Override + public {{classname}} {{name}}({{{datatypeWithEnum}}} {{name}}) { + {{#vendorExtensions.x-is-jackson-optional-nullable}} + this.{{setter}}(JsonNullable.<{{{datatypeWithEnum}}}>of({{name}})); + {{/vendorExtensions.x-is-jackson-optional-nullable}} + {{^vendorExtensions.x-is-jackson-optional-nullable}} + this.{{setter}}({{name}}); + {{/vendorExtensions.x-is-jackson-optional-nullable}} + return this; + } + + {{/isOverridden}} + {{/allVars}} + {{/parent}} + /** + * Return true if this {{name}} object is equal to o. + */ + @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}}{{#vendorExtensions.x-is-jackson-optional-nullable}}equalsNullable(this.{{name}}, {{classVarName}}.{{name}}){{/vendorExtensions.x-is-jackson-optional-nullable}}{{^vendorExtensions.x-is-jackson-optional-nullable}}{{#isByteArray}}Arrays{{/isByteArray}}{{^isByteArray}}Objects{{/isByteArray}}.equals(this.{{name}}, {{classVarName}}.{{name}}){{/vendorExtensions.x-is-jackson-optional-nullable}}{{^-last}} && + {{/-last}}{{/vars}}{{#additionalPropertiesType}}&& + Objects.equals(this.additionalProperties, {{classVarName}}.additionalProperties){{/additionalPropertiesType}}{{#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}}{{#vendorExtensions.x-is-jackson-optional-nullable}}hashCodeNullable({{name}}){{/vendorExtensions.x-is-jackson-optional-nullable}}{{^vendorExtensions.x-is-jackson-optional-nullable}}{{^isByteArray}}{{name}}{{/isByteArray}}{{#isByteArray}}Arrays.hashCode({{name}}){{/isByteArray}}{{/vendorExtensions.x-is-jackson-optional-nullable}}{{^-last}}, {{/-last}}{{/vars}}{{#parent}}{{#hasVars}}, {{/hasVars}}super.hashCode(){{/parent}}{{#additionalPropertiesType}}, additionalProperties{{/additionalPropertiesType}}); + {{/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(toIndentedString({{name}})).append("\n"); + {{/vars}} + {{#additionalPropertiesType}} + sb.append(" additionalProperties: ").append(toIndentedString(additionalProperties)).append("\n"); + {{/additionalPropertiesType}} + 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 "); + } +{{#supportUrlQuery}} + + /** + * Convert the instance into URL query string. + * + * @return URL query string + */ + public String toUrlQueryString() { + return toUrlQueryString(null); + } + + /** + * Convert the instance into URL query string. + * + * @param prefix prefix of the query string + * @return URL query string + */ + public String toUrlQueryString(String prefix) { + String suffix = ""; + String containerSuffix = ""; + String containerPrefix = ""; + if (prefix == null) { + // style=form, explode=true, e.g. /pet?name=cat&type=manx + prefix = ""; + } else { + // deepObject style e.g. /pet?id[name]=cat&id[type]=manx + prefix = prefix + "["; + suffix = "]"; + containerSuffix = "]"; + containerPrefix = "["; + } + + StringJoiner joiner = new StringJoiner("&"); + + {{#allVars}} + // add `{{baseName}}` to the URL query string + {{#isArray}} + {{#items.isPrimitiveType}} + {{#uniqueItems}} + if ({{getter}}() != null) { + int i = 0; + for ({{{items.dataType}}} _item : {{getter}}()) { + joiner.add(String.format("%s{{baseName}}%s%s=%s", prefix, suffix, + "".equals(suffix) ? "" : String.format("%s%d%s", containerPrefix, i, containerSuffix), + URLEncoder.encode(String.valueOf(_item), StandardCharsets.UTF_8).replaceAll("\\+", "%20"))); + } + i++; + } + {{/uniqueItems}} + {{^uniqueItems}} + if ({{getter}}() != null) { + for (int i = 0; i < {{getter}}().size(); i++) { + joiner.add(String.format("%s{{baseName}}%s%s=%s", prefix, suffix, + "".equals(suffix) ? "" : String.format("%s%d%s", containerPrefix, i, containerSuffix), + URLEncoder.encode(String.valueOf({{getter}}().get(i)), StandardCharsets.UTF_8).replaceAll("\\+", "%20"))); + } + } + {{/uniqueItems}} + {{/items.isPrimitiveType}} + {{^items.isPrimitiveType}} + {{#items.isModel}} + {{#uniqueItems}} + if ({{getter}}() != null) { + int i = 0; + for ({{{items.dataType}}} _item : {{getter}}()) { + if (_item != null) { + joiner.add(_item.toUrlQueryString(String.format("%s{{baseName}}%s%s", prefix, suffix, + "".equals(suffix) ? "" : String.format("%s%d%s", containerPrefix, i, containerSuffix)))); + } + } + i++; + } + {{/uniqueItems}} + {{^uniqueItems}} + if ({{getter}}() != null) { + for (int i = 0; i < {{getter}}().size(); i++) { + if ({{getter}}().get(i) != null) { + joiner.add({{getter}}().get(i).toUrlQueryString(String.format("%s{{baseName}}%s%s", prefix, suffix, + "".equals(suffix) ? "" : String.format("%s%d%s", containerPrefix, i, containerSuffix)))); + } + } + } + {{/uniqueItems}} + {{/items.isModel}} + {{^items.isModel}} + {{#uniqueItems}} + if ({{getter}}() != null) { + int i = 0; + for ({{{items.dataType}}} _item : {{getter}}()) { + if (_item != null) { + joiner.add(String.format("%s{{baseName}}%s%s=%s", prefix, suffix, + "".equals(suffix) ? "" : String.format("%s%d%s", containerPrefix, i, containerSuffix), + URLEncoder.encode(String.valueOf(_item), StandardCharsets.UTF_8).replaceAll("\\+", "%20"))); + } + i++; + } + } + {{/uniqueItems}} + {{^uniqueItems}} + if ({{getter}}() != null) { + for (int i = 0; i < {{getter}}().size(); i++) { + if ({{getter}}().get(i) != null) { + joiner.add(String.format("%s{{baseName}}%s%s=%s", prefix, suffix, + "".equals(suffix) ? "" : String.format("%s%d%s", containerPrefix, i, containerSuffix), + URLEncoder.encode(String.valueOf({{getter}}().get(i)), StandardCharsets.UTF_8).replaceAll("\\+", "%20"))); + } + } + } + {{/uniqueItems}} + {{/items.isModel}} + {{/items.isPrimitiveType}} + {{/isArray}} + {{^isArray}} + {{#isMap}} + {{^items.isModel}} + if ({{getter}}() != null) { + for (String _key : {{getter}}().keySet()) { + joiner.add(String.format("%s{{baseName}}%s%s=%s", prefix, suffix, + "".equals(suffix) ? "" : String.format("%s%d%s", containerPrefix, _key, containerSuffix), + {{getter}}().get(_key), URLEncoder.encode(String.valueOf({{getter}}().get(_key)), StandardCharsets.UTF_8).replaceAll("\\+", "%20"))); + } + } + {{/items.isModel}} + {{#items.isModel}} + if ({{getter}}() != null) { + for (String _key : {{getter}}().keySet()) { + if ({{getter}}().get(_key) != null) { + joiner.add({{getter}}().get(_key).toUrlQueryString(String.format("%s{{baseName}}%s%s", prefix, suffix, + "".equals(suffix) ? "" : String.format("%s%d%s", containerPrefix, _key, containerSuffix)))); + } + } + } + {{/items.isModel}} + {{/isMap}} + {{^isMap}} + {{#isPrimitiveType}} + if ({{getter}}() != null) { + joiner.add(String.format("%s{{{baseName}}}%s=%s", prefix, suffix, URLEncoder.encode(String.valueOf({{{getter}}}()), StandardCharsets.UTF_8).replaceAll("\\+", "%20"))); + } + {{/isPrimitiveType}} + {{^isPrimitiveType}} + {{#isModel}} + if ({{getter}}() != null) { + joiner.add({{getter}}().toUrlQueryString(prefix + "{{{baseName}}}" + suffix)); + } + {{/isModel}} + {{^isModel}} + if ({{getter}}() != null) { + joiner.add(String.format("%s{{{baseName}}}%s=%s", prefix, suffix, URLEncoder.encode(String.valueOf({{{getter}}}()), StandardCharsets.UTF_8).replaceAll("\\+", "%20"))); + } + {{/isModel}} + {{/isPrimitiveType}} + {{/isMap}} + {{/isArray}} + + {{/allVars}} + return joiner.toString(); + } +{{/supportUrlQuery}} +{{#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}} +{{#discriminator}} +static { + // Initialize and register the discriminator mappings. + Map> mappings = new HashMap>(); + {{#mappedModels}} + mappings.put("{{mappingName}}", {{modelName}}.class); + {{/mappedModels}} + mappings.put("{{name}}", {{classname}}.class); + JSON.registerDiscriminator({{classname}}.class, "{{propertyBaseName}}", mappings); +} +{{/discriminator}} +} diff --git a/config/clients/java/template/libraries/native/travis.mustache b/config/clients/java/template/libraries/native/travis.mustache new file mode 100644 index 00000000..c9464747 --- /dev/null +++ b/config/clients/java/template/libraries/native/travis.mustache @@ -0,0 +1,16 @@ +# +# Generated by: https://openapi-generator.tech +# +language: java +jdk: + - oraclejdk11 +before_install: + # ensure gradlew has proper permission + - chmod a+x ./gradlew +script: + # test using maven + - mvn test + # uncomment below to test using gradle + # - gradle test + # uncomment below to test using sbt + # - sbt test diff --git a/config/clients/java/template/licenseInfo.mustache b/config/clients/java/template/licenseInfo.mustache new file mode 100644 index 00000000..c66209f2 --- /dev/null +++ b/config/clients/java/template/licenseInfo.mustache @@ -0,0 +1,11 @@ +/* + * {{{appName}}} + * {{{appDescription}}} + * + * {{#version}}The version of the OpenAPI document: {{{.}}}{{/version}} + * {{#infoEmail}}Contact: {{{.}}}{{/infoEmail}} + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ diff --git a/config/clients/java/template/modelInnerEnum.mustache b/config/clients/java/template/modelInnerEnum.mustache new file mode 100644 index 00000000..43ad2986 --- /dev/null +++ b/config/clients/java/template/modelInnerEnum.mustache @@ -0,0 +1,95 @@ + /** + * {{description}}{{^description}}Gets or Sets {{{name}}}{{/description}} + */ +{{#gson}} + @JsonAdapter({{{datatypeWithEnum}}}{{^datatypeWithEnum}}{{classname}}{{/datatypeWithEnum}}.Adapter.class) +{{/gson}} +{{#jsonb}} + @JsonbTypeSerializer({{datatypeWithEnum}}.Serializer.class) + @JsonbTypeDeserializer({{datatypeWithEnum}}.Deserializer.class) +{{/jsonb}} +{{#withXml}} + @XmlType(name="{{datatypeWithEnum}}") + @XmlEnum({{dataType}}.class) +{{/withXml}} + {{>additionalEnumTypeAnnotations}}public enum {{{datatypeWithEnum}}}{{^datatypeWithEnum}}{{classname}}{{/datatypeWithEnum}} { + {{#allowableValues}} + {{#enumVars}} + {{#enumDescription}} + /** + * {{.}} + */ + {{/enumDescription}} + {{#withXml}} + @XmlEnumValue({{#isInteger}}"{{/isInteger}}{{#isDouble}}"{{/isDouble}}{{#isLong}}"{{/isLong}}{{#isFloat}}"{{/isFloat}}{{{value}}}{{#isInteger}}"{{/isInteger}}{{#isDouble}}"{{/isDouble}}{{#isLong}}"{{/isLong}}{{#isFloat}}"{{/isFloat}}) + {{/withXml}} + {{{name}}}({{{value}}}){{^-last}}, + {{/-last}}{{#-last}};{{/-last}} + {{/enumVars}} + {{/allowableValues}} + + private {{{dataType}}} value; + + {{{datatypeWithEnum}}}{{^datatypeWithEnum}}{{classname}}{{/datatypeWithEnum}}({{{dataType}}} value) { + this.value = value; + } + +{{#jackson}} + @JsonValue +{{/jackson}} + public {{{dataType}}} getValue() { + return value; + } + + @Override + public String toString() { + return String.valueOf(value); + } + +{{#jackson}} + @JsonCreator +{{/jackson}} + public static {{{datatypeWithEnum}}}{{^datatypeWithEnum}}{{{classname}}}{{/datatypeWithEnum}} fromValue({{{dataType}}} value) { + for ({{{datatypeWithEnum}}}{{^datatypeWithEnum}}{{{classname}}}{{/datatypeWithEnum}} b : {{{datatypeWithEnum}}}{{^datatypeWithEnum}}{{{classname}}}{{/datatypeWithEnum}}.values()) { + if (b.value.{{^isString}}equals{{/isString}}{{#isString}}{{#useEnumCaseInsensitive}}equalsIgnoreCase{{/useEnumCaseInsensitive}}{{^useEnumCaseInsensitive}}equals{{/useEnumCaseInsensitive}}{{/isString}}(value)) { + return b; + } + } + {{#isNullable}}return null;{{/isNullable}}{{^isNullable}}{{#enumUnknownDefaultCase}}{{#allowableValues}}{{#enumVars}}{{#-last}}return {{{name}}};{{/-last}}{{/enumVars}}{{/allowableValues}}{{/enumUnknownDefaultCase}}{{^enumUnknownDefaultCase}}throw new IllegalArgumentException("Unexpected value '" + value + "'");{{/enumUnknownDefaultCase}}{{/isNullable}} + } +{{#gson}} + + public static class Adapter extends TypeAdapter<{{{datatypeWithEnum}}}{{^datatypeWithEnum}}{{classname}}{{/datatypeWithEnum}}> { + @Override + public void write(final JsonWriter jsonWriter, final {{{datatypeWithEnum}}}{{^datatypeWithEnum}}{{classname}}{{/datatypeWithEnum}} enumeration) throws IOException { + jsonWriter.value(enumeration.getValue()); + } + + @Override + public {{{datatypeWithEnum}}}{{^datatypeWithEnum}}{{classname}}{{/datatypeWithEnum}} read(final JsonReader jsonReader) throws IOException { + {{^isNumber}}{{{dataType}}}{{/isNumber}}{{#isNumber}}String{{/isNumber}} value = {{#isFloat}}(float){{/isFloat}} jsonReader.{{#isNumber}}nextString(){{/isNumber}}{{#isInteger}}nextInt(){{/isInteger}}{{^isNumber}}{{^isInteger}}{{#isFloat}}nextDouble{{/isFloat}}{{^isFloat}}next{{{dataType}}}{{/isFloat}}(){{/isInteger}}{{/isNumber}}; + return {{{datatypeWithEnum}}}{{^datatypeWithEnum}}{{classname}}{{/datatypeWithEnum}}.fromValue({{#isNumber}}new BigDecimal({{/isNumber}}value{{#isNumber}}){{/isNumber}}); + } + } +{{/gson}} +{{#jsonb}} + public static final class Deserializer implements JsonbDeserializer<{{datatypeWithEnum}}> { + @Override + public {{datatypeWithEnum}} deserialize(JsonParser parser, DeserializationContext ctx, Type rtType) { + for ({{{datatypeWithEnum}}}{{^datatypeWithEnum}}{{{classname}}}{{/datatypeWithEnum}} b : {{{datatypeWithEnum}}}{{^datatypeWithEnum}}{{{classname}}}{{/datatypeWithEnum}}.values()) { + if (String.valueOf(b.value).equals(parser.getString())) { + return b; + } + } + {{#useNullForUnknownEnumValue}}return null;{{/useNullForUnknownEnumValue}}{{^useNullForUnknownEnumValue}}throw new IllegalArgumentException("Unexpected value '" + parser.getString() + "'");{{/useNullForUnknownEnumValue}} + } + } + + public static final class Serializer implements JsonbSerializer<{{datatypeWithEnum}}> { + @Override + public void serialize({{datatypeWithEnum}} obj, JsonGenerator generator, SerializationContext ctx) { + generator.write(obj.value); + } + } +{{/jsonb}} + } diff --git a/config/clients/java/template/model_test.mustache b/config/clients/java/template/model_test.mustache new file mode 100644 index 00000000..99c56f61 --- /dev/null +++ b/config/clients/java/template/model_test.mustache @@ -0,0 +1,43 @@ +{{>licenseInfo}} + +package {{package}}; + +{{#imports}}import {{import}}; +{{/imports}} +import org.junit.jupiter.api.Assert; +import org.junit.jupiter.api.Ignore; +import org.junit.jupiter.api.Test; + +/** + * Model tests for {{classname}} + */ +public class {{classname}}Test { + {{#models}} + {{#model}} + {{^vendorExtensions.x-is-one-of-interface}} + {{^isEnum}} + private final {{classname}} model = new {{classname}}(); + + {{/isEnum}} + /** + * Model tests for {{classname}} + */ + @Test + public void test{{classname}}() { + // TODO: test {{classname}} + } + + {{#allVars}} + /** + * Test the property '{{name}}' + */ + @Test + public void {{name}}Test() { + // TODO: test {{name}} + } + + {{/allVars}} + {{/vendorExtensions.x-is-one-of-interface}} + {{/model}} + {{/models}} +} diff --git a/config/clients/java/template/oneof_interface.mustache b/config/clients/java/template/oneof_interface.mustache new file mode 100644 index 00000000..68be2ac6 --- /dev/null +++ b/config/clients/java/template/oneof_interface.mustache @@ -0,0 +1,6 @@ +{{>additionalOneOfTypeAnnotations}}{{>typeInfoAnnotation}}{{>xmlAnnotation}} +public interface {{classname}} {{#vendorExtensions.x-implements}}{{#-first}}extends {{{.}}}{{/-first}}{{^-first}}, {{{.}}}{{/-first}}{{/vendorExtensions.x-implements}} { + {{#discriminator}} + public {{propertyType}} {{propertyGetter}}(); + {{/discriminator}} +} diff --git a/config/clients/java/template/openapi.mustache b/config/clients/java/template/openapi.mustache new file mode 100644 index 00000000..34fbb53f --- /dev/null +++ b/config/clients/java/template/openapi.mustache @@ -0,0 +1 @@ +{{{openapi-yaml}}} diff --git a/config/clients/java/template/pojo_doc.mustache b/config/clients/java/template/pojo_doc.mustache new file mode 100644 index 00000000..bae0bc48 --- /dev/null +++ b/config/clients/java/template/pojo_doc.mustache @@ -0,0 +1,37 @@ +# {{#vendorExtensions.x-is-one-of-interface}}Interface {{/vendorExtensions.x-is-one-of-interface}}{{classname}} + +{{#description}}{{&description}} +{{/description}} +{{^vendorExtensions.x-is-one-of-interface}} + +## Properties + +| Name | Type | Description | Notes | +|------------ | ------------- | ------------- | -------------| +{{#vars}}|**{{name}}** | {{#isEnum}}[**{{datatypeWithEnum}}**](#{{datatypeWithEnum}}){{/isEnum}}{{^isEnum}}{{#isContainer}}{{#isArray}}{{#items}}{{#isModel}}[{{/isModel}}{{/items}}**{{baseType}}{{#items}}<{{dataType}}>**{{#isModel}}]({{^baseType}}{{dataType}}{{/baseType}}{{baseType}}.md){{/isModel}}{{/items}}{{/isArray}}{{#isMap}}{{#items}}{{#isModel}}[{{/isModel}}**Map<String, {{dataType}}>**{{#isModel}}]({{^baseType}}{{dataType}}{{/baseType}}{{baseType}}.md){{/isModel}}{{/items}}{{/isMap}}{{/isContainer}}{{^isContainer}}{{#isModel}}[{{/isModel}}**{{dataType}}**{{#isModel}}]({{^baseType}}{{dataType}}{{/baseType}}{{baseType}}.md){{/isModel}}{{/isContainer}}{{/isEnum}} | {{description}} | {{^required}} [optional]{{/required}}{{#isReadOnly}} [readonly]{{/isReadOnly}} | +{{/vars}} +{{#vars}}{{#isEnum}} + + +## Enum: {{datatypeWithEnum}} + +| Name | Value | +|---- | -----|{{#allowableValues}}{{#enumVars}} +| {{name}} | {{value}} |{{/enumVars}}{{/allowableValues}} +{{/isEnum}}{{/vars}} +{{#vendorExtensions.x-implements.0}} + +## Implemented Interfaces + +{{#vendorExtensions.x-implements}} +* {{{.}}} +{{/vendorExtensions.x-implements}} +{{/vendorExtensions.x-implements.0}} +{{/vendorExtensions.x-is-one-of-interface}} +{{#vendorExtensions.x-is-one-of-interface}} +## Implementing Classes + +{{#oneOf}} +* {{{.}}} +{{/oneOf}} +{{/vendorExtensions.x-is-one-of-interface}} diff --git a/config/clients/java/template/settings.gradle.mustache b/config/clients/java/template/settings.gradle.mustache new file mode 100644 index 00000000..448dc076 --- /dev/null +++ b/config/clients/java/template/settings.gradle.mustache @@ -0,0 +1 @@ +rootProject.name = '{{artifactId}}' \ No newline at end of file diff --git a/config/clients/java/template/typeInfoAnnotation.mustache b/config/clients/java/template/typeInfoAnnotation.mustache new file mode 100644 index 00000000..c833321e --- /dev/null +++ b/config/clients/java/template/typeInfoAnnotation.mustache @@ -0,0 +1,17 @@ +{{#jackson}} + +@JsonIgnoreProperties( + value = "{{{discriminator.propertyBaseName}}}", // ignore manually set {{{discriminator.propertyBaseName}}}, it will be automatically generated by Jackson during serialization + allowSetters = true // allows the {{{discriminator.propertyBaseName}}} to be set during deserialization +) +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "{{{discriminator.propertyBaseName}}}", visible = true) +{{#discriminator.mappedModels}} +{{#-first}} +@JsonSubTypes({ +{{/-first}} + @JsonSubTypes.Type(value = {{modelName}}.class, name = "{{^vendorExtensions.x-discriminator-value}}{{mappingName}}{{/vendorExtensions.x-discriminator-value}}{{#vendorExtensions.x-discriminator-value}}{{{vendorExtensions.x-discriminator-value}}}{{/vendorExtensions.x-discriminator-value}}"), +{{#-last}} +}) +{{/-last}} +{{/discriminator.mappedModels}} +{{/jackson}} diff --git a/config/clients/java/template/util-StringUtil.java.mustache b/config/clients/java/template/util-StringUtil.java.mustache new file mode 100644 index 00000000..9e390036 --- /dev/null +++ b/config/clients/java/template/util-StringUtil.java.mustache @@ -0,0 +1,23 @@ +{{>licenseInfo}} +package dev.openfga.util; + +import java.util.function.Predicate; +import java.util.regex.Pattern; + +public class StringUtil { + private StringUtil() {} // Instantiation prevented for utility class. + + private static final Predicate NULL_OR_WS = + Pattern.compile("^\\s*$").asMatchPredicate(); + + /** + * Returns true when the String is null, empty or contains only whitespace + * characters. + * + * @param str The String being tested. + * @return true iff str is null, empty or contains only whitespace. + */ + public static boolean isNullOrWhitespace(String str) { + return str == null || NULL_OR_WS.test(str); + } +} diff --git a/config/clients/java/template/util-StringUtilTest.java.mustache b/config/clients/java/template/util-StringUtilTest.java.mustache new file mode 100644 index 00000000..04eb5e93 --- /dev/null +++ b/config/clients/java/template/util-StringUtilTest.java.mustache @@ -0,0 +1,39 @@ +{{>licenseInfo}} +package dev.openfga.util; + +import org.junit.jupiter.api.Test; + +import static dev.openfga.util.StringUtil.isNullOrWhitespace; +import static org.junit.jupiter.api.Assertions.*; + +class StringUtilTest { + @Test + public void isNullOrWhitespace_someContent_false() { + assertFalse(isNullOrWhitespace("abc")); + } + + @Test + public void isNullOrWhitespace_null_true() { + assertTrue(isNullOrWhitespace(null)); + } + + @Test + public void isNullOrWhitespace_empty_true() { + assertTrue(isNullOrWhitespace("")); + } + + @Test + public void isNullOrWhitespace_singleSpace_true() { + assertTrue(isNullOrWhitespace(" ")); + } + + @Test + public void isNullOrWhitespace_multipleSpace_true() { + assertTrue(isNullOrWhitespace(" ")); + } + + @Test + public void isNullOrWhitespace_multipleOtherWhitespace_true() { + assertTrue(isNullOrWhitespace(" \t\r\n")); + } +} \ No newline at end of file diff --git a/config/common/files/.github/workflows/semgrep.yaml b/config/common/files/.github/workflows/semgrep.yaml index 66e24d30..44718e20 100644 --- a/config/common/files/.github/workflows/semgrep.yaml +++ b/config/common/files/.github/workflows/semgrep.yaml @@ -11,7 +11,7 @@ jobs: image: returntocorp/semgrep if: (github.actor != 'dependabot[bot]' && github.actor != 'snyk-bot') steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.2 - run: semgrep ci env: SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }}