diff --git a/.gitignore b/.gitignore index b8638f2e7..7ccebda40 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-client/build.gradle.kts b/java-client/build.gradle.kts index 35f639bfc..0352ffbe4 100644 --- a/java-client/build.gradle.kts +++ b/java-client/build.gradle.kts @@ -87,6 +87,14 @@ java { } } +sourceSets { + main { + java { + srcDir("src/generated/java") + } + } +} + tasks.withType { expand( "version" to version, diff --git a/java-client/src/main/java/org/opensearch/client/util/ApiTypeHelper.java b/java-client/src/main/java/org/opensearch/client/util/ApiTypeHelper.java index 1122975bc..55138eaaa 100644 --- a/java-client/src/main/java/org/opensearch/client/util/ApiTypeHelper.java +++ b/java-client/src/main/java/org/opensearch/client/util/ApiTypeHelper.java @@ -39,6 +39,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import javax.annotation.Nonnull; import javax.annotation.Nullable; /** @@ -134,6 +135,7 @@ public static boolean isDefined(List list) { /** * Returns an unmodifiable view of a list. If {@code list} is {@code null}, an {@link #undefinedList()} is returned. */ + @Nonnull public static List unmodifiable(@Nullable List list) { if (list == null) { return undefinedList(); diff --git a/java-codegen/build.gradle.kts b/java-codegen/build.gradle.kts new file mode 100644 index 000000000..41c298935 --- /dev/null +++ b/java-codegen/build.gradle.kts @@ -0,0 +1,262 @@ +/* + * 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. + */ + +/* + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +import com.github.jk1.license.ProjectData +import com.github.jk1.license.render.ReportRenderer +import java.io.FileWriter + +buildscript { + repositories { + mavenLocal() + maven(url = "https://aws.oss.sonatype.org/content/repositories/snapshots") + mavenCentral() + maven(url = "https://plugins.gradle.org/m2/") + } + dependencies { + "classpath"(group = "org.opensearch.gradle", name = "build-tools", version = "3.0.0-SNAPSHOT") + } +} + +plugins { + application + id("com.github.jk1.dependency-license-report") version "2.8" + id("org.owasp.dependencycheck") version "9.2.0" + id("com.diffplug.spotless") version "6.25.0" +} +apply(plugin = "opensearch.repositories") +apply(plugin = "org.owasp.dependencycheck") + +val runtimeJavaVersion = (System.getProperty("runtime.java")?.toInt())?.let(JavaVersion::toVersion) ?: JavaVersion.current() +logger.quiet("=======================================") +logger.quiet(" Runtime JDK Version : $runtimeJavaVersion") +logger.quiet(" Gradle JDK Version : " + JavaVersion.current()) +logger.quiet("=======================================") + +java { + targetCompatibility = JavaVersion.VERSION_11 + sourceCompatibility = JavaVersion.VERSION_11 + + withJavadocJar() + withSourcesJar() + + toolchain { + languageVersion = JavaLanguageVersion.of(runtimeJavaVersion.majorVersion) + vendor = JvmVendorSpec.ADOPTIUM + } +} + +application { + mainClass.set("org.opensearch.client.codegen.Main") + applicationDefaultJvmArgs = listOf( + "--add-exports", "jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED", + "--add-exports", "jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED", + "--add-exports", "jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED", + "--add-exports", "jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED", + "--add-exports", "jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED" + ) +} + +tasks.named("run") { + args = listOf( + "--input", "$projectDir/opensearch-openapi.yaml", + "--eclipse-config", "$rootDir/buildSrc/formatterConfig.xml", + "--output", "${project(":java-client").projectDir}/src/generated/java/" + ) +} + +tasks.withType { + expand( + "version" to version, + "git_revision" to (if (rootProject.extra.has("gitHashFull")) rootProject.extra["gitHashFull"] else "unknown") + ) +} + +tasks.withType().configureEach{ + options { + encoding = "UTF-8" + } +} + +tasks.withType { + doFirst { + if (rootProject.extra.has("gitHashFull")) { + val jar = this as Jar + jar.manifest.attributes["X-Git-Revision"] = rootProject.extra["gitHashFull"] + jar.manifest.attributes["X-Git-Commit-Time"] = rootProject .extra["gitCommitTime"] + } else { + throw GradleException("No git information available") + } + } + + manifest { + attributes["Implementation-Title"] = "OpenSearch Java client code generator" + attributes["Implementation-Vendor"] = "OpenSearch" + attributes["Implementation-URL"] = "https://github.com/opensearch-project/opensearch-java/" + attributes["Build-Date"] = rootProject.extra["buildTime"] + } + + metaInf { + from("../LICENSE.txt") + from("../NOTICE.txt") + } +} + +tasks.withType { + useJUnitPlatform() + testLogging { + events("passed", "skipped", "failed") + } +} + +tasks.build { + dependsOn("spotlessJavaCheck") +} + +dependencies { + // Apache 2.0 + implementation("io.swagger.parser.v3", "swagger-parser", "2.1.22") + + // (New) BSD + implementation("com.samskivert", "jmustache", "1.16") + + // Apache 2.0 + implementation("commons-cli", "commons-cli", "1.8.0") + implementation("commons-logging", "commons-logging", "1.3.2") + implementation("org.apache.commons", "commons-lang3", "3.14.0") + implementation("org.apache.commons", "commons-text", "1.12.0") + implementation("org.apache.logging.log4j", "log4j-api", "[2.17.1,3.0)") + implementation("org.apache.logging.log4j", "log4j-core", "[2.17.1,3.0)") + implementation("org.apache.logging.log4j", "log4j-slf4j2-impl", "[2.17.1,3.0)") + + // Apache 2.0 + implementation("com.fasterxml.jackson.core", "jackson-core", "2.17.1") + implementation("com.fasterxml.jackson.core", "jackson-databind", "2.17.1") + + // Apache 2.0 + implementation("com.diffplug.spotless", "spotless-lib", "2.45.0") + implementation("com.diffplug.spotless", "spotless-lib-extra", "2.45.0") + + // Apache 2.0 + // 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.20") + implementation("org.apache.maven.resolver", "maven-resolver-supplier", "1.9.20") + + // EPL-2.0 + testImplementation(platform("org.junit:junit-bom:5.10.2")) + testImplementation("org.junit.jupiter", "junit-jupiter") + testRuntimeOnly("org.junit.platform", "junit-platform-launcher") +} + +licenseReport { + renderers = arrayOf(SpdxReporter(rootProject.layout.buildDirectory.file("release/codegen-dependencies.csv").get().getAsFile())) + excludeGroups = arrayOf() +} + +class SpdxReporter(val dest: File) : ReportRenderer { + // License names to their SPDX identifier + val spdxIds = mapOf( + "Apache 2" to "Apache-2.0", + "Apache 2.0" to "Apache-2.0", + "Apache-2.0" to "Apache-2.0", + "\"Apache-2.0\";link=\"https://www.apache.org/licenses/LICENSE-2.0.txt\"" to "Apache-2.0", + "\"Apache 2.0\";link=\"http://www.apache.org/licenses/LICENSE-2.0.txt\"" to "Apache-2.0", + "\"Apache License 2.0\";link=\"http://www.apache.org/licenses/LICENSE-2.0.html\"" to "Apache-2.0", + "Apache License 2.0" to "Apache-2.0", + "Apache License, version 2.0" to "Apache-2.0", + "Apache License, Version 2.0" to "Apache-2.0", + "Apache Software License, version 2.0" to "Apache-2.0", + "The Apache License, Version 2.0" to "Apache-2.0", + "The Apache Software License, Version 2.0" to "Apache-2.0", + "BSD Zero Clause License" to "0BSD", + "The (New) BSD License" to "BSD-3-Clause", + "EDL 1.0" to "BSD-3-Clause", + "Eclipse Distribution License - v 1.0" to "BSD-3-Clause", + "Eclipse Distribution License (New BSD License)" to "BSD-3-Clause", + "Eclipse Public License 2.0" to "EPL-2.0", + "Eclipse Public License v. 2.0" to "EPL-2.0", + "Eclipse Public License - v 2.0" to "EPL-2.0", + "GNU General Public License, version 2 with the GNU Classpath Exception" to "GPL-2.0 WITH Classpath-exception-2.0", + "COMMON DEVELOPMENT AND DISTRIBUTION LICENSE (CDDL) Version 1.0" to "CDDL-1.0", + "Lesser General Public License, version 3 or greater" to "LGPL-3.0+", + "MIT" to "MIT", + "MIT License" to "MIT", + "The MIT License" to "MIT", + "Mozilla Public License, Version 2.0" to "MPL-2.0", + "Public Domain" to "PUBLIC-DOMAIN" + ) + + private fun quote(str: String) : String { + return if (str.contains(',') || str.contains("\"")) { + "\"" + str.replace("\"", "\"\"") + "\"" + } else { + str + } + } + + override fun render(data: ProjectData?) { + dest.parentFile.mkdirs() + FileWriter(dest).use { out -> + out.append("name,url,version,revision,license\n") + data?.allDependencies?.forEach { dep -> + val info = com.github.jk1.license.render.LicenseDataCollector.multiModuleLicenseInfo(dep) + + val depVersion = dep.version + val depName = dep.group + ":" + dep.name + val depUrl = if (info.moduleUrls.isNotEmpty()) { info.moduleUrls.first() } else { "" } + + val licenseIds = info.licenses.mapNotNull { license -> + license.name?.let { + checkNotNull(spdxIds[it]) { "No SPDX identifier for $license" } + } + }.toSet() + + // Combine multiple licenses. + // See https://spdx.github.io/spdx-spec/appendix-IV-SPDX-license-expressions/#composite-license-expressions + val licenseId = licenseIds.joinToString(" OR ") + out.append("${quote(depName)},${quote(depUrl)},${quote(depVersion)},,${quote(licenseId)}\n") + } + } + } +} + +tasks.withType { + doLast { + ant.withGroovyBuilder { + "checksum"("algorithm" to "md5", "file" to archiveFile.get()) + "checksum"("algorithm" to "sha1", "file" to archiveFile.get()) + "checksum"("algorithm" to "sha-256", "file" to archiveFile.get(), "fileext" to ".sha256") + "checksum"("algorithm" to "sha-512", "file" to archiveFile.get(), "fileext" to ".sha512") + } + } +} + +spotless { + java { + target("**/*.java") + + // Use the default importOrder configuration + importOrder() + removeUnusedImports() + + eclipse().configFile("../buildSrc/formatterConfig.xml") + + trimTrailingWhitespace() + endWithNewline() + } +} 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 new file mode 100644 index 000000000..115408b25 --- /dev/null +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/Main.java @@ -0,0 +1,129 @@ +/* + * 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; + +import java.io.File; +import java.io.IOException; +import java.io.PrintWriter; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Comparator; +import java.util.stream.Stream; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.HelpFormatter; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.commons.cli.ParseException; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +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.ShapeRenderingContext; +import org.opensearch.client.codegen.model.SpecTransformer; +import org.opensearch.client.codegen.openapi.OpenApiSpecification; + +public class Main { + private static final Logger LOGGER = LogManager.getLogger(); + private static final OperationGroup.Matcher OPERATION_MATCHER = OperationGroup.matcher(); + + public static void main(String[] args) { + var inputOpt = Option.builder("i") + .longOpt("input") + .desc("The URI or path of the OpenAPI specification") + .argName("INPUT") + .hasArg() + .required() + .build(); + var eclipseConfigOpt = Option.builder() + .longOpt("eclipse-config") + .desc("The path of the Eclipse formatting config file") + .argName("ECLIPSE_CONFIG") + .hasArg() + .required() + .build(); + var outputOpt = Option.builder("o") + .longOpt("output") + .desc("The path to the output directory to generate code into") + .argName("OUTPUT") + .hasArg() + .required() + .build(); + var helpOpt = Option.builder("h").longOpt("help").desc("Print this help information").build(); + final var usageString = + "Main.class --input https://.../opensearch-openapi.yaml --eclipse-config ./buildSrc/formatterConfig.xml --output ./java-client/src/generated/java"; + + var options = new Options().addOption(inputOpt).addOption(eclipseConfigOpt).addOption(outputOpt).addOption(helpOpt); + + var cliParser = new DefaultParser(); + + try { + var cli = cliParser.parse(options, args); + + if (cli.hasOption(helpOpt)) { + var helpFormatter = HelpFormatter.builder().get(); + helpFormatter.printHelp(usageString, options); + return; + } + + var specLocation = new URI(cli.getOptionValue(inputOpt)); + var eclipseConfig = new File(cli.getOptionValue(eclipseConfigOpt)); + var outputDir = new File(cli.getOptionValue(outputOpt)); + LOGGER.info("Specification Location: {}", specLocation); + LOGGER.info("Eclipse Configuration: {}", eclipseConfig); + LOGGER.info("Output Directory: {}", outputDir); + + Namespace root = parseSpec(specLocation); + + cleanDirectory(outputDir); + + final var rootPackageOutputDir = new File(outputDir, root.getPackageName().replace('.', '/')); + + try ( + var ctx = ShapeRenderingContext.builder() + .withOutputDir(rootPackageOutputDir) + .withJavaCodeFormatter(b -> b.withRootDir(rootPackageOutputDir.toPath()).withEclipseFormatterConfig(eclipseConfig)) + .withTemplateLoader(b -> b.withTemplatesResourceSubPath("/org/opensearch/client/codegen/templates")) + .build() + ) { + root.render(ctx); + } + } catch (ParseException e) { + LOGGER.error("Argument Parsing Failed. Reason: {}", e.getMessage()); + + var helpFormatter = HelpFormatter.builder().setPrintWriter(new PrintWriter(System.err)).get(); + helpFormatter.printHelp(usageString, options); + + System.exit(1); + } catch (Throwable e) { + LOGGER.fatal("Unexpected Error", e); + System.exit(1); + } + } + + private static Namespace parseSpec(URI location) throws ApiSpecificationParseException { + 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) { + throw new RenderException("Unable to cleanup output directory: " + dir, 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 new file mode 100644 index 000000000..8370a06c1 --- /dev/null +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/NameSanitizer.java @@ -0,0 +1,33 @@ +/* + * 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; + +import java.util.HashSet; +import java.util.Set; +import javax.annotation.Nonnull; +import org.opensearch.client.codegen.utils.Strings; + +public class NameSanitizer { + private static final Set reservedWords = new HashSet<>() { + { + add("default"); + add("native"); + add("transient"); + } + }; + + @Nonnull + public static String wireNameToField(@Nonnull String wireName) { + var name = Strings.toCamelCase(wireName); + if (reservedWords.contains(name)) { + name += "_"; + } + return name; + } +} diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/exceptions/ApiSpecificationParseException.java b/java-codegen/src/main/java/org/opensearch/client/codegen/exceptions/ApiSpecificationParseException.java new file mode 100644 index 000000000..eb8c8e823 --- /dev/null +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/exceptions/ApiSpecificationParseException.java @@ -0,0 +1,21 @@ +/* + * 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.exceptions; + +import java.util.List; + +public class ApiSpecificationParseException extends RuntimeException { + public ApiSpecificationParseException(String msg, Exception inner) { + super(msg, inner); + } + + public ApiSpecificationParseException(String msg, List errors) { + super(msg + "\n-" + String.join("\n-", errors)); + } +} diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/exceptions/JavaFormatterException.java b/java-codegen/src/main/java/org/opensearch/client/codegen/exceptions/JavaFormatterException.java new file mode 100644 index 000000000..66a069905 --- /dev/null +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/exceptions/JavaFormatterException.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.exceptions; + +public class JavaFormatterException extends Exception { + public JavaFormatterException(String message) { + super(message); + } + + public JavaFormatterException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/exceptions/RenderException.java b/java-codegen/src/main/java/org/opensearch/client/codegen/exceptions/RenderException.java new file mode 100644 index 000000000..22822558d --- /dev/null +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/exceptions/RenderException.java @@ -0,0 +1,15 @@ +/* + * 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.exceptions; + +public class RenderException extends Exception { + public RenderException(String msg, Exception inner) { + super(msg, inner); + } +} diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/model/ArrayShape.java b/java-codegen/src/main/java/org/opensearch/client/codegen/model/ArrayShape.java new file mode 100644 index 000000000..c45e73fda --- /dev/null +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/model/ArrayShape.java @@ -0,0 +1,40 @@ +/* + * 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.model; + +import java.util.Collection; +import java.util.List; + +public class ArrayShape extends ObjectShape { + private final Field valueBodyField; + + public ArrayShape(Namespace parent, String className, Type arrayType, String typedefName) { + super(parent, className, typedefName); + this.valueBodyField = new Field("_value_body", arrayType, true, "Response value.", null); + } + + @Override + public Collection getFields() { + return List.of(valueBodyField); + } + + @Override + public Collection getAnnotations() { + return List.of(Types.Client.Json.JsonpDeserializable); + } + + @Override + public Collection getImplementsTypes() { + return List.of(Types.Client.Json.JsonpSerializable); + } + + public Field getValueBodyField() { + return valueBodyField; + } +} 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 new file mode 100644 index 000000000..efe8c0582 --- /dev/null +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/model/Deprecation.java @@ -0,0 +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(@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/EnumShape.java b/java-codegen/src/main/java/org/opensearch/client/codegen/model/EnumShape.java new file mode 100644 index 000000000..64cd36b76 --- /dev/null +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/model/EnumShape.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.model; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import org.opensearch.client.codegen.utils.Strings; + +public class EnumShape extends Shape { + private final List variants; + + public EnumShape(Namespace parent, String className, List variants, String typedefName) { + super(parent, className, typedefName); + this.variants = variants; + } + + public Collection getVariants() { + return Collections.unmodifiableCollection(variants); + } + + public static class Variant { + private final String wireName; + + public Variant(String wireName) { + this.wireName = wireName; + } + + public String getWireName() { + return wireName; + } + + public String getName() { + return Strings.toPascalCase(wireName); + } + } +} 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 new file mode 100644 index 000000000..f2a95028d --- /dev/null +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/model/Field.java @@ -0,0 +1,74 @@ +/* + * 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.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; + @Nullable + private final Deprecation deprecation; + + public Field( + @Nonnull String wireName, + @Nonnull Type type, + boolean required, + @Nullable String description, + @Nullable Deprecation deprecation + ) { + 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; + this.deprecation = deprecation; + } + + @Nonnull + public String getWireName() { + return wireName; + } + + @Nonnull + public String getName() { + return NameSanitizer.wireNameToField(wireName); + } + + @Nonnull + public Type getType() { + return required ? type : type.getBoxed(); + } + + public boolean isRequired() { + return required; + } + + public void setRequired(boolean required) { + this.required = required; + } + + @Nullable + public String getDescription() { + return description; + } + + @Nullable + public Deprecation getDeprecation() { + return deprecation; + } +} 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 new file mode 100644 index 000000000..8050fc21e --- /dev/null +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/model/HttpPath.java @@ -0,0 +1,120 @@ +/* + * 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.model; + +import java.util.ArrayList; +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; +import org.opensearch.client.codegen.utils.Lists; + +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) { + var parts = new ArrayList(); + var isParameter = false; + var text = new StringBuilder(); + + for (char c : httpPath.toCharArray()) { + if (c == '{' || c == '}') { + if (text.length() > 0) { + parts.add(Part.from(isParameter, text.toString(), pathParams)); + } + text.setLength(0); + isParameter = c == '{'; + } else { + text.append(c); + } + } + + if (text.length() > 0) { + parts.add(Part.from(isParameter, text.toString(), pathParams)); + } + + return new HttpPath(parts, operation.getDeprecation().orElse(null), operation.getVersionAdded().orElse(null)); + } + + 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; + } + + public List getParams() { + return Lists.filterMap(parts, Part::isParameter, Part::getParameter); + } + + public Set getParamNameSet() { + return parts.stream().filter(Part::isParameter).map(p -> p.getParameter().getName()).collect(Collectors.toSet()); + } + + public Deprecation getDeprecation() { + return deprecation; + } + + public Collection getParts() { + return parts; + } + + public boolean hasParams() { + return parts.stream().anyMatch(Part::isParameter); + } + + @Override + public String toString() { + return parts.stream().map(Part::toString).collect(Collectors.joining()); + } + + public static class Part { + private final Either part; + + public static Part from(boolean isParameter, String text, Map pathParams) { + if (text == null || text.isEmpty()) throw new IllegalStateException("Text cannot be null or empty"); + if (!isParameter) return new Part(Either.right(text)); + var param = pathParams.get(text); + if (param == null) throw new IllegalStateException("Parameter not found: " + text); + return new Part(Either.left(param)); + } + + private Part(Either part) { + this.part = part; + } + + public boolean isParameter() { + return part.isLeft(); + } + + public String getContent() { + return part.getRightOrThrow((p) -> new IllegalStateException("Cannot get content of non-content part")); + } + + public Field getParameter() { + return part.getLeftOrThrow((c) -> new IllegalStateException("Cannot get parameter of non-parameter part")); + } + + @Override + public String toString() { + return part.fold(f -> '{' + f.getName() + '}', Function.identity()); + } + } +} 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 new file mode 100644 index 000000000..6e313b0f4 --- /dev/null +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/model/Namespace.java @@ -0,0 +1,110 @@ +/* + * 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.model; + +import java.util.ArrayList; +import java.util.Collection; +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.exceptions.RenderException; +import org.opensearch.client.codegen.utils.Lists; +import org.opensearch.client.codegen.utils.Strings; + +public class Namespace extends Shape { + private final String name; + private final Map children = new TreeMap<>(); + private final Map operations = new TreeMap<>(); + private final List shapes = new ArrayList<>(); + + public Namespace() { + this(null, ""); + } + + private Namespace(Namespace parent, String name) { + super(parent, null, null); + this.name = name; + } + + public void addOperation(RequestShape operation) { + operations.put(operation.getId(), operation); + addShape(operation); + } + + public void addShape(Shape shape) { + shapes.add(shape); + } + + @Override + public String getPackageName() { + return parent != null ? parent.getPackageName() + "." + getPackageNamePart() : "org.opensearch.client.opensearch"; + } + + private String getPackageNamePart() { + return name; + } + + @Nonnull + public Namespace child(@Nullable String name) { + if (name == null || name.isEmpty()) { + return this; + } + + int idx = name.indexOf('.'); + var childName = idx >= 0 ? name.substring(0, idx) : name; + var grandChildName = idx >= 0 ? name.substring(idx + 1) : null; + + Namespace child = children.computeIfAbsent(childName, n -> new Namespace(this, n)); + return grandChildName == null ? child : child.child(grandChildName); + } + + @Override + public void render(ShapeRenderingContext ctx) throws RenderException { + for (Namespace child : children.values()) { + child.render(ctx.forSubDir(child.getPackageNamePart())); + } + + for (Shape shape : shapes) { + shape.render(ctx); + } + + if (operations.isEmpty()) return; + + // TODO: Render clients when won't be partial and conflict with non-generated code + // new Client(this, false).render(outputDir, formatter); + // new Client(this, true).render(outputDir, formatter); + } + + private static class Client extends Shape { + private final boolean async; + + private Client(Namespace parent, boolean async) { + super(parent, "OpenSearch" + Strings.toPascalCase(parent.name) + (async ? "Async" : "") + "Client", null); + this.async = async; + } + + public String getName() { + return parent.name; + } + + public Collection getChildren() { + return Lists.filterMap(parent.children.values(), n -> !n.operations.isEmpty(), n -> new Client(n, async)); + } + + public Collection getOperations() { + return parent.operations.values(); + } + + public boolean isAsync() { + return this.async; + } + } +} diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/model/ObjectShape.java b/java-codegen/src/main/java/org/opensearch/client/codegen/model/ObjectShape.java new file mode 100644 index 000000000..91bcdbdfa --- /dev/null +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/model/ObjectShape.java @@ -0,0 +1,66 @@ +/* + * 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.model; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +public class ObjectShape extends Shape { + protected Type extendsType; + protected final Map bodyFields = new TreeMap<>(); + protected Field additionalPropertiesField; + + public ObjectShape(Namespace parent, String className, String typedefName) { + super(parent, className, typedefName); + } + + public void addBodyField(Field field) { + bodyFields.put(field.getName(), field); + } + + public Collection getBodyFields() { + return bodyFields.values(); + } + + public Collection getFields() { + if (additionalPropertiesField != null) { + var fields = new ArrayList<>(getBodyFields()); + fields.add(additionalPropertiesField); + return fields; + } + return getBodyFields(); + } + + public void setAdditionalPropertiesField(Field field) { + additionalPropertiesField = field; + } + + public Field getAdditionalPropertiesField() { + return additionalPropertiesField; + } + + public void setExtendsType(Type extendsType) { + this.extendsType = extendsType; + } + + public Type getExtendsType() { + return extendsType; + } + + public Collection getImplementsTypes() { + return !bodyFields.isEmpty() ? List.of(Types.Client.Json.JsonpSerializable) : null; + } + + public Collection getAnnotations() { + return !bodyFields.isEmpty() ? List.of(Types.Client.Json.JsonpDeserializable) : null; + } +} 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 new file mode 100644 index 000000000..0ccf01276 --- /dev/null +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/model/OperationGroup.java @@ -0,0 +1,120 @@ +package org.opensearch.client.codegen.model; + +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; + + @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); + } + return new OperationGroup(operationGroup.substring(0, index), operationGroup.substring(index + 1)); + } + + private OperationGroup(@Nullable String namespace, @Nonnull String name) { + this.namespace = namespace; + this.name = Strings.requireNonBlank(name, "name must not be blank"); + } + + @Nonnull + public Optional getNamespace() { + return Optional.ofNullable(namespace); + } + + @Nonnull + public String getName() { + return name; + } + + @Override + public String toString() { + return namespace == null ? name : namespace + "." + name; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + + if (o == null || getClass() != o.getClass()) return false; + + OperationGroup that = (OperationGroup) o; + + return new EqualsBuilder().append(namespace, that.namespace).append(name, that.name).isEquals(); + } + + @Override + public int hashCode() { + return new HashCodeBuilder(17, 37).append(namespace).append(name).toHashCode(); + } + + @Nonnull + public static Matcher matcher() { + return new Matcher(); + } + + public static class Matcher { + private final Set namespaces = new HashSet<>(); + private final Set operations = new HashSet<>(); + private final Collection patterns = new HashSet<>(); + + private Matcher() {} + + @Nonnull + public Matcher add(@Nullable String namespace, @Nullable String... operations) { + if (operations == null || operations.length == 0) { + namespaces.add(Strings.requireNonBlank(namespace, "namespace must not be blank")); + } else { + for (String operation : operations) { + add(new OperationGroup(namespace, operation)); + } + } + return this; + } + + @Nonnull + public Matcher add(@Nonnull OperationGroup operation) { + operations.add(Objects.requireNonNull(operation, "operation must not be null")); + return this; + } + + @Nonnull + public Matcher add(@Nonnull Pattern pattern) { + patterns.add(Objects.requireNonNull(pattern, "pattern must not be null")); + return this; + } + + 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)) { + return true; + } + var str = operation.toString(); + for (Pattern pattern : patterns) { + if (pattern.matcher(str).matches()) { + return true; + } + } + return false; + } + } +} 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 new file mode 100644 index 000000000..8110eaf17 --- /dev/null +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/model/RequestShape.java @@ -0,0 +1,162 @@ +/* + * 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.model; + +import java.util.ArrayList; +import java.util.Collection; +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(@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; + } + + public String getId() { + return operationGroup.getName(); + } + + @Nullable + public String getDescription() { + return description; + } + + public String getHttpMethod() { + return Streams.sortedBy(httpMethods.stream(), m -> { + switch (m) { + case "PUT": + case "DELETE": + case "PATCH": + case "HEAD": + return 0; + case "POST": + return 1; + case "GET": + return 2; + default: + return 99; + } + }).findFirst().orElseThrow(); + } + + public void addSupportedHttpMethod(String method) { + httpMethods.add(method); + } + + public String getResponseType() { + return responseClassName(operationGroup); + } + + public boolean hasRequestBody() { + return !getBodyFields().isEmpty(); + } + + public void addQueryParam(Field field) { + queryParams.put(field.getName(), field); + addField(field); + } + + public Collection getQueryParams() { + return queryParams.values(); + } + + public boolean hasQueryParams() { + return !queryParams.isEmpty(); + } + + public void addHttpPath(HttpPath httpPath) { + httpPaths.add(httpPath); + } + + public Collection getHttpPaths() { + return httpPaths; + } + + public HttpPath getFirstHttpPath() { + return httpPaths.get(0); + } + + public boolean hasSinglePath() { + return httpPaths.size() == 1; + } + + public void addPathParam(Field field) { + pathParams.put(field.getName(), field); + addField(field); + } + + public Collection> getIndexedPathParams() { + var indexed = new ArrayList>(); + var i = 0; + for (var param : pathParams.values()) { + indexed.add(Pair.of(param.getName(), i++)); + } + return indexed; + } + + public Collection getPathParams() { + return pathParams.values(); + } + + private void addField(Field field) { + fields.put(field.getName(), field); + } + + @Override + public Collection getFields() { + return fields.values(); + } + + public boolean hasAnyRequiredFields() { + return fields.values().stream().anyMatch(Field::isRequired); + } + + @Nonnull + public static String requestClassName(@Nonnull OperationGroup operationGroup) { + Objects.requireNonNull(operationGroup, "operationGroup must not be null"); + return Strings.toPascalCase(operationGroup.getName()) + "Request"; + } + + @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/Shape.java b/java-codegen/src/main/java/org/opensearch/client/codegen/model/Shape.java new file mode 100644 index 000000000..7b8fcb166 --- /dev/null +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/model/Shape.java @@ -0,0 +1,68 @@ +/* + * 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.model; + +import java.util.HashSet; +import java.util.Set; +import java.util.TreeSet; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.opensearch.client.codegen.exceptions.RenderException; + +public abstract class Shape { + private static final Logger LOGGER = LogManager.getLogger(); + protected final Namespace parent; + private final String className; + private final Set referencedTypes = new HashSet<>(); + private final String typedefName; + + public Shape(Namespace parent, String className, String typedefName) { + this.parent = parent; + this.className = className; + this.typedefName = typedefName; + } + + public Type getType() { + return Type.builder().pkg(getPackageName()).name(className).build(); + } + + public Namespace getParent() { + return this.parent; + } + + public String getPackageName() { + return parent.getPackageName(); + } + + public String getClassName() { + return this.className; + } + + public String getTypedefName() { + return this.typedefName; + } + + public void render(ShapeRenderingContext ctx) throws RenderException { + var outFile = ctx.getOutputFile(className + ".java"); + LOGGER.info("Rendering: {}", outFile); + var renderer = ctx.getTemplateRenderer(b -> b.withFormatter(Type.class, t -> { + referencedTypes.add(t); + return t.toString(); + })); + renderer.renderJava(this, outFile); + } + + public Set getImports() { + var imports = new TreeSet(); + for (var type : referencedTypes) { + type.getRequiredImports(imports, getPackageName()); + } + return imports; + } +} diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/model/ShapeRenderingContext.java b/java-codegen/src/main/java/org/opensearch/client/codegen/model/ShapeRenderingContext.java new file mode 100644 index 000000000..0a99f2004 --- /dev/null +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/model/ShapeRenderingContext.java @@ -0,0 +1,127 @@ +/* + * 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.model; + +import java.io.File; +import java.util.Objects; +import java.util.function.Function; +import javax.annotation.Nonnull; +import org.opensearch.client.codegen.renderer.JavaCodeFormatter; +import org.opensearch.client.codegen.renderer.TemplateLoader; +import org.opensearch.client.codegen.renderer.TemplateRenderer; +import org.opensearch.client.codegen.renderer.TemplateValueFormatter; +import org.opensearch.client.codegen.utils.Strings; + +public final class ShapeRenderingContext implements AutoCloseable { + @Nonnull + private final File outputDir; + @Nonnull + private final TemplateLoader templateLoader; + @Nonnull + private final JavaCodeFormatter javaCodeFormatter; + private final boolean ownedJavaCodeFormatter; + + private ShapeRenderingContext(Builder builder) { + this.outputDir = Objects.requireNonNull(builder.outputDir, "outputDir must not be null"); + this.templateLoader = Objects.requireNonNull(builder.templateLoader, "templateLoader must not be null"); + this.javaCodeFormatter = Objects.requireNonNull(builder.javaCodeFormatter, "javaCodeFormatter must not be null"); + this.ownedJavaCodeFormatter = builder.ownedJavaCodeFormatter; + } + + @Nonnull + public ShapeRenderingContext forSubDir(@Nonnull String name) { + return builder().withOutputDir(new File(outputDir, Strings.requireNonBlank(name, "name must not be null"))) + .withTemplateLoader(templateLoader) + .withJavaCodeFormatter(javaCodeFormatter) + .build(); + } + + @Nonnull + public File getOutputFile(@Nonnull String name) { + outputDir.mkdirs(); + return new File(outputDir, Strings.requireNonBlank(name, "name must not be blank")); + } + + @Nonnull + public TemplateRenderer getTemplateRenderer( + @Nonnull Function valueFormatterConfigurator + ) { + return TemplateRenderer.builder() + .withValueFormatter(valueFormatterConfigurator) + .withTemplateLoader(templateLoader) + .withJavaCodeFormatter(javaCodeFormatter) + .build(); + } + + @Override + public void close() throws Exception { + if (ownedJavaCodeFormatter) { + javaCodeFormatter.close(); + } + } + + @Nonnull + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private File outputDir; + private TemplateLoader templateLoader; + private JavaCodeFormatter javaCodeFormatter; + private boolean ownedJavaCodeFormatter; + + @Nonnull + public Builder withOutputDir(@Nonnull File outputDir) { + this.outputDir = Objects.requireNonNull(outputDir, "outputDir must not be null"); + return this; + } + + @Nonnull + public Builder withTemplateLoader(@Nonnull TemplateLoader templateLoader) { + this.templateLoader = Objects.requireNonNull(templateLoader, "templateLoader must not be null"); + return this; + } + + @Nonnull + public Builder withTemplateLoader(@Nonnull Function configurator) { + return withTemplateLoader( + Objects.requireNonNull(configurator, "configurator must not be null").apply(TemplateLoader.builder()).build() + ); + } + + @Nonnull + public Builder withJavaCodeFormatter(@Nonnull JavaCodeFormatter javaCodeFormatter, boolean owned) { + if (this.ownedJavaCodeFormatter && this.javaCodeFormatter != null) { + this.javaCodeFormatter.close(); + } + this.javaCodeFormatter = Objects.requireNonNull(javaCodeFormatter, "javaCodeFormatter must not be null"); + this.ownedJavaCodeFormatter = owned; + return this; + } + + @Nonnull + public Builder withJavaCodeFormatter(@Nonnull JavaCodeFormatter javaCodeFormatter) { + return withJavaCodeFormatter(javaCodeFormatter, false); + } + + @Nonnull + public Builder withJavaCodeFormatter(@Nonnull Function configurator) { + return withJavaCodeFormatter( + Objects.requireNonNull(configurator, "configurator must not be null").apply(JavaCodeFormatter.builder()).build(), + true + ); + } + + @Nonnull + public ShapeRenderingContext build() { + return new ShapeRenderingContext(this); + } + } +} 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 new file mode 100644 index 000000000..7c7067ec7 --- /dev/null +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/model/SpecTransformer.java @@ -0,0 +1,435 @@ +/* + * 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.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.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +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.OpenApiMediaType; +import org.opensearch.client.codegen.openapi.OpenApiOperation; +import org.opensearch.client.codegen.openapi.OpenApiParameter; +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.OpenApiSchemaFormat; +import org.opensearch.client.codegen.openapi.OpenApiSchemaType; +import org.opensearch.client.codegen.openapi.OpenApiSpecification; +import org.opensearch.client.codegen.utils.Lists; + +public class SpecTransformer { + private static final Logger LOGGER = LogManager.getLogger(); + @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(@Nonnull OperationGroup.Matcher matcher) { + this.matcher = Objects.requireNonNull(matcher, "matcher must not be null"); + } + + @Nonnull + public Namespace getRoot() { + return root; + } + + public void visit(@Nonnull OpenApiSpecification spec) { + Objects.requireNonNull(spec, "spec must not be null"); + + LOGGER.info("Visiting Specification: {}", spec); + + var groupedOperations = new HashMap>(); + + 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(@Nonnull OperationGroup group, @Nonnull List variants) { + LOGGER.info("Visiting Operation Group: {} [{} variants]", group, variants.size()); + + var parent = root.child(group.getNamespace().orElse(null)); + + var requestShape = visit(parent, group, variants); + parent.addOperation(requestShape); + + 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.ANONYMOUS_OBJECT); + + visit(parent, requestShape.getResponseType(), group + ".Response", responseSchema); + } + + @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 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().toString().toUpperCase()); + + var httpPathStr = variant.getHttpPath(); + if (!seenHttpPaths.add(httpPathStr)) { + continue; + } + + variant.getAllRelevantParameters(In.Path).forEach(parameter -> { + var paramName = parameter.getName().orElseThrow(); + if (!allPathParams.containsKey(paramName)) { + allPathParams.put(paramName, visit(parameter)); + } + }); + + var httpPath = HttpPath.from(httpPathStr, variant, allPathParams); + + (httpPath.getDeprecation() == null ? canonicalPaths : deprecatedPaths).put(httpPath.getParamNameSet(), httpPath); + + if (requiredPathParams != null) { + requiredPathParams.retainAll(httpPath.getParamNameSet()); + } else { + requiredPathParams = new HashSet<>(httpPath.getParamNameSet()); + } + } + + Stream.of( + canonicalPaths.values().stream(), + deprecatedPaths.entrySet().stream().filter(p -> !canonicalPaths.containsKey(p.getKey())).map(Map.Entry::getValue) + ).flatMap(Function.identity()).sorted((p1, p2) -> { + var params1 = p1.getParams(); + var p1Size = params1.size(); + var params2 = p2.getParams(); + var p2Size = params2.size(); + var len = Math.max(p1Size, p2Size); + + for (int i = 0; i < len; i++) { + 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; + } + } + + return 0; + }).forEachOrdered(shape::addHttpPath); + + for (var entry : allPathParams.entrySet()) { + entry.getValue().setRequired(requiredPathParams.contains(entry.getKey())); + shape.addPathParam(entry.getValue()); + } + + variants.stream() + .flatMap(v -> v.getAllRelevantParameters(In.Query).stream()) + .filter(p -> !p.isGlobal()) + .map(this::visit) + .forEachOrdered(shape::addQueryParam); + + 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.ANONYMOUS_OBJECT); + + visitInto(bodySchema, shape); + + if (shape.getExtendsType() == null) { + shape.setExtendsType(Types.Client.OpenSearch._Types.RequestBase); + } + + return shape; + } + + private Field visit(OpenApiParameter parameter) { + LOGGER.info("Visiting Parameter: {}", parameter); + return new Field( + parameter.getName().orElseThrow(), + mapType(parameter.getSchema().orElseThrow()), + parameter.getRequired(), + parameter.getDescription().orElse(null), + parameter.getDeprecation().orElse(null) + ); + } + + private void visit(OpenApiSchema schema) { + var namespace = schema.getNamespace().orElseThrow(); + var name = schema.getName().orElseThrow(); + visit(root.child(namespace), name, namespace + "." + name, schema); + } + + private void visit(Namespace parent, String className, String typedefName, OpenApiSchema schema) { + if (!visitedSchemas.add(schema)) { + return; + } + + LOGGER.info("Visiting Schema: {}", schema); + + Shape shape; + + if (schema.isArray()) { + shape = new ArrayShape(parent, className, mapType(schema), typedefName); + } else if (schema.isObject() || schema.hasAllOf() || schema.equals(OpenApiSchema.ANONYMOUS_OBJECT)) { + var objShape = new ObjectShape(parent, className, typedefName); + visitInto(schema, objShape); + shape = objShape; + } else if (schema.isString() && schema.hasEnums()) { + shape = new EnumShape(parent, className, Lists.map(schema.getEnums().orElseThrow(), EnumShape.Variant::new), typedefName); + } else if (schema.hasOneOf()) { + var taggedUnion = new TaggedUnionShape(parent, className, typedefName); + schema.getOneOf().orElseThrow().forEach(s -> { + var title = s.getTitle() + .orElseThrow(() -> new IllegalStateException("oneOf variant [" + s.getPointer() + "] is missing a `title` tag")); + taggedUnion.addVariant(title, mapType(s)); + }); + shape = taggedUnion; + } else { + throw new NotImplementedException("Unsupported schema: " + schema); + } + + parent.addShape(shape); + } + + private void visitInto(OpenApiSchema schema, ObjectShape shape) { + var allOf = schema.getAllOf(); + if (allOf.isPresent()) { + shape.setExtendsType(mapType(allOf.get().get(0))); + schema = allOf.get().get(1); + } + + 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), null)) + ) + ); + + var additionalProperties = schema.getAdditionalProperties(); + if (additionalProperties.isPresent()) { + var valueType = mapType(additionalProperties.get()); + shape.setAdditionalPropertiesField( + new Field( + "metadata", + Types.Java.Util.Map(Types.Java.Lang.String, valueType), + false, + additionalProperties.get().getDescription().orElse(null), + null + ) + ); + } + } + + private Type mapType(OpenApiSchema schema) { + return mapType(schema, false); + } + + private Type mapType(OpenApiSchema schema, boolean boxed) { + var type = schemaToType.get(schema); + if (type == null) { + type = mapTypeInner(schema); + schemaToType.put(schema, type); + } + return boxed ? type.getBoxed() : type; + } + + private Type mapTypeInner(OpenApiSchema schema) { + if (schema.has$ref()) { + schema = schema.resolve(); + + if (!shouldKeepRef(schema)) { + return mapType(schema); + } + + visit(schema); + + return Type.builder() + .pkg(Types.Client.OpenSearch.PACKAGE + "." + schema.getNamespace().orElseThrow()) + .name(schema.getName().orElseThrow()) + .isEnum(schema.hasEnums()) + .build(); + } + + var oneOf = schema.getOneOf(); + if (oneOf.isPresent()) { + return mapOneOf(oneOf.get()); + } + + var allOf = schema.getAllOf(); + if (allOf.isPresent()) { + return mapAllOf(allOf.get()); + } + + var type = schema.getType(); + + if (type.isEmpty()) { + return Types.Client.Json.JsonData; + } + + switch (type.get()) { + case Object: + return mapObject(schema); + case Array: + return mapArray(schema); + case String: + if (schema.getPattern().map("^([0-9]+)(?:d|h|m|s|ms|micros|nanos)$"::equals).orElse(false)) + return Types.Client.OpenSearch._Types.Time; + return Types.Java.Lang.String; + case Boolean: + return Types.Primitive.Boolean; + case Integer: + case Number: + return mapNumber(schema); + } + + throw new UnsupportedOperationException("Can not get type name for: " + type); + } + + private Type mapOneOf(List oneOf) { + if (oneOf.size() == 2) { + var first = oneOf.get(0); + var second = oneOf.get(1); + + if (isOneOfSingleAndArray(oneOf)) { + return mapType(second); + } + + if ((first.isString() && (second.isString() || second.isNumber())) || (first.isNumber() && second.isString())) { + return Types.Java.Lang.String; + } + + if (first.isBoolean() && second.isString()) { + return Types.Primitive.Boolean; + } + } + + throw new UnsupportedOperationException("Can not get type name for oneOf: " + oneOf); + } + + private Type mapAllOf(List allOf) { + if (allOf.size() == 1) { + return mapType(allOf.get(0)); + } + + throw new UnsupportedOperationException("Can not get type name for allOf: " + allOf); + } + + private Type mapObject(OpenApiSchema schema) { + var values = schema.getAdditionalProperties().map(s -> mapType(s, true)).orElse(Types.Client.Json.JsonData); + return Types.Java.Util.Map(Types.Java.Lang.String, values); + } + + private Type mapArray(OpenApiSchema schema) { + var items = schema.getItems().map(i -> mapType(i, true)).orElse(Types.Java.Lang.String); + return Types.Java.Util.List(items); + } + + private Type mapNumber(OpenApiSchema schema) { + var format = schema.getFormat().orElse(OpenApiSchemaFormat.Int32); + switch (format) { + case Int32: + return Types.Primitive.Int; + case Int64: + return Types.Primitive.Long; + case Float: + return Types.Primitive.Float; + case Double: + return Types.Primitive.Double; + default: + throw new UnsupportedOperationException("Can not get type name for integer/number with format: " + format); + } + } + + private boolean shouldKeepRef(OpenApiSchema schema) { + if (schema.isNumber()) { + return false; + } + if (schema.isString() && schema.getEnums().isEmpty()) { + return false; + } + if (schema.getOneOf().isPresent()) { + return schema.getOneOf().orElseThrow().stream().allMatch(s -> s.getTitle().isPresent()); + } + if (schema.getAllOf().isPresent()) { + var types = schema.determineTypes(); + return types.size() == 1 && types.iterator().next().equals(OpenApiSchemaType.Object); + } + return true; + } + + private static boolean isOneOfSingleAndArray(List oneOf) { + if (oneOf.size() != 2) { + return false; + } + var second = oneOf.get(1); + if (!second.isArray()) { + return false; + } + var first = oneOf.get(0); + var items = second.getItems().orElseThrow(); + return first.getType().equals(items.getType()) && first.get$ref().equals(items.get$ref()); + } +} diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/model/TaggedUnionShape.java b/java-codegen/src/main/java/org/opensearch/client/codegen/model/TaggedUnionShape.java new file mode 100644 index 000000000..2eb78fcc5 --- /dev/null +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/model/TaggedUnionShape.java @@ -0,0 +1,65 @@ +/* + * 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.model; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import org.opensearch.client.codegen.utils.Lists; + +public class TaggedUnionShape extends ObjectShape { + private final List variants = new ArrayList<>(); + + public TaggedUnionShape(Namespace parent, String className, String typedefName) { + super(parent, className, typedefName); + } + + public void addVariant(String name, Type type) { + variants.add(new Variant(name, type.getBoxed())); + } + + public Collection getVariants() { + return variants; + } + + public Collection getPrimitiveVariants() { + return Lists.filter(variants, v -> v.getType().isPrimitive()); + } + + @Override + public Collection getAnnotations() { + return List.of(Types.Client.Json.JsonpDeserializable); + } + + @Override + public Collection getImplementsTypes() { + return List.of( + Types.Client.Util.TaggedUnion(getType().getNestedType("Kind"), Types.Java.Lang.Object), + Types.Client.Json.JsonpSerializable + ); + } + + public static class Variant { + private final String name; + private final Type type; + + protected Variant(String name, Type type) { + this.name = name; + this.type = type; + } + + public String getName() { + return name; + } + + public Type getType() { + return type; + } + } +} diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/model/Type.java b/java-codegen/src/main/java/org/opensearch/client/codegen/model/Type.java new file mode 100644 index 000000000..623b8988d --- /dev/null +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/model/Type.java @@ -0,0 +1,230 @@ +/* + * 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.model; + +import static org.opensearch.client.codegen.model.Types.Client; +import static org.opensearch.client.codegen.model.Types.Java; + +import com.samskivert.mustache.Mustache; +import java.util.Arrays; +import java.util.Set; +import java.util.stream.Collectors; +import org.opensearch.client.codegen.renderer.lambdas.TypeQueryParamifyLambda; +import org.opensearch.client.codegen.renderer.lambdas.TypeSerializerLambda; + +public class Type { + private static final Set PRIMITIVES = Set.of( + "String", + "boolean", + "Boolean", + "char", + "Character", + "byte", + "Byte", + "short", + "Short", + "int", + "Integer", + "long", + "Long", + "float", + "Float", + "double", + "Double" + ); + + public static Builder builder() { + return new Builder(); + } + + private final String pkg; + private final String name; + private final Type[] genericArgs; + private final boolean isEnum; + + private Type(Builder builder) { + this.pkg = builder.pkg; + this.name = builder.name; + this.genericArgs = builder.genericArgs; + this.isEnum = builder.isEnum; + } + + public Builder toBuilder() { + return new Builder().pkg(pkg).name(name).genericArgs(genericArgs).isEnum(isEnum); + } + + @Override + public String toString() { + String str = name; + if (genericArgs != null && genericArgs.length > 0) { + str += "<"; + str += Arrays.stream(genericArgs).map(Type::toString).collect(Collectors.joining(", ")); + str += ">"; + } + return str; + } + + public Type getBoxed() { + switch (name) { + case "char": + return Java.Lang.Character; + case "boolean": + return Java.Lang.Boolean; + case "byte": + return Java.Lang.Byte; + case "short": + return Java.Lang.Short; + case "int": + return Java.Lang.Integer; + case "long": + return Java.Lang.Long; + case "float": + return Java.Lang.Float; + case "double": + return Java.Lang.Double; + default: + return this; + } + } + + public boolean isMap() { + return "Map".equals(name); + } + + public Type getMapEntryType() { + if (!isMap()) return null; + + return Java.Util.MapEntry(this.genericArgs[0], this.genericArgs[1]); + } + + public Type getMapKeyType() { + if (!isMap()) return null; + + return this.genericArgs[0]; + } + + public Type getMapValueType() { + if (!isMap()) return null; + + return this.genericArgs[1]; + } + + public boolean isList() { + return "List".equals(name); + } + + public Type getListValueType() { + if (!isList()) return null; + + return this.genericArgs[0]; + } + + public boolean isListOrMap() { + return isList() || isMap(); + } + + public boolean isString() { + return "String".equals(name); + } + + public boolean isPrimitive() { + return PRIMITIVES.contains(name); + } + + public boolean isEnum() { + return isEnum; + } + + public boolean isTime() { + return "Time".equals(name); + } + + public boolean isBuiltIn() { + return isListOrMap() || isPrimitive() || "JsonData".equals(name); + } + + public boolean hasBuilder() { + return !isBuiltIn() && !isEnum(); + } + + public Type getBuilderType() { + if (!hasBuilder()) return null; + + return getNestedType("Builder"); + } + + public Type getBuilderFnType() { + if (!hasBuilder()) return null; + + return Java.Util.Function.Function(getBuilderType(), Client.Util.ObjectBuilder(this)); + } + + public Type getNestedType(String name) { + return builder().pkg(pkg).name(this.name + "." + name).build(); + } + + public Mustache.Lambda serializer() { + return new TypeSerializerLambda(this, false); + } + + public Mustache.Lambda directSerializer() { + return new TypeSerializerLambda(this, true); + } + + public void getRequiredImports(Set imports, String currentPkg) { + if (pkg != null && !pkg.equals(Java.Lang.PACKAGE) && !pkg.equals(currentPkg)) { + var dotIdx = name.indexOf('.'); + imports.add(pkg + '.' + (dotIdx > 0 ? name.substring(0, dotIdx) : name)); + } + if (genericArgs != null) { + for (Type arg : genericArgs) { + arg.getRequiredImports(imports, currentPkg); + } + } + } + + public Type withGenericArgs(Type... genericArgs) { + return toBuilder().genericArgs(genericArgs).build(); + } + + public Mustache.Lambda queryParamify() { + return new TypeQueryParamifyLambda(this); + } + + public static final class Builder { + private String pkg; + private String name; + private Type[] genericArgs; + private boolean isEnum; + + public Builder pkg(String pkg) { + this.pkg = pkg; + return this; + } + + public Builder name(String name) { + this.name = name; + return this; + } + + public Builder genericArgs(Type... genericArgs) { + this.genericArgs = genericArgs; + return this; + } + + public Builder isEnum(boolean isEnum) { + this.isEnum = isEnum; + return this; + } + + public Type build() { + return new Type(this); + } + } +} diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/model/Types.java b/java-codegen/src/main/java/org/opensearch/client/codegen/model/Types.java new file mode 100644 index 000000000..34e39f66b --- /dev/null +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/model/Types.java @@ -0,0 +1,186 @@ +package org.opensearch.client.codegen.model; + +import java.lang.reflect.Modifier; +import java.util.HashMap; +import java.util.Map; + +public final class Types { + public static final Map TYPES_MAP = asMap(Types.class); + + private static Map asMap(Class clazz) { + var map = new HashMap(); + + for (var subClazz : clazz.getDeclaredClasses()) { + if ((subClazz.getModifiers() & Modifier.STATIC) == 0) { + continue; + } + map.put(subClazz.getSimpleName(), asMap(subClazz)); + } + + for (var field : clazz.getDeclaredFields()) { + if ((field.getModifiers() & Modifier.STATIC) == 0 || !Type.class.isAssignableFrom(field.getType())) { + continue; + } + try { + map.put(field.getName(), field.get(null)); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + return map; + } + + public static final class Primitive { + public static final Type Boolean = Type.builder().name("boolean").build(); + public static final Type Int = Type.builder().name("int").build(); + public static final Type Long = Type.builder().name("long").build(); + public static final Type Float = Type.builder().name("float").build(); + public static final Type Double = Type.builder().name("double").build(); + } + + public static final class Java { + public static final String PACKAGE = "java"; + + public static final class Io { + public static final String PACKAGE = Java.PACKAGE + ".io"; + public static final Type IOException = Type.builder().pkg(PACKAGE).name("IOException").build(); + } + + public static final class Lang { + public static final String PACKAGE = Java.PACKAGE + ".lang"; + public static final Type String = Type.builder().pkg(PACKAGE).name("String").build(); + public static final Type Character = Type.builder().pkg(PACKAGE).name("Character").build(); + public static final Type Boolean = Type.builder().pkg(PACKAGE).name("Boolean").build(); + public static final Type Byte = Type.builder().pkg(PACKAGE).name("Byte").build(); + public static final Type Short = Type.builder().pkg(PACKAGE).name("Short").build(); + public static final Type Integer = Type.builder().pkg(PACKAGE).name("Integer").build(); + public static final Type Long = Type.builder().pkg(PACKAGE).name("Long").build(); + public static final Type Float = Type.builder().pkg(PACKAGE).name("Float").build(); + public static final Type Double = Type.builder().pkg(PACKAGE).name("Double").build(); + public static final Type Object = Type.builder().pkg(PACKAGE).name("Object").build(); + } + + public static final class Util { + public static final String PACKAGE = Java.PACKAGE + ".util"; + public static final Type HashMap = Type.builder().pkg(PACKAGE).name("HashMap").build(); + + public static Type Map(Type keyType, Type valueType) { + return Map.withGenericArgs(keyType, valueType); + } + + public static final Type Map = Type.builder().pkg(PACKAGE).name("Map").build(); + + public static Type MapEntry(Type keyType, Type valueType) { + return Type.builder().pkg(PACKAGE).name("Map.Entry").genericArgs(keyType, valueType).build(); + } + + public static Type List(Type valueType) { + return Type.builder().pkg(PACKAGE).name("List").genericArgs(valueType).build(); + } + + public static final class Concurrent { + public static final String PACKAGE = Util.PACKAGE + ".concurrent"; + public static final Type CompletableFuture = Type.builder().pkg(PACKAGE).name("CompletableFuture").build(); + } + + public static final class Function { + public static final String PACKAGE = Util.PACKAGE + ".function"; + + public static Type Function(Type argType, Type returnType) { + return Type.builder().pkg(PACKAGE).name("Function").genericArgs(argType, returnType).build(); + } + } + + public static final class Stream { + public static final String PACKAGE = Util.PACKAGE + ".stream"; + public static final Type Collectors = Type.builder().pkg(PACKAGE).name("Collectors").build(); + } + } + } + + public static final class Javax { + public static final String PACKAGE = "javax"; + + public static final class Annotation { + public static final String PACKAGE = Javax.PACKAGE + ".annotation"; + public static final Type Nullable = Type.builder().pkg(PACKAGE).name("Nullable").build(); + } + } + + public static final class Client { + public static final String PACKAGE = "org.opensearch.client"; + + public static final Type ApiClient = Type.builder().pkg(PACKAGE).name("ApiClient").build(); + + public static final class Json { + public static final String PACKAGE = Client.PACKAGE + ".json"; + public static final Type JsonData = Type.builder().pkg(PACKAGE).name("JsonData").build(); + public static final Type JsonpDeserializable = Type.builder().pkg(PACKAGE).name("JsonpDeserializable").build(); + public static final Type JsonpDeserializer = Type.builder().pkg(PACKAGE).name("JsonpDeserializer").build(); + public static final Type JsonEnum = Type.builder().pkg(PACKAGE).name("JsonEnum").build(); + public static final Type JsonpMapper = Type.builder().pkg(PACKAGE).name("JsonpMapper").build(); + public static final Type JsonpSerializable = Type.builder().pkg(PACKAGE).name("JsonpSerializable").build(); + public static final Type ObjectBuilderDeserializer = Type.builder().pkg(PACKAGE).name("ObjectBuilderDeserializer").build(); + public static final Type ObjectDeserializer = Type.builder().pkg(PACKAGE).name("ObjectDeserializer").build(); + public static final Type UnionDeserializer = Type.builder().pkg(PACKAGE).name("UnionDeserializer").build(); + } + + public static final class OpenSearch { + public static final String PACKAGE = Client.PACKAGE + ".opensearch"; + + public static final class _Types { + public static final String PACKAGE = OpenSearch.PACKAGE + "._types"; + public static final Type ErrorResponse = Type.builder().pkg(PACKAGE).name("ErrorResponse").build(); + public static final Type OpenSearchException = Type.builder().pkg(PACKAGE).name("OpenSearchException").build(); + public static final Type RequestBase = Type.builder().pkg(PACKAGE).name("RequestBase").build(); + public static final Type Time = Type.builder().pkg(PACKAGE).name("Time").build(); + } + } + + public static final class Transport { + public static final String PACKAGE = Client.PACKAGE + ".transport"; + public static final Type Endpoint = Type.builder().pkg(PACKAGE).name("Endpoint").build(); + public static final Type JsonEndpoint = Type.builder().pkg(PACKAGE).name("JsonEndpoint").build(); + public static final Type OpenSearchTransport = Type.builder().pkg(PACKAGE).name("OpenSearchTransport").build(); + public static final Type TransportOptions = Type.builder().pkg(PACKAGE).name("TransportOptions").build(); + + public static final class Endpoints { + public static final String PACKAGE = Transport.PACKAGE + ".endpoints"; + public static final Type SimpleEndpoint = Type.builder().pkg(PACKAGE).name("SimpleEndpoint").build(); + } + } + + public static final class Util { + public static final String PACKAGE = Client.PACKAGE + ".util"; + public static final Type ApiTypeHelper = Type.builder().pkg(PACKAGE).name("ApiTypeHelper").build(); + + public static Type ObjectBuilder(Type type) { + return ObjectBuilder.withGenericArgs(type); + } + + public static final Type ObjectBuilder = Type.builder().pkg(PACKAGE).name("ObjectBuilder").build(); + public static final Type ObjectBuilderBase = Type.builder().pkg(PACKAGE).name("ObjectBuilderBase").build(); + + public static Type TaggedUnion(Type tagType, Type baseType) { + return TaggedUnion.withGenericArgs(tagType, baseType); + } + + public static final Type TaggedUnion = Type.builder().pkg(PACKAGE).name("TaggedUnion").build(); + public static final Type TaggedUnionUtils = Type.builder().pkg(PACKAGE).name("TaggedUnionUtils").build(); + } + } + + public static final class Jakarta { + public static final String PACKAGE = "jakarta"; + + public static final class Json { + public static final String PACKAGE = Jakarta.PACKAGE + ".json"; + + public static final class Stream { + public static final String PACKAGE = Json.PACKAGE + ".stream"; + public static final Type JsonGenerator = Type.builder().pkg(PACKAGE).name("JsonGenerator").build(); + } + } + } +} 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 000000000..0659e5e72 --- /dev/null +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/HttpMethod.java @@ -0,0 +1,41 @@ +package org.opensearch.client.codegen.openapi; + +import io.swagger.v3.oas.models.PathItem; +import java.util.Map; +import java.util.Objects; +import javax.annotation.Nonnull; +import org.opensearch.client.codegen.utils.Maps; +import org.opensearch.client.codegen.utils.Strings; + +public enum HttpMethod { + Post, + Get, + Put, + Patch, + Delete, + Head, + Options, + Trace; + + private static final Map VALUES = Maps.createLookupOf(values(), HttpMethod::toString); + + @Nonnull + public static HttpMethod from(@Nonnull String httpMethod) { + var value = VALUES.get(Strings.requireNonBlank(httpMethod, "value must not be blank")); + if (value == null) { + throw new IllegalArgumentException("Unknown HTTP method: " + httpMethod); + } + return value; + } + + @Nonnull + public static HttpMethod from(@Nonnull PathItem.HttpMethod httpMethod) { + Objects.requireNonNull(httpMethod, "httpMethod must not be null"); + return from(httpMethod.name().toLowerCase()); + } + + @Override + public String toString() { + return name().toLowerCase(); + } +} 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 000000000..709d0f1d5 --- /dev/null +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/HttpStatusCode.java @@ -0,0 +1,42 @@ +/* + * 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 org.opensearch.client.codegen.utils.Maps; +import org.opensearch.client.codegen.utils.Strings; + +public enum HttpStatusCode { + Ok("200"), + BadRequest("400"), + Forbidden("403"), + InternalServerError("500"), + NotImplemented("501"); + + private static final Map VALUES = Maps.createLookupOf(values(), HttpStatusCode::toString); + + 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 = VALUES.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 000000000..9426b4bfa --- /dev/null +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/In.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 java.util.Map; +import javax.annotation.Nonnull; +import org.opensearch.client.codegen.utils.Maps; +import org.opensearch.client.codegen.utils.Strings; + +public enum In { + Query, + Path; + + private static final Map VALUES = Maps.createLookupOf(values(), In::toString); + + @Nonnull + public static In from(@Nonnull String in) { + var value = VALUES.get(Strings.requireNonBlank(in, "in must not be blank")); + if (value == null) { + throw new IllegalArgumentException("Unknown in: " + in); + } + return value; + } + + @Override + public String toString() { + return name().toLowerCase(); + } +} 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 new file mode 100644 index 000000000..d20ee4ec7 --- /dev/null +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/JsonPointer.java @@ -0,0 +1,138 @@ +/* + * 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.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 WeakHashMap> PARENT_CACHE = new WeakHashMap<>(); + @Nonnull + public static final JsonPointer ROOT = new JsonPointer(new String[0]); + + @Nonnull + private final String[] keys; + + @Nonnull + public static JsonPointer parse(@Nonnull String pointer) { + Objects.requireNonNull(pointer, "pointer must not be null"); + + if (pointer.isEmpty()) { + return ROOT; + } + + if (!pointer.startsWith("/")) { + throw new IllegalArgumentException("Invalid JSON pointer: \"" + pointer + "\""); + } + + var parts = pointer.substring(1).split("/"); + + return new JsonPointer(Arrays.stream(parts).map(JsonPointer::unescape).toArray(String[]::new)); + } + + @Nonnull + public static JsonPointer of(@Nonnull String... keys) { + return new JsonPointer(keys); + } + + 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(); + } + return Optional.of(keys[keys.length - 1]); + } + + @Nonnull + public Optional getParent() { + 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())); + } + + return parent; + } + + @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 boolean isDirectChildOf(@Nonnull JsonPointer other) { + Objects.requireNonNull(other, "other must not be null"); + return getParent().map(other::equals).orElse(false); + } + + @Override + public String toString() { + 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 (o == null || getClass() != o.getClass()) { + return false; + } + + JsonPointer that = (JsonPointer) o; + + return new EqualsBuilder().append(keys, that.keys).isEquals(); + } + + @Override + public int hashCode() { + return new HashCodeBuilder(17, 37).append(keys).toHashCode(); + } + + @Nonnull + private static String escape(@Nonnull String key) { + Objects.requireNonNull(key, "key must not be null"); + return key.replace("~", "~0").replace("/", "~1"); + } + + @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 new file mode 100644 index 000000000..398cb8e04 --- /dev/null +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/MimeType.java @@ -0,0 +1,41 @@ +/* + * 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 javax.annotation.Nonnull; +import org.opensearch.client.codegen.utils.Maps; +import org.opensearch.client.codegen.utils.Strings; + +public enum MimeType { + Json("application/json"), + NdJson("application/x-ndjson"); + + private static final Map VALUES = Maps.createLookupOf(values(), MimeType::toString); + + private final String mimeType; + + MimeType(String mimeType) { + this.mimeType = mimeType; + } + + @Override + public String toString() { + return this.mimeType; + } + + @Nonnull + public static MimeType from(@Nonnull String mimeType) { + var value = VALUES.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/OpenApiComponents.java b/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiComponents.java new file mode 100644 index 000000000..606f3b750 --- /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 new file mode 100644 index 000000000..89cabc2ee --- /dev/null +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiContent.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.media.Content; +import javax.annotation.Nonnull; + +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 000000000..30653b72a --- /dev/null +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiElement.java @@ -0,0 +1,149 @@ +/* + * 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.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.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; +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 OpenApiSpecification specification; + @Nonnull + private final JsonPointer pointer; + + OpenApiElement(@Nullable OpenApiElement parent, @Nonnull JsonPointer pointer) { + this.specification = ifNonnull(parent, p -> p.getSpecification().orElse(null)); + this.pointer = Objects.requireNonNull(pointer, "pointer must not be null"); + if (this.specification != null) { + this.specification.addElement(this.pointer, self()); + } + } + + @Nonnull + @SuppressWarnings("unchecked") + protected TSelf self() { + return (TSelf) this; + } + + @Nonnull + protected Optional getSpecification() { + return Optional.ofNullable(specification); + } + + @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.map(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(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + + if (o == null || getClass() != o.getClass()) { + return false; + } + + OpenApiElement that = (OpenApiElement) o; + + return new EqualsBuilder().append(specification, that.specification).append(pointer, that.pointer).isEquals(); + } + + @Override + public int hashCode() { + return new HashCodeBuilder(17, 37).append(specification).append(pointer).toHashCode(); + } +} 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 000000000..f02f5b27a --- /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/OpenApiMediaType.java b/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiMediaType.java new file mode 100644 index 000000000..68ae95489 --- /dev/null +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiMediaType.java @@ -0,0 +1,31 @@ +/* + * 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.MediaType; +import java.util.Objects; +import java.util.Optional; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +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 Optional.ofNullable(schema); + } +} 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 new file mode 100644 index 000000000..34b2f4be9 --- /dev/null +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiOperation.java @@ -0,0 +1,119 @@ +/* + * 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 io.swagger.v3.oas.models.Operation; +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 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(@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 parentPath.getHttpPath(); + } + + @Nonnull + public HttpMethod getHttpMethod() { + return httpMethod; + } + + @Nonnull + public OperationGroup getOperationGroup() { + return operationGroup; + } + + @Nonnull + public Optional getDescription() { + return Optional.ofNullable(description); + } + + @Nonnull + public Optional getRequestBody() { + return Optional.ofNullable(requestBody); + } + + @Nonnull + public Optional getResponses() { + return Optional.ofNullable(responses); + } + + @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()); + } + + @Nonnull + public Optional getVersionAdded() { + return Optional.ofNullable(versionAdded); + } + + @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 000000000..a118b6a57 --- /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 new file mode 100644 index 000000000..442873eab --- /dev/null +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiParameter.java @@ -0,0 +1,91 @@ +/* + * 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 io.swagger.v3.oas.models.parameters.Parameter; +import java.util.Optional; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import org.opensearch.client.codegen.model.Deprecation; +import org.opensearch.client.codegen.utils.Maps; + +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; + @Nullable + private final Boolean isDeprecated; + @Nullable + private final String versionDeprecated; + @Nullable + private final String deprecationMessage; + @Nullable + private final Boolean isGlobal; + + 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); + this.isDeprecated = parameter.getDeprecated(); + var extensions = parameter.getExtensions(); + this.versionDeprecated = Maps.tryGet(extensions, "x-version-deprecated").map(String::valueOf).orElse(null); + this.deprecationMessage = Maps.tryGet(extensions, "x-deprecation-message").map(String::valueOf).orElse(null); + this.isGlobal = (Boolean) Maps.tryGet(extensions, "x-global").orElse(null); + } + + @Nonnull + public Optional getName() { + return Optional.ofNullable(name); + } + + @Nonnull + public Optional getDescription() { + return Optional.ofNullable(description); + } + + @Nonnull + public Optional getIn() { + return Optional.ofNullable(in); + } + + public boolean getRequired() { + return isRequired != null && isRequired; + } + + @Nonnull + public Optional getSchema() { + return Optional.ofNullable(schema); + } + + public boolean isGlobal() { + return isGlobal != null && isGlobal; + } + + public boolean isDeprecated() { + return isDeprecated != null && isDeprecated; + } + + @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/OpenApiPath.java b/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiPath.java new file mode 100644 index 000000000..b8957df56 --- /dev/null +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiPath.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.openapi; + +import io.swagger.v3.oas.models.PathItem; +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; + @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; + } + + @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/OpenApiRefElement.java b/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiRefElement.java new file mode 100644 index 000000000..55f682962 --- /dev/null +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiRefElement.java @@ -0,0 +1,114 @@ +/* + * 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")); + } + + 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 String toString() { + return new ToStringBuilder(this).append("pointer", pointer).append("relativeLocation", relativeLocation).toString(); + } + + @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(); + } + } +} 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 new file mode 100644 index 000000000..e9f5b649d --- /dev/null +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiRequestBody.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.parameters.RequestBody; +import javax.annotation.Nonnull; + +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 000000000..bab510a29 --- /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 000000000..732200ad7 --- /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 new file mode 100644 index 000000000..3ad80242c --- /dev/null +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiSchema.java @@ -0,0 +1,238 @@ +/* + * 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 io.swagger.v3.oas.models.media.ObjectSchema; +import io.swagger.v3.oas.models.media.Schema; +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.Collectors; +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; + @Nullable + private final String title; + @Nullable + private final String pattern; + + 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).replaceAll("\\._common", "").replaceAll("^_common", "_types"); + name = key.substring(colonIdx + 1); + } else { + namespace = null; + name = key; + } + } else { + name = null; + namespace = null; + } + + description = schema.getDescription(); + + 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); + + format = ifNonnull(schema.getFormat(), OpenApiSchemaFormat::from); + + allOf = children("allOf", schema.getAllOf(), OpenApiSchema::new); + oneOf = children("oneOf", schema.getOneOf(), OpenApiSchema::new); + + enums = ifNonnull(schema.getEnum(), e -> Lists.map(e, String::valueOf)); + + items = child("items", schema.getItems(), OpenApiSchema::new); + + additionalProperties = child("additionalProperties", (Schema) schema.getAdditionalProperties(), OpenApiSchema::new); + + properties = children("properties", schema.getProperties(), OpenApiSchema::new); + required = ifNonnull(schema.getRequired(), HashSet::new); + + title = schema.getTitle(); + + pattern = schema.getPattern(); + } + + @Nonnull + public Optional getName() { + return Optional.ofNullable(name); + } + + @Nonnull + public Optional getNamespace() { + return Optional.ofNullable(namespace); + } + + @Nonnull + public Optional getDescription() { + return Optional.ofNullable(description); + } + + @Nonnull + public Optional getType() { + return Optional.ofNullable(type); + } + + @Nonnull + public Optional getFormat() { + return Optional.ofNullable(format); + } + + public boolean is(@Nonnull OpenApiSchemaType type) { + Objects.requireNonNull(type, "type must not be null"); + return getType().map(type::equals).orElse(false); + } + + public boolean isArray() { + return is(OpenApiSchemaType.Array); + } + + public boolean isBoolean() { + return is(OpenApiSchemaType.Boolean); + } + + public boolean isNumber() { + return is(OpenApiSchemaType.Number); + } + + public boolean isObject() { + return is(OpenApiSchemaType.Object); + } + + public boolean isString() { + return is(OpenApiSchemaType.String); + } + + public boolean hasAllOf() { + return allOf != null && !allOf.isEmpty(); + } + + @Nonnull + public Optional> getAllOf() { + return Lists.unmodifiableOpt(allOf); + } + + public boolean hasOneOf() { + return oneOf != null && !oneOf.isEmpty(); + } + + @Nonnull + public Optional> getOneOf() { + return Lists.unmodifiableOpt(oneOf); + } + + public boolean hasEnums() { + return enums != null && !enums.isEmpty(); + } + + @Nonnull + public Optional> getEnums() { + return Lists.unmodifiableOpt(enums); + } + + @Nonnull + public Optional getItems() { + return Optional.ofNullable(items); + } + + @Nonnull + public Optional getAdditionalProperties() { + return Optional.ofNullable(additionalProperties); + } + + @Nonnull + public Optional> getProperties() { + return Maps.unmodifiableOpt(properties); + } + + @Nonnull + public Optional> getRequired() { + return Sets.unmodifiableOpt(required); + } + + @Nonnull + public Optional getTitle() { + return Optional.ofNullable(title); + } + + @Nonnull + public Optional getPattern() { + return Optional.ofNullable(pattern); + } + + @Nonnull + public Set determineTypes() { + if (type != null) { + return Set.of(type); + } else if (has$ref()) { + return resolve().determineTypes(); + } else if (allOf != null) { + var types = allOf.stream().map(OpenApiSchema::determineTypes).flatMap(Set::stream).collect(Collectors.toSet()); + if (types.size() > 1) { + var typeString = types.stream().map(OpenApiSchemaType::toString).collect(Collectors.joining(", ")); + throw new IllegalStateException("allOf schema must have a uniform type [" + getPointer() + "]: " + typeString); + } + return types; + } else if (oneOf != null) { + return oneOf.stream().map(OpenApiSchema::determineTypes).flatMap(Set::stream).collect(Collectors.toSet()); + } + + throw new IllegalStateException("Cannot determine type for schema: " + getPointer()); + } +} 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 000000000..b9b467a68 --- /dev/null +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiSchemaFormat.java @@ -0,0 +1,37 @@ +/* + * 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 javax.annotation.Nonnull; +import org.opensearch.client.codegen.utils.Maps; +import org.opensearch.client.codegen.utils.Strings; + +public enum OpenApiSchemaFormat { + Float, + Double, + Int32, + Int64; + + private static final Map VALUES = Maps.createLookupOf(values(), OpenApiSchemaFormat::toString); + + @Nonnull + public static OpenApiSchemaFormat from(@Nonnull String format) { + var value = VALUES.get(Strings.requireNonBlank(format, "format must not be blank")); + if (value == null) { + throw new IllegalArgumentException("Unknown format: " + format); + } + return value; + } + + @Override + public String toString() { + return name().toLowerCase(); + } +} 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 000000000..82cd66f05 --- /dev/null +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiSchemaType.java @@ -0,0 +1,39 @@ +/* + * 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 javax.annotation.Nonnull; +import org.opensearch.client.codegen.utils.Maps; +import org.opensearch.client.codegen.utils.Strings; + +public enum OpenApiSchemaType { + Array, + Boolean, + Integer, + Number, + Object, + String; + + private static final Map VALUES = Maps.createLookupOf(values(), OpenApiSchemaType::toString); + + @Nonnull + public static OpenApiSchemaType from(@Nonnull String type) { + var value = VALUES.get(Strings.requireNonBlank(type, "type must not be blank")); + if (value == null) { + throw new IllegalArgumentException("Unknown type: " + type); + } + return value; + } + + @Override + public java.lang.String toString() { + return name().toLowerCase(); + } +} 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 000000000..aa09da8d0 --- /dev/null +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/openapi/OpenApiSpecification.java @@ -0,0 +1,135 @@ +/* + * 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.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.opensearch.client.codegen.exceptions.ApiSpecificationParseException; +import org.opensearch.client.codegen.utils.Maps; + +public class OpenApiSpecification extends OpenApiElement { + private static final Logger LOGGER = LogManager.getLogger(); + 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); + } + LOGGER.info("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; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + + if (o == null || getClass() != o.getClass()) { + return false; + } + + OpenApiSpecification that = (OpenApiSpecification) o; + + return new EqualsBuilder().appendSuper(super.equals(o)).append(location, that.location).isEquals(); + } + + @Override + public int hashCode() { + return new HashCodeBuilder(17, 37).appendSuper(super.hashCode()).append(location).toHashCode(); + } + + @Override + public String toString() { + return new ToStringBuilder(this).append("location", location).toString(); + } +} diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/renderer/JavaCodeFormatter.java b/java-codegen/src/main/java/org/opensearch/client/codegen/renderer/JavaCodeFormatter.java new file mode 100644 index 000000000..6f5421627 --- /dev/null +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/renderer/JavaCodeFormatter.java @@ -0,0 +1,99 @@ +/* + * 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.renderer; + +import com.diffplug.spotless.FormatExceptionPolicyStrict; +import com.diffplug.spotless.Formatter; +import com.diffplug.spotless.LineEnding; +import com.diffplug.spotless.Provisioner; +import com.diffplug.spotless.extra.java.EclipseJdtFormatterStep; +import com.diffplug.spotless.generic.EndWithNewlineStep; +import com.diffplug.spotless.generic.TrimTrailingWhitespaceStep; +import com.diffplug.spotless.java.ImportOrderStep; +import com.diffplug.spotless.java.RemoveUnusedImportsStep; +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.util.List; +import java.util.Objects; +import javax.annotation.Nonnull; +import org.opensearch.client.codegen.exceptions.JavaFormatterException; +import org.opensearch.client.codegen.utils.MavenArtifactResolver; + +public class JavaCodeFormatter implements AutoCloseable { + private final Formatter formatter; + + private JavaCodeFormatter(@Nonnull Builder builder) { + Objects.requireNonNull(builder, "builder must not be null"); + + Provisioner provisioner = MavenArtifactResolver.createDefault()::resolve; + + var eclipseFormatter = EclipseJdtFormatterStep.createBuilder(provisioner); + eclipseFormatter.setPreferences( + List.of(Objects.requireNonNull(builder.eclipseFormatterConfig, "eclipseFormatterConfig must not be null")) + ); + + var steps = List.of( + ImportOrderStep.forJava().createFrom(), + RemoveUnusedImportsStep.create(RemoveUnusedImportsStep.defaultFormatter(), provisioner), + eclipseFormatter.build(), + TrimTrailingWhitespaceStep.create(), + EndWithNewlineStep.create() + ); + + this.formatter = Formatter.builder() + .name("java") + .lineEndingsPolicy(LineEnding.UNIX.createPolicy()) + .encoding(StandardCharsets.UTF_8) + .rootDir(Objects.requireNonNull(builder.rootDir, "rootDir must not be null")) + .steps(steps) + .exceptionPolicy(new FormatExceptionPolicyStrict()) + .build(); + } + + public void format(File file) throws JavaFormatterException { + try { + formatter.applyTo(file); + } catch (Throwable e) { + throw new JavaFormatterException("Failed to format: " + file, e); + } + } + + @Override + public void close() { + formatter.close(); + } + + @Nonnull + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private Path rootDir; + private File eclipseFormatterConfig; + + @Nonnull + public Builder withRootDir(@Nonnull Path rootDir) { + this.rootDir = Objects.requireNonNull(rootDir, "rootDir must not be null"); + return this; + } + + @Nonnull + public Builder withEclipseFormatterConfig(@Nonnull File eclipseFormatterConfig) { + this.eclipseFormatterConfig = Objects.requireNonNull(eclipseFormatterConfig, "eclipseFormatterConfig must not be null"); + return this; + } + + @Nonnull + public JavaCodeFormatter build() { + return new JavaCodeFormatter(this); + } + } +} diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/renderer/TemplateFragmentUtils.java b/java-codegen/src/main/java/org/opensearch/client/codegen/renderer/TemplateFragmentUtils.java new file mode 100644 index 000000000..35a35143c --- /dev/null +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/renderer/TemplateFragmentUtils.java @@ -0,0 +1,34 @@ +/* + * 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.renderer; + +import com.samskivert.mustache.Template; +import java.util.Optional; + +public final class TemplateFragmentUtils { + private TemplateFragmentUtils() {} + + @SuppressWarnings("unchecked") + public static Optional findParentContext(Template.Fragment fragment, Class clazz) { + var i = 0; + while (true) { + Object ctx; + + try { + ctx = fragment.context(i++); + } catch (NullPointerException ignored) { + return Optional.empty(); + } + + if (clazz.isAssignableFrom(ctx.getClass())) { + return Optional.of((T) ctx); + } + } + } +} diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/renderer/TemplateGlobalContext.java b/java-codegen/src/main/java/org/opensearch/client/codegen/renderer/TemplateGlobalContext.java new file mode 100644 index 000000000..69e121a2f --- /dev/null +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/renderer/TemplateGlobalContext.java @@ -0,0 +1,89 @@ +/* + * 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.renderer; + +import com.samskivert.mustache.Mustache; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import org.apache.commons.text.StringEscapeUtils; +import org.opensearch.client.codegen.model.Types; +import org.opensearch.client.codegen.renderer.lambdas.TemplateStringLambda; +import org.opensearch.client.codegen.utils.Strings; + +public final class TemplateGlobalContext implements Mustache.CustomContext { + @Nonnull + private final Map values; + @Nonnull + private final TemplateRenderer renderer; + + private TemplateGlobalContext(@Nonnull Builder builder) { + Objects.requireNonNull(builder, "builder must not be null"); + this.values = Objects.requireNonNull(builder.values, "values must not be null"); + this.renderer = Objects.requireNonNull(builder.renderer, "renderer must not be null"); + } + + @Override + @Nullable + public Object get(@Nonnull String name) throws Exception { + return values.get(Strings.requireNonBlank(name, "name must not be blank")); + } + + @Nonnull + public TemplateRenderer getRenderer() { + return renderer; + } + + public static Builder builder() { + return new Builder().withLambda("quoted", s -> '\"' + StringEscapeUtils.escapeJava(s) + '\"') + .withLambda("camelCase", Strings::toCamelCase) + .withLambda("pascalCase", Strings::toPascalCase) + .withLambda("toLower", s -> s.toLowerCase()) + .withLambda("ERROR", s -> { + throw new RuntimeException(s); + }) + .withValue("TYPES", Types.TYPES_MAP); + } + + public static final class Builder { + private final Map values = new HashMap<>(); + private TemplateRenderer renderer; + + @Nonnull + public Builder withLambda(@Nonnull String name, @Nonnull TemplateStringLambda lambda) { + return withLambda(name, TemplateStringLambda.asMustacheLambda(lambda)); + } + + @Nonnull + public Builder withLambda(@Nonnull String name, @Nonnull Mustache.Lambda lambda) { + return withValue(name, lambda); + } + + @Nonnull + public Builder withValue(@Nonnull String name, @Nonnull Object value) { + Objects.requireNonNull(name, "name must not be null"); + Objects.requireNonNull(value, "value must not be null"); + values.put(name, value); + return this; + } + + @Nonnull + public Builder withRenderer(@Nonnull TemplateRenderer renderer) { + this.renderer = Objects.requireNonNull(renderer, "renderer must not be null"); + return this; + } + + @Nonnull + public TemplateGlobalContext build() { + return new TemplateGlobalContext(this); + } + } +} diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/renderer/TemplateLoader.java b/java-codegen/src/main/java/org/opensearch/client/codegen/renderer/TemplateLoader.java new file mode 100644 index 000000000..5bff6f12f --- /dev/null +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/renderer/TemplateLoader.java @@ -0,0 +1,81 @@ +/* + * 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.renderer; + +import com.samskivert.mustache.Mustache; +import java.io.IOException; +import java.io.Reader; +import java.io.StringReader; +import java.nio.charset.StandardCharsets; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import javax.annotation.Nonnull; +import org.apache.commons.io.IOUtils; +import org.opensearch.client.codegen.utils.Strings; + +public final class TemplateLoader implements Mustache.TemplateLoader { + private static final ConcurrentHashMap CACHE = new ConcurrentHashMap<>(); + @Nonnull + private final String templatesResourceSubPath; + + private TemplateLoader(@Nonnull Builder builder) { + Objects.requireNonNull(builder, "builder must not be null"); + this.templatesResourceSubPath = Strings.requireNonBlank( + builder.templatesResourceSubPath, + "templatesResourceSubPath must not be blank" + ); + } + + @Nonnull + @Override + public Reader getTemplate(@Nonnull String name) throws Exception { + Strings.requireNonBlank(name, "name must not be blank"); + var path = templatesResourceSubPath + name + ".mustache"; + + var contents = CACHE.get(path); + + if (contents == null) { + try { + contents = IOUtils.resourceToString(path, StandardCharsets.UTF_8); + CACHE.put(path, contents); + } catch (IOException e) { + throw new Exception("Unable to load template: " + path, e); + } + } + + return new StringReader(contents); + } + + @Nonnull + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private String templatesResourceSubPath; + + @Nonnull + public Builder withTemplatesResourceSubPath(@Nonnull String templatesResourceSubPath) { + Strings.requireNonBlank(templatesResourceSubPath, "templatesResourceSubPath must not be blank"); + if (!templatesResourceSubPath.startsWith("/")) { + throw new IllegalArgumentException("templatesResourceSubPath must be absolute"); + } + if (!templatesResourceSubPath.endsWith("/")) { + templatesResourceSubPath += "/"; + } + this.templatesResourceSubPath = templatesResourceSubPath; + return this; + } + + @Nonnull + public TemplateLoader build() { + return new TemplateLoader(this); + } + } +} diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/renderer/TemplateRenderer.java b/java-codegen/src/main/java/org/opensearch/client/codegen/renderer/TemplateRenderer.java new file mode 100644 index 000000000..e957c528d --- /dev/null +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/renderer/TemplateRenderer.java @@ -0,0 +1,115 @@ +/* + * 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.renderer; + +import com.samskivert.mustache.Mustache; +import com.samskivert.mustache.MustacheException; +import com.samskivert.mustache.Template; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.StringWriter; +import java.io.Writer; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; +import javax.annotation.Nonnull; +import org.opensearch.client.codegen.exceptions.JavaFormatterException; +import org.opensearch.client.codegen.exceptions.RenderException; +import org.opensearch.client.codegen.model.Shape; + +public final class TemplateRenderer { + @Nonnull + private final Mustache.Compiler compiler; + @Nonnull + private final TemplateGlobalContext context; + @Nonnull + private final JavaCodeFormatter javaCodeFormatter; + @Nonnull + private final ConcurrentHashMap templateCache = new ConcurrentHashMap<>(); + + private TemplateRenderer(@Nonnull Builder builder) { + Objects.requireNonNull(builder, "builder must not be null"); + this.compiler = Mustache.compiler() + .escapeHTML(false) + .withLoader(Objects.requireNonNull(builder.templateLoader, "templateLoader must not be null")) + .withFormatter(Objects.requireNonNull(builder.valueFormatter, "valueFormatter must not be null")); + this.context = TemplateGlobalContext.builder().withRenderer(this).build(); + this.javaCodeFormatter = Objects.requireNonNull(builder.javaCodeFormatter, "javaCodeFormatter must not be null"); + } + + public void render(String templateName, Object context, Writer out) throws RenderException { + try { + templateCache.computeIfAbsent(templateName, compiler::loadTemplate).execute(context, this.context, out); + } catch (MustacheException e) { + throw new RenderException("Failed to render: " + context, e); + } + } + + public String render(String templateName, Object context) throws RenderException { + var out = new StringWriter(); + render(templateName, context, out); + return out.toString(); + } + + public void renderJava(Shape shape, File outputFile) throws RenderException { + var classBody = render(shape.getClass().getSimpleName(), shape); + var classHeader = render("Partials/ClassHeader", shape); + + try (Writer fileWriter = new FileWriter(outputFile)) { + fileWriter.write(classHeader); + fileWriter.write("\n\n"); + fileWriter.write(classBody); + } catch (IOException e) { + throw new RenderException("Unable to write rendered output to: " + outputFile, e); + } + + try { + javaCodeFormatter.format(outputFile); + } catch (JavaFormatterException e) { + throw new RenderException("Unable to format rendered output: " + outputFile, e); + } + } + + @Nonnull + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private TemplateValueFormatter valueFormatter; + private TemplateLoader templateLoader; + private JavaCodeFormatter javaCodeFormatter; + + @Nonnull + public Builder withValueFormatter(@Nonnull Function configurator) { + this.valueFormatter = Objects.requireNonNull(configurator, "configurator must not be null") + .apply(TemplateValueFormatter.builder()) + .build(); + return this; + } + + @Nonnull + public Builder withTemplateLoader(@Nonnull TemplateLoader templateLoader) { + this.templateLoader = Objects.requireNonNull(templateLoader, "templateLoader must not be null"); + return this; + } + + @Nonnull + public Builder withJavaCodeFormatter(@Nonnull JavaCodeFormatter javaCodeFormatter) { + this.javaCodeFormatter = Objects.requireNonNull(javaCodeFormatter, "javaCodeFormatter must not be null"); + return this; + } + + @Nonnull + public TemplateRenderer build() { + return new TemplateRenderer(this); + } + } +} diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/renderer/TemplateValueFormatter.java b/java-codegen/src/main/java/org/opensearch/client/codegen/renderer/TemplateValueFormatter.java new file mode 100644 index 000000000..26e75de95 --- /dev/null +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/renderer/TemplateValueFormatter.java @@ -0,0 +1,68 @@ +/* + * 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.renderer; + +import com.samskivert.mustache.Mustache; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import javax.annotation.Nonnull; + +public final class TemplateValueFormatter implements Mustache.Formatter { + @Nonnull + private final Map, Formatter> formatters; + + private TemplateValueFormatter(@Nonnull Builder builder) { + Objects.requireNonNull(builder, "builder must not be null"); + this.formatters = Objects.requireNonNull(builder.formatters, "formatters must not be null"); + } + + @Override + @Nonnull + public CharSequence format(@Nonnull Object value) { + Objects.requireNonNull(value, "value must not be null"); + return format(value, value.getClass()); + } + + @SuppressWarnings("unchecked") + @Nonnull + private CharSequence format(@Nonnull Object value, @Nonnull Class clazz) { + Objects.requireNonNull(value, "value must not be null"); + Objects.requireNonNull(clazz, "clazz must not be null"); + var formatter = (Formatter) formatters.get(clazz); + if (formatter != null) return formatter.format((T) value); + return String.valueOf(value); + } + + @FunctionalInterface + public interface Formatter { + @Nonnull + CharSequence format(@Nonnull T value); + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private final Map, Formatter> formatters = new HashMap<>(); + + @Nonnull + public Builder withFormatter(@Nonnull Class clazz, @Nonnull Formatter formatter) { + Objects.requireNonNull(clazz, "clazz must not be null"); + Objects.requireNonNull(formatter, "formatter must not be null"); + formatters.put(clazz, formatter); + return this; + } + + public TemplateValueFormatter build() { + return new TemplateValueFormatter(this); + } + } +} diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/renderer/lambdas/TemplateRenderingLambda.java b/java-codegen/src/main/java/org/opensearch/client/codegen/renderer/lambdas/TemplateRenderingLambda.java new file mode 100644 index 000000000..18df162e4 --- /dev/null +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/renderer/lambdas/TemplateRenderingLambda.java @@ -0,0 +1,41 @@ +/* + * 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.renderer.lambdas; + +import com.samskivert.mustache.Mustache; +import com.samskivert.mustache.Template; +import java.io.IOException; +import java.io.Writer; +import javax.annotation.Nonnull; +import org.opensearch.client.codegen.exceptions.RenderException; +import org.opensearch.client.codegen.renderer.TemplateFragmentUtils; +import org.opensearch.client.codegen.renderer.TemplateGlobalContext; +import org.opensearch.client.codegen.utils.Strings; + +public abstract class TemplateRenderingLambda implements Mustache.Lambda { + @Nonnull + private final String templateName; + + protected TemplateRenderingLambda(@Nonnull String templateName) { + this.templateName = Strings.requireNonBlank(templateName, "templateName must not be blank"); + } + + @Override + public void execute(Template.Fragment fragment, Writer out) throws IOException { + var renderer = TemplateFragmentUtils.findParentContext(fragment, TemplateGlobalContext.class).orElseThrow().getRenderer(); + + try { + renderer.render(templateName, getContext(fragment), out); + } catch (RenderException e) { + throw new RuntimeException(e); + } + } + + public abstract Object getContext(Template.Fragment fragment); +} diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/renderer/lambdas/TemplateStringLambda.java b/java-codegen/src/main/java/org/opensearch/client/codegen/renderer/lambdas/TemplateStringLambda.java new file mode 100644 index 000000000..cd8f762e2 --- /dev/null +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/renderer/lambdas/TemplateStringLambda.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.renderer.lambdas; + +import com.samskivert.mustache.Mustache; +import java.util.Objects; +import javax.annotation.Nonnull; + +@FunctionalInterface +public interface TemplateStringLambda { + @Nonnull + String execute(@Nonnull String input); + + static Mustache.Lambda asMustacheLambda(@Nonnull TemplateStringLambda lambda) { + Objects.requireNonNull(lambda, "lambda must not be null"); + return (fragment, out) -> out.write(lambda.execute(fragment.execute())); + } +} diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/renderer/lambdas/TypeQueryParamifyLambda.java b/java-codegen/src/main/java/org/opensearch/client/codegen/renderer/lambdas/TypeQueryParamifyLambda.java new file mode 100644 index 000000000..fe93f4987 --- /dev/null +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/renderer/lambdas/TypeQueryParamifyLambda.java @@ -0,0 +1,52 @@ +/* + * 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.renderer.lambdas; + +import com.samskivert.mustache.Template; +import java.util.Objects; +import javax.annotation.Nonnull; +import org.opensearch.client.codegen.model.Type; +import org.opensearch.client.codegen.utils.Strings; + +public final class TypeQueryParamifyLambda extends TemplateRenderingLambda { + @Nonnull + private final Type type; + + public TypeQueryParamifyLambda(Type type) { + super("Type/queryParamify"); + this.type = Objects.requireNonNull(type, "type must not be null"); + } + + @Override + public Object getContext(Template.Fragment fragment) { + return new Context(type, fragment.execute()); + } + + public static final class Context { + @Nonnull + private final Type type; + @Nonnull + private final String value; + + private Context(@Nonnull Type type, @Nonnull String value) { + this.type = Objects.requireNonNull(type, "type must not be null"); + this.value = Strings.requireNonBlank(value, "value must not be blank"); + } + + @Nonnull + public Type getType() { + return type; + } + + @Nonnull + public String getValue() { + return value; + } + } +} diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/renderer/lambdas/TypeSerializerLambda.java b/java-codegen/src/main/java/org/opensearch/client/codegen/renderer/lambdas/TypeSerializerLambda.java new file mode 100644 index 000000000..c8c2f1c14 --- /dev/null +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/renderer/lambdas/TypeSerializerLambda.java @@ -0,0 +1,60 @@ +/* + * 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.renderer.lambdas; + +import com.samskivert.mustache.Template; +import java.util.Objects; +import javax.annotation.Nonnull; +import org.opensearch.client.codegen.model.Type; +import org.opensearch.client.codegen.renderer.TemplateFragmentUtils; +import org.opensearch.client.codegen.utils.Strings; + +public final class TypeSerializerLambda extends TemplateRenderingLambda { + @Nonnull + private final Type type; + + public TypeSerializerLambda(Type type, boolean direct) { + super("Type/" + (direct ? "directSerializer" : "serializer")); + this.type = Objects.requireNonNull(type, "type must not be null"); + } + + @Override + public Object getContext(Template.Fragment fragment) { + var depth = TemplateFragmentUtils.findParentContext(fragment, Context.class).map(ctx -> ctx.depth + 1).orElse(0); + return new Context(type, fragment.execute(), depth); + } + + public static final class Context { + @Nonnull + private final Type type; + @Nonnull + private final String value; + private final int depth; + + private Context(@Nonnull Type type, @Nonnull String value, int depth) { + this.type = Objects.requireNonNull(type, "type must not be null"); + this.value = Strings.requireNonBlank(value, "value must not be blank"); + this.depth = depth; + } + + @Nonnull + public Type getType() { + return type; + } + + @Nonnull + public String getValue() { + return value; + } + + public int getDepth() { + return depth; + } + } +} diff --git a/java-codegen/src/main/java/org/opensearch/client/codegen/utils/Either.java b/java-codegen/src/main/java/org/opensearch/client/codegen/utils/Either.java new file mode 100644 index 000000000..0f9e3dd2c --- /dev/null +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/utils/Either.java @@ -0,0 +1,126 @@ +/* + * 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.Objects; +import java.util.function.Function; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import org.jetbrains.annotations.Contract; + +public abstract class Either { + public abstract boolean isLeft(); + + public abstract boolean isRight(); + + @Nullable + public abstract L getLeft(); + + @Nullable + public abstract R getRight(); + + @Nullable + public L getLeftOrThrow(@Nonnull Function exceptionFn) throws X { + Objects.requireNonNull(exceptionFn, "exceptionFn must not be null"); + if (isLeft()) { + return getLeft(); + } else { + throw exceptionFn.apply(getRight()); + } + } + + public R getRightOrThrow(Function exceptionFn) throws X { + Objects.requireNonNull(exceptionFn, "exceptionFn must not be null"); + if (isRight()) { + return getRight(); + } else { + throw exceptionFn.apply(getLeft()); + } + } + + public T fold(Function leftFn, Function rightFn) { + if (isLeft()) { + return leftFn.apply(getLeft()); + } else { + return rightFn.apply(getRight()); + } + } + + @Nonnull + public static Either left(@Nullable L value) { + return new Left<>(value); + } + + @Nonnull + public static Either right(@Nullable R value) { + return new Right<>(value); + } + + private static class Left extends Either { + @Nullable + private final L value; + + private Left(@Nullable L value) { + this.value = value; + } + + @Override + public boolean isLeft() { + return true; + } + + @Override + public boolean isRight() { + return false; + } + + @Nullable + @Override + public L getLeft() { + return value; + } + + @Contract("-> fail") + @Override + public R getRight() { + throw new IllegalStateException("Cannot get right value from left"); + } + } + + private static class Right extends Either { + @Nullable + private final R value; + + private Right(@Nullable R value) { + this.value = value; + } + + @Override + public boolean isLeft() { + return false; + } + + @Override + public boolean isRight() { + return true; + } + + @Contract("-> fail") + @Override + public L getLeft() { + throw new IllegalStateException("Cannot get left value from right"); + } + + @Nullable + @Override + public R getRight() { + return value; + } + } +} 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 000000000..538678731 --- /dev/null +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/utils/Functional.java @@ -0,0 +1,26 @@ +/* + * 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.Objects; +import java.util.function.Function; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import org.jetbrains.annotations.Contract; + +public final class Functional { + private Functional() {} + + @Contract("null,_->null;_,_->_") + @Nullable + public static R ifNonnull(@Nullable T value, @Nonnull Function mapper) { + Objects.requireNonNull(mapper, "mapper must not be null"); + 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 000000000..f807bc497 --- /dev/null +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/utils/Lists.java @@ -0,0 +1,69 @@ +/* + * 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.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Function; +import java.util.function.Predicate; +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 filter(@Nonnull Collection list, @Nonnull Predicate predicate) { + Objects.requireNonNull(list, "list must not be null"); + Objects.requireNonNull(predicate, "predicate must not be null"); + return list.stream().filter(predicate).collect(Collectors.toList()); + } + + @Nonnull + public static List map(@Nonnull Collection list, @Nonnull Function mapper) { + Objects.requireNonNull(list, "list must not be null"); + Objects.requireNonNull(mapper, "mapper must not be null"); + return list.stream().map(mapper).collect(Collectors.toList()); + } + + @Nonnull + public static List map(@Nonnull List list, @Nonnull ItemMapper mapper) { + Objects.requireNonNull(list, "list must not be null"); + Objects.requireNonNull(mapper, "mapper must not be null"); + return IntStream.range(0, list.size()).mapToObj(i -> mapper.map(i, list.get(i))).collect(Collectors.toList()); + } + + @FunctionalInterface + public interface ItemMapper { + @Nonnull + TOut map(int index, @Nonnull TIn item); + } + + @Nonnull + public static List filterMap( + @Nonnull Collection list, + @Nonnull Predicate predicate, + @Nonnull Function mapper + ) { + Objects.requireNonNull(list, "list must not be null"); + Objects.requireNonNull(predicate, "predicate must not be null"); + Objects.requireNonNull(mapper, "mapper must not be null"); + return list.stream().filter(predicate).map(mapper).collect(Collectors.toList()); + } +} 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 000000000..b16f6f039 --- /dev/null +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/utils/Maps.java @@ -0,0 +1,71 @@ +/* + * 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.Arrays; +import java.util.Collections; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +public final class Maps { + private Maps() {} + + @Nonnull + public static Optional tryGet(@Nullable Map map, @Nonnull TKey key) { + Objects.requireNonNull(key, "key must not be null"); + return Optional.ofNullable(map).flatMap(m -> Optional.ofNullable(m.get(key))); + } + + @Nonnull + public static Map createLookupOf(@Nonnull TValue[] values, @Nonnull Function by) { + Objects.requireNonNull(values, "values must not be null"); + return createLookupOf(Arrays.stream(values), by); + } + + @Nonnull + public static Map createLookupOf(@Nonnull Stream values, @Nonnull Function by) { + Objects.requireNonNull(values, "values must not be null"); + Objects.requireNonNull(by, "by must not be null"); + return values.collect(Collectors.toMap(by, Function.identity())); + } + + @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/MavenArtifactResolver.java b/java-codegen/src/main/java/org/opensearch/client/codegen/utils/MavenArtifactResolver.java new file mode 100644 index 000000000..024d4b5e9 --- /dev/null +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/utils/MavenArtifactResolver.java @@ -0,0 +1,85 @@ +/* + * 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.io.File; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; +import javax.annotation.Nonnull; +import org.apache.maven.repository.internal.MavenRepositorySystemUtils; +import org.eclipse.aether.RepositorySystem; +import org.eclipse.aether.RepositorySystemSession; +import org.eclipse.aether.artifact.DefaultArtifact; +import org.eclipse.aether.collection.CollectRequest; +import org.eclipse.aether.graph.Dependency; +import org.eclipse.aether.graph.Exclusion; +import org.eclipse.aether.repository.LocalRepository; +import org.eclipse.aether.repository.RemoteRepository; +import org.eclipse.aether.resolution.DependencyRequest; +import org.eclipse.aether.resolution.DependencyResolutionException; +import org.eclipse.aether.supplier.RepositorySystemSupplier; + +public class MavenArtifactResolver { + @Nonnull + public static MavenArtifactResolver createDefault() { + var repositorySystem = new RepositorySystemSupplier().get(); + var session = MavenRepositorySystemUtils.newSession(); + session.setSystemProperties(System.getProperties()); + var mavenLocal = new LocalRepository(new File(System.getProperty("user.home") + "/.m2/repository")); + session.setLocalRepositoryManager(repositorySystem.newLocalRepositoryManager(session, mavenLocal)); + + var mavenCentral = new RemoteRepository.Builder("central", "default", "https://repo.maven.apache.org/maven2/").build(); + return new MavenArtifactResolver(repositorySystem, session, List.of(mavenCentral)); + } + + private final static Collection EXCLUDE_ALL_TRANSITIVES = Collections.singleton(new Exclusion("*", "*", "*", "*")); + + @Nonnull + private final RepositorySystem repositorySystem; + @Nonnull + private final RepositorySystemSession session; + @Nonnull + private final List repositories; + + public MavenArtifactResolver( + @Nonnull RepositorySystem repositorySystem, + @Nonnull RepositorySystemSession session, + @Nonnull List repositories + ) { + this.repositorySystem = Objects.requireNonNull(repositorySystem, "repositorySystem must not be null"); + this.session = Objects.requireNonNull(session, "session must not be null"); + this.repositories = Objects.requireNonNull(repositories, "repositories must not be null"); + } + + @Nonnull + public Set resolve(boolean withTransitives, @Nonnull Collection mavenCoordinates) { + Objects.requireNonNull(mavenCoordinates, "mavenCoordinates must not be null"); + + var dependencies = Lists.map( + mavenCoordinates, + coord -> new Dependency(new DefaultArtifact(coord), null, null, withTransitives ? null : EXCLUDE_ALL_TRANSITIVES) + ); + + var request = new DependencyRequest(new CollectRequest(dependencies, null, repositories), null); + + try { + return repositorySystem.resolveDependencies(session, request) + .getArtifactResults() + .stream() + .map(r -> r.getArtifact().getFile()) + .collect(Collectors.toSet()); + } catch (DependencyResolutionException ex) { + throw new RuntimeException("Failed to resolve dependencies", ex); + } + } +} 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 000000000..b63536f6d --- /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 new file mode 100644 index 000000000..3db157707 --- /dev/null +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/utils/Streams.java @@ -0,0 +1,31 @@ +/* + * 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.Objects; +import java.util.function.Function; +import java.util.stream.Stream; +import javax.annotation.Nonnull; + +public final class Streams { + private Streams() {} + + @Nonnull + public static > Stream sortedBy(@Nonnull Stream stream, @Nonnull Function keyExtractor) { + Objects.requireNonNull(stream, "stream must not be null"); + Objects.requireNonNull(keyExtractor, "keyExtractor must not be null"); + return stream.sorted((a, b) -> { + var ka = keyExtractor.apply(a); + var kb = keyExtractor.apply(b); + if (ka == null) return kb == null ? 0 : -1; + if (kb == null) return 1; + return ka.compareTo(kb); + }); + } +} 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 new file mode 100644 index 000000000..a8afed924 --- /dev/null +++ b/java-codegen/src/main/java/org/opensearch/client/codegen/utils/Strings.java @@ -0,0 +1,64 @@ +/* + * 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.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 boolean isNullOrEmpty(@Nullable String str) { + return str == null || str.isEmpty(); + } + + public static boolean isNullOrBlank(@Nullable String str) { + return str == null || str.isBlank(); + } + + @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; + } + + @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|[-:.])", "_") + .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); + } +} diff --git a/java-codegen/src/main/resources/log4j2.xml b/java-codegen/src/main/resources/log4j2.xml new file mode 100644 index 000000000..a388d16da --- /dev/null +++ b/java-codegen/src/main/resources/log4j2.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/java-codegen/src/main/resources/org/opensearch/client/codegen/templates/ArrayShape.mustache b/java-codegen/src/main/resources/org/opensearch/client/codegen/templates/ArrayShape.mustache new file mode 100644 index 000000000..86acdf44f --- /dev/null +++ b/java-codegen/src/main/resources/org/opensearch/client/codegen/templates/ArrayShape.mustache @@ -0,0 +1,31 @@ +{{>ObjectShape/ClassDeclaration}} { + {{>ObjectShape/Fields}} + + // --------------------------------------------------------------------------------------------- + + {{>ObjectShape/Constructor}} + + {{>ObjectShape/Getters}} + + /** + * Serialize this value to JSON. + */ + public void serialize({{TYPES.Jakarta.Json.Stream.JsonGenerator}} generator, {{TYPES.Client.Json.JsonpMapper}} mapper) { + {{#valueBodyField.type.serializer}}this.{{valueBodyField.name}}{{/valueBodyField.type.serializer}} + } + + // --------------------------------------------------------------------------------------------- + + {{>ObjectShape/Builder}} + + public static final {{TYPES.Client.Json.JsonpDeserializer}}<{{className}}> _DESERIALIZER = create{{className}}Deserializer(); + + protected static {{TYPES.Client.Json.JsonpDeserializer}}<{{className}}> create{{className}}Deserializer() { + {{TYPES.Client.Json.JsonpDeserializer}}<{{valueBodyField.type}}> valueDeserializer = {{#valueBodyField.type}}{{>Type/deserializer}}{{/valueBodyField.type}}; + + return {{TYPES.Client.Json.JsonpDeserializer}}.of( + valueDeserializer.acceptedEvents(), + (parser, mapper) -> new Builder().valueBody(valueDeserializer.deserialize(parser, mapper)).build() + ); + } +} \ No newline at end of file diff --git a/java-codegen/src/main/resources/org/opensearch/client/codegen/templates/Client.mustache b/java-codegen/src/main/resources/org/opensearch/client/codegen/templates/Client.mustache new file mode 100644 index 000000000..134ea1d90 --- /dev/null +++ b/java-codegen/src/main/resources/org/opensearch/client/codegen/templates/Client.mustache @@ -0,0 +1,51 @@ +public class {{className}} extends {{TYPES.Client.ApiClient}}<{{TYPES.Client.Transport.OpenSearchTransport}}, {{className}}> { + public {{className}}({{TYPES.Client.Transport.OpenSearchTransport}} transport) { + super(transport, null); + } + + public {{className}}({{TYPES.Client.Transport.OpenSearchTransport}} transport, @{{TYPES.Javax.Annotation.Nullable}} {{TYPES.Client.Transport.TransportOptions}} transportOptions) { + super(transport, transportOptions); + } + + @Override + public {{className}} withTransportOptions(@{{TYPES.Javax.Annotation.Nullable}} {{TYPES.Client.Transport.TransportOptions}} transportOptions) { + return new {{className}}(this.transport, transportOptions); + } +{{#children}} + + public {{type}} {{#camelCase}}{{name}}{{/camelCase}}() { + return new {{type}}(this.transport, this.transportOptions); + } +{{/children}} +{{#operations}} + + // ----- Endpoint: {{operationGroup}} + + /** + * {{description}} + */ + public {{#async}}{{TYPES.Java.Util.Concurrent.CompletableFuture}}<{{/async}}{{responseType}}{{#async}}>{{/async}} {{#camelCase}}{{id}}{{/camelCase}}({{type}} request) throws {{TYPES.Java.Io.IOException}}, {{TYPES.Client.OpenSearch._Types.OpenSearchException}} { + return this.transport.performRequest{{#async}}Async{{/async}}(request, {{type}}._ENDPOINT, this.transportOptions); + } + + /** + * {{description}} + * + * @param fn a function that initializes a builder to create the {@link {{type}}} + */ + public final {{#async}}{{TYPES.Java.Util.Concurrent.CompletableFuture}}<{{/async}}{{responseType}}{{#async}}>{{/async}} {{#camelCase}}{{id}}{{/camelCase}}({{type.builderFnType}} fn) + throws IOException, OpenSearchException { + return {{#camelCase}}{{id}}{{/camelCase}}(fn.apply(new {{type.builderType}}()).build()); + } + {{^hasAnyRequiredFields}} + + /** + * {{description}} + */ + public {{#async}}{{TYPES.Java.Util.Concurrent.CompletableFuture}}<{{/async}}{{responseType}}{{#async}}>{{/async}} {{#camelCase}}{{id}}{{/camelCase}}() + throws IOException, OpenSearchException { + return {{#camelCase}}{{id}}{{/camelCase}}(new {{type.builderType}}().build()); + } + {{/hasAnyRequiredFields}} +{{/operations}} +} \ No newline at end of file diff --git a/java-codegen/src/main/resources/org/opensearch/client/codegen/templates/EnumShape.mustache b/java-codegen/src/main/resources/org/opensearch/client/codegen/templates/EnumShape.mustache new file mode 100644 index 000000000..129fe297a --- /dev/null +++ b/java-codegen/src/main/resources/org/opensearch/client/codegen/templates/EnumShape.mustache @@ -0,0 +1,24 @@ +// typedef: {{typedefName}} + +{{#description}} +/** + * {{.}} + */ +{{/description}} +@{{TYPES.Client.Json.JsonpDeserializable}} +public enum {{className}} implements {{TYPES.Client.Json.JsonEnum}} { + {{#variants}} + {{name}}({{#quoted}}{{wireName}}{{/quoted}}){{^-last}},{{/-last}} + {{/variants}} + ; + + private final String jsonValue; + + {{className}}(String jsonValue) { + this.jsonValue = jsonValue; + } + + public String jsonValue() { return this.jsonValue; } + + public static final JsonEnum.Deserializer<{{className}}> _DESERIALIZER = new JsonEnum.Deserializer<>({{className}}.values()); +} \ No newline at end of file diff --git a/java-codegen/src/main/resources/org/opensearch/client/codegen/templates/ObjectShape.mustache b/java-codegen/src/main/resources/org/opensearch/client/codegen/templates/ObjectShape.mustache new file mode 100644 index 000000000..b6f252f6c --- /dev/null +++ b/java-codegen/src/main/resources/org/opensearch/client/codegen/templates/ObjectShape.mustache @@ -0,0 +1,20 @@ +{{>ObjectShape/ClassDeclaration}} { + {{>ObjectShape/Fields}} + + // --------------------------------------------------------------------------------------------- + + {{>ObjectShape/Constructor}} + + {{>ObjectShape/Getters}} + + {{>ObjectShape/Serialize}} + + + // --------------------------------------------------------------------------------------------- + + {{>ObjectShape/Builder}} + + // --------------------------------------------------------------------------------------------- + + {{>ObjectShape/Deserialize}} +} \ No newline at end of file diff --git a/java-codegen/src/main/resources/org/opensearch/client/codegen/templates/ObjectShape/Builder.mustache b/java-codegen/src/main/resources/org/opensearch/client/codegen/templates/ObjectShape/Builder.mustache new file mode 100644 index 000000000..56d73c16c --- /dev/null +++ b/java-codegen/src/main/resources/org/opensearch/client/codegen/templates/ObjectShape/Builder.mustache @@ -0,0 +1,77 @@ + /** + * Builder for {@link {{className}}}. + */ + public static class Builder extends {{TYPES.Client.Util.ObjectBuilderBase}} implements {{TYPES.Client.Util.ObjectBuilder}}<{{className}}> { + {{#fields}} + @{{TYPES.Javax.Annotation.Nullable}} private {{type.boxed}} {{name}}; + {{/fields}} + + {{#fields}} + {{#type.isMap}} + {{>ObjectShape/FieldDoc/Basic}} + {{#deprecation}}@Deprecated{{/deprecation}} + public final Builder {{name}}({{type}} map) { + this.{{name}} = _mapPutAll(this.{{name}}, map); + return this; + } + + {{>ObjectShape/FieldDoc/Basic}} + {{#deprecation}}@Deprecated{{/deprecation}} + public final Builder {{name}}({{type.mapKeyType}} key, {{type.mapValueType}} value) { + this.{{name}} = _mapPut(this.{{name}}, key, value); + return this; + } + {{/type.isMap}} + {{#type.isList}} + {{>ObjectShape/FieldDoc/ListAddAll}} + {{#deprecation}}@Deprecated{{/deprecation}} + public final Builder {{name}}({{type}} list) { + this.{{name}} = _listAddAll(this.{{name}}, list); + return this; + } + + {{>ObjectShape/FieldDoc/ListAdd}} + {{#deprecation}}@Deprecated{{/deprecation}} + public final Builder {{name}}({{type.listValueType}} value, {{type.listValueType}}... values) { + this.{{name}} = _listAdd(this.{{name}}, value, values); + return this; + } + {{#type.listValueType.hasBuilder}} + + {{>ObjectShape/FieldDoc/ListAddBuilderFn}} + {{#deprecation}}@Deprecated{{/deprecation}} + public final Builder {{name}}({{type.listValueType.builderFnType}} fn) { + return {{name}}(fn.apply(new {{type.listValueType.builderType}}()).build()); + } + {{/type.listValueType.hasBuilder}} + {{/type.isList}} + {{^type.isListOrMap}} + {{>ObjectShape/FieldDoc/Basic}} + {{#deprecation}}@Deprecated{{/deprecation}} + public final Builder {{name}}({{^required}}@{{TYPES.Javax.Annotation.Nullable}} {{/required}}{{type}} value) { + this.{{name}} = value; + return this; + } + {{#type.hasBuilder}} + + {{>ObjectShape/FieldDoc/Basic}} + {{#deprecation}}@Deprecated{{/deprecation}} + public final Builder {{name}}({{type.builderFnType}} fn) { + return this.{{name}}(fn.apply(new {{type.builderType}}()).build()); + } + {{/type.hasBuilder}} + {{/type.isListOrMap}} + + {{/fields}} + /** + * Builds a {@link {{className}}}. + * + * @throws NullPointerException + * if some of the required fields are null. + */ + public {{className}} build() { + _checkSingleUse(); + + return new {{className}}(this); + } + } diff --git a/java-codegen/src/main/resources/org/opensearch/client/codegen/templates/ObjectShape/ClassDeclaration.mustache b/java-codegen/src/main/resources/org/opensearch/client/codegen/templates/ObjectShape/ClassDeclaration.mustache new file mode 100644 index 000000000..f08d1623b --- /dev/null +++ b/java-codegen/src/main/resources/org/opensearch/client/codegen/templates/ObjectShape/ClassDeclaration.mustache @@ -0,0 +1,11 @@ +// typedef: {{typedefName}} + +{{#description}} +/** +* {{.}} +*/ +{{/description}} +{{#annotations}} +@{{.}} +{{/annotations}} +public class {{className}}{{#extendsType}} extends {{.}}{{/extendsType}}{{#implementsTypes}}{{#-first}} implements{{/-first}} {{.}}{{^-last}},{{/-last}}{{/implementsTypes}} diff --git a/java-codegen/src/main/resources/org/opensearch/client/codegen/templates/ObjectShape/Constructor.mustache b/java-codegen/src/main/resources/org/opensearch/client/codegen/templates/ObjectShape/Constructor.mustache new file mode 100644 index 000000000..92995e9d0 --- /dev/null +++ b/java-codegen/src/main/resources/org/opensearch/client/codegen/templates/ObjectShape/Constructor.mustache @@ -0,0 +1,24 @@ + private {{className}}(Builder builder) { +{{#fields}} +{{#type.isListOrMap}} + {{#required}} + this.{{name}} = {{TYPES.Client.Util.ApiTypeHelper}}.unmodifiableRequired(builder.{{name}}, this, {{#quoted}}{{name}}{{/quoted}}); + {{/required}} + {{^required}} + this.{{name}} = {{TYPES.Client.Util.ApiTypeHelper}}.unmodifiable(builder.{{name}}); + {{/required}} +{{/type.isListOrMap}} +{{^type.isListOrMap}} + {{#required}} + this.{{name}} = {{TYPES.Client.Util.ApiTypeHelper}}.requireNonNull(builder.{{name}}, this, {{#quoted}}{{name}}{{/quoted}}); + {{/required}} + {{^required}} + this.{{name}} = builder.{{name}}; + {{/required}} +{{/type.isListOrMap}} +{{/fields}} + } + + public static {{className}} of({{type.builderFnType}} fn) { + return fn.apply(new Builder()).build(); + } \ No newline at end of file diff --git a/java-codegen/src/main/resources/org/opensearch/client/codegen/templates/ObjectShape/Deserialize.mustache b/java-codegen/src/main/resources/org/opensearch/client/codegen/templates/ObjectShape/Deserialize.mustache new file mode 100644 index 000000000..42966164a --- /dev/null +++ b/java-codegen/src/main/resources/org/opensearch/client/codegen/templates/ObjectShape/Deserialize.mustache @@ -0,0 +1,18 @@ + /** + * Json deserializer for {@link {{className}}} + */ + public static final {{TYPES.Client.Json.JsonpDeserializer}}<{{className}}> _DESERIALIZER = {{TYPES.Client.Json.ObjectBuilderDeserializer}}.lazy(Builder::new, {{className}}::setup{{className}}Deserializer); + + protected static void setup{{className}}Deserializer({{TYPES.Client.Json.ObjectDeserializer}}<{{type.builderType}}> op) { + {{#bodyFields}} + op.add(Builder::{{name}}, {{#type}}{{>Type/deserializer}}{{/type}}, {{#quoted}}{{wireName}}{{/quoted}}); + {{/bodyFields}} + {{#additionalPropertiesField}} + op.setUnknownFieldHandler((builder, name, parser, mapper) -> { + if (builder.{{name}} == null) { + builder.{{name}} = new {{TYPES.Java.Util.HashMap}}<>(); + } + builder.{{name}}.put(name, {{#type.mapValueType}}{{>Type/deserializer}}{{/type.mapValueType}}.deserialize(parser, mapper)); + }); + {{/additionalPropertiesField}} + } \ No newline at end of file diff --git a/java-codegen/src/main/resources/org/opensearch/client/codegen/templates/ObjectShape/FieldDoc/Basic.mustache b/java-codegen/src/main/resources/org/opensearch/client/codegen/templates/ObjectShape/FieldDoc/Basic.mustache new file mode 100644 index 000000000..c09290663 --- /dev/null +++ b/java-codegen/src/main/resources/org/opensearch/client/codegen/templates/ObjectShape/FieldDoc/Basic.mustache @@ -0,0 +1,5 @@ +/** + +{{>ObjectShape/FieldDoc/BasicInner}} + +*/ \ No newline at end of file diff --git a/java-codegen/src/main/resources/org/opensearch/client/codegen/templates/ObjectShape/FieldDoc/BasicInner.mustache b/java-codegen/src/main/resources/org/opensearch/client/codegen/templates/ObjectShape/FieldDoc/BasicInner.mustache new file mode 100644 index 000000000..89f13ae78 --- /dev/null +++ b/java-codegen/src/main/resources/org/opensearch/client/codegen/templates/ObjectShape/FieldDoc/BasicInner.mustache @@ -0,0 +1,5 @@ +{{#description}} +*

{{#required}}Required - {{/required}}{{.}}

+{{/description}} + +*

API name: {@code {{wireName}}}

\ No newline at end of file diff --git a/java-codegen/src/main/resources/org/opensearch/client/codegen/templates/ObjectShape/FieldDoc/ListAdd.mustache b/java-codegen/src/main/resources/org/opensearch/client/codegen/templates/ObjectShape/FieldDoc/ListAdd.mustache new file mode 100644 index 000000000..d8720ba08 --- /dev/null +++ b/java-codegen/src/main/resources/org/opensearch/client/codegen/templates/ObjectShape/FieldDoc/ListAdd.mustache @@ -0,0 +1,5 @@ +/** +{{>ObjectShape/FieldDoc/BasicInner}} + +*

Adds one or more values to {{name}}.

+*/ \ No newline at end of file diff --git a/java-codegen/src/main/resources/org/opensearch/client/codegen/templates/ObjectShape/FieldDoc/ListAddAll.mustache b/java-codegen/src/main/resources/org/opensearch/client/codegen/templates/ObjectShape/FieldDoc/ListAddAll.mustache new file mode 100644 index 000000000..2894d9549 --- /dev/null +++ b/java-codegen/src/main/resources/org/opensearch/client/codegen/templates/ObjectShape/FieldDoc/ListAddAll.mustache @@ -0,0 +1,5 @@ +/** +{{>ObjectShape/FieldDoc/BasicInner}} + +*

Adds all elements of list to {{name}}.

+*/ \ No newline at end of file diff --git a/java-codegen/src/main/resources/org/opensearch/client/codegen/templates/ObjectShape/FieldDoc/ListAddBuilderFn.mustache b/java-codegen/src/main/resources/org/opensearch/client/codegen/templates/ObjectShape/FieldDoc/ListAddBuilderFn.mustache new file mode 100644 index 000000000..867d0eca2 --- /dev/null +++ b/java-codegen/src/main/resources/org/opensearch/client/codegen/templates/ObjectShape/FieldDoc/ListAddBuilderFn.mustache @@ -0,0 +1,5 @@ +/** +{{>ObjectShape/FieldDoc/BasicInner}} + +*

Adds a value to {{name}} using a builder lambda.

+*/ \ No newline at end of file diff --git a/java-codegen/src/main/resources/org/opensearch/client/codegen/templates/ObjectShape/Fields.mustache b/java-codegen/src/main/resources/org/opensearch/client/codegen/templates/ObjectShape/Fields.mustache new file mode 100644 index 000000000..8c40ad79d --- /dev/null +++ b/java-codegen/src/main/resources/org/opensearch/client/codegen/templates/ObjectShape/Fields.mustache @@ -0,0 +1,10 @@ +{{#fields}} + + {{#deprecation}}@Deprecated{{/deprecation}} + {{^required}} + {{^type.isListOrMap}} + @{{TYPES.Javax.Annotation.Nullable}} + {{/type.isListOrMap}} + {{/required}} + private final {{type}} {{name}}; +{{/fields}} diff --git a/java-codegen/src/main/resources/org/opensearch/client/codegen/templates/ObjectShape/Getters.mustache b/java-codegen/src/main/resources/org/opensearch/client/codegen/templates/ObjectShape/Getters.mustache new file mode 100644 index 000000000..61fea36e5 --- /dev/null +++ b/java-codegen/src/main/resources/org/opensearch/client/codegen/templates/ObjectShape/Getters.mustache @@ -0,0 +1,13 @@ +{{#fields}} + + {{>ObjectShape/FieldDoc/Basic}} + {{#deprecation}}@Deprecated{{/deprecation}} + {{^required}} + {{^type.isListOrMap}} + @{{TYPES.Javax.Annotation.Nullable}} + {{/type.isListOrMap}} + {{/required}} + public final {{type}} {{name}}() { + return this.{{name}}; + } +{{/fields}} diff --git a/java-codegen/src/main/resources/org/opensearch/client/codegen/templates/ObjectShape/Serialize.mustache b/java-codegen/src/main/resources/org/opensearch/client/codegen/templates/ObjectShape/Serialize.mustache new file mode 100644 index 000000000..2a2e1e914 --- /dev/null +++ b/java-codegen/src/main/resources/org/opensearch/client/codegen/templates/ObjectShape/Serialize.mustache @@ -0,0 +1,31 @@ + /** + * Serialize this object to JSON. + */ + @Override + public void serialize({{TYPES.Jakarta.Json.Stream.JsonGenerator}} generator, {{TYPES.Client.Json.JsonpMapper}} mapper) { + generator.writeStartObject(); + serializeInternal(generator, mapper); + generator.writeEnd(); + } + + protected void serializeInternal({{TYPES.Jakarta.Json.Stream.JsonGenerator}} generator, {{TYPES.Client.Json.JsonpMapper}} mapper) { +{{#additionalPropertiesField}} + {{#type.directSerializer}}this.{{name}}{{/type.directSerializer}} +{{/additionalPropertiesField}} +{{#bodyFields}} + {{^required}} + {{#type.isListOrMap}} + if ({{TYPES.Client.Util.ApiTypeHelper}}.isDefined(this.{{name}})) { + {{/type.isListOrMap}} + {{^type.isListOrMap}} + if (this.{{name}} != null) { + {{/type.isListOrMap}} + {{/required}} + generator.writeKey({{#quoted}}{{wireName}}{{/quoted}}); + {{#type.serializer}}this.{{name}}{{/type.serializer}} + {{^required}} + } + {{/required}} + +{{/bodyFields}} + } \ No newline at end of file diff --git a/java-codegen/src/main/resources/org/opensearch/client/codegen/templates/Partials/ClassHeader.mustache b/java-codegen/src/main/resources/org/opensearch/client/codegen/templates/Partials/ClassHeader.mustache new file mode 100644 index 000000000..b04007b76 --- /dev/null +++ b/java-codegen/src/main/resources/org/opensearch/client/codegen/templates/Partials/ClassHeader.mustache @@ -0,0 +1,41 @@ +/* + * 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. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/* + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +//---------------------------------------------------- +// THIS CODE IS GENERATED. MANUAL EDITS WILL BE LOST. +//---------------------------------------------------- + +package {{packageName}}; + +{{#imports}} +import {{.}}; +{{/imports}} \ No newline at end of file diff --git a/java-codegen/src/main/resources/org/opensearch/client/codegen/templates/RequestShape.mustache b/java-codegen/src/main/resources/org/opensearch/client/codegen/templates/RequestShape.mustache new file mode 100644 index 000000000..913277c48 --- /dev/null +++ b/java-codegen/src/main/resources/org/opensearch/client/codegen/templates/RequestShape.mustache @@ -0,0 +1,80 @@ +{{>ObjectShape/ClassDeclaration}} { + {{>ObjectShape/Fields}} + + // --------------------------------------------------------------------------------------------- + + {{>ObjectShape/Constructor}} + + {{>ObjectShape/Getters}} + + {{#hasRequestBody}} + {{>ObjectShape/Serialize}} + {{/hasRequestBody}} + + // --------------------------------------------------------------------------------------------- + + {{>ObjectShape/Builder}} + {{#hasRequestBody}} + + // --------------------------------------------------------------------------------------------- + + {{>ObjectShape/Deserialize}}{{/hasRequestBody}} + + // --------------------------------------------------------------------------------------------- + + /** + * Endpoint "{@code {{operationGroup}}}". + */ + public static final {{TYPES.Client.Transport.Endpoint}}<{{className}}, {{responseType}}, {{TYPES.Client.OpenSearch._Types.ErrorResponse}}> _ENDPOINT = new {{TYPES.Client.Transport.Endpoints.SimpleEndpoint}}<>( + // Request method + request -> {{#quoted}}{{httpMethod}}{{/quoted}}, + // Request path + request -> { + {{#hasSinglePath}} + {{#firstPath}}{{>RequestShape/HttpPathBuilder}}{{/firstPath}} + {{/hasSinglePath}} + {{^hasSinglePath}} + {{#indexedPathParams}} + final int _{{left}} = 1 << {{right}}; + {{/indexedPathParams}} + + int propsSet = 0; + + {{#pathParams}} + if ({{TYPES.Client.Util.ApiTypeHelper}}.isDefined(request.{{name}}())) propsSet |= _{{name}}; + {{/pathParams}} + + {{#httpPaths}} + if (propsSet == {{#hasParams}}({{#params}}{{^-first}} | {{/-first}}_{{name}}{{/params}}){{/hasParams}}{{^hasParams}}0{{/hasParams}}) { + {{>RequestShape/HttpPathBuilder}} + } + {{/httpPaths}} + + throw {{TYPES.Client.Transport.Endpoints.SimpleEndpoint}}.noPathTemplateFound("path"); + {{/hasSinglePath}} + }, + // Request parameters + {{#hasQueryParams}} + request -> { + {{TYPES.Java.Util.Map}} params = new {{TYPES.Java.Util.HashMap}}<>(); + {{#queryParams}} + {{#type.isListOrMap}} + if ({{TYPES.Client.Util.ApiTypeHelper}}.isDefined(request.{{name}})) { + {{/type.isListOrMap}} + {{^type.isListOrMap}} + if (request.{{name}} != null) { + {{/type.isListOrMap}} + params.put({{#quoted}}{{wireName}}{{/quoted}}, {{#type.queryParamify}}request.{{name}}{{/type.queryParamify}}); + } + {{/queryParams}} + return params; + }, + {{/hasQueryParams}} + {{^hasQueryParams}} + {{TYPES.Client.Transport.Endpoints.SimpleEndpoint}}.emptyMap(), + {{/hasQueryParams}} + {{TYPES.Client.Transport.Endpoints.SimpleEndpoint}}.emptyMap(), + {{hasRequestBody}}, + {{responseType}}._DESERIALIZER + ); +} \ No newline at end of file diff --git a/java-codegen/src/main/resources/org/opensearch/client/codegen/templates/RequestShape/HttpPathBuilder.mustache b/java-codegen/src/main/resources/org/opensearch/client/codegen/templates/RequestShape/HttpPathBuilder.mustache new file mode 100644 index 000000000..e23db5a3f --- /dev/null +++ b/java-codegen/src/main/resources/org/opensearch/client/codegen/templates/RequestShape/HttpPathBuilder.mustache @@ -0,0 +1,15 @@ +{{#hasParams}} + StringBuilder buf = new StringBuilder(); + {{#parts}} + {{#isParameter}} + SimpleEndpoint.pathEncode({{#parameter.type.queryParamify}}request.{{parameter.name}}{{/parameter.type.queryParamify}}, buf); + {{/isParameter}} + {{^isParameter}} + buf.append({{#quoted}}{{content}}{{/quoted}}); + {{/isParameter}} + {{/parts}} + return buf.toString(); +{{/hasParams}} +{{^hasParams}} + return {{#quoted}}{{this}}{{/quoted}}; +{{/hasParams}} \ No newline at end of file diff --git a/java-codegen/src/main/resources/org/opensearch/client/codegen/templates/TaggedUnionShape.mustache b/java-codegen/src/main/resources/org/opensearch/client/codegen/templates/TaggedUnionShape.mustache new file mode 100644 index 000000000..2615d80e6 --- /dev/null +++ b/java-codegen/src/main/resources/org/opensearch/client/codegen/templates/TaggedUnionShape.mustache @@ -0,0 +1,112 @@ +{{>ObjectShape/ClassDeclaration}} { + public enum Kind { + {{#variants}}{{#pascalCase}}{{name}}{{/pascalCase}}{{^-last}},{{/-last}}{{/variants}} + } + + private final Kind _kind; + private final Object _value; + + @Override + public final Kind _kind() { + return _kind; + } + + @Override + public final Object _get() { + return _value; + } + + private {{className}}(Kind kind, Object value) { + this._kind = kind; + this._value = value; + } + + private {{className}}(Builder builder) { + this._kind = {{TYPES.Client.Util.ApiTypeHelper}}.requireNonNull(builder._kind, builder, ""); + this._value = {{TYPES.Client.Util.ApiTypeHelper}}.requireNonNull(builder._value, builder, ""); + } + + public static {{className}} of({{type.builderFnType}} fn) { + return fn.apply(new Builder()).build(); + } + + public String _toJsonString() { + switch (_kind) { + {{#variants}} + case {{#pascalCase}}{{name}}{{/pascalCase}}: + return {{#type.queryParamify}}this.{{name}}(){{/type.queryParamify}}; + {{/variants}} + default: + throw new IllegalStateException("Unknown kind " + _kind); + } + } + + {{#variants}} + /** + * Is this variant instance of kind {@code {{name}}}? + */ + public boolean is{{#pascalCase}}{{name}}{{/pascalCase}}() { + return _kind == Kind.{{#pascalCase}}{{name}}{{/pascalCase}}; + } + + /** + * Get the {@code {{name}}} variant value. + * + * @throws IllegalStateException if the current variant is not the {@code {{name}}} kind. + */ + public {{type}} {{name}}() { + return {{TYPES.Client.Util.TaggedUnionUtils}}.get(this, Kind.{{#pascalCase}}{{name}}{{/pascalCase}}); + } + + {{/variants}} + + @Override + public void serialize({{TYPES.Jakarta.Json.Stream.JsonGenerator}} generator, {{TYPES.Client.Json.JsonpMapper}} mapper) { + if (_value instanceof {{TYPES.Client.Json.JsonpSerializable}}) { + (({{TYPES.Client.Json.JsonpSerializable}}) _value).serialize(generator, mapper); + }{{#primitiveVariants}}{{#-first}} else { + switch (_kind) { + {{/-first}} + case {{#pascalCase}}{{name}}{{/pascalCase}}: + {{#type.serializer}}(({{type}}) this._value){{/type.serializer}} + break; + {{#-last}} + } + } + {{/-last}} + {{/primitiveVariants}} + } + + public static class Builder extends {{TYPES.Client.Util.ObjectBuilderBase}} implements {{TYPES.Client.Util.ObjectBuilder}}<{{className}}> { + private Kind _kind; + private Object _value; + + {{#variants}} + public {{TYPES.Client.Util.ObjectBuilder}}<{{className}}> {{name}}({{type}} v) { + this._kind = Kind.{{#pascalCase}}{{name}}{{/pascalCase}}; + this._value = v; + return this; + } + {{#type.hasBuilder}} + + public {{TYPES.Client.Util.ObjectBuilder}}<{{className}}> {{name}}({{type.builderFnType}} fn) { + return this.{{name}}(fn.apply(new {{type.builderType}}()).build()); + } + {{/type.hasBuilder}} + + {{/variants}} + @Override + public {{className}} build() { + _checkSingleUse(); + return new {{className}}(this); + } + } + + private static {{TYPES.Client.Json.JsonpDeserializer}}<{{className}}> build{{className}}Deserializer() { + return new {{TYPES.Client.Json.UnionDeserializer.builderType}}<{{className}}, Kind, Object>({{className}}::new, false) + {{#variants}}.addMember(Kind.{{#pascalCase}}{{name}}{{/pascalCase}}, {{#type}}{{>Type/deserializer}}{{/type}}){{/variants}} + .build(); + } + + public static final {{TYPES.Client.Json.JsonpDeserializer}}<{{className}}> _DESERIALIZER = {{TYPES.Client.Json.JsonpDeserializer}}.lazy({{className}}::build{{className}}Deserializer); +} diff --git a/java-codegen/src/main/resources/org/opensearch/client/codegen/templates/Type/deserializer.mustache b/java-codegen/src/main/resources/org/opensearch/client/codegen/templates/Type/deserializer.mustache new file mode 100644 index 000000000..52c491332 --- /dev/null +++ b/java-codegen/src/main/resources/org/opensearch/client/codegen/templates/Type/deserializer.mustache @@ -0,0 +1,14 @@ +{{#isPrimitive}} + JsonpDeserializer.{{#toLower}}{{boxed.name}}{{/toLower}}Deserializer() +{{/isPrimitive}} +{{^isPrimitive}} + {{#isList}} + JsonpDeserializer.arrayDeserializer({{#listValueType}}{{>Type/deserializer}}{{/listValueType}}) + {{/isList}} + {{#isMap}} + JsonpDeserializer.stringMapDeserializer({{#mapValueType}}{{>Type/deserializer}}{{/mapValueType}}) + {{/isMap}} + {{^isListOrMap}} + {{name}}._DESERIALIZER + {{/isListOrMap}} +{{/isPrimitive}} \ No newline at end of file diff --git a/java-codegen/src/main/resources/org/opensearch/client/codegen/templates/Type/directSerializer.mustache b/java-codegen/src/main/resources/org/opensearch/client/codegen/templates/Type/directSerializer.mustache new file mode 100644 index 000000000..66ba3e4dd --- /dev/null +++ b/java-codegen/src/main/resources/org/opensearch/client/codegen/templates/Type/directSerializer.mustache @@ -0,0 +1,19 @@ +{{#type.isMap}} + for ({{type.mapEntryType}} item{{depth}} : {{value}}.entrySet()) { + generator.writeKey(item{{depth}}.getKey()); + {{#type.mapValueType.serializer}}item{{depth}}.getValue(){{/type.mapValueType.serializer}} + } +{{/type.isMap}} +{{#type.isList}} + for ({{type.listValueType}} item{{depth}} : {{value}}) { + {{#type.listValueType.serializer}}item{{depth}}{{/type.listValueType.serializer}} + } +{{/type.isList}} +{{^type.isListOrMap}} + {{#type.isPrimitive}} + generator.write({{value}}); + {{/type.isPrimitive}} + {{^type.isPrimitive}} + {{value}}.serialize(generator, mapper); + {{/type.isPrimitive}} +{{/type.isListOrMap}} \ No newline at end of file diff --git a/java-codegen/src/main/resources/org/opensearch/client/codegen/templates/Type/queryParamify.mustache b/java-codegen/src/main/resources/org/opensearch/client/codegen/templates/Type/queryParamify.mustache new file mode 100644 index 000000000..1abc099c5 --- /dev/null +++ b/java-codegen/src/main/resources/org/opensearch/client/codegen/templates/Type/queryParamify.mustache @@ -0,0 +1,29 @@ +{{#type.isString}} + {{value}} +{{/type.isString}} +{{^type.isString}} + {{#type.isEnum}} + {{value}}.jsonValue() + {{/type.isEnum}} + {{^type.isEnum}} + {{#type.isPrimitive}} + String.valueOf({{value}}) + {{/type.isPrimitive}} + {{^type.isPrimitive}} + {{#type.isList}} + {{#type.listValueType.isString}} + String.join(",", {{value}}) + {{/type.listValueType.isString}} + {{^type.listValueType.isString}} + {{value}} + .stream() + .map(v -> {{#type.listValueType.queryParamify}}v{{/type.listValueType.queryParamify}}) + .collect({{TYPES.Java.Util.Stream.Collectors}}.joining(",")) + {{/type.listValueType.isString}} + {{/type.isList}} + {{^type.isList}} + {{value}}._toJsonString() + {{/type.isList}} + {{/type.isPrimitive}} + {{/type.isEnum}} +{{/type.isString}} diff --git a/java-codegen/src/main/resources/org/opensearch/client/codegen/templates/Type/serializer.mustache b/java-codegen/src/main/resources/org/opensearch/client/codegen/templates/Type/serializer.mustache new file mode 100644 index 000000000..d72fbd95b --- /dev/null +++ b/java-codegen/src/main/resources/org/opensearch/client/codegen/templates/Type/serializer.mustache @@ -0,0 +1,13 @@ +{{#type.isMap}} + generator.writeStartObject(); +{{/type.isMap}} +{{#type.isList}} + generator.writeStartArray(); +{{/type.isList}} +{{>Type/directSerializer}} +{{#type.isMap}} + generator.writeEnd(); +{{/type.isMap}} +{{#type.isList}} + generator.writeEnd(); +{{/type.isList}} \ No newline at end of file diff --git a/java-codegen/src/main/resources/org/opensearch/client/codegen/version.properties b/java-codegen/src/main/resources/org/opensearch/client/codegen/version.properties new file mode 100644 index 000000000..0ff745086 --- /dev/null +++ b/java-codegen/src/main/resources/org/opensearch/client/codegen/version.properties @@ -0,0 +1,10 @@ +# +# 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. +# + +version=${version} +git_revision=${git_revision} diff --git a/java-codegen/src/test/java/org/opensearch/client/codegen/utils/FunctionalTests.java b/java-codegen/src/test/java/org/opensearch/client/codegen/utils/FunctionalTests.java new file mode 100644 index 000000000..24bf4f49e --- /dev/null +++ b/java-codegen/src/test/java/org/opensearch/client/codegen/utils/FunctionalTests.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.utils; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import org.junit.jupiter.api.Test; + +public class FunctionalTests { + @Test + public void ifNonnull_withNullValue_shouldReturnNull() { + assertNull(Functional.ifNonnull((String) null, Integer::parseInt)); + } + + @Test + public void ifNonnull_withNonNullValue_shouldReturnMappedValue() { + var mapped = Functional.ifNonnull("18", Integer::parseInt); + assertEquals(18, mapped); + } +} diff --git a/java-codegen/src/test/java/org/opensearch/client/codegen/utils/ListsTests.java b/java-codegen/src/test/java/org/opensearch/client/codegen/utils/ListsTests.java new file mode 100644 index 000000000..6c5174cf2 --- /dev/null +++ b/java-codegen/src/test/java/org/opensearch/client/codegen/utils/ListsTests.java @@ -0,0 +1,54 @@ +/* + * 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 static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrowsExactly; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.Test; + +public class ListsTests { + @Test + public void unmodifiableOpt_withNullList_shouldReturnEmptyOptional() { + assertTrue(Lists.unmodifiableOpt(null).isEmpty()); + } + + @Test + public void unmodifiableOpt_withNonNullList_shouldReturnUnmodifiableList() { + var opt = Lists.unmodifiableOpt(new ArrayList() { + { + add("foobar"); + } + }); + assertTrue(opt.isPresent()); + var list = opt.get(); + assertEquals(1, list.size()); + assertEquals("foobar", list.get(0)); + assertEquals("java.util.Collections$UnmodifiableRandomAccessList", list.getClass().getName()); + assertThrowsExactly(UnsupportedOperationException.class, () -> list.add("hello world")); + } + + @Test + public void map_shouldMapIntoNewList() { + var input = new ArrayList<>() { + { + add("foobar"); + add("hello world"); + } + }; + + var output = Lists.map(input, (i, v) -> i + "-" + v); + + assertEquals(List.of("foobar", "hello world"), input); + assertEquals(List.of("0-foobar", "1-hello world"), output); + } +} diff --git a/java-codegen/src/test/java/org/opensearch/client/codegen/utils/MapsTests.java b/java-codegen/src/test/java/org/opensearch/client/codegen/utils/MapsTests.java new file mode 100644 index 000000000..19ff63292 --- /dev/null +++ b/java-codegen/src/test/java/org/opensearch/client/codegen/utils/MapsTests.java @@ -0,0 +1,54 @@ +/* + * 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 static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrowsExactly; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.HashMap; +import java.util.Map; +import org.junit.jupiter.api.Test; + +public class MapsTests { + @Test + public void unmodifiableOpt_withNullMap_shouldReturnEmptyOptional() { + assertTrue(Maps.unmodifiableOpt(null).isEmpty()); + } + + @Test + public void unmodifiableOpt_withNonNullMap_shouldReturnUnmodifiableMap() { + var opt = Maps.unmodifiableOpt(new HashMap() { + { + put("foo", "bar"); + } + }); + assertTrue(opt.isPresent()); + var map = opt.get(); + assertEquals(1, map.size()); + assertEquals("bar", map.get("foo")); + assertEquals("java.util.Collections$UnmodifiableMap", map.getClass().getName()); + assertThrowsExactly(UnsupportedOperationException.class, () -> map.put("hello", "world")); + } + + @Test + public void transform_shouldTransformIntoNewMap() { + var input = new HashMap<>() { + { + put("foo", "bar"); + put("hello", "world"); + } + }; + + var output = Maps.transform(input, (k, v) -> v, (k, v) -> k + "-" + v); + + assertEquals(Map.of("foo", "bar", "hello", "world"), input); + assertEquals(Map.of("bar", "foo-bar", "world", "hello-world"), output); + } +} diff --git a/java-codegen/src/test/java/org/opensearch/client/codegen/utils/MavenArtifactResolverTests.java b/java-codegen/src/test/java/org/opensearch/client/codegen/utils/MavenArtifactResolverTests.java new file mode 100644 index 000000000..5c976b053 --- /dev/null +++ b/java-codegen/src/test/java/org/opensearch/client/codegen/utils/MavenArtifactResolverTests.java @@ -0,0 +1,51 @@ +/* + * 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 static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.apache.commons.codec.digest.DigestUtils; +import org.junit.jupiter.api.Test; + +public class MavenArtifactResolverTests { + @Test + public void resolves() throws IOException { + // https://repo.maven.apache.org/maven2/com/google/code/findbugs/jsr305/3.0.2/ + final var group = "com.google.code.findbugs"; + final var name = "jsr305"; + final var version = "3.0.2"; + final var md5Sum = "dd83accb899363c32b07d7a1b2e4ce40"; + final var fileSize = 19936; + + final var coordinates = group + ":" + name + ":" + version; + final var fileSubPath = Stream.concat(Arrays.stream(group.split("\\.")), Stream.of(name, version, name + "-" + version + ".jar")) + .collect(Collectors.joining(File.separator)); + + var files = MavenArtifactResolver.createDefault().resolve(false, Collections.singleton(coordinates)); + assertEquals(1, files.size()); + + var file = files.iterator().next(); + + assertTrue(file.getAbsolutePath().endsWith(fileSubPath)); + assertTrue(file.exists()); + assertEquals(fileSize, file.length()); + + try (var is = new FileInputStream(file)) { + assertEquals(md5Sum, DigestUtils.md5Hex(is)); + } + } +} diff --git a/java-codegen/src/test/java/org/opensearch/client/codegen/utils/SetsTests.java b/java-codegen/src/test/java/org/opensearch/client/codegen/utils/SetsTests.java new file mode 100644 index 000000000..026c00b1f --- /dev/null +++ b/java-codegen/src/test/java/org/opensearch/client/codegen/utils/SetsTests.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.utils; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrowsExactly; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.HashSet; +import java.util.List; +import org.junit.jupiter.api.Test; + +public class SetsTests { + @Test + public void unmodifiableOpt_withNullSet_shouldReturnEmptyOptional() { + assertTrue(Sets.unmodifiableOpt(null).isEmpty()); + } + + @Test + public void unmodifiableOpt_withNonNullSet_shouldReturnUnmodifiableSet() { + var opt = Sets.unmodifiableOpt(new HashSet<>(List.of("foobar"))); + assertTrue(opt.isPresent()); + var set = opt.get(); + assertEquals(1, set.size()); + assertEquals("foobar", set.iterator().next()); + assertEquals("java.util.Collections$UnmodifiableSet", set.getClass().getName()); + assertThrowsExactly(UnsupportedOperationException.class, () -> set.add("hello world")); + } +} diff --git a/java-codegen/src/test/java/org/opensearch/client/codegen/utils/StreamsTests.java b/java-codegen/src/test/java/org/opensearch/client/codegen/utils/StreamsTests.java new file mode 100644 index 000000000..67df3194a --- /dev/null +++ b/java-codegen/src/test/java/org/opensearch/client/codegen/utils/StreamsTests.java @@ -0,0 +1,33 @@ +/* + * 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 static org.junit.jupiter.api.Assertions.assertArrayEquals; + +import java.util.HashMap; +import java.util.stream.Stream; +import org.junit.jupiter.api.Test; + +public class StreamsTests { + @Test + public void sortedBy() { + var stream = Stream.of("a", "b", "c", "d", "e"); + var ordering = new HashMap() { + { + put("c", 1); + put("e", 2); + put("a", 3); + put("d", 4); + put("b", 5); + } + }; + var output = Streams.sortedBy(stream, ordering::get).toArray(String[]::new); + assertArrayEquals(new String[] { "c", "e", "a", "d", "b" }, output); + } +} diff --git a/java-codegen/src/test/java/org/opensearch/client/codegen/utils/StringsTests.java b/java-codegen/src/test/java/org/opensearch/client/codegen/utils/StringsTests.java new file mode 100644 index 000000000..806374333 --- /dev/null +++ b/java-codegen/src/test/java/org/opensearch/client/codegen/utils/StringsTests.java @@ -0,0 +1,114 @@ +/* + * 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 static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrowsExactly; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.NullSource; +import org.junit.jupiter.params.provider.ValueSource; + +public class StringsTests { + @ParameterizedTest(name = "[{index}] \"{0}\" is a null or empty string") + @NullSource + @ValueSource(strings = { "" }) + public void isNullOrEmpty_withNullOrEmptyInput_shouldBeTrue(String input) { + assertTrue(Strings.isNullOrEmpty(input)); + } + + @ParameterizedTest(name = "[{index}] \"{0}\" is not an empty string") + @ValueSource(strings = { " ", " ", "\n", "foo", "bar", "baz" }) + public void isNullOrEmpty_withNonEmptyInput_shouldBeFalse(String input) { + assertFalse(Strings.isNullOrEmpty(input)); + } + + @ParameterizedTest(name = "[{index}] \"{0}\" is a null or blank string") + @NullSource + @ValueSource(strings = { "", " ", " ", "\n" }) + public void isNullOrBlank_withNullOrBlankInput_shouldBeTrue(String input) { + assertTrue(Strings.isNullOrBlank(input)); + } + + @ParameterizedTest(name = "[{index}] \"{0}\" is not a blank string") + @ValueSource(strings = { "foo", "bar", "baz" }) + public void isNullOrBlank_withNonBlankInput_shouldBeFalse(String input) { + assertFalse(Strings.isNullOrBlank(input)); + } + + @ParameterizedTest(name = "[{index}] \"{0}\" is a disallowed blank string") + @NullSource + @ValueSource(strings = { "", " ", " ", "\n" }) + public void requireNonBlank_withNullOrBlankInput_shouldThrowException(String input) { + var message = "input `" + input + "` must not be blank"; + assertThrowsExactly(IllegalArgumentException.class, () -> Strings.requireNonBlank(input, message), message); + } + + @ParameterizedTest(name = "[{index}] \"{0}\" is not a blank string") + @ValueSource(strings = { "foo", "bar", "baz" }) + public void requireNonBlank_withNonBlankInput_shouldPassThroughValue(String input) { + var output = assertDoesNotThrow(() -> Strings.requireNonBlank(input, "input must not be blank")); + assertEquals(input, output); + } + + @ParameterizedTest(name = "[{index}] \"{0}\" in snake case should be \"{1}\"") + @CsvSource({ + "camelCase, camel_case", + "PascalCase, pascal_case", + "snake_case, snake_case", + "SHOUTY_SNAKE, shouty_snake", + "kebab-case, kebab_case", + "SHOUTY-KEBAB, shouty_kebab", + "dot.separated, dot_separated", + "colon:separated, colon_separated", + "oneword, oneword", + "Spaced out, spaced_out" }) + public void toSnakeCase_shouldReturnSnakeCaseOfInput(String input, String expected) { + var output = Strings.toSnakeCase(input); + assertEquals(expected, output); + } + + @ParameterizedTest(name = "[{index}] \"{0}\" in camel case should be \"{1}\"") + @CsvSource({ + "camelCase, camelCase", + "PascalCase, pascalCase", + "snake_case, snakeCase", + "SHOUTY_SNAKE, shoutySnake", + "kebab-case, kebabCase", + "SHOUTY-KEBAB, shoutyKebab", + "dot.separated, dotSeparated", + "colon:separated, colonSeparated", + "oneword, oneword", + "Spaced out, spacedOut" }) + public void toCamelCase_shouldReturnCamelCaseOfInput(String input, String expected) { + var output = Strings.toCamelCase(input); + assertEquals(expected, output); + } + + @ParameterizedTest(name = "[{index}] \"{0}\" in pascal case should be \"{1}\"") + @CsvSource({ + "camelCase, CamelCase", + "PascalCase, PascalCase", + "snake_case, SnakeCase", + "SHOUTY_SNAKE, ShoutySnake", + "kebab-case, KebabCase", + "SHOUTY-KEBAB, ShoutyKebab", + "dot.separated, DotSeparated", + "colon:separated, ColonSeparated", + "oneword, Oneword", + "Spaced out, SpacedOut" }) + public void toPascalCase_shouldReturnPascalCaseOfInput(String input, String expected) { + var output = Strings.toPascalCase(input); + assertEquals(expected, output); + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 9c0431612..ae27c7cf6 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -36,4 +36,5 @@ plugins { rootProject.name = "opensearch-java" include("java-client") +include("java-codegen") include("samples")