From 3d5f9c881b042198cfdb06c34125c978ffaafbf5 Mon Sep 17 00:00:00 2001 From: Thomas Farr Date: Thu, 16 May 2024 12:08:40 +1200 Subject: [PATCH] Improve parsing logic Signed-off-by: Thomas Farr --- .gitignore | 4 +- java-codegen/build.gradle.kts | 5 +- .../client/codegen/JavaFormatter.java | 1 - .../org/opensearch/client/codegen/Main.java | 10 +- .../client/codegen/NameSanitizer.java | 4 +- .../client/codegen/model/Deprecation.java | 18 +- .../client/codegen/model/Field.java | 17 +- .../client/codegen/model/HttpPath.java | 12 +- .../client/codegen/model/Namespace.java | 5 +- .../client/codegen/model/OperationGroup.java | 45 ++-- .../client/codegen/model/RequestShape.java | 22 +- .../client/codegen/model/SpecTransformer.java | 163 ++++++++----- .../client/codegen/openapi/HttpMethod.java | 29 +++ .../client/codegen/openapi/HttpStatus.java | 24 -- .../codegen/openapi/HttpStatusCode.java | 44 ++++ .../opensearch/client/codegen/openapi/In.java | 23 ++ .../client/codegen/openapi/JsonPointer.java | 121 +++++---- .../client/codegen/openapi/MimeType.java | 23 +- .../codegen/openapi/OpenApiApiResponse.java | 27 --- .../codegen/openapi/OpenApiApiResponses.java | 18 -- .../codegen/openapi/OpenApiComponents.java | 35 +++ .../codegen/openapi/OpenApiContent.java | 8 +- .../codegen/openapi/OpenApiElement.java | 130 ++++++++++ .../codegen/openapi/OpenApiExtensions.java | 18 -- .../codegen/openapi/OpenApiMapElement.java | 43 ++++ .../codegen/openapi/OpenApiMapObject.java | 32 --- .../codegen/openapi/OpenApiMediaType.java | 17 +- .../client/codegen/openapi/OpenApiObject.java | 121 --------- .../codegen/openapi/OpenApiOperation.java | 117 ++++++--- .../openapi/OpenApiOperationBodyElement.java | 27 +++ .../codegen/openapi/OpenApiParameter.java | 60 +++-- .../client/codegen/openapi/OpenApiPath.java | 48 ++-- .../codegen/openapi/OpenApiProperty.java | 28 --- .../codegen/openapi/OpenApiRefElement.java | 139 +++++++++++ .../codegen/openapi/OpenApiRefObject.java | 50 ---- .../codegen/openapi/OpenApiRequestBody.java | 17 +- .../codegen/openapi/OpenApiResponse.java | 18 ++ .../codegen/openapi/OpenApiResponses.java | 18 ++ .../client/codegen/openapi/OpenApiSchema.java | 229 ++++++++++-------- .../codegen/openapi/OpenApiSchemaFormat.java | 25 ++ .../codegen/openapi/OpenApiSchemaType.java | 27 +++ .../client/codegen/openapi/OpenApiSpec.java | 115 --------- .../codegen/openapi/OpenApiSpecification.java | 104 ++++++++ .../client/codegen/utils/Functional.java | 19 ++ .../client/codegen/utils/Lists.java | 48 ++++ .../opensearch/client/codegen/utils/Maps.java | 49 ++++ .../opensearch/client/codegen/utils/Sets.java | 24 ++ .../client/codegen/utils/Streams.java | 7 +- .../client/codegen/utils/Strings.java | 45 +++- 49 files changed, 1447 insertions(+), 786 deletions(-) create mode 100644 java-codegen/src/main/java/org/opensearch/client/codegen/openapi/HttpMethod.java delete mode 100644 java-codegen/src/main/java/org/opensearch/client/codegen/openapi/HttpStatus.java create mode 100644 java-codegen/src/main/java/org/opensearch/client/codegen/openapi/HttpStatusCode.java create mode 100644 java-codegen/src/main/java/org/opensearch/client/codegen/openapi/In.java delete mode 100644 java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiApiResponse.java delete mode 100644 java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiApiResponses.java create mode 100644 java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiComponents.java create mode 100644 java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiElement.java delete mode 100644 java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiExtensions.java create mode 100644 java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiMapElement.java delete mode 100644 java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiMapObject.java delete mode 100644 java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiObject.java create mode 100644 java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiOperationBodyElement.java delete mode 100644 java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiProperty.java create mode 100644 java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiRefElement.java delete mode 100644 java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiRefObject.java create mode 100644 java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiResponse.java create mode 100644 java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiResponses.java create mode 100644 java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiSchemaFormat.java create mode 100644 java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiSchemaType.java delete mode 100644 java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiSpec.java create mode 100644 java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiSpecification.java create mode 100644 java-codegen/src/main/java/org/opensearch/client/codegen/utils/Functional.java create mode 100644 java-codegen/src/main/java/org/opensearch/client/codegen/utils/Lists.java create mode 100644 java-codegen/src/main/java/org/opensearch/client/codegen/utils/Maps.java create mode 100644 java-codegen/src/main/java/org/opensearch/client/codegen/utils/Sets.java diff --git a/.gitignore b/.gitignore index b8638f2e72..7ccebda403 100644 --- a/.gitignore +++ b/.gitignore @@ -22,4 +22,6 @@ gradle-app.setting .ci/output java-client/bin -samples/bin \ No newline at end of file +samples/bin + +.DS_Store diff --git a/java-codegen/build.gradle.kts b/java-codegen/build.gradle.kts index c3b527bf41..d4b1031295 100644 --- a/java-codegen/build.gradle.kts +++ b/java-codegen/build.gradle.kts @@ -69,7 +69,7 @@ application { tasks.named("run") { args = listOf( - "https://raw.githubusercontent.com/opensearch-project/opensearch-api-specification/feature/native_openapi/spec/OpenSearch.openapi.yaml", + "https://github.com/opensearch-project/opensearch-api-specification/releases/download/main/opensearch-openapi.yaml", "$rootDir/buildSrc/formatterConfig.xml", "${project(":java-client").projectDir}/src/generated/java/" ) @@ -139,6 +139,9 @@ dependencies { // https://search.maven.org/artifact/com.google.code.findbugs/jsr305 implementation("com.google.code.findbugs:jsr305:3.0.2") + // Apache 2.0 + compileOnly("org.jetbrains:annotations:24.1.0") + // Apache 2.0 implementation("org.apache.maven.resolver:maven-resolver-api:1.9.18") implementation("org.apache.maven.resolver:maven-resolver-supplier:1.9.18") diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/JavaFormatter.java b/java-codegen/src/main/java/org/opensearch/client/codegen/JavaFormatter.java index 383cde2a8e..e68c714b2f 100644 --- a/java-codegen/src/main/java/org/opensearch/client/codegen/JavaFormatter.java +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/JavaFormatter.java @@ -19,7 +19,6 @@ import com.diffplug.spotless.java.ImportOrderStep; import com.diffplug.spotless.java.RemoveUnusedImportsStep; import java.io.File; -import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.util.List; diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/Main.java b/java-codegen/src/main/java/org/opensearch/client/codegen/Main.java index caa1a855a9..d61e3c9c32 100644 --- a/java-codegen/src/main/java/org/opensearch/client/codegen/Main.java +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/Main.java @@ -13,17 +13,14 @@ import java.net.URI; import java.nio.file.Files; import java.nio.file.Path; -import java.util.Collection; import java.util.Comparator; -import java.util.List; -import java.util.regex.Pattern; import java.util.stream.Stream; import org.opensearch.client.codegen.exceptions.ApiSpecificationParseException; import org.opensearch.client.codegen.exceptions.RenderException; import org.opensearch.client.codegen.model.Namespace; import org.opensearch.client.codegen.model.OperationGroup; import org.opensearch.client.codegen.model.SpecTransformer; -import org.opensearch.client.codegen.openapi.OpenApiSpec; +import org.opensearch.client.codegen.openapi.OpenApiSpecification; public class Main { private static final OperationGroup.Matcher OPERATION_MATCHER = OperationGroup.matcher(); @@ -59,13 +56,16 @@ public static void main(String[] args) { } private static Namespace parseSpec(URI location) throws ApiSpecificationParseException { - var spec = OpenApiSpec.parse(location); + var spec = OpenApiSpecification.retrieve(location); var transformer = new SpecTransformer(OPERATION_MATCHER); transformer.visit(spec); return transformer.getRoot(); } private static void cleanDirectory(File dir) throws RenderException { + if (!dir.exists()) { + return; + } try (Stream walker = Files.walk(dir.toPath())) { walker.sorted(Comparator.reverseOrder()).map(Path::toFile).forEach(File::delete); } catch (IOException e) { diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/NameSanitizer.java b/java-codegen/src/main/java/org/opensearch/client/codegen/NameSanitizer.java index 2e44c30cae..8370a06c14 100644 --- a/java-codegen/src/main/java/org/opensearch/client/codegen/NameSanitizer.java +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/NameSanitizer.java @@ -10,6 +10,7 @@ import java.util.HashSet; import java.util.Set; +import javax.annotation.Nonnull; import org.opensearch.client.codegen.utils.Strings; public class NameSanitizer { @@ -21,7 +22,8 @@ public class NameSanitizer { } }; - public static String wireNameToField(String wireName) { + @Nonnull + public static String wireNameToField(@Nonnull String wireName) { var name = Strings.toCamelCase(wireName); if (reservedWords.contains(name)) { name += "_"; diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/model/Deprecation.java b/java-codegen/src/main/java/org/opensearch/client/codegen/model/Deprecation.java index 41c45999bb..efe8c05826 100644 --- a/java-codegen/src/main/java/org/opensearch/client/codegen/model/Deprecation.java +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/model/Deprecation.java @@ -1,11 +1,27 @@ package org.opensearch.client.codegen.model; +import java.util.Optional; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + public class Deprecation { + @Nullable private final String description; + @Nullable private final String version; - public Deprecation(String description, String version) { + public Deprecation(@Nullable String description, @Nullable String version) { this.description = description; this.version = version; } + + @Nonnull + public Optional getDescription() { + return Optional.ofNullable(description); + } + + @Nonnull + public Optional getVersion() { + return Optional.ofNullable(version); + } } diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/model/Field.java b/java-codegen/src/main/java/org/opensearch/client/codegen/model/Field.java index 87234a75a9..84b3c1063d 100644 --- a/java-codegen/src/main/java/org/opensearch/client/codegen/model/Field.java +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/model/Field.java @@ -8,29 +8,39 @@ package org.opensearch.client.codegen.model; +import java.util.Objects; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; import org.opensearch.client.codegen.NameSanitizer; +import org.opensearch.client.codegen.utils.Strings; public class Field { + @Nonnull private final String wireName; + @Nonnull private final Type type; private boolean required; + @Nullable private final String description; - public Field(String wireName, Type type, boolean required, String description) { - this.wireName = wireName; - this.type = type; + public Field(@Nonnull String wireName, @Nonnull Type type, boolean required, @Nullable String description) { + this.wireName = Strings.requireNonBlank(wireName, "wireName must not be null"); + this.type = Objects.requireNonNull(type, "type must not be null"); this.required = required; this.description = description; } + @Nonnull public String getWireName() { return wireName; } + @Nonnull public String getName() { return NameSanitizer.wireNameToField(wireName); } + @Nonnull public Type getType() { return required ? type : type.getBoxed(); } @@ -43,6 +53,7 @@ public void setRequired(boolean required) { this.required = required; } + @Nullable public String getDescription() { return description; } diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/model/HttpPath.java b/java-codegen/src/main/java/org/opensearch/client/codegen/model/HttpPath.java index 584e01e3a1..d150c64d17 100644 --- a/java-codegen/src/main/java/org/opensearch/client/codegen/model/HttpPath.java +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/model/HttpPath.java @@ -12,15 +12,21 @@ import java.util.Collection; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; import org.opensearch.client.codegen.openapi.OpenApiOperation; import org.opensearch.client.codegen.utils.Either; public class HttpPath { + @Nonnull private final List parts; + @Nullable private final Deprecation deprecation; + @Nullable private final String versionAdded; public static HttpPath from(String httpPath, OpenApiOperation operation, Map pathParams) { @@ -44,11 +50,11 @@ public static HttpPath from(String httpPath, OpenApiOperation operation, Map parts, Deprecation deprecation, String versionAdded) { - this.parts = parts; + private HttpPath(@Nonnull List parts, @Nullable Deprecation deprecation, @Nullable String versionAdded) { + this.parts = Objects.requireNonNull(parts, "parts must not be null"); this.deprecation = deprecation; this.versionAdded = versionAdded; } diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/model/Namespace.java b/java-codegen/src/main/java/org/opensearch/client/codegen/model/Namespace.java index e606c38e1e..effa09a10c 100644 --- a/java-codegen/src/main/java/org/opensearch/client/codegen/model/Namespace.java +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/model/Namespace.java @@ -14,6 +14,8 @@ import java.util.List; import java.util.Map; import java.util.TreeMap; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; import org.opensearch.client.codegen.JavaFormatter; import org.opensearch.client.codegen.exceptions.RenderException; import org.opensearch.client.codegen.utils.Strings; @@ -51,7 +53,8 @@ private String getPackageNamePart() { return name; } - public Namespace child(String name) { + @Nonnull + public Namespace child(@Nullable String name) { if (name == null || name.isEmpty()) { return this; } diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/model/OperationGroup.java b/java-codegen/src/main/java/org/opensearch/client/codegen/model/OperationGroup.java index 1c26479a12..0ccf012762 100644 --- a/java-codegen/src/main/java/org/opensearch/client/codegen/model/OperationGroup.java +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/model/OperationGroup.java @@ -2,16 +2,25 @@ import java.util.Collection; import java.util.HashSet; +import java.util.Objects; +import java.util.Optional; import java.util.Set; import java.util.regex.Pattern; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; import org.apache.commons.lang3.builder.EqualsBuilder; import org.apache.commons.lang3.builder.HashCodeBuilder; +import org.opensearch.client.codegen.utils.Strings; public class OperationGroup { + @Nullable private final String namespace; + @Nonnull private final String name; - public static OperationGroup from(String operationGroup) { + @Nonnull + public static OperationGroup from(@Nonnull String operationGroup) { + Strings.requireNonBlank(operationGroup, "operationGroup must not be blank"); int index = operationGroup.lastIndexOf('.'); if (index == -1) { return new OperationGroup(null, operationGroup); @@ -19,15 +28,17 @@ public static OperationGroup from(String operationGroup) { return new OperationGroup(operationGroup.substring(0, index), operationGroup.substring(index + 1)); } - private OperationGroup(String namespace, String name) { + private OperationGroup(@Nullable String namespace, @Nonnull String name) { this.namespace = namespace; - this.name = name; + this.name = Strings.requireNonBlank(name, "name must not be blank"); } - public String getNamespace() { - return namespace; + @Nonnull + public Optional getNamespace() { + return Optional.ofNullable(namespace); } + @Nonnull public String getName() { return name; } @@ -53,6 +64,7 @@ public int hashCode() { return new HashCodeBuilder(17, 37).append(namespace).append(name).toHashCode(); } + @Nonnull public static Matcher matcher() { return new Matcher(); } @@ -62,12 +74,12 @@ public static class Matcher { private final Set operations = new HashSet<>(); private final Collection patterns = new HashSet<>(); - private Matcher() { - } + private Matcher() {} - public Matcher add(String namespace, String... operations) { + @Nonnull + public Matcher add(@Nullable String namespace, @Nullable String... operations) { if (operations == null || operations.length == 0) { - namespaces.add(namespace); + namespaces.add(Strings.requireNonBlank(namespace, "namespace must not be blank")); } else { for (String operation : operations) { add(new OperationGroup(namespace, operation)); @@ -76,18 +88,21 @@ public Matcher add(String namespace, String... operations) { return this; } - public Matcher add(OperationGroup operation) { - operations.add(operation); + @Nonnull + public Matcher add(@Nonnull OperationGroup operation) { + operations.add(Objects.requireNonNull(operation, "operation must not be null")); return this; } - public Matcher add(Pattern pattern) { - patterns.add(pattern); + @Nonnull + public Matcher add(@Nonnull Pattern pattern) { + patterns.add(Objects.requireNonNull(pattern, "pattern must not be null")); return this; } - public boolean matches(OperationGroup operation) { - if (namespaces.contains(operation.getNamespace())) { + public boolean matches(@Nonnull OperationGroup operation) { + Objects.requireNonNull(operation, "operation must not be null"); + if (operation.getNamespace().map(namespaces::contains).orElse(false)) { return true; } if (operations.contains(operation)) { diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/model/RequestShape.java b/java-codegen/src/main/java/org/opensearch/client/codegen/model/RequestShape.java index 5e677249f7..8110eaf171 100644 --- a/java-codegen/src/main/java/org/opensearch/client/codegen/model/RequestShape.java +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/model/RequestShape.java @@ -13,27 +13,38 @@ import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.TreeMap; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; import org.apache.commons.lang3.tuple.Pair; import org.opensearch.client.codegen.utils.Streams; import org.opensearch.client.codegen.utils.Strings; public class RequestShape extends ObjectShape { + @Nonnull private final OperationGroup operationGroup; + @Nullable private final String description; + @Nonnull private final Set httpMethods = new HashSet<>(); + @Nonnull private final List httpPaths = new ArrayList<>(); + @Nonnull private final Map queryParams = new TreeMap<>(); + @Nonnull private final Map pathParams = new TreeMap<>(); + @Nonnull private final Map fields = new TreeMap<>(); - public RequestShape(Namespace parent, OperationGroup operationGroup, String description) { + public RequestShape(@Nonnull Namespace parent, @Nonnull OperationGroup operationGroup, @Nullable String description) { super(parent, requestClassName(operationGroup), operationGroup + ".Request"); this.operationGroup = operationGroup; this.description = description; } + @Nonnull public OperationGroup getOperationGroup() { return operationGroup; } @@ -42,6 +53,7 @@ public String getId() { return operationGroup.getName(); } + @Nullable public String getDescription() { return description; } @@ -136,11 +148,15 @@ public boolean hasAnyRequiredFields() { return fields.values().stream().anyMatch(Field::isRequired); } - public static String requestClassName(OperationGroup operationGroup) { + @Nonnull + public static String requestClassName(@Nonnull OperationGroup operationGroup) { + Objects.requireNonNull(operationGroup, "operationGroup must not be null"); return Strings.toPascalCase(operationGroup.getName()) + "Request"; } - public static String responseClassName(OperationGroup operationGroup) { + @Nonnull + public static String responseClassName(@Nonnull OperationGroup operationGroup) { + Objects.requireNonNull(operationGroup, "operationGroup must not be null"); return Strings.toPascalCase(operationGroup.getName()) + "Response"; } } diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/model/SpecTransformer.java b/java-codegen/src/main/java/org/opensearch/client/codegen/model/SpecTransformer.java index 974e23322c..5ce5c05332 100644 --- a/java-codegen/src/main/java/org/opensearch/client/codegen/model/SpecTransformer.java +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/model/SpecTransformer.java @@ -9,88 +9,119 @@ package org.opensearch.client.codegen.model; import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Objects; +import java.util.Optional; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; import java.util.stream.Stream; +import javax.annotation.Nonnull; import org.apache.commons.lang3.NotImplementedException; -import org.opensearch.client.codegen.openapi.HttpStatus; +import org.opensearch.client.codegen.openapi.HttpStatusCode; +import org.opensearch.client.codegen.openapi.In; import org.opensearch.client.codegen.openapi.MimeType; -import org.opensearch.client.codegen.openapi.OpenApiApiResponse; import org.opensearch.client.codegen.openapi.OpenApiMediaType; import org.opensearch.client.codegen.openapi.OpenApiOperation; import org.opensearch.client.codegen.openapi.OpenApiParameter; -import org.opensearch.client.codegen.openapi.OpenApiProperty; +import org.opensearch.client.codegen.openapi.OpenApiPath; import org.opensearch.client.codegen.openapi.OpenApiRequestBody; +import org.opensearch.client.codegen.openapi.OpenApiResponse; import org.opensearch.client.codegen.openapi.OpenApiSchema; -import org.opensearch.client.codegen.openapi.OpenApiSpec; +import org.opensearch.client.codegen.openapi.OpenApiSchemaFormat; +import org.opensearch.client.codegen.openapi.OpenApiSpecification; public class SpecTransformer { + @Nonnull private final OperationGroup.Matcher matcher; + @Nonnull private final Namespace root = new Namespace(); + @Nonnull private final Set visitedSchemas = new HashSet<>(); + @Nonnull private final Map schemaToType = new ConcurrentHashMap<>(); - public SpecTransformer(OperationGroup.Matcher matcher) { - this.matcher = matcher; + public SpecTransformer(@Nonnull OperationGroup.Matcher matcher) { + this.matcher = Objects.requireNonNull(matcher, "matcher must not be null"); } + @Nonnull public Namespace getRoot() { return root; } - public void visit(OpenApiSpec spec) { + public void visit(@Nonnull OpenApiSpecification spec) { + Objects.requireNonNull(spec, "spec must not be null"); + var groupedOperations = new HashMap>(); - spec.getOperations().forEach((operation) -> { - var group = operation.getXOperationGroup(); - if (!matcher.matches(group)) return; - groupedOperations.computeIfAbsent(group, k -> new ArrayList<>()).add(operation); - }); + spec.getPaths() + .stream() + .map(Map::values) + .flatMap(Collection::stream) + .map(OpenApiPath::getOperations) + .flatMap(Optional::stream) + .map(Map::values) + .flatMap(Collection::stream) + .forEach(operation -> { + var group = operation.getOperationGroup(); + if (!matcher.matches(group)) { + return; + } + groupedOperations.computeIfAbsent(group, k -> new ArrayList<>()).add(operation); + }); groupedOperations.forEach(this::visit); } - private void visit(OperationGroup group, List variants) { - var parent = root.child(group.getNamespace()); + private void visit(@Nonnull OperationGroup group, @Nonnull List variants) { + var parent = root.child(group.getNamespace().orElse(null)); var requestShape = visit(parent, group, variants); parent.addOperation(requestShape); - var responseSchema = variants.get(0) - .getResponses() - .flatMap(r -> r.get(HttpStatus.OK)) - .map(OpenApiApiResponse::resolve) - .flatMap(OpenApiApiResponse::getContent) + var responseSchema = variants.stream() + .map(OpenApiOperation::getResponses) + .flatMap(Optional::stream) + .findFirst() + .flatMap(r -> r.get(HttpStatusCode.OK)) + .map(OpenApiResponse::resolve) + .flatMap(OpenApiResponse::getContent) .flatMap(c -> c.get(MimeType.JSON)) .flatMap(OpenApiMediaType::getSchema) .map(OpenApiSchema::resolve) - .orElse(OpenApiSchema.EMPTY); + .orElse(OpenApiSchema.ANONYMOUS_OBJECT); visit(parent, requestShape.getResponseType(), group + ".Response", responseSchema); } - private RequestShape visit(Namespace parent, OperationGroup group, List variants) { + @Nonnull + private RequestShape visit(@Nonnull Namespace parent, @Nonnull OperationGroup group, @Nonnull List variants) { var seenHttpPaths = new HashSet(); HashSet requiredPathParams = null; var allPathParams = new HashMap(); var canonicalPaths = new HashMap, HttpPath>(); var deprecatedPaths = new HashMap, HttpPath>(); - var shape = new RequestShape(parent, group, variants.get(0).getDescription()); + var description = variants.stream().map(OpenApiOperation::getDescription).flatMap(Optional::stream).findFirst().orElse(null); + + var shape = new RequestShape(parent, group, description); for (var variant : variants) { shape.addSupportedHttpMethod(variant.getHttpMethod().name()); var httpPathStr = variant.getHttpPath(); - if (!seenHttpPaths.add(httpPathStr)) continue; + if (!seenHttpPaths.add(httpPathStr)) { + continue; + } - variant.getAllApplicableParameters(OpenApiParameter.In.PATH).forEach(parameter -> { - var paramName = parameter.getName(); + variant.getAllRelevantParameters(In.PATH).forEach(parameter -> { + var paramName = parameter.getName().orElseThrow(); if (!allPathParams.containsKey(paramName)) { allPathParams.put(paramName, visit(parameter)); } @@ -118,10 +149,16 @@ private RequestShape visit(Namespace parent, OperationGroup group, List= p1Size) return -1; - if (i >= p2Size) return 1; + if (i >= p1Size) { + return -1; + } + if (i >= p2Size) { + return 1; + } var cmp = params1.get(i).getName().compareTo(params2.get(i).getName()); - if (cmp != 0) return cmp; + if (cmp != 0) { + return cmp; + } } return 0; @@ -132,19 +169,18 @@ private RequestShape visit(Namespace parent, OperationGroup group, List v.getAllApplicableParameters(OpenApiParameter.In.QUERY)) - .map(this::visit) - .forEachOrdered(shape::addQueryParam); + variants.stream().flatMap(v -> v.getAllRelevantParameters(In.QUERY).stream()).map(this::visit).forEachOrdered(shape::addQueryParam); - var bodySchema = variants.get(0) - .getRequestBody() + var bodySchema = variants.stream() + .map(OpenApiOperation::getRequestBody) + .flatMap(Optional::stream) + .findFirst() .map(OpenApiRequestBody::resolve) .flatMap(OpenApiRequestBody::getContent) .flatMap(c -> c.get(MimeType.JSON)) .flatMap(OpenApiMediaType::getSchema) .map(OpenApiSchema::resolve) - .orElse(OpenApiSchema.EMPTY); + .orElse(OpenApiSchema.ANONYMOUS_OBJECT); visitInto(bodySchema, shape); @@ -157,39 +193,42 @@ private RequestShape visit(Namespace parent, OperationGroup group, List taggedUnion.addVariant(s.resolve().getName(), mapType(s))); + schema.getOneOf().orElseThrow().forEach(s -> taggedUnion.addVariant(s.resolve().getName().orElseThrow(), mapType(s))); shape = taggedUnion; } else { throw new NotImplementedException("Unsupported schema: " + schema); @@ -205,7 +244,13 @@ private void visitInto(OpenApiSchema schema, ObjectShape shape) { schema = allOf.get().get(1); } - schema.getProperties().map(this::visit).forEachOrdered(shape::addBodyField); + final var required = schema.getRequired().orElse(Collections.emptySet()); + schema.getProperties() + .ifPresent( + props -> props.forEach( + (k, v) -> shape.addBodyField(new Field(k, mapType(v), required.contains(k), v.getDescription().orElse(null))) + ) + ); var additionalProperties = schema.getAdditionalProperties(); if (additionalProperties.isPresent()) { @@ -215,7 +260,7 @@ private void visitInto(OpenApiSchema schema, ObjectShape shape) { "metadata", Types.Java.Util.Map(Types.Java.Lang.String, valueType), false, - additionalProperties.get().getDescription() + additionalProperties.get().getDescription().orElse(null) ) ); } @@ -226,11 +271,7 @@ private Type mapType(OpenApiSchema schema) { } private Type mapType(OpenApiSchema schema, boolean boxed) { - Type type = schemaToType.get(schema); - if (type == null) { - type = mapTypeInner(schema); - schemaToType.put(schema, type); - } + var type = schemaToType.computeIfAbsent(schema, this::mapTypeInner); return boxed ? type.getBoxed() : type; } @@ -246,7 +287,7 @@ private Type mapTypeInner(OpenApiSchema schema) { return Type.builder() .pkg(Types.Client.OpenSearch.PACKAGE + "." + schema.getNamespace()) - .name(schema.getName()) + .name(schema.getName().orElseThrow()) .isEnum(schema.hasEnums()) .build(); } @@ -274,8 +315,6 @@ private Type mapTypeInner(OpenApiSchema schema) { case INTEGER: case NUMBER: return mapNumber(schema); - case TIME: - return Types.Client.OpenSearch._Types.Time; } throw new UnsupportedOperationException("Can not get type name for: " + type); @@ -317,7 +356,7 @@ private Type mapArray(OpenApiSchema schema) { } private Type mapNumber(OpenApiSchema schema) { - var format = schema.getFormat().orElse(OpenApiSchema.Format.INT32); + var format = schema.getFormat().orElse(OpenApiSchemaFormat.INT32); switch (format) { case INT32: return Types.Primitive.Int; @@ -335,8 +374,8 @@ private Type mapNumber(OpenApiSchema schema) { private boolean shouldKeepRef(OpenApiSchema schema) { var type = schema.getType(); if (type.isEmpty()) { - if (schema.getOneOf().isPresent()) { - return schema.getOneOf().get().get(0).resolve().getType().map(OpenApiSchema.Type.OBJECT::equals).orElse(false); + if (schema.hasOneOf()) { + return schema.getOneOf().orElseThrow().get(0).resolve().isObject(); } return false; } diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/HttpMethod.java b/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/HttpMethod.java new file mode 100644 index 0000000000..32ce44deac --- /dev/null +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/HttpMethod.java @@ -0,0 +1,29 @@ +package org.opensearch.client.codegen.openapi; + +import io.swagger.v3.oas.models.PathItem; +import java.util.Objects; +import javax.annotation.Nonnull; +import org.opensearch.client.codegen.utils.Strings; + +public enum HttpMethod { + POST, + GET, + PUT, + PATCH, + DELETE, + HEAD, + OPTIONS, + TRACE; + + @Nonnull + public static HttpMethod from(@Nonnull String value) { + Strings.requireNonBlank(value, "value must not be blank"); + return HttpMethod.valueOf(value.toUpperCase()); + } + + @Nonnull + public static HttpMethod from(@Nonnull PathItem.HttpMethod value) { + Objects.requireNonNull(value, "value must not be null"); + return HttpMethod.valueOf(value.name()); + } +} diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/HttpStatus.java b/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/HttpStatus.java deleted file mode 100644 index a0a7c97ad1..0000000000 --- a/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/HttpStatus.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.client.codegen.openapi; - -public enum HttpStatus { - OK("200"); - - private final String code; - - HttpStatus(String code) { - this.code = code; - } - - @Override - public String toString() { - return code; - } -} diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/HttpStatusCode.java b/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/HttpStatusCode.java new file mode 100644 index 0000000000..fecda814ea --- /dev/null +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/HttpStatusCode.java @@ -0,0 +1,44 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.client.codegen.openapi; + +import java.util.Arrays; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; +import org.opensearch.client.codegen.utils.Strings; + +public enum HttpStatusCode { + OK("200"), + BAD_REQUEST("400"), + FORBIDDEN("403"), + INTERNAL_SERVER_ERROR("500"), + NOT_IMPLEMENTED("501"); + + private static final Map STATUS_CODES = Arrays.stream(values()) + .collect(Collectors.toMap(HttpStatusCode::toString, Function.identity())); + private final String code; + + HttpStatusCode(String code) { + this.code = code; + } + + @Override + public String toString() { + return this.code; + } + + public static HttpStatusCode from(String code) { + var value = STATUS_CODES.get(Strings.requireNonBlank(code, "code must not be blank")); + if (value == null) { + throw new IllegalArgumentException("Unknown status code: " + code); + } + return value; + } +} diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/In.java b/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/In.java new file mode 100644 index 0000000000..7e1d516136 --- /dev/null +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/In.java @@ -0,0 +1,23 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.client.codegen.openapi; + +import javax.annotation.Nonnull; +import org.opensearch.client.codegen.utils.Strings; + +public enum In { + QUERY, + PATH; + + @Nonnull + public static In from(@Nonnull String in) { + Strings.requireNonBlank(in, "in must not be blank"); + return In.valueOf(in.toUpperCase()); + } +} diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/JsonPointer.java b/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/JsonPointer.java index 0ab3b3b78b..d20ee4ec71 100644 --- a/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/JsonPointer.java +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/JsonPointer.java @@ -8,94 +8,131 @@ package org.opensearch.client.codegen.openapi; +import java.lang.ref.WeakReference; +import java.util.Arrays; import java.util.Objects; import java.util.Optional; +import java.util.WeakHashMap; +import javax.annotation.Nonnull; import org.apache.commons.lang3.builder.EqualsBuilder; import org.apache.commons.lang3.builder.HashCodeBuilder; public class JsonPointer { - private static final JsonPointer ROOT = new JsonPointer(null, null); + private static final WeakHashMap> PARENT_CACHE = new WeakHashMap<>(); + @Nonnull + public static final JsonPointer ROOT = new JsonPointer(new String[0]); - private final JsonPointer parent; - private final String key; + @Nonnull + private final String[] keys; - public static JsonPointer parse(String pointer) { - if (pointer.isEmpty()) return ROOT; - if (!pointer.startsWith("/")) throw new IllegalArgumentException("Invalid JSON pointer: \"" + pointer + "\""); + @Nonnull + public static JsonPointer parse(@Nonnull String pointer) { + Objects.requireNonNull(pointer, "pointer must not be null"); - var parts = pointer.substring(1).split("/"); + if (pointer.isEmpty()) { + return ROOT; + } - JsonPointer result = ROOT; - for (var part : parts) { - result = result.append(unescape(part)); + if (!pointer.startsWith("/")) { + throw new IllegalArgumentException("Invalid JSON pointer: \"" + pointer + "\""); } - return result; + var parts = pointer.substring(1).split("/"); + + return new JsonPointer(Arrays.stream(parts).map(JsonPointer::unescape).toArray(String[]::new)); } - public static JsonPointer of(String... keys) { - JsonPointer result = ROOT; - for (var key : keys) { - result = result.append(key); - } - return ROOT.append(keys); + @Nonnull + public static JsonPointer of(@Nonnull String... keys) { + return new JsonPointer(keys); } - private JsonPointer(JsonPointer parent, String key) { - if (parent != null) { - Objects.requireNonNull(key, "key must not be null"); + private JsonPointer(@Nonnull String[] keys) { + Objects.requireNonNull(keys, "keys must not be null"); + this.keys = Arrays.copyOf(keys, keys.length); + } + + @Nonnull + public Optional getLastKey() { + if (keys.length == 0) { + return Optional.empty(); } - this.parent = parent; - this.key = key; + return Optional.of(keys[keys.length - 1]); } + @Nonnull public Optional getParent() { - return Optional.ofNullable(parent); - } + if (keys.length == 0) return Optional.empty(); + if (keys.length == 1) return Optional.of(ROOT); + + var parent = Optional.ofNullable(PARENT_CACHE.get(this)).flatMap(ref -> Optional.ofNullable(ref.get())); + + if (parent.isEmpty()) { + var newKeys = new String[this.keys.length - 1]; + System.arraycopy(this.keys, 0, newKeys, 0, newKeys.length); + parent = Optional.of(new JsonPointer(newKeys)); + PARENT_CACHE.put(this, new WeakReference<>(parent.get())); + } - public Optional getKey() { - return Optional.ofNullable(key); + return parent; } - public JsonPointer append(String key) { - Objects.requireNonNull(key, "key must not be null"); - return new JsonPointer(this, key); + @Nonnull + public JsonPointer append(@Nonnull String... keys) { + Objects.requireNonNull(keys, "keys must not be null"); + var newKeys = new String[this.keys.length + keys.length]; + System.arraycopy(this.keys, 0, newKeys, 0, this.keys.length); + System.arraycopy(keys, 0, newKeys, this.keys.length, keys.length); + return new JsonPointer(newKeys); } - public JsonPointer append(String... keys) { - JsonPointer result = this; - for (var key : keys) { - result = result.append(key); - } - return result; + public boolean isDirectChildOf(@Nonnull JsonPointer other) { + Objects.requireNonNull(other, "other must not be null"); + return getParent().map(other::equals).orElse(false); } @Override public String toString() { - return (parent != null ? parent : "") + (key != null ? "/" + escape(key) : ""); + if (keys.length == 0) { + return ""; + } + + var builder = new StringBuilder(); + for (var key : keys) { + builder.append("/").append(escape(key)); + } + return builder.toString(); } @Override public boolean equals(Object o) { - if (this == o) return true; + if (this == o) { + return true; + } - if (o == null || getClass() != o.getClass()) return false; + if (o == null || getClass() != o.getClass()) { + return false; + } JsonPointer that = (JsonPointer) o; - return new EqualsBuilder().append(parent, that.parent).append(key, that.key).isEquals(); + return new EqualsBuilder().append(keys, that.keys).isEquals(); } @Override public int hashCode() { - return new HashCodeBuilder(17, 37).append(parent).append(key).toHashCode(); + return new HashCodeBuilder(17, 37).append(keys).toHashCode(); } - private static String escape(String key) { + @Nonnull + private static String escape(@Nonnull String key) { + Objects.requireNonNull(key, "key must not be null"); return key.replace("~", "~0").replace("/", "~1"); } - private static String unescape(String key) { + @Nonnull + private static String unescape(@Nonnull String key) { + Objects.requireNonNull(key, "key must not be null"); return key.replace("~1", "/").replace("~0", "~"); } } diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/MimeType.java b/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/MimeType.java index aabe210c6d..3d587b4ab4 100644 --- a/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/MimeType.java +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/MimeType.java @@ -8,9 +8,19 @@ package org.opensearch.client.codegen.openapi; +import java.util.Arrays; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; +import javax.annotation.Nonnull; +import org.opensearch.client.codegen.utils.Strings; + public enum MimeType { - JSON("application/json"); + JSON("application/json"), + NDJSON("application/x-ndjson"); + private static final Map MIME_TYPES = Arrays.stream(MimeType.values()) + .collect(Collectors.toMap(MimeType::toString, Function.identity())); private final String mimeType; MimeType(String mimeType) { @@ -19,6 +29,15 @@ public enum MimeType { @Override public String toString() { - return mimeType; + return this.mimeType; + } + + @Nonnull + public static MimeType from(@Nonnull String mimeType) { + var value = MIME_TYPES.get(Strings.requireNonBlank(mimeType, "mimeType must not be blank")); + if (value == null) { + throw new IllegalArgumentException("Unknown mime type: " + mimeType); + } + return value; } } diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiApiResponse.java b/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiApiResponse.java deleted file mode 100644 index 13162cdfee..0000000000 --- a/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiApiResponse.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.client.codegen.openapi; - -import io.swagger.v3.oas.models.responses.ApiResponse; -import java.util.Optional; - -public class OpenApiApiResponse extends OpenApiRefObject { - protected OpenApiApiResponse(OpenApiSpec parent, JsonPointer jsonPtr, ApiResponse inner) { - super(parent, jsonPtr, inner, OpenApiApiResponse::new, api -> api.getComponents().getResponses(), ApiResponse::get$ref); - } - - public Optional getContent() { - return childOpt("content", ApiResponse::getContent, OpenApiContent::new); - } - - @Override - protected OpenApiApiResponse getSelf() { - return this; - } -} diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiApiResponses.java b/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiApiResponses.java deleted file mode 100644 index 3f7e24d7fd..0000000000 --- a/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiApiResponses.java +++ /dev/null @@ -1,18 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.client.codegen.openapi; - -import io.swagger.v3.oas.models.responses.ApiResponse; -import io.swagger.v3.oas.models.responses.ApiResponses; - -public class OpenApiApiResponses extends OpenApiMapObject { - protected OpenApiApiResponses(OpenApiSpec parent, JsonPointer jsonPtr, ApiResponses inner) { - super(parent, jsonPtr, inner, OpenApiApiResponse::new); - } -} diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiComponents.java b/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiComponents.java new file mode 100644 index 0000000000..606f3b7505 --- /dev/null +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiComponents.java @@ -0,0 +1,35 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.client.codegen.openapi; + +import io.swagger.v3.oas.models.Components; +import java.util.Map; +import java.util.Objects; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +public class OpenApiComponents extends OpenApiElement { + @Nullable + private final Map schemas; + @Nullable + private final Map parameters; + @Nullable + private final Map responses; + @Nullable + private final Map requestBodies; + + protected OpenApiComponents(@Nonnull OpenApiSpecification parent, @Nonnull JsonPointer pointer, @Nonnull Components components) { + super(parent, pointer); + Objects.requireNonNull(components, "components must not be null"); + this.schemas = children("schemas", components.getSchemas(), OpenApiSchema::new); + this.parameters = children("parameters", components.getParameters(), OpenApiParameter::new); + this.responses = children("responses", components.getResponses(), OpenApiResponse::new); + this.requestBodies = children("requestBodies", components.getRequestBodies(), OpenApiRequestBody::new); + } +} diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiContent.java b/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiContent.java index 5aba7ea4c8..89cabc2eee 100644 --- a/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiContent.java +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiContent.java @@ -9,10 +9,10 @@ package org.opensearch.client.codegen.openapi; import io.swagger.v3.oas.models.media.Content; -import io.swagger.v3.oas.models.media.MediaType; +import javax.annotation.Nonnull; -public class OpenApiContent extends OpenApiMapObject { - protected OpenApiContent(OpenApiSpec parent, JsonPointer jsonPtr, Content inner) { - super(parent, jsonPtr, inner, OpenApiMediaType::new); +public class OpenApiContent extends OpenApiMapElement { + protected OpenApiContent(@Nonnull OpenApiElement parent, @Nonnull JsonPointer pointer, @Nonnull Content content) { + super(parent, pointer, content, MimeType::from, OpenApiMediaType::new); } } diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiElement.java b/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiElement.java new file mode 100644 index 0000000000..c11b6dbf39 --- /dev/null +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiElement.java @@ -0,0 +1,130 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.client.codegen.openapi; + +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Function; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.opensearch.client.codegen.utils.Lists; +import org.opensearch.client.codegen.utils.Maps; +import org.opensearch.client.codegen.utils.Strings; + +public abstract class OpenApiElement> { + @Nullable + private final OpenApiElement parent; + @Nonnull + private final JsonPointer pointer; + + OpenApiElement(@Nullable OpenApiElement parent, @Nonnull JsonPointer pointer) { + this.parent = parent; + this.pointer = Objects.requireNonNull(pointer, "pointer must not be null"); + if (parent != null) { + this.getSpecification().ifPresent(s -> s.addElement(this.pointer, self())); + } + } + + @Nonnull + @SuppressWarnings("unchecked") + protected TSelf self() { + return (TSelf) this; + } + + @Nonnull + protected Optional getSpecification() { + return getParent().flatMap(OpenApiElement::getSpecification); + } + + @Nonnull + protected Optional> getParent() { + return Optional.ofNullable(parent); + } + + @Nonnull + public JsonPointer getPointer() { + return pointer; + } + + @Nonnull + private JsonPointer childPtr(@Nonnull String key) { + return pointer.append(Strings.requireNonBlank(key, "key must not be blank")); + } + + @Nullable + TOut child(@Nonnull String key, @Nullable TIn child, @Nonnull Factory factory) { + if (child == null) { + return null; + } + return Objects.requireNonNull(factory, "factory must not be null").create(self(), childPtr(key), child); + } + + @Nullable + List children(@Nonnull String key, @Nullable List children, @Nonnull Factory factory) { + if (children == null) { + return null; + } + Objects.requireNonNull(factory, "factory must not be null"); + var basePtr = childPtr(key); + var self = self(); + return Lists.transform(children, (i, v) -> factory.create(self, basePtr.append(String.valueOf(i)), v)); + } + + @Nullable + Map children( + @Nullable Map children, + @Nonnull Function keyMapper, + @Nonnull Factory valueFactory + ) { + return children(pointer, children, keyMapper, valueFactory); + } + + @Nullable + Map children( + @Nonnull String key, + @Nullable Map children, + @Nonnull Factory valueFactory + ) { + return children(childPtr(key), children, Function.identity(), valueFactory); + } + + @Nullable + Map children( + @Nonnull JsonPointer basePtr, + @Nullable Map children, + @Nonnull Function keyMapper, + @Nonnull Factory valueFactory + ) { + if (children == null) { + return null; + } + Objects.requireNonNull(basePtr, "basePtr must not be null"); + Objects.requireNonNull(keyMapper, "keyMapper must not be null"); + Objects.requireNonNull(valueFactory, "valueFactory must not be null"); + var self = self(); + return Maps.transform( + children, + (k, v) -> keyMapper.apply(k), + (k, v) -> valueFactory.create(self, basePtr.append(keyMapper.apply(k).toString()), v) + ); + } + + interface Factory, TIn, TOut> { + @Nonnull + TOut create(@Nonnull TParent parent, @Nonnull JsonPointer pointer, @Nonnull TIn value); + } + + @Override + public String toString() { + return new ToStringBuilder(this).append("pointer", pointer).toString(); + } +} diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiExtensions.java b/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiExtensions.java deleted file mode 100644 index 8c49df85dc..0000000000 --- a/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiExtensions.java +++ /dev/null @@ -1,18 +0,0 @@ -package org.opensearch.client.codegen.openapi; - -import java.util.Map; -import java.util.Optional; - -public interface OpenApiExtensions { - Map getExtensions(); - - default Optional getExtension(String name) { - Map extensions = getExtensions(); - if (extensions == null) return Optional.empty(); - return Optional.ofNullable(extensions.get(name)); - } - - default Optional getExtensionAsString(String name) { - return getExtension(name).map(String.class::cast); - } -} diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiMapElement.java b/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiMapElement.java new file mode 100644 index 0000000000..f02f5b27ad --- /dev/null +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiMapElement.java @@ -0,0 +1,43 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.client.codegen.openapi; + +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Function; +import javax.annotation.Nonnull; + +public abstract class OpenApiMapElement, TKey, TValue extends OpenApiElement> + extends OpenApiElement { + @Nonnull + private final Map value; + + OpenApiMapElement( + @Nonnull OpenApiElement parent, + @Nonnull JsonPointer pointer, + @Nonnull Map value, + @Nonnull Function keyMapper, + @Nonnull Factory factory + ) { + super(parent, pointer); + this.value = Objects.requireNonNull( + children( + Objects.requireNonNull(value, "value must not be null"), + Objects.requireNonNull(keyMapper, "keyMapper must not be null"), + Objects.requireNonNull(factory, "factory must not be null") + ) + ); + } + + @Nonnull + public Optional get(@Nonnull TKey key) { + return Optional.ofNullable(value.get(Objects.requireNonNull(key, "key must not be null"))); + } +} diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiMapObject.java b/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiMapObject.java deleted file mode 100644 index d44b53483c..0000000000 --- a/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiMapObject.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.client.codegen.openapi; - -import java.util.Map; -import java.util.Objects; -import java.util.Optional; - -public abstract class OpenApiMapObject< - TKey extends Enum, - TValue extends OpenApiObject, - TInner extends Map, - TValueInner> extends OpenApiObject { - private final Factory factory; - - protected OpenApiMapObject(OpenApiSpec parent, JsonPointer jsonPtr, TInner inner, Factory factory) { - super(parent, jsonPtr, inner); - Objects.requireNonNull(factory); - this.factory = factory; - } - - public Optional get(TKey key) { - var keyStr = key.toString(); - return childOpt(keyStr, i -> i.get(keyStr), factory); - } -} diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiMediaType.java b/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiMediaType.java index b80894da5e..68ae95489f 100644 --- a/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiMediaType.java +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiMediaType.java @@ -9,14 +9,23 @@ package org.opensearch.client.codegen.openapi; import io.swagger.v3.oas.models.media.MediaType; +import java.util.Objects; import java.util.Optional; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; -public class OpenApiMediaType extends OpenApiObject { - protected OpenApiMediaType(OpenApiSpec parent, JsonPointer jsonPtr, MediaType inner) { - super(parent, jsonPtr, inner); +public class OpenApiMediaType extends OpenApiElement { + @Nullable + private final OpenApiSchema schema; + + protected OpenApiMediaType(@Nonnull OpenApiContent parent, @Nonnull JsonPointer pointer, @Nonnull MediaType mediaType) { + super(parent, pointer); + Objects.requireNonNull(mediaType, "mediaType must not be null"); + this.schema = child("schema", mediaType.getSchema(), OpenApiSchema::new); } + @Nonnull public Optional getSchema() { - return childOpt("schema", MediaType::getSchema, OpenApiSchema::new); + return Optional.ofNullable(schema); } } diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiObject.java b/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiObject.java deleted file mode 100644 index d7e2b88202..0000000000 --- a/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiObject.java +++ /dev/null @@ -1,121 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.client.codegen.openapi; - -import java.util.List; -import java.util.Objects; -import java.util.Optional; -import java.util.function.Function; -import java.util.stream.IntStream; -import java.util.stream.Stream; -import org.apache.commons.lang3.builder.EqualsBuilder; -import org.apache.commons.lang3.builder.HashCodeBuilder; -import org.apache.commons.lang3.builder.ToStringBuilder; - -public abstract class OpenApiObject { - private final OpenApiSpec parent; - private final JsonPointer jsonPtr; - private final TInner inner; - - protected OpenApiObject(OpenApiSpec parent, JsonPointer jsonPtr, TInner inner) { - Objects.requireNonNull(inner, "inner cannot be null"); - this.parent = parent; - this.jsonPtr = jsonPtr; - this.inner = inner; - } - - public OpenApiSpec getParent() { - return parent; - } - - public JsonPointer getJsonPtr() { - return jsonPtr; - } - - public String getLocation() { - return parent.getLocation().toString() + "#" + jsonPtr; - } - - public TInner getInner() { - return inner; - } - - protected JsonPointer childPtr(String... keys) { - return getJsonPtr().append(keys); - } - - private > Optional childOpt( - String key, - TChildInner child, - Factory factory - ) { - return Optional.ofNullable(child).map(c -> factory.create(getParent(), getJsonPtr().append(key), c)); - } - - protected > Optional childOpt( - String key, - Function getter, - Factory factory - ) { - return childOpt(key, getter.apply(getInner()), factory); - } - - protected > Stream children( - String key, - List children, - Factory factory - ) { - if (children == null || children.isEmpty()) return Stream.empty(); - var basePtr = getJsonPtr().append(key); - return IntStream.range(0, children.size()) - .mapToObj(i -> factory.create(getParent(), basePtr.append(String.valueOf(i)), children.get(i))); - } - - protected > Stream children( - String key, - Function> getter, - Factory factory - ) { - return children(key, getter.apply(getInner()), factory); - } - - protected > Optional> childrenOpt( - String key, - Function> getter, - Factory factory - ) { - return Optional.ofNullable(getter.apply(getInner())).map(l -> children(key, l, factory).toList()); - } - - @Override - public String toString() { - return new ToStringBuilder(this).append("parent", parent).append("jsonPtr", jsonPtr).append("inner", inner).toString(); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - - if (o == null || getClass() != o.getClass()) return false; - - OpenApiObject that = (OpenApiObject) o; - - return new EqualsBuilder().append(parent, that.parent).append(jsonPtr, that.jsonPtr).isEquals(); - } - - @Override - public int hashCode() { - return new HashCodeBuilder(17, 37).append(parent).append(jsonPtr).toHashCode(); - } - - @FunctionalInterface - public interface Factory> { - T create(OpenApiSpec parent, JsonPointer jsonPtr, TInner inner); - } -} diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiOperation.java b/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiOperation.java index 6c4576fa87..34b2f4be9b 100644 --- a/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiOperation.java +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiOperation.java @@ -8,71 +8,112 @@ package org.opensearch.client.codegen.openapi; +import static org.opensearch.client.codegen.utils.Functional.ifNonnull; + import io.swagger.v3.oas.models.Operation; -import io.swagger.v3.oas.models.PathItem; -import java.util.Map; +import java.util.List; +import java.util.Objects; import java.util.Optional; +import java.util.stream.Collectors; import java.util.stream.Stream; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; import org.opensearch.client.codegen.model.Deprecation; import org.opensearch.client.codegen.model.OperationGroup; -public class OpenApiOperation extends OpenApiObject implements OpenApiExtensions { - private final OpenApiPath path; - private final PathItem.HttpMethod httpMethod; +public class OpenApiOperation extends OpenApiElement { + @Nonnull + private final OpenApiPath parentPath; + @Nonnull + private final HttpMethod httpMethod; + @Nonnull + private final String id; + @Nullable + private final List parameters; + @Nonnull + private final OperationGroup operationGroup; + @Nullable + private final Boolean isDeprecated; + @Nullable + private final String description; + @Nullable + private final OpenApiRequestBody requestBody; + @Nullable + private final OpenApiResponses responses; + @Nullable + private final String versionAdded; + @Nullable + private final String versionDeprecated; + @Nullable + private final String deprecationMessage; - protected OpenApiOperation(OpenApiPath path, PathItem.HttpMethod httpMethod, Operation operation) { - super(path.getParent(), path.childPtr(httpMethod.toString().toLowerCase()), operation); - this.path = path; - this.httpMethod = httpMethod; + protected OpenApiOperation(@Nonnull OpenApiPath parent, @Nonnull JsonPointer pointer, @Nonnull Operation operation) { + super(parent, pointer); + this.parentPath = Objects.requireNonNull(parent, "parent must not be null"); + this.httpMethod = HttpMethod.from(pointer.getLastKey().orElseThrow()); + Objects.requireNonNull(operation, "operation must not be null"); + this.id = Objects.requireNonNull(operation.getOperationId()); + this.parameters = children("parameters", operation.getParameters(), OpenApiParameter::new); + var extensions = Objects.requireNonNull(operation.getExtensions(), "operation must have extensions defined"); + this.operationGroup = OperationGroup.from((String) extensions.get("x-operation-group")); + this.isDeprecated = operation.getDeprecated(); + this.description = operation.getDescription(); + this.requestBody = child("requestBody", operation.getRequestBody(), OpenApiRequestBody::new); + this.responses = child("responses", operation.getResponses(), OpenApiResponses::new); + this.versionAdded = ifNonnull(extensions.get("x-version-added"), String::valueOf); + this.versionDeprecated = ifNonnull(extensions.get("x-version-deprecated"), String::valueOf); + this.deprecationMessage = ifNonnull(extensions.get("x-deprecation-message"), String::valueOf); } + @Nonnull public String getHttpPath() { - return path.getHttpPath(); + return parentPath.getHttpPath(); } - public PathItem.HttpMethod getHttpMethod() { + @Nonnull + public HttpMethod getHttpMethod() { return httpMethod; } - public String getDescription() { - return getInner().getDescription(); - } - - public Stream getParameters() { - return children("parameters", Operation::getParameters, OpenApiParameter::new); + @Nonnull + public OperationGroup getOperationGroup() { + return operationGroup; } - public Stream getAllApplicableParameters(OpenApiParameter.In in) { - return Stream.concat(path.getParameters(), getParameters()).map(OpenApiParameter::resolve).filter(p -> in.equals(p.getIn())); + @Nonnull + public Optional getDescription() { + return Optional.ofNullable(description); } + @Nonnull public Optional getRequestBody() { - return Optional.ofNullable(getInner().getRequestBody()) - .map(body -> new OpenApiRequestBody(getParent(), childPtr("requestBody"), body)); - } - - public Optional getResponses() { - return Optional.ofNullable(getInner().getResponses()) - .map(responses -> new OpenApiApiResponses(getParent(), childPtr("responses"), responses)); + return Optional.ofNullable(requestBody); } - @Override - public Map getExtensions() { - return getInner().getExtensions(); + @Nonnull + public Optional getResponses() { + return Optional.ofNullable(responses); } - public OperationGroup getXOperationGroup() { - return getExtensionAsString("x-operation-group").map(OperationGroup::from).orElseThrow(); + @Nonnull + public List getAllRelevantParameters(@Nonnull In in) { + Objects.requireNonNull(in, "in must not be null"); + return Stream.of(parentPath.getParameters(), Optional.ofNullable(parameters)) + .flatMap(Optional::stream) + .flatMap(List::stream) + .map(OpenApiRefElement::resolve) + .filter(p -> p.getIn().equals(Optional.of(in))) + .collect(Collectors.toList()); } - public Optional getXVersionAdded() { - return getExtensionAsString("x-version-added"); + @Nonnull + public Optional getVersionAdded() { + return Optional.ofNullable(versionAdded); } - public Deprecation getDeprecation() { - var msg = getExtensionAsString("x-deprecation-message"); - var version = getExtensionAsString("x-version-deprecated"); - if (msg.isEmpty() && version.isEmpty()) return null; - return new Deprecation(msg.orElse(null), version.orElse(null)); + @Nonnull + public Optional getDeprecation() { + if (versionDeprecated == null && deprecationMessage == null) return Optional.empty(); + return Optional.of(new Deprecation(deprecationMessage, versionDeprecated)); } } diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiOperationBodyElement.java b/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiOperationBodyElement.java new file mode 100644 index 0000000000..a118b6a572 --- /dev/null +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiOperationBodyElement.java @@ -0,0 +1,27 @@ +package org.opensearch.client.codegen.openapi; + +import io.swagger.v3.oas.models.media.Content; +import java.util.Optional; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +public abstract class OpenApiOperationBodyElement> extends OpenApiRefElement { + @Nullable + private final OpenApiContent content; + + protected OpenApiOperationBodyElement( + @Nonnull OpenApiElement parent, + @Nonnull JsonPointer pointer, + @Nullable String $ref, + @Nullable Content content, + @Nonnull Class clazz + ) { + super(parent, pointer, $ref, clazz); + this.content = child("content", content, OpenApiContent::new); + } + + @Nonnull + public Optional getContent() { + return Optional.ofNullable(content); + } +} diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiParameter.java b/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiParameter.java index e82a055ff2..0d3def2406 100644 --- a/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiParameter.java +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiParameter.java @@ -8,45 +8,55 @@ package org.opensearch.client.codegen.openapi; +import static org.opensearch.client.codegen.utils.Functional.ifNonnull; + import io.swagger.v3.oas.models.parameters.Parameter; import java.util.Optional; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +public class OpenApiParameter extends OpenApiRefElement { + @Nullable + private final String name; + @Nullable + private final String description; + @Nullable + private final In in; + @Nullable + private final Boolean isRequired; + @Nullable + private final OpenApiSchema schema; -public class OpenApiParameter extends OpenApiRefObject { - protected OpenApiParameter(OpenApiSpec parent, JsonPointer jsonPtr, Parameter parameter) { - super(parent, jsonPtr, parameter, OpenApiParameter::new, api -> api.getComponents().getParameters(), Parameter::get$ref); + protected OpenApiParameter(@Nullable OpenApiElement parent, @Nonnull JsonPointer pointer, @Nonnull Parameter parameter) { + super(parent, pointer, parameter.get$ref(), OpenApiParameter.class); + this.name = parameter.getName(); + this.description = parameter.getDescription(); + this.in = ifNonnull(parameter.getIn(), In::from); + this.isRequired = parameter.getRequired(); + this.schema = child("schema", parameter.getSchema(), OpenApiSchema::new); } - public String getName() { - return getInner().getName(); + @Nonnull + public Optional getName() { + return Optional.ofNullable(name); } - public String getDescription() { - return getInner().getDescription(); + @Nonnull + public Optional getDescription() { + return Optional.ofNullable(description); } - public In getIn() { - return In.from(getInner().getIn()); + @Nonnull + public Optional getIn() { + return Optional.ofNullable(in); } public boolean getRequired() { - return getInner().getRequired(); + return isRequired != null && isRequired; } + @Nonnull public Optional getSchema() { - return childOpt("schema", Parameter::getSchema, OpenApiSchema::new); - } - - @Override - protected OpenApiParameter getSelf() { - return this; - } - - public enum In { - QUERY, - PATH; - - public static In from(String s) { - return valueOf(s.toUpperCase()); - } + return Optional.ofNullable(schema); } } diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiPath.java b/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiPath.java index a80355c155..b8957df568 100644 --- a/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiPath.java +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiPath.java @@ -8,32 +8,42 @@ package org.opensearch.client.codegen.openapi; -import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.PathItem; -import java.util.stream.Stream; - -public class OpenApiPath extends OpenApiRefObject { +import java.util.List; +import java.util.Map; +import java.util.Optional; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import org.opensearch.client.codegen.utils.Lists; +import org.opensearch.client.codegen.utils.Maps; + +public class OpenApiPath extends OpenApiRefElement { + @Nonnull private final String httpPath; - - protected OpenApiPath(OpenApiSpec parent, JsonPointer jsonPtr, String httpPath, PathItem pathItem) { - super(parent, jsonPtr, pathItem, (api, jPath, p) -> new OpenApiPath(api, jPath, httpPath, p), OpenAPI::getPaths, PathItem::get$ref); - this.httpPath = httpPath; - } - - public Stream getOperations() { - return getInner().readOperationsMap().entrySet().stream().map(e -> new OpenApiOperation(this, e.getKey(), e.getValue())); - } - - public Stream getParameters() { - return children("parameters", PathItem::getParameters, OpenApiParameter::new); + @Nullable + private final Map operations; + @Nullable + private final List parameters; + + protected OpenApiPath(@Nonnull OpenApiSpecification parent, @Nonnull JsonPointer pointer, @Nonnull PathItem pathItem) { + super(parent, pointer, pathItem.get$ref(), OpenApiPath.class); + this.httpPath = pointer.getLastKey().orElseThrow(); + this.operations = children(pathItem.readOperationsMap(), HttpMethod::from, OpenApiOperation::new); + this.parameters = children("parameters", pathItem.getParameters(), OpenApiParameter::new); } + @Nonnull public String getHttpPath() { return httpPath; } - @Override - protected OpenApiPath getSelf() { - return this; + @Nonnull + public Optional> getOperations() { + return Maps.unmodifiableOpt(operations); + } + + @Nonnull + public Optional> getParameters() { + return Lists.unmodifiableOpt(parameters); } } diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiProperty.java b/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiProperty.java deleted file mode 100644 index 33d87cf4c8..0000000000 --- a/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiProperty.java +++ /dev/null @@ -1,28 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.client.codegen.openapi; - -import io.swagger.v3.oas.models.media.Schema; - -public class OpenApiProperty extends OpenApiSchema { - private final boolean required; - - protected OpenApiProperty(OpenApiSpec parent, JsonPointer jsonPtr, Schema schema, boolean required) { - super(parent, jsonPtr, schema); - this.required = required; - } - - public String getName() { - return getJsonPtr().getKey().orElseThrow(); - } - - public boolean isRequired() { - return required; - } -} diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiRefElement.java b/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiRefElement.java new file mode 100644 index 0000000000..4023c9a910 --- /dev/null +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiRefElement.java @@ -0,0 +1,139 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.client.codegen.openapi; + +import static org.opensearch.client.codegen.utils.Functional.ifNonnull; + +import java.util.Objects; +import java.util.Optional; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.opensearch.client.codegen.utils.Strings; + +public abstract class OpenApiRefElement> extends OpenApiElement { + @Nullable + private final RelativeRef $ref; + @Nonnull + private final Class clazz; + + OpenApiRefElement( + @Nullable OpenApiElement parent, + @Nonnull JsonPointer pointer, + @Nullable String $ref, + @Nonnull Class clazz + ) { + super(parent, pointer); + this.$ref = ifNonnull($ref, RelativeRef::parse); + this.clazz = Objects.requireNonNull(clazz, "clazz must not be null"); + } + + public boolean has$ref() { + return $ref != null; + } + + @Nonnull + public Optional get$ref() { + return Optional.ofNullable($ref); + } + + @Nonnull + public TSelf resolve() { + if ($ref == null) { + return self(); + } + return getSpecification().map(s -> $ref.resolveIn(s, clazz)) + .orElseThrow(() -> new UnsupportedOperationException("Cannot resolve $ref in anonymous component")); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + + if (o == null || getClass() != o.getClass()) { + return false; + } + + OpenApiRefElement that = (OpenApiRefElement) o; + + return new EqualsBuilder().append($ref, that.$ref).append(clazz, that.clazz).isEquals(); + } + + @Override + public int hashCode() { + return new HashCodeBuilder(17, 37).append($ref).append(clazz).toHashCode(); + } + + @Override + public String toString() { + return new ToStringBuilder(this).append("$ref", $ref).append("clazz", clazz).toString(); + } + + public static class RelativeRef { + @Nonnull + public static RelativeRef parse(@Nonnull String $ref) { + Objects.requireNonNull($ref, "$ref must not be null"); + var fragmentIdx = $ref.indexOf('#'); + if (fragmentIdx < 0) throw new IllegalArgumentException("$ref must be a valid relative reference"); + + var relativeLocation = $ref.substring(0, fragmentIdx); + var pointer = JsonPointer.parse($ref.substring(fragmentIdx + 1)); + return new RelativeRef(relativeLocation, pointer); + } + + @Nullable + private final String relativeLocation; + @Nonnull + private final JsonPointer pointer; + + private RelativeRef(@Nullable String relativeLocation, @Nonnull JsonPointer pointer) { + this.relativeLocation = relativeLocation; + this.pointer = Objects.requireNonNull(pointer, "pointer must not be null"); + } + + public > TElement resolveIn(OpenApiSpecification specification, Class clazz) { + Objects.requireNonNull(specification, "specification must not be null"); + + if (!Strings.isNullOrEmpty(relativeLocation)) { + specification = OpenApiSpecification.retrieve(specification.getLocation().resolve(relativeLocation)); + } + + return specification.getElement(pointer, clazz); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + + if (o == null || getClass() != o.getClass()) { + return false; + } + + RelativeRef that = (RelativeRef) o; + + return new EqualsBuilder().append(relativeLocation, that.relativeLocation).append(pointer, that.pointer).isEquals(); + } + + @Override + public int hashCode() { + return new HashCodeBuilder(17, 37).append(relativeLocation).append(pointer).toHashCode(); + } + + @Override + public String toString() { + return new ToStringBuilder(this).append("pointer", pointer).append("relativeLocation", relativeLocation).toString(); + } + } +} diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiRefObject.java b/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiRefObject.java deleted file mode 100644 index ceeee7684e..0000000000 --- a/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiRefObject.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.client.codegen.openapi; - -import io.swagger.v3.oas.models.OpenAPI; -import java.util.Map; -import java.util.Optional; -import java.util.function.Function; - -public abstract class OpenApiRefObject, TInner> extends OpenApiObject { - private final Factory factory; - private final Function> componentsGetter; - private final Function get$ref; - - protected OpenApiRefObject( - OpenApiSpec parent, - JsonPointer jsonPtr, - TInner inner, - Factory factory, - Function> componentsGetter, - Function get$ref - ) { - super(parent, jsonPtr, inner); - this.factory = factory; - this.componentsGetter = componentsGetter; - this.get$ref = get$ref; - } - - public Optional get$ref() { - return Optional.ofNullable(get$ref.apply(getInner())); - } - - public boolean has$ref() { - return get$ref().isPresent(); - } - - protected abstract TSelf getSelf(); - - public TSelf resolve() { - if (!has$ref()) return getSelf(); - var resolved = getParent().resolve(componentsGetter, getJsonPtr(), getInner(), get$ref); - return factory.create(resolved.getLeft(), resolved.getMiddle(), resolved.getRight()); - } -} diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiRequestBody.java b/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiRequestBody.java index 256c7531b8..e9f5b649d6 100644 --- a/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiRequestBody.java +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiRequestBody.java @@ -9,19 +9,10 @@ package org.opensearch.client.codegen.openapi; import io.swagger.v3.oas.models.parameters.RequestBody; -import java.util.Optional; +import javax.annotation.Nonnull; -public class OpenApiRequestBody extends OpenApiRefObject { - protected OpenApiRequestBody(OpenApiSpec parent, JsonPointer jsonPtr, RequestBody inner) { - super(parent, jsonPtr, inner, OpenApiRequestBody::new, api -> api.getComponents().getRequestBodies(), RequestBody::get$ref); - } - - public Optional getContent() { - return childOpt("content", RequestBody::getContent, OpenApiContent::new); - } - - @Override - protected OpenApiRequestBody getSelf() { - return this; +public class OpenApiRequestBody extends OpenApiOperationBodyElement { + protected OpenApiRequestBody(@Nonnull OpenApiElement parent, @Nonnull JsonPointer pointer, @Nonnull RequestBody requestBody) { + super(parent, pointer, requestBody.get$ref(), requestBody.getContent(), OpenApiRequestBody.class); } } diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiResponse.java b/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiResponse.java new file mode 100644 index 0000000000..bab510a295 --- /dev/null +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiResponse.java @@ -0,0 +1,18 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.client.codegen.openapi; + +import io.swagger.v3.oas.models.responses.ApiResponse; +import javax.annotation.Nonnull; + +public class OpenApiResponse extends OpenApiOperationBodyElement { + protected OpenApiResponse(@Nonnull OpenApiElement parent, @Nonnull JsonPointer pointer, @Nonnull ApiResponse response) { + super(parent, pointer, response.get$ref(), response.getContent(), OpenApiResponse.class); + } +} diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiResponses.java b/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiResponses.java new file mode 100644 index 0000000000..732200ad77 --- /dev/null +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiResponses.java @@ -0,0 +1,18 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.client.codegen.openapi; + +import io.swagger.v3.oas.models.responses.ApiResponses; +import javax.annotation.Nonnull; + +public class OpenApiResponses extends OpenApiMapElement { + protected OpenApiResponses(@Nonnull OpenApiOperation parent, @Nonnull JsonPointer pointer, @Nonnull ApiResponses responses) { + super(parent, pointer, responses, HttpStatusCode::from, OpenApiResponse::new); + } +} diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiSchema.java b/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiSchema.java index 6c7c915455..c9cbeb14cb 100644 --- a/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiSchema.java +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiSchema.java @@ -8,155 +8,192 @@ package org.opensearch.client.codegen.openapi; +import static org.opensearch.client.codegen.utils.Functional.ifNonnull; + +import io.swagger.v3.oas.models.media.ObjectSchema; import io.swagger.v3.oas.models.media.Schema; -import java.nio.file.Path; import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.Set; -import java.util.stream.Stream; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import org.opensearch.client.codegen.utils.Lists; +import org.opensearch.client.codegen.utils.Maps; +import org.opensearch.client.codegen.utils.Sets; + +public class OpenApiSchema extends OpenApiRefElement { + private static final JsonPointer ANONYMOUS = JsonPointer.of(""); + + public static final OpenApiSchema ANONYMOUS_OBJECT = new OpenApiSchema(null, ANONYMOUS.append("object"), new ObjectSchema()); + + @Nullable + private final String name; + @Nullable + private final String namespace; + @Nullable + private final String description; + @Nullable + private final OpenApiSchemaType type; + @Nullable + private final OpenApiSchemaFormat format; + @Nullable + private final List allOf; + @Nullable + private final List oneOf; + @Nullable + private final List enums; + @Nullable + private final OpenApiSchema items; + @Nullable + private final OpenApiSchema additionalProperties; + @Nullable + private final Map properties; + @Nullable + private final Set required; + + protected OpenApiSchema(@Nullable OpenApiElement parent, @Nonnull JsonPointer pointer, @Nonnull Schema schema) { + super(parent, pointer, schema.get$ref(), OpenApiSchema.class); + + if (pointer.isDirectChildOf(JsonPointer.of("components", "schemas"))) { + var key = pointer.getLastKey().orElseThrow(); + var colonIdx = key.indexOf(':'); + if (colonIdx >= 0) { + namespace = key.substring(0, colonIdx); + name = key.substring(colonIdx + 1); + } else { + namespace = null; + name = key; + } + } else { + name = null; + namespace = null; + } -@SuppressWarnings({ "rawtypes", "unchecked" }) -public class OpenApiSchema extends OpenApiRefObject implements OpenApiExtensions { - public static final OpenApiSchema EMPTY = new OpenApiSchema(null, null, new Schema<>()); + description = schema.getDescription(); - protected OpenApiSchema(OpenApiSpec parent, JsonPointer jsonPtr, Schema schema) { - super(parent, jsonPtr, schema, OpenApiSchema::new, api -> api.getComponents().getSchemas(), Schema::get$ref); - } + type = Optional.ofNullable(schema.getTypes()).flatMap(types -> { + switch (types.size()) { + case 0: + return Optional.empty(); + case 1: + return Optional.of(types.iterator().next()); + default: + throw new IllegalArgumentException("Multiple types not yet supported: " + types); + } + }).or(() -> Optional.ofNullable(schema.getType())).map(OpenApiSchemaType::from).orElse(null); - public String getDescription() { - return getInner().getDescription(); - } + format = ifNonnull(schema.getFormat(), OpenApiSchemaFormat::from); - public Optional getType() { - return getXDataType().or(() -> Optional.ofNullable(getInner().getType())).or(() -> { - var types = (Set) getInner().getTypes(); - if (types == null || types.isEmpty()) return Optional.empty(); - if (types.size() > 1) { - throw new IllegalStateException("Multiple types are not yet supported"); - } - return Optional.of(types.toArray(new String[0])[0]); - }).map(Type::from); - } + allOf = children("allOf", schema.getAllOf(), OpenApiSchema::new); + oneOf = children("oneOf", schema.getOneOf(), OpenApiSchema::new); - public boolean is(Type type) { - return getType().map(type::equals).orElse(false); - } + enums = ifNonnull(schema.getEnum(), e -> Lists.transform(e, String::valueOf)); - public boolean isString() { - return is(Type.STRING); - } + items = child("items", schema.getItems(), OpenApiSchema::new); - public boolean isNumber() { - return is(Type.NUMBER); - } + additionalProperties = child("additionalProperties", (Schema) schema.getAdditionalProperties(), OpenApiSchema::new); - public boolean isBoolean() { - return is(Type.BOOLEAN); + properties = children("properties", schema.getProperties(), OpenApiSchema::new); + required = ifNonnull(schema.getRequired(), HashSet::new); } - public boolean isArray() { - return is(Type.ARRAY); + @Nonnull + public Optional getName() { + return Optional.ofNullable(name); } - public boolean isObject() { - return is(Type.OBJECT); + @Nonnull + public Optional getNamespace() { + return Optional.ofNullable(namespace); } - public Optional getFormat() { - return Optional.ofNullable(getInner().getFormat()).map(Format::from); + @Nonnull + public Optional getDescription() { + return Optional.ofNullable(description); } - public Optional> getEnum() { - return Optional.ofNullable((List) getInner().getEnum()); + @Nonnull + public Optional getType() { + return Optional.ofNullable(type); } - public boolean hasEnums() { - return getEnum().map(l -> !l.isEmpty()).orElse(false); + @Nonnull + public Optional getFormat() { + return Optional.ofNullable(format); } - public Optional> getOneOf() { - return childrenOpt("oneOf", Schema::getOneOf, OpenApiSchema::new); + public boolean is(@Nonnull OpenApiSchemaType type) { + Objects.requireNonNull(type, "type must not be null"); + return getType().map(type::equals).orElse(false); } - public boolean hasAllOf() { - return getAllOf().isPresent(); + public boolean isArray() { + return is(OpenApiSchemaType.ARRAY); } - public Optional> getAllOf() { - return childrenOpt("allOf", Schema::getAllOf, OpenApiSchema::new); + public boolean isBoolean() { + return is(OpenApiSchemaType.BOOLEAN); } - public Optional getItems() { - return childOpt("items", Schema::getItems, OpenApiSchema::new); + public boolean isNumber() { + return is(OpenApiSchemaType.NUMBER); } - public Optional getAdditionalProperties() { - return childOpt("additionalProperties", s -> (Schema) s.getAdditionalProperties(), OpenApiSchema::new); + public boolean isObject() { + return is(OpenApiSchemaType.OBJECT); } - public Set getRequired() { - return Optional.ofNullable((List) getInner().getRequired()).map(HashSet::new).orElseGet(HashSet::new); + public boolean isString() { + return is(OpenApiSchemaType.STRING); } - public Stream getProperties() { - var properties = (Map>) getInner().getProperties(); - if (properties == null) return Stream.empty(); - var basePtr = getJsonPtr().append("properties"); - var requiredFields = getRequired(); - return properties.entrySet() - .stream() - .map(e -> new OpenApiProperty(getParent(), basePtr.append(e.getKey()), e.getValue(), requiredFields.contains(e.getKey()))); + public boolean hasAllOf() { + return allOf != null && !allOf.isEmpty(); } - @Override - public Map getExtensions() { - return getInner().getExtensions(); + @Nonnull + public Optional> getAllOf() { + return Lists.unmodifiableOpt(allOf); } - public Optional getXDataType() { - return getExtensionAsString("x-data-type"); + public boolean hasOneOf() { + return oneOf != null && !oneOf.isEmpty(); } - public String getName() { - return getJsonPtr().getKey().orElseThrow(); + @Nonnull + public Optional> getOneOf() { + return Lists.unmodifiableOpt(oneOf); } - public String getNamespace() { - var schemaFile = Path.of(getParent().getLocation().getPath()).getFileName().toString(); - var ns = schemaFile.substring(0, schemaFile.lastIndexOf('.')); - // Match existing naming scheme - return ns.replaceAll("\\._common", "").replaceAll("^_common", "_types"); + public boolean hasEnums() { + return enums != null && !enums.isEmpty(); } - @Override - protected OpenApiSchema getSelf() { - return this; + @Nonnull + public Optional> getEnums() { + return Lists.unmodifiableOpt(enums); } - public enum Type { - ARRAY, - BOOLEAN, - INTEGER, - NUMBER, - OBJECT, - STRING, - TIME; + @Nonnull + public Optional getItems() { + return Optional.ofNullable(items); + } - public static Type from(String s) { - return valueOf(s.toUpperCase()); - } + @Nonnull + public Optional getAdditionalProperties() { + return Optional.ofNullable(additionalProperties); } - public enum Format { - INT32, - INT64, - FLOAT, - DOUBLE; + @Nonnull + public Optional> getProperties() { + return Maps.unmodifiableOpt(properties); + } - public static Format from(String s) { - return valueOf(s.toUpperCase()); - } + @Nonnull + public Optional> getRequired() { + return Sets.unmodifiableOpt(required); } } diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiSchemaFormat.java b/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiSchemaFormat.java new file mode 100644 index 0000000000..a33e015df1 --- /dev/null +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiSchemaFormat.java @@ -0,0 +1,25 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.client.codegen.openapi; + +import javax.annotation.Nonnull; +import org.opensearch.client.codegen.utils.Strings; + +public enum OpenApiSchemaFormat { + FLOAT, + DOUBLE, + INT32, + INT64; + + @Nonnull + public static OpenApiSchemaFormat from(@Nonnull String format) { + Strings.requireNonBlank(format, "format must not be blank"); + return OpenApiSchemaFormat.valueOf(format.toUpperCase()); + } +} diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiSchemaType.java b/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiSchemaType.java new file mode 100644 index 0000000000..9efb91dd5d --- /dev/null +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiSchemaType.java @@ -0,0 +1,27 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.client.codegen.openapi; + +import javax.annotation.Nonnull; +import org.opensearch.client.codegen.utils.Strings; + +public enum OpenApiSchemaType { + ARRAY, + BOOLEAN, + INTEGER, + NUMBER, + OBJECT, + STRING; + + @Nonnull + public static OpenApiSchemaType from(@Nonnull String type) { + Strings.requireNonBlank(type, "type must not be blank"); + return OpenApiSchemaType.valueOf(type.toUpperCase()); + } +} diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiSpec.java b/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiSpec.java deleted file mode 100644 index fd67145990..0000000000 --- a/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiSpec.java +++ /dev/null @@ -1,115 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.client.codegen.openapi; - -import io.swagger.v3.oas.models.OpenAPI; -import io.swagger.v3.parser.OpenAPIV3Parser; -import io.swagger.v3.parser.core.models.ParseOptions; -import java.net.URI; -import java.util.HashMap; -import java.util.Map; -import java.util.function.Function; -import java.util.stream.Stream; -import org.apache.commons.lang3.builder.EqualsBuilder; -import org.apache.commons.lang3.builder.HashCodeBuilder; -import org.apache.commons.lang3.builder.ToStringBuilder; -import org.apache.commons.lang3.tuple.Triple; -import org.opensearch.client.codegen.exceptions.ApiSpecificationParseException; - -public class OpenApiSpec { - private static final ParseOptions PARSE_OPTIONS = new ParseOptions(); - private static final OpenAPIV3Parser PARSER = new OpenAPIV3Parser(); - private static final Map SPECS = new HashMap<>(); - - public static OpenApiSpec parse(URI location) { - if (SPECS.containsKey(location)) { - return SPECS.get(location); - } - System.out.println("Parsing spec: " + location); - var result = PARSER.readLocation(location.toString(), null, PARSE_OPTIONS); - var openApi = result.getOpenAPI(); - if (openApi == null) { - throw new ApiSpecificationParseException("Unable to parse spec: " + location, result.getMessages()); - } - var spec = new OpenApiSpec(location, openApi); - SPECS.put(location, spec); - return spec; - } - - private final URI location; - private final OpenAPI api; - - private OpenApiSpec(URI location, OpenAPI api) { - this.location = location; - this.api = api; - } - - protected Triple resolve( - Function> componentsGetter, - JsonPointer jsonPtr, - T item, - Function get$ref - ) { - var spec = this; - String $ref; - while (($ref = get$ref.apply(item)) != null) { - var fragmentIdx = $ref.indexOf('#'); - - if (fragmentIdx > 0) { - spec = parse(spec.location.resolve($ref.substring(0, fragmentIdx))); - } - - jsonPtr = JsonPointer.parse($ref.substring(fragmentIdx + 1)); - - item = componentsGetter.apply(spec.api).get(jsonPtr.getKey().orElseThrow()); - - if (item == null) { - throw new IllegalStateException("Failed to resolve reference: " + $ref); - } - } - return Triple.of(spec, jsonPtr, item); - } - - public URI getLocation() { - return location; - } - - public Stream getPaths() { - var pathsPtr = JsonPointer.of("paths"); - return api.getPaths() - .entrySet() - .stream() - .map(e -> new OpenApiPath(this, pathsPtr.append(e.getKey()), e.getKey(), e.getValue()).resolve()); - } - - public Stream getOperations() { - return getPaths().flatMap(OpenApiPath::getOperations); - } - - @Override - public String toString() { - return new ToStringBuilder(this).append("location", location).toString(); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - - if (o == null || getClass() != o.getClass()) return false; - - OpenApiSpec that = (OpenApiSpec) o; - - return new EqualsBuilder().append(location, that.location).isEquals(); - } - - @Override - public int hashCode() { - return new HashCodeBuilder(17, 37).append(location).toHashCode(); - } -} diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiSpecification.java b/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiSpecification.java new file mode 100644 index 0000000000..1918f20206 --- /dev/null +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiSpecification.java @@ -0,0 +1,104 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.client.codegen.openapi; + +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.parser.OpenAPIV3Parser; +import io.swagger.v3.parser.core.models.ParseOptions; +import java.net.URI; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import org.opensearch.client.codegen.exceptions.ApiSpecificationParseException; +import org.opensearch.client.codegen.utils.Maps; + +public class OpenApiSpecification extends OpenApiElement { + private static final ParseOptions PARSE_OPTIONS = new ParseOptions(); + private static final OpenAPIV3Parser PARSER = new OpenAPIV3Parser(); + private static final Map SPECS = new HashMap<>(); + + public static OpenApiSpecification retrieve(URI location) { + if (SPECS.containsKey(location)) { + return SPECS.get(location); + } + System.out.println("Parsing spec: " + location); + var result = PARSER.readLocation(location.toString(), null, PARSE_OPTIONS); + var openApi = result.getOpenAPI(); + if (openApi == null) { + throw new ApiSpecificationParseException("Unable to parse spec: " + location, result.getMessages()); + } + var spec = new OpenApiSpecification(location, openApi); + SPECS.put(location, spec); + return spec; + } + + @Nonnull + private final Set cachedElements = new HashSet<>(); + @Nonnull + private final Map, Map>> elementCache = new HashMap<>(); + @Nonnull + private final URI location; + @Nullable + private final Map paths; + @Nullable + private final OpenApiComponents components; + + private OpenApiSpecification(@Nonnull URI location, @Nonnull OpenAPI openApi) { + super(null, JsonPointer.ROOT); + this.location = Objects.requireNonNull(location, "location must not be null"); + Objects.requireNonNull(openApi, "openAPI must not be null"); + this.paths = children("paths", openApi.getPaths(), OpenApiPath::new); + this.components = child("components", openApi.getComponents(), OpenApiComponents::new); + } + + @Nonnull + @Override + protected Optional getSpecification() { + return Optional.of(this); + } + + > void addElement(@Nonnull JsonPointer pointer, @Nonnull T component) { + Objects.requireNonNull(pointer, "pointer must not be null"); + Objects.requireNonNull(component, "component must not be null"); + if (!cachedElements.add(pointer)) { + throw new IllegalStateException("Component with pointer `" + pointer + "` has already been registered"); + } + elementCache.computeIfAbsent(component.getClass(), k -> new HashMap<>()).put(pointer, component); + } + + @Nonnull + > T getElement(@Nonnull JsonPointer pointer, @Nonnull Class type) { + Objects.requireNonNull(pointer, "pointer must not be null"); + Objects.requireNonNull(type, "type must not be null"); + return Optional.ofNullable(elementCache.get(type)) + .flatMap(m -> Optional.ofNullable(m.get(pointer))) + .map(type::cast) + .orElseThrow(() -> new IllegalStateException("Unable to resolve component of type `" + type + "` with pointer: " + pointer)); + } + + @Nonnull + public Optional> getPaths() { + return Maps.unmodifiableOpt(paths); + } + + @Nonnull + public Optional getComponents() { + return Optional.ofNullable(components); + } + + @Nonnull + public URI getLocation() { + return location; + } +} diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/utils/Functional.java b/java-codegen/src/main/java/org/opensearch/client/codegen/utils/Functional.java new file mode 100644 index 0000000000..88255b1c05 --- /dev/null +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/utils/Functional.java @@ -0,0 +1,19 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.client.codegen.utils; + +import java.util.function.Function; + +public final class Functional { + private Functional() {} + + public static R ifNonnull(T value, Function mapper) { + return value != null ? mapper.apply(value) : null; + } +} diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/utils/Lists.java b/java-codegen/src/main/java/org/opensearch/client/codegen/utils/Lists.java new file mode 100644 index 0000000000..6761de69c7 --- /dev/null +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/utils/Lists.java @@ -0,0 +1,48 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.client.codegen.utils; + +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +public final class Lists { + private Lists() {} + + @Nonnull + public static Optional> unmodifiableOpt(@Nullable List list) { + return Optional.ofNullable(list).map(Collections::unmodifiableList); + } + + @Nonnull + public static List transform(@Nonnull List list, @Nonnull Function function) { + Objects.requireNonNull(list, "list must not be null"); + Objects.requireNonNull(function, "function must not be null"); + return list.stream().map(function).collect(Collectors.toList()); + } + + @Nonnull + public static List transform(@Nonnull List list, @Nonnull ItemMapper function) { + Objects.requireNonNull(list, "list must not be null"); + Objects.requireNonNull(function, "function must not be null"); + return IntStream.range(0, list.size()).mapToObj(i -> function.map(i, list.get(i))).toList(); + } + + @FunctionalInterface + public interface ItemMapper { + @Nonnull + TOut map(int index, @Nonnull TIn item); + } +} diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/utils/Maps.java b/java-codegen/src/main/java/org/opensearch/client/codegen/utils/Maps.java new file mode 100644 index 0000000000..bf1e274223 --- /dev/null +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/utils/Maps.java @@ -0,0 +1,49 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.client.codegen.utils; + +import java.util.Collections; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +public final class Maps { + private Maps() {} + + @Nonnull + public static Optional> unmodifiableOpt(@Nullable Map map) { + return Optional.ofNullable(map).map(Collections::unmodifiableMap); + } + + @Nonnull + public static Map transform( + @Nonnull Map map, + @Nonnull EntryMapper keyMapper, + @Nonnull EntryMapper valueMapper + ) { + Objects.requireNonNull(map, "map must not be null"); + Objects.requireNonNull(keyMapper, "keyMapper must not be null"); + Objects.requireNonNull(valueMapper, "valueMapper must not be null"); + return map.entrySet().stream().collect(Collectors.toMap(keyMapper::map, valueMapper::map)); + } + + @FunctionalInterface + public interface EntryMapper { + @Nonnull + TResult map(@Nonnull TKey key, @Nonnull TValue value); + + @Nonnull + default TResult map(@Nonnull Map.Entry entry) { + return map(entry.getKey(), entry.getValue()); + } + } +} diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/utils/Sets.java b/java-codegen/src/main/java/org/opensearch/client/codegen/utils/Sets.java new file mode 100644 index 0000000000..b63536f6d7 --- /dev/null +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/utils/Sets.java @@ -0,0 +1,24 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.client.codegen.utils; + +import java.util.Collections; +import java.util.Optional; +import java.util.Set; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +public final class Sets { + private Sets() {} + + @Nonnull + public static Optional> unmodifiableOpt(@Nullable Set set) { + return Optional.ofNullable(set).map(Collections::unmodifiableSet); + } +} diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/utils/Streams.java b/java-codegen/src/main/java/org/opensearch/client/codegen/utils/Streams.java index b8f94230e8..2e7655a3c9 100644 --- a/java-codegen/src/main/java/org/opensearch/client/codegen/utils/Streams.java +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/utils/Streams.java @@ -1,13 +1,10 @@ package org.opensearch.client.codegen.utils; -import java.util.Collection; import java.util.function.Function; import java.util.stream.Stream; -public class Streams { - public static Stream tryOf(Collection list) { - return list == null ? Stream.empty() : list.stream(); - } +public final class Streams { + private Streams() {} public static > Stream sortedBy(Stream stream, Function keyExtractor) { return stream.sorted((a, b) -> { diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/utils/Strings.java b/java-codegen/src/main/java/org/opensearch/client/codegen/utils/Strings.java index 911d7dd7c6..a8afed9249 100644 --- a/java-codegen/src/main/java/org/opensearch/client/codegen/utils/Strings.java +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/utils/Strings.java @@ -9,27 +9,56 @@ package org.opensearch.client.codegen.utils; import java.util.Locale; +import java.util.Objects; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; import org.apache.commons.text.CaseUtils; +import org.jetbrains.annotations.Contract; public final class Strings { private Strings() {} - public static String toCamelCase(String str) { - return toCamelCase(str, false); + public static boolean isNullOrEmpty(@Nullable String str) { + return str == null || str.isEmpty(); } - public static String toPascalCase(String str) { - return toCamelCase(str, true); + public static boolean isNullOrBlank(@Nullable String str) { + return str == null || str.isBlank(); } - private static String toCamelCase(String str, boolean capitalizeFirstLetter) { - return CaseUtils.toCamelCase(toSnakeCase(str), capitalizeFirstLetter, '_', '.'); + @Nonnull + @Contract(value = "null, _ -> fail; _, _ -> param1", pure = true) + public static String requireNonBlank(@Nullable String str, @Nullable String message) { + if (isNullOrBlank(str)) { + throw new IllegalArgumentException(message); + } + return str; } - public static String toSnakeCase(String str) { + @Nonnull + public static String toSnakeCase(@Nonnull String str) { + Objects.requireNonNull(str, "str must not be null"); + if (str.isEmpty()) { + return str; + } return str.replaceAll("([A-Z]+)([A-Z][a-z])", "$1_$2") .replaceAll("([a-z\\d])([A-Z])", "$1_$2") - .replaceAll("(\\s|-)", "_") + .replaceAll("(\\s|[-:.])", "_") .toLowerCase(Locale.US); } + + @Nonnull + private static String toCamelCase(@Nonnull String str, boolean capitalizeFirstLetter) { + return CaseUtils.toCamelCase(toSnakeCase(str), capitalizeFirstLetter, '_'); + } + + @Nonnull + public static String toCamelCase(@Nonnull String str) { + return toCamelCase(str, false); + } + + @Nonnull + public static String toPascalCase(@Nonnull String str) { + return toCamelCase(str, true); + } }