diff --git a/cspell.yaml b/cspell.yaml index f0a0312be2..a0341e9365 100644 --- a/cspell.yaml +++ b/cspell.yaml @@ -6,8 +6,11 @@ dictionaries: - typescript words: - Adoptium + - alzimmer + - amqp - AQID - atrule + - atteo - autorest - azsdk - azsdkengsys @@ -22,6 +25,7 @@ words: - cadlplayground - clsx - cobertura + - codehaus - codeql - Contoso - CORGE @@ -31,15 +35,19 @@ words: - dbaeumer - Dcodegen - debouncer + - Declipse - Dedupes - destructures - devdiv - Diagnoser + - Dlog - dogfood - Dorg + - Dosgi - Dskip - eastus - ecmarkup + - EMBEDME - esbenp - esbuild - espt @@ -47,13 +55,19 @@ words: - fluentui - genproto - globby + - graalvm + - Gson + - imple - Infima - inlines - inmemory - instanceid - interner - intrinsics + - itor + - Jacoco - jdwp + - jobject - jsyaml - keyer - lifecyle @@ -62,7 +76,9 @@ words: - LINUXVMIMAGE - lzutf - MACVMIMAGE + - mgmt - mocharc + - mqtt - msbuild - MSRC - multis @@ -73,9 +89,11 @@ words: - noopener - noreferrer - nostdlib + - noverify - npmjs - nupkg - oapi + - ODATA - OIDC - oneds - oneof @@ -92,6 +110,7 @@ words: - protoc - psscriptanalyzer - pwsh + - reactivex - recase - regen - respecify @@ -99,8 +118,13 @@ words: - rushx - safeint - segmentof + - serde - sfixed - sint + - snakeyaml + - srnagar + - statment + - sses - ssvs - strs - syncpack @@ -120,6 +144,7 @@ words: - Uncapitalize - uncollapsed - undifferentiable + - Ungroup - uninstantiated - unioned - unparented @@ -139,6 +164,7 @@ words: - WINDOWSVMIMAGE - xiaofei - xlarge + - xors - xplat ignorePaths: - "**/node_modules/**" diff --git a/packages/http-client-java/eng/scripts/Generate.ps1 b/packages/http-client-java/eng/scripts/Generate.ps1 index a854cac607..f3bc96d1b3 100644 --- a/packages/http-client-java/eng/scripts/Generate.ps1 +++ b/packages/http-client-java/eng/scripts/Generate.ps1 @@ -18,4 +18,4 @@ Invoke "npm run build:emitter" $testDir = Join-Path $repoRoot 'test' -Invoke "npx tsp compile $testDir/literal.tsp --trace @typespec/http-client-java --emit @typespec/http-client-java --option @typespec/http-client-java.emitter-output-dir=$testDir --option @typespec/http-client-java.save-inputs=true" +Invoke "npx tsp compile $testDir/literal.tsp --trace @typespec/http-client-java --emit @typespec/http-client-java --option @typespec/http-client-java.emitter-output-dir=$testDir/tsp-output --option @typespec/http-client-java.save-inputs=true" diff --git a/packages/http-client-java/generator/http-client-generator-core/pom.xml b/packages/http-client-java/generator/http-client-generator-core/pom.xml new file mode 100644 index 0000000000..4839948754 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/pom.xml @@ -0,0 +1,74 @@ + + 4.0.0 + + com.microsoft.typespec + typespec-java-generator + 1.0.0-beta.1 + + + com.microsoft.typespec + http-client-generator-core + jar + + http-client-generator-core + http://maven.apache.org + + + UTF-8 + + + + + com.azure + azure-core + 1.51.0 + + + com.azure + azure-json + 1.2.0 + + + com.azure + azure-xml + 1.1.0 + + + org.yaml + snakeyaml + 2.0 + + + org.slf4j + slf4j-api + 1.7.36 + + + + com.github.javaparser + javaparser-core + 3.25.10 + + + org.apache.ant + ant + 1.10.14 + + + org.eclipse.lsp4j + org.eclipse.lsp4j + 0.21.1 + + + org.eclipse.jdt + org.eclipse.jdt.core + 3.33.0 + + + org.atteo + evo-inflector + 1.3 + + + diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/Javagen.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/Javagen.java new file mode 100644 index 0000000000..79388efa65 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/Javagen.java @@ -0,0 +1,365 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core; + +import com.microsoft.typespec.http.client.generator.core.extension.jsonrpc.Connection; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.ApiVersion; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.CodeModel; +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; +import com.microsoft.typespec.http.client.generator.core.extension.plugin.NewPlugin; +import com.microsoft.typespec.http.client.generator.core.extension.plugin.PluginLogger; +import com.microsoft.typespec.http.client.generator.core.mapper.Mappers; +import com.microsoft.typespec.http.client.generator.core.mapper.PomMapper; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.AsyncSyncClient; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.Client; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientBuilder; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientException; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethodExample; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientModel; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientModels; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientResponse; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.EnumType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.MethodGroupClient; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.PackageInfo; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.Pom; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ProtocolExample; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ServiceClient; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ServiceVersion; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.TestContext; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.UnionModels; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.XmlSequenceWrapper; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaFile; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaPackage; +import com.microsoft.typespec.http.client.generator.core.model.projectmodel.Project; +import com.microsoft.typespec.http.client.generator.core.model.projectmodel.TextFile; +import com.microsoft.typespec.http.client.generator.core.model.xmlmodel.XmlFile; +import com.microsoft.typespec.http.client.generator.core.postprocessor.Postprocessor; +import com.microsoft.typespec.http.client.generator.core.preprocessor.Preprocessor; +import com.microsoft.typespec.http.client.generator.core.util.ClientModelUtil; +import com.microsoft.typespec.http.client.generator.core.util.SchemaUtil; +import com.azure.core.util.CoreUtils; +import org.slf4j.Logger; +import org.yaml.snakeyaml.DumperOptions; +import org.yaml.snakeyaml.LoaderOptions; +import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.constructor.Constructor; +import org.yaml.snakeyaml.inspector.TrustedTagInspector; +import org.yaml.snakeyaml.introspector.Property; +import org.yaml.snakeyaml.nodes.NodeTuple; +import org.yaml.snakeyaml.nodes.Tag; +import org.yaml.snakeyaml.representer.Representer; + +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +public class Javagen extends NewPlugin { + private final Logger logger = new PluginLogger(this, Javagen.class); + protected static Javagen instance; + + public Javagen(Connection connection, String plugin, String sessionId) { + super(connection, plugin, sessionId); + instance = this; + } + + public static Javagen getPluginInstance() { + return instance; + } + + @Override + public boolean processInternal() { + this.clear(); + + JavaSettings settings = JavaSettings.getInstance(); + + try { + // Step 1: Parse input yaml as CodeModel + CodeModel codeModel = new Preprocessor(this, connection, pluginName, sessionId) + .processCodeModel(); + + // Step 2: Map + Client client = Mappers.getClientMapper().map(codeModel); + + // Step 3: Write to templates + JavaPackage javaPackage = writeToTemplates(codeModel, client, settings, true); + + //Step 4: Print to files + // Then for each formatted file write the file. This is done synchronously as there is potential race + // conditions that can lead to deadlocking. + new Postprocessor(this).postProcess(javaPackage.getJavaFiles().stream() + .collect(Collectors.toMap(JavaFile::getFilePath, file -> file.getContents().toString()))); + + for (XmlFile xmlFile : javaPackage.getXmlFiles()) { + writeFile(xmlFile.getFilePath(), xmlFile.getContents().toString(), null); + } + for (TextFile textFile : javaPackage.getTextFiles()) { + writeFile(textFile.getFilePath(), textFile.getContents(), null); + } + + String artifactId = ClientModelUtil.getArtifactId(); + if (!CoreUtils.isNullOrEmpty(artifactId)) { + writeFile("src/main/resources/" + artifactId + ".properties", + "name=${project.artifactId}\nversion=${project.version}\n", null); + } + } catch (Exception ex) { + logger.error("Failed to generate code.", ex); + return false; + } + return true; + } + + CodeModel parseCodeModel(String fileName) { + String file = readFile(fileName); + Representer representer = new Representer(new DumperOptions()) { + @Override + protected NodeTuple representJavaBeanProperty(Object javaBean, Property property, Object propertyValue, + Tag customTag) { + // if value of property is null, ignore it. + if (propertyValue == null) { + return null; + } else { + return super.representJavaBeanProperty(javaBean, property, propertyValue, customTag); + } + } + }; + + LoaderOptions loaderOptions = new LoaderOptions(); + loaderOptions.setCodePointLimit(50 * 1024 * 1024); + loaderOptions.setMaxAliasesForCollections(Integer.MAX_VALUE); + loaderOptions.setNestingDepthLimit(Integer.MAX_VALUE); + loaderOptions.setTagInspector(new TrustedTagInspector()); + Yaml newYaml = new Yaml(new Constructor(loaderOptions), representer, new DumperOptions(), loaderOptions); + return newYaml.loadAs(file, CodeModel.class); + } + + protected JavaPackage writeToTemplates(CodeModel codeModel, Client client, JavaSettings settings, + boolean generateSwaggerMarkdown) { + JavaPackage javaPackage = new JavaPackage(this); + if (client.getServiceClient() != null || !CoreUtils.isNullOrEmpty(client.getServiceClients())) { + // Service client + if (CoreUtils.isNullOrEmpty(client.getServiceClients())) { + javaPackage.addServiceClient(client.getServiceClient().getPackage(), + client.getServiceClient().getClassName(), client.getServiceClient()); + } else { + // multi-client from TypeSpec + for (ServiceClient serviceClient : client.getServiceClients()) { + javaPackage.addServiceClient(serviceClient.getPackage(), serviceClient.getClassName(), serviceClient); + } + } + + if (settings.isGenerateClientInterfaces()) { + javaPackage.addServiceClientInterface(client.getServiceClient().getInterfaceName(), + client.getServiceClient()); + } + + // Async/sync service clients + for (AsyncSyncClient asyncClient : client.getAsyncClients()) { + javaPackage.addAsyncServiceClient(asyncClient.getPackageName(), asyncClient); + } + for (AsyncSyncClient syncClient : client.getSyncClients()) { + boolean syncClientWrapAsync = settings.isSyncClientWrapAsyncClient() + // HLC could have sync method that is harder to convert, e.g. Flux -> InputStream + && settings.isDataPlaneClient() + // 1-1 match of SyncClient and AsyncClient + && client.getAsyncClients().size() == client.getSyncClients().size(); + javaPackage.addSyncServiceClient(syncClient.getPackageName(), syncClient, syncClientWrapAsync); + } + + // Service client builder + for (ClientBuilder clientBuilder : client.getClientBuilders()) { + javaPackage.addServiceClientBuilder(clientBuilder); + } + + // Method group + if (CoreUtils.isNullOrEmpty(client.getServiceClients())) { + writeMethodGroupClient(javaPackage, client.getServiceClient(), settings); + } else { + // multi-client from TypeSpec + for (ServiceClient serviceClient : client.getServiceClients()) { + writeMethodGroupClient(javaPackage, serviceClient, settings); + } + } + + // Sample + if (settings.isDataPlaneClient() && settings.isGenerateSamples()) { + for (ProtocolExample protocolExample : client.getProtocolExamples()) { + javaPackage.addProtocolExamples(protocolExample); + } + for (ClientMethodExample clientMethodExample : client.getClientMethodExamples()) { + javaPackage.addClientMethodExamples(clientMethodExample); + } + } + + // Test + if (settings.isDataPlaneClient() && settings.isGenerateTests()) { + if (!client.getSyncClients().isEmpty() && client.getSyncClients().iterator().next().getClientBuilder() != null) { + List serviceClients = client.getServiceClients(); + if (CoreUtils.isNullOrEmpty(serviceClients)) { + serviceClients = Collections.singletonList(client.getServiceClient()); + } + TestContext testContext = new TestContext(serviceClients, client.getSyncClients()); + + // base test class + javaPackage.addProtocolTestBase(testContext); + + // test cases as Disabled + if (!client.getProtocolExamples().isEmpty()) { + client.getProtocolExamples().forEach(protocolExample -> javaPackage.addProtocolTest(new TestContext<>(testContext, protocolExample))); + } + if (!client.getClientMethodExamples().isEmpty()) { + client.getClientMethodExamples().forEach(clientMethodExample -> javaPackage.addClientMethodTest(new TestContext<>(testContext, clientMethodExample))); + } + } + } + + // Service version + if (settings.isDataPlaneClient()) { + String packageName = settings.getPackage(); + if (CoreUtils.isNullOrEmpty(client.getServiceClients())) { + List serviceVersions = settings.getServiceVersions(); + if (CoreUtils.isNullOrEmpty(serviceVersions)) { + List apiVersions = ClientModelUtil.getApiVersions(codeModel); + if (!CoreUtils.isNullOrEmpty(apiVersions)) { + serviceVersions = apiVersions; + } else { + throw new IllegalArgumentException("'api-version' not found. Please configure 'serviceVersions' option."); + } + } + + String serviceName; + if (settings.getServiceName() == null) { + serviceName = client.getServiceClient().getInterfaceName(); + } else { + serviceName = settings.getServiceName(); + } + String className = ClientModelUtil.getServiceVersionClassName(ClientModelUtil.getClientInterfaceName(codeModel)); + javaPackage.addServiceVersion(packageName, new ServiceVersion(className, serviceName, serviceVersions)); + } else { + // multi-client from TypeSpec + for (com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Client client1 : codeModel.getClients()) { + if (client1.getServiceVersion() != null) { + javaPackage.addServiceVersion(packageName, + new ServiceVersion( + SchemaUtil.getJavaName(client1.getServiceVersion()), + client1.getServiceVersion().getLanguage().getDefault().getDescription(), + client1.getApiVersions().stream().map(ApiVersion::getVersion).collect(Collectors.toList()))); + } + } + } + } + } + + // GraalVM config + if (settings.isGenerateGraalVmConfig()) { + javaPackage.addGraalVmConfig(Project.AZURE_GROUP_ID, ClientModelUtil.getArtifactId(), client.getGraalVmConfig()); + } + + writeClientModels(client, javaPackage, settings); + + writeHelperClasses(client, codeModel, javaPackage, settings); + + // Unit tests on client model + if (settings.isGenerateTests() && !settings.isDataPlaneClient()) { + for (ClientModel model : client.getModels()) { + if (!model.isStronglyTypedHeader()) { + javaPackage.addModelUnitTest(model); + } + } + } + + // Package-info + for (PackageInfo packageInfo : client.getPackageInfos()) { + javaPackage.addPackageInfo(packageInfo.getPackage(), "package-info", packageInfo); + } + + if (settings.isDataPlaneClient()) { + Project project = new Project(client, ClientModelUtil.getApiVersions(codeModel)); + if (settings.isSdkIntegration()) { + project.integrateWithSdk(); + } + + Set externalPackageNames = ClientModelUtil.getExternalPackageNamesUsedInClient(client.getModels(), codeModel); + client.getModuleInfo().checkForAdditionalDependencies(externalPackageNames); + project.checkForAdditionalDependencies(externalPackageNames); + + // Module-info + javaPackage.addModuleInfo(client.getModuleInfo()); + + // POM + if (settings.isRegeneratePom()) { + Pom pom = new PomMapper().map(project); + javaPackage.addPom("pom.xml", pom); + } + + // Readme, Changelog + if (settings.isSdkIntegration()) { + javaPackage.addReadmeMarkdown(project); + if (generateSwaggerMarkdown) { + javaPackage.addSwaggerReadmeMarkdown(project); + } + javaPackage.addChangelogMarkdown(project); + + // test proxy asserts.json + javaPackage.addTestProxyAssetsJson(project); + + // Blank readme sample + javaPackage.addProtocolExamplesBlank(); + } + } + return javaPackage; + } + + protected void writeClientModels(Client client, JavaPackage javaPackage, JavaSettings settings) { + if (!settings.isDataPlaneClient()) { + // Client model + for (ClientModel model : client.getModels()) { + javaPackage.addModel(model.getPackage(), model.getName(), model); + } + + // Enum + for (EnumType enumType : client.getEnums()) { + javaPackage.addEnum(enumType.getPackage(), enumType.getName(), enumType); + } + + // Response + for (ClientResponse response : client.getResponseModels()) { + javaPackage.addClientResponse(response.getPackage(), response.getName(), response); + } + + // Exception + for (ClientException exception : client.getExceptions()) { + javaPackage.addException(exception.getPackage(), exception.getName(), exception); + } + + // XML sequence wrapper + for (XmlSequenceWrapper xmlSequenceWrapper : client.getXmlSequenceWrappers()) { + javaPackage.addXmlSequenceWrapper(xmlSequenceWrapper.getPackage(), + xmlSequenceWrapper.getWrapperClassName(), xmlSequenceWrapper); + } + } + } + + protected void writeHelperClasses(Client client, CodeModel codeModel, JavaPackage javaPackage, JavaSettings settings) { + } + + private static void writeMethodGroupClient(JavaPackage javaPackage, ServiceClient serviceClient, JavaSettings settings) { + for (MethodGroupClient methodGroupClient : serviceClient.getMethodGroupClients()) { + javaPackage.addMethodGroup(methodGroupClient.getPackage(), methodGroupClient.getClassName(), methodGroupClient); + if (settings.isGenerateClientInterfaces()) { + javaPackage.addMethodGroupInterface(methodGroupClient.getInterfaceName(), methodGroupClient); + } + } + } + + private void clear() { + ClientModels.getInstance().clear(); + UnionModels.getInstance().clear(); + JavaSettings.clear(); + } + + public Logger getLogger() { + return this.logger; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/Main.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/Main.java new file mode 100644 index 0000000000..c89dea708f --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/Main.java @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core; + +import com.microsoft.typespec.http.client.generator.core.extension.jsonrpc.Connection; + +public class Main { + + public static void main(String[] args) { + Connection connection = new Connection(System.out, System.in); + connection.dispatch("GetPluginNames", () -> "[\"javagen\"]"); + connection.dispatch("Process", (plugin, sessionId) -> new Javagen(connection, plugin, sessionId).process()); + connection.dispatchNotification("Shutdown", connection::stop); + connection.waitForAll(); + System.exit(0); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/customization/AnnotationCustomization.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/customization/AnnotationCustomization.java new file mode 100644 index 0000000000..6b6019dece --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/customization/AnnotationCustomization.java @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.customization; + +import com.microsoft.typespec.http.client.generator.core.customization.implementation.ls.EclipseLanguageClient; +import org.eclipse.lsp4j.SymbolInformation; + +import java.lang.annotation.Annotation; + +public final class AnnotationCustomization extends CodeCustomization { + private final String packageName; + private final String className; + private final String methodName; + private final String fieldName; + private final A annotation; + + static AnnotationCustomization createClassAnnotationCustomization(Editor editor, + EclipseLanguageClient languageClient, SymbolInformation symbol, String packageName, String className, + A annotation) { + return new AnnotationCustomization<>(editor, languageClient, symbol, packageName, className, null, null, + annotation); + } + + static AnnotationCustomization createMethodAnnotationCustomization(Editor editor, + EclipseLanguageClient languageClient, SymbolInformation symbol, String packageName, String className, + String methodName, A annotation) { + return new AnnotationCustomization<>(editor, languageClient, symbol, packageName, className, methodName, null, + annotation); + } + + static AnnotationCustomization createFieldAnnotationCustomization(Editor editor, + EclipseLanguageClient languageClient, SymbolInformation symbol, String packageName, String className, + String fieldName, A annotation) { + return new AnnotationCustomization<>(editor, languageClient, symbol, packageName, className, null, fieldName, + annotation); + } + + private AnnotationCustomization(Editor editor, EclipseLanguageClient languageClient, SymbolInformation symbol, + String packageName, String className, String methodName, String fieldName, A annotation) { + super(editor, languageClient, symbol); + this.packageName = packageName; + this.className = className; + this.methodName = methodName; + this.fieldName = fieldName; + this.annotation = annotation; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/customization/ClassCustomization.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/customization/ClassCustomization.java new file mode 100644 index 0000000000..8a17da5611 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/customization/ClassCustomization.java @@ -0,0 +1,611 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.customization; + +import com.microsoft.typespec.http.client.generator.core.customization.implementation.Utils; +import com.microsoft.typespec.http.client.generator.core.customization.implementation.ls.EclipseLanguageClient; +import com.github.javaparser.StaticJavaParser; +import com.github.javaparser.ast.CompilationUnit; +import org.eclipse.lsp4j.FileChangeType; +import org.eclipse.lsp4j.FileEvent; +import org.eclipse.lsp4j.Position; +import org.eclipse.lsp4j.SymbolInformation; +import org.eclipse.lsp4j.SymbolKind; +import org.eclipse.lsp4j.TextEdit; +import org.eclipse.lsp4j.WorkspaceEdit; + +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +/** + * The class level customization for an AutoRest generated class. + */ +public final class ClassCustomization extends CodeCustomization { + private static final int INDENT_LENGTH = 4; + + /* + * This pattern attempts to find the first line of a method string that doesn't have a first non-space character of + * '*' or '/'. From there it captures all word and space characters before and inside '( )' ignoring any trailing + * spaces and an opening '{'. + */ + private static final Pattern METHOD_SIGNATURE_PATTERN = + Pattern.compile("^\\s*([^/*][\\w\\s]+\\([\\w\\s<>,\\.]*\\))\\s*\\{?$", Pattern.MULTILINE); + + /* + * This pattern attempts to find the first line of a constructor string that doesn't have a first non-space + * character of '*' or '/', effectively the first non-Javadoc line. From there it captures all word and space + * characters before and inside '( )' ignoring any trailing spaces and an opening '{'. + */ + private static final Pattern CONSTRUCTOR_SIGNATURE_PATTERN = + Pattern.compile("^\\s*([^/*][\\w\\s]+\\([\\w\\s<>,\\.]*\\))\\s*\\{?$", Pattern.MULTILINE); + + private static final Pattern BLOCK_OPEN = Pattern.compile("\\) *\\{"); + private static final Pattern PUBLIC_MODIFIER = Pattern.compile(" *public "); + private static final Pattern PRIVATE_MODIFIER = Pattern.compile(" *private "); + private static final Pattern MEMBER_PARAMS = Pattern.compile("\\(.*\\)"); + + private final String packageName; + private final String className; + + ClassCustomization(Editor editor, EclipseLanguageClient languageClient, String packageName, String className, + SymbolInformation classSymbol) { + super(editor, languageClient, classSymbol); + + this.packageName = packageName; + this.className = className; + } + + /** + * Gets the name of the class this customization is using. + * + * @return The name of the class. + */ + public String getClassName() { + return className; + } + + /** + * Adds imports to the class. + * + * @param imports Imports to add. + * @return A new {@link ClassCustomization} updated with the new imports for chaining. + */ + public ClassCustomization addImports(String... imports) { + if (imports != null) { + return Utils.addImports(Arrays.asList(imports), this, this::refreshSymbol); + } + + return this; + } + + /** + * Adds a static block to the class. The {@code staticCodeBlock} should include the static keyword followed by + * the static code. + * @param staticCodeBlock The static code block including the static keyword. + * @return The updated {@link ClassCustomization}. + */ + public ClassCustomization addStaticBlock(String staticCodeBlock) { + return addStaticBlock(staticCodeBlock, null); + } + + /** + * Adds a static block to the class. + * + * @param staticCodeBlock The static code block. If this is {@code null} or an empty string, the class is not + * modified and the {@link ClassCustomization} instance is returned without any change. + * @param importsToAdd The list of imports to add to the class. + * @return The updated {@link ClassCustomization}. + */ + public ClassCustomization addStaticBlock(String staticCodeBlock, List importsToAdd) { + + if (Utils.isNullOrEmpty(staticCodeBlock)) { + return this; + } + + // the class declaration line + int lastSymbolLine = symbol.getLocation().getRange().getStart().getLine(); + + // Find the last field symbol. + Optional lastSymbol = languageClient.listDocumentSymbols(fileUri).stream() + .filter(symbol -> symbol.getKind() == SymbolKind.Field) + .reduce((first, second) -> second); + + int indentAmount = INDENT_LENGTH; + if (lastSymbol.isPresent()) { + // the line number of the last field declaration + lastSymbolLine = lastSymbol.get().getLocation().getRange().getStart().getLine(); + indentAmount = Utils.getIndent(editor.getFileLine(fileName, lastSymbolLine)).length(); + } +// System.out.println("indent amount " + indentAmount); + + // start the static block from the next line of the last field or the line after class declaration + int staticBlockStartLine = lastSymbolLine + 1; + editor.insertBlankLine(fileName, staticBlockStartLine, false); + Position staticBlockPosition = editor.insertBlankLineWithIndent(fileName, staticBlockStartLine, indentAmount); + if(!staticCodeBlock.trim().startsWith("static")) { + staticCodeBlock = "static { " + System.lineSeparator() + staticCodeBlock + System.lineSeparator() + "}"; + } + + editor.replaceWithIndentedContent(fileName, staticBlockPosition, staticBlockPosition, staticCodeBlock, + staticBlockPosition.getCharacter()); + if (importsToAdd != null) { + return Utils.addImports(importsToAdd, this, this::refreshSymbol); + } + return this; + } + + /** + * Gets the method level customization for a method in the class. + * + * @param methodNameOrSignature the method name or signature + * @return the method level customization + */ + public MethodCustomization getMethod(String methodNameOrSignature) { + String methodName; + String methodSignature = null; + if (methodNameOrSignature.contains("(")) { + // method signature + methodSignature = BLOCK_OPEN.matcher(methodNameOrSignature).replaceFirst(""); + methodSignature = PUBLIC_MODIFIER.matcher(methodSignature).replaceFirst(""); + methodSignature = PRIVATE_MODIFIER.matcher(methodSignature).replaceFirst(""); + + String returnTypeAndMethodName = methodNameOrSignature.split("\\(")[0]; + if (returnTypeAndMethodName.contains(" ")) { + methodName = Utils.ANYTHING_THEN_SPACE_PATTERN.matcher(returnTypeAndMethodName).replaceAll(""); + } else { + methodName = returnTypeAndMethodName; + } + } else { + methodName = methodNameOrSignature; + } + Optional methodSymbol = languageClient.listDocumentSymbols(fileUri) + .stream().filter(si -> si.getKind() == SymbolKind.Method + && MEMBER_PARAMS.matcher(si.getName()).replaceFirst("").equals(methodName)) + .filter(si -> editor.getFileLine(fileName, si.getLocation().getRange().getStart().getLine()).contains(methodNameOrSignature)) + .findFirst(); + if (methodSymbol.isEmpty()) { + throw new IllegalArgumentException("Method " + methodNameOrSignature + " does not exist in class " + className); + } + if (methodSignature == null) { + methodSignature = editor.getFileLine(fileName, methodSymbol.get().getLocation().getRange().getStart().getLine()); + methodSignature = BLOCK_OPEN.matcher(methodSignature).replaceFirst(""); + methodSignature = PUBLIC_MODIFIER.matcher(methodSignature).replaceFirst(""); + methodSignature = PRIVATE_MODIFIER.matcher(methodSignature).replaceFirst(""); + } + return new MethodCustomization(editor, languageClient, packageName, className, methodName, methodSignature, methodSymbol.get()); + } + + /** + * Gets the constructor level customization for a constructor in the class. + *

+ * If only the constructor name is passed and the class has multiple constructors an error will be thrown to prevent + * ambiguous runtime behavior. + * + * @param constructorNameOrSignature The constructor name or signature. + * @return The constructor level customization. + * @throws IllegalStateException If only the constructor name is passed and the class has multiple constructors. + */ + public ConstructorCustomization getConstructor(String constructorNameOrSignature) { + String constructorName; + String constructorSignature = null; + if (constructorNameOrSignature.contains("(")) { + // method signature + constructorSignature = BLOCK_OPEN.matcher(constructorNameOrSignature).replaceFirst(""); + constructorSignature = PUBLIC_MODIFIER.matcher(constructorSignature).replaceFirst(""); + constructorSignature = PRIVATE_MODIFIER.matcher(constructorSignature).replaceFirst(""); + String returnTypeAndMethodName = constructorNameOrSignature.split("\\(")[0]; + if (returnTypeAndMethodName.contains(" ")) { + constructorName = Utils.ANYTHING_THEN_SPACE_PATTERN.matcher(returnTypeAndMethodName).replaceAll(""); + } else { + constructorName = returnTypeAndMethodName; + } + } else { + constructorName = constructorNameOrSignature; + } + + List constructorSymbol = languageClient.listDocumentSymbols(fileUri) + .stream().filter(si -> si.getKind() == SymbolKind.Constructor + && MEMBER_PARAMS.matcher(si.getName()).replaceFirst("").equals(constructorName)) + .filter(si -> editor.getFileLine(fileName, si.getLocation().getRange().getStart().getLine()) + .contains(constructorNameOrSignature)) + .collect(Collectors.toList()); + + if (constructorSymbol.size() > 1) { + throw new IllegalStateException("Multiple instances of " + constructorNameOrSignature + " exist in the " + + "class. Use a more specific constructor signature."); + } + + if (constructorSymbol.isEmpty()) { + throw new IllegalArgumentException("Constructor " + constructorNameOrSignature + " does not exist in class " + + className); + } + + if (constructorSignature == null) { + constructorSignature = editor.getFileLine(fileName, constructorSymbol.get(0).getLocation().getRange().getStart().getLine()); + constructorSignature = BLOCK_OPEN.matcher(constructorSignature).replaceFirst(""); + constructorSignature = PUBLIC_MODIFIER.matcher(constructorSignature).replaceFirst(""); + constructorSignature = PRIVATE_MODIFIER.matcher(constructorSignature).replaceFirst(""); + } + return new ConstructorCustomization(editor, languageClient, packageName, className, constructorSignature, + constructorSymbol.get(0)); + } + + /** + * Gets the property level customization for a property in the class. + *

+ * For constant properties use {@link #getConstant(String)}. + * + * @param propertyName the property name + * @return the property level customization + */ + public PropertyCustomization getProperty(String propertyName) { + Optional propertySymbol = languageClient.listDocumentSymbols(fileUri) + .stream().filter(si -> si.getKind() == SymbolKind.Field && si.getName().equals(propertyName)) + .findFirst(); + + if (propertySymbol.isEmpty()) { + throw new IllegalArgumentException("Property " + propertyName + " does not exist in class " + className); + } + + return new PropertyCustomization(editor, languageClient, packageName, className, propertySymbol.get(), + propertyName); + } + + /** + * Gets the constant level customization for a constant in the class. + *

+ * For instance properties use {@link #getProperty(String)}. + * + * @param constantName The constant name. + * @return The constant level customization. + */ + public ConstantCustomization getConstant(String constantName) { + Optional propertySymbol = languageClient.listDocumentSymbols(fileUri) + .stream().filter(si -> si.getKind() == SymbolKind.Constant && si.getName().equals(constantName)) + .findFirst(); + + if (propertySymbol.isEmpty()) { + throw new IllegalArgumentException("Constant " + constantName + " does not exist in class " + className); + } + + return new ConstantCustomization(editor, languageClient, packageName, className, propertySymbol.get(), + constantName); + } + + /** + * Gets the Javadoc customization for this class. + * + * @return the Javadoc customization + */ + public JavadocCustomization getJavadoc() { + return new JavadocCustomization(editor, languageClient, fileUri, fileName, + symbol.getLocation().getRange().getStart().getLine()); + } + + /** + * Adds a constructor to this class. + * + * @param constructor The entire constructor as a literal string. + * @return The constructor level customization for the added constructor. + */ + public ConstructorCustomization addConstructor(String constructor) { + return addConstructor(constructor, null); + } + + /** + * Adds a constructor to this class. + * + * @param constructor The entire constructor as a literal string. + * @param importsToAdd Any additional imports required by the constructor. These will be custom types or types that + * are ambiguous on which to use such as {@code List} or the utility class {@code Arrays}. + * @return The constructor level customization for the added constructor. + */ + public ConstructorCustomization addConstructor(String constructor, List importsToAdd) { + // Get the signature of the constructor. + Matcher constructorSignatureMatcher = CONSTRUCTOR_SIGNATURE_PATTERN.matcher(constructor); + String constructorSignature = null; + if (constructorSignatureMatcher.find()) { + constructorSignature = constructorSignatureMatcher.group(1); + } + + // Find all constructor and field symbols. + List constructorLocationFinder = languageClient.listDocumentSymbols(fileUri).stream() + .filter(symbol -> symbol.getKind() == SymbolKind.Field || symbol.getKind() == SymbolKind.Constructor) + .collect(Collectors.toList()); + + // If no constructors or fields exist in the class place the constructor after the class declaration line. + // Otherwise place the constructor after the last constructor or field. + int constructorStartLine; + if (Utils.isNullOrEmpty(constructorLocationFinder)) { + constructorStartLine = symbol.getLocation().getRange().getStart().getLine(); + } else { + SymbolInformation symbol = constructorLocationFinder.get(constructorLocationFinder.size() - 1); + + // If the last symbol before the new constructor is a field only a new line needs to be inserted. + // Otherwise if the last symbol is a constructor its closing '}' needs to be found and then a new line + // needs to be inserted. + if (symbol.getKind() == SymbolKind.Field) { + constructorStartLine = symbol.getLocation().getRange().getStart().getLine(); + } else { + constructorStartLine = symbol.getLocation().getRange().getStart().getLine(); + + List fileLines = editor.getFileLines(fileName); + String currentLine = fileLines.get(constructorStartLine); + String constructorIdent = Utils.getIndent(currentLine); + while (!currentLine.endsWith("}") || !currentLine.equals(constructorIdent + "}")) { + currentLine = fileLines.get(++constructorStartLine); + } + } + } + + int indentAmount = Utils.getIndent(editor.getFileLine(fileName, constructorStartLine)).length(); + + editor.insertBlankLine(fileName, ++constructorStartLine, false); + Position constructorPosition = editor.insertBlankLineWithIndent(fileName, ++constructorStartLine, indentAmount); + + editor.replaceWithIndentedContent(fileName, constructorPosition, constructorPosition, constructor, + constructorPosition.getCharacter()); + + final String ctorSignature = (constructorSignature == null) + ? editor.getFileLine(fileName, constructorStartLine) + : constructorSignature; + + return Utils.addImports(importsToAdd, this, () -> getConstructor(ctorSignature)); + } + + /** + * Adds a method to this class. + * + * @param method The entire method as a literal string. + * @return The method level customization for the added method. + */ + public MethodCustomization addMethod(String method) { + return addMethod(method, null); + } + + /** + * Adds a method to this class. + * + * @param method The entire method as a literal string. + * @param importsToAdd Any additional imports required by the constructor. These will be custom types or types that + * are ambiguous on which to use such as {@code List} or the utility class {@code Arrays}. + * @return The method level customization for the added method. + */ + public MethodCustomization addMethod(String method, List importsToAdd) { + // Get the signature of the method. + Matcher methodSignatureMatcher = METHOD_SIGNATURE_PATTERN.matcher(method); + String methodSignature = null; + if (methodSignatureMatcher.find()) { + methodSignature = methodSignatureMatcher.group(1); + } + + // find position + List fileLines = editor.getFileLines(fileName); + int lineNum = fileLines.size(); + String currentLine = fileLines.get(--lineNum); + while (!currentLine.endsWith("}") || currentLine.startsWith("}")) { + currentLine = fileLines.get(--lineNum); + } + + int indentAmount = Utils.getIndent(currentLine).length(); + + editor.insertBlankLine(fileName, ++lineNum, false); + Position newMethod = editor.insertBlankLineWithIndent(fileName, ++lineNum, indentAmount); + + // replace + editor.replaceWithIndentedContent(fileName, newMethod, newMethod, method, newMethod.getCharacter()); + + final String mSig = (methodSignature == null) ? editor.getFileLine(fileName, lineNum) : methodSignature; + + return Utils.addImports(importsToAdd, this, () -> getMethod(mSig)); + } + + /** + * Removes a method from this class. + *

+ * If there exists multiple methods with the same name or signature only the first one found will be removed. + *

+ * This method doesn't update usages of the method being removed. If the method was used elsewhere those usages will + * have to be updated or removed in another customization, or customizations. + *

+ * If this removes the only method contained in the class this will result in a class with no methods. + * + * @param methodNameOrSignature The name or signature of the method being removed. + * @return The current ClassCustomization. + */ + public ClassCustomization removeMethod(String methodNameOrSignature) { + MethodCustomization methodCustomization = getMethod(methodNameOrSignature); + + int methodSignatureLine = methodCustomization.getSymbol().getLocation().getRange().getStart().getLine(); + + // Begin by getting the method's Javadoc to determine where to begin removal of the method. + Position start = methodCustomization.getJavadoc().getJavadocRange().getStart(); + + // Find the ending location of the method being removed. + String bodyPositionFinder = editor.getFileLine(fileName, methodSignatureLine); + String methodBlockIndent = Utils.getIndent(bodyPositionFinder); + + // Go until the beginning of the next method is found. + int endLine = Utils.walkDownFileUntilLineMatches(editor, fileName, methodSignatureLine, + lineContent -> lineContent.matches(methodBlockIndent + ".")); + Position end = new Position(endLine, editor.getFileLine(fileName, endLine).length()); + + // Remove the method. + editor.replace(fileName, start, end, ""); + FileEvent fileEvent = new FileEvent(); + fileEvent.setUri(fileUri); + fileEvent.setType(FileChangeType.Changed); + languageClient.notifyWatchedFilesChanged(Collections.singletonList(fileEvent)); + + return this; + } + + /** + * Renames a class in the package. + * + * @param newName the new simple name for this class + * @return The current ClassCustomization. + */ + public ClassCustomization rename(String newName) { + WorkspaceEdit workspaceEdit = languageClient.renameSymbol(fileUri, + symbol.getLocation().getRange().getStart(), newName); + List changes = new ArrayList<>(); + for (Map.Entry> edit : workspaceEdit.getChanges().entrySet()) { + int i = edit.getKey().indexOf("src/main/java/"); + String oldEntry = edit.getKey().substring(i); + if (editor.getContents().containsKey(oldEntry)) { + for (TextEdit textEdit : edit.getValue()) { + editor.replace(oldEntry, textEdit.getRange().getStart(), textEdit.getRange().getEnd(), textEdit.getNewText()); + } + FileEvent fileEvent = new FileEvent(); + fileEvent.setUri(edit.getKey()); + if (oldEntry.endsWith("/" + className + ".java")) { + String newEntry = oldEntry.replace(className + ".java", newName + ".java"); + editor.renameFile(oldEntry, newEntry); + String newUri = edit.getKey().replace(className + ".java", newName + ".java"); + fileEvent.setType(FileChangeType.Deleted); + changes.add(fileEvent); + FileEvent newFile = new FileEvent(); + newFile.setUri(newUri); + newFile.setType(FileChangeType.Created); + changes.add(newFile); + } else { + fileEvent.setType(FileChangeType.Changed); + changes.add(fileEvent); + } + } + } + languageClient.notifyWatchedFilesChanged(changes); + + String packagePath = packageName.replace(".", "/"); + Optional newClassSymbol = languageClient.findWorkspaceSymbol(newName) + .stream().filter(si -> si.getLocation().getUri().endsWith(packagePath + "/" + newName + ".java")) + .findFirst(); + if (newClassSymbol.isEmpty()) { + throw new IllegalArgumentException("Renamed failed with new class " + newName + " not found."); + } + return new ClassCustomization(editor, languageClient, packageName, newName, newClassSymbol.get()); + } + + /** + * Replace the modifier for this class. + *

+ * For compound modifiers such as {@code public abstract} use bitwise OR ({@code |}) of multiple Modifiers, {@code + * Modifier.PUBLIC | Modifier.ABSTRACT}. + *

+ * Pass {@code 0} for {@code modifiers} to indicate that the method has no modifiers. + * + * @param modifiers The {@link Modifier Modifiers} for the class. + * @return The updated ClassCustomization object. + * @throws IllegalArgumentException If the {@code modifier} is less than {@code 0} or any {@link Modifier} included + * in the bitwise OR isn't a valid class {@link Modifier}. + */ + public ClassCustomization setModifier(int modifiers) { + languageClient.listDocumentSymbols(symbol.getLocation().getUri()) + .stream().filter(si -> si.getKind() == SymbolKind.Class && si.getName().equals(className)) + .findFirst() + .ifPresent(symbolInformation -> Utils.replaceModifier(symbolInformation, editor, languageClient, + "(?:.+ )?class " + className, "class " + className, Modifier.classModifiers(), modifiers)); + + return refreshSymbol(); + } + + /** + * Add an annotation on the class. The annotation class will be automatically imported. + * + * @param annotation the annotation to add to the class. The leading @ can be omitted. + * @return the current class customization for chaining + */ + public ClassCustomization addAnnotation(String annotation) { + if (!annotation.startsWith("@")) { + annotation = "@" + annotation; + } + + Optional symbol = languageClient.listDocumentSymbols(fileUri) + .stream().filter(si -> si.getKind() == SymbolKind.Class) + .findFirst(); + if (symbol.isPresent()) { + if (editor.getContents().containsKey(fileName)) { + int line = symbol.get().getLocation().getRange().getStart().getLine(); + Position position = editor.insertBlankLine(fileName, line, true); + editor.replace(fileName, position, position, annotation); + + FileEvent fileEvent = new FileEvent(); + fileEvent.setUri(fileUri); + fileEvent.setType(FileChangeType.Changed); + languageClient.notifyWatchedFilesChanged(Collections.singletonList(fileEvent)); + + Utils.organizeImportsOnRange(languageClient, editor, fileUri, symbol.get().getLocation().getRange()); + } + } + + return refreshSymbol(); + } + + /** + * Remove an annotation from the class. + * + * @param annotation the annotation to remove from the class. The leading @ can be omitted. + * @return the current class customization for chaining + */ + public ClassCustomization removeAnnotation(String annotation) { + return Utils.removeAnnotation(this, compilationUnit -> compilationUnit.getClassByName(className).get() + .getAnnotationByName(Utils.cleanAnnotationName(annotation)), this::refreshSymbol); + } + + /** + * Rename an enum member if the current class is an enum class. + * + * @param enumMemberName the current enum member name + * @param newName the new enum member name + * @return the current class customization for chaining + */ + public ClassCustomization renameEnumMember(String enumMemberName, String newName) { + String fileUri = symbol.getLocation().getUri(); + String lowercaseEnumMemberName = enumMemberName.toLowerCase(); + + List edits = new ArrayList<>(); + for (SymbolInformation si : languageClient.listDocumentSymbols(fileUri)) { + if (!si.getName().toLowerCase().contains(lowercaseEnumMemberName)) { + continue; + } + + edits.add(languageClient.renameSymbol(fileUri, si.getLocation().getRange().getStart(), newName)); + } + Utils.applyWorkspaceEdits(edits, editor, languageClient); + return this; + } + + /** + * Allows for a fully controlled modification of the abstract syntax tree that represents this class. + * + * @param astCustomization The abstract syntax tree customization callback. + * @return A new ClassCustomization for this class with the abstract syntax tree changes applied. + */ + public ClassCustomization customizeAst(Consumer astCustomization) { + CompilationUnit astToEdit = StaticJavaParser.parse(editor.getFileContent(fileName)); + astCustomization.accept(astToEdit); + editor.replaceFile(fileName, astToEdit.toString()); + + FileEvent fileEvent = new FileEvent(); + fileEvent.setUri(fileUri); + fileEvent.setType(FileChangeType.Changed); + languageClient.notifyWatchedFilesChanged(Collections.singletonList(fileEvent)); + + return refreshSymbol(); + } + + ClassCustomization refreshSymbol() { + return new PackageCustomization(editor, languageClient, packageName).getClass(className); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/customization/CodeCustomization.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/customization/CodeCustomization.java new file mode 100644 index 0000000000..b8d08e0cfa --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/customization/CodeCustomization.java @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.customization; + +import com.microsoft.typespec.http.client.generator.core.customization.implementation.ls.EclipseLanguageClient; +import org.eclipse.lsp4j.SymbolInformation; + +/** + * Base class for all code based customizations. + */ +public abstract class CodeCustomization { + final Editor editor; + final EclipseLanguageClient languageClient; + final SymbolInformation symbol; + final String fileUri; + final String fileName; + + CodeCustomization(Editor editor, EclipseLanguageClient languageClient, SymbolInformation symbol) { + this.editor = editor; + this.languageClient = languageClient; + this.symbol = symbol; + this.fileUri = symbol.getLocation().getUri(); + int i = fileUri.toString().indexOf("src/main/java/"); + this.fileName = fileUri.toString().substring(i); + } + + /** + * The Editor managing the state of the CodeCustomization. + * + * @return The Editor. + */ + public final Editor getEditor() { + return editor; + } + + /** + * The EclipseLanguageClient managing validation of the CodeCustomization. + * + * @return The EclipseLanguageClient. + */ + public final EclipseLanguageClient getLanguageClient() { + return languageClient; + } + + /** + * The SymbolInformation managing information about the CodeCustomization. + * + * @return The SymbolInformation. + */ + public final SymbolInformation getSymbol() { + return symbol; + } + + /** + * The URI of the file containing where the code for the CodeCustomization exists. + * + * @return The URI of the file. + */ + public final String getFileUri() { + return fileUri; + } + + /** + * The name of the file containing where the code for the CodeCustomization exists. + * + * @return The name of the file. + */ + public final String getFileName() { + return fileName; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/customization/ConstantCustomization.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/customization/ConstantCustomization.java new file mode 100644 index 0000000000..7ee7b49497 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/customization/ConstantCustomization.java @@ -0,0 +1,180 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.customization; + +import com.microsoft.typespec.http.client.generator.core.customization.implementation.Utils; +import com.microsoft.typespec.http.client.generator.core.customization.implementation.ls.EclipseLanguageClient; +import org.eclipse.lsp4j.SymbolInformation; +import org.eclipse.lsp4j.SymbolKind; +import org.eclipse.lsp4j.WorkspaceEdit; + +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.regex.Pattern; + +/** + * Customization for an AutoRest generated constant property. + *

+ * For instance property customizations use {@link PropertyCustomization}. + */ +public final class ConstantCustomization extends CodeCustomization { + private static final Pattern METHOD_PARAMS_CAPTURE = Pattern.compile("\\(.*\\)"); + + private final String packageName; + private final String className; + private final String constantName; + + ConstantCustomization(Editor editor, EclipseLanguageClient languageClient, String packageName, String className, + SymbolInformation symbol, String constantName) { + super(editor, languageClient, symbol); + + this.packageName = packageName; + this.className = className; + this.constantName = constantName; + } + + /** + * Gets the name of the class that contains this constant. + * + * @return The name of the class that contains this constant. + */ + public String getClassName() { + return className; + } + + /** + * Gets the name of this constant. + * + * @return The name of this constant. + */ + public String getConstantName() { + return constantName; + } + + /** + * Gets the Javadoc customization for this constant. + * + * @return The Javadoc customization. + */ + public JavadocCustomization getJavadoc() { + return new JavadocCustomization(editor, languageClient, fileUri, fileName, + symbol.getLocation().getRange().getStart().getLine()); + } + + /** + * Replace the modifier for this constant. + *

+ * For compound modifiers such as {@code public abstract} use bitwise OR ({@code |}) of multiple Modifiers, {@code + * Modifier.PUBLIC | Modifier.ABSTRACT}. + *

+ * This operation doesn't allow for the constant to lose constant status, so + * {@code Modifier.STATIC | Modifier.FINAL} will be added to the passed {@code modifiers}. + *

+ * Pass {@code 0} for {@code modifiers} to indicate that the constant has no modifiers. + * + * @param modifiers The {@link Modifier Modifiers} for the constant. + * @return The updated ConstantCustomization object. + * @throws IllegalArgumentException If the {@code modifier} is less than to {@code 0} or any {@link Modifier} + * included in the bitwise OR isn't a valid constant {@link Modifier}. + */ + public ConstantCustomization setModifier(int modifiers) { + Utils.replaceModifier(symbol, editor, languageClient, "(?:.+ )?(\\w+ )" + constantName + "\\(", + "$1" + constantName + "(", Modifier.fieldModifiers(), Modifier.STATIC | Modifier.FINAL | modifiers); + + return refreshCustomization(constantName); + } + + /** + * Renames the constant. + *

+ * This operation doesn't allow for the constant to lose naming conventions of capitalized and underscore delimited + * words, so the {@code newName} will be capitalized. + *

+ * This is a refactor operation, all references of the constant will be renamed and the getter method(s) for this + * property will be renamed accordingly as well. + * + * @param newName The new name for the constant. + * @return A new instance of {@link ConstantCustomization} for chaining. + * @throws NullPointerException If {@code newName} is null. + */ + public ConstantCustomization rename(String newName) { + Objects.requireNonNull(newName, "'newName' cannot be null."); + + String lowercaseConstantName = constantName.toLowerCase(); + String currentCamelName = constantToMethodName(constantName); + String lowercaseCurrentCamelName = currentCamelName.toLowerCase(); + String newCamelName = constantToMethodName(newName); + + List edits = new ArrayList<>(); + for (SymbolInformation si : languageClient.listDocumentSymbols(fileUri)) { + String symbolName = si.getName().toLowerCase(); + if (!symbolName.contains(lowercaseConstantName) && !symbolName.contains(lowercaseCurrentCamelName)) { + continue; + } + + if (si.getKind() == SymbolKind.Constant) { + edits.add(languageClient.renameSymbol(fileUri, si.getLocation().getRange().getStart(), newName)); + } else if (si.getKind() == SymbolKind.Method) { + String methodName = si.getName().replace(currentCamelName, newCamelName) + .replace(constantName, newName); + methodName = METHOD_PARAMS_CAPTURE.matcher(methodName).replaceFirst(""); + edits.add(languageClient.renameSymbol(fileUri, si.getLocation().getRange().getStart(), methodName)); + } + } + + Utils.applyWorkspaceEdits(edits, editor, languageClient); + return refreshCustomization(newName); + } + + private static String constantToMethodName(String constantName) { + // Constants will be in the form A_WORD_SPLIT_BY_UNDERSCORE_AND_CAPITALIZED, which, if used as-is won't follow + // getter, or method, naming conventions of getAWordInCamelCase. + // + // Split the constant name on '_' and lower case all characters after the first. + StringBuilder camelBuilder = new StringBuilder(constantName.length()); + + for (String word : constantName.split("_")) { + if (word.isEmpty()) { + continue; + } + + camelBuilder.append(word.charAt(0)); + if (word.length() > 1) { + camelBuilder.append(word.substring(1).toLowerCase()); + } + } + + return camelBuilder.toString(); + } + + /** + * Add an annotation to a property in the class. + * + * @param annotation the annotation to add. The leading @ can be omitted. + * @return A new instance of {@link ConstantCustomization} for chaining. + */ + public ConstantCustomization addAnnotation(String annotation) { + return Utils.addAnnotation(annotation, this, () -> refreshCustomization(constantName)); + } + + /** + * Remove an annotation from the constant. + * + * @param annotation the annotation to remove from the constant. The leading @ can be omitted. + * @return A new instance of {@link ConstantCustomization} for chaining. + */ + public ConstantCustomization removeAnnotation(String annotation) { + return Utils.removeAnnotation(this, compilationUnit -> compilationUnit.getClassByName(className).get() + .getFieldByName(constantName).get() + .getAnnotationByName(Utils.cleanAnnotationName(annotation)), () -> refreshCustomization(constantName)); + } + + private ConstantCustomization refreshCustomization(String constantName) { + return new PackageCustomization(editor, languageClient, packageName) + .getClass(className) + .getConstant(constantName); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/customization/ConstructorCustomization.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/customization/ConstructorCustomization.java new file mode 100644 index 0000000000..a8f4b74c78 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/customization/ConstructorCustomization.java @@ -0,0 +1,159 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.customization; + +import com.microsoft.typespec.http.client.generator.core.customization.implementation.Utils; +import com.microsoft.typespec.http.client.generator.core.customization.implementation.ls.EclipseLanguageClient; +import org.eclipse.lsp4j.SymbolInformation; + +import java.lang.reflect.Modifier; +import java.util.List; + +/** + * The constructor level customization for an AutoRest generated constructor. + */ +public final class ConstructorCustomization extends CodeCustomization { + private final String packageName; + private final String className; + private final String constructorSignature; + + ConstructorCustomization(Editor editor, EclipseLanguageClient languageClient, String packageName, String className, + String constructorSignature, SymbolInformation symbol) { + super(editor, languageClient, symbol); + this.packageName = packageName; + this.className = className; + this.constructorSignature = constructorSignature; + } + + /** + * Gets the name of the class containing the constructor. + * + * @return The name of the class containing the constructor. + */ + public String getClassName() { + return className; + } + + /** + * Gets the Javadoc customization for this constructor. + * + * @return The Javadoc customization for this constructor. + */ + public JavadocCustomization getJavadoc() { + return new JavadocCustomization(editor, languageClient, fileUri, fileName, + symbol.getLocation().getRange().getStart().getLine()); + } + + /** + * Add an annotation to the constructor. + * + * @param annotation The annotation to add to the constructor. The leading @ can be omitted. + * @return A new ConstructorCustomization representing the updated constructor. + */ + public ConstructorCustomization addAnnotation(String annotation) { + return Utils.addAnnotation(annotation, this, () -> refreshCustomization(constructorSignature)); + } + + /** + * Remove an annotation from the constructor. + * + * @param annotation The annotation to remove from the constructor. The leading @ can be omitted. + * @return A new ConstructorCustomization representing the updated constructor. + */ + public ConstructorCustomization removeAnnotation(String annotation) { + return Utils.removeAnnotation(this, compilationUnit -> compilationUnit.getClassByName(className).get() + .getConstructors() + .stream() + .filter(ctor -> Utils.declarationContainsSymbol(ctor.getRange().get(), symbol.getLocation().getRange())) + .findFirst().get() + .getAnnotationByName(Utils.cleanAnnotationName(annotation)), + () -> refreshCustomization(constructorSignature)); + } + + /** + * Replace the modifier for this constructor. + *

+ * For compound modifiers such as {@code public abstract} use bitwise OR ({@code |}) of multiple Modifiers, + * {@code Modifier.PUBLIC | Modifier.ABSTRACT}. + *

+ * Pass {@code 0} for {@code modifiers} to indicate that the constructor has no modifiers. + * + * @param modifiers The {@link Modifier Modifiers} for the constructor. + * @return A new ConstructorCustomization representing the updated constructor. + * @throws IllegalArgumentException If the {@code modifier} is less than to {@code 0} or any {@link Modifier} + * included in the bitwise OR isn't a valid constructor {@link Modifier}. + */ + public ConstructorCustomization setModifier(int modifiers) { + Utils.replaceModifier(symbol, editor, languageClient, "(?:.+ )?" + className + "\\(", className + "(", + Modifier.constructorModifiers(), modifiers); + + return refreshCustomization(constructorSignature); + } + + /** + * Replace the parameters of the constructor. + * + * @param newParameters New constructor parameters. + * @return A new ConstructorCustomization representing the updated constructor. + */ + public ConstructorCustomization replaceParameters(String newParameters) { + return replaceParameters(newParameters, null); + } + + /** + * Replaces the parameters of the constructor and adds any additional imports required by the new parameters. + * + * @param newParameters New constructor parameters. + * @param importsToAdd Any additional imports required by the constructor. These will be custom types or types that + * are ambiguous on which to use such as {@code List} or the utility class {@code Arrays}. + * @return A new ConstructorCustomization representing the updated constructor. + */ + public ConstructorCustomization replaceParameters(String newParameters, List importsToAdd) { + String newSignature = className + "(" + newParameters + ")"; + + ClassCustomization classCustomization = new PackageCustomization(editor, languageClient, packageName) + .getClass(className); + + ClassCustomization updatedClassCustomization = Utils.addImports(importsToAdd, classCustomization, + classCustomization::refreshSymbol); + + return Utils.replaceParameters(newParameters, updatedClassCustomization.getConstructor(constructorSignature), + () -> updatedClassCustomization.getConstructor(newSignature)); + } + + /** + * Replace the body of the constructor. + * + * @param newBody New constructor body. + * @return A new ConstructorCustomization representing the updated constructor. + */ + public ConstructorCustomization replaceBody(String newBody) { + return replaceBody(newBody, null); + } + + /** + * Replaces the body of the constructor and adds any additional imports required by the new body. + * + * @param newBody New constructor body. + * @param importsToAdd Any additional imports required by the constructor. These will be custom types or types that + * are ambiguous on which to use such as {@code List} or the utility class {@code Arrays}. + * @return A new ConstructorCustomization representing the updated constructor. + */ + public ConstructorCustomization replaceBody(String newBody, List importsToAdd) { + ClassCustomization classCustomization = new PackageCustomization(editor, languageClient, packageName) + .getClass(className); + + ClassCustomization updatedClassCustomization = Utils.addImports(importsToAdd, classCustomization, + classCustomization::refreshSymbol); + + return Utils.replaceBody(newBody, updatedClassCustomization.getConstructor(constructorSignature), + () -> updatedClassCustomization.getConstructor(constructorSignature)); + } + + private ConstructorCustomization refreshCustomization(String constructorSignature) { + return new PackageCustomization(editor, languageClient, packageName) + .getClass(className) + .getConstructor(constructorSignature); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/customization/Customization.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/customization/Customization.java new file mode 100644 index 0000000000..26883a3d7c --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/customization/Customization.java @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.customization; + +import com.microsoft.typespec.http.client.generator.core.customization.implementation.Utils; +import com.microsoft.typespec.http.client.generator.core.customization.implementation.ls.EclipseLanguageClient; +import com.microsoft.typespec.http.client.generator.core.extension.base.util.FileUtils; +import org.slf4j.Logger; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.util.Map; + +/** + * The base class for customization. Extend this class to plug into AutoRest generation. + */ +public abstract class Customization { + /** + * Start the customization process. This is called by the post processor in AutoRest. + * + * @param files the map of files generated in the previous steps in AutoRest + * @param logger the logger + * @return the map of files after customization + */ + public final Map run(Map files, Logger logger) { + Path tempDirWithPrefix; + + // Populate editor + Editor editor; + try { + tempDirWithPrefix = FileUtils.createTempDirectory("temp"); + editor = new Editor(files, tempDirWithPrefix); + InputStream pomStream = Customization.class.getResourceAsStream("/pom.xml"); + byte[] buffer = new byte[pomStream.available()]; + pomStream.read(buffer); + editor.addFile("pom.xml", new String(buffer, StandardCharsets.UTF_8)); + } catch (IOException e) { + throw new RuntimeException(e); + } + + // Start language client + try (EclipseLanguageClient languageClient + = new EclipseLanguageClient(null, tempDirWithPrefix.toString(), logger)) { + languageClient.initialize(); + customize(new LibraryCustomization(editor, languageClient), logger); + editor.removeFile("pom.xml"); + return editor.getContents(); + } catch (Exception e) { + throw new RuntimeException(e); + } finally { + Utils.deleteDirectory(tempDirWithPrefix.toFile()); + } + } + + /** + * Override this method to customize the client library. + * + * @param libraryCustomization the top level customization object + * @param logger the logger + */ + public abstract void customize(LibraryCustomization libraryCustomization, Logger logger); +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/customization/Editor.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/customization/Editor.java new file mode 100644 index 0000000000..d41a05b1f4 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/customization/Editor.java @@ -0,0 +1,362 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.customization; + +import com.microsoft.typespec.http.client.generator.core.customization.implementation.Utils; +import org.eclipse.lsp4j.Position; +import org.eclipse.lsp4j.Range; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Scanner; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +/** + * The raw editor containing the current files being customized. + */ +public final class Editor { + private final Path rootDir; + private final Map contents; + private final Map> lines; + private final Map paths; + + /** + * Creates an editor instance with the file contents and the root directory path. + * + * @param contents the map from file relative paths (starting with "src/main/java") and file contents + * @param rootDir the root directory path containing the files + */ + public Editor(Map contents, Path rootDir) { + this.contents = new HashMap<>(contents); + this.lines = new HashMap<>(); + this.paths = new HashMap<>(); + this.rootDir = rootDir; + for (Map.Entry content : contents.entrySet()) { + addFile(content.getKey(), content.getValue()); + } + + } + + /** + * Gets the mapping from file relative paths (starting with "src/main/java") to file contents. + * + * @return the mapping + */ + public Map getContents() { + return contents; + } + + /** + * Adds a new file. + * + * @param name the relative path of the file, starting with "src/main/java" + * @param content the file content + */ + public void addFile(String name, String content) { + addOrReplaceFile(name, content, false); + } + + /** + * Replaces an existing file with new content. + * + * @param name The relative path of the file, starting with "src/main/java". + * @param content The content of the file. + */ + public void replaceFile(String name, String content) { + addOrReplaceFile(name, content, true); + } + + private void addOrReplaceFile(String name, String content, boolean isReplace) { + Path newFilePath = Paths.get(rootDir.toString(), name); + File newFile = newFilePath.toFile(); + if (!newFile.getParentFile().exists()) { + newFile.getParentFile().mkdirs(); + } + + try { + boolean fileCreated = newFile.createNewFile(); + + try (BufferedWriter writer = Files.newBufferedWriter(newFile.toPath())) { + writer.write(content); + } + + if (fileCreated || isReplace) { + contents.put(name, content); + lines.put(name, splitContentIntoLines(content)); + paths.put(name, newFilePath); + } + } catch (IOException e) { + throw new RuntimeException(); + } + } + + /** + * Removes a file. + * + * @param name the relative file path, starting with "src/main/java" + */ + public void removeFile(String name) { + contents.remove(name); + lines.remove(name); + paths.get(name).toFile().delete(); + paths.remove(name); + } + + /** + * Gets the content of a file. + * + * @param name the relative path of a file, starting with "src/main/java" + * @return the file content + */ + public String getFileContent(String name) { + return contents.get(name); + } + + /** + * Gets the file content split into lines. + * + * @param name the relative path of a file, starting with "src/main/java" + * @return the file content split into lines + */ + public List getFileLines(String name) { + return lines.get(name); + + } + + /** + * Gets a line in a file. + * + * @param name the relative path of a file, starting with "src/main/java" + * @param line the line number + * @return the file content in this line + */ + public String getFileLine(String name, int line) { + return lines.get(name).get(line); + } + + /** + * Inserts a blank line at a given line number. + * + * @param fileName the relative path of a file, starting with "src/main/java" + * @param line the line number to insert a new line + * @param indented if the line should be indented at the same level as the next line + * @return the position of the cursor after indentation (if indented) in this line + */ + public Position insertBlankLine(String fileName, int line, boolean indented) { + if (!indented) { + return insertBlankLineWithIndent(fileName, line, 0); + } else { + int indentAmount = Utils.getIndent(lines.get(fileName).get(line)).length(); + + return insertBlankLineWithIndent(fileName, line, indentAmount); + } + } + + public Position insertBlankLineWithIndent(String fileName, int line, int indentAmount) { + String indentation = IntStream.range(0, indentAmount).mapToObj(ignored -> " ").collect(Collectors.joining()); + lines.get(fileName).add(line, indentation); + contents.put(fileName, joinLinesIntoContent(lines.get(fileName))); + return new Position(line, indentation.length()); + } + + /** + * Replaces a chunk of a text with a new text in the file. + * + * @param fileName the relative path of a file, starting with "src/main/java" + * @param start the starting position to replace, inclusive + * @param end the ending position to replace, exclusive + * @param newContent the new content to replace the chunk + */ + public void replace(String fileName, Position start, Position end, String newContent) { + replaceWithIndentedContent(fileName, start, end, newContent, 0); + } + + public void replaceWithIndentedContent(String fileName, Position start, Position end, String newContent, + int newLineIndent) { + String indent = IntStream.range(0, newLineIndent).mapToObj(ignored -> " ").collect(Collectors.joining()); + StringBuilder stringBuilder = new StringBuilder(4096); + List lineContent = lines.get(fileName); + + // Copy lines until the start of the change is reached. + for (int i = 0; i != start.getLine(); i++) { + Utils.writeLine(stringBuilder, lineContent.get(i)); + } + + // Copy until the start of the change. + stringBuilder.append(lineContent.get(start.getLine()), 0, start.getCharacter()); + + List replacementLineContent = splitContentIntoLines(newContent); + + // Add the change. + if (replacementLineContent.size() > 0) { + for (int i = 0; i != replacementLineContent.size() - 1; i++) { + if (i > 0) { + stringBuilder.append(indent); + } + + Utils.writeLine(stringBuilder, replacementLineContent.get(i)); + } + + stringBuilder.append(indent).append(replacementLineContent.get(replacementLineContent.size() - 1)); + } + + Utils.writeLine(stringBuilder, lineContent.get(end.getLine()).substring(end.getCharacter())); + + // Copy the rest of the file until its end. + for (int i = end.getLine() + 1; i != lineContent.size(); i++) { + Utils.writeLine(stringBuilder, lineContent.get(i)); + } + + contents.put(fileName, stringBuilder.toString()); + lines.put(fileName, splitContentIntoLines(contents.get(fileName))); + try (BufferedWriter fileWriter = Files.newBufferedWriter(paths.get(fileName))) { + fileWriter.write(contents.get(fileName)); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + /** + * Renames a file. This simply renames the file, without renaming any class content. + * + * @param fileName the original relative path of a file, starting with "src/main/java" + * @param newName the new relative path of the file, starting with "src/main/java" + */ + public void renameFile(String fileName, String newName) { + contents.put(newName, contents.remove(fileName)); + lines.put(newName, lines.remove(fileName)); + Path path = paths.remove(fileName); + Path newPath = Paths.get(rootDir.toString(), newName); + path.toFile().renameTo(newPath.toFile()); + paths.put(newName, newPath); + } + + /** + * Searches all occurrences of a text in the file. + * + * @param fileName the relative path of a file, starting with "src/main/java" + * @param text the text to search + * @return the list of ranges containing the occurrences + */ + public List searchText(String fileName, String text) { + if (!lines.containsKey(fileName)) { + return null; + } else { + List occurrences = new ArrayList<>(); + for (int i = 0; i != lines.get(fileName).size(); i++) { + String line = lines.get(fileName).get(i); + if (line.contains(text)) { + int start = line.indexOf(text); + while (start != -1) { + int end = start + text.length(); + occurrences.add(new Range(new Position(i, start), new Position(i, end))); + start = line.indexOf(text, end); + } + } + } + return occurrences; + } + } + + /** + * Searches the first occurrence of a text in the file. + * + * @param fileName the relative path of a file, starting with "src/main/java" + * @param text the text to search + * @return the range containing the occurrence + */ + public Range searchTextFirstOccurrence(String fileName, String text) { + List ranges = searchText(fileName, text); + if (ranges != null && !ranges.isEmpty()) { + return ranges.get(0); + } + return null; + } + + /** + * Gets the text content in a range in the file. The lines will be joined with an optional delimiter. If the + * delimiter is null, the lines will be joined with line endings. + * + * @param fileName the relative path of a file, starting with "src/main/java" + * @param range the range to convert to text + * @param delimiter the optional delimiter described above + * @return the text in the range + */ + public String getTextInRange(String fileName, Range range, String delimiter) { + return getTextInRange(fileName, range, delimiter, str -> str); + } + + /** + * Gets the text content in a range in the file. + *

+ * If {@code delimiter} isn't null the lines will be joined using the delimiter. Otherwise, the lines will be joined + * using newline. + *

+ * If {@code lineCleaner} isn't null each line of content read from the file will use the line cleaner before adding + * it to the result. + * + * @param fileName The name of the file where content will be read. + * @param range The range in the file where content will be read. + * @param delimiter Optional delimiter to join read lines of the file. + * @param lineCleaner Optional function that will cleanse each line of the file that is read. + * @return The text in the range. + */ + public String getTextInRange(String fileName, Range range, String delimiter, Function lineCleaner) { + StringBuilder stringBuilder = new StringBuilder(4096); + for (int line = range.getStart().getLine(); line <= range.getEnd().getLine(); line++) { + String lineContent = getFileLine(fileName, line); + int truncateIndex = 0; + if (line == range.getStart().getLine()) { + lineContent = lineContent.substring(range.getStart().getCharacter()); + truncateIndex = range.getStart().getCharacter(); + } + + if (line == range.getEnd().getLine()) { + lineContent = lineContent.substring(0, range.getEnd().getCharacter() - truncateIndex); + } + + if (lineCleaner != null) { + lineContent = lineCleaner.apply(lineContent); + } + + if (delimiter == null) { + Utils.writeLine(stringBuilder, lineContent); + } else { + if (stringBuilder.length() == 0) { + stringBuilder.append(lineContent); + } else { + stringBuilder.append(delimiter).append(lineContent); + } + } + } + + return stringBuilder.toString(); + } + + private static List splitContentIntoLines(String content) { + List res = new ArrayList<>(); + Scanner scanner = new Scanner(content); + while (scanner.hasNextLine()) { + res.add(scanner.nextLine()); + } + if (content.endsWith("\n")) { + res.add(""); + } + return res; + } + + private static String joinLinesIntoContent(List lines) { + return String.join(System.lineSeparator(), lines); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/customization/JavadocCustomization.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/customization/JavadocCustomization.java new file mode 100644 index 0000000000..fbe6115168 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/customization/JavadocCustomization.java @@ -0,0 +1,458 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.customization; + +import com.microsoft.typespec.http.client.generator.core.customization.implementation.Utils; +import com.microsoft.typespec.http.client.generator.core.customization.implementation.ls.EclipseLanguageClient; +import org.eclipse.lsp4j.FileChangeType; +import org.eclipse.lsp4j.FileEvent; +import org.eclipse.lsp4j.Position; +import org.eclipse.lsp4j.Range; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + + +/** + * The Javadoc customization for an AutoRest generated classes and methods. + */ +public final class JavadocCustomization { + /* + * This pattern attempts to cleanse a line of a Javadoc. + * + * The scenarios handled by this pattern are the following: + * + * 1. A single line Javadoc + * 2. An indented single line Javadoc + * 3. A part of a Javadoc + * 4. An indented part of a Javadoc + * 5. A Javadoc where the closing line contains text + * 6. An indented Javadoc where the closing line contains text + */ + private static final Pattern JAVADOC_LINE_CLEANER = Pattern.compile("^\\s*/?\\*{1,2}\\s?(.*?)(?:\\s*\\*/)?$"); + + private static final Pattern EMPTY_JAVADOC_LINE_PATTERN = Pattern.compile("\\s*\\*/?\\s*"); + + private static final Pattern THROWS_TAG = Pattern.compile(".*@throws "); + private static final Pattern PARAM_TAG = Pattern.compile(".*@param "); + private static final Pattern SPACE_THEN_ANYTHING = Pattern.compile(" .*"); + private static final Pattern JAVADOC_LINE_WITH_CONTENT = Pattern.compile("\\* .*$"); + private static final Pattern END_JAVADOC_LINE = Pattern.compile(" \\*/$"); + private static final Pattern JAVADOC_CONTENT = Pattern.compile(" +\\* "); + + private final EclipseLanguageClient languageClient; + private final Editor editor; + private final String fileUri; + private final String fileName; + private final String indent; + + private String descriptionDocs; + private final Map paramDocs; + private String returnDoc; + private final Map throwsDocs; + private final List seeDocs; + private String sinceDoc; + private String deprecatedDoc; + private Range javadocRange; + + JavadocCustomization(Editor editor, EclipseLanguageClient languageClient, String fileUri, String fileName, + int symbolLine) { + this.editor = editor; + this.languageClient = languageClient; + + this.paramDocs = new LinkedHashMap<>(); + this.throwsDocs = new LinkedHashMap<>(); + this.seeDocs = new ArrayList<>(); + + this.fileUri = fileUri; + this.fileName = fileName; + + this.indent = Utils.getIndent(editor.getFileLine(fileName, symbolLine)); + parseJavadoc(symbolLine); + } + + Range getJavadocRange() { + return javadocRange; + } + + public JavadocCustomization replace(JavadocCustomization other) { + this.descriptionDocs = other.descriptionDocs; + + this.paramDocs.clear(); + if (other.paramDocs != null) { + this.paramDocs.putAll(other.paramDocs); + } + + this.returnDoc = other.returnDoc; + + this.throwsDocs.clear(); + if (other.throwsDocs != null) { + this.throwsDocs.putAll(other.throwsDocs); + } + + this.seeDocs.clear(); + if (other.seeDocs != null) { + this.seeDocs.addAll(other.seeDocs); + } + + this.sinceDoc = other.sinceDoc; + this.deprecatedDoc = other.deprecatedDoc; + commit(); + return this; + } + + /** + * Gets the Javadoc description. + * + * @return The Javadoc description. + */ + public String getDescription() { + return descriptionDocs; + } + + /** + * Sets the description in the Javadoc. + * + * @param description the description for the current class/method. + * @return the Javadoc customization object for chaining + */ + public JavadocCustomization setDescription(String description) { + return performChange(this.descriptionDocs, description, () -> this.descriptionDocs = description); + } + + /** + * Gets a read-only view of the Javadoc params. + * + * @return Read-only view of the Javadoc params. + */ + public Map getParams() { + return Collections.unmodifiableMap(paramDocs); + } + + /** + * Sets the param Javadoc for a parameter on the method. + * + * @param parameterName the parameter name on the method + * @param description the description for this parameter + * @return the Javadoc customization object for chaining + */ + public JavadocCustomization setParam(String parameterName, String description) { + return performChange(paramDocs.get(parameterName), description, + () -> paramDocs.put(parameterName, description)); + } + + /** + * Removes a parameter Javadoc on the method. + * + * @param parameterName the name of the parameter on the method + * @return the Javadoc customization object for chaining + */ + public JavadocCustomization removeParam(String parameterName) { + paramDocs.remove(parameterName); + commit(); + return this; + } + + /** + * Gets the Javadoc return. + * + * @return The Javadoc return. + */ + public String getReturn() { + return returnDoc; + } + + /** + * Sets the return Javadoc on the method. + * + * @param description the description for the return value + * @return the Javadoc customization object for chaining + */ + public JavadocCustomization setReturn(String description) { + return performChange(returnDoc, description, () -> this.returnDoc = description); + } + + /** + * Removes the return Javadoc for a method. + * + * @return the Javadoc customization object for chaining + */ + public JavadocCustomization removeReturn() { + return performChange(returnDoc, null, () -> this.returnDoc = null); + } + + /** + * Gets a read-only view of the Javadoc throws. + * + * @return Read-only view of the Javadoc throws. + */ + public Map getThrows() { + return Collections.unmodifiableMap(throwsDocs); + } + + /** + * Adds a throws Javadoc for a method. + * + * @param exceptionType the type of the exception the method will throw + * @param description the description for the exception + * @return the Javadoc customization object for chaining + */ + public JavadocCustomization addThrows(String exceptionType, String description) { + return performChange(throwsDocs.get(exceptionType), description, + () -> throwsDocs.put(exceptionType, description)); + } + + /** + * Removes a throw Javadoc for a method. + * + * @param exceptionType the type of the exception the method will throw + * @return the Javadoc customization object for chaining + */ + public JavadocCustomization removeThrows(String exceptionType) { + throwsDocs.remove(exceptionType); + commit(); + return this; + } + + /** + * Gets a read-only view of the Javadoc sees. + * + * @return Read-only view of the Javadoc sees. + */ + public List getSees() { + return Collections.unmodifiableList(seeDocs); + } + + /** + * Adds a see Javadoc. + * + * @param seeDoc the link to the extra documentation + * @return the Javadoc customization object for chaining + * @see Oracle docs on see tag + */ + public JavadocCustomization addSee(String seeDoc) { + seeDocs.add(seeDoc); + commit(); + return this; + } + + /** + * Gets the Javadoc since. + * + * @return The Javadoc since. + */ + public String getSince() { + return sinceDoc; + } + + /** + * Sets the since Javadoc on the method. + * + * @param sinceDoc the version for the since tag + * @return the Javadoc customization object for chaining + */ + public JavadocCustomization setSince(String sinceDoc) { + return performChange(this.sinceDoc, sinceDoc, () -> this.sinceDoc = sinceDoc); + } + + /** + * Removes the Javadoc since. + * + * @return The updated JavadocCustomization object. + */ + public JavadocCustomization removeSince() { + return performChange(this.sinceDoc, null, () -> this.sinceDoc = null); + } + + /** + * Gets the Javadoc deprecated. + * + * @return The Javadoc deprecated. + */ + public String getDeprecated() { + return deprecatedDoc; + } + + /** + * Sets the deprecated Javadoc on the method. + * + * @param deprecatedDoc the deprecation reason + * @return the Javadoc customization object for chaining + */ + public JavadocCustomization setDeprecated(String deprecatedDoc) { + return performChange(this.deprecatedDoc, deprecatedDoc, () -> this.deprecatedDoc = deprecatedDoc); + } + + /** + * Removes the Javadoc deprecated. + * + * @return The updated JavadocCustomization object. + */ + public JavadocCustomization removeDeprecated() { + return performChange(this.deprecatedDoc, null, () -> this.deprecatedDoc = null); + } + + private void initialize(int symbolLine) { + editor.insertBlankLine(fileName, symbolLine++, false); + editor.replace(fileName, new Position(symbolLine, 0), new Position(symbolLine, 0), indent); + Position javadocCursor = new Position(symbolLine, indent.length()); + javadocRange = new Range(javadocCursor, javadocCursor); + ++symbolLine; + FileEvent blankLineEvent = new FileEvent(); + blankLineEvent.setUri(fileUri); + blankLineEvent.setType(FileChangeType.Changed); + languageClient.notifyWatchedFilesChanged(Collections.singletonList(blankLineEvent)); + } + + private void parseJavadoc(int symbolLine) { + String lineContent = editor.getFileLine(fileName, --symbolLine); + while (lineContent.startsWith(indent + "@")) { + lineContent = editor.getFileLine(fileName, --symbolLine); + } + if (lineContent.endsWith("*/")) { + Position javadocEnd = new Position(symbolLine, lineContent.length()); + int currentDocEndLine = symbolLine; + while (!lineContent.contains("/*")) { + if (lineContent.contains("@throws")) { + String type = THROWS_TAG.matcher(lineContent).replaceFirst(""); + type = SPACE_THEN_ANYTHING.matcher(type).replaceFirst(""); + Position docStart = new Position(symbolLine, lineContent.indexOf("@throws") + 8); + Position docEnd = new Position(currentDocEndLine, editor.getFileLine(fileName, currentDocEndLine).length()); + throwsDocs.put(type, readJavadocTextRange(editor, fileName, docStart, docEnd)); + currentDocEndLine = symbolLine - 1; + } else if (lineContent.contains("@return")) { + Position docStart = new Position(symbolLine, lineContent.indexOf("@return") + 8); + Position docEnd = new Position(currentDocEndLine, editor.getFileLine(fileName, currentDocEndLine).length()); + returnDoc = readJavadocTextRange(editor, fileName, docStart, docEnd); + currentDocEndLine = symbolLine - 1; + } else if (lineContent.contains("@since")) { + Position docStart = new Position(symbolLine, lineContent.indexOf("@since") + 7); + Position docEnd = new Position(currentDocEndLine, editor.getFileLine(fileName, currentDocEndLine).length()); + sinceDoc = readJavadocTextRange(editor, fileName, docStart, docEnd); + currentDocEndLine = symbolLine - 1; + } else if (lineContent.contains("@see")) { + Position docStart = new Position(symbolLine, lineContent.indexOf("@see") + 5); + Position docEnd = new Position(currentDocEndLine, editor.getFileLine(fileName, currentDocEndLine).length()); + seeDocs.add(readJavadocTextRange(editor, fileName, docStart, docEnd)); + currentDocEndLine = symbolLine - 1; + } else if (lineContent.contains("@deprecated")) { + Position docStart = new Position(symbolLine, lineContent.indexOf("@deprecated") + 5); + Position docEnd = new Position(currentDocEndLine, editor.getFileLine(fileName, currentDocEndLine).length()); + deprecatedDoc = readJavadocTextRange(editor, fileName, docStart, docEnd); + currentDocEndLine = symbolLine - 1; + } else if (lineContent.contains("@param")) { + String name = PARAM_TAG.matcher(lineContent).replaceFirst(""); + name = SPACE_THEN_ANYTHING.matcher(name).replaceFirst(""); + Position docStart = new Position(symbolLine, lineContent.indexOf("@param") + 8 + name.length()); + Position docEnd = new Position(currentDocEndLine, editor.getFileLine(fileName, currentDocEndLine).length()); + paramDocs.put(name, readJavadocTextRange(editor, fileName, docStart, docEnd)); + currentDocEndLine = symbolLine - 1; + } else if (EMPTY_JAVADOC_LINE_PATTERN.matcher(lineContent).matches()) { + // empty line + currentDocEndLine--; + } + lineContent = editor.getFileLine(fileName, --symbolLine); + } + Position javadocStart = new Position(symbolLine, indent.length()); + javadocRange = new Range(javadocStart, javadocEnd); + if (lineContent.endsWith("/*") || lineContent.endsWith("/**")) { + symbolLine++; + } + Position descriptionStart = new Position(symbolLine, JAVADOC_LINE_WITH_CONTENT.matcher(editor.getFileLine(fileName, symbolLine)).replaceFirst("").length() + 2); + String descriptionEndLineContent = editor.getFileLine(fileName, currentDocEndLine); + while (descriptionEndLineContent.trim().endsWith("*")) { + descriptionEndLineContent = editor.getFileLine(fileName, --currentDocEndLine); + } + Position descriptionEnd = new Position(currentDocEndLine, END_JAVADOC_LINE.matcher(descriptionEndLineContent).replaceFirst("").length()); + this.descriptionDocs = JAVADOC_CONTENT.matcher(editor.getTextInRange(fileName, new Range(descriptionStart, descriptionEnd), " ")) + .replaceAll(" ").trim(); + } else { + initialize(symbolLine); + } + } + + private static String readJavadocTextRange(Editor editor, String fileName, Position docStart, + Position docEnd) { + return editor.getTextInRange(fileName, new Range(docStart, docEnd), " ", line -> { + Matcher lineCleaningMatch = JAVADOC_LINE_CLEANER.matcher(line); + return (lineCleaningMatch.find()) ? lineCleaningMatch.group(1) : line; + }).trim(); + } + + private void commit() { + // Given this method is self-contained use StringBuilder as it doesn't have synchronization. + // Additional start with a sizeable 4kb buffer to reduce chances of resizing while keeping it small. + StringBuilder stringBuilder = new StringBuilder(4096); + + Utils.writeLine(stringBuilder, "/**"); + if (descriptionDocs != null) { + Utils.writeLine(stringBuilder.append(indent).append(" * "), descriptionDocs); + } + + if (!paramDocs.isEmpty() || !throwsDocs.isEmpty() || returnDoc != null || deprecatedDoc != null) { + Utils.writeLine(stringBuilder.append(indent), " * "); + + for (Map.Entry paramDoc : paramDocs.entrySet()) { + Utils.writeLine(stringBuilder.append(indent) + .append(" * @param ") + .append(paramDoc.getKey()) + .append(" "), paramDoc.getValue()); + } + + if (returnDoc != null) { + Utils.writeLine(stringBuilder.append(indent).append(" * @return "), returnDoc); + } + + for (Map.Entry throwsDoc : throwsDocs.entrySet()) { + Utils.writeLine(stringBuilder.append(indent) + .append(" * @throws ") + .append(throwsDoc.getKey()) + .append(" "), throwsDoc.getValue()); + } + + for (String seeDoc : seeDocs) { + Utils.writeLine(stringBuilder.append(indent).append(" * @see "), seeDoc); + } + + if (sinceDoc != null) { + Utils.writeLine(stringBuilder.append(indent).append(" * @since "), sinceDoc); + } + + if (deprecatedDoc != null) { + Utils.writeLine(stringBuilder.append(indent).append(" * @deprecated "), deprecatedDoc); + } + + } + + stringBuilder.append(indent).append(" */"); + + editor.replace(fileName, javadocRange.getStart(), javadocRange.getEnd(), stringBuilder.toString()); + FileEvent replaceEvent = new FileEvent(); + replaceEvent.setUri(fileUri); + replaceEvent.setType(FileChangeType.Changed); + languageClient.notifyWatchedFilesChanged(Collections.singletonList(replaceEvent)); + + int javadocStartLine = javadocRange.getStart().getLine(); + String lineContent = editor.getFileLine(fileName, javadocStartLine); + while (!lineContent.endsWith("*/")) { + lineContent = editor.getFileLine(fileName, ++javadocStartLine); + } + parseJavadoc(javadocStartLine + 1); + } + + private JavadocCustomization performChange(String oldValue, String newValue, Runnable changePerformer) { + if (!Objects.equals(oldValue, newValue)) { + changePerformer.run(); + commit(); + } + + return this; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/customization/LibraryCustomization.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/customization/LibraryCustomization.java new file mode 100644 index 0000000000..22dba0faa1 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/customization/LibraryCustomization.java @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.customization; + +import com.microsoft.typespec.http.client.generator.core.customization.implementation.Utils; +import com.microsoft.typespec.http.client.generator.core.customization.implementation.ls.EclipseLanguageClient; +import org.eclipse.lsp4j.SymbolInformation; + +import java.util.Optional; + +/** + * The top level customization for an AutoRest generated client library. + */ +public final class LibraryCustomization { + private final EclipseLanguageClient languageClient; + private final Editor editor; + + LibraryCustomization(Editor editor, EclipseLanguageClient languageClient) { + this.editor = editor; + this.languageClient = languageClient; + } + + /** + * Gets the package level customization for a Java package in the client library. + * + * @param packageName the fully qualified name of the package + * @return the package level customization. + */ + public PackageCustomization getPackage(String packageName) { + return new PackageCustomization(editor, languageClient, packageName); + } + + /** + * Gets the class level customization for a Java class in the client library. + * + * @param packageName the fully qualified name of the package + * @param className the simple name of the class + * @return the class level customization + */ + public ClassCustomization getClass(String packageName, String className) { + String packagePath = packageName.replace(".", "/"); + Optional classSymbol = languageClient.findWorkspaceSymbol(className).stream() + // findWorkspace symbol finds all classes that contain the classname term + // The filter that checks the filename only works if there are no nested classes + // So, when customizing client classes that contain service interface, this can incorrectly return + // the service interface instead of the client class. So, we should add another check for exact name match + .filter(si -> si.getName().equals(className)) + .filter(si -> si.getLocation().getUri().toString().endsWith(packagePath + "/" + className + ".java")) + .findFirst(); + + return Utils.returnIfPresentOrThrow(classSymbol, + symbol -> new ClassCustomization(editor, languageClient, packageName, className, symbol), + () -> new IllegalArgumentException(className + " does not exist in package " + packageName)); + } + + /** + * Gets the raw editor containing the current files being edited and eventually emitted to the disk. + * + * @return the raw editor + */ + public Editor getRawEditor() { + return editor; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/customization/MethodCustomization.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/customization/MethodCustomization.java new file mode 100644 index 0000000000..a76e76ac30 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/customization/MethodCustomization.java @@ -0,0 +1,308 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.customization; + +import com.microsoft.typespec.http.client.generator.core.customization.implementation.Utils; +import com.microsoft.typespec.http.client.generator.core.customization.implementation.ls.EclipseLanguageClient; +import org.eclipse.lsp4j.FileChangeType; +import org.eclipse.lsp4j.FileEvent; +import org.eclipse.lsp4j.Position; +import org.eclipse.lsp4j.Range; +import org.eclipse.lsp4j.SymbolInformation; +import org.eclipse.lsp4j.TextEdit; +import org.eclipse.lsp4j.WorkspaceEdit; + +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + + +/** + * The method level customization for an AutoRest generated method. + */ +public final class MethodCustomization extends CodeCustomization { + private final String packageName; + private final String className; + private final String methodName; + private final String methodSignature; + + MethodCustomization(Editor editor, EclipseLanguageClient languageClient, String packageName, String className, + String methodName, String methodSignature, SymbolInformation symbol) { + super(editor, languageClient, symbol); + this.packageName = packageName; + this.className = className; + this.methodName = methodName; + this.methodSignature = methodSignature; + } + + /** + * Gets the name of the method this customization is using. + * + * @return The name of the method. + */ + public String getMethodName() { + return methodName; + } + + /** + * Gets the name of the class containing the method. + * + * @return The name of the class containing the method. + */ + public String getClassName() { + return className; + } + + /** + * Gets the Javadoc customization for this method. + * + * @return the Javadoc customization + */ + public JavadocCustomization getJavadoc() { + return new JavadocCustomization(editor, languageClient, fileUri, fileName, + symbol.getLocation().getRange().getStart().getLine()); + } + + /** + * Rename a method in the class. This is a refactor operation. All references to this method across the client + * library will be automatically modified. + * + * @param newName the new name for the method + * @return the current method customization for chaining + */ + public MethodCustomization rename(String newName) { + WorkspaceEdit edit = languageClient.renameSymbol(fileUri, symbol.getLocation().getRange().getStart(), newName); + Utils.applyWorkspaceEdit(edit, editor, languageClient); + + return refreshCustomization(methodSignature.replace(methodName + "(", newName + "(")); + } + + /** + * Add an annotation to a method in the class. + * + * @param annotation the annotation to add. The leading @ can be omitted. + * @return the current method customization for chaining + */ + public MethodCustomization addAnnotation(String annotation) { + return Utils.addAnnotation(annotation, this, () -> refreshCustomization(methodSignature)); + } + + /** + * Remove an annotation from the method. + * + * @param annotation the annotation to remove from the method. The leading @ can be omitted. + * @return the current method customization for chaining + */ + public MethodCustomization removeAnnotation(String annotation) { + return Utils.removeAnnotation(this, compilationUnit -> compilationUnit.getClassByName(className).get() + .getMethodsByName(methodName) + .stream() + .filter(method -> Utils.declarationContainsSymbol(method.getRange().get(), symbol.getLocation().getRange())) + .findFirst().get() + .getAnnotationByName(Utils.cleanAnnotationName(annotation)), () -> refreshCustomization(methodSignature)); + } + + /** + * Replace the modifier for this method. + *

+ * For compound modifiers such as {@code public abstract} use bitwise OR ({@code |}) of multiple Modifiers, {@code + * Modifier.PUBLIC | Modifier.ABSTRACT}. + *

+ * Pass {@code 0} for {@code modifiers} to indicate that the method has no modifiers. + * + * @param modifiers The {@link Modifier Modifiers} for the method. + * @return The updated MethodCustomization object. + * @throws IllegalArgumentException If the {@code modifier} is less than to {@code 0} or any {@link Modifier} + * included in the bitwise OR isn't a valid method {@link Modifier}. + */ + public MethodCustomization setModifier(int modifiers) { + Utils.replaceModifier(symbol, editor, languageClient, "(?:.+ )?(\\w+ )" + methodName + "\\(", + "$1" + methodName + "(", Modifier.methodModifiers(), modifiers); + + return refreshCustomization(methodSignature); + } + + /** + * Replace the parameters of the method. + * + * @param newParameters New method parameters. + * @return The updated MethodCustomization object. + */ + public MethodCustomization replaceParameters(String newParameters) { + return replaceParameters(newParameters, null); + } + + /** + * Replaces the parameters of the method and adds any additional imports required by the new parameters. + * + * @param newParameters New method parameters. + * @param importsToAdd Any additional imports required by the method. These will be custom types or types that + * are ambiguous on which to use such as {@code List} or the utility class {@code Arrays}. + * @return A new MethodCustomization representing the updated method. + */ + public MethodCustomization replaceParameters(String newParameters, List importsToAdd) { + String newSignature = methodName + "(" + newParameters + ")"; + + ClassCustomization classCustomization = new PackageCustomization(editor, languageClient, packageName) + .getClass(className); + + ClassCustomization updatedClassCustomization = Utils.addImports(importsToAdd, classCustomization, + classCustomization::refreshSymbol); + + return Utils.replaceParameters(newParameters, updatedClassCustomization.getMethod(methodSignature), + () -> updatedClassCustomization.getMethod(newSignature)); + } + + /** + * Replace the body of the method. + * + * @param newBody New method body. + * @return The updated MethodCustomization object. + */ + public MethodCustomization replaceBody(String newBody) { + return replaceBody(newBody, null); + } + + /** + * Replaces the body of the method and adds any additional imports required by the new body. + * + * @param newBody New method body. + * @param importsToAdd Any additional imports required by the method. These will be custom types or types that + * are ambiguous on which to use such as {@code List} or the utility class {@code Arrays}. + * @return A new MethodCustomization representing the updated method. + */ + public MethodCustomization replaceBody(String newBody, List importsToAdd) { + ClassCustomization classCustomization = new PackageCustomization(editor, languageClient, packageName) + .getClass(className); + + ClassCustomization updatedClassCustomization = Utils.addImports(importsToAdd, classCustomization, + classCustomization::refreshSymbol); + + return Utils.replaceBody(newBody, updatedClassCustomization.getMethod(methodSignature), + () -> updatedClassCustomization.getMethod(methodSignature)); + } + + /** + * Change the return type of the method. The new return type will be automatically imported. + * + *

+ * The {@code returnValueFormatter} can be used to transform the return value. If the original return type is {@code + * void}, simply pass the new return expression to {@code returnValueFormatter}; if the new return type is {@code + * void}, pass {@code null} to {@code returnValueFormatter}; if either the original return type nor the new return + * type is {@code void}, the {@code returnValueFormatter} should be a String formatter that contains exactly 1 + * instance of {@code %s}. + * + * @param newReturnType the simple name of the new return type + * @param returnValueFormatter the return value String formatter as described above + * @return the current method customization for chaining + */ + public MethodCustomization setReturnType(String newReturnType, String returnValueFormatter) { + return setReturnType(newReturnType, returnValueFormatter, false); + } + + /** + * Change the return type of the method. The new return type will be automatically imported. + * + *

+ * The {@code returnValueFormatter} can be used to transform the return value. If the original return type is {@code + * void}, simply pass the new return expression to {@code returnValueFormatter}; if the new return type is {@code + * void}, pass {@code null} to {@code returnValueFormatter}; if either the original return type nor the new return + * type is {@code void}, the {@code returnValueFormatter} should be a String formatter that contains exactly 1 + * instance of {@code %s}. + * + * @param newReturnType the simple name of the new return type + * @param returnValueFormatter the return value String formatter as described above + * @param replaceReturnStatement if set to {@code true}, the return statement will be replaced by the provided + * returnValueFormatter text with exactly one instance of {@code %s}. If set to true, appropriate semi-colons, + * parentheses, opening and closing of code blocks have to be taken care of in the {@code returnValueFormatter}. + * @return the current method customization for chaining + */ + public MethodCustomization setReturnType(String newReturnType, String returnValueFormatter, + boolean replaceReturnStatement) { + List edits = new ArrayList<>(); + + int line = symbol.getLocation().getRange().getStart().getLine(); + Position start = new Position(line, 0); + String oldLineContent = editor.getFileLine(fileName, line); + Position end = new Position(line, oldLineContent.length()); + String newLineContent = oldLineContent.replaceFirst("(\\w.* )?(\\w+) " + methodName + "\\(", + "$1" + newReturnType + " " + methodName + "("); + TextEdit signatureEdit = new TextEdit(); + signatureEdit.setNewText(newLineContent); + signatureEdit.setRange(new Range(start, end)); + edits.add(signatureEdit); + + String methodIndent = Utils.getIndent(editor.getFileLine(fileName, line)); + String methodContentIndent = Utils.getIndent(editor.getFileLine(fileName, line + 1)); + String oldReturnType = oldLineContent.replaceAll(" " + methodName + "\\(.*", "") + .replaceFirst(methodIndent + "(\\w.* )?", "").trim(); + int returnLine = -1; + while (!oldLineContent.startsWith(methodIndent + "}")) { + if (oldLineContent.contains("return ")) { + returnLine = line; + } + oldLineContent = editor.getFileLine(fileName, ++line); + } + if (returnLine == -1) { + // no return statement, originally void return type + editor.insertBlankLine(fileName, line, false); + FileEvent blankLineEvent = new FileEvent(); + blankLineEvent.setUri(fileUri); + blankLineEvent.setType(FileChangeType.Changed); + languageClient.notifyWatchedFilesChanged(Collections.singletonList(blankLineEvent)); + + TextEdit returnEdit = new TextEdit(); + returnEdit.setRange(new Range(new Position(line, 0), new Position(line, 0))); + returnEdit.setNewText(methodContentIndent + "return " + returnValueFormatter + ";"); + edits.add(returnEdit); + } else if (newReturnType.equals("void")) { + // remove return statement + TextEdit returnEdit = new TextEdit(); + returnEdit.setNewText(""); + returnEdit.setRange(new Range(new Position(returnLine, 0), new Position(line, 0))); + edits.add(returnEdit); + } else { + // replace return statement + TextEdit returnValueEdit = new TextEdit(); + String returnLineText = editor.getFileLine(fileName, returnLine); + returnValueEdit.setRange(new Range(new Position(returnLine, 0), new Position(returnLine, returnLineText.length()))); + returnValueEdit.setNewText(returnLineText.replace("return ", oldReturnType + " returnValue = ")); + edits.add(returnValueEdit); + + editor.insertBlankLine(fileName, line, false); + FileEvent blankLineEvent = new FileEvent(); + blankLineEvent.setUri(fileUri); + blankLineEvent.setType(FileChangeType.Changed); + languageClient.notifyWatchedFilesChanged(Collections.singletonList(blankLineEvent)); + + TextEdit returnEdit = new TextEdit(); + returnEdit.setRange(new Range(new Position(line, 0), new Position(line, 0))); + + if (replaceReturnStatement) { + returnEdit.setNewText(String.format(returnValueFormatter, "returnValue")); + } else { + returnEdit.setNewText(methodContentIndent + "return " + String.format(returnValueFormatter, "returnValue") + ";"); + } + + edits.add(returnEdit); + } + + WorkspaceEdit workspaceEdit = new WorkspaceEdit(); + workspaceEdit.setChanges(Collections.singletonMap(fileUri, edits)); + Utils.applyWorkspaceEdit(workspaceEdit, editor, languageClient); + + Utils.organizeImportsOnRange(languageClient, editor, fileUri, new Range(start, end)); + + String newMethodSignature = methodSignature.replace(oldReturnType + " " + methodName, newReturnType + " " + methodName); + + return refreshCustomization(newMethodSignature); + } + + private MethodCustomization refreshCustomization(String methodSignature) { + return new PackageCustomization(editor, languageClient, packageName) + .getClass(className) + .getMethod(methodSignature); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/customization/PackageCustomization.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/customization/PackageCustomization.java new file mode 100644 index 0000000000..2f086943be --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/customization/PackageCustomization.java @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.customization; + +import com.microsoft.typespec.http.client.generator.core.customization.implementation.Utils; +import com.microsoft.typespec.http.client.generator.core.customization.implementation.ls.EclipseLanguageClient; +import org.eclipse.lsp4j.SymbolInformation; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +/** + * The package level customization for an AutoRest generated client library. + */ +public final class PackageCustomization { + private final EclipseLanguageClient languageClient; + private final Editor editor; + private final String packageName; + + PackageCustomization(Editor editor, EclipseLanguageClient languageClient, String packageName) { + this.editor = editor; + this.languageClient = languageClient; + this.packageName = packageName; + } + + /** + * Gets the class level customization for a Java class in the package. + * + * @param className the simple name of the class + * @return the class level customization + */ + public ClassCustomization getClass(String className) { + String packagePath = packageName.replace(".", "/"); + Optional classSymbol = languageClient.findWorkspaceSymbol(className).stream() + // findWorkspace symbol finds all classes that contain the classname term + // The filter that checks the filename only works if there are no nested classes + // So, when customizing client classes that contain service interface, this can incorrectly return + // the service interface instead of the client class. So, we should add another check for exact name match + .filter(si -> si.getName().equals(className)) + .filter(si -> si.getLocation().getUri().endsWith(packagePath + "/" + className + ".java")) + .findFirst(); + + return Utils.returnIfPresentOrThrow(classSymbol, + symbol -> new ClassCustomization(editor, languageClient, packageName, className, symbol), + () -> new IllegalArgumentException(className + " does not exist in package " + packageName)); + } + + /** + * This method lists all the classes in this package. + * @return A list of classes that are in this package. + */ + public List listClasses() { + return languageClient.findWorkspaceSymbol("*") + .stream() + .filter(si -> si.getContainerName().equals(packageName)) + .map(classSymbol -> new ClassCustomization(editor, languageClient, packageName, + classSymbol.getName(), classSymbol)) + .collect(Collectors.toList()); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/customization/PropertyCustomization.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/customization/PropertyCustomization.java new file mode 100644 index 0000000000..ddadccef8c --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/customization/PropertyCustomization.java @@ -0,0 +1,164 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.customization; + +import com.microsoft.typespec.http.client.generator.core.customization.implementation.Utils; +import com.microsoft.typespec.http.client.generator.core.customization.implementation.ls.EclipseLanguageClient; +import com.microsoft.typespec.http.client.generator.core.customization.implementation.ls.models.JavaCodeActionKind; +import org.eclipse.lsp4j.CodeAction; +import org.eclipse.lsp4j.SymbolInformation; +import org.eclipse.lsp4j.SymbolKind; +import org.eclipse.lsp4j.WorkspaceEdit; + +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + + +/** + * Customization for an AutoRest generated instance property. + *

+ * For constant property customizations use {@link ConstantCustomization}. + */ +public final class PropertyCustomization extends CodeCustomization { + private static final Pattern METHOD_PARAMS_CAPTURE = Pattern.compile("\\(.*\\)"); + + private final String packageName; + private final String className; + private final String propertyName; + + PropertyCustomization(Editor editor, EclipseLanguageClient languageClient, String packageName, String className, + SymbolInformation symbol, String propertyName) { + super(editor, languageClient, symbol); + this.packageName = packageName; + this.className = className; + this.propertyName = propertyName; + } + + /** + * Gets the name of the class that contains this property. + * + * @return The name of the class that contains this property. + */ + public String getClassName() { + return className; + } + + /** + * Gets the name of this property. + * + * @return The name of this property. + */ + public String getPropertyName() { + return propertyName; + } + + /** + * Rename a property in the class. This is a refactor operation. All references of the property will be renamed and + * the getter and setter method(s) for this property will be renamed accordingly as well. + * + * @param newName the new name for the property + * @return the current class customization for chaining + */ + public PropertyCustomization rename(String newName) { + List symbols = languageClient.listDocumentSymbols(fileUri) + .stream().filter(si -> si.getName().toLowerCase().contains(propertyName.toLowerCase())) + .collect(Collectors.toList()); + String propertyPascalName = propertyName.substring(0, 1).toUpperCase() + propertyName.substring(1); + String newPascalName = newName.substring(0, 1).toUpperCase() + newName.substring(1); + + List edits = new ArrayList<>(); + for (SymbolInformation symbol : symbols) { + if (symbol.getKind() == SymbolKind.Field) { + edits.add(languageClient.renameSymbol(fileUri, symbol.getLocation().getRange().getStart(), newName)); + } else if (symbol.getKind() == SymbolKind.Method) { + String methodName = symbol.getName().replace(propertyPascalName, newPascalName) + .replace(propertyName, newName); + methodName = METHOD_PARAMS_CAPTURE.matcher(methodName).replaceFirst(""); + edits.add(languageClient.renameSymbol(fileUri, symbol.getLocation().getRange().getStart(), methodName)); + } + } + + Utils.applyWorkspaceEdits(edits, editor, languageClient); + return refreshCustomization(newName); + } + + /** + * Add an annotation to a property in the class. + * + * @param annotation the annotation to add. The leading @ can be omitted. + * @return the current property customization for chaining + */ + public PropertyCustomization addAnnotation(String annotation) { + return Utils.addAnnotation(annotation, this, () -> refreshCustomization(propertyName)); + } + + /** + * Remove an annotation from the property. + * + * @param annotation the annotation to remove from the property. The leading @ can be omitted. + * @return the current property customization for chaining + */ + public PropertyCustomization removeAnnotation(String annotation) { + return Utils.removeAnnotation(this, compilationUnit -> compilationUnit.getClassByName(className).get() + .getFieldByName(propertyName).get() + .getAnnotationByName(Utils.cleanAnnotationName(annotation)), () -> refreshCustomization(propertyName)); + } + + /** + * Generates a getter and a setter method(s) for a property in the class. This is a refactor operation. If a getter + * or a setter is already available on the class, the current getter or setter will be kept. + * + * @return the current class customization for chaining + */ + public PropertyCustomization generateGetterAndSetter() { + Optional generateAccessors = languageClient.listCodeActions(fileUri, symbol.getLocation().getRange(), + JavaCodeActionKind.SOURCE_GENERATE_ACCESSORS.toString()) + .stream().filter(ca -> ca.getKind().equals(JavaCodeActionKind.SOURCE_GENERATE_ACCESSORS.toString())) + .findFirst(); + if (generateAccessors.isPresent()) { + Utils.applyWorkspaceEdit(generateAccessors.get().getEdit(), editor, languageClient); + + String setterMethod = "set" + propertyName.substring(0, 1).toUpperCase() + propertyName.substring(1); + new PackageCustomization(editor, languageClient, packageName) + .getClass(className) + .getMethod(setterMethod).setReturnType(className, "this"); + } + + return this; + } + + /** + * Replace the modifier for this property. + *

+ * For compound modifiers such as {@code public final} use bitwise OR ({@code |}) of multiple Modifiers, {@code + * Modifier.PUBLIC | Modifier.FINAL}. + *

+ * Pass {@code 0} for {@code modifiers} to indicate that the property has no modifiers. + * + * @param modifiers The {@link Modifier Modifiers} for the property. + * @return The updated PropertyCustomization object. + * @throws IllegalArgumentException If the {@code modifier} is less than {@code 0} or any {@link Modifier} included + * in the bitwise OR isn't a valid property {@link Modifier}. + */ + public PropertyCustomization setModifier(int modifiers) { + String target = " *(?:(?:public|protected|private|static|final|transient|volatile) ?)*(.* )"; + languageClient.listDocumentSymbols(symbol.getLocation().getUri()) + .stream().filter(si -> si.getKind() == SymbolKind.Field && si.getName().equals(propertyName)) + .findFirst() + .ifPresent(symbolInformation -> Utils.replaceModifier(symbolInformation, editor, languageClient, + target + propertyName, "$1" + propertyName, Modifier.fieldModifiers(), modifiers)); + + return refreshCustomization(propertyName); + } + + private PropertyCustomization refreshCustomization(String propertyName) { + return new PackageCustomization(editor, languageClient, packageName) + .getClass(className) + .getProperty(propertyName); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/customization/implementation/Utils.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/customization/implementation/Utils.java new file mode 100644 index 0000000000..cde89f581b --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/customization/implementation/Utils.java @@ -0,0 +1,521 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.customization.implementation; + +import com.microsoft.typespec.http.client.generator.core.customization.ClassCustomization; +import com.microsoft.typespec.http.client.generator.core.customization.CodeCustomization; +import com.microsoft.typespec.http.client.generator.core.customization.Editor; +import com.microsoft.typespec.http.client.generator.core.customization.implementation.ls.EclipseLanguageClient; +import com.github.javaparser.StaticJavaParser; +import com.github.javaparser.ast.CompilationUnit; +import com.github.javaparser.ast.expr.AnnotationExpr; +import org.eclipse.lsp4j.CodeActionKind; +import org.eclipse.lsp4j.FileChangeType; +import org.eclipse.lsp4j.FileEvent; +import org.eclipse.lsp4j.Position; +import org.eclipse.lsp4j.Range; +import org.eclipse.lsp4j.SymbolInformation; +import org.eclipse.lsp4j.TextEdit; +import org.eclipse.lsp4j.WorkspaceEdit; + +import java.io.File; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +public class Utils { + /** + * This pattern determines the indentation of the passed string. Effectively it creates a group containing all + * spaces before the first word character. + */ + public static final Pattern INDENT_DETERMINATION_PATTERN = Pattern.compile("^(\\s*).*$"); + + /** + * This pattern matches a Java package declaration. + */ + private static final Pattern PACKAGE_PATTERN = Pattern.compile("package\\s[\\w\\.]+;"); + + /** + * This pattern matches anything then the space. + */ + public static final Pattern ANYTHING_THEN_SPACE_PATTERN = Pattern.compile(".* "); + + /* + * This pattern determines if a line is a beginning of constructor or method. The following is an explanation of + * the pattern: + * + * 1. Capture all leading space characters. + * 2. Capture all modifiers for the constructor or method. + * 3. If a method, capture the return type. + * 4. Capture the name of the constructor or method. + * + * The following are the groups return: + * + * 1. The entire matching declaration from the beginning of the string used to determine the beginning offset of the + * parameters in the constructor or method. + * 2. Any modifiers for the constructor or method. This may be empty/null. + * 3. If a method, the return type. If a constructor, empty/null. + * 4. The name of the constructor or method. + */ + private static final Pattern BEGINNING_OF_PARAMETERS_PATTERN = + Pattern.compile("^(\\s*(?:([\\w\\s]*?)\\s)?(?:([a-zA-Z$_][\\w]*?)\\s+)?([a-zA-Z$_][\\w]*?)\\s*)\\(.*$"); + + private static final Pattern ENDING_OF_PARAMETERS_PATTERN = Pattern.compile("^(.*)\\)\\s*\\{.*$"); + + public static void applyWorkspaceEdit(WorkspaceEdit workspaceEdit, Editor editor, EclipseLanguageClient languageClient) { + Map changes = new HashMap<>(); + applyWorkspaceEditInternal(workspaceEdit.getChanges(), changes, editor); + languageClient.notifyWatchedFilesChanged(new ArrayList<>(changes.values())); + } + + public static void applyWorkspaceEdits(List workspaceEdits, Editor editor, + EclipseLanguageClient languageClient) { + if (workspaceEdits == null || workspaceEdits.isEmpty()) { + return; + } + + Map changes = new HashMap<>(); + for (WorkspaceEdit workspaceEdit : workspaceEdits) { + applyWorkspaceEditInternal(workspaceEdit.getChanges(), changes, editor); + } + + languageClient.notifyWatchedFilesChanged(new ArrayList<>(changes.values())); + } + + private static void applyWorkspaceEditInternal(Map> edits, + Map changes, Editor editor) { + if (edits == null || edits.isEmpty()) { + return; + } + + for (Map.Entry> edit : edits.entrySet()) { + int i = edit.getKey().indexOf("src/main/java/"); + String fileName = edit.getKey().substring(i); + if (editor.getContents().containsKey(fileName)) { + for (TextEdit textEdit : edit.getValue()) { + editor.replace(fileName, textEdit.getRange().getStart(), textEdit.getRange().getEnd(), textEdit.getNewText()); + } + changes.putIfAbsent(fileName, new FileEvent(edit.getKey(), FileChangeType.Changed)); + } + } + } + + public static void applyTextEdits(String fileUri, List textEdits, Editor editor, EclipseLanguageClient languageClient) { + List changes = new ArrayList<>(); + int i = fileUri.indexOf("src/main/java/"); + String fileName = fileUri.substring(i); + if (editor.getContents().containsKey(fileName)) { + for (int j = textEdits.size() - 1; j >= 0; j--) { + TextEdit textEdit = textEdits.get(j); + editor.replace(fileName, textEdit.getRange().getStart(), textEdit.getRange().getEnd(), textEdit.getNewText()); + } + FileEvent fileEvent = new FileEvent(); + fileEvent.setUri(fileUri); + fileEvent.setType(FileChangeType.Changed); + changes.add(fileEvent); + } + languageClient.notifyWatchedFilesChanged(changes); + } + + public static void deleteDirectory(File directoryToBeDeleted) { + File[] allContents = directoryToBeDeleted.listFiles(); + if (allContents != null) { + for (File file : allContents) { + deleteDirectory(file); + } + } + directoryToBeDeleted.delete(); + } + + public static boolean isNullOrEmpty(CharSequence charSequence) { + return charSequence == null || charSequence.length() == 0; + } + + public static boolean isNullOrEmpty(T[] array) { + return array == null || array.length == 0; + } + + public static boolean isNullOrEmpty(Iterable iterable) { + return (iterable == null || !iterable.iterator().hasNext()); + } + + static void validateModifiers(int validTypeModifiers, int newModifiers) { + // 0 indicates no modifiers. + if (newModifiers == 0) { + return; + } + + if (newModifiers < 0) { + throw new IllegalArgumentException("Modifiers aren't allowed to be less than or equal to 0."); + } + + if (validTypeModifiers != (validTypeModifiers | newModifiers)) { + throw new IllegalArgumentException("Modifiers contain illegal modifiers for the type."); + } + } + + /** + * Replaces the modifier for a given symbol. + * + * @param symbol The symbol having its modifier replaced. + * @param editor The editor containing information about the symbol. + * @param languageClient The language client handling replacement of the modifiers. + * @param replaceTarget A string regex that determines how the modifiers are replaced. + * @param modifierReplaceBase A string that determines the base modifier replacement. + * @param validaTypeModifiers The modifier bit flag used to validate the new modifiers. + * @param newModifiers The new modifiers for the symbol. + */ + public static void replaceModifier(SymbolInformation symbol, Editor editor, EclipseLanguageClient languageClient, + String replaceTarget, String modifierReplaceBase, int validaTypeModifiers, int newModifiers) { + validateModifiers(validaTypeModifiers, newModifiers); + + String fileUri = symbol.getLocation().getUri(); + int i = fileUri.indexOf("src/main/java/"); + String fileName = fileUri.substring(i); + + int line = symbol.getLocation().getRange().getStart().getLine(); + Position start = new Position(line, 0); + String oldLineContent = editor.getFileLine(fileName, line); + Position end = new Position(line, oldLineContent.length()); + + String newModifiersString = Modifier.toString(newModifiers); + String newLineContent = (isNullOrEmpty(newModifiersString)) + ? oldLineContent.replaceFirst(replaceTarget, modifierReplaceBase) + : oldLineContent.replaceFirst(replaceTarget, newModifiersString + " " + modifierReplaceBase); + + TextEdit textEdit = new TextEdit(); + textEdit.setNewText(newLineContent); + textEdit.setRange(new Range(start, end)); + WorkspaceEdit workspaceEdit = new WorkspaceEdit(); + workspaceEdit.setChanges(Collections.singletonMap(fileUri, Collections.singletonList(textEdit))); + Utils.applyWorkspaceEdit(workspaceEdit, editor, languageClient); + } + + public static S returnIfPresentOrThrow(Optional optional, Function returnFormatter, + Supplier orThrow) { + if (optional.isPresent()) { + return returnFormatter.apply(optional.get()); + } + + throw orThrow.get(); + } + + public static void writeLine(StringBuilder stringBuilder, String text) { + stringBuilder.append(text).append(System.lineSeparator()); + } + + /** + * Walks down the lines of a file until the line matches a predicate. + * + * @param editor The editor containing the file's information. + * @param fileName The name of the file. + * @param startLine The line to start walking. + * @param linePredicate The predicate that determines when a matching line is found. + * @return The first line that matches the predicate. If no line in the file matches the predicate {@code -1} is + * returned. + */ + public static int walkDownFileUntilLineMatches(Editor editor, String fileName, int startLine, + Predicate linePredicate) { + return walkFileUntilLineMatches(editor, fileName, startLine, linePredicate, true); + } + + /** + * Walks up the lines of a file until the line matches a predicate. + * + * @param editor The editor containing the file's information. + * @param fileName The name of the file. + * @param startLine The line to start walking. + * @param linePredicate The predicate that determines when a matching line is found. + * @return The first line that matches the predicate. If no line in the file matches the predicate {@code -1} is + * returned. + */ + public static int walkUpFileUntilLineMatches(Editor editor, String fileName, int startLine, + Predicate linePredicate) { + return walkFileUntilLineMatches(editor, fileName, startLine, linePredicate, false); + } + + private static int walkFileUntilLineMatches(Editor editor, String fileName, int startLine, + Predicate linePredicate, boolean isWalkingDown) { + int matchingLine = -1; + + List fileLines = editor.getFileLines(fileName); + if (isWalkingDown) { + for (int line = startLine; line < fileLines.size(); line++) { + if (linePredicate.test(fileLines.get(line))) { + matchingLine = line; + break; + } + } + } else { + for (int line = startLine; line >= 0; line--) { + if (linePredicate.test(fileLines.get(line))) { + matchingLine = line; + break; + } + } + } + + return matchingLine; + } + + /** + * Utility method to add an annotation to a code block. + * + * @param annotation The annotation to add. + * @param customization The customization having an annotation added. + * @param refreshedCustomizationSupplier A supplier that returns a refreshed customization after the annotation is + * added. + * @param The type of the customization. + * @return A refreshed customization after the annotation was added. + */ + public static T addAnnotation(String annotation, CodeCustomization customization, + Supplier refreshedCustomizationSupplier) { + SymbolInformation symbol = customization.getSymbol(); + Editor editor = customization.getEditor(); + String fileName = customization.getFileName(); + String fileUri = customization.getFileUri(); + EclipseLanguageClient languageClient = customization.getLanguageClient(); + + if (!annotation.startsWith("@")) { + annotation = "@" + annotation; + } + + if (editor.getContents().containsKey(fileName)) { + int line = symbol.getLocation().getRange().getStart().getLine(); + Position position = editor.insertBlankLine(fileName, line, true); + editor.replace(fileName, position, position, annotation); + + FileEvent fileEvent = new FileEvent(); + fileEvent.setUri(fileUri); + fileEvent.setType(FileChangeType.Changed); + languageClient.notifyWatchedFilesChanged(Collections.singletonList(fileEvent)); + + organizeImportsOnRange(languageClient, editor, fileUri, symbol.getLocation().getRange()); + } + + return refreshedCustomizationSupplier.get(); + } + + /** + * Removes the leading {@literal @} in an annotation name, if it exists. + * + * @param annotationName The annotation name. + * @return The annotation with any leading {@literal @} removed. + */ + public static String cleanAnnotationName(String annotationName) { + return annotationName.startsWith("@") ? annotationName.substring(1) : annotationName; + } + + /** + * Utility method to remove an annotation from a code block. + * + * @param codeCustomization The customization having an annotation removed. + * @param annotationRetriever Function that retrieves the potential annotation. + * @param refreshedCustomizationSupplier Supplier that returns a refreshed customization after the annotation is + * removed. + * @param The type of the customization. + * @return A refreshed customization if the annotation was removed, otherwise the customization as-is. + */ + public static T removeAnnotation(T codeCustomization, + Function> annotationRetriever, + Supplier refreshedCustomizationSupplier) { + Editor editor = codeCustomization.getEditor(); + String fileName = codeCustomization.getFileName(); + + CompilationUnit compilationUnit = StaticJavaParser.parse(editor.getFileContent(fileName)); + Optional potentialAnnotation = annotationRetriever.apply(compilationUnit); + + if (potentialAnnotation.isPresent()) { + potentialAnnotation.get().remove(); + editor.replaceFile(fileName, compilationUnit.toString()); + Utils.sendFilesChangeNotification(codeCustomization.getLanguageClient(), codeCustomization.getFileUri()); + return refreshedCustomizationSupplier.get(); + } else { + return codeCustomization; + } + } + + /** + * Notifies watchers of a file that it has changed. + * + * @param languageClient The {@link EclipseLanguageClient} sending the file changed notification. + * @param fileUri The URI of the file that was changed. + */ + public static void sendFilesChangeNotification(EclipseLanguageClient languageClient, String fileUri) { + FileEvent fileEvent = new FileEvent(); + fileEvent.setUri(fileUri); + fileEvent.setType(FileChangeType.Changed); + languageClient.notifyWatchedFilesChanged(Collections.singletonList(fileEvent)); + } + + public static boolean declarationContainsSymbol(com.github.javaparser.Range declarationRange, + Range symbolRange) { + return declarationRange.begin.line <= symbolRange.getStart().getLine() + && declarationRange.end.line >= symbolRange.getStart().getLine(); + } + + /** + * Utility method to replace a body of a code block. + * + * @param newBody The new body. + * @param customization The customization having its body replaced. + * @param refreshedCustomizationSupplier A supplier that returns a refreshed customization after the body is + * replaced. + * @param The type of the customization. + * @return A refreshed customization after the body was replaced. + */ + public static T replaceBody(String newBody, CodeCustomization customization, + Supplier refreshedCustomizationSupplier) { + SymbolInformation symbol = customization.getSymbol(); + Editor editor = customization.getEditor(); + String fileName = customization.getFileName(); + + int line = symbol.getLocation().getRange().getStart().getLine(); + String methodBlockIndent = getIndent(editor.getFileLine(fileName, line)); + + // Loop until the line containing the body start is found. + Pattern startPattern = Pattern.compile(".*\\{\\s*"); + int startLine = walkDownFileUntilLineMatches(editor, fileName, line, lineContent -> + startPattern.matcher(lineContent).matches()) + 1; // Plus one since the start is after the opening '{' + + // Then determine the base indentation level for the body. + String methodContentIndent = getIndent(editor.getFileLine(fileName, startLine)); + Position oldBodyStart = new Position(startLine, methodContentIndent.length()); + + // Then continue iterating over lines until the body close line is found. + Pattern closePattern = Pattern.compile(methodBlockIndent + "}\\s*"); + int lastLine = walkDownFileUntilLineMatches(editor, fileName, startLine, lineContent -> + closePattern.matcher(lineContent).matches()) - 1; // Minus one since the end is before the closing '}' + Position oldBodyEnd = new Position(lastLine, editor.getFileLine(fileName, lastLine).length()); + + editor.replaceWithIndentedContent(fileName, oldBodyStart, oldBodyEnd, newBody, methodContentIndent.length()); + FileEvent fileEvent = new FileEvent(); + fileEvent.setUri(customization.getFileUri()); + fileEvent.setType(FileChangeType.Changed); + customization.getLanguageClient().notifyWatchedFilesChanged(Collections.singletonList(fileEvent)); + + // Return the refreshed customization. + return refreshedCustomizationSupplier.get(); + } + + public static T replaceParameters(String newParameters, + CodeCustomization customization, Supplier refreshCustomizationSupplier) { + SymbolInformation symbol = customization.getSymbol(); + Editor editor = customization.getEditor(); + String fileName = customization.getFileName(); + String fileUri = customization.getFileUri(); + EclipseLanguageClient languageClient = customization.getLanguageClient(); + + // Beginning line of the symbol. + int line = symbol.getLocation().getRange().getStart().getLine(); + + // First find the starting location of the parameters. + // The beginning of the parameters may not be on the same line as the start of the signature. + Matcher matcher = BEGINNING_OF_PARAMETERS_PATTERN.matcher(editor.getFileLine(fileName, line)); + while (!matcher.matches()) { + matcher = BEGINNING_OF_PARAMETERS_PATTERN.matcher(editor.getFileLine(fileName, ++line)); + } + + // Now that the line where the parameters begin is found create its position. + // Starting character is inclusive of the character offset, so add one as ')' isn't included in the capture. + Position parametersStart = new Position(line, matcher.group(1).length() + 1); + + // Then find where the parameters end. + // The ending of the parameters may not be on the same line as the start of the parameters. + matcher = ENDING_OF_PARAMETERS_PATTERN.matcher(editor.getFileLine(fileName, line)); + while (!matcher.matches()) { + matcher = ENDING_OF_PARAMETERS_PATTERN.matcher(editor.getFileLine(fileName, ++line)); + } + + // Now that the line where the parameters end is found gets create its position. + // Ending character is exclusive of the character offset. + Position parametersEnd = new Position(line, matcher.group(1).length()); + + editor.replace(fileName, parametersStart, parametersEnd, newParameters); + + FileEvent fileEvent = new FileEvent(); + fileEvent.setUri(fileUri); + fileEvent.setType(FileChangeType.Changed); + languageClient.notifyWatchedFilesChanged(Collections.singletonList(fileEvent)); + + return refreshCustomizationSupplier.get(); + } + + public static String getIndent(String content) { + Matcher matcher = INDENT_DETERMINATION_PATTERN.matcher(content); + return matcher.matches() ? matcher.group(1) : ""; + } + + /** + * Adds imports to the customization. + * + * @param importsToAdd Imports to add. + * @param customization Code customization to add imports. + * @param refreshCustomizationSupplier A supplier that returns a refreshed customization after the imports are + * added. + * @param Type of the customization. + * @return A refreshed customization. + */ + public static T addImports(List importsToAdd, + ClassCustomization customization, Supplier refreshCustomizationSupplier) { + EclipseLanguageClient languageClient = customization.getLanguageClient(); + Editor editor = customization.getEditor(); + String fileUri = customization.getFileUri(); + String fileName = customization.getFileName(); + + // Only add imports if they exist. + if (!isNullOrEmpty(importsToAdd)) { + // Always place imports after the package. + // The language server will format the imports once added, so location doesn't matter. + int importLine = Utils.walkDownFileUntilLineMatches(editor, fileName, 0, + line -> PACKAGE_PATTERN.matcher(line).matches()) + 1; + + Position importPosition = new Position(importLine, 0); + String imports = importsToAdd.stream() + .map(importToAdd -> "import " + importToAdd + ";") + .collect(Collectors.joining("\n")); + + editor.insertBlankLine(fileName, importLine, false); + editor.replace(fileName, importPosition, importPosition, imports); + } + + FileEvent fileEvent = new FileEvent(); + fileEvent.setUri(fileUri); + fileEvent.setType(FileChangeType.Changed); + languageClient.notifyWatchedFilesChanged(Collections.singletonList(fileEvent)); + + return refreshCustomizationSupplier.get(); + } + + public static void organizeImportsOnRange(EclipseLanguageClient languageClient, Editor editor, String fileUri, + Range range) { + languageClient.listCodeActions(fileUri, range, CodeActionKind.SourceOrganizeImports).stream() + .filter(ca -> ca.getKind().equals(CodeActionKind.SourceOrganizeImports)) + .findFirst() + .ifPresent(action -> Utils.applyWorkspaceEdit(action.getEdit(), editor, languageClient)); + } + + public static boolean isWindows() { + String osName = System.getProperty("os.name"); + return osName != null && osName.startsWith("Windows"); + } + + public static boolean isMac() { + String osName = System.getProperty("os.name"); + return osName != null && (osName.startsWith("Mac") || osName.startsWith("Darwin")); + } + + private Utils() { + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/customization/implementation/ls/EclipseLanguageClient.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/customization/implementation/ls/EclipseLanguageClient.java new file mode 100644 index 0000000000..1b7e6da015 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/customization/implementation/ls/EclipseLanguageClient.java @@ -0,0 +1,233 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.customization.implementation.ls; + +import com.microsoft.typespec.http.client.generator.core.customization.implementation.ls.models.JavaCodeActionKind; +import com.microsoft.typespec.http.client.generator.core.extension.jsonrpc.Connection; +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import org.eclipse.lsp4j.ClientCapabilities; +import org.eclipse.lsp4j.CodeAction; +import org.eclipse.lsp4j.CodeActionCapabilities; +import org.eclipse.lsp4j.CodeActionContext; +import org.eclipse.lsp4j.CodeActionKind; +import org.eclipse.lsp4j.CodeActionKindCapabilities; +import org.eclipse.lsp4j.CodeActionLiteralSupportCapabilities; +import org.eclipse.lsp4j.CodeActionParams; +import org.eclipse.lsp4j.DidChangeWatchedFilesParams; +import org.eclipse.lsp4j.DocumentSymbolParams; +import org.eclipse.lsp4j.FileEvent; +import org.eclipse.lsp4j.InitializeParams; +import org.eclipse.lsp4j.InitializeResult; +import org.eclipse.lsp4j.Position; +import org.eclipse.lsp4j.Range; +import org.eclipse.lsp4j.RenameParams; +import org.eclipse.lsp4j.ShowDocumentCapabilities; +import org.eclipse.lsp4j.SymbolCapabilities; +import org.eclipse.lsp4j.SymbolInformation; +import org.eclipse.lsp4j.SymbolKind; +import org.eclipse.lsp4j.SymbolKindCapabilities; +import org.eclipse.lsp4j.TextDocumentClientCapabilities; +import org.eclipse.lsp4j.TextDocumentIdentifier; +import org.eclipse.lsp4j.TextEdit; +import org.eclipse.lsp4j.WindowClientCapabilities; +import org.eclipse.lsp4j.WindowShowMessageRequestActionItemCapabilities; +import org.eclipse.lsp4j.WindowShowMessageRequestCapabilities; +import org.eclipse.lsp4j.WorkspaceClientCapabilities; +import org.eclipse.lsp4j.WorkspaceEdit; +import org.eclipse.lsp4j.WorkspaceFolder; +import org.eclipse.lsp4j.WorkspaceSymbolParams; +import org.eclipse.lsp4j.jsonrpc.json.MessageJsonHandler; +import org.slf4j.Logger; + +import java.io.File; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.EnumSet; +import java.util.List; + +public class EclipseLanguageClient implements AutoCloseable { + private static final Gson GSON = new MessageJsonHandler(null).getDefaultGsonBuilder().create(); + + private static final Type LIST_SYMBOL_INFORMATION = createParameterizedType(List.class, SymbolInformation.class); + private static final Type LIST_CODE_ACTION = createParameterizedType(List.class, CodeAction.class); + + private final EclipseLanguageServerFacade server; + private final Connection connection; + private final String workspaceDir; + + public EclipseLanguageClient(String pathToLanguageServerPlugin, String workspaceDir, Logger logger) { + try { + this.workspaceDir = new File(workspaceDir).toURI().toString(); + this.server = new EclipseLanguageServerFacade(pathToLanguageServerPlugin, logger); + this.connection = new Connection(server.getOutputStream(), server.getInputStream()); + } catch (Exception e) { + throw new RuntimeException(e); + } + + if (!server.isAlive()) { + server.shutdown(); + logger.error("Language server failed to start: " + server.getServerError()); + throw new RuntimeException("Language server failed to start: " + server.getServerError()); + } + } + + public void initialize() { + int pid = (int) ProcessHandle.current().pid(); + + InitializeParams initializeParams = new InitializeParams(); + initializeParams.setProcessId(pid); + initializeParams.setRootUri(workspaceDir); + initializeParams.setWorkspaceFolders(new ArrayList<>()); + WorkspaceFolder workspaceFolder = new WorkspaceFolder(); + workspaceFolder.setName("root"); + workspaceFolder.setUri(workspaceDir); + initializeParams.getWorkspaceFolders().add(workspaceFolder); + initializeParams.setTrace("message"); + initializeParams.setCapabilities(new ClientCapabilities()); + + // Configure window capabilities to disable everything as the server is run in headless mode. + WindowClientCapabilities windowClientCapabilities = new WindowClientCapabilities(); + windowClientCapabilities.setWorkDoneProgress(false); + windowClientCapabilities.setShowDocument(new ShowDocumentCapabilities(false)); + windowClientCapabilities.setShowMessage(new WindowShowMessageRequestCapabilities()); + windowClientCapabilities.getShowMessage().setMessageActionItem(new WindowShowMessageRequestActionItemCapabilities(false)); + initializeParams.getCapabilities().setWindow(windowClientCapabilities); + + // Configure workspace capabilities to support workspace folders and all symbol kinds. + WorkspaceClientCapabilities workspaceClientCapabilities = new WorkspaceClientCapabilities(); + workspaceClientCapabilities.setWorkspaceFolders(true); + workspaceClientCapabilities.setSymbol(new SymbolCapabilities( + new SymbolKindCapabilities(Arrays.asList(SymbolKind.values())), false)); + + // Configure text document capabilities to support code actions and all code action kinds. + List supportedCodeActions = new ArrayList<>(Arrays.asList(CodeActionKind.QuickFix, + CodeActionKind.Refactor, CodeActionKind.RefactorExtract, CodeActionKind.RefactorInline, + CodeActionKind.RefactorRewrite, CodeActionKind.Source, CodeActionKind.SourceOrganizeImports)); + EnumSet.allOf(JavaCodeActionKind.class) + .forEach(javaCodeActionKind -> supportedCodeActions.add(javaCodeActionKind.toString())); + TextDocumentClientCapabilities textDocumentClientCapabilities = new TextDocumentClientCapabilities(); + textDocumentClientCapabilities.setCodeAction(new CodeActionCapabilities( + new CodeActionLiteralSupportCapabilities(new CodeActionKindCapabilities(supportedCodeActions)), false)); + initializeParams.getCapabilities().setTextDocument(textDocumentClientCapabilities); + + sendRequest(connection, "initialize", initializeParams, InitializeResult.class); + connection.notifyWithSerializedObject("initialized", "null"); + try { + Thread.sleep(2500); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + public void notifyWatchedFilesChanged(List changes) { + if (changes == null || changes.isEmpty()) { + return; + } + + DidChangeWatchedFilesParams params = new DidChangeWatchedFilesParams(changes); + connection.notifyWithSerializedObject("workspace/didChangeWatchedFiles", GSON.toJson(params)); + try { + // Wait for a moment as notify requests don't have a response. So, they're effectively fire and forget, + // which can result in some race conditions with customizations. + Thread.sleep(100); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + public List findWorkspaceSymbol(String query) { + WorkspaceSymbolParams workspaceSymbolParams = new WorkspaceSymbolParams(); + workspaceSymbolParams.setQuery(query); + + return sendRequest(connection, "workspace/symbol", workspaceSymbolParams, LIST_SYMBOL_INFORMATION); + } + + public List listDocumentSymbols(String fileUri) { + DocumentSymbolParams documentSymbolParams = new DocumentSymbolParams(); + documentSymbolParams.setTextDocument(new TextDocumentIdentifier(fileUri)); + + return sendRequest(connection, "textDocument/documentSymbol", documentSymbolParams, LIST_SYMBOL_INFORMATION); + } + + public WorkspaceEdit renameSymbol(String fileUri, Position symbolPosition, String newName) { + RenameParams renameParams = new RenameParams(); + renameParams.setTextDocument(new TextDocumentIdentifier(fileUri)); + renameParams.setPosition(symbolPosition); + renameParams.setNewName(newName); + + return replaceTabsWithSpaces(sendRequest(connection, "textDocument/rename", renameParams, WorkspaceEdit.class)); + } + + public List listCodeActions(String fileUri, Range range, String codeActionKind) { + CodeActionContext context = new CodeActionContext(Collections.emptyList()); + context.setOnly(Collections.singletonList(codeActionKind)); + CodeActionParams codeActionParams = new CodeActionParams(new TextDocumentIdentifier(fileUri), range, context); + + List codeActions = sendRequest(connection, "textDocument/codeAction", codeActionParams, LIST_CODE_ACTION); + for (CodeAction codeAction : codeActions) { + if (codeAction.getEdit() != null) { + continue; + } + + if ("java.apply.workspaceEdit".equals(codeAction.getCommand().getCommand())) { + codeAction.setEdit(replaceTabsWithSpaces( + GSON.fromJson((JsonObject) codeAction.getCommand().getArguments().get(0), WorkspaceEdit.class))); + } + } + + return codeActions; + } + + private WorkspaceEdit replaceTabsWithSpaces(WorkspaceEdit workspaceEdit) { + if (workspaceEdit.getChanges() == null) { + return workspaceEdit; + } + + for (List textEdits : workspaceEdit.getChanges().values()) { + for (TextEdit textEdit : textEdits) { + textEdit.setNewText(textEdit.getNewText().replace("\t", " ")); + } + } + + return workspaceEdit; + } + + public void close() { + try { + connection.request("shutdown"); + connection.notifyWithSerializedObject("exit", "null"); + connection.stop(); + server.shutdown(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private static T sendRequest(Connection connection, String method, Object param, Type responseType) { + return GSON.fromJson(connection.requestWithSerializedObject(method, GSON.toJson(param)), responseType); + } + + private static Type createParameterizedType(Type rawType, Type... typeArguments) { + return new ParameterizedType() { + @Override + public Type[] getActualTypeArguments() { + return typeArguments; + } + + @Override + public Type getRawType() { + return rawType; + } + + @Override + public Type getOwnerType() { + return null; + } + }; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/customization/implementation/ls/EclipseLanguageServerFacade.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/customization/implementation/ls/EclipseLanguageServerFacade.java new file mode 100644 index 0000000000..5306ba9e86 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/customization/implementation/ls/EclipseLanguageServerFacade.java @@ -0,0 +1,176 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.customization.implementation.ls; + +import com.microsoft.typespec.http.client.generator.core.customization.implementation.Utils; +import org.apache.tools.tar.TarEntry; +import org.apache.tools.tar.TarInputStream; +import org.slf4j.Logger; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UncheckedIOException; +import java.net.URI; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.zip.GZIPInputStream; + +public class EclipseLanguageServerFacade { + private static final String DOWNLOAD_BASE_URL + = "https://www.eclipse.org/downloads/download.php?file=/jdtls/milestones/"; + + private final Process server; + + public EclipseLanguageServerFacade(String pathToLanguageServerPlugin, Logger logger) { + Runtime.getRuntime().addShutdownHook(new Thread(this::shutdown)); + try { + int javaVersion = Runtime.version().feature(); + + Path languageServerPath = (pathToLanguageServerPlugin == null) + ? getLanguageServerDirectory(javaVersion, logger) + : Paths.get(pathToLanguageServerPlugin).resolve("jdt-language-server"); + + List command = new ArrayList<>(); + command.add("java"); + command.add("-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=1044"); + command.add("-Declipse.application=org.eclipse.jdt.ls.core.id1"); + command.add("-Dosgi.bundles.defaultStartLevel=4"); + command.add("-Declipse.product=org.eclipse.jdt.ls.core.product"); + command.add("-Dlog.protocol=true"); + command.add("-Dlog.level=ALL"); + command.add("-noverify"); + command.add("-Xmx1G"); + command.add("-jar"); + + // This will need to get update when the target version of Eclipse language server changes. + if (javaVersion < 17) { + // JAR to start v1.12.0 + command.add("./plugins/org.eclipse.equinox.launcher_1.6.400.v20210924-0641.jar"); + } else if (javaVersion < 21) { + // JAR to start v1.29.0 + command.add("./plugins/org.eclipse.equinox.launcher_1.6.500.v20230717-2134.jar"); + } else { + // JAR to start v1.31.0 + command.add("./plugins/org.eclipse.equinox.launcher_1.6.700.v20231214-2017.jar"); + } + + command.add("--add-modules=ALL-SYSTEM"); + command.add("--add-opens java.base/java.util=ALL-UNNAMED"); + command.add("--add-opens java.base/java.lang=ALL-UNNAMED"); + + command.add("-configuration"); + + if (Utils.isWindows()) { + command.add("./config_win"); + } else if (Utils.isMac()) { + command.add("./config_mac"); + } else { + command.add("./config_linux"); + } + + logger.info("Starting Eclipse JDT language server at {}", languageServerPath); + server = new ProcessBuilder(command) + .redirectOutput(ProcessBuilder.Redirect.PIPE) + .redirectInput(ProcessBuilder.Redirect.PIPE) + .redirectErrorStream(true) + .directory(languageServerPath.toFile()) + .start(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private static Path getLanguageServerDirectory(int javaVersion, Logger logger) throws IOException { + Path tmp = Paths.get(System.getProperty("java.io.tmpdir")); + Path autorestLanguageServer = tmp.resolve("autorest-java-language-server"); + + URL downloadUrl; + Path languageServerPath; + if (javaVersion < 17) { + // Eclipse JDT language server version 1.12.0 is the last version that supports Java 11, which is + // autorest.java's baseline. + downloadUrl = URI.create(DOWNLOAD_BASE_URL + "1.12.0/jdt-language-server-1.12.0-202206011637.tar.gz") + .toURL(); + languageServerPath = autorestLanguageServer.resolve("1.12.0"); + } else if (javaVersion < 21) { + // Eclipse JDT language server version 1.29.0 is the latest version that supports Java 17. + // In the future this else statement may need to be replaced with an else if as newer versions of + // Eclipse JDT language server may baseline on Java 21 (or later). + downloadUrl = URI.create(DOWNLOAD_BASE_URL + "1.29.0/jdt-language-server-1.29.0-202310261436.tar.gz") + .toURL(); + languageServerPath = autorestLanguageServer.resolve("1.29.0"); + } else { + // Eclipse JDT language server version 1.31.0 is the latest version that supports Java 21. + // In the future this else statement may need to be replaced with an else if as newer versions of + // Eclipse JDT language server may baseline on Java 25 (or later). + downloadUrl = URI.create(DOWNLOAD_BASE_URL + "1.31.0/jdt-language-server-1.31.0-202401111522.tar.gz") + .toURL(); + languageServerPath = autorestLanguageServer.resolve("1.31.0"); + } + + Path languageServer = languageServerPath.resolve("jdt-language-server"); + if (!Files.exists(languageServerPath) || !Files.exists(languageServer)) { + Files.createDirectories(languageServerPath); + Path zipPath = languageServerPath.resolve("jdt-language-server.tar.gz"); + logger.info("Downloading Eclipse JDT language server from {} to {}", downloadUrl, zipPath); + try (InputStream in = downloadUrl.openStream()) { + Files.copy(in, zipPath); + } + logger.info("Downloaded Eclipse JDT language server to {}", zipPath); + + return unzipLanguageServer(zipPath); + } + + return languageServer; + } + + private static Path unzipLanguageServer(Path zipPath) throws IOException { + try (TarInputStream tar = new TarInputStream(new GZIPInputStream(Files.newInputStream(zipPath)))) { + Path languageServerDirectory = zipPath.getParent().resolve("jdt-language-server"); + Files.createDirectory(languageServerDirectory); + TarEntry entry; + while ((entry = tar.getNextEntry()) != null) { + if (entry.isDirectory()) { + Files.createDirectories(languageServerDirectory.resolve(entry.getName())); + } else { + Files.copy(tar, languageServerDirectory.resolve(entry.getName())); + } + } + + return languageServerDirectory; + } + } + + OutputStream getOutputStream() { + return server.getOutputStream(); + } + + InputStream getInputStream() { + return server.getInputStream(); + } + + boolean isAlive() { + return server.isAlive(); + } + + String getServerError() { + try { + return new String(server.getInputStream().readAllBytes(), StandardCharsets.UTF_8); + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + + public void shutdown() { + if (server != null && server.isAlive()) { + server.destroyForcibly(); + } + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/customization/implementation/ls/models/JavaCodeActionKind.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/customization/implementation/ls/models/JavaCodeActionKind.java new file mode 100644 index 0000000000..7c80e3a646 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/customization/implementation/ls/models/JavaCodeActionKind.java @@ -0,0 +1,111 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.customization.implementation.ls.models; + +import org.eclipse.lsp4j.CodeActionKind; + +import java.util.HashMap; +import java.util.Map; + +public enum JavaCodeActionKind { + /** + * Base kind for "generate" source actions + */ + SOURCE_GENERATE(CodeActionKind.Source + ".generate"), + + /** + * Generate accessors kind + */ + SOURCE_GENERATE_ACCESSORS(SOURCE_GENERATE + ".accessors"), + + /** + * Generate hashCode/equals kind + */ + SOURCE_GENERATE_HASHCODE_EQUALS(SOURCE_GENERATE + ".hashCodeEquals"), + + /** + * Generate toString kind + */ + SOURCE_GENERATE_TO_STRING(SOURCE_GENERATE + ".toString"), + + /** + * Generate constructors kind + */ + SOURCE_GENERATE_CONSTRUCTORS(SOURCE_GENERATE + ".constructors"), + + /** + * Generate delegate methods + */ + SOURCE_GENERATE_DELEGATE_METHODS(SOURCE_GENERATE + ".delegateMethods"), + + /** + * Override/Implement methods kind + */ + SOURCE_OVERRIDE_METHODS(CodeActionKind.Source + ".overrideMethods"), + + /** + * Extract to method kind + */ + REFACTOR_EXTRACT_METHOD(CodeActionKind.RefactorExtract + ".function"), // using `.function` instead of `.method` to match existing keybinding), + + /** + * Extract to constant kind + */ + REFACTOR_EXTRACT_CONSTANT(CodeActionKind.RefactorExtract + ".constant"), + + /** + * Extract to variable kind + */ + REFACTOR_EXTRACT_VARIABLE(CodeActionKind.RefactorExtract + ".variable"), + + /** + * Extract to field kind + */ + REFACTOR_EXTRACT_FIELD(CodeActionKind.RefactorExtract + ".field"), + + /** + * Move kind + */ + REFACTOR_MOVE(CodeActionKind.Refactor + ".move"), + + /** + * Assign statement to new local variable + */ + REFACTOR_ASSIGN_VARIABLE(CodeActionKind.Refactor + ".assign.variable"), + + /** + * Assign statement to new field + */ + REFACTOR_ASSIGN_FIELD(CodeActionKind.Refactor + ".assign.field"), + + /** + * Base kind for "quickassist" code actions + */ + QUICK_ASSIST("quickassist"); + + private static final Map STRING_TO_KIND_MAP; + + static { + STRING_TO_KIND_MAP = new HashMap<>(); + + for (JavaCodeActionKind kind : JavaCodeActionKind.values()) { + STRING_TO_KIND_MAP.putIfAbsent(kind.value, kind); + } + } + + private final String value; + + JavaCodeActionKind(String value) { + this.value = value; + } + + public static JavaCodeActionKind fromString(String value) { + return STRING_TO_KIND_MAP.get(value); + } + + @Override + public String toString() { + return value; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/base/util/FileUtils.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/base/util/FileUtils.java new file mode 100644 index 0000000000..d9237240f0 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/base/util/FileUtils.java @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.typespec.http.client.generator.core.extension.base.util; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +/** + * Utility class for file operations. + */ +public final class FileUtils { + private FileUtils() { + } + + /** + * Creates a temporary directory. + *

+ * If the environment setting {@code codegen.java.temp.directory} is set, the directory will be created under the + * specified path. Otherwise, the directory will be created under the system default temporary directory. + *

+ * {@link System#getProperty(String)} is checked before {@link System#getenv(String)}. + *

+ * If {@code codegen.java.temp.directory} is set to a non-existent path, the directory will be created under the + * system default temporary directory. + * + * @param prefix The prefix string to be used in generating the directory's name; may be {@code null}. + * @return The path to the newly created directory. + * @throws IOException If an I/O error occurs. + */ + public static Path createTempDirectory(String prefix) throws IOException { + String tempDirectory = System.getProperty("codegen.java.temp.directory"); + if (tempDirectory == null) { + tempDirectory = System.getenv("codegen.java.temp.directory"); + } + + if (tempDirectory != null) { + Path tempDirectoryPath = Paths.get(tempDirectory); + if (Files.exists(tempDirectoryPath)) { + return Files.createTempDirectory(tempDirectoryPath, prefix); + } + } + + return Files.createTempDirectory(prefix); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/base/util/HttpExceptionType.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/base/util/HttpExceptionType.java new file mode 100644 index 0000000000..63a1db53c6 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/base/util/HttpExceptionType.java @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.extension.base.util; + +/** + * Represents exception types for HTTP requests and responses. + */ +public enum HttpExceptionType { + /** + * The exception thrown when failing to authenticate the HTTP request with status code of {@code 4XX}, typically + * {@code 401 Unauthorized}. + * + *

A runtime exception indicating request authorization failure caused by one of the following scenarios:

+ *
    + *
  • A client did not send the required authorization credentials to access the requested resource, i.e. + * Authorization HTTP header is missing in the request
  • + *
  • If the request contains the HTTP Authorization header, then the exception indicates that authorization + * has been refused for the credentials contained in the request header.
  • + *
+ */ + CLIENT_AUTHENTICATION, + + /** + * The exception thrown when the HTTP request tried to create an already existing resource and received a status + * code {@code 4XX}, typically {@code 412 Conflict}. + */ + RESOURCE_EXISTS, + + /** + * The exception thrown for invalid resource modification with status code of {@code 4XX}, typically + * {@code 409 Conflict}. + */ + RESOURCE_MODIFIED, + + /** + * The exception thrown when receiving an error response with status code {@code 412 response} (for update) or + * {@code 404 Not Found} (for get/post). + */ + RESOURCE_NOT_FOUND, + + /** + * This exception thrown when an HTTP request has reached the maximum number of redirect attempts with a status code + * of {@code 3XX}. + */ + TOO_MANY_REDIRECTS +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/base/util/JsonUtils.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/base/util/JsonUtils.java new file mode 100644 index 0000000000..f5403b246e --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/base/util/JsonUtils.java @@ -0,0 +1,95 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.typespec.http.client.generator.core.extension.base.util; + +import com.azure.json.JsonReader; +import com.azure.json.JsonToken; +import com.azure.json.WriteValueCallback; + +import java.io.IOException; +import java.util.function.Supplier; + +/** + * Utility classes that help simplify repetitive code with {@code azure-json}. + */ +public final class JsonUtils { + /** + * Reads a JSON object from the passed {@code jsonReader} and creates an object of type {@code T} using the passed + * {@code objectCreator}. The {@code callback} is then called for each field in the JSON object to read the field + * value and set it on the object. + * + * @param jsonReader The JSON reader to read the object from. + * @param objectCreator The supplier that creates a new instance of the object. + * @param callback The callback that reads the field value and sets it on the object. + * @return The object created from the JSON object. + * @param The type of object to create. + * @throws IOException If an error occurs while reading the JSON object. + */ + public static T readObject(JsonReader jsonReader, Supplier objectCreator, ReadObjectCallback callback) + throws IOException { + return jsonReader.readObject(reader -> { + T object = objectCreator.get(); + fieldReaderLoop(reader, (fieldName, r) -> callback.read(object, fieldName, r)); + return object; + }); + } + + /** + * Reads a JSON object from the passed {@code jsonReader} and creates an object of type {@code T} using the passed + * {@code objectCreator}. This method will skip the children of each field in the JSON object. + * + * @param jsonReader The JSON reader to read the object from. + * @param objectCreator The supplier that creates a new instance of the object. + * @return The object created from the JSON object. + * @param The type of object to create. + * @throws IOException If an error occurs while reading the JSON object. + */ + public static T readEmptyObject(JsonReader jsonReader, Supplier objectCreator) throws IOException { + return jsonReader.readObject(reader -> { + T object = objectCreator.get(); + fieldReaderLoop(reader, (fieldName, r) -> r.skipChildren()); + return object; + }); + } + + /** + * Callback for reading a JSON object. + * + * @param The type of the object being read. + */ + public interface ReadObjectCallback { + /** + * Reads a field from the JSON object and sets it on the object. + * + * @param object The object to set the field on. + * @param fieldName The name of the field being read. + * @param jsonReader The JSON reader to read the field value from. + * @throws IOException If an error occurs while reading the field value. + */ + void read(T object, String fieldName, JsonReader jsonReader) throws IOException; + } + + /** + * Helper method to iterate over the field of a JSON object. + *

+ * This method will reader the passed {@code jsonReader} until the end of the object is reached. For each field it + * will get the field name and iterate the reader to the next token. This method will then pass the field name and + * reader to the {@code fieldConsumer} for it to consume the JSON field as needed for the object being read. + * + * @param jsonReader The JSON reader to read the object from. + * @param fieldConsumer The consumer that will consume the field name and reader for each field in the object. + * @throws IOException If an error occurs while reading the JSON object. + */ + public static void fieldReaderLoop(JsonReader jsonReader, WriteValueCallback fieldConsumer) + throws IOException { + while (jsonReader.nextToken() != JsonToken.END_OBJECT) { + String fieldName = jsonReader.getFieldName(); + jsonReader.nextToken(); + + fieldConsumer.write(fieldName, jsonReader); + } + } + + private JsonUtils() { + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/jsonrpc/Connection.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/jsonrpc/Connection.java new file mode 100644 index 0000000000..13e019a6c3 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/jsonrpc/Connection.java @@ -0,0 +1,478 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.extension.jsonrpc; + +import com.azure.json.JsonProviders; +import com.azure.json.JsonReader; +import com.azure.json.JsonWriter; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UncheckedIOException; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.Supplier; + +/** + * Represents a connection. + */ +public class Connection { + private OutputStream writer; + private PeekingBinaryReader reader; + private boolean isDisposed = false; + private final AtomicInteger requestId; + private final Map> tasks = new ConcurrentHashMap<>(); + private final ExecutorService executorService = Executors.newCachedThreadPool(); + private final CompletableFuture loop; + private final Map> dispatch = new ConcurrentHashMap<>(); + + /** + * Create a new Connection. + * + * @param writer The output stream to write to. + * @param input The input stream to read from. + */ + public Connection(OutputStream writer, InputStream input) { + this.writer = writer; + this.reader = new PeekingBinaryReader(input); + this.loop = CompletableFuture.runAsync(this::listen); + this.requestId = new AtomicInteger(0); + } + + private boolean isAlive = true; + + /** + * Stops the connection. + */ + public void stop() { + isAlive = false; + loop.cancel(true); + } + + private String readJson() { + String jsonText = ""; + while (true) { + try { + jsonText += reader.readAsciiLine(); // + "\n"; + } catch (IOException e) { + throw new RuntimeException("Cannot read JSON input"); + } + try { + validateJsonText(jsonText); + return jsonText; + } catch (IOException e) { + // not enough text? + } + } + } + + /** + * Tests that the passed {@code jsonText} is valid JSON. + * + * @param jsonText The JSON text to validate. + * @throws IOException If the JSON text is invalid. + */ + private static void validateJsonText(String jsonText) throws IOException { + try (JsonReader jsonReader = JsonProviders.createReader(jsonText)) { + jsonReader.readUntyped(); + } + } + + /** + * Dispatches a message. + * + * @param path The path. + * @param method The method that gets the result as a JSON string. + */ + public void dispatch(String path, Supplier method) { + dispatch.put(path, input -> { + String result = method.get(); + + return (result == null) ? "null" : result; + }); + } + + private static List readArguments(String input) { + try (JsonReader jsonReader = JsonProviders.createReader(input)) { + List ret = jsonReader.readArray(JsonReader::getString); + if (ret.size() == 2) { + // Return passed array if size is larger than 0, otherwise return a new ArrayList + return ret; + } + + throw new RuntimeException("Invalid number of arguments"); + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + + /** + * Dispatches a notification. + * + * @param path The path. + * @param method The method. + */ + public void dispatchNotification(String path, Runnable method) { + dispatch.put(path, input -> { + method.run(); + return null; + }); + } + + /** + * Dispatches a message. + * + * @param path The path. + * @param method The method that gets the result as a JSON string. + */ + public void dispatch(String path, BiFunction method) { + dispatch.put(path, input -> { + List args = readArguments(input); + return String.valueOf(method.apply(args.get(0), args.get(1))); + }); + } + + private String readJson(int contentLength) { + try { + return new String(reader.readBytes(contentLength), StandardCharsets.UTF_8); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private boolean listen() { + while (isAlive) { + try { + int ch = reader.peekByte(); + if (-1 == ch) { + // didn't get anything. start again, it'll know if we're shutting down + break; + } + + if ('{' == ch || '[' == ch) { + // looks like a json block or array. let's do this. + // don't wait for this to finish! + process(readJson(), '{' == ch); + + // we're done here, start again. + continue; + } + + // We're looking at headers + Map headers = new HashMap<>(); + String line = reader.readAsciiLine(); + while (line != null && !line.isEmpty()) { + String[] bits = line.split(":", 2); + headers.put(bits[0].trim(), bits[1].trim()); + line = reader.readAsciiLine(); + } + + ch = reader.peekByte(); + // the next character had better be a { or [ + if ('{' == ch || '[' == ch) { + String contentLengthStr = headers.get("Content-Length"); + if (contentLengthStr != null && !contentLengthStr.isEmpty()) { + int contentLength = Integer.parseInt(contentLengthStr); + // don't wait for this to finish! + process(readJson(contentLength), '{' == ch); + continue; + } + // looks like a json block or array. let's do this. + // don't wait for this to finish! + process(readJson(), '{' == ch); + // we're done here, start again. + continue; + } + + return false; + + } catch (Exception e) { + if (!isAlive) { + throw new RuntimeException(e); + } + } + } + return false; + } + + /** + * Processes a message. + * + * @param content The content. + * @param isObject Whether the JSON {@code content} is a JSON object. + */ + public void process(String content, boolean isObject) { + // The only times this method is called is when the beginning portion of the JSON text is '{' or '['. + // So, instead of the previous design when using Jackson where a fully processed JsonNode was passed, use a + // simpler parameter 'isObject' to check if we are in a valid processing state. + if (!isObject) { + System.err.println("Unhandled: Batch Request"); + return; + } + + executorService.submit(() -> { + + try (JsonReader jsonReader = JsonProviders.createReader(content)) { + Map jobject = jsonReader.readMap(reader -> { + if (reader.isStartArrayOrObject()) { + return reader.readChildren(); + } else { + return reader.getString(); + } + }); + + String method = jobject.get("method"); + if (method != null) { + int id = processIdField(jobject.get("id")); + + // this is a method call. + // pass it to the service that is listening... + if (dispatch.containsKey(method)) { + Function fn = dispatch.get(method); + String parameters = jobject.get("params"); + String result = fn.apply(parameters); + if (id != -1) { + // if this is a request, send the response. + respond(id, result); + } + } + return; + } + + if (jobject.containsKey("result")) { + String result = jobject.get("result"); + int id = processIdField(jobject.get("id")); + if (id != -1) { + CompletableFuture f = tasks.remove(id); + + try { + f.complete(result); + } catch (Exception e) { + f.completeExceptionally(e); + } + } + return; + } + + String error = jobject.get("error"); + if (error != null) { + int id = processIdField(jobject.get("id")); + if (id != -1) { + CompletableFuture f = tasks.remove(id); + + try (JsonReader errorReader = JsonProviders.createReader(error)) { + Map errorObject = errorReader.readMap(JsonReader::readUntyped); + + String message = String.valueOf(errorObject.get("message")); + Object dataField = errorObject.get("data"); + if (dataField != null) { + message += " (" + dataField + ")"; + } + f.completeExceptionally(new RuntimeException(message)); + } catch (Exception e) { + f.completeExceptionally(e); + } + } + } + } catch (Exception e) { + e.printStackTrace(); + } + }); + } + + private static int processIdField(String idField) { + return (idField == null) ? -1 : Integer.parseInt(idField); + } + + /** + * Closes the connection. + * + * @throws IOException If an I/O error occurs. + */ + protected void close() throws IOException { + // ensure that we are in a cancelled state. + isAlive = false; + if (!isDisposed) { + // make sure we can't dispose twice + isDisposed = true; + tasks.forEach((ignored, future) -> future.cancel(true)); + + writer.close(); + writer = null; + reader.close(); + reader = null; + } + } + + private void send(String text) { + byte[] data = text.getBytes(StandardCharsets.UTF_8); + byte[] header = ("Content-Length: " + data.length + "\r\n\r\n").getBytes(StandardCharsets.US_ASCII); + byte[] buffer = new byte[header.length + data.length]; + System.arraycopy(header, 0, buffer, 0, header.length); + System.arraycopy(data, 0, buffer, header.length, data.length); + try { + writer.write(buffer); + writer.flush(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + /** + * Sends an error. + * + * @param id The id. + * @param code The code. + * @param message The message. + */ + public void sendError(int id, int code, String message) { + try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + JsonWriter jsonWriter = JsonProviders.createWriter(outputStream)) { + jsonWriter.writeStartObject() + .writeStringField("jsonrpc", "2.0") + .writeIntField("id", id) + .writeStringField("message", message) + .writeStartObject("error") + .writeIntField("code", code) + .writeEndObject() + .writeEndObject() + .flush(); + + send(outputStream.toString(StandardCharsets.UTF_8)); + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + + /** + * Sends a response. + * + * @param id The id. + * @param value The value. + */ + public void respond(int id, String value) { + try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + JsonWriter jsonWriter = JsonProviders.createWriter(outputStream)) { + jsonWriter.writeStartObject() + .writeStringField("jsonrpc", "2.0") + .writeIntField("id", id) + .writeRawField("result", value) + .writeEndObject() + .flush(); + + send(outputStream.toString(StandardCharsets.UTF_8)); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + /** + * Sends a notification. + * + * @param methodName The method name. + * @param values The values. + */ + public void notify(String methodName, Object... values) { + try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + JsonWriter jsonWriter = JsonProviders.createWriter(outputStream)) { + jsonWriter.writeArray(values, JsonWriter::writeUntyped).flush(); + notifyWithSerializedObject(methodName, outputStream.toString(StandardCharsets.UTF_8)); + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + + /** + * Sends a notification. + * + * @param methodName The method name. + * @param serializedObject The serialized object. + */ + public void notifyWithSerializedObject(String methodName, String serializedObject) { + String json = (serializedObject == null) + ? "{\"jsonrpc\":\"2.0\",\"method\":\"" + methodName + "\"}" + : "{\"jsonrpc\":\"2.0\",\"method\":\"" + methodName + "\",\"params\":" + serializedObject + "}"; + + send(json); + } + + /** + * Sends a request. + * + * @param methodName The method name. + * @param values The values. + * @return The result. + */ + public String request(String methodName, Object... values) { + int id = requestId.getAndIncrement(); + CompletableFuture response = new CompletableFuture<>(); + tasks.put(id, response); + + try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + JsonWriter jsonWriter = JsonProviders.createWriter(outputStream)) { + jsonWriter.writeStartObject() + .writeStringField("jsonrpc", "2.0") + .writeStringField("method", methodName) + .writeIntField("id", id) + .writeArrayField("params", values, JsonWriter::writeUntyped) + .writeEndObject() + .flush(); + + send(outputStream.toString(StandardCharsets.UTF_8)); + + return response.get(); + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } catch (InterruptedException | ExecutionException e) { + throw new RuntimeException(e); + } + } + + /** + * Sends a request. + * + * @param method The method name. + * @param serializedObject The serialized object. + * @return The result. + */ + public String requestWithSerializedObject(String method, String serializedObject) { + int id = requestId.getAndIncrement(); + CompletableFuture response = new CompletableFuture<>(); + tasks.put(id, response); + + String json = (serializedObject == null) + ? "{\"jsonrpc\":\"2.0\",\"method\":\"" + method + "\",\"id\":" + id + "}" + : "{\"jsonrpc\":\"2.0\",\"method\":\"" + method + "\",\"id\":" + id + ",\"params\":" + serializedObject + "}"; + + send(json); + try { + return response.get(); + } catch (InterruptedException | ExecutionException e) { + throw new RuntimeException(e); + } + } + + /** + * Waits for all requests to complete. + */ + public void waitForAll() { + try { + loop.get(); + } catch (InterruptedException | ExecutionException e) { + throw new RuntimeException(e); + } + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/jsonrpc/PeekingBinaryReader.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/jsonrpc/PeekingBinaryReader.java new file mode 100644 index 0000000000..05ebd10565 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/jsonrpc/PeekingBinaryReader.java @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.extension.jsonrpc; + +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; + +class PeekingBinaryReader implements Closeable { + Integer lastByte; + InputStream input; + + PeekingBinaryReader(InputStream input) { + this.lastByte = null; + this.input = input; + } + + int readByte() throws IOException { + if (lastByte != null) { + int result = lastByte; + lastByte = null; + return result; + } + return input.read(); + } + + int peekByte() throws IOException { + if (lastByte != null) { + return lastByte; + } + int result = readByte(); + if (result != -1) { + lastByte = result; + } + return result; + } + + byte[] readBytes(int count) throws IOException { + byte[] buffer = new byte[count]; + int read = 0; + if (count > 0 && lastByte != null) { + buffer[read++] = (byte) (int) lastByte; + lastByte = null; + } + while (read < count) { + read += input.read(buffer, read, count - read); + } + return buffer; + } + + String readAsciiLine() throws IOException { + StringBuilder result = new StringBuilder(); + int c = readByte(); + while (c != '\r' && c != '\n' && c != -1) { + result.append((char) c); + c = readByte(); + } + if (c == '\r' && peekByte() == '\n') { + readByte(); + } + if (c == -1 && result.length() == 0) { + return null; + } + return result.toString(); + } + + public void close() throws IOException { + input.close(); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/jsonrpc/package-info.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/jsonrpc/package-info.java new file mode 100644 index 0000000000..d0a67b5808 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/jsonrpc/package-info.java @@ -0,0 +1,7 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/** + * Contains JSON RPC related classes. + */ +package com.microsoft.typespec.http.client.generator.core.extension.jsonrpc; diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/Message.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/Message.java new file mode 100644 index 0000000000..fb2e39a285 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/Message.java @@ -0,0 +1,184 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.extension.model; + +import com.azure.json.JsonReader; +import com.azure.json.JsonSerializable; +import com.azure.json.JsonToken; +import com.azure.json.JsonWriter; + +import java.io.IOException; +import java.util.List; + +/** + * Represents a message. + */ +public class Message implements JsonSerializable { + + /** + * Represents a message channel. + */ + public MessageChannel channel; + + /** + * Represents details. + */ + public Object details; + + /** + * Represents text. + */ + public String text; + + /** + * Represents a key. + */ + public List key; + + /** + * Represents a source location. + */ + public List source; + + /** + * Creates a new instance of the Message class. + */ + public Message() { + } + + /** + * Gets the source location of the message. + * + * @return The source location of the message. + */ + public List getSource() { + return source; + } + + /** + * Gets the key of the message. + * + * @return The key of the message. + */ + public List getKey() { + return key; + } + + /** + * Gets the details of the message. + * + * @return The details of the message. + */ + public Object getDetails() { + return details; + } + + /** + * Gets the channel of the message. + * + * @return The channel of the message. + */ + public MessageChannel getChannel() { + return channel; + } + + /** + * Gets the text of the message. + * + * @return The text of the message. + */ + public String getText() { + return text; + } + + /** + * Sets the channel of the message. + * + * @param channel The channel of the message. + */ + public void setChannel(MessageChannel channel) { + this.channel = channel; + } + + /** + * Sets the details of the message. + * + * @param details The details of the message. + */ + public void setDetails(Object details) { + this.details = details; + } + + /** + * Sets the key of the message. + * + * @param key The key of the message. + */ + public void setKey(List key) { + this.key = key; + } + + /** + * Sets the source location of the message. + * + * @param source The source location of the message. + */ + public void setSource(List source) { + this.source = source; + } + + /** + * Sets the text of the message. + * + * @param text The text of the message. + */ + public void setText(String text) { + this.text = text; + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + return jsonWriter.writeStartObject() + .writeStringField("Channel", channel == null ? null : channel.toString()) + .writeUntypedField("Details", details) + .writeStringField("Text", text) + .writeArrayField("Key", key, JsonWriter::writeString) + .writeArrayField("Source", source, JsonWriter::writeJson) + .writeEndObject(); + } + + /** + * Deserializes a Message instance from the JSON data. + * + * @param jsonReader The JSON reader to deserialize from. + * @return A Message instance deserialized from the JSON data. + * @throws IOException If an error occurs during deserialization. + */ + public static Message fromJson(JsonReader jsonReader) throws IOException { + return jsonReader.readObject(reader -> { + Message message = new Message(); + + while (reader.nextToken() != JsonToken.END_OBJECT) { + String fieldName = reader.getFieldName(); + reader.nextToken(); + + if ("Channel".equals(fieldName)) { + message.channel = MessageChannel.valueOf(reader.getString()); + } else if ("Details".equals(fieldName)) { + message.details = reader.readUntyped(); + } else if ("Text".equals(fieldName)) { + message.text = reader.getString(); + } else if ("Key".equals(fieldName)) { + message.key = reader.readArray(JsonReader::getString); + } else if ("Source".equals(fieldName)) { + message.source = reader.readArray(SourceLocation::fromJson); + } else { + reader.skipChildren(); + } + } + + return message; + }); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/MessageChannel.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/MessageChannel.java new file mode 100644 index 0000000000..21048f9b8d --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/MessageChannel.java @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.extension.model; + +/** + * Represents a message channel. + */ +public enum MessageChannel { + /** + * Represents an information message. + */ + INFORMATION("information"), + + /** + * Represents a hint message. + */ + HINT("hint"), + + /** + * Represents a warning message. + */ + WARNING("warning"), + + /** + * Represents a debug message. + */ + DEBUG("debug"), + + /** + * Represents a verbose message. + */ + VERBOSE("verbose"), + + /** + * Represents an error message. + */ + ERROR("error"), + + /** + * Represents a fatal message. + */ + FATAL("fatal"), + + /** + * Represents a file message. + */ + FILE("file"), + + /** + * Represents a configuration message. + */ + CONFIGURATION("configuration"); + + private final String value; + + MessageChannel(String value) { + this.value = value; + } + + @Override + public String toString() { + return this.value; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/SmartLocation.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/SmartLocation.java new file mode 100644 index 0000000000..9252a6f9de --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/SmartLocation.java @@ -0,0 +1,76 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.extension.model; + +import com.azure.json.JsonReader; +import com.azure.json.JsonSerializable; +import com.azure.json.JsonToken; +import com.azure.json.JsonWriter; + +import java.io.IOException; +import java.util.List; + +/** + * Represents a smart location. + */ +public class SmartLocation implements JsonSerializable { + private List path; + + /** + * Creates a new instance of the SmartLocation class. + */ + public SmartLocation() { + } + + /** + * Gets the path of the location. + * + * @return The path of the location. + */ + public List getPath() { + return path; + } + + /** + * Sets the path of the location. + * + * @param path The path of the location. + */ + public void setPath(List path) { + this.path = path; + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + return jsonWriter.writeStartObject() + .writeArrayField("path", path, JsonWriter::writeUntyped) + .writeEndObject(); + } + + /** + * Deserializes a SmartLocation instance from the JSON data. + * + * @param jsonReader The JSON reader to deserialize from. + * @return A SmartLocation instance deserialized from the JSON data. + * @throws IOException If an error occurs during deserialization. + */ + public static SmartLocation fromJson(JsonReader jsonReader) throws IOException { + return jsonReader.readObject(reader -> { + SmartLocation smartLocation = new SmartLocation(); + + while (reader.nextToken() != JsonToken.END_OBJECT) { + String fieldName = reader.getFieldName(); + reader.nextToken(); + + if ("path".equals(fieldName)) { + smartLocation.path = reader.readArray(JsonReader::readUntyped); + } else { + reader.skipChildren();; + } + } + + return smartLocation; + }); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/SourceLocation.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/SourceLocation.java new file mode 100644 index 0000000000..6951e5294d --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/SourceLocation.java @@ -0,0 +1,97 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.extension.model; + +import com.azure.json.JsonReader; +import com.azure.json.JsonSerializable; +import com.azure.json.JsonToken; +import com.azure.json.JsonWriter; + +import java.io.IOException; + +/** + * Represents a source location. + */ +public class SourceLocation implements JsonSerializable { + private String document; + private SmartLocation position; + + /** + * Creates a new instance of the SourceLocation class. + */ + public SourceLocation() { + } + + /** + * Gets the position of the location. + * + * @return The position of the location. + */ + public SmartLocation getPosition() { + return position; + } + + /** + * Gets the document of the location. + * + * @return The document of the location. + */ + public String getDocument() { + return document; + } + + /** + * Sets the document of the location. + * + * @param document The document of the location. + */ + public void setDocument(String document) { + this.document = document; + } + + /** + * Sets the position of the location. + * + * @param position The position of the location. + */ + public void setPosition(SmartLocation position) { + this.position = position; + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + return jsonWriter.writeStartObject() + .writeStringField("document", document) + .writeJsonField("position", position) + .writeEndObject(); + } + + /** + * Deserializes a SourceLocation instance from the JSON data. + * + * @param jsonReader The JSON reader to deserialize from. + * @return A SourceLocation instance deserialized from the JSON data. + * @throws IOException If an error occurs during deserialization. + */ + public static SourceLocation fromJson(JsonReader jsonReader) throws IOException { + return jsonReader.readObject(reader -> { + SourceLocation sourceLocation = new SourceLocation(); + + while (reader.nextToken() != JsonToken.END_OBJECT) { + String fieldName = reader.getFieldName(); + reader.nextToken(); + + if ("document".equals(fieldName)) { + sourceLocation.document = reader.getString(); + } else if ("position".equals(fieldName)) { + sourceLocation.position = SmartLocation.fromJson(reader); + } else { + reader.skipChildren(); + } + } + + return sourceLocation; + }); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/AndSchema.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/AndSchema.java new file mode 100644 index 0000000000..fe7888270f --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/AndSchema.java @@ -0,0 +1,120 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.extension.model.codemodel; + +import com.microsoft.typespec.http.client.generator.core.extension.base.util.JsonUtils; +import com.azure.json.JsonReader; +import com.azure.json.JsonWriter; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + * An AND relationship between several schemas. + */ +public class AndSchema extends ComplexSchema { + private List allOf = new ArrayList<>(); + private String discriminatorValue; + + /** + * Creates a new instance of the AndSchema class. + */ + public AndSchema() { + super(); + } + + /** + * Gets the schemas that this schema composes. (Required) + * + * @return The schemas that this schema composes. + */ + public List getAllOf() { + return allOf; + } + + /** + * Sets the schemas that this schema composes. (Required) + * + * @param allOf The schemas that this schema composes. + */ + public void setAllOf(List allOf) { + this.allOf = allOf; + } + + /** + * Gets the value of the discriminator for this schema. + * + * @return The value of the discriminator for this schema. + */ + public String getDiscriminatorValue() { + return discriminatorValue; + } + + /** + * Sets the value of the discriminator for this schema. + * + * @param discriminatorValue The value of the discriminator for this schema. + */ + public void setDiscriminatorValue(String discriminatorValue) { + this.discriminatorValue = discriminatorValue; + } + + @Override + public String toString() { + return AndSchema.class.getName() + "@" + Integer.toHexString(System.identityHashCode(this)) + "[allOf=" + + Objects.toString(allOf, "") + ",discriminatorValue=" + + Objects.toString(discriminatorValue, "") + ']'; + } + + @Override + public int hashCode() { + return Objects.hash(allOf, discriminatorValue); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof AndSchema)) { + return false; + } + AndSchema rhs = ((AndSchema) other); + return Objects.equals(allOf, rhs.allOf) && Objects.equals(discriminatorValue, rhs.discriminatorValue); + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + return super.writeParentProperties(jsonWriter.writeStartObject()) + .writeArrayField("allOf", allOf, JsonWriter::writeJson) + .writeStringField("discriminatorValue", discriminatorValue) + .writeEndObject(); + } + + /** + * Deserializes an AndSchema instance from the JSON data. + * + * @param jsonReader The JSON reader to deserialize from. + * @return An AndSchema instance deserialized from the JSON data. + * @throws IOException If an error occurs during deserialization. + */ + public static AndSchema fromJson(JsonReader jsonReader) throws IOException { + return JsonUtils.readObject(jsonReader, AndSchema::new, (schema, fieldName, reader) -> { + if (schema.tryConsumeParentProperties(schema, fieldName, reader)) { + return; + } + + if ("allOf".equals(fieldName)) { + schema.allOf = reader.readArray(ComplexSchema::fromJson); + } else if ("discriminatorValue".equals(fieldName)) { + schema.discriminatorValue = reader.getString(); + } else { + reader.skipChildren(); + } + }); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/AnnotatedPropertyUtils.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/AnnotatedPropertyUtils.java new file mode 100644 index 0000000000..e7d57dd740 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/AnnotatedPropertyUtils.java @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.extension.model.codemodel; + +import org.yaml.snakeyaml.introspector.BeanAccess; +import org.yaml.snakeyaml.introspector.Property; +import org.yaml.snakeyaml.introspector.PropertyUtils; + +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * PropertyUtils that leverage @YamlProperty annotation on getter methods. + * @see YamlProperty + */ +public class AnnotatedPropertyUtils extends PropertyUtils { + private final Map, Map> cachedPropertyMap = new HashMap<>(); + + /** + * Creates a new instance of AnnotatedPropertyUtils class. + */ + public AnnotatedPropertyUtils() { + super(); + } + + @Override + protected Map getPropertiesMap(Class type, BeanAccess bAccess) { + if (cachedPropertyMap.get(type) != null) { + return new LinkedHashMap<>(cachedPropertyMap.get(type)); + } + Map propertyMap = super.getPropertiesMap(type, bAccess); + Map mappedPropertyMap = new HashMap<>(); + for (String propertyName : propertyMap.keySet()) { + Property property = propertyMap.get(propertyName); + YamlProperty yamlProperty = property.getAnnotation(YamlProperty.class); + if (yamlProperty != null) { + mappedPropertyMap.put(yamlProperty.value(), property); + } + } + propertyMap.putAll(mappedPropertyMap); + cachedPropertyMap.put(type, propertyMap); + return propertyMap; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/AnySchema.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/AnySchema.java new file mode 100644 index 0000000000..10844b6622 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/AnySchema.java @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.extension.model.codemodel; + +import com.microsoft.typespec.http.client.generator.core.extension.base.util.JsonUtils; +import com.azure.json.JsonReader; +import com.azure.json.JsonWriter; + +import java.io.IOException; + +/** + * A schema that is non-object, non-complex. + */ +public class AnySchema extends Schema { + /** + * Creates a new instance of the AnySchema class. + */ + public AnySchema() { + super(); + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + return super.toJson(jsonWriter); + } + + /** + * Deserializes an AnySchema instance from the JSON data. + * + * @param jsonReader The JSON reader to deserialize from. + * @return An AnySchema instance deserialized from the JSON data. + * @throws IOException If an error occurs during deserialization. + */ + public static AnySchema fromJson(JsonReader jsonReader) throws IOException { + return JsonUtils.readObject(jsonReader, AnySchema::new, (schema, fieldName, reader) -> { + if (!schema.tryConsumeParentProperties(schema, fieldName, reader)) { + reader.skipChildren(); + } + }); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/ApiVersion.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/ApiVersion.java new file mode 100644 index 0000000000..cb41ec17a5 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/ApiVersion.java @@ -0,0 +1,175 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.extension.model.codemodel; + +import com.microsoft.typespec.http.client.generator.core.extension.base.util.JsonUtils; +import com.azure.json.JsonReader; +import com.azure.json.JsonSerializable; +import com.azure.json.JsonWriter; + +import java.io.IOException; +import java.util.Objects; + +/** + * - since API version formats range from + * Azure ARM API date style (2018-01-01) to semver (1.2.3) + * and virtually any other text, this value tends to be an + * opaque string with the possibility of a modifier to indicate + * that it is a range. + *

+ * options: + * - prepend a dash or append a plus to indicate a range + * (ie, '2018-01-01+' or '-2019-01-01', or '1.0+' ) + *

+ * - semver-range style (ie, '^1.0.0' or '~1.0.0' ) + */ +public class ApiVersion implements JsonSerializable { + private String version; + private ApiVersion.Range range; + + /** + * Creates a new instance of the ApiVersion class. + */ + public ApiVersion() { + } + + /** + * Gets the API version string used in the API. (Required) + * + * @return The API version string used in the API. + */ + public String getVersion() { + return version; + } + + /** + * Sets the API version string used in the API. (Required) + * + * @param version The API version string used in the API. + */ + public void setVersion(String version) { + this.version = version; + } + + /** + * Gets the range of the API version. + * + * @return The range of the API version. + */ + public ApiVersion.Range getRange() { + return range; + } + + /** + * Sets the range of the API version. + * + * @param range The range of the API version. + */ + public void setRange(ApiVersion.Range range) { + this.range = range; + } + + @Override + public String toString() { + return ApiVersion.class.getName() + "@" + Integer.toHexString(System.identityHashCode(this)) + "[version=" + + Objects.toString(version, "") + ",range=" + Objects.toString(range, "") + ']'; + } + + @Override + public int hashCode() { + return Objects.hash(version, range); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof ApiVersion)) { + return false; + } + + ApiVersion rhs = ((ApiVersion) other); + return Objects.equals(version, rhs.version) && Objects.equals(range, rhs.range); + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + return jsonWriter.writeStartObject() + .writeStringField("version", version) + .writeStringField("range", range == null ? null : range.toString()) + .writeEndObject(); + } + + /** + * Deserializes an ApiVersion instance from the JSON data. + * + * @param jsonReader The JSON reader to deserialize from. + * @return An ApiVersion instance deserialized from the JSON data. + * @throws IOException If an error occurs during deserialization. + */ + public static ApiVersion fromJson(JsonReader jsonReader) throws IOException { + return JsonUtils.readObject(jsonReader, ApiVersion::new, (version, fieldName, reader) -> { + if ("version".equals(fieldName)) { + version.version = reader.getString(); + } else if ("range".equals(fieldName)) { + version.range = ApiVersion.Range.fromValue(reader.getString()); + } else { + reader.skipChildren(); + } + }); + } + + /** + * Represents the range of the API version. + */ + public enum Range { + /** + * Represents a range that is empty. + */ + __EMPTY__("+"), + + /** + * Represents a range that is empty. + */ + __EMPTY___("-"); + private final String value; + + Range(String value) { + this.value = value; + } + + @Override + public String toString() { + return this.value; + } + + /** + * Gets the value of the range. + * + * @return The value of the range. + */ + public String value() { + return this.value; + } + + /** + * Parses a string value into a range value. + * + * @param value The string value to parse. + * @return The parsed range value. + * @throws IllegalArgumentException thrown if the value does not match any of the known range values. + */ + public static ApiVersion.Range fromValue(String value) { + if ("+".equals(value)) { + return __EMPTY__; + } else if ("-".equals(value)) { + return __EMPTY___; + } else { + throw new IllegalArgumentException(value); + } + } + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/ArmIdSchema.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/ArmIdSchema.java new file mode 100644 index 0000000000..ad370d2cfc --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/ArmIdSchema.java @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.extension.model.codemodel; + +import com.microsoft.typespec.http.client.generator.core.extension.base.util.JsonUtils; +import com.azure.json.JsonReader; +import com.azure.json.JsonWriter; + +import java.io.IOException; + +/** + * Represents an ARM ID schema. + */ +public class ArmIdSchema extends PrimitiveSchema { + /** + * Creates a new instance of the ArmIdSchema class. + */ + public ArmIdSchema() { + super(); + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + return super.toJson(jsonWriter); + } + + /** + * Deserializes an ArmIdSchema instance from the JSON data. + * + * @param jsonReader The JSON reader to deserialize from. + * @return An ArmIdSchema instance deserialized from the JSON data. + * @throws IOException If an error occurs during deserialization. + */ + public static ArmIdSchema fromJson(JsonReader jsonReader) throws IOException { + return JsonUtils.readObject(jsonReader, ArmIdSchema::new, (schema, fieldName, reader) -> { + if (!schema.tryConsumeParentProperties(schema, fieldName, reader)) { + reader.skipChildren(); + } + }); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/ArraySchema.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/ArraySchema.java new file mode 100644 index 0000000000..8a04d1502a --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/ArraySchema.java @@ -0,0 +1,164 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.extension.model.codemodel; + +import com.microsoft.typespec.http.client.generator.core.extension.base.util.JsonUtils; +import com.azure.json.JsonReader; +import com.azure.json.JsonWriter; + +import java.io.IOException; +import java.util.Objects; + +/** + * Represents an array schema. + */ +public class ArraySchema extends ValueSchema { + private Schema elementType; + private double maxItems; + private double minItems; + private boolean uniqueItems; + + /** + * Creates a new instance of the ArraySchema class. + */ + public ArraySchema() { + super(); + } + + /** + * Gets the type of elements in the array. (Required) + * + * @return The type of elements in the array. + */ + public Schema getElementType() { + return elementType; + } + + /** + * Sets the type of elements in the array. (Required) + * + * @param elementType The type of elements in the array. + */ + public void setElementType(Schema elementType) { + this.elementType = elementType; + } + + /** + * Gets the maximum number of elements in the array. + * + * @return The maximum number of elements in the array. + */ + public double getMaxItems() { + return maxItems; + } + + /** + * Sets the maximum number of elements in the array. + * + * @param maxItems The maximum number of elements in the array. + */ + public void setMaxItems(double maxItems) { + this.maxItems = maxItems; + } + + /** + * Gets the minimum number of elements in the array. + * + * @return The minimum number of elements in the array. + */ + public double getMinItems() { + return minItems; + } + + /** + * Sets the minimum number of elements in the array. + * + * @param minItems The minimum number of elements in the array. + */ + public void setMinItems(double minItems) { + this.minItems = minItems; + } + + /** + * Gets whether the elements in the array should be unique. + * + * @return Whether the elements in the array should be unique. + */ + public boolean isUniqueItems() { + return uniqueItems; + } + + /** + * Sets whether the elements in the array should be unique. + * + * @param uniqueItems Whether the elements in the array should be unique. + */ + public void setUniqueItems(boolean uniqueItems) { + this.uniqueItems = uniqueItems; + } + + @Override + public String toString() { + return ArraySchema.class.getName() + '@' + Integer.toHexString(System.identityHashCode(this)) + "[elementType=" + + Objects.toString(elementType, "") + ",maxItems=" + maxItems + ",minItems=" + minItems + + ",uniqueItems=" + uniqueItems + ']'; + } + + @Override + public int hashCode() { + return Objects.hash(elementType, maxItems, minItems, uniqueItems); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof ArraySchema)) { + return false; + } + + ArraySchema rhs = ((ArraySchema) other); + return minItems == rhs.minItems && maxItems == rhs.maxItems && uniqueItems == rhs.uniqueItems + && Objects.equals(this.elementType, rhs.elementType); + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + return super.writeParentProperties(jsonWriter.writeStartObject()) + .writeJsonField("elementType", elementType) + .writeDoubleField("maxItems", maxItems) + .writeDoubleField("minItems", minItems) + .writeBooleanField("uniqueItems", uniqueItems) + .writeEndObject(); + } + + /** + * Deserializes an ArraySchema instance from the JSON data. + * + * @param jsonReader The JSON reader to deserialize from. + * @return An ArraySchema instance deserialized from the JSON data. + * @throws IOException If an error occurs during deserialization. + */ + public static ArraySchema fromJson(JsonReader jsonReader) throws IOException { + return JsonUtils.readObject(jsonReader, ArraySchema::new, (schema, fieldName, reader) -> { + if (schema.tryConsumeParentProperties(schema, fieldName, reader)) { + return; + } + + if ("elementType".equals(fieldName)) { + schema.elementType = Schema.fromJson(reader); + } else if ("maxItems".equals(fieldName)) { + schema.maxItems = reader.getDouble(); + } else if ("minItems".equals(fieldName)) { + schema.minItems = reader.getDouble(); + } else if ("uniqueItems".equals(fieldName)) { + schema.uniqueItems = reader.getBoolean(); + } else { + reader.skipChildren(); + } + }); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/BinarySchema.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/BinarySchema.java new file mode 100644 index 0000000000..ea2d4e251b --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/BinarySchema.java @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.extension.model.codemodel; + +import com.microsoft.typespec.http.client.generator.core.extension.base.util.JsonUtils; +import com.azure.json.JsonReader; +import com.azure.json.JsonWriter; + +import java.io.IOException; + +/** + * Represent a binary schema. + */ +public class BinarySchema extends Schema { + /** + * Create a new instance of the BinarySchema class. + */ + public BinarySchema() { + super(); + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + return super.toJson(jsonWriter); + } + + /** + * Deserializes a BinarySchema instance from the JSON data. + * + * @param jsonReader The JSON reader to deserialize from. + * @return A BinarySchema instance deserialized from the JSON data. + * @throws IOException If an error occurs during deserialization. + */ + public static BinarySchema fromJson(JsonReader jsonReader) throws IOException { + return JsonUtils.readObject(jsonReader, BinarySchema::new, (schema, fieldName, reader) -> { + if (!schema.tryConsumeParentProperties(schema, fieldName, reader)) { + reader.skipChildren(); + } + }); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/BooleanSchema.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/BooleanSchema.java new file mode 100644 index 0000000000..9eccb0f2d2 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/BooleanSchema.java @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.extension.model.codemodel; + +import com.microsoft.typespec.http.client.generator.core.extension.base.util.JsonUtils; +import com.azure.json.JsonReader; +import com.azure.json.JsonWriter; + +import java.io.IOException; + +/** + * Represents a boolean schema. + */ +public class BooleanSchema extends PrimitiveSchema { + + /** + * Creates a new instance of the BooleanSchema class. + */ + public BooleanSchema() { + super(); + } + + @Override + public String toString() { + return BooleanSchema.class.getName() + "@" + Integer.toHexString(System.identityHashCode(this)) + "[]"; + } + + @Override + public int hashCode() { + return 1; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + return other instanceof BooleanSchema; + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + return super.toJson(jsonWriter); + } + + /** + * Deserializes a BooleanSchema instance from the JSON data. + * + * @param jsonReader The JSON reader to deserialize from. + * @return A BooleanSchema instance deserialized from the JSON data. + * @throws IOException If an error occurs during deserialization. + */ + public static BooleanSchema fromJson(JsonReader jsonReader) throws IOException { + return JsonUtils.readObject(jsonReader, BooleanSchema::new, (schema, fieldName, reader) -> { + if (!schema.tryConsumeParentProperties(schema, fieldName, reader)) { + reader.skipChildren(); + } + }); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/ByteArraySchema.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/ByteArraySchema.java new file mode 100644 index 0000000000..f24d78abf5 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/ByteArraySchema.java @@ -0,0 +1,150 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.extension.model.codemodel; + +import com.microsoft.typespec.http.client.generator.core.extension.base.util.JsonUtils; +import com.azure.json.JsonReader; +import com.azure.json.JsonWriter; + +import java.io.IOException; +import java.util.Objects; + +/** + * Represents a byte array schema. + */ +public class ByteArraySchema extends PrimitiveSchema { + private ByteArraySchema.Format format; + + /** + * Creates a new instance of the ByteArraySchema class. + */ + public ByteArraySchema() { + super(); + } + + /** + * Gets the byte array format. (Required) + * + * @return The byte array format. + */ + public ByteArraySchema.Format getFormat() { + return format; + } + + /** + * Sets the byte array format. (Required) + * + * @param format The byte array format. + */ + public void setFormat(ByteArraySchema.Format format) { + this.format = format; + } + + @Override + public String toString() { + return ByteArraySchema.class.getName() + "@" + Integer.toHexString(System.identityHashCode(this)) + "[format=" + + Objects.toString(format, "") + "]"; + } + + @Override + public int hashCode() { + return Objects.hashCode(format); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof ByteArraySchema)) { + return false; + } + + ByteArraySchema rhs = ((ByteArraySchema) other); + return Objects.equals(format, rhs.format); + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + return super.writeParentProperties(jsonWriter.writeStartObject()) + .writeStringField("format", format == null ? null : format.toString()) + .writeEndObject(); + } + + /** + * Deserializes a ByteArraySchema instance from the JSON data. + * + * @param jsonReader The JSON reader to deserialize from. + * @return A ByteArraySchema instance deserialized from the JSON data. + * @throws IOException If an error occurs during deserialization. + */ + public static ByteArraySchema fromJson(JsonReader jsonReader) throws IOException { + return JsonUtils.readObject(jsonReader, ByteArraySchema::new, (schema, fieldName, reader) -> { + if (schema.tryConsumeParentProperties(schema, fieldName, reader)) { + return; + } + + if ("format".equals(fieldName)) { + schema.format = ByteArraySchema.Format.fromValue(reader.getString()); + } else { + reader.skipChildren(); + } + }); + } + + /** + * Represents the format of the byte array. + */ + public enum Format { + /** + * The byte array is encoded as a base64url string. + */ + BASE_64_URL("base64url"), + + /** + * The byte array is encoded as a byte string. + */ + BYTE("byte"); + + private final String value; + + Format(String value) { + this.value = value; + } + + @Override + public String toString() { + return this.value; + } + + /** + * Gets the value of the format. + * + * @return The value of the format. + */ + public String value() { + return this.value; + } + + /** + * Parses a string to a ByteArraySchema.Format. + * + * @param value The value to parse. + * @return The parsed ByteArraySchema.Format. + * @throws IllegalArgumentException If the value does not match a known ByteArraySchema.Format. + */ + public static ByteArraySchema.Format fromValue(String value) { + if ("base64url".equals(value)) { + return BASE_64_URL; + } else if ("byte".equals(value)) { + return BYTE; + } else { + throw new IllegalArgumentException(value); + } + } + + } + +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/CSharpLanguage.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/CSharpLanguage.java new file mode 100644 index 0000000000..3b595d83a4 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/CSharpLanguage.java @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.extension.model.codemodel; + +import com.microsoft.typespec.http.client.generator.core.extension.base.util.JsonUtils; +import com.azure.json.JsonReader; +import com.azure.json.JsonSerializable; +import com.azure.json.JsonWriter; + +import java.io.IOException; + +/** + * Represents the C# language. + */ +public class CSharpLanguage implements JsonSerializable { + + /** + * Creates a new instance of the CSharpLanguage class. + */ + public CSharpLanguage() { + } + + @Override + public String toString() { + return CSharpLanguage.class.getName() + "@" + Integer.toHexString(System.identityHashCode(this)) + "[]"; + } + + @Override + public int hashCode() { + return 1; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + return other instanceof CSharpLanguage; + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + return jsonWriter.writeStartObject().writeEndObject(); + } + + /** + * Deserializes a CSharpLanguage instance from the JSON data. + * + * @param jsonReader The JSON reader to deserialize from. + * @return A CSharpLanguage instance deserialized from the JSON data. + * @throws IOException If an error occurs during deserialization. + */ + public static CSharpLanguage fromJson(JsonReader jsonReader) throws IOException { + return JsonUtils.readEmptyObject(jsonReader, CSharpLanguage::new); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/CharSchema.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/CharSchema.java new file mode 100644 index 0000000000..f3b0fbcfd5 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/CharSchema.java @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.extension.model.codemodel; + +import com.microsoft.typespec.http.client.generator.core.extension.base.util.JsonUtils; +import com.azure.json.JsonReader; +import com.azure.json.JsonWriter; + +import java.io.IOException; + +/** + * Represents a char schema. + */ +public class CharSchema extends PrimitiveSchema { + + /** + * Creates a new instance of the CharSchema class. + */ + public CharSchema() { + super(); + } + + @Override + public String toString() { + return CharSchema.class.getName() + "@" + Integer.toHexString(System.identityHashCode(this)) + "[]"; + } + + @Override + public int hashCode() { + return 1; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + return other instanceof CharSchema; + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + return super.toJson(jsonWriter); + } + + /** + * Deserializes a CharSchema instance from the JSON data. + * + * @param jsonReader The JSON reader to deserialize from. + * @return A CharSchema instance deserialized from the JSON data. + * @throws IOException If an error occurs during deserialization. + */ + public static CharSchema fromJson(JsonReader jsonReader) throws IOException { + return JsonUtils.readObject(jsonReader, CharSchema::new, (schema, fieldName, reader) -> { + if (!schema.tryConsumeParentProperties(schema, fieldName, reader)) { + reader.skipChildren(); + } + }); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/ChoiceSchema.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/ChoiceSchema.java new file mode 100644 index 0000000000..5575a74732 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/ChoiceSchema.java @@ -0,0 +1,179 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.extension.model.codemodel; + +import com.microsoft.typespec.http.client.generator.core.extension.base.util.JsonUtils; +import com.azure.json.JsonReader; +import com.azure.json.JsonWriter; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + * Represents a choice schema. + */ +public class ChoiceSchema extends ValueSchema { + private Schema choiceType; + private List choices = new ArrayList<>(); + private String summary; + private String crossLanguageDefinitionId; + + /** + * Creates a new instance of the ChoiceSchema class. + */ + public ChoiceSchema() { + super(); + } + + /** + * Gets the type of the choice. (Required) + * + * @return The type of the choice. + */ + public Schema getChoiceType() { + return choiceType; + } + + /** + * Sets the type of the choice. (Required) + * + * @param choiceType The type of the choice. + */ + public void setChoiceType(Schema choiceType) { + this.choiceType = choiceType; + } + + /** + * Gets the possible choices. (Required) + * + * @return The possible choices. + */ + public List getChoices() { + return choices; + } + + /** + * Sets the possible choices. (Required) + * + * @param choices The possible choices. + */ + public void setChoices(List choices) { + this.choices = choices; + } + + @Override + public String getSummary() { + return summary; + } + + @Override + public void setSummary(String summary) { + this.summary = summary; + } + + /** + * Gets the cross-language definition id. + * + * @return The cross-language definition id. + */ + public String getCrossLanguageDefinitionId() { + return crossLanguageDefinitionId; + } + + /** + * Sets the cross-language definition id. + * + * @param crossLanguageDefinitionId The cross-language definition id. + */ + public void setCrossLanguageDefinitionId(String crossLanguageDefinitionId) { + this.crossLanguageDefinitionId = crossLanguageDefinitionId; + } + + @Override + public String toString() { + return sharedToString(this, ChoiceSchema.class.getName()); + } + + static String sharedToString(ChoiceSchema value, String className) { + return className + "@" + Integer.toHexString(System.identityHashCode(value)) + "[choiceType=" + + Objects.toString(value.choiceType, "") + ",choices=" + Objects.toString(value.choices, "") + + ']'; + } + + @Override + public int hashCode() { + return sharedHashCode(this); + } + + static int sharedHashCode(ChoiceSchema value) { + return Objects.hash(value.choiceType, value.choices, value.getLanguage().getJava().getName()); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + if (!(other instanceof ChoiceSchema)) { + return false; + } + + return sharedEquals(this, (ChoiceSchema) other); + } + + static boolean sharedEquals(ChoiceSchema lhs, ChoiceSchema rhs) { + return Objects.equals(lhs.choiceType, rhs.choiceType) && Objects.equals(lhs.choices, rhs.choices) + && Objects.equals(lhs.getLanguage().getJava().getName(), rhs.getLanguage().getJava().getName()); + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + return writeParentProperties(jsonWriter.writeStartObject()).writeEndObject(); + } + + JsonWriter writeParentProperties(JsonWriter jsonWriter) throws IOException { + return super.writeParentProperties(jsonWriter) + .writeJsonField("choiceType", choiceType) + .writeArrayField("choices", choices, JsonWriter::writeJson) + .writeStringField("summary", summary) + .writeStringField("crossLanguageDefinitionId", crossLanguageDefinitionId); + } + + /** + * Deserializes a ChoiceSchema instance from the JSON data. + * + * @param jsonReader The JSON reader to deserialize from. + * @return A ChoiceSchema instance deserialized from the JSON data. + * @throws IOException If an error occurs during deserialization. + */ + public static ChoiceSchema fromJson(JsonReader jsonReader) throws IOException { + return JsonUtils.readObject(jsonReader, ChoiceSchema::new, (schema, fieldName, reader) -> { + if (!schema.tryConsumeParentProperties(schema, fieldName, reader)) { + reader.skipChildren(); + } + }); + } + + boolean tryConsumeParentProperties(ChoiceSchema schema, String fieldName, JsonReader reader) throws IOException { + if (super.tryConsumeParentProperties(schema, fieldName, reader)) { + return true; + } else if ("choiceType".equals(fieldName)) { + schema.choiceType = Schema.fromJson(reader); + return true; + } else if ("choices".equals(fieldName)) { + schema.choices = reader.readArray(ChoiceValue::fromJson); + return true; + } else if ("summary".equals(fieldName)) { + schema.summary = reader.getString(); + return true; + } else if ("crossLanguageDefinitionId".equals(fieldName)) { + schema.crossLanguageDefinitionId = reader.getString(); + return true; + } + + return false; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/ChoiceValue.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/ChoiceValue.java new file mode 100644 index 0000000000..190b840445 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/ChoiceValue.java @@ -0,0 +1,137 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.extension.model.codemodel; + +import com.microsoft.typespec.http.client.generator.core.extension.base.util.JsonUtils; +import com.azure.json.JsonReader; +import com.azure.json.JsonSerializable; +import com.azure.json.JsonWriter; + +import java.io.IOException; +import java.util.Objects; + +/** + * Represents a choice value. + */ +public class ChoiceValue implements JsonSerializable { + private Languages language; + private String value; + private DictionaryAny extensions; + + /** + * Creates a new instance of the ChoiceValue class. + */ + public ChoiceValue() { + } + + /** + * Gets the language for the choice value. (Required) + * + * @return The language for the choice value. + */ + public Languages getLanguage() { + return language; + } + + /** + * Sets the language for the choice value. (Required) + * + * @param language The language for the choice value. + */ + public void setLanguage(Languages language) { + this.language = language; + } + + /** + * Gets the value of the choice. (Required) + * + * @return The value of the choice. + */ + public String getValue() { + return value; + } + + /** + * Sets the value of the choice. (Required) + * + * @param value The value of the choice. + */ + public void setValue(String value) { + this.value = value; + } + + /** + * Gets the extensions for the choice value. + * + * @return The extensions for the choice value. + */ + public DictionaryAny getExtensions() { + return extensions; + } + + /** + * Sets the extensions for the choice value. + * + * @param extensions The extensions for the choice value. + */ + public void setExtensions(DictionaryAny extensions) { + this.extensions = extensions; + } + + @Override + public String toString() { + return ChoiceValue.class.getName() + "@" + Integer.toHexString(System.identityHashCode(this)) + "[language=" + + Objects.toString(language, "") + ",value=" + Objects.toString(value, "") + ",extensions=" + + Objects.toString(extensions, "") + ']'; + } + + @Override + public int hashCode() { + return Objects.hash(value); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof ChoiceValue)) { + return false; + } + + ChoiceValue rhs = ((ChoiceValue) other); + return Objects.equals(this.value, rhs.value); + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + return jsonWriter.writeStartObject() + .writeJsonField("language", language) + .writeStringField("value", value) + .writeJsonField("extensions", extensions) + .writeEndObject(); + } + + /** + * Deserializes a ChoiceValue instance from the JSON data. + * + * @param jsonReader The JSON reader to deserialize from. + * @return A ChoiceValue instance deserialized from the JSON data. + * @throws IOException If an error occurs during deserialization. + */ + public static ChoiceValue fromJson(JsonReader jsonReader) throws IOException { + return JsonUtils.readObject(jsonReader, ChoiceValue::new, (value, fieldName, reader) -> { + if ("language".equals(fieldName)) { + value.language = Languages.fromJson(reader); + } else if ("value".equals(fieldName)) { + value.value = reader.getString(); + } else if ("extensions".equals(fieldName)) { + value.extensions = DictionaryAny.fromJson(reader); + } else { + reader.skipChildren(); + } + }); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/Client.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/Client.java new file mode 100644 index 0000000000..835e0da8bb --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/Client.java @@ -0,0 +1,218 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.extension.model.codemodel; + +import com.microsoft.typespec.http.client.generator.core.extension.base.util.JsonUtils; +import com.azure.json.JsonReader; +import com.azure.json.JsonWriter; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * Represents a client. + */ +public class Client extends Metadata { + private String summary; + private List operationGroups = new ArrayList<>(); + private List globalParameters = new ArrayList<>(); + private Security security; + private List apiVersions = new ArrayList<>(); + private ServiceVersion serviceVersion; + private String crossLanguageDefinitionId; + + /** + * Creates a new instance of the Client class. + */ + public Client() { + super(); + } + + /** + * Gets the summary of the client. + * + * @return The summary of the client. + */ + public String getSummary() { + return summary; + } + + /** + * Sets the summary of the client. + * + * @param summary The summary of the client. + */ + public void setSummary(String summary) { + this.summary = summary; + } + + /** + * Gets the operation groups of the client. + * + * @return The operation groups of the client. + */ + public List getOperationGroups() { + return operationGroups; + } + + /** + * Sets the operation groups of the client. + * + * @param operationGroups The operation groups of the client. + */ + public void setOperationGroups(List operationGroups) { + this.operationGroups = operationGroups; + } + + /** + * Gets the global parameters of the client. + * + * @return The global parameters of the client. + */ + public List getGlobalParameters() { + return globalParameters; + } + + /** + * Sets the global parameters of the client. + * + * @param globalParameters The global parameters of the client. + */ + public void setGlobalParameters(List globalParameters) { + this.globalParameters = globalParameters; + } + + /** + * Gets the security of the client. + * + * @return The security of the client. + */ + public Security getSecurity() { + return security; + } + + /** + * Sets the security of the client. + * + * @param security The security of the client. + */ + public void setSecurity(Security security) { + this.security = security; + } + + /** + * Gets the API versions of the client. + * + * @return The API versions of the client. + */ + public List getApiVersions() { + return apiVersions; + } + + /** + * Sets the API versions of the client. + * + * @param apiVersions The API versions of the client. + */ + public void setApiVersions(List apiVersions) { + this.apiVersions = apiVersions; + } + + /** + * Gets the service version of the client. + * + * @return The service version of the client. + */ + public ServiceVersion getServiceVersion() { + return serviceVersion; + } + + /** + * Sets the service version of the client. + * + * @param serviceVersion The service version of the client. + */ + public void setServiceVersion(ServiceVersion serviceVersion) { + this.serviceVersion = serviceVersion; + } + + /** + * Gets the cross-language definition id of the client. + * + * @return The cross-language definition id of the client. + */ + public String getCrossLanguageDefinitionId() { + return crossLanguageDefinitionId; + } + + /** + * Sets the cross-language definition id of the client. + * + * @param crossLanguageDefinitionId The cross-language definition id of the client. + */ + public void setCrossLanguageDefinitionId(String crossLanguageDefinitionId) { + this.crossLanguageDefinitionId = crossLanguageDefinitionId; + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + return writeParentProperties(jsonWriter.writeStartObject()).writeEndObject(); + } + + JsonWriter writeParentProperties(JsonWriter jsonWriter) throws IOException { + return super.writeParentProperties(jsonWriter) + .writeStringField("summary", summary) + .writeArrayField("operationGroups", operationGroups, JsonWriter::writeJson) + .writeArrayField("globalParameters", globalParameters, JsonWriter::writeJson) + .writeJsonField("security", security) + .writeArrayField("apiVersions", apiVersions, JsonWriter::writeJson) + .writeJsonField("serviceVersion", serviceVersion) + .writeStringField("crossLanguageDefinitionId", crossLanguageDefinitionId); + } + + /** + * Deserializes a Client instance from the JSON data. + * + * @param jsonReader The JSON reader to deserialize from. + * @return A Client instance deserialized from the JSON data. + * @throws IOException If an error occurs during deserialization. + */ + public static Client fromJson(JsonReader jsonReader) throws IOException { + return JsonUtils.readObject(jsonReader, Client::new, (client, fieldName, reader) -> { + if (!client.tryConsumeParentProperties(client, fieldName, reader)) { + reader.skipChildren(); + } + }); + } + + boolean tryConsumeParentProperties(Client client, String fieldName, JsonReader reader) throws IOException { + if (super.tryConsumeParentProperties(client, fieldName, reader)) { + return true; + } else if ("summary".equals(fieldName)) { + client.summary = reader.getString(); + return true; + } else if ("operationGroups".equals(fieldName)) { + client.operationGroups = reader.readArray(OperationGroup::fromJson); + return true; + } else if ("globalParameters".equals(fieldName)) { + client.globalParameters = reader.readArray(Parameter::fromJson); + return true; + } else if ("security".equals(fieldName)) { + client.security = Security.fromJson(reader); + return true; + } else if ("apiVersions".equals(fieldName)) { + client.apiVersions = reader.readArray(ApiVersion::fromJson); + return true; + } else if ("serviceVersion".equals(fieldName)) { + client.serviceVersion = ServiceVersion.fromJson(reader); + return true; + } else if ("crossLanguageDefinitionId".equals(fieldName)) { + client.crossLanguageDefinitionId = reader.getString(); + return true; + } + + return false; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/CodeModel.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/CodeModel.java new file mode 100644 index 0000000000..4bdb671caf --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/CodeModel.java @@ -0,0 +1,140 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.extension.model.codemodel; + +import com.microsoft.typespec.http.client.generator.core.extension.base.util.JsonUtils; +import com.azure.json.JsonReader; +import com.azure.json.JsonWriter; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * Represents a code model. + *

+ * A code model contains all the information required to generate a service API. + */ +public class CodeModel extends Client { + private Info info; + private Schemas schemas; + private List clients = new ArrayList<>(); + private TestModel testModel; + + /** + * Creates a new instance of the CodeModel class. + */ + public CodeModel() { + super(); + } + + /** + * Gets the code model information. (Required) + * + * @return The code model information. + */ + public Info getInfo() { + return info; + } + + /** + * Sets the code model information. (Required) + * + * @param info The code model information. + */ + public void setInfo(Info info) { + this.info = info; + } + + /** + * Gets the full set of schemas for a given service, categorized into convenient collections. (Required) + * + * @return The full set of schemas for a given service, categorized into convenient collections. + */ + public Schemas getSchemas() { + return schemas; + } + + /** + * Sets the full set of schemas for a given service, categorized into convenient collections. (Required) + * + * @param schemas The full set of schemas for a given service, categorized into convenient collections. + */ + public void setSchemas(Schemas schemas) { + this.schemas = schemas; + } + + /** + * Gets the clients of the code model. + * + * @return The clients of the code model. + */ + public List getClients() { + return clients; + } + + /** + * Sets the clients of the code model. + * + * @param clients The clients of the code model. + */ + public void setClients(List clients) { + this.clients = clients; + } + + /** + * Gets the test model definition. + * + * @return The test model definition. + */ + public TestModel getTestModel() { + return testModel; + } + + /** + * Sets the test model definition. + * + * @param testModel The test model definition. + */ + public void setTestModel(TestModel testModel) { + this.testModel = testModel; + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + return super.writeParentProperties(jsonWriter.writeStartObject()) + .writeJsonField("info", info) + .writeJsonField("schemas", schemas) + .writeArrayField("clients", clients, JsonWriter::writeJson) + .writeJsonField("testModel", testModel) + .writeEndObject(); + } + + /** + * Deserializes a CodeModel instance from the JSON data. + * + * @param jsonReader The JSON reader to deserialize from. + * @return A CodeModel instance deserialized from the JSON data. + * @throws IOException If an error occurs during deserialization. + */ + public static CodeModel fromJson(JsonReader jsonReader) throws IOException { + return JsonUtils.readObject(jsonReader, CodeModel::new, (codeModel, fieldName, reader) -> { + if (codeModel.tryConsumeParentProperties(codeModel, fieldName, reader)) { + return; + } + + if ("info".equals(fieldName)) { + codeModel.info = Info.fromJson(reader); + } else if ("schemas".equals(fieldName)) { + codeModel.schemas = Schemas.fromJson(reader); + } else if ("clients".equals(fieldName)) { + codeModel.clients = reader.readArray(Client::fromJson); + } else if ("testModel".equals(fieldName)) { + codeModel.testModel = TestModel.fromJson(reader); + } else { + reader.skipChildren(); + } + }); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/CodeModelCustomConstructor.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/CodeModelCustomConstructor.java new file mode 100644 index 0000000000..4fc184c3fd --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/CodeModelCustomConstructor.java @@ -0,0 +1,413 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.extension.model.codemodel; + +import com.microsoft.typespec.http.client.generator.core.extension.model.extensionmodel.XmsExamples; +import com.microsoft.typespec.http.client.generator.core.extension.model.extensionmodel.XmsExamples; +import org.yaml.snakeyaml.LoaderOptions; +import org.yaml.snakeyaml.constructor.Constructor; +import org.yaml.snakeyaml.nodes.MappingNode; +import org.yaml.snakeyaml.nodes.Node; +import org.yaml.snakeyaml.nodes.NodeId; +import org.yaml.snakeyaml.nodes.NodeTuple; +import org.yaml.snakeyaml.nodes.ScalarNode; +import org.yaml.snakeyaml.nodes.SequenceNode; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Custom constructor for a CodeModel. + */ +public class CodeModelCustomConstructor extends Constructor { + /** + * Creates a new instance of the CodeModelCustomConstructor class. + * + * @param loaderOptions The options for the loader. + */ + public CodeModelCustomConstructor(LoaderOptions loaderOptions) { + super(loaderOptions); + yamlClassConstructors.put(NodeId.scalar, new TypeEnumConstruct()); + yamlClassConstructors.put(NodeId.mapping, new TypeMapConstruct()); + } + + class TypeEnumConstruct extends Constructor.ConstructScalar { + @Override + public Object construct(Node node) { + Class type = node.getType(); + if (type.equals(Schema.AllSchemaTypes.class)) { + return Schema.AllSchemaTypes.fromValue(((ScalarNode) node).getValue()); + }/* else if (type.equals(ChoiceSchema.Type.class)) { + return ChoiceSchema.Type.fromValue(((ScalarNode) node).getValue()); + } else if (type.equals(SealedChoiceSchema.Type.class)) { + return SealedChoiceSchema.Type.fromValue(((ScalarNode) node).getValue()); + }*/ else if (type.equals(Parameter.ImplementationLocation.class)) { + return Parameter.ImplementationLocation.fromValue(((ScalarNode) node).getValue()); + } else if (type.equals(DateTimeSchema.Format.class)) { + return DateTimeSchema.Format.fromValue(((ScalarNode) node).getValue()); + } else if (type.equals(ByteArraySchema.Format.class)) { + return ByteArraySchema.Format.fromValue(((ScalarNode) node).getValue()); + } else if (type.equals(RequestParameterLocation.class)) { + return RequestParameterLocation.fromValue(((ScalarNode) node).getValue()); + } else if (type.equals(SerializationStyle.class)) { + return SerializationStyle.fromValue(((ScalarNode) node).getValue()); + } else if (type.equals(KnownMediaType.class)) { + return KnownMediaType.fromValue(((ScalarNode) node).getValue()); + } else if (type.equals(Scheme.SecuritySchemeType.class)) { + return Scheme.SecuritySchemeType.fromValue(((ScalarNode) node).getValue()); + } else if (type.equals(TestScenarioStepType.class)) { + return TestScenarioStepType.fromValue(((ScalarNode) node).getValue()); + } else if (type.equals(ScenarioTestScope.class)) { + return ScenarioTestScope.fromValue(((ScalarNode) node).getValue()); + } else if (type.equals(SchemaContext.class)) { + return SchemaContext.fromValue(((ScalarNode) node).getValue()); + } else if (type.equals(DurationSchema.Format.class)) { + return DurationSchema.Format.fromValue(((ScalarNode) node).getValue()); + } else { + // create JavaBean + return super.construct(node); + } + } + } + + class TypeMapConstruct extends Constructor.ConstructMapping { + @Override + public Object construct(Node node) { + MappingNode mappingNode = (MappingNode) node; + for (NodeTuple tuple : mappingNode.getValue()) { + ScalarNode key = (ScalarNode) tuple.getKeyNode(); + switch (key.getValue()) { + case "arrays": { + SequenceNode value = (SequenceNode) tuple.getValueNode(); + value.setListType(ArraySchema.class); + break; + } + case "ands": { + SequenceNode value = (SequenceNode) tuple.getValueNode(); + value.setListType(AndSchema.class); + break; + } + case "ors": { + SequenceNode value = (SequenceNode) tuple.getValueNode(); + value.setListType(OrSchema.class); + break; + } + case "xors": { + SequenceNode value = (SequenceNode) tuple.getValueNode(); + value.setListType(XorSchema.class); + break; + } + case "objects": { + SequenceNode value = (SequenceNode) tuple.getValueNode(); + value.setListType(ObjectSchema.class); + break; + } + case "choices": { + SequenceNode value = (SequenceNode) tuple.getValueNode(); + value.setListType(ChoiceSchema.class); + break; + } + case "parameterGroups": { + SequenceNode value = (SequenceNode) tuple.getValueNode(); + value.setListType(ParameterGroupSchema.class); + break; + } + case "sealedChoices": { + SequenceNode value = (SequenceNode) tuple.getValueNode(); + value.setListType(SealedChoiceSchema.class); + break; + } + case "flags": { + SequenceNode value = (SequenceNode) tuple.getValueNode(); + value.setListType(FlagSchema.class); + break; + } + case "dictionaries": { + SequenceNode value = (SequenceNode) tuple.getValueNode(); + value.setListType(DictionarySchema.class); + break; + } + case "constants": { + SequenceNode value = (SequenceNode) tuple.getValueNode(); + value.setListType(ConstantSchema.class); + break; + } + case "primitives": { + SequenceNode value = (SequenceNode) tuple.getValueNode(); + value.setListType(Object.class); + break; + } + case "properties": { + SequenceNode value = (SequenceNode) tuple.getValueNode(); + value.setListType(Property.class); + break; + } + case "binaries": { + SequenceNode value = (SequenceNode) tuple.getValueNode(); + value.setListType(BinarySchema.class); + break; + } + case "booleans": { + SequenceNode value = (SequenceNode) tuple.getValueNode(); + value.setListType(BooleanSchema.class); + break; + } + case "bytearrays": { + SequenceNode value = (SequenceNode) tuple.getValueNode(); + value.setListType(ByteArraySchema.class); + break; + } + case "numbers": { + SequenceNode value = (SequenceNode) tuple.getValueNode(); + value.setListType(NumberSchema.class); + break; + } + case "uris": { + SequenceNode value = (SequenceNode) tuple.getValueNode(); + value.setListType(UriSchema.class); + break; + } + case "anyObjects": + case "any": { + SequenceNode value = (SequenceNode) tuple.getValueNode(); + value.setListType(AnySchema.class); + break; + } + case "times": { + SequenceNode value = (SequenceNode) tuple.getValueNode(); + value.setListType(TimeSchema.class); + break; + } + case "armIds": { + SequenceNode value = (SequenceNode) tuple.getValueNode(); + value.setListType(ArmIdSchema.class); + break; + } + case "requests": { + SequenceNode value = (SequenceNode) tuple.getValueNode(); + value.setListType(Request.class); + break; + } + case "exceptions": + case "responses": { + SequenceNode value = (SequenceNode) tuple.getValueNode(); + value.setListType(Response.class); + break; + } + case "immediate": + case "all": + if (tuple.getValueNode() instanceof SequenceNode) { + SequenceNode value = (SequenceNode) tuple.getValueNode(); + for (Node item : value.getValue()) { + item.setType(getSchemaTypeFromMappingNode((MappingNode) item)); + } + break; + } else if (tuple.getValueNode() instanceof MappingNode) { + MappingNode value = (MappingNode) tuple.getValueNode(); + for (NodeTuple item : value.getValue()) { + item.getValueNode() + .setType(getSchemaTypeFromMappingNode((MappingNode) (item.getValueNode()))); + } + break; + } + case "allOf": { + SequenceNode value = (SequenceNode) tuple.getValueNode(); + for (Node item : value.getValue()) { + item.setType(getSchemaTypeFromMappingNode((MappingNode) item)); + } + break; + } + case "choiceType": + case "elementType": + case "valueType": + case "schema": { + MappingNode value = (MappingNode) tuple.getValueNode(); + value.setType(getSchemaTypeFromMappingNode(value)); + break; + } + case "extensions": { + MappingNode value = (MappingNode) tuple.getValueNode(); + List actualValues = new ArrayList<>(); + for (NodeTuple extension : value.getValue()) { + ScalarNode keyNode = (ScalarNode) extension.getKeyNode(); + if ("x-ms-pageable".equals(keyNode.getValue())) { + actualValues.add(new NodeTuple( + new ScalarNode(keyNode.getTag(), "xmsPageable", keyNode.getStartMark(), + keyNode.getEndMark(), keyNode.getScalarStyle()), extension.getValueNode())); + } else if ("x-ms-skip-url-encoding".equals(keyNode.getValue())) { + actualValues.add(new NodeTuple( + new ScalarNode(keyNode.getTag(), "xmsSkipUrlEncoding", keyNode.getStartMark(), + keyNode.getEndMark(), keyNode.getScalarStyle()), extension.getValueNode())); + } else if ("x-ms-client-flatten".equals(keyNode.getValue())) { + actualValues.add(new NodeTuple( + new ScalarNode(keyNode.getTag(), "xmsClientFlatten", keyNode.getStartMark(), + keyNode.getEndMark(), keyNode.getScalarStyle()), extension.getValueNode())); + } else if ("x-ms-long-running-operation".equals(keyNode.getValue())) { + actualValues.add(new NodeTuple( + new ScalarNode(keyNode.getTag(), "xmsLongRunningOperation", keyNode.getStartMark(), + keyNode.getEndMark(), keyNode.getScalarStyle()), extension.getValueNode())); + } else if ("x-ms-flattened".equals(keyNode.getValue())) { + actualValues.add(new NodeTuple( + new ScalarNode(keyNode.getTag(), "xmsFlattened", keyNode.getStartMark(), + keyNode.getEndMark(), keyNode.getScalarStyle()), extension.getValueNode())); + } else if ("x-ms-azure-resource".equals(keyNode.getValue())) { + actualValues.add(new NodeTuple( + new ScalarNode(keyNode.getTag(), "xmsAzureResource", keyNode.getStartMark(), + keyNode.getEndMark(), keyNode.getScalarStyle()), extension.getValueNode())); + } else if ("x-ms-mutability".equals(keyNode.getValue())) { + actualValues.add(new NodeTuple( + new ScalarNode(keyNode.getTag(), "xmsMutability", keyNode.getStartMark(), + keyNode.getEndMark(), keyNode.getScalarStyle()), extension.getValueNode())); + } else if ("x-ms-header-collection-prefix".equals(keyNode.getValue())) { + actualValues.add(new NodeTuple( + new ScalarNode(keyNode.getTag(), "xmsHeaderCollectionPrefix", + keyNode.getStartMark(), keyNode.getEndMark(), keyNode.getScalarStyle()), + extension.getValueNode())); + } else if ("x-internal-autorest-anonymous-schema".equals(keyNode.getValue())) { + actualValues.add(new NodeTuple( + new ScalarNode(keyNode.getTag(), "xmsInternalAutorestAnonymousSchema", + keyNode.getStartMark(), keyNode.getEndMark(), keyNode.getScalarStyle()), + extension.getValueNode())); + } else if ("x-ms-long-running-operation-options".equals(keyNode.getValue())) { + actualValues.add(new NodeTuple( + new ScalarNode(keyNode.getTag(), "xmsLongRunningOperationOptions", + keyNode.getStartMark(), keyNode.getEndMark(), keyNode.getScalarStyle()), + extension.getValueNode())); + } else if ("x-ms-examples".equals(keyNode.getValue())) { + actualValues.add(new NodeTuple( + new ScalarNode(keyNode.getTag(), "xmsExamples", keyNode.getStartMark(), + keyNode.getEndMark(), keyNode.getScalarStyle()), extension.getValueNode())); + } else if ("x-ms-arm-id-details".equals(keyNode.getValue())) { + actualValues.add(new NodeTuple( + new ScalarNode(keyNode.getTag(), "xmsArmIdDetails", keyNode.getStartMark(), + keyNode.getEndMark(), keyNode.getScalarStyle()), extension.getValueNode())); + } else if ("x-ms-secret".equals(keyNode.getValue())) { + actualValues.add(new NodeTuple( + new ScalarNode(keyNode.getTag(), "xmsSecret", keyNode.getStartMark(), + keyNode.getEndMark(), keyNode.getScalarStyle()), extension.getValueNode())); + } else if ("x-ms-versioning-added".equals(keyNode.getValue())) { + actualValues.add(new NodeTuple( + new ScalarNode(keyNode.getTag(), "xmsVersioningAdded", keyNode.getStartMark(), + keyNode.getEndMark(), keyNode.getScalarStyle()), extension.getValueNode())); + } else { + // handle properties that do not contain hyphen in name + actualValues.add(new NodeTuple(keyNode, extension.getValueNode())); + } + } + value.setValue(actualValues); + break; + } + case "xmsLongRunningOperationOptions": { + MappingNode value = (MappingNode) tuple.getValueNode(); + List actualValues = new ArrayList<>(); + for (NodeTuple extension : value.getValue()) { + ScalarNode keyNode = (ScalarNode) extension.getKeyNode(); + if ("final-state-via".equals(keyNode.getValue())) { + actualValues.add(new NodeTuple( + new ScalarNode(keyNode.getTag(), "finalStateVia", keyNode.getStartMark(), + keyNode.getEndMark(), keyNode.getScalarStyle()), extension.getValueNode())); + } + } + value.setValue(actualValues); + break; + } + } + } + return super.construct(mappingNode); + } + + @Override + protected Object constructJavaBean2ndStep(MappingNode node, Object object) { + if (node.getType().equals(XmsExamples.class)) { + // deserialize to Map, while Object would be LinkedHashMap + Map examples = new HashMap<>(); + for (NodeTuple tuple : node.getValue()) { + examples.put(((ScalarNode) tuple.getKeyNode()).getValue(), constructObject(tuple.getValueNode())); + } + XmsExamples xmsExamples = new XmsExamples(); + xmsExamples.setExamples(examples); + return xmsExamples; + } else { + return super.constructJavaBean2ndStep(node, object); + } + } + } + + private static Class getSchemaTypeFromMappingNode(MappingNode value) { + for (NodeTuple schemaProps : value.getValue()) { + if (((ScalarNode) schemaProps.getKeyNode()).getValue().equals("type")) { + switch (((ScalarNode) schemaProps.getValueNode()).getValue()) { + case "any-object": + case "any": + return AnySchema.class; + case "and": + return AndSchema.class; + case "array": + return ArraySchema.class; + case "boolean": + return BooleanSchema.class; + case "binary": + return BinarySchema.class; + case "byte-array": + return ByteArraySchema.class; + case "char": + return CharSchema.class; + case "choice": + return ChoiceSchema.class; + case "constant": + return ConstantSchema.class; + case "credential": + return CredentialSchema.class; + case "date": + return DateSchema.class; + case "date-time": + return DateTimeSchema.class; + case "dictionary": + return DictionarySchema.class; + case "duration": + return DurationSchema.class; + case "flag": + return FlagSchema.class; + case "group": + return ObjectSchema.class; + case "integer": + return NumberSchema.class; + case "not": + return NotSchema.class; + case "number": + return NumberSchema.class; + case "object": + return ObjectSchema.class; + case "odata-query": + return ODataQuerySchema.class; + case "or": + return OrSchema.class; + case "parameter-group": + return ParameterGroupSchema.class; + case "sealed-choice": + return SealedChoiceSchema.class; + case "string": + return StringSchema.class; + case "time": + return TimeSchema.class; + case "unixtime": + return UnixTimeSchema.class; + case "uri": + return UriSchema.class; + case "uuid": + return UuidSchema.class; + case "xor": + return XorSchema.class; + case "arm-id": + return ArmIdSchema.class; + default: + return Schema.class; + } + } + } + return Schema.class; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/ComplexSchema.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/ComplexSchema.java new file mode 100644 index 0000000000..a9fdd60ceb --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/ComplexSchema.java @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.extension.model.codemodel; + +import com.microsoft.typespec.http.client.generator.core.extension.base.util.JsonUtils; +import com.azure.json.JsonReader; +import com.azure.json.JsonWriter; + +import java.io.IOException; + +/** + * Represents a complex schema (types that can be an object). + */ +public class ComplexSchema extends Schema { + /** + * Creates a new instance of the ComplexSchema class. + */ + public ComplexSchema() { + super(); + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + return super.toJson(jsonWriter); + } + + /** + * Deserializes a ComplexSchema instance from the JSON data. + * + * @param jsonReader The JSON reader to deserialize from. + * @return A ComplexSchema instance deserialized from the JSON data. + * @throws IOException If an error occurs during deserialization. + */ + public static ComplexSchema fromJson(JsonReader jsonReader) throws IOException { + return JsonUtils.readObject(jsonReader, ComplexSchema::new, (schema, fieldName, reader) -> { + if (!schema.tryConsumeParentProperties(schema, fieldName, reader)) { + reader.skipChildren(); + } + }); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/ConstantSchema.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/ConstantSchema.java new file mode 100644 index 0000000000..2f0414677d --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/ConstantSchema.java @@ -0,0 +1,88 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.extension.model.codemodel; + +import com.microsoft.typespec.http.client.generator.core.extension.base.util.JsonUtils; +import com.azure.json.JsonReader; +import com.azure.json.JsonWriter; + +import java.io.IOException; + +/** + * Represents a constant schema. + */ +public class ConstantSchema extends Schema { + private Schema valueType; + private ConstantValue value; + + /** + * Creates a new instance of the ConstantSchema class. + */ + public ConstantSchema() { + super(); + } + + /** + * Gets the value type. (Required) + * + * @return The value type. + */ + public Schema getValueType() { + return valueType; + } + + /** + * Sets the value type. (Required) + * + * @param valueType The value type. + */ + public void setValueType(Schema valueType) { + this.valueType = valueType; + } + + /** + * Gets the actual constant value. (Required) + * + * @return The actual constant value. + */ + public ConstantValue getValue() { + return value; + } + + /** + * Sets the actual constant value. (Required) + * + * @param value The actual constant value. + */ + public void setValue(ConstantValue value) { + this.value = value; + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + return jsonWriter.writeStartObject() + .writeJsonField("valueType", valueType) + .writeJsonField("value", value) + .writeEndObject(); + } + + /** + * Deserializes a ConstantSchema instance from the JSON data. + * + * @param jsonReader The JSON reader to deserialize from. + * @return A ConstantSchema instance deserialized from the JSON data. + * @throws IOException If an error occurs during deserialization. + */ + public static ConstantSchema fromJson(JsonReader jsonReader) throws IOException { + return JsonUtils.readObject(jsonReader, ConstantSchema::new, (schema, fieldName, reader) -> { + if ("valueType".equals(fieldName)) { + schema.valueType = Schema.fromJson(reader); + } else if ("value".equals(fieldName)) { + schema.value = ConstantValue.fromJson(reader); + } else { + reader.skipChildren(); + } + }); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/ConstantValue.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/ConstantValue.java new file mode 100644 index 0000000000..629a08ab9b --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/ConstantValue.java @@ -0,0 +1,137 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.extension.model.codemodel; + +import com.microsoft.typespec.http.client.generator.core.extension.base.util.JsonUtils; +import com.azure.json.JsonReader; +import com.azure.json.JsonSerializable; +import com.azure.json.JsonWriter; + +import java.io.IOException; +import java.util.Objects; + +/** + * Represents a constant value. + */ +public class ConstantValue implements JsonSerializable { + private Languages language; + private Object value; + private DictionaryAny extensions; + + /** + * Creates a new instance of the ConstantValue class. + */ + public ConstantValue() { + } + + /** + * Gets the language of the value. (Required) + * + * @return The language of the value. + */ + public Languages getLanguage() { + return language; + } + + /** + * Sets the language of the value. (Required) + * + * @param language The language of the value. + */ + public void setLanguage(Languages language) { + this.language = language; + } + + /** + * Gets the actual constant value to use. (Required) + * + * @return The actual constant value to use. + */ + public Object getValue() { + return value; + } + + /** + * Sets the actual constant value to use. (Required) + * + * @param value The actual constant value to use. + */ + public void setValue(Object value) { + this.value = value; + } + + /** + * Gets the custom extensible metadata for individual language generators. + * + * @return The custom extensible metadata for individual language generators. + */ + public DictionaryAny getExtensions() { + return extensions; + } + + /** + * Sets the custom extensible metadata for individual language generators. + * + * @param extensions The custom extensible metadata for individual language generators. + */ + public void setExtensions(DictionaryAny extensions) { + this.extensions = extensions; + } + + @Override + public String toString() { + return ConstantValue.class.getName() + "@" + Integer.toHexString(System.identityHashCode(this)) + "[language=" + + language + ", value=" + value + ", extensions=" + extensions + "]"; + } + + @Override + public int hashCode() { + return Objects.hash(language, extensions, value); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof ConstantValue)) { + return false; + } + + ConstantValue rhs = ((ConstantValue) other); + return Objects.equals(language, rhs.language) && Objects.equals(extensions, rhs.extensions) + && Objects.equals(value, rhs.value); + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + return jsonWriter.writeStartObject() + .writeJsonField("language", language) + .writeUntypedField("value", value) + .writeJsonField("extensions", extensions) + .writeEndObject(); + } + + /** + * Deserializes a ConstantValue instance from the JSON data. + * + * @param jsonReader The JSON reader to deserialize from. + * @return A ConstantValue instance deserialized from the JSON data. + * @throws IOException If an error occurs during deserialization. + */ + public static ConstantValue fromJson(JsonReader jsonReader) throws IOException { + return JsonUtils.readObject(jsonReader, ConstantValue::new, (value, fieldName, reader) -> { + if ("language".equals(fieldName)) { + value.language = Languages.fromJson(reader); + } else if ("value".equals(fieldName)) { + value.value = reader.readUntyped(); + } else if ("extensions".equals(fieldName)) { + value.extensions = DictionaryAny.fromJson(reader); + } else { + reader.skipChildren(); + } + }); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/Contact.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/Contact.java new file mode 100644 index 0000000000..c869bc1022 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/Contact.java @@ -0,0 +1,159 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.extension.model.codemodel; + +import com.microsoft.typespec.http.client.generator.core.extension.base.util.JsonUtils; +import com.azure.json.JsonReader; +import com.azure.json.JsonSerializable; +import com.azure.json.JsonWriter; + +import java.io.IOException; +import java.util.Objects; + +/** + * Represents a contact. + */ +public class Contact implements JsonSerializable { + private String name; + private String url; + private String email; + private DictionaryAny extensions; + + /** + * Creates a new instance of the Contact class. + */ + public Contact() { + } + + /** + * Gets the name of the contact. + * + * @return The name of the contact. + */ + public String getName() { + return name; + } + + /** + * Sets the name of the contact. + * + * @param name The name of the contact. + */ + public void setName(String name) { + this.name = name; + } + + /** + * Gets the URL of the contact. + * + * @return The URL of the contact. + */ + public String getUrl() { + return url; + } + + /** + * Sets the URL of the contact. + * + * @param url The URL of the contact. + */ + public void setUrl(String url) { + this.url = url; + } + + /** + * Gets the email of the contact. + * + * @return The email of the contact. + */ + public String getEmail() { + return email; + } + + /** + * Sets the email of the contact. + * + * @param email The email of the contact. + */ + public void setEmail(String email) { + this.email = email; + } + + /** + * Gets the custom extensible metadata for individual language generators. + * + * @return The custom extensible metadata for individual language generators. + */ + public DictionaryAny getExtensions() { + return extensions; + } + + /** + * Sets the custom extensible metadata for individual language generators. + * + * @param extensions The custom extensible metadata for individual language generators. + */ + public void setExtensions(DictionaryAny extensions) { + this.extensions = extensions; + } + + @Override + public String toString() { + return Contact.class.getName() + "@" + Integer.toHexString(System.identityHashCode(this)) + "[name=" + name + + ", url=" + url + ", email=" + email + ", extensions=" + extensions + "]"; + } + + @Override + public int hashCode() { + return Objects.hash(name, extensions, url, email); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof Contact)) { + return false; + } + + Contact rhs = ((Contact) other); + return Objects.equals(name, rhs.name) && Objects.equals(extensions, rhs.extensions) + && Objects.equals(url, rhs.url) && Objects.equals(email, rhs.email); + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + return jsonWriter.writeStartObject() + .writeStringField("name", name) + .writeStringField("url", url) + .writeStringField("email", email) + .writeJsonField("extensions", extensions) + .writeEndObject(); + } + + /** + * Deserializes a Constant instance from the JSON data. + * + * @param jsonReader The JSON reader to deserialize from. + * @return A Constant instance deserialized from the JSON data. + * @throws IOException If an error occurs during deserialization. + */ + public static Contact fromJson(JsonReader jsonReader) throws IOException { + return JsonUtils.readObject(jsonReader, Contact::new, (contact, fieldName, reader) -> { + if ("name".equals(fieldName)) { + contact.name = reader.getString(); + } else if ("url".equals(fieldName)) { + contact.url = reader.getString(); + } else if ("email".equals(fieldName)) { + contact.email = reader.getString(); + } else if ("extensions".equals(fieldName)) { + contact.extensions = DictionaryAny.fromJson(reader); + } else { + reader.skipChildren(); + } + }); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/ConvenienceApi.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/ConvenienceApi.java new file mode 100644 index 0000000000..c0d14d7c8e --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/ConvenienceApi.java @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.extension.model.codemodel; + +import com.microsoft.typespec.http.client.generator.core.extension.base.util.JsonUtils; +import com.azure.json.JsonReader; +import com.azure.json.JsonWriter; + +import java.io.IOException; +import java.util.List; + +/** + * Represents a convenience API. + */ +public class ConvenienceApi extends Metadata { + private List requests; + + /** + * Creates a new instance of the ConvenienceApi class. + */ + public ConvenienceApi() { + super(); + } + + /** + * Gets the requests of the convenience API. + * + * @return The requests of the convenience API. + */ + public List getRequests() { + return requests; + } + + /** + * Sets the requests of the convenience API. + * + * @param requests The requests of the convenience API. + */ + public void setRequests(List requests) { + this.requests = requests; + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + return super.writeParentProperties(jsonWriter.writeStartObject()) + .writeArrayField("requests", requests, JsonWriter::writeJson) + .writeEndObject(); + } + + /** + * Deserializes a ConvenienceApi instance from the JSON data. + * + * @param jsonReader The JSON reader to deserialize from. + * @return A ConvenienceApi instance deserialized from the JSON data. + * @throws IOException If an error occurs during deserialization. + */ + public static ConvenienceApi fromJson(JsonReader jsonReader) throws IOException { + return JsonUtils.readObject(jsonReader, ConvenienceApi::new, (convenienceApi, fieldName, reader) -> { + if (convenienceApi.tryConsumeParentProperties(convenienceApi, fieldName, reader)) { + return; + } + + if ("requests".equals(fieldName)) { + convenienceApi.requests = reader.readArray(Request::fromJson); + } else { + reader.skipChildren(); + } + }); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/CredentialSchema.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/CredentialSchema.java new file mode 100644 index 0000000000..d7782e376e --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/CredentialSchema.java @@ -0,0 +1,138 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.extension.model.codemodel; + +import com.microsoft.typespec.http.client.generator.core.extension.base.util.JsonUtils; +import com.azure.json.JsonReader; +import com.azure.json.JsonWriter; + +import java.io.IOException; +import java.util.Objects; + +/** + * Represents a credential schema. + */ +public class CredentialSchema extends PrimitiveSchema { + private double maxLength; + private double minLength; + private String pattern; + + /** + * Creates a new instance of the CredentialSchema class. + */ + public CredentialSchema() { + super(); + } + + /** + * Get the maximum length of the string. + * + * @return The maximum length of the string. + */ + public double getMaxLength() { + return maxLength; + } + + /** + * Set the maximum length of the string. + * + * @param maxLength The maximum length of the string. + */ + public void setMaxLength(double maxLength) { + this.maxLength = maxLength; + } + + /** + * Get the minimum length of the string. + * + * @return The minimum length of the string. + */ + public double getMinLength() { + return minLength; + } + + /** + * Set the minimum length of the string. + * + * @param minLength The minimum length of the string. + */ + public void setMinLength(double minLength) { + this.minLength = minLength; + } + + /** + * Get a regular expression that the string must be validated against. + * + * @return A regular expression that the string must be validated against. + */ + public String getPattern() { + return pattern; + } + + /** + * Set a regular expression that the string must be validated against. + * + * @param pattern A regular expression that the string must be validated against. + */ + public void setPattern(String pattern) { + this.pattern = pattern; + } + + @Override + public String toString() { + return CredentialSchema.class.getName() + "@" + Integer.toHexString(System.identityHashCode(this)) + + "[maxLength=" + maxLength + ",minLength=" + minLength + ",pattern=" + Objects.toString(pattern, "") + + "]"; + } + + @Override + public int hashCode() { + return Objects.hash(pattern, maxLength, minLength); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof CredentialSchema)) { + return false; + } + + CredentialSchema rhs = ((CredentialSchema) other); + return Objects.equals(maxLength, rhs.maxLength) && Objects.equals(minLength, rhs.minLength) + && Objects.equals(pattern, rhs.pattern); + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + return jsonWriter.writeStartObject() + .writeDoubleField("maxLength", maxLength) + .writeDoubleField("minLength", minLength) + .writeStringField("pattern", pattern) + .writeEndObject(); + } + + /** + * Deserializes a CredentialSchema instance from the JSON data. + * + * @param jsonReader The JSON reader to deserialize from. + * @return A CredentialSchema instance deserialized from the JSON data. + * @throws IOException If an error occurs during deserialization. + */ + public static CredentialSchema fromJson(JsonReader jsonReader) throws IOException { + return JsonUtils.readObject(jsonReader, CredentialSchema::new, (schema, fieldName, reader) -> { + if ("maxLength".equals(fieldName)) { + schema.maxLength = reader.getDouble(); + } else if ("minLength".equals(fieldName)) { + schema.minLength = reader.getDouble(); + } else if ("pattern".equals(fieldName)) { + schema.pattern = reader.getString(); + } else { + reader.skipChildren();; + } + }); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/DateSchema.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/DateSchema.java new file mode 100644 index 0000000000..a21745288d --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/DateSchema.java @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.extension.model.codemodel; + +import com.microsoft.typespec.http.client.generator.core.extension.base.util.JsonUtils; +import com.azure.json.JsonReader; +import com.azure.json.JsonWriter; + +import java.io.IOException; + +/** + * Represents a date value. + */ +public class DateSchema extends PrimitiveSchema { + + /** + * Creates a new instance of the DateSchema class. + */ + public DateSchema() { + super(); + } + + @Override + public String toString() { + return DateSchema.class.getName() + "@" + Integer.toHexString(System.identityHashCode(this)) + "[]"; + } + + @Override + public int hashCode() { + return 1; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + return other instanceof DateSchema; + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + return super.toJson(jsonWriter); + } + + /** + * Deserializes a DateSchema instance from the JSON data. + * + * @param jsonReader The JSON reader to deserialize from. + * @return A DateSchema instance deserialized from the JSON data. + * @throws IOException If an error occurs during deserialization. + */ + public static DateSchema fromJson(JsonReader jsonReader) throws IOException { + return JsonUtils.readObject(jsonReader, DateSchema::new, (schema, fieldName, reader) -> { + if (!schema.tryConsumeParentProperties(schema, fieldName, reader)) { + reader.skipChildren(); + } + }); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/DateTimeSchema.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/DateTimeSchema.java new file mode 100644 index 0000000000..6606946121 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/DateTimeSchema.java @@ -0,0 +1,145 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.extension.model.codemodel; + +import com.microsoft.typespec.http.client.generator.core.extension.base.util.JsonUtils; +import com.azure.json.JsonReader; +import com.azure.json.JsonWriter; + +import java.io.IOException; +import java.util.Objects; + +/** + * Represents a date-time value. + */ +public class DateTimeSchema extends PrimitiveSchema { + private DateTimeSchema.Format format; + + /** + * Creates a new instance of the DateTimeSchema class. + */ + public DateTimeSchema() { + super(); + } + + /** + * Gets the date-time format. (Required) + * + * @return The date-time format. + */ + public DateTimeSchema.Format getFormat() { + return format; + } + + /** + * Sets the date-time format. (Required) + * + * @param format The date-time format. + */ + public void setFormat(DateTimeSchema.Format format) { + this.format = format; + } + + @Override + public String toString() { + return DateTimeSchema.class.getName() + '@' + Integer.toHexString(System.identityHashCode(this)) + "[format=" + + Objects.toString(format, "") + ']'; + } + + @Override + public int hashCode() { + return Objects.hash(format); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof DateTimeSchema)) { + return false; + } + + DateTimeSchema rhs = ((DateTimeSchema) other); + return format == rhs.format; + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + return jsonWriter.writeStartObject() + .writeStringField("format", format == null ? null : format.toString()) + .writeEndObject(); + } + + /** + * Deserializes a DateTimeSchema instance from the JSON data. + * + * @param jsonReader The JSON reader to deserialize from. + * @return A DateTimeSchema instance deserialized from the JSON data. + * @throws IOException If an error occurs during deserialization. + */ + public static DateTimeSchema fromJson(JsonReader jsonReader) throws IOException { + return JsonUtils.readObject(jsonReader, DateTimeSchema::new, (schema, fieldName, reader) -> { + if ("format".equals(fieldName)) { + schema.format = Format.fromValue(reader.getString()); + } else { + reader.skipChildren(); + } + }); + } + + /** + * The format of the date-time. + */ + public enum Format { + /** + * The date-time format. + */ + DATE_TIME("date-time"), + + /** + * The RFC 1123 date-time format. + */ + DATE_TIME_RFC_1123("date-time-rfc1123"); + private final String value; + + Format(String value) { + this.value = value; + } + + @Override + public String toString() { + return this.value; + } + + /** + * Gets the value of the format. + * + * @return The value of the format. + */ + public String value() { + return this.value; + } + + /** + * Parses a value to a Format instance. + * + * @param value The value to parse. + * @return The parsed Format instance. + * @throws IllegalArgumentException If the value does not match a known Format. + */ + public static DateTimeSchema.Format fromValue(String value) { + if ("date-time".equals(value)) { + return DATE_TIME; + } else if ("date-time-rfc1123".equals(value)) { + return DATE_TIME_RFC_1123; + } else { + throw new IllegalArgumentException(value); + } + } + + } + +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/Deprecation.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/Deprecation.java new file mode 100644 index 0000000000..71e64b3a6c --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/Deprecation.java @@ -0,0 +1,116 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.extension.model.codemodel; + +import com.microsoft.typespec.http.client.generator.core.extension.base.util.JsonUtils; +import com.azure.json.JsonReader; +import com.azure.json.JsonSerializable; +import com.azure.json.JsonWriter; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + * Represents deprecation information. + */ +public class Deprecation implements JsonSerializable { + private String message; + private List apiVersions = new ArrayList<>(); + + /** + * Creates a new instance of the Deprecation class. + */ + public Deprecation() { + } + + /** + * Gets the deprecated message. (Required) + * + * @return The deprecated message. + */ + public String getMessage() { + return message; + } + + /** + * Sets the deprecated message. (Required) + * + * @param message The deprecated message. + */ + public void setMessage(String message) { + this.message = message; + } + + /** + * Gets the API versions that this deprecation is applicable to. (Required) + * + * @return The API versions that this deprecation is applicable to. + */ + public List getApiVersions() { + return apiVersions; + } + + /** + * Sets the API versions that this deprecation is applicable to. (Required) + * + * @param apiVersions The API versions that this deprecation is applicable to. + */ + public void setApiVersions(List apiVersions) { + this.apiVersions = apiVersions; + } + + @Override + public String toString() { + return Deprecation.class.getName() + "@" + Integer.toHexString(System.identityHashCode(this)) + "[message=" + + Objects.toString(message, "") + ",apiVersions=" + Objects.toString(apiVersions, "") + "]"; + } + + @Override + public int hashCode() { + return Objects.hash(message, apiVersions); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof Deprecation)) { + return false; + } + + Deprecation rhs = ((Deprecation) other); + return Objects.equals(message, rhs.message) && Objects.equals(apiVersions, rhs.apiVersions); + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + return jsonWriter.writeStartObject() + .writeStringField("message", message) + .writeArrayField("apiVersions", apiVersions, JsonWriter::writeJson) + .writeEndObject(); + } + + /** + * Deserializes a Deprecation instance from the JSON data. + * + * @param jsonReader The JSON reader to deserialize from. + * @return A Deprecation instance deserialized from the JSON data. + * @throws IOException If an error occurs during deserialization. + */ + public static Deprecation fromJson(JsonReader jsonReader) throws IOException { + return JsonUtils.readObject(jsonReader, Deprecation::new, (deprecation, fieldName, reader) -> { + if ("message".equals(fieldName)) { + deprecation.message = reader.getString(); + } else if ("apiVersions".equals(fieldName)) { + deprecation.apiVersions = reader.readArray(ApiVersion::fromJson); + } else { + reader.skipChildren(); + } + }); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/DictionaryAny.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/DictionaryAny.java new file mode 100644 index 0000000000..22298c36a5 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/DictionaryAny.java @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.extension.model.codemodel; + +import com.microsoft.typespec.http.client.generator.core.extension.base.util.JsonUtils; +import com.azure.json.JsonReader; +import com.azure.json.JsonSerializable; +import com.azure.json.JsonWriter; + +import java.io.IOException; + +/** + * Represents a dictionary of any type. + */ +public class DictionaryAny implements JsonSerializable { + + /** + * Creates a new instance of the DictionaryAny class. + */ + public DictionaryAny() { + } + + @Override + public String toString() { + return DictionaryAny.class.getName() + "@" + Integer.toHexString(System.identityHashCode(this)) + "[]"; + } + + @Override + public int hashCode() { + return 1; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + return other instanceof DictionaryAny; + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + return jsonWriter.writeStartObject().writeEndObject(); + } + + /** + * Deserializes a DictionaryAny instance from the JSON data. + * + * @param jsonReader The JSON reader to deserialize from. + * @return A DictionaryAny instance deserialized from the JSON data. + * @throws IOException If an error occurs during deserialization. + */ + public static DictionaryAny fromJson(JsonReader jsonReader) throws IOException { + return JsonUtils.readEmptyObject(jsonReader, DictionaryAny::new); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/DictionaryApiVersion.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/DictionaryApiVersion.java new file mode 100644 index 0000000000..f0880b6085 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/DictionaryApiVersion.java @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.extension.model.codemodel; + +import com.microsoft.typespec.http.client.generator.core.extension.base.util.JsonUtils; +import com.azure.json.JsonReader; +import com.azure.json.JsonSerializable; +import com.azure.json.JsonWriter; + +import java.io.IOException; + +/** + * Represents the version of the dictionary API. + */ +public class DictionaryApiVersion implements JsonSerializable { + + /** + * Creates a new instance of the DictionaryApiVersion class. + */ + public DictionaryApiVersion() { + } + + @Override + public String toString() { + return DictionaryApiVersion.class.getName() + "@" + Integer.toHexString(System.identityHashCode(this)) + "[]"; + } + + @Override + public int hashCode() { + return 1; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + return other instanceof DictionaryApiVersion; + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + return jsonWriter.writeStartObject().writeEndObject(); + } + + /** + * Deserializes a DictionaryApiVersion instance from the JSON data. + * + * @param jsonReader The JSON reader to deserialize from. + * @return A DictionaryApiVersion instance deserialized from the JSON data. + * @throws IOException If an error occurs during deserialization. + */ + public static DictionaryApiVersion fromJson(JsonReader jsonReader) throws IOException { + return JsonUtils.readEmptyObject(jsonReader, DictionaryApiVersion::new); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/DictionarySchema.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/DictionarySchema.java new file mode 100644 index 0000000000..6b85d9c242 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/DictionarySchema.java @@ -0,0 +1,115 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.extension.model.codemodel; + +import com.microsoft.typespec.http.client.generator.core.extension.base.util.JsonUtils; +import com.azure.json.JsonReader; +import com.azure.json.JsonWriter; + +import java.io.IOException; +import java.util.Objects; + +/** + * Represents a key-value collection. + */ +public class DictionarySchema extends ComplexSchema { + private Schema elementType; + private Boolean nullableItems; + + /** + * Creates a new instance of the DictionarySchema class. + */ + public DictionarySchema() { + super(); + } + + /** + * Gets the type of the elements in the dictionary. (Required) + * + * @return The type of the elements in the dictionary. + */ + public Schema getElementType() { + return elementType; + } + + /** + * Sets the type of the elements in the dictionary. (Required) + * + * @param elementType The type of the elements in the dictionary. + */ + public void setElementType(Schema elementType) { + this.elementType = elementType; + } + + /** + * Gets whether the items in the dictionary can be null. + * + * @return Whether the items in the dictionary can be null. + */ + public Boolean getNullableItems() { + return nullableItems; + } + + /** + * Sets whether the items in the dictionary can be null. + * + * @param nullableItems Whether the items in the dictionary can be null. + */ + public void setNullableItems(Boolean nullableItems) { + this.nullableItems = nullableItems; + } + + @Override + public String toString() { + return DictionarySchema.class.getName() + '@' + Integer.toHexString(System.identityHashCode(this)) + + "[elementType=" + Objects.toString(elementType, "") + ",nullableItems=" + + Objects.toString(nullableItems, "") + "]"; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + + if (!(o instanceof DictionarySchema)) { + return false; + } + + DictionarySchema that = (DictionarySchema) o; + return Objects.equals(elementType, that.elementType) && Objects.equals(nullableItems, that.nullableItems); + } + + @Override + public int hashCode() { + return Objects.hash(elementType, nullableItems); + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + return jsonWriter.writeStartObject() + .writeJsonField("elementType", elementType) + .writeBooleanField("nullableItems", nullableItems) + .writeEndObject(); + } + + /** + * Deserializes a DictionarySchema instance from the JSON data. + * + * @param jsonReader The JSON reader to deserialize from. + * @return A DictionarySchema instance deserialized from the JSON data. + * @throws IOException If an error occurs during deserialization. + */ + public static DictionarySchema fromJson(JsonReader jsonReader) throws IOException { + return JsonUtils.readObject(jsonReader, DictionarySchema::new, (schema, fieldName, reader) -> { + if ("elementType".equals(fieldName)) { + schema.elementType = Schema.fromJson(reader); + } else if ("nullableItems".equals(fieldName)) { + schema.nullableItems = reader.getNullable(JsonReader::getBoolean); + } else { + reader.skipChildren(); + } + }); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/Discriminator.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/Discriminator.java new file mode 100644 index 0000000000..b1ffda69ad --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/Discriminator.java @@ -0,0 +1,111 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.extension.model.codemodel; + +import com.microsoft.typespec.http.client.generator.core.extension.base.util.JsonUtils; +import com.azure.json.JsonReader; +import com.azure.json.JsonSerializable; +import com.azure.json.JsonWriter; + +import java.io.IOException; +import java.util.Map; + +/** + * Represents a discriminator for polymorphic types. + */ +public class Discriminator implements JsonSerializable { + private Property property; + private Map immediate; + private Map all; + + /** + * Creates a new instance of the Discriminator class. + */ + public Discriminator() { + } + + /** + * Gets the property that is used to discriminate between the polymorphic types. + * + * @return The property that is used to discriminate between the polymorphic types. + */ + public Property getProperty() { + return property; + } + + /** + * Sets the property that is used to discriminate between the polymorphic types. + * + * @param property The property that is used to discriminate between the polymorphic types. + */ + public void setProperty(Property property) { + this.property = property; + } + + /** + * Gets the immediate polymorphic types. + * + * @return The immediate polymorphic types. + */ + public Map getImmediate() { + return immediate; + } + + /** + * Sets the immediate polymorphic types. + * + * @param immediate The immediate polymorphic types. + */ + public void setImmediate(Map immediate) { + this.immediate = immediate; + } + + /** + * Gets all polymorphic types. + * + * @return All polymorphic types. + */ + public Map getAll() { + return all; + } + + /** + * Sets all polymorphic types. + * + * @param all All polymorphic types. + */ + public void setAll(Map all) { + this.all = all; + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + return jsonWriter.writeStartObject() + .writeJsonField("property", property) + .writeMapField("immediate", immediate, JsonWriter::writeJson) + .writeMapField("all", all, JsonWriter::writeJson) + .writeEndObject(); + } + + /** + * Deserializes a Discriminator instance from the JSON data. + * + * @param jsonReader The JSON reader to deserialize from. + * @return A Discriminator instance deserialized from the JSON data. + * @throws IOException If an error occurs during deserialization. + */ + public static Discriminator fromJson(JsonReader jsonReader) throws IOException { + return JsonUtils.readObject(jsonReader, Discriminator::new, (discriminator, fieldName, reader) -> { + if ("property".equals(fieldName)) { + discriminator.property = Property.fromJson(reader); + } else if ("immediate".equals(fieldName)) { + discriminator.immediate = reader.readMap(ComplexSchema::fromJson); + } else if ("all".equals(fieldName)) { + discriminator.all = reader.readMap(ComplexSchema::fromJson); + } else { + reader.skipChildren(); + } + }); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/DurationSchema.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/DurationSchema.java new file mode 100644 index 0000000000..faff15f697 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/DurationSchema.java @@ -0,0 +1,151 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.extension.model.codemodel; + +import com.microsoft.typespec.http.client.generator.core.extension.base.util.JsonUtils; +import com.azure.json.JsonReader; +import com.azure.json.JsonWriter; + +import java.io.IOException; +import java.util.Objects; + +/** + * Represents a Duration value. + */ +public class DurationSchema extends PrimitiveSchema { + private Format format; + + /** + * Creates a new instance of the DurationSchema class. + */ + public DurationSchema() { + super(); + } + + /** + * Gets the duration format. + * + * @return The duration format. + */ + public Format getFormat() { + return format; + } + + /** + * Sets the duration format. + * + * @param format The duration format. + */ + public void setFormat(Format format) { + this.format = format; + } + + @Override + public String toString() { + return DurationSchema.class.getName() + '@' + Integer.toHexString(System.identityHashCode(this)) + "[format=" + + Objects.toString(format, "") + ']'; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + + if (!(o instanceof DurationSchema)) { + return false; + } + + DurationSchema that = (DurationSchema) o; + return format == that.format; + } + + @Override + public int hashCode() { + return Objects.hash(format); + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + return jsonWriter.writeStartObject() + .writeStringField("format", format == null ? null : format.toString()) + .writeEndObject(); + } + + /** + * Deserializes a DurationSchema instance from the JSON data. + * + * @param jsonReader The JSON reader to deserialize from. + * @return A DurationSchema instance deserialized from the JSON data. + * @throws IOException If an error occurs during deserialization. + */ + public static DurationSchema fromJson(JsonReader jsonReader) throws IOException { + return JsonUtils.readObject(jsonReader, DurationSchema::new, (schema, fieldName, reader) -> { + if ("format".equals(fieldName)) { + schema.format = Format.fromValue(reader.getString()); + } else { + reader.skipChildren(); + } + }); + } + + /** + * The format of the duration. + */ + public enum Format { + /** + * The duration is in RFC3339 format. + */ + DURATION("duration-rfc3339"), + + /** + * The duration is in seconds as an integer. + */ + SECONDS_INTEGER("seconds-integer"), + + /** + * The duration is in seconds as a number. + */ + SECONDS_NUMBER("seconds-number"); + + private final String value; + + Format(String value) { + this.value = value; + } + + @Override + public String toString() { + return this.value; + } + + /** + * Gets the string value of the format. + * + * @return The string value of the format. + */ + public String value() { + return this.value; + } + + /** + * Parses a string value to a Format instance. + * + * @param value The string value to parse. + * @return The parsed Format instance. + * @throws IllegalArgumentException If the string value does not correspond to a valid Format instance. + */ + public static Format fromValue(String value) { + if ("duration-rfc3339".equals(value)) { + return DURATION; + } else if ("seconds-integer".equals(value)) { + return SECONDS_INTEGER; + } else if ("seconds-number".equals(value)) { + return SECONDS_NUMBER; + } else { + throw new IllegalArgumentException(value); + } + } + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/ExternalDocumentation.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/ExternalDocumentation.java new file mode 100644 index 0000000000..4627da2a26 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/ExternalDocumentation.java @@ -0,0 +1,138 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.extension.model.codemodel; + +import com.microsoft.typespec.http.client.generator.core.extension.base.util.JsonUtils; +import com.azure.json.JsonReader; +import com.azure.json.JsonSerializable; +import com.azure.json.JsonWriter; + +import java.io.IOException; +import java.util.Objects; + +/** + * Represents a reference to external documentation. + */ +public class ExternalDocumentation implements JsonSerializable { + private String description; + private String url; + private DictionaryAny extensions; + + /** + * Creates a new instance of the ExternalDocumentation class. + */ + public ExternalDocumentation() { + } + + /** + * Gets the description of the external documentation. + * + * @return The description of the external documentation. + */ + public String getDescription() { + return description; + } + + /** + * Sets the description of the external documentation. + * + * @param description The description of the external documentation. + */ + public void setDescription(String description) { + this.description = description; + } + + /** + * Gets the URL of the external documentation. + * + * @return The URL of the external documentation. + */ + public String getUrl() { + return url; + } + + /** + * Sets the URL of the external documentation. + * + * @param url The URL of the external documentation. + */ + public void setUrl(String url) { + this.url = url; + } + + /** + * Gets the extensions of the external documentation. + * + * @return The extensions of the external documentation. + */ + public DictionaryAny getExtensions() { + return extensions; + } + + /** + * Sets the extensions of the external documentation. + * + * @param extensions The extensions of the external documentation. + */ + public void setExtensions(DictionaryAny extensions) { + this.extensions = extensions; + } + + @Override + public String toString() { + return ExternalDocumentation.class.getName() + "@" + Integer.toHexString(System.identityHashCode(this)) + + "[description=" + Objects.toString(description, "") + ",url=" + Objects.toString(url, "") + + ",extensions=" + Objects.toString(extensions, "") + "]"; + } + + @Override + public int hashCode() { + return Objects.hash(description, extensions, url); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof ExternalDocumentation)) { + return false; + } + + ExternalDocumentation rhs = ((ExternalDocumentation) other); + return Objects.equals(description, rhs.description) && Objects.equals(url, rhs.url) + && Objects.equals(extensions, rhs.extensions); + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + return jsonWriter.writeStartObject() + .writeStringField("description", description) + .writeStringField("url", url) + .writeJsonField("extensions", extensions) + .writeEndObject(); + } + + /** + * Deserializes an ExternalDocumentation instance from the JSON data. + * + * @param jsonReader The JSON reader to deserialize from. + * @return An ExternalDocumentation instance deserialized from the JSON data. + * @throws IOException If an error occurs during deserialization. + */ + public static ExternalDocumentation fromJson(JsonReader jsonReader) throws IOException { + return JsonUtils.readObject(jsonReader, ExternalDocumentation::new, (documentation, fieldName, reader) -> { + if ("description".equals(fieldName)) { + documentation.description = reader.getString(); + } else if ("url".equals(fieldName)) { + documentation.url = reader.getString(); + } else if ("extensions".equals(fieldName)) { + documentation.extensions = DictionaryAny.fromJson(reader); + } else { + reader.skipChildren(); + } + }); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/FlagSchema.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/FlagSchema.java new file mode 100644 index 0000000000..0f4dbe1d0b --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/FlagSchema.java @@ -0,0 +1,94 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.extension.model.codemodel; + +import com.microsoft.typespec.http.client.generator.core.extension.base.util.JsonUtils; +import com.azure.json.JsonReader; +import com.azure.json.JsonWriter; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + * Represents a flag schema. + */ +public class FlagSchema extends ValueSchema { + private List choices = new ArrayList<>(); + + /** + * Creates a new instance of the FlagSchema class. + */ + public FlagSchema() { + super(); + } + + /** + * Get the possible choices in the set. (Required) + * + * @return The possible choices in the set. + */ + public List getChoices() { + return choices; + } + + /** + * Set the possible choices in the set. (Required) + * + * @param choices The possible choices in the set. + */ + public void setChoices(List choices) { + this.choices = choices; + } + + @Override + public String toString() { + return FlagSchema.class.getName() + "@" + Integer.toHexString(System.identityHashCode(this)) + "[choices=" + + choices + "]"; + } + + @Override + public int hashCode() { + return Objects.hashCode(choices); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof FlagSchema)) { + return false; + } + + FlagSchema rhs = ((FlagSchema) other); + return Objects.equals(choices, rhs.choices); + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + return jsonWriter.writeStartObject() + .writeArrayField("choices", choices, JsonWriter::writeJson) + .writeEndObject(); + } + + /** + * Deserializes a FlagSchema instance from the JSON data. + * + * @param jsonReader The JSON reader to deserialize from. + * @return A FlagSchema instance deserialized from the JSON data. + * @throws IOException If an error occurs during deserialization. + */ + public static FlagSchema fromJson(JsonReader jsonReader) throws IOException { + return JsonUtils.readObject(jsonReader, FlagSchema::new, (schema, fieldName, reader) -> { + if ("choices".equals(fieldName)) { + schema.choices = reader.readArray(FlagValue::fromJson); + } else { + reader.skipChildren(); + } + }); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/FlagValue.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/FlagValue.java new file mode 100644 index 0000000000..9646f8f8d9 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/FlagValue.java @@ -0,0 +1,138 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.extension.model.codemodel; + +import com.microsoft.typespec.http.client.generator.core.extension.base.util.JsonUtils; +import com.azure.json.JsonReader; +import com.azure.json.JsonSerializable; +import com.azure.json.JsonWriter; + +import java.io.IOException; +import java.util.Objects; + +/** + * Represents a flag value. + */ +public class FlagValue implements JsonSerializable { + private Languages language; + private double value; + private DictionaryAny extensions; + + /** + * Creates a new instance of the FlagValue class. + */ + public FlagValue() { + } + + /** + * Gets the language of the flag value. (Required) + * + * @return The language of the flag value. + */ + public Languages getLanguage() { + return language; + } + + /** + * Sets the language of the flag value. (Required) + * + * @param language The language of the flag value. + */ + public void setLanguage(Languages language) { + this.language = language; + } + + /** + * Gets the value of the flag. (Required) + * + * @return The value of the flag. + */ + public double getValue() { + return value; + } + + /** + * Sets the value of the flag. (Required) + * + * @param value The value of the flag. + */ + public void setValue(double value) { + this.value = value; + } + + /** + * Gets the extensions of the flag value. + * + * @return The extensions of the flag value. + */ + public DictionaryAny getExtensions() { + return extensions; + } + + /** + * Sets the extensions of the flag value. + * + * @param extensions The extensions of the flag value. + */ + public void setExtensions(DictionaryAny extensions) { + this.extensions = extensions; + } + + @Override + public String toString() { + return FlagValue.class.getName() + "@" + Integer.toHexString(System.identityHashCode(this)) + "[language=" + + Objects.toString(language, "") + ",value=" + value + ",extensions=" + + Objects.toString(extensions, "") + "]"; + } + + @Override + public int hashCode() { + return Objects.hash(language, extensions, value); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof FlagValue)) { + return false; + } + + FlagValue rhs = ((FlagValue) other); + return value == rhs.value && Objects.equals(language, rhs.language) + && Objects.equals(extensions, rhs.extensions); + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + return jsonWriter.writeStartObject() + .writeJsonField("language", language) + .writeDoubleField("value", value) + .writeJsonField("extensions", extensions) + .writeEndObject(); + } + + /** + * Deserializes a FlagValue instance from the JSON data. + * + * @param jsonReader The JSON reader to deserialize from. + * @return A FlagValue instance deserialized from the JSON data. + * @throws IOException If an error occurs during deserialization. + */ + public static FlagValue fromJson(JsonReader jsonReader) throws IOException { + return JsonUtils.readObject(jsonReader, FlagValue::new, (value, fieldName, reader) -> { + if ("language".equals(fieldName)) { + value.language = Languages.fromJson(reader); + } else if ("value".equals(fieldName)) { + value.value = reader.getDouble(); + } else if ("extensions".equals(fieldName)) { + value.extensions = DictionaryAny.fromJson(reader); + } else { + reader.skipChildren(); + } + }); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/Header.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/Header.java new file mode 100644 index 0000000000..631b053e1f --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/Header.java @@ -0,0 +1,111 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.extension.model.codemodel; + +import com.microsoft.typespec.http.client.generator.core.extension.model.extensionmodel.XmsExtensions; +import com.microsoft.typespec.http.client.generator.core.extension.base.util.JsonUtils; +import com.azure.json.JsonReader; +import com.azure.json.JsonSerializable; +import com.azure.json.JsonWriter; + +import java.io.IOException; + +/** + * Represents a header. + */ +public class Header implements JsonSerializable

{ + private String header; + private Schema schema; + private XmsExtensions extensions; + + /** + * Creates a new instance of the Header class. + */ + public Header() { + } + + /** + * Gets the name of the header. + * + * @return The name of the header. + */ + public String getHeader() { + return header; + } + + /** + * Sets the name of the header. + * + * @param header The name of the header. + */ + public void setHeader(String header) { + this.header = header; + } + + /** + * Gets the schema of the header. + * + * @return The schema of the header. + */ + public Schema getSchema() { + return schema; + } + + /** + * Sets the schema of the header. + * + * @param schema The schema of the header. + */ + public void setSchema(Schema schema) { + this.schema = schema; + } + + /** + * Gets the extensions of the header. + * + * @return The extensions of the header. + */ + public XmsExtensions getExtensions() { + return extensions; + } + + /** + * Sets the extensions of the header. + * + * @param extensions The extensions of the header. + */ + public void setExtensions(XmsExtensions extensions) { + this.extensions = extensions; + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + return jsonWriter.writeStartObject() + .writeStringField("header", header) + .writeJsonField("schema", schema) + .writeJsonField("extensions", extensions) + .writeEndObject(); + } + + /** + * Deserializes a Header instance from the JSON data. + * + * @param jsonReader The JSON reader to deserialize from. + * @return A Header instance deserialized from the JSON data. + * @throws IOException If an error occurs during deserialization. + */ + public static Header fromJson(JsonReader jsonReader) throws IOException { + return JsonUtils.readObject(jsonReader, Header::new, (header, fieldName, reader) -> { + if ("header".equals(fieldName)) { + header.header = reader.getString(); + } else if ("schema".equals(fieldName)) { + header.schema = Schema.fromJson(reader); + } else if ("extensions".equals(fieldName)) { + header.extensions = XmsExtensions.fromJson(reader); + } else { + reader.skipChildren(); + } + }); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/Info.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/Info.java new file mode 100644 index 0000000000..5a88bd91af --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/Info.java @@ -0,0 +1,230 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.extension.model.codemodel; + +import com.microsoft.typespec.http.client.generator.core.extension.base.util.JsonUtils; +import com.azure.json.JsonReader; +import com.azure.json.JsonSerializable; +import com.azure.json.JsonWriter; + +import java.io.IOException; +import java.util.Objects; + +/** + * Represents code model info. + */ +public class Info implements JsonSerializable { + private String title; + private String description; + private String termsOfService; + private Contact contact; + private License license; + private ExternalDocumentation externalDocs; + private DictionaryAny extensions; + + /** + * Creates a new instance of the Info class. + */ + public Info() { + } + + /** + * Gets the title of the service. (Required) + * + * @return The title of the service. + */ + public String getTitle() { + return title; + } + + /** + * Sets the title of the service. (Required) + * + * @param title The title of the service. + */ + public void setTitle(String title) { + this.title = title; + } + + /** + * Gets the description of the service. + * + * @return The description of the service. + */ + public String getDescription() { + return description; + } + + /** + * Sets the description of the service. + * + * @param description The description of the service. + */ + public void setDescription(String description) { + this.description = description; + } + + /** + * Gets the URL of the terms of service. + * + * @return The URL of the terms of service. + */ + public String getTermsOfService() { + return termsOfService; + } + + /** + * Sets the URL of the terms of service. + * + * @param termsOfService The URL of the terms of service. + */ + public void setTermsOfService(String termsOfService) { + this.termsOfService = termsOfService; + } + + /** + * Gets the contact information. + * + * @return The contact information. + */ + public Contact getContact() { + return contact; + } + + /** + * Sets the contact information. + * + * @param contact The contact information. + */ + public void setContact(Contact contact) { + this.contact = contact; + } + + /** + * Gets the license information. + * + * @return The license information. + */ + public License getLicense() { + return license; + } + + /** + * Sets the license information. + * + * @param license The license information. + */ + public void setLicense(License license) { + this.license = license; + } + + /** + * Gets the reference to external documentation. + * + * @return The reference to external documentation. + */ + public ExternalDocumentation getExternalDocs() { + return externalDocs; + } + + /** + * Sets the reference to external documentation. + * + * @param externalDocs The reference to external documentation. + */ + public void setExternalDocs(ExternalDocumentation externalDocs) { + this.externalDocs = externalDocs; + } + + /** + * Gets the extensions of the service. + * + * @return The extensions of the service. + */ + public DictionaryAny getExtensions() { + return extensions; + } + + /** + * Sets the extensions of the service. + * + * @param extensions The extensions of the service. + */ + public void setExtensions(DictionaryAny extensions) { + this.extensions = extensions; + } + + @Override + public String toString() { + return Info.class.getName() + "@" + Integer.toHexString(System.identityHashCode(this)) + "[title=" + + Objects.toString(title, "") + ",description=" + Objects.toString(description, "") + + ",termsOfService=" + Objects.toString(termsOfService, "") + ",contact=" + + Objects.toString(contact, "") + ",license=" + Objects.toString(license, "") + ",externalDocs=" + + Objects.toString(externalDocs, "") + ",extensions=" + Objects.toString(extensions, "") + "]"; + } + + @Override + public int hashCode() { + return Objects.hash(license, extensions, contact, description, termsOfService, externalDocs, title); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof Info)) { + return false; + } + + Info rhs = ((Info) other); + return Objects.equals(title, rhs.title) && Objects.equals(description, rhs.description) + && Objects.equals(termsOfService, rhs.termsOfService) && Objects.equals(contact, rhs.contact) + && Objects.equals(license, rhs.license) && Objects.equals(externalDocs, rhs.externalDocs) + && Objects.equals(extensions, rhs.extensions); + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + return jsonWriter.writeStartObject() + .writeStringField("title", title) + .writeStringField("description", description) + .writeStringField("termsOfService", termsOfService) + .writeJsonField("contact", contact) + .writeJsonField("license", license) + .writeJsonField("externalDocs", externalDocs) + .writeJsonField("extensions", extensions) + .writeEndObject(); + } + + /** + * Deserializes an Info instance from the JSON data. + * + * @param jsonReader The JSON reader to deserialize from. + * @return An Info instance deserialized from the JSON data. + * @throws IOException If an error occurs during deserialization. + */ + public static Info fromJson(JsonReader jsonReader) throws IOException { + return JsonUtils.readObject(jsonReader, Info::new, (info, fieldName, reader) -> { + if ("title".equals(fieldName)) { + info.title = reader.getString(); + } else if ("description".equals(fieldName)) { + info.description = reader.getString(); + } else if ("termsOfService".equals(fieldName)) { + info.termsOfService = reader.getString(); + } else if ("contact".equals(fieldName)) { + info.contact = Contact.fromJson(reader); + } else if ("license".equals(fieldName)) { + info.license = License.fromJson(reader); + } else if ("externalDocs".equals(fieldName)) { + info.externalDocs = ExternalDocumentation.fromJson(reader); + } else if ("extensions".equals(fieldName)) { + info.extensions = DictionaryAny.fromJson(reader); + } else { + reader.skipChildren(); + } + }); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/KnownMediaType.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/KnownMediaType.java new file mode 100644 index 0000000000..ce2ea8fc56 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/KnownMediaType.java @@ -0,0 +1,107 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.extension.model.codemodel; + +import java.util.HashMap; +import java.util.Map; + +/** + * Known media types. + */ +public enum KnownMediaType { + /** + * The media type is binary. + */ + BINARY("binary"), + + /** + * The media type is a form. + */ + FORM("form"), + + /** + * The media type is JSON. + */ + JSON("json"), + + /** + * The media type is multipart. + */ + MULTIPART("multipart"), + + /** + * The media type is text. + */ + TEXT("text"), + + /** + * The media type is unknown. + */ + UNKNOWN("unknown"), + + /** + * The media type is XML. + */ + XML("xml"); + + private final String value; + private final static Map CONSTANTS = new HashMap<>(); + + static { + for (KnownMediaType c: values()) { + CONSTANTS.put(c.value, c); + } + } + + KnownMediaType(String value) { + this.value = value; + } + + @Override + public String toString() { + return this.value; + } + + /** + * Get the string value of the KnownMediaType. + * + * @return The string value. + */ + public String value() { + return this.value; + } + + /** + * Get the KnownMediaType from a string value. + * + * @param value The string value. + * @return The KnownMediaType. + * @throws IllegalArgumentException If the string value doesn't correspond to a KnownMediaType. + */ + public static KnownMediaType fromValue(String value) { + KnownMediaType constant = CONSTANTS.get(value); + if (constant == null) { + throw new IllegalArgumentException(value); + } else { + return constant; + } + } + + /** + * Get the content type for the KnownMediaType. + * + * @return The content type. + */ + public String getContentType() { + switch (this) { + case BINARY: return "application/octet-stream"; + case FORM: return "application/x-www-form-urlencoded"; + case JSON: return "application/json"; + case MULTIPART: return "multipart/form-data"; + case TEXT: return "text/plain"; + case XML: return "application/xml"; + default: return JSON.getContentType(); + } + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/Language.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/Language.java new file mode 100644 index 0000000000..c99335b817 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/Language.java @@ -0,0 +1,182 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.extension.model.codemodel; + +import com.azure.json.JsonReader; +import com.azure.json.JsonSerializable; +import com.azure.json.JsonWriter; + +import java.io.IOException; + +import static com.microsoft.typespec.http.client.generator.core.extension.base.util.JsonUtils.readObject; + +/** + * Represents the per-language metadata. + */ +public class Language implements JsonSerializable { + private String name; + private String serializedName; + private String description; + private String summary; + private String namespace; + private String comment; + + /** + * Creates a new instance of the Language class. + */ + public Language() { + } + + /** + * Gets the name used in actual implementation. (Required) + * + * @return The name used in actual implementation. + */ + public String getName() { + return name; + } + + /** + * Sets the name used in actual implementation. (Required) + * + * @param name The name used in actual implementation. + */ + public void setName(String name) { + this.name = name; + } + + /** + * Gets the serialized name. + * + * @return The serialized name. + */ + public String getSerializedName() { + return serializedName; + } + + /** + * Sets the serialized name. + * + * @param serializedName The serialized name. + */ + public void setSerializedName(String serializedName) { + this.serializedName = serializedName; + } + + /** + * Gets the description. (Required) + * + * @return The description. + */ + public String getDescription() { + return description; + } + + /** + * Sets the description. (Required) + * + * @param description The description. + */ + public void setDescription(String description) { + this.description = description; + } + + /** + * Gets the summary. + * + * @return The summary. + */ + public String getSummary() { + return summary; + } + + /** + * Sets the summary. + * + * @param summary The summary. + */ + public void setSummary(String summary) { + this.summary = summary; + } + + /** + * Gets the namespace. + * + * @return The namespace. + */ + public String getNamespace() { + return namespace; + } + + /** + * Sets the namespace. + * + * @param namespace The namespace. + */ + public void setNamespace(String namespace) { + this.namespace = namespace; + } + + /** + * Gets the comment. + * + * @return The comment. + */ + public String getComment() { + return comment; + } + + /** + * Sets the comment. + * + * @param comment The comment. + */ + public void setComment(String comment) { + this.comment = comment; + } + + @Override + public String toString() { + return "Language{name='" + name + "', serializedName='" + serializedName + "'}"; + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + return jsonWriter.writeStartObject() + .writeStringField("name", name) + .writeStringField("serializedName", serializedName) + .writeStringField("description", description) + .writeStringField("summary", summary) + .writeStringField("namespace", namespace) + .writeStringField("comment", comment) + .writeEndObject(); + } + + /** + * Deserializes a Language instance from the JSON data. + * + * @param jsonReader The JSON reader to deserialize from. + * @return A Language instance deserialized from the JSON data. + * @throws IOException If an error occurs during deserialization. + */ + public static Language fromJson(JsonReader jsonReader) throws IOException { + return readObject(jsonReader, Language::new, (language, fieldName, reader) -> { + if ("name".equals(fieldName)) { + language.name = reader.getString(); + } else if ("serializedName".equals(fieldName)) { + language.serializedName = reader.getString(); + } else if ("description".equals(fieldName)) { + language.description = reader.getString(); + } else if ("summary".equals(fieldName)) { + language.summary = reader.getString(); + } else if ("namespace".equals(fieldName)) { + language.namespace = reader.getString(); + } else if ("comment".equals(fieldName)) { + language.comment = reader.getString(); + } else { + reader.skipChildren(); + } + }); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/Languages.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/Languages.java new file mode 100644 index 0000000000..e93c04232e --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/Languages.java @@ -0,0 +1,362 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.extension.model.codemodel; + +import com.azure.json.JsonReader; +import com.azure.json.JsonSerializable; +import com.azure.json.JsonWriter; + +import java.io.IOException; +import java.util.Objects; + +import static com.microsoft.typespec.http.client.generator.core.extension.base.util.JsonUtils.readObject; + +/** + * Represents all languages. + */ +public class Languages implements JsonSerializable { + private Language _default; + private CSharpLanguage csharp; + private Language python; + private Language ruby; + private Language go; + private Language typescript; + private Language javascript; + private Language powershell; + private Language java; + private Language c; + private Language cpp; + private Language swift; + private Language objectivec; + + /** + * Creates a new instance of the Languages class. + */ + public Languages() { + } + + /** + * Gets the default language. (Required) + * + * @return The default language. + */ + public Language getDefault() { + return _default; + } + + /** + * Sets the default language. (Required) + * + * @param _default The default language. + */ + public void setDefault(Language _default) { + this._default = _default; + } + + /** + * Gets the C# language. + * + * @return The C# language. + */ + public CSharpLanguage getCsharp() { + return csharp; + } + + /** + * Sets the C# language. + * + * @param csharp The C# language. + */ + public void setCsharp(CSharpLanguage csharp) { + this.csharp = csharp; + } + + /** + * Gets the Python language. + * + * @return The Python language. + */ + public Language getPython() { + return python; + } + + /** + * Sets the Python language. + * + * @param python The Python language. + */ + public void setPython(Language python) { + this.python = python; + } + + /** + * Gets the Ruby language. + * + * @return The Ruby language. + */ + public Language getRuby() { + return ruby; + } + + /** + * Sets the Ruby language. + * + * @param ruby The Ruby language. + */ + public void setRuby(Language ruby) { + this.ruby = ruby; + } + + /** + * Gets the Go language. + * + * @return The Go language. + */ + public Language getGo() { + return go; + } + + /** + * Sets the Go language. + * + * @param go The Go language. + */ + public void setGo(Language go) { + this.go = go; + } + + /** + * Gets the TypeScript language. + * + * @return The TypeScript language. + */ + public Language getTypescript() { + return typescript; + } + + /** + * Sets the TypeScript language. + * + * @param typescript The TypeScript language. + */ + public void setTypescript(Language typescript) { + this.typescript = typescript; + } + + /** + * Gets the JavaScript language. + * + * @return The JavaScript language. + */ + public Language getJavascript() { + return javascript; + } + + /** + * Sets the JavaScript language. + * + * @param javascript The JavaScript language. + */ + public void setJavascript(Language javascript) { + this.javascript = javascript; + } + + /** + * Gets the PowerShell language. + * + * @return The PowerShell language. + */ + public Language getPowershell() { + return powershell; + } + + /** + * Sets the PowerShell language. + * + * @param powershell The PowerShell language. + */ + public void setPowershell(Language powershell) { + this.powershell = powershell; + } + + /** + * Gets the Java language. + * + * @return The Java language. + */ + public Language getJava() { + return java; + } + + /** + * Sets the Java language. + * + * @param java The Java language. + */ + public void setJava(Language java) { + this.java = java; + } + + /** + * Gets the C language. + * + * @return The C language. + */ + public Language getC() { + return c; + } + + /** + * Sets the C language. + * + * @param c The C language. + */ + public void setC(Language c) { + this.c = c; + } + + /** + * Gets the C++ language. + * + * @return The C++ language. + */ + public Language getCpp() { + return cpp; + } + + /** + * Sets the C++ language. + * + * @param cpp The C++ language. + */ + public void setCpp(Language cpp) { + this.cpp = cpp; + } + + /** + * Gets the Swift language. + * + * @return The Swift language. + */ + public Language getSwift() { + return swift; + } + + /** + * Sets the Swift language. + * + * @param swift The Swift language. + */ + public void setSwift(Language swift) { + this.swift = swift; + } + + /** + * Gets the Objective-C language. + * + * @return The Objective-C language. + */ + public Language getObjectivec() { + return objectivec; + } + + /** + * Sets the Objective-C language. + * + * @param objectivec The Objective-C language. + */ + public void setObjectivec(Language objectivec) { + this.objectivec = objectivec; + } + + @Override + public int hashCode() { + return Objects.hash(_default, python, cpp, c, go, objectivec, javascript, ruby, csharp, java, powershell, + typescript, swift); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof Languages)) { + return false; + } + + Languages rhs = ((Languages) other); + return Objects.equals(_default, rhs._default) && Objects.equals(python, rhs.python) + && Objects.equals(cpp, rhs.cpp) && Objects.equals(c, rhs.c) && Objects.equals(go, rhs.go) + && Objects.equals(objectivec, rhs.objectivec) && Objects.equals(javascript, rhs.javascript) + && Objects.equals(ruby, rhs.ruby) && Objects.equals(csharp, rhs.csharp) && Objects.equals(java, rhs.java) + && Objects.equals(powershell, rhs.powershell) && Objects.equals(typescript, rhs.typescript) + && Objects.equals(swift, rhs.swift); + } + + @Override + public String toString() { + return "Languages{default=" + _default + ", java=" + java + '}'; + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + return jsonWriter.writeStartObject() + .writeJsonField("default", _default) + .writeJsonField("csharp", csharp) + .writeJsonField("python", python) + .writeJsonField("ruby", ruby) + .writeJsonField("go", go) + .writeJsonField("typescript", typescript) + .writeJsonField("javascript", javascript) + .writeJsonField("powershell", powershell) + .writeJsonField("java", java) + .writeJsonField("c", c) + .writeJsonField("cpp", cpp) + .writeJsonField("swift", swift) + .writeJsonField("objectivec", objectivec) + .writeEndObject(); + } + + /** + * Deserializes a Languages instance from the JSON data. + * + * @param jsonReader The JSON reader to deserialize from. + * @return A Languages instance deserialized from the JSON data. + * @throws IOException If an error occurs during deserialization. + */ + public static Languages fromJson(JsonReader jsonReader) throws IOException { + return readObject(jsonReader, Languages::new, (languages, fieldName, reader) -> { + if ("_default".equals(fieldName)) { + languages._default = Language.fromJson(reader); + } else if ("csharp".equals(fieldName)) { + languages.csharp = CSharpLanguage.fromJson(reader); + } else if ("python".equals(fieldName)) { + languages.python = Language.fromJson(reader); + } else if ("ruby".equals(fieldName)) { + languages.ruby = Language.fromJson(reader); + } else if ("go".equals(fieldName)) { + languages.go = Language.fromJson(reader); + } else if ("typescript".equals(fieldName)) { + languages.typescript = Language.fromJson(reader); + } else if ("javascript".equals(fieldName)) { + languages.javascript = Language.fromJson(reader); + } else if ("powershell".equals(fieldName)) { + languages.powershell = Language.fromJson(reader); + } else if ("java".equals(fieldName)) { + languages.java = Language.fromJson(reader); + } else if ("c".equals(fieldName)) { + languages.c = Language.fromJson(reader); + } else if ("cpp".equals(fieldName)) { + languages.cpp = Language.fromJson(reader); + } else if ("swift".equals(fieldName)) { + languages.swift = Language.fromJson(reader); + } else if ("objectivec".equals(fieldName)) { + languages.objectivec = Language.fromJson(reader); + } else { + reader.skipChildren(); + } + }); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/License.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/License.java new file mode 100644 index 0000000000..cd9ba755af --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/License.java @@ -0,0 +1,138 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.extension.model.codemodel; + +import com.microsoft.typespec.http.client.generator.core.extension.base.util.JsonUtils; +import com.azure.json.JsonReader; +import com.azure.json.JsonSerializable; +import com.azure.json.JsonWriter; + +import java.io.IOException; +import java.util.Objects; + +/** + * Represents license information. + */ +public class License implements JsonSerializable { + private String name; + private String url; + private DictionaryAny extensions; + + /** + * Creates a new instance of the License class. + */ + public License() { + } + + /** + * The name of the license. (Required) + * + * @return The name of the license. + */ + public String getName() { + return name; + } + + /** + * Sets the name of the license. (Required) + * + * @param name The name of the license. + */ + public void setName(String name) { + this.name = name; + } + + /** + * Gets the URL pointing to the full license text. + * + * @return The URL pointing to the full license text. + */ + public String getUrl() { + return url; + } + + /** + * Sets the URL pointing to the full license text. + * + * @param url The URL pointing to the full license text. + */ + public void setUrl(String url) { + this.url = url; + } + + /** + * Gets the extensions of the license. + * + * @return The extensions of the license. + */ + public DictionaryAny getExtensions() { + return extensions; + } + + /** + * Sets the extensions of the license. + * + * @param extensions The extensions of the license. + */ + public void setExtensions(DictionaryAny extensions) { + this.extensions = extensions; + } + + @Override + public String toString() { + return License.class.getName() + "@" + Integer.toHexString(System.identityHashCode(this)) + "[name=" + + Objects.toString(name, "") + ",url=" + Objects.toString(url, "") + ",extensions=" + + Objects.toString(extensions, "") + "]"; + } + + @Override + public int hashCode() { + return Objects.hash(name, url, extensions); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof License)) { + return false; + } + + License rhs = ((License) other); + return Objects.equals(name, rhs.name) && Objects.equals(url, rhs.url) + && Objects.equals(extensions, rhs.extensions); + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + return jsonWriter.writeStartObject() + .writeStringField("name", name) + .writeStringField("url", url) + .writeJsonField("extensions", extensions) + .writeEndObject(); + } + + /** + * Deserializes a License instance from the JSON data. + * + * @param jsonReader The JSON reader to deserialize from. + * @return A License instance deserialized from the JSON data. + * @throws IOException If an error occurs during deserialization. + */ + public static License fromJson(JsonReader jsonReader) throws IOException { + return JsonUtils.readObject(jsonReader, License::new, (license, fieldName, reader) -> { + if ("name".equals(fieldName)) { + license.name = reader.getString(); + } else if ("url".equals(fieldName)) { + license.url = reader.getString(); + } else if ("extensions".equals(fieldName)) { + license.extensions = DictionaryAny.fromJson(reader); + } else { + reader.skipChildren(); + } + }); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/LongRunningMetadata.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/LongRunningMetadata.java new file mode 100644 index 0000000000..e77ad1df4d --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/LongRunningMetadata.java @@ -0,0 +1,132 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.extension.model.codemodel; + +import com.microsoft.typespec.http.client.generator.core.extension.base.util.JsonUtils; +import com.azure.json.JsonReader; +import com.azure.json.JsonSerializable; +import com.azure.json.JsonWriter; + +import java.io.IOException; + +/** + * Represents the metadata for long-running operations. + */ +public class LongRunningMetadata implements JsonSerializable { + private ObjectSchema pollResultType; + private ObjectSchema finalResultType; + private Metadata pollingStrategy; + private String finalResultPropertySerializedName; + + /** + * Creates a new instance of the LongRunningMetadata class. + */ + public LongRunningMetadata() { + } + + /** + * Gets the poll result type. + * + * @return The poll result type. + */ + public ObjectSchema getPollResultType() { + return pollResultType; + } + + /** + * Sets the poll result type. + * + * @param pollResultType The poll result type. + */ + public void setPollResultType(ObjectSchema pollResultType) { + this.pollResultType = pollResultType; + } + + /** + * Gets the final result type. + * + * @return The final result type. + */ + public ObjectSchema getFinalResultType() { + return finalResultType; + } + + /** + * Sets the final result type. + * + * @param finalResultType The final result type. + */ + public void setFinalResultType(ObjectSchema finalResultType) { + this.finalResultType = finalResultType; + } + + /** + * Gets the polling strategy. + * + * @return The polling strategy. + */ + public Metadata getPollingStrategy() { + return pollingStrategy; + } + + /** + * Sets the polling strategy. + * + * @param pollingStrategy The polling strategy. + */ + public void setPollingStrategy(Metadata pollingStrategy) { + this.pollingStrategy = pollingStrategy; + } + + /** + * Gets the serialized name for the property of final result. + * + * @return the serialized name for the property of final result. + */ + public String getFinalResultPropertySerializedName() { + return finalResultPropertySerializedName; + } + + /** + * Sets the serialized name for the property of final result. + * + * @param finalResultPropertySerializedName the serialized name for the property of final result. + */ + public void setFinalResultPropertySerializedName(String finalResultPropertySerializedName) { + this.finalResultPropertySerializedName = finalResultPropertySerializedName; + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + return jsonWriter.writeStartObject() + .writeJsonField("pollResultType", pollResultType) + .writeJsonField("finalResultType", finalResultType) + .writeJsonField("pollingStrategy", pollingStrategy) + .writeStringField("finalResultPropertySerializedName", finalResultPropertySerializedName) + .writeEndObject(); + } + + /** + * Deserializes a LongRunningMetadata instance from the JSON data. + * + * @param jsonReader The JSON reader to deserialize from. + * @return A LongRunningMetadata instance deserialized from the JSON data. + * @throws IOException If an error occurs during deserialization. + */ + public static LongRunningMetadata fromJson(JsonReader jsonReader) throws IOException { + return JsonUtils.readObject(jsonReader, LongRunningMetadata::new, (lroMetadata, fieldName, reader) -> { + if ("pollResultType".equals(fieldName)) { + lroMetadata.pollResultType = ObjectSchema.fromJson(reader); + } else if ("finalResultType".equals(fieldName)) { + lroMetadata.finalResultType = ObjectSchema.fromJson(reader); + } else if ("pollingStrategy".equals(fieldName)) { + lroMetadata.pollingStrategy = Metadata.fromJson(reader); + } else if ("finalResultPropertySerializedName".equals(fieldName)) { + lroMetadata.finalResultPropertySerializedName = reader.getFieldName(); + } else { + reader.skipChildren(); + } + }); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/Metadata.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/Metadata.java new file mode 100644 index 0000000000..4fad173095 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/Metadata.java @@ -0,0 +1,121 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.extension.model.codemodel; + +import com.microsoft.typespec.http.client.generator.core.extension.model.extensionmodel.XmsExtensions; +import com.microsoft.typespec.http.client.generator.core.extension.base.util.JsonUtils; +import com.azure.json.JsonReader; +import com.azure.json.JsonSerializable; +import com.azure.json.JsonWriter; + +import java.io.IOException; + +/** + * Represents metadata. + */ +public class Metadata implements JsonSerializable { + private Languages language; + private Protocols protocol; + private XmsExtensions extensions; + + /** + * Creates a new instance of the Metadata class. + */ + public Metadata() { + } + + /** + * Gets the language of the metadata. (Required) + * + * @return The language of the metadata. + */ + public Languages getLanguage() { + return language; + } + + /** + * Sets the language of the metadata. (Required) + * + * @param language The language of the metadata. + */ + public void setLanguage(Languages language) { + this.language = language; + } + + /** + * Gets the protocol of the metadata. (Required) + * + * @return The protocol of the metadata. + */ + public Protocols getProtocol() { + return protocol; + } + + /** + * Sets the protocol of the metadata. (Required) + * + * @param protocol The protocol of the metadata. + */ + public void setProtocol(Protocols protocol) { + this.protocol = protocol; + } + + /** + * Gets the extensions of the metadata. + * + * @return The extensions of the metadata. + */ + public XmsExtensions getExtensions() { + return extensions; + } + + /** + * Sets the extensions of the metadata. + * + * @param extensions The extensions of the metadata. + */ + public void setExtensions(XmsExtensions extensions) { + this.extensions = extensions; + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + return writeParentProperties(jsonWriter.writeStartObject()).writeEndObject(); + } + + JsonWriter writeParentProperties(JsonWriter jsonWriter) throws IOException { + return jsonWriter.writeJsonField("language", language) + .writeJsonField("protocol", protocol) + .writeJsonField("extensions", extensions); + } + + /** + * Deserializes a Metadata instance from the JSON data. + * + * @param jsonReader The JSON reader to deserialize from. + * @return A Metadata instance deserialized from the JSON data. + * @throws IOException If an error occurs during deserialization. + */ + public static Metadata fromJson(JsonReader jsonReader) throws IOException { + return JsonUtils.readObject(jsonReader, Metadata::new, (metadata, fieldName, reader) -> { + if (!metadata.tryConsumeParentProperties(metadata, fieldName, reader)) { + reader.skipChildren(); + } + }); + } + + boolean tryConsumeParentProperties(Metadata metadata, String fieldName, JsonReader reader) throws IOException { + if ("language".equals(fieldName)) { + metadata.language = Languages.fromJson(reader); + return true; + } else if ("protocol".equals(fieldName)) { + metadata.protocol = Protocols.fromJson(reader); + return true; + } else if ("extensions".equals(fieldName)) { + metadata.extensions = XmsExtensions.fromJson(reader); + return true; + } + return false; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/NotSchema.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/NotSchema.java new file mode 100644 index 0000000000..dfb9b8abd7 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/NotSchema.java @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.extension.model.codemodel; + +import com.microsoft.typespec.http.client.generator.core.extension.base.util.JsonUtils; +import com.azure.json.JsonReader; +import com.azure.json.JsonWriter; + +import java.io.IOException; + +/** + * Represents a NOT relationship between schemas. + */ +public class NotSchema extends Schema { + private Schema not; + + /** + * Creates a new instance of the NotSchema class. + */ + public NotSchema() { + } + + /** + * Gets the schema that this may not be. (Required) + * + * @return The schema that this may not be. + */ + public Schema getNot() { + return not; + } + + /** + * Sets the schema that this may not be. (Required) + * + * @param not The schema that this may not be. + */ + public void setNot(Schema not) { + this.not = not; + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + return jsonWriter.writeStartObject() + .writeJsonField("not", not) + .writeEndObject(); + } + + /** + * Deserializes a NotSchema instance from the JSON data. + * + * @param jsonReader The JSON reader to deserialize from. + * @return A NotSchema instance deserialized from the JSON data. + * @throws IOException If an error occurs during deserialization. + */ + public static NotSchema fromJson(JsonReader jsonReader) throws IOException { + return JsonUtils.readObject(jsonReader, NotSchema::new, (schema, fieldName, reader) -> { + if ("not".equals(fieldName)) { + schema.not = Schema.fromJson(reader); + } else { + reader.skipChildren(); + } + }); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/NumberSchema.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/NumberSchema.java new file mode 100644 index 0000000000..2fe75ff786 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/NumberSchema.java @@ -0,0 +1,179 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.extension.model.codemodel; + +import com.microsoft.typespec.http.client.generator.core.extension.base.util.JsonUtils; +import com.azure.json.JsonReader; +import com.azure.json.JsonWriter; + +import java.io.IOException; + +/** + * Represents a Number value. + */ +public class NumberSchema extends PrimitiveSchema { + private double precision; + private double multipleOf; + private double maximum; + private boolean exclusiveMaximum; + private double minimum; + private boolean exclusiveMinimum; + + /** + * Creates a new instance of the NumberSchema class. + */ + public NumberSchema() { + } + + /** + * The precision of the number. (Required) + * + * @return The precision of the number. + */ + public double getPrecision() { + return precision; + } + + /** + * Sets the precision of the number. (Required) + * + * @param precision The precision of the number. + */ + public void setPrecision(double precision) { + this.precision = precision; + } + + /** + * Gets the multiple of this number must be, if set. + * + * @return The multiple of this number must be, if set. + */ + public double getMultipleOf() { + return multipleOf; + } + + /** + * Sets the multiple of this number must be, if set. + * + * @param multipleOf The multiple of this number must be, if set. + */ + public void setMultipleOf(double multipleOf) { + this.multipleOf = multipleOf; + } + + /** + * Gets the maximum value, if set. + * + * @return The maximum value, if set. + */ + public double getMaximum() { + return maximum; + } + + /** + * Sets the maximum value, if set. + * + * @param maximum The maximum value, if set. + */ + public void setMaximum(double maximum) { + this.maximum = maximum; + } + + /** + * Gets whether the maximum value is exclusive. + * + * @return Whether the maximum value is exclusive. + */ + public boolean isExclusiveMaximum() { + return exclusiveMaximum; + } + + /** + * Sets whether the maximum value is exclusive. + * + * @param exclusiveMaximum Whether the maximum value is exclusive. + */ + public void setExclusiveMaximum(boolean exclusiveMaximum) { + this.exclusiveMaximum = exclusiveMaximum; + } + + /** + * Gets the minimum value, if set. + * + * @return The minimum value, if set. + */ + public double getMinimum() { + return minimum; + } + + /** + * Sets the minimum value, if set. + * + * @param minimum The minimum value, if set. + */ + public void setMinimum(double minimum) { + this.minimum = minimum; + } + + /** + * Gets whether the minimum value is exclusive. + * + * @return Whether the minimum value is exclusive. + */ + public boolean isExclusiveMinimum() { + return exclusiveMinimum; + } + + /** + * Sets whether the minimum value is exclusive. + * + * @param exclusiveMinimum Whether the minimum value is exclusive. + */ + public void setExclusiveMinimum(boolean exclusiveMinimum) { + this.exclusiveMinimum = exclusiveMinimum; + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + return super.writeParentProperties(jsonWriter.writeStartObject()) + .writeDoubleField("precision", precision) + .writeDoubleField("multipleOf", multipleOf) + .writeDoubleField("maximum", maximum) + .writeBooleanField("exclusiveMaximum", exclusiveMaximum) + .writeDoubleField("minimum", minimum) + .writeBooleanField("exclusiveMinimum", exclusiveMinimum) + .writeEndObject(); + } + + /** + * Deserializes a NumberSchema instance from the JSON data. + * + * @param jsonReader The JSON reader to deserialize from. + * @return A NumberSchema instance deserialized from the JSON data. + * @throws IOException If an error occurs during deserialization. + */ + public static NumberSchema fromJson(JsonReader jsonReader) throws IOException { + return JsonUtils.readObject(jsonReader, NumberSchema::new, (schema, fieldName, reader) -> { + if (schema.tryConsumeParentProperties(schema, fieldName, reader)) { + return; + } + + if ("precision".equals(fieldName)) { + schema.precision = reader.getDouble(); + } else if ("multipleOf".equals(fieldName)) { + schema.multipleOf = reader.getDouble(); + } else if ("maximum".equals(fieldName)) { + schema.maximum = reader.getDouble(); + } else if ("exclusiveMaximum".equals(fieldName)) { + schema.exclusiveMaximum = reader.getBoolean(); + } else if ("minimum".equals(fieldName)) { + schema.minimum = reader.getDouble(); + } else if ("exclusiveMinimum".equals(fieldName)) { + schema.exclusiveMinimum = reader.getBoolean(); + } else { + reader.skipChildren(); + } + }); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/ODataQuerySchema.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/ODataQuerySchema.java new file mode 100644 index 0000000000..bda2ea6629 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/ODataQuerySchema.java @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.extension.model.codemodel; + +import com.microsoft.typespec.http.client.generator.core.extension.base.util.JsonUtils; +import com.azure.json.JsonReader; +import com.azure.json.JsonWriter; + +import java.io.IOException; + +/** + * Represents an ODataQuery value. + */ +public class ODataQuerySchema extends Schema { + + /** + * Creates a new instance of the ODataQuerySchema class. + */ + public ODataQuerySchema() { + } + + @Override + public String toString() { + return ODataQuerySchema.class.getName() + "@" + Integer.toHexString(System.identityHashCode(this)) + "[]"; + } + + @Override + public int hashCode() { + return 1; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + return other instanceof ODataQuerySchema; + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + return super.toJson(jsonWriter); + } + + /** + * Deserializes an ODataQuerySchema instance from the JSON data. + * + * @param jsonReader The JSON reader to deserialize from. + * @return An ODataQuerySchema instance deserialized from the JSON data. + * @throws IOException If an error occurs during deserialization. + */ + public static ODataQuerySchema fromJson(JsonReader jsonReader) throws IOException { + return JsonUtils.readObject(jsonReader, ODataQuerySchema::new, (schema, fieldName, reader) -> { + if (!schema.tryConsumeParentProperties(schema, fieldName, reader)) { + reader.skipChildren(); + } + }); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/ObjectSchema.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/ObjectSchema.java new file mode 100644 index 0000000000..e4da0c37eb --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/ObjectSchema.java @@ -0,0 +1,268 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.extension.model.codemodel; + +import com.microsoft.typespec.http.client.generator.core.extension.base.util.JsonUtils; +import com.azure.json.JsonReader; +import com.azure.json.JsonWriter; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * Represents an object with child properties. + */ +public class ObjectSchema extends ComplexSchema { + private Discriminator discriminator; + private List properties = new ArrayList<>(); + private double maxProperties; + private double minProperties; + private Relations parents; + private Relations children; + private String discriminatorValue; + // internal use, not from modelerfour + private boolean flattenedSchema; + // internal use, not from modelerfour + private boolean stronglyTypedHeader; + private String crossLanguageDefinitionId; + + /** + * Creates a new instance of the ObjectSchema class. + */ + public ObjectSchema() { + } + + /** + * Gets the discriminator for this object. + * + * @return The discriminator for this object. + */ + public Discriminator getDiscriminator() { + return discriminator; + } + + /** + * Sets the discriminator for this object. + * + * @param discriminator The discriminator for this object. + */ + public void setDiscriminator(Discriminator discriminator) { + this.discriminator = discriminator; + } + + /** + * Gets the properties that are in this object. + * + * @return The properties that are in this object. + */ + public List getProperties() { + return properties; + } + + /** + * Sets the properties that are in this object. + * + * @param properties The properties that are in this object. + */ + public void setProperties(List properties) { + this.properties = properties; + } + + /** + * Gets the maximum number of properties permitted. + * + * @return The maximum number of properties permitted. + */ + public double getMaxProperties() { + return maxProperties; + } + + /** + * Sets the maximum number of properties permitted. + * + * @param maxProperties The maximum number of properties permitted. + */ + public void setMaxProperties(double maxProperties) { + this.maxProperties = maxProperties; + } + + /** + * Gets the minimum number of properties permitted. + * + * @return The minimum number of properties permitted. + */ + public double getMinProperties() { + return minProperties; + } + + /** + * Sets the minimum number of properties permitted. + * + * @param minProperties The minimum number of properties permitted. + */ + public void setMinProperties(double minProperties) { + this.minProperties = minProperties; + } + + /** + * Gets the parents of this object. + * + * @return The parents of this object. + */ + public Relations getParents() { + return parents; + } + + /** + * Sets the parents of this object. + * + * @param parents The parents of this object. + */ + public void setParents(Relations parents) { + this.parents = parents; + } + + /** + * Gets the children of this object. + * + * @return The children of this object. + */ + public Relations getChildren() { + return children; + } + + /** + * Sets the children of this object. + * + * @param children The children of this object. + */ + public void setChildren(Relations children) { + this.children = children; + } + + /** + * Gets the discriminator value for this object. + * + * @return The discriminator value for this object. + */ + public String getDiscriminatorValue() { + return discriminatorValue; + } + + /** + * Sets the discriminator value for this object. + * + * @param discriminatorValue The discriminator value for this object. + */ + public void setDiscriminatorValue(String discriminatorValue) { + this.discriminatorValue = discriminatorValue; + } + + /** + * Gets whether this schema represents a flattened schema. + * + * @return Whether this schema represents a flattened schema. + */ + public boolean isFlattenedSchema() { + return flattenedSchema; + } + + /** + * Sets whether this schema represents a flattened schema. + * + * @param flattenedSchema Whether this schema represents a flattened schema. + */ + public void setFlattenedSchema(boolean flattenedSchema) { + this.flattenedSchema = flattenedSchema; + } + + /** + * Gets whether this schema represents a strongly-typed HTTP headers object. + * + * @return Whether this schema represents a strongly-typed HTTP headers object. + */ + public boolean isStronglyTypedHeader() { + return stronglyTypedHeader; + } + + /** + * Sets whether this schema represents a strongly-typed HTTP headers object. + * + * @param stronglyTypedHeader Whether this schema represents a strongly-typed HTTP headers object. + */ + public void setStronglyTypedHeader(boolean stronglyTypedHeader) { + this.stronglyTypedHeader = stronglyTypedHeader; + } + + /** + * Gets the cross-language definition ID for this object. + * + * @return The cross-language definition ID for this object. + */ + public String getCrossLanguageDefinitionId() { + return crossLanguageDefinitionId; + } + + /** + * Sets the cross-language definition ID for this object. + * + * @param crossLanguageDefinitionId The cross-language definition ID for this object. + */ + public void setCrossLanguageDefinitionId(String crossLanguageDefinitionId) { + this.crossLanguageDefinitionId = crossLanguageDefinitionId; + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + return super.writeParentProperties(jsonWriter.writeStartObject()) + .writeJsonField("discriminator", discriminator) + .writeArrayField("properties", properties, JsonWriter::writeJson) + .writeDoubleField("maxProperties", maxProperties) + .writeDoubleField("minProperties", minProperties) + .writeJsonField("parents", parents) + .writeJsonField("children", children) + .writeStringField("discriminatorValue", discriminatorValue) + .writeEndObject(); + } + + /** + * Deserializes an ObjectSchema instance from the JSON data. + * + * @param jsonReader The JSON reader to deserialize from. + * @return An ObjectSchema instance deserialized from the JSON data. + * @throws IOException If an error occurs during deserialization. + */ + public static ObjectSchema fromJson(JsonReader jsonReader) throws IOException { + return JsonUtils.readObject(jsonReader, ObjectSchema::new, (schema, fieldName, reader) -> { + if (schema.tryConsumeParentProperties(schema, fieldName, reader)) { + return; + } + + if ("discriminator".equals(fieldName)) { + schema.discriminator = Discriminator.fromJson(reader); + } else if ("properties".equals(fieldName)) { + schema.properties = reader.readArray(Property::fromJson); + } else if ("maxProperties".equals(fieldName)) { + schema.maxProperties = reader.getDouble(); + } else if ("minProperties".equals(fieldName)) { + schema.minProperties = reader.getDouble(); + } else if ("parents".equals(fieldName)) { + schema.parents = Relations.fromJson(reader); + } else if ("children".equals(fieldName)) { + schema.children = Relations.fromJson(reader); + } else if ("discriminatorValue".equals(fieldName)) { + schema.discriminatorValue = reader.getString(); + } else if ("flattenedSchema".equals(fieldName)) { + schema.flattenedSchema = reader.getBoolean(); + } else if ("stronglyTypedHeader".equals(fieldName)) { + schema.stronglyTypedHeader = reader.getBoolean(); + } else if ("crossLanguageDefinitionId".equals(fieldName)) { + schema.crossLanguageDefinitionId = reader.getString(); + } else { + reader.skipChildren(); + } + }); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/Operation.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/Operation.java new file mode 100644 index 0000000000..f264461d6b --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/Operation.java @@ -0,0 +1,510 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.extension.model.codemodel; + +import com.microsoft.typespec.http.client.generator.core.extension.base.util.JsonUtils; +import com.azure.json.JsonReader; +import com.azure.json.JsonWriter; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * Represents a single callable endpoint with a discrete set of inputs, and any number of output possibilities + * (responses or exceptions). + */ +public class Operation extends Metadata { + private String operationId; + private List parameters = new ArrayList<>(); + private List signatureParameters = new ArrayList<>(); + private List requests; + private List responses = new ArrayList<>(); + private List exceptions = new ArrayList<>(); + private DictionaryApiVersion profile; + private String $key; + private String description; + private String uid; + private String summary; + private List apiVersions = new ArrayList<>(); + private Deprecation deprecated; + private ExternalDocumentation externalDocs; + private List specialHeaders; + private LongRunningMetadata lroMetadata; + private ConvenienceApi convenienceApi; + private Boolean generateProtocolApi; + private Boolean internalApi; + private String crossLanguageDefinitionId; + // internal + private OperationGroup operationGroup; + + /** + * Creates a new instance of the Operation class. + */ + public Operation() { + } + + /** + * Gets the operation ID. + * + * @return The operation ID. + */ + public String getOperationId() { + return operationId; + } + + /** + * Sets the operation ID. + * + * @param operationId The operation ID. + */ + public void setOperationId(String operationId) { + this.operationId = operationId; + } + + /** + * Gets the requests for this operation. (Required) + * + * @return The requests for this operation. + */ + public List getRequests() { + return requests; + } + + /** + * Sets the requests for this operation. (Required) + * + * @param requests The requests for this operation. + */ + public void setRequests(List requests) { + this.requests = requests; + } + + /** + * Gets the responses that indicate a successful call. + * + * @return The responses that indicate a successful call. + */ + public List getResponses() { + return responses; + } + + /** + * Sets the responses that indicate a successful call. + * + * @param responses The responses that indicate a successful call. + */ + public void setResponses(List responses) { + this.responses = responses; + } + + /** + * Gets the responses that indicate a failed call. + * + * @return The responses that indicate a failed call. + */ + public List getExceptions() { + return exceptions; + } + + /** + * Sets the responses that indicate a failed call. + * + * @param exceptions The responses that indicate a failed call. + */ + public void setExceptions(List exceptions) { + this.exceptions = exceptions; + } + + /** + * Gets the profile of the operation. + * + * @return The profile of the operation. + */ + public DictionaryApiVersion getProfile() { + return profile; + } + + /** + * Sets the profile of the operation. + * + * @param profile The profile of the operation. + */ + public void setProfile(DictionaryApiVersion profile) { + this.profile = profile; + } + + /** + * Gets the key of the operation. (Required) + * + * @return The key of the operation. + */ + public String get$key() { + return $key; + } + + /** + * Sets the key of the operation. (Required) + * + * @param $key The key of the operation. + */ + public void set$key(String $key) { + this.$key = $key; + } + + /** + * Gets the description of the operation. (Required) + * + * @return The description of the operation. + */ + public String getDescription() { + return description; + } + + /** + * Sets the description of the operation. (Required) + * + * @param description The description of the operation. + */ + public void setDescription(String description) { + this.description = description; + } + + /** + * Gets the UID of the operation. (Required) + * + * @return The UID of the operation. + */ + public String getUid() { + return uid; + } + + /** + * Sets the UID of the operation. (Required) + * + * @param uid The UID of the operation. + */ + public void setUid(String uid) { + this.uid = uid; + } + + /** + * Gets the summary of the operation. + * + * @return The summary of the operation. + */ + public String getSummary() { + return summary; + } + + /** + * Sets the summary of the operation. + * + * @param summary The summary of the operation. + */ + public void setSummary(String summary) { + this.summary = summary; + } + + /** + * Gets the API versions that this applies to. Undefined means all versions. + * + * @return The API versions that this applies to. Undefined means all versions. + */ + public List getApiVersions() { + return apiVersions; + } + + /** + * Sets the API versions that this applies to. Undefined means all versions. + * + * @param apiVersions The API versions that this applies to. Undefined means all versions. + */ + public void setApiVersions(List apiVersions) { + this.apiVersions = apiVersions; + } + + /** + * Gets the deprecation information for the operation. + * + * @return The deprecation information for the operation. + */ + public Deprecation getDeprecated() { + return deprecated; + } + + /** + * Sets the deprecation information for the operation. + * + * @param deprecated The deprecation information for the operation. + */ + public void setDeprecated(Deprecation deprecated) { + this.deprecated = deprecated; + } + + /** + * Gets a reference to external documentation. + * + * @return A reference to external documentation. + */ + public ExternalDocumentation getExternalDocs() { + return externalDocs; + } + + /** + * Sets a reference to external documentation. + * + * @param externalDocs A reference to external documentation. + */ + public void setExternalDocs(ExternalDocumentation externalDocs) { + this.externalDocs = externalDocs; + } + + /** + * Gets the operation group. + * + * @return The operation group. + */ + public OperationGroup getOperationGroup() { + return operationGroup; + } + + /** + * Sets the operation group. + * + * @param operationGroup The operation group. + */ + public void setOperationGroup(OperationGroup operationGroup) { + this.operationGroup = operationGroup; + } + + /** + * Gets the parameters for this operation. + * + * @return The parameters for this operation. + */ + public List getParameters() { + return parameters; + } + + /** + * Sets the parameters for this operation. + * + * @param parameters The parameters for this operation. + */ + public void setParameters(List parameters) { + this.parameters = parameters; + } + + /** + * Gets the signature parameters for this operation. + * + * @return The signature parameters for this operation. + */ + public List getSignatureParameters() { + return signatureParameters; + } + + /** + * Sets the signature parameters for this operation. + * + * @param signatureParameters The signature parameters for this operation. + */ + public void setSignatureParameters(List signatureParameters) { + this.signatureParameters = signatureParameters; + } + + /** + * Gets headers that require special processing, e.g. Repeatability-Request-ID + * + * @return Headers that require special processing, e.g. Repeatability-Request-ID + */ + public List getSpecialHeaders() { + return specialHeaders; + } + + /** + * Sets headers that require special processing, e.g. Repeatability-Request-ID + * + * @param specialHeaders Headers that require special processing, e.g. Repeatability-Request-ID + */ + public void setSpecialHeaders(List specialHeaders) { + this.specialHeaders = specialHeaders; + } + + /** + * Gets the metadata for long-running operations. + * + * @return The metadata for long-running operations. + */ + public LongRunningMetadata getLroMetadata() { + return lroMetadata; + } + + /** + * Sets the metadata for long-running operations. + * + * @param lroMetadata The metadata for long-running operations. + */ + public void setLroMetadata(LongRunningMetadata lroMetadata) { + this.lroMetadata = lroMetadata; + } + + /** + * Gets the convenience API. + * + * @return The convenience API. + */ + public ConvenienceApi getConvenienceApi() { + return convenienceApi; + } + + /** + * Sets the convenience API. + * + * @param convenienceApi The convenience API. + */ + public void setConvenienceApi(ConvenienceApi convenienceApi) { + this.convenienceApi = convenienceApi; + } + + /** + * Gets whether to generate protocol API. + * + * @return Whether to generate protocol API. + */ + public Boolean getGenerateProtocolApi() { + return generateProtocolApi; + } + + /** + * Sets whether to generate protocol API. + * + * @param generateProtocolApi Whether to generate protocol API. + */ + public void setGenerateProtocolApi(Boolean generateProtocolApi) { + this.generateProtocolApi = generateProtocolApi; + } + + /** + * Gets whether this is an internal API. + * + * @return Whether this is an internal API. + */ + public Boolean getInternalApi() { + return internalApi; + } + + /** + * Sets whether this is an internal API. + * + * @param internalApi Whether this is an internal API. + */ + public void setInternalApi(Boolean internalApi) { + this.internalApi = internalApi; + } + + /** + * Gets the cross-language definition ID. + * + * @return The cross-language definition ID. + */ + public String getCrossLanguageDefinitionId() { + return crossLanguageDefinitionId; + } + + /** + * Sets the cross-language definition ID. + * + * @param crossLanguageDefinitionId The cross-language definition ID. + */ + public void setCrossLanguageDefinitionId(String crossLanguageDefinitionId) { + this.crossLanguageDefinitionId = crossLanguageDefinitionId; + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + return super.writeParentProperties(jsonWriter.writeStartObject()) + .writeStringField("operationId", operationId) + .writeArrayField("parameters", parameters, JsonWriter::writeJson) + .writeArrayField("signatureParameters", signatureParameters, JsonWriter::writeJson) + .writeArrayField("requests", requests, JsonWriter::writeJson) + .writeArrayField("responses", responses, JsonWriter::writeJson) + .writeArrayField("exceptions", exceptions, JsonWriter::writeJson) + .writeJsonField("profile", profile) + .writeStringField("$key", $key) + .writeStringField("description", description) + .writeStringField("uid", uid) + .writeStringField("summary", summary) + .writeArrayField("apiVersions", apiVersions, JsonWriter::writeJson) + .writeJsonField("deprecated", deprecated) + .writeJsonField("externalDocs", externalDocs) + .writeArrayField("specialHeaders", specialHeaders, JsonWriter::writeString) + .writeJsonField("lroMetadata", lroMetadata) + .writeJsonField("convenienceApi", convenienceApi) + .writeBooleanField("generateProtocolApi", generateProtocolApi) + .writeBooleanField("internalApi", internalApi) + .writeStringField("crossLanguageDefinitionId", crossLanguageDefinitionId) + .writeEndObject(); + } + + /** + * Deserializes an Operation instance from the JSON data. + * + * @param jsonReader The JSON reader to deserialize from. + * @return An Operation instance deserialized from the JSON data. + * @throws IOException If an error occurs during deserialization. + */ + public static Operation fromJson(JsonReader jsonReader) throws IOException { + return JsonUtils.readObject(jsonReader, Operation::new, (operation, fieldName, reader) -> { + if (operation.tryConsumeParentProperties(operation, fieldName, reader)) { + return; + } + + if ("operationId".equals(fieldName)) { + operation.operationId = reader.getString(); + } else if ("parameters".equals(fieldName)) { + operation.parameters = reader.readArray(Parameter::fromJson); + } else if ("signatureParameters".equals(fieldName)) { + operation.signatureParameters = reader.readArray(Parameter::fromJson); + } else if ("requests".equals(fieldName)) { + operation.requests = reader.readArray(Request::fromJson); + } else if ("responses".equals(fieldName)) { + operation.responses = reader.readArray(Response::fromJson); + } else if ("exceptions".equals(fieldName)) { + operation.exceptions = reader.readArray(Response::fromJson); + } else if ("profile".equals(fieldName)) { + operation.profile = DictionaryApiVersion.fromJson(reader); + } else if ("$key".equals(fieldName)) { + operation.$key = reader.getString(); + } else if ("description".equals(fieldName)) { + operation.description = reader.getString(); + } else if ("uid".equals(fieldName)) { + operation.uid = reader.getString(); + } else if ("summary".equals(fieldName)) { + operation.summary = reader.getString(); + } else if ("apiVersions".equals(fieldName)) { + operation.apiVersions = reader.readArray(ApiVersion::fromJson); + } else if ("deprecated".equals(fieldName)) { + operation.deprecated = Deprecation.fromJson(reader); + } else if ("externalDocs".equals(fieldName)) { + operation.externalDocs = ExternalDocumentation.fromJson(reader); + } else if ("specialHeaders".equals(fieldName)) { + operation.specialHeaders = reader.readArray(JsonReader::getString); + } else if ("lroMetadata".equals(fieldName)) { + operation.lroMetadata = LongRunningMetadata.fromJson(reader); + } else if ("convenienceApi".equals(fieldName)) { + operation.convenienceApi = ConvenienceApi.fromJson(reader); + } else if ("generateProtocolApi".equals(fieldName)) { + operation.generateProtocolApi = reader.getBoolean(); + } else if ("internalApi".equals(fieldName)) { + operation.internalApi = reader.getBoolean(); + } else if ("crossLanguageDefinitionId".equals(fieldName)) { + operation.crossLanguageDefinitionId = reader.getString(); + } else { + reader.skipChildren(); + } + }); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/OperationGroup.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/OperationGroup.java new file mode 100644 index 0000000000..473f79d3dc --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/OperationGroup.java @@ -0,0 +1,115 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.extension.model.codemodel; + +import com.microsoft.typespec.http.client.generator.core.extension.base.util.JsonUtils; +import com.azure.json.JsonReader; +import com.azure.json.JsonWriter; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * Represents an operation group, a container around set of operations. + */ +public class OperationGroup extends Metadata { + private String $key; + private List operations = new ArrayList(); + private Client codeModel; + + /** + * Creates a new instance of the OperationGroup class. + */ + public OperationGroup() { + } + + /** + * Gets the key of the operation group. (Required) + * + * @return The key of the operation group. + */ + public String get$key() { + return $key; + } + + /** + * Sets the key of the operation group. (Required) + * + * @param $key The key of the operation group. + */ + public void set$key(String $key) { + this.$key = $key; + } + + /** + * Gets the operations that are in this operation group. (Required) + * + * @return The operations that are in this operation group. + */ + public List getOperations() { + return operations; + } + + /** + * Sets the operations that are in this operation group. (Required) + * + * @param operations The operations that are in this operation group. + */ + public void setOperations(List operations) { + this.operations = operations; + } + + /** + * Gets the client which contains the operation group. + * + * @return The client which contains the operation group. + */ + public Client getCodeModel() { + return codeModel; + } + + /** + * Sets the client which contains the operation group. + * + * @param codeModel The client which contains the operation group. + */ + public void setCodeModel(Client codeModel) { + this.codeModel = codeModel; + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + return super.writeParentProperties(jsonWriter.writeStartObject()) + .writeStringField("$key", $key) + .writeArrayField("operations", operations, JsonWriter::writeJson) + .writeJsonField("codeModel", codeModel) + .writeEndObject(); + } + + /** + * Deserializes an OperationGroup instance from the JSON data. + * + * @param jsonReader The JSON reader to deserialize from. + * @return An OperationGroup instance deserialized from the JSON data. + * @throws IOException If an error occurs during deserialization. + */ + public static OperationGroup fromJson(JsonReader jsonReader) throws IOException { + return JsonUtils.readObject(jsonReader, OperationGroup::new, (group, fieldName, reader) -> { + if (group.tryConsumeParentProperties(group, fieldName, reader)) { + return; + } + + if ("$key".equals(fieldName)) { + group.$key = reader.getString(); + } else if ("operations".equals(fieldName)) { + group.operations = reader.readArray(Operation::fromJson); + } else if ("codeModel".equals(fieldName)) { + group.codeModel = CodeModel.fromJson(reader); + } else { + reader.skipChildren(); + } + }); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/OrSchema.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/OrSchema.java new file mode 100644 index 0000000000..2a18517cb7 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/OrSchema.java @@ -0,0 +1,93 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.extension.model.codemodel; + +import com.microsoft.typespec.http.client.generator.core.extension.base.util.JsonUtils; +import com.azure.json.JsonReader; +import com.azure.json.JsonWriter; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + * Represents an OR relationship between several schemas/ + */ +public class OrSchema extends ComplexSchema { + private List anyOf = new ArrayList<>(); + + /** + * Creates a new instance of the OrSchema class. + */ + public OrSchema() { + } + + /** + * Gets the set of schemas that this schema is composed of. Every schema is optional. (Required) + * + * @return The set of schemas that this schema is composed of. Every schema is optional. + */ + public List getAnyOf() { + return anyOf; + } + + /** + * Sets the set of schemas that this schema is composed of. Every schema is optional. (Required) + * + * @param anyOf The set of schemas that this schema is composed of. Every schema is optional. + */ + public void setAnyOf(List anyOf) { + this.anyOf = anyOf; + } + + @Override + public String toString() { + return OrSchema.class.getName() + "@" + Integer.toHexString(System.identityHashCode(this)) + "[anyOf=" + + Objects.toString(anyOf, "") + "]"; + } + + @Override + public int hashCode() { + return Objects.hash(anyOf); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof OrSchema)) { + return false; + } + + OrSchema rhs = (OrSchema) other; + return Objects.equals(anyOf, rhs.anyOf); + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + return jsonWriter.writeStartObject() + .writeArrayField("anyOf", anyOf, JsonWriter::writeJson) + .writeEndObject(); + } + + /** + * Deserializes an OrSchema instance from the JSON data. + * + * @param jsonReader The JSON reader to deserialize from. + * @return An OrSchema instance deserialized from the JSON data. + * @throws IOException If an error occurs during deserialization. + */ + public static OrSchema fromJson(JsonReader jsonReader) throws IOException { + return JsonUtils.readObject(jsonReader, OrSchema::new, (schema, fieldName, reader) -> { + if ("anyOf".equals(fieldName)) { + schema.anyOf = reader.readArray(ObjectSchema::fromJson); + } else { + reader.skipChildren(); + } + }); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/Parameter.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/Parameter.java new file mode 100644 index 0000000000..68f5f44727 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/Parameter.java @@ -0,0 +1,295 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.extension.model.codemodel; + +import com.microsoft.typespec.http.client.generator.core.extension.base.util.JsonUtils; +import com.azure.json.JsonReader; +import com.azure.json.JsonWriter; + +import java.io.IOException; + +/** + * Represents a discrete input for an operation. + */ +public class Parameter extends Value { + private String clientDefaultValue; + private Parameter.ImplementationLocation implementation; + private Operation operation; + private boolean flattened = false; + private Parameter originalParameter; + private Parameter groupedBy; + private Property targetProperty; + private String origin; + private String summary; + + /** + * Creates a new instance of the Parameter class. + */ + public Parameter() { + } + + /** + * Gets the default value for the parameter in the client. + * + * @return The default value for the parameter in the client. + */ + public String getClientDefaultValue() { + return clientDefaultValue; + } + + /** + * Sets the default value for the parameter in the client. + * + * @param clientDefaultValue The default value for the parameter in the client. + */ + public void setClientDefaultValue(String clientDefaultValue) { + this.clientDefaultValue = clientDefaultValue; + } + + /** + * Gets the location of the parameter's implementation. + * + * @return The location of the parameter's implementation. + */ + public Parameter.ImplementationLocation getImplementation() { + return implementation; + } + + /** + * Sets the location of the parameter's implementation. + * + * @param implementation The location of the parameter's implementation. + */ + public void setImplementation(Parameter.ImplementationLocation implementation) { + this.implementation = implementation; + } + + /** + * Gets the operation that this parameter is used in. + * + * @return The operation that this parameter is used in. + */ + public Operation getOperation() { + return operation; + } + + /** + * Sets the operation that this parameter is used in. + * + * @param operation The operation that this parameter is used in. + */ + public void setOperation(Operation operation) { + this.operation = operation; + } + + /** + * Gets whether the parameter is flattened. + * + * @return Whether the parameter is flattened. + */ + public boolean isFlattened() { + return flattened; + } + + /** + * Sets whether the parameter is flattened. + * + * @param hidden Whether the parameter is flattened. + */ + public void setFlattened(boolean hidden) { + this.flattened = hidden; + } + + /** + * Gets the original parameter that this parameter is derived from. + * + * @return The original parameter that this parameter is derived from. + */ + public Parameter getOriginalParameter() { + return originalParameter; + } + + /** + * Sets the original parameter that this parameter is derived from. + * + * @param originalParameter The original parameter that this parameter is derived from. + */ + public void setOriginalParameter(Parameter originalParameter) { + this.originalParameter = originalParameter; + } + + /** + * Gets the property that this parameter is targeting. + * + * @return The property that this parameter is targeting. + */ + public Property getTargetProperty() { + return targetProperty; + } + + /** + * Sets the property that this parameter is targeting. + * + * @param targetProperty The property that this parameter is targeting. + */ + public void setTargetProperty(Property targetProperty) { + this.targetProperty = targetProperty; + } + + /** + * Gets the parameter that this parameter is grouped by. + * + * @return The parameter that this parameter is grouped by. + */ + public Parameter getGroupedBy() { + return groupedBy; + } + + /** + * Sets the parameter that this parameter is grouped by. + * + * @param groupedBy The parameter that this parameter is grouped by. + */ + public void setGroupedBy(Parameter groupedBy) { + this.groupedBy = groupedBy; + } + + /** + * The origin of the parameter. + * + * @return The origin of the parameter. + */ + public String getOrigin() { + return origin; + } + + /** + * Sets the origin of the parameter. + * + * @param origin The origin of the parameter. + */ + public void setOrigin(String origin) { + this.origin = origin; + } + + @Override + public String getSummary() { + return summary; + } + + @Override + public void setSummary(String summary) { + this.summary = summary; + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + return super.writeParentProperties(jsonWriter.writeStartObject()) + .writeStringField("clientDefaultValue", clientDefaultValue) + .writeStringField("implementation", implementation == null ? null : implementation.toString()) + .writeJsonField("operation", operation) + .writeBooleanField("flattened", flattened) + .writeJsonField("originalParameter", originalParameter) + .writeJsonField("groupedBy", groupedBy) + .writeJsonField("targetProperty", targetProperty) + .writeStringField("origin", origin) + .writeStringField("summary", summary) + .writeEndObject(); + } + + /** + * Deserializes a Parameter instance from the JSON data. + * + * @param jsonReader The JSON reader to deserialize from. + * @return A Parameter instance deserialized from the JSON data. + * @throws IOException If an error occurs during deserialization. + */ + public static Parameter fromJson(JsonReader jsonReader) throws IOException { + return JsonUtils.readObject(jsonReader, Parameter::new, (parameter, fieldName, reader) -> { + if (parameter.tryConsumeParentProperties(parameter, fieldName, reader)) { + return; + } + + if ("clientDefaultValue".equals(fieldName)) { + parameter.clientDefaultValue = reader.getString(); + } else if ("implementation".equals(fieldName)) { + parameter.implementation = ImplementationLocation.fromValue(reader.getString()); + } else if ("operation".equals(fieldName)) { + parameter.operation = Operation.fromJson(reader); + } else if ("flattened".equals(fieldName)) { + parameter.flattened = reader.getBoolean(); + } else if ("originalParameter".equals(fieldName)) { + parameter.originalParameter = Parameter.fromJson(reader); + } else if ("groupedBy".equals(fieldName)) { + parameter.groupedBy = Parameter.fromJson(reader); + } else if ("targetProperty".equals(fieldName)) { + parameter.targetProperty = Property.fromJson(reader); + } else if ("origin".equals(fieldName)) { + parameter.origin = reader.getString(); + } else if ("summary".equals(fieldName)) { + parameter.summary = reader.getString(); + } else { + reader.skipChildren(); + } + }); + } + + /** + * The location of the parameter's implementation. + */ + public enum ImplementationLocation { + /** + * The parameter is implemented in the client. + */ + CLIENT("Client"), + + /** + * The parameter is implemented in the context. + */ + CONTEXT("Context"), + + /** + * The parameter is implemented in the method. + */ + METHOD("Method"); + private final String value; + + ImplementationLocation(String value) { + this.value = value; + } + + @Override + public String toString() { + return this.value; + } + + /** + * The value of the location. + * + * @return The value of the location. + */ + public String value() { + return this.value; + } + + /** + * Parses a value to a location. + * + * @param value The value to parse. + * @return The parsed location. + * @throws IllegalArgumentException thrown if the value does not match any of the known locations. + */ + public static Parameter.ImplementationLocation fromValue(String value) { + if ("Client".equals(value)) { + return CLIENT; + } else if ("Context".equals(value)) { + return CONTEXT; + } else if ("Method".equals(value)) { + return METHOD; + } else { + throw new IllegalArgumentException(value); + } + } + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/ParameterGroupSchema.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/ParameterGroupSchema.java new file mode 100644 index 0000000000..140aa05e7b --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/ParameterGroupSchema.java @@ -0,0 +1,93 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.extension.model.codemodel; + +import com.microsoft.typespec.http.client.generator.core.extension.base.util.JsonUtils; +import com.azure.json.JsonReader; +import com.azure.json.JsonWriter; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + * Represents a set of parameters. + */ +public class ParameterGroupSchema extends ComplexSchema { + private List parameters = new ArrayList<>(); + + /** + * Creates a new instance of the ParameterGroupSchema class. + */ + public ParameterGroupSchema() { + } + + /** + * Gets the collection of properties that are in this object. (Required) + * + * @return The collection of properties that are in this object. + */ + public List getParameters() { + return parameters; + } + + /** + * Sets the collection of properties that are in this object. (Required) + * + * @param parameters The collection of properties that are in this object. + */ + public void setParameters(List parameters) { + this.parameters = parameters; + } + + @Override + public String toString() { + return ParameterGroupSchema.class.getName() + '@' + Integer.toHexString(System.identityHashCode(this)) + + "[parameters=" + Objects.toString(parameters, "") + "]"; + } + + @Override + public int hashCode() { + return Objects.hashCode(parameters); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof ParameterGroupSchema)) { + return false; + } + + ParameterGroupSchema rhs = ((ParameterGroupSchema) other); + return Objects.equals(parameters, rhs.parameters); + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + return jsonWriter.writeStartObject() + .writeArrayField("parameters", parameters, JsonWriter::writeJson) + .writeEndObject(); + } + + /** + * Deserializes a ParameterGroupSchema instance from the JSON data. + * + * @param jsonReader The JSON reader to deserialize from. + * @return A ParameterGroupSchema instance deserialized from the JSON data. + * @throws IOException If an error occurs during deserialization. + */ + public static ParameterGroupSchema fromJson(JsonReader jsonReader) throws IOException { + return JsonUtils.readObject(jsonReader, ParameterGroupSchema::new, (relations, fieldName, reader) -> { + if ("parameters".equals(fieldName)) { + relations.parameters = reader.readArray(Parameter::fromJson); + } else { + reader.skipChildren(); + } + }); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/PrimitiveSchema.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/PrimitiveSchema.java new file mode 100644 index 0000000000..f044c14bf5 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/PrimitiveSchema.java @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.extension.model.codemodel; + +import com.microsoft.typespec.http.client.generator.core.extension.base.util.JsonUtils; +import com.azure.json.JsonReader; +import com.azure.json.JsonWriter; + +import java.io.IOException; + +/** + * Schema types that are primitive language values + */ +public class PrimitiveSchema extends ValueSchema { + /** + * Creates a new instance of the PrimitiveSchema class. + */ + public PrimitiveSchema() { + super(); + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + return super.toJson(jsonWriter); + } + + /** + * Deserializes a PrimitiveSchema instance from the JSON data. + * + * @param jsonReader The JSON reader to deserialize from. + * @return A PrimitiveSchema instance deserialized from the JSON data. + * @throws IOException If an error occurs during deserialization. + */ + public static PrimitiveSchema fromJson(JsonReader jsonReader) throws IOException { + return JsonUtils.readObject(jsonReader, PrimitiveSchema::new, (schema, fieldName, reader) -> { + if (!schema.tryConsumeParentProperties(schema, fieldName, reader)) { + reader.skipChildren(); + } + }); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/Property.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/Property.java new file mode 100644 index 0000000000..f5b41d15f9 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/Property.java @@ -0,0 +1,214 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.extension.model.codemodel; + +import com.microsoft.typespec.http.client.generator.core.extension.base.util.JsonUtils; +import com.azure.json.JsonReader; +import com.azure.json.JsonWriter; + +import java.io.IOException; +import java.util.List; + +/** + * Represents a property that is a child value in an object. + */ +public class Property extends Value { + private boolean readOnly; + private String serializedName; + private boolean isDiscriminator; + private List flattenedNames; + private List originalParameter; + private String clientDefaultValue; + private String summary; + // internal use, not from modelerfour + private ObjectSchema parentSchema; + + /** + * Creates a new instance of the Property class. + */ + public Property() { + } + + /** + * Gets whether the property is read-only. + * + * @return Whether the property is read-only. + */ + public boolean isReadOnly() { + return readOnly; + } + + /** + * Sets whether the property is read-only. + * + * @param readOnly Whether the property is read-only. + */ + public void setReadOnly(boolean readOnly) { + this.readOnly = readOnly; + } + + /** + * Gets the wire name of this property. (Required) + * + * @return The wire name of this property. + */ + public String getSerializedName() { + return serializedName; + } + + /** + * Sets the wire name of this property. (Required) + * + * @param serializedName The wire name of this property. + */ + public void setSerializedName(String serializedName) { + this.serializedName = serializedName; + } + + /** + * Gets whether the property is used as a discriminator for a polymorphic type. + * + * @return Whether the property is used as a discriminator for a polymorphic type. + */ + public boolean isIsDiscriminator() { + return isDiscriminator; + } + + /** + * Sets whether the property is used as a discriminator for a polymorphic type. + * + * @param isDiscriminator Whether the property is used as a discriminator for a polymorphic type. + */ + public void setIsDiscriminator(boolean isDiscriminator) { + this.isDiscriminator = isDiscriminator; + } + + /** + * Gets the flattened names of this property. + * + * @return The flattened names of this property. + */ + public List getFlattenedNames() { + return flattenedNames; + } + + /** + * Sets the flattened names of this property. + * + * @param flattenedNames The flattened names of this property. + */ + public void setFlattenedNames(List flattenedNames) { + this.flattenedNames = flattenedNames; + } + + /** + * Gets the parent schema of this property. + * + * @return The parent schema of this property. + */ + public ObjectSchema getParentSchema() { + return parentSchema; + } + + /** + * Sets the parent schema of this property. + * + * @param parentSchema The parent schema of this property. + */ + public void setParentSchema(ObjectSchema parentSchema) { + this.parentSchema = parentSchema; + } + + /** + * Gets the original parameter that this property is derived from. + * + * @return The original parameter that this property is derived from. + */ + public List getOriginalParameter() { + return originalParameter; + } + + /** + * Sets the original parameter that this property is derived from. + * + * @param originalParameter The original parameter that this property is derived from. + */ + public void setOriginalParameter(List originalParameter) { + this.originalParameter = originalParameter; + } + + /** + * Gets the client default value of this property. + * + * @return The client default value of this property. + */ + public String getClientDefaultValue() { + return clientDefaultValue; + } + + /** + * Sets the client default value of this property. + * + * @param clientDefaultValue The client default value of this property. + */ + public void setClientDefaultValue(String clientDefaultValue) { + this.clientDefaultValue = clientDefaultValue; + } + + @Override + public String getSummary() { + return summary; + } + + @Override + public void setSummary(String summary) { + this.summary = summary; + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + return super.writeParentProperties(jsonWriter.writeStartObject()) + .writeBooleanField("readOnly", readOnly) + .writeStringField("serializedName", serializedName) + .writeBooleanField("isDiscriminator", isDiscriminator) + .writeArrayField("flattenedNames", flattenedNames, JsonWriter::writeString) + .writeArrayField("originalParameter", originalParameter, JsonWriter::writeJson) + .writeStringField("clientDefaultValue", clientDefaultValue) + .writeStringField("summary", summary) + .writeEndObject(); + } + + /** + * Deserializes a Property instance from the JSON data. + * + * @param jsonReader The JSON reader to deserialize from. + * @return A Property instance deserialized from the JSON data. + * @throws IOException If an error occurs during deserialization. + */ + public static Property fromJson(JsonReader jsonReader) throws IOException { + return JsonUtils.readObject(jsonReader, Property::new, (property, fieldName, reader) -> { + if (property.tryConsumeParentProperties(property, fieldName, reader)) { + return; + } + + if ("readOnly".equals(fieldName)) { + property.readOnly = reader.getBoolean(); + } else if ("serializedName".equals(fieldName)) { + property.serializedName = reader.getString(); + } else if ("isDiscriminator".equals(fieldName)) { + property.isDiscriminator = reader.getBoolean(); + } else if ("flattenedNames".equals(fieldName)) { + property.flattenedNames = reader.readArray(JsonReader::getString); + } else if ("originalParameter".equals(fieldName)) { + property.originalParameter = reader.readArray(Parameter::fromJson); + } else if ("clientDefaultValue".equals(fieldName)) { + property.clientDefaultValue = reader.getString(); + } else if ("summary".equals(fieldName)) { + property.summary = reader.getString(); + } else { + reader.skipChildren(); + } + }); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/Protocol.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/Protocol.java new file mode 100644 index 0000000000..fc2e0eaccb --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/Protocol.java @@ -0,0 +1,287 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.extension.model.codemodel; + +import com.microsoft.typespec.http.client.generator.core.extension.base.util.JsonUtils; +import com.azure.json.JsonReader; +import com.azure.json.JsonSerializable; +import com.azure.json.JsonWriter; + +import java.io.IOException; +import java.util.List; + +/** + * Represents the per-protocol metadata on a given aspect. + */ +public class Protocol implements JsonSerializable { + private RequestParameterLocation in; + private String path; + private String uri; + private String method; + private KnownMediaType knownMediaType; + private SerializationStyle style; + private boolean explode; + private List mediaTypes; + private List servers; + private List statusCodes; + private List
headers; + + /** + * Creates a new instance of the Protocol class. + */ + public Protocol() { + } + + /** + * Gets the location of the parameter. + * + * @return The location of the parameter. + */ + public RequestParameterLocation getIn() { + return in; + } + + /** + * Sets the location of the parameter. + * + * @param in The location of the parameter. + */ + public void setIn(RequestParameterLocation in) { + this.in = in; + } + + /** + * Gets the path of the protocol. + * + * @return The path of the protocol. + */ + public String getPath() { + return path; + } + + /** + * Sets the path of the protocol. + * + * @param path The path of the protocol. + */ + public void setPath(String path) { + this.path = path; + } + + /** + * Gets the method of the protocol. + * + * @return The method of the protocol. + */ + public String getMethod() { + return method; + } + + /** + * Sets the method of the protocol. + * + * @param method The method of the protocol. + */ + public void setMethod(String method) { + this.method = method; + } + + /** + * Gets the known media type of the protocol. + * + * @return The known media type of the protocol. + */ + public KnownMediaType getKnownMediaType() { + return knownMediaType; + } + + /** + * Sets the known media type of the protocol. + * + * @param knownMediaType The known media type of the protocol. + */ + public void setKnownMediaType(KnownMediaType knownMediaType) { + this.knownMediaType = knownMediaType; + } + + /** + * Gets the servers of the protocol. + * + * @return The servers of the protocol. + */ + public List getServers() { + return servers; + } + + /** + * Sets the servers of the protocol. + * + * @param servers The servers of the protocol. + */ + public void setServers(List servers) { + this.servers = servers; + } + + /** + * Gets the media types of the protocol. + * + * @return The media types of the protocol. + */ + public List getMediaTypes() { + return mediaTypes; + } + + /** + * Sets the media types of the protocol. + * + * @param mediaTypes The media types of the protocol. + */ + public void setMediaTypes(List mediaTypes) { + this.mediaTypes = mediaTypes; + } + + /** + * Gets the status codes of the protocol. + * + * @return The status codes of the protocol. + */ + public List getStatusCodes() { + return statusCodes; + } + + /** + * Sets the status codes of the protocol. + * + * @param statusCodes The status codes of the protocol. + */ + public void setStatusCodes(List statusCodes) { + this.statusCodes = statusCodes; + } + + /** + * Gets the headers of the protocol. + * + * @return The headers of the protocol. + */ + public List
getHeaders() { + return headers; + } + + /** + * Sets the headers of the protocol. + * + * @param headers The headers of the protocol. + */ + public void setHeaders(List
headers) { + this.headers = headers; + } + + /** + * Gets the URI of the protocol. + * + * @return The URI of the protocol. + */ + public String getUri() { + return uri; + } + + /** + * Sets the URI of the protocol. + * + * @param uri The URI of the protocol. + */ + public void setUri(String uri) { + this.uri = uri; + } + + /** + * Gets the serialization style of the protocol. + * + * @return The serialization style of the protocol. + */ + public SerializationStyle getStyle() { + return style; + } + + /** + * Sets the serialization style of the protocol. + * + * @param style The serialization style of the protocol. + */ + public void setStyle(SerializationStyle style) { + this.style = style; + } + + /** + * Gets whether the protocol should be exploded. + * + * @return Whether the protocol should be exploded. + */ + public boolean getExplode() { + return explode; + } + + /** + * Sets whether the protocol should be exploded. + * + * @param explode Whether the protocol should be exploded. + */ + public void setExplode(boolean explode) { + this.explode = explode; + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + return jsonWriter.writeStartObject() + .writeStringField("in", in == null ? null : in.toString()) + .writeStringField("path", path) + .writeStringField("uri", uri) + .writeStringField("method", method) + .writeStringField("knownMediaType", knownMediaType == null ? null : knownMediaType.toString()) + .writeStringField("style", style == null ? null : style.toString()) + .writeBooleanField("explode", explode) + .writeArrayField("mediaTypes", mediaTypes, JsonWriter::writeString) + .writeArrayField("servers", servers, JsonWriter::writeJson) + .writeArrayField("statusCodes", statusCodes, JsonWriter::writeString) + .writeArrayField("headers", headers, JsonWriter::writeJson) + .writeEndObject(); + } + + /** + * Deserializes a Protocol instance from the JSON data. + * + * @param jsonReader The JSON reader to deserialize from. + * @return A Protocol instance deserialized from the JSON data. + * @throws IOException If an error occurs during deserialization. + */ + public static Protocol fromJson(JsonReader jsonReader) throws IOException { + return JsonUtils.readObject(jsonReader, Protocol::new, (protocol, fieldName, reader) -> { + if ("in".equals(fieldName)) { + protocol.in = RequestParameterLocation.fromValue(reader.getString()); + } else if ("path".equals(fieldName)) { + protocol.path = reader.getString(); + } else if ("uri".equals(fieldName)) { + protocol.uri = reader.getString(); + } else if ("method".equals(fieldName)) { + protocol.method = reader.getString(); + } else if ("knownMediaType".equals(fieldName)) { + protocol.knownMediaType = KnownMediaType.fromValue(reader.getString()); + } else if ("style".equals(fieldName)) { + protocol.style = SerializationStyle.fromValue(reader.getString()); + } else if ("explode".equals(fieldName)) { + protocol.explode = reader.getBoolean(); + } else if ("mediaTypes".equals(fieldName)) { + protocol.mediaTypes = reader.readArray(JsonReader::getString); + } else if ("servers".equals(fieldName)) { + protocol.servers = reader.readArray(Server::fromJson); + } else if ("statusCodes".equals(fieldName)) { + protocol.statusCodes = reader.readArray(JsonReader::getString); + } else if ("headers".equals(fieldName)) { + protocol.headers = reader.readArray(Header::fromJson); + } else { + reader.skipChildren(); + } + }); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/Protocols.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/Protocols.java new file mode 100644 index 0000000000..325a73e2cf --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/Protocols.java @@ -0,0 +1,160 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.extension.model.codemodel; + +import com.microsoft.typespec.http.client.generator.core.extension.base.util.JsonUtils; +import com.azure.json.JsonReader; +import com.azure.json.JsonSerializable; +import com.azure.json.JsonWriter; + +import java.io.IOException; +import java.util.Objects; + +/** + * Represents custom extensible metadata for individual protocols (ie, HTTP, etc), + */ +public class Protocols implements JsonSerializable { + private Protocol http; + private Protocol amqp; + private Protocol mqtt; + private Protocol jsonrpc; + + /** + * Creates a new instance of the Protocols class. + */ + public Protocols() { + } + + /** + * Gets the HTTP protocol. + * + * @return The HTTP protocol. + */ + public Protocol getHttp() { + return http; + } + + /** + * Sets the HTTP protocol. + * + * @param http The HTTP protocol. + */ + public void setHttp(Protocol http) { + this.http = http; + } + + /** + * Gets the AMQP protocol. + * + * @return The AMQP protocol. + */ + public Protocol getAmqp() { + return amqp; + } + + /** + * Sets the AMQP protocol. + * + * @param amqp The AMQP protocol. + */ + public void setAmqp(Protocol amqp) { + this.amqp = amqp; + } + + /** + * Gets the MQTT protocol. + * + * @return The MQTT protocol. + */ + public Protocol getMqtt() { + return mqtt; + } + + /** + * Sets the MQTT protocol. + * + * @param mqtt The MQTT protocol. + */ + public void setMqtt(Protocol mqtt) { + this.mqtt = mqtt; + } + + /** + * Gets the JSON-RPC protocol. + * + * @return The JSON-RPC protocol. + */ + public Protocol getJsonrpc() { + return jsonrpc; + } + + /** + * Sets the JSON-RPC protocol. + * + * @param jsonrpc The JSON-RPC protocol. + */ + public void setJsonrpc(Protocol jsonrpc) { + this.jsonrpc = jsonrpc; + } + + @Override + public String toString() { + return Protocols.class.getName() + "@" + Integer.toHexString(System.identityHashCode(this)) + "[http=" + + Objects.toString(http, "") + ",amqp=" + Objects.toString(amqp, "") + ",mqtt=" + + Objects.toString(mqtt, "") + ",jsonrpc=" + Objects.toString(jsonrpc, "") + "]"; + } + + @Override + public int hashCode() { + return Objects.hash(http, jsonrpc, amqp, mqtt); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof Protocols)) { + return false; + } + + Protocols rhs = ((Protocols) other); + return Objects.equals(http, rhs.http) && Objects.equals(jsonrpc, rhs.jsonrpc) && Objects.equals(amqp, rhs.amqp) + && Objects.equals(mqtt, rhs.mqtt); + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + return jsonWriter.writeStartObject() + .writeJsonField("http", http) + .writeJsonField("amqp", amqp) + .writeJsonField("mqtt", mqtt) + .writeJsonField("jsonrpc", jsonrpc) + .writeEndObject(); + } + + /** + * Deserializes a Protocols instance from the JSON data. + * + * @param jsonReader The JSON reader to deserialize from. + * @return A Protocols instance deserialized from the JSON data. + * @throws IOException If an error occurs during deserialization. + */ + public static Protocols fromJson(JsonReader jsonReader) throws IOException { + return JsonUtils.readObject(jsonReader, Protocols::new, (protocols, fieldName, reader) -> { + if ("http".equals(fieldName)) { + protocols.http = Protocol.fromJson(reader); + } else if ("amqp".equals(fieldName)) { + protocols.amqp = Protocol.fromJson(reader); + } else if ("mqtt".equals(fieldName)) { + protocols.mqtt = Protocol.fromJson(reader); + } else if ("jsonrpc".equals(fieldName)) { + protocols.jsonrpc = Protocol.fromJson(reader); + } else { + reader.skipChildren(); + } + }); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/Relations.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/Relations.java new file mode 100644 index 0000000000..eb3de13289 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/Relations.java @@ -0,0 +1,89 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.extension.model.codemodel; + +import com.microsoft.typespec.http.client.generator.core.extension.base.util.JsonUtils; +import com.azure.json.JsonReader; +import com.azure.json.JsonSerializable; +import com.azure.json.JsonWriter; + +import java.io.IOException; +import java.util.List; + +/** + * Represents relations between schemas. + */ +public class Relations implements JsonSerializable { + private List all; + private List immediate; + + /** + * Creates a new instance of the Relations class. + */ + public Relations() { + } + + /** + * Gets all schemas. + * + * @return The all schemas. + */ + public List getAll() { + return all; + } + + /** + * Sets all schemas. + * + * @param all The all schemas. + */ + public void setAll(List all) { + this.all = all; + } + + /** + * Gets immediate schemas. + * + * @return The immediate schemas. + */ + public List getImmediate() { + return immediate; + } + + /** + * Sets immediate schemas. + * + * @param immediate The immediate schemas. + */ + public void setImmediate(List immediate) { + this.immediate = immediate; + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + return jsonWriter.writeStartObject() + .writeArrayField("all", all, JsonWriter::writeJson) + .writeArrayField("immediate", immediate, JsonWriter::writeJson) + .writeEndObject(); + } + + /** + * Deserializes a Relations instance from the JSON data. + * + * @param jsonReader The JSON reader to deserialize from. + * @return A Relations instance deserialized from the JSON data. + * @throws IOException If an error occurs during deserialization. + */ + public static Relations fromJson(JsonReader jsonReader) throws IOException { + return JsonUtils.readObject(jsonReader, Relations::new, (relations, fieldName, reader) -> { + if ("all".equals(fieldName)) { + relations.all = reader.readArray(Schema::fromJson); + } else if ("immediate".equals(fieldName)) { + relations.immediate = reader.readArray(Schema::fromJson); + } else { + reader.skipChildren(); + } + }); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/Request.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/Request.java new file mode 100644 index 0000000000..89c47e47eb --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/Request.java @@ -0,0 +1,93 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.extension.model.codemodel; + +import com.microsoft.typespec.http.client.generator.core.extension.base.util.JsonUtils; +import com.azure.json.JsonReader; +import com.azure.json.JsonWriter; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * Represents a request to an operation. + */ +public class Request extends Metadata { + private List parameters = new ArrayList<>(); + private List signatureParameters = new ArrayList<>(); + + /** + * Creates a new instance of the Request class. + */ + public Request() { + } + + /** + * Gets the parameter inputs to the operation. + * + * @return The parameter inputs to the operation. + */ + public List getParameters() { + return parameters; + } + + /** + * Sets the parameter inputs to the operation. + * + * @param parameters The parameter inputs to the operation. + */ + public void setParameters(List parameters) { + this.parameters = parameters; + } + + /** + * Gets the signature parameters. + * + * @return The signature parameters. + */ + public List getSignatureParameters() { + return signatureParameters; + } + + /** + * Sets the signature parameters. + * + * @param signatureParameters The signature parameters. + */ + public void setSignatureParameters(List signatureParameters) { + this.signatureParameters = signatureParameters; + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + return super.writeParentProperties(jsonWriter.writeStartObject()) + .writeArrayField("parameters", parameters, JsonWriter::writeJson) + .writeArrayField("signatureParameters", signatureParameters, JsonWriter::writeJson) + .writeEndObject(); + } + + /** + * Deserializes a Request instance from the JSON data. + * + * @param jsonReader The JSON reader to deserialize from. + * @return A Request instance deserialized from the JSON data. + * @throws IOException If an error occurs during deserialization. + */ + public static Request fromJson(JsonReader jsonReader) throws IOException { + return JsonUtils.readObject(jsonReader, Request::new, (request, fieldName, reader) -> { + if (request.tryConsumeParentProperties(request, fieldName, reader)) { + return; + } + + if ("parameters".equals(fieldName)) { + request.parameters = reader.readArray(Parameter::fromJson); + } else if ("signatureParameters".equals(fieldName)) { + request.signatureParameters = reader.readArray(Parameter::fromJson); + } else { + reader.skipChildren(); + } + }); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/RequestParameterLocation.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/RequestParameterLocation.java new file mode 100644 index 0000000000..de58638480 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/RequestParameterLocation.java @@ -0,0 +1,94 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.extension.model.codemodel; + +import java.util.HashMap; +import java.util.Map; + +/** + * The location of a parameter within an HTTP request. + */ +public enum RequestParameterLocation { + /** + * The parameter is in the request body. + */ + BODY("body"), + + /** + * The parameter is in a cookie. + */ + COOKIE("cookie"), + + /** + * The parameter is in the request URI. + */ + URI("uri"), + + /** + * The parameter is in the request path. + */ + PATH("path"), + + /** + * The parameter is in the request header. + */ + HEADER("header"), + + /** + * The parameter is not in the request. + */ + NONE("none"), + + /** + * The parameter is in the request query. + */ + QUERY("query"); + + private final String value; + private final static Map CONSTANTS = new HashMap<>(); + + static { + for (RequestParameterLocation c : values()) { + CONSTANTS.put(c.value, c); + } + } + + RequestParameterLocation(String value) { + this.value = value; + } + + @Override + public String toString() { + if ("uri".equals(this.value)) { + return "host"; + } else { + return this.value; + } + } + + /** + * Gets the value of the parameter location. + * + * @return The value of the parameter location. + */ + public String value() { + return this.value; + } + + /** + * Returns the enum constant of this type with the specified value. + * + * @param value The value of the constant. + * @return The enum constant of this type with the specified value. + * @throws IllegalArgumentException If the specified value does not map to one of the constants in the enum. + */ + public static RequestParameterLocation fromValue(String value) { + RequestParameterLocation constant = CONSTANTS.get(value); + if (constant == null) { + throw new IllegalArgumentException(value); + } else { + return constant; + } + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/Response.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/Response.java new file mode 100644 index 0000000000..77f1ee776c --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/Response.java @@ -0,0 +1,100 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.extension.model.codemodel; + +import com.microsoft.typespec.http.client.generator.core.extension.base.util.JsonUtils; +import com.azure.json.JsonReader; +import com.azure.json.JsonWriter; + +import java.io.IOException; + +/** + * Represents a response from a service. + */ +public class Response extends Metadata { + private Schema schema; + private Boolean binary; + + /** + * Creates a new instance of the Response class. + */ + public Response() { + } + + /** + * Gets the schema of the response. + * + * @return The schema of the response. + */ + public Schema getSchema() { + return schema; + } + + /** + * Sets the schema of the response. + * + * @param schema The schema of the response. + */ + public void setSchema(Schema schema) { + this.schema = schema; + } + + /** + * Gets whether the response is binary. + * + * @return Whether the response is binary. + */ + public Boolean getBinary() { + return binary; + } + + /** + * Sets whether the response is binary. + * + * @param binary Whether the response is binary. + */ + public void setBinary(Boolean binary) { + this.binary = binary; + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + return writeParentProperties(jsonWriter.writeStartObject()).writeEndObject(); + } + + JsonWriter writeParentProperties(JsonWriter jsonWriter) throws IOException { + return super.writeParentProperties(jsonWriter) + .writeJsonField("schema", schema) + .writeBooleanField("binary", binary); + } + + /** + * Deserializes a Response instance from the JSON data. + * + * @param jsonReader The JSON reader to deserialize from. + * @return A Response instance deserialized from the JSON data. + * @throws IOException If an error occurs during deserialization. + */ + public static Response fromJson(JsonReader jsonReader) throws IOException { + return JsonUtils.readObject(jsonReader, Response::new, (response, fieldName, reader) -> { + if (!response.tryConsumeParentProperties(response, fieldName, reader)) { + reader.skipChildren(); + } + }); + } + + boolean tryConsumeParentProperties(Response response, String fieldName, JsonReader reader) throws IOException { + if (super.tryConsumeParentProperties(response, fieldName, reader)) { + return true; + } else if ("schema".equals(fieldName)) { + response.schema = Schema.fromJson(reader); + return true; + } else if ("binary".equals(fieldName)) { + response.binary = reader.getNullable(JsonReader::getBoolean); + return true; + } + + return false; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/ScenarioStep.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/ScenarioStep.java new file mode 100644 index 0000000000..000ef305e0 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/ScenarioStep.java @@ -0,0 +1,177 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.extension.model.codemodel; + +import com.microsoft.typespec.http.client.generator.core.extension.base.util.JsonUtils; +import com.azure.json.JsonReader; +import com.azure.json.JsonSerializable; +import com.azure.json.JsonWriter; + +import java.io.IOException; +import java.util.Map; + +/** + * Represents a step in a scenario. + */ +public class ScenarioStep implements JsonSerializable { + private TestScenarioStepType type; + private String operationId; + private String exampleFile; + private String exampleName; + private Map requestParameters; + private String description; + + /** + * Creates a new instance of the ScenarioStep class. + */ + public ScenarioStep() { + } + + /** + * Gets the description of the step. + * + * @return The description of the step. + */ + public String getDescription() { + return description; + } + + /** + * Sets the description of the step. + * + * @param description The description of the step. + */ + public void setDescription(String description) { + this.description = description; + } + + /** + * Gets the type of the step. + * + * @return The type of the step. + */ + public TestScenarioStepType getType() { + return type; + } + + /** + * Sets the type of the step. + * + * @param type The type of the step. + */ + public void setType(TestScenarioStepType type) { + this.type = type; + } + + /** + * Gets the operation id of the step. + * + * @return The operation id of the step. + */ + public String getOperationId() { + return operationId; + } + + /** + * Sets the operation id of the step. + * + * @param operationId The operation id of the step. + */ + public void setOperationId(String operationId) { + this.operationId = operationId; + } + + /** + * Gets the example file of the step. + * + * @return The example file of the step. + */ + public String getExampleFile() { + return exampleFile; + } + + /** + * Sets the example file of the step. + * + * @param exampleFile The example file of the step. + */ + public void setExampleFile(String exampleFile) { + this.exampleFile = exampleFile; + } + + /** + * Gets the example name of the step. + * + * @return The example name of the step. + */ + public String getExampleName() { + return exampleName; + } + + /** + * Sets the example name of the step. + * + * @param exampleName The example name of the step. + */ + public void setExampleName(String exampleName) { + this.exampleName = exampleName; + } + + /** + * Gets the request parameters of the step. + * + * @return The request parameters of the step. + */ + public Map getRequestParameters() { + return requestParameters; + } + + /** + * Sets the request parameters of the step. + * + * @param requestParameters The request parameters of the step. + */ + public void setRequestParameters(Map requestParameters) { + this.requestParameters = requestParameters; + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + return jsonWriter.writeStartObject() + .writeStringField("type", type == null ? null : type.toString()) + .writeStringField("operationId", operationId) + .writeStringField("exampleFile", exampleFile) + .writeStringField("exampleName", exampleName) + .writeMapField("requestParameters", requestParameters, JsonWriter::writeUntyped) + .writeStringField("description", description) + .writeEndObject(); + } + + /** + * Deserializes a ScenarioStep instance from the JSON data. + * + * @param jsonReader The JSON reader to deserialize from. + * @return A ScenarioStep instance deserialized from the JSON data. + * @throws IOException If an error occurs during deserialization. + */ + public static ScenarioStep fromJson(JsonReader jsonReader) throws IOException { + return JsonUtils.readObject(jsonReader, ScenarioStep::new, (step, fieldName, reader) -> { + if ("type".equals(fieldName)) { + step.type = TestScenarioStepType.fromValue(reader.getString()); + } else if ("operationId".equals(fieldName)) { + step.operationId = reader.getString(); + } else if ("exampleFile".equals(fieldName)) { + step.exampleFile = reader.getString(); + } else if ("exampleName".equals(fieldName)) { + step.exampleName = reader.getString(); + } else if ("requestParameters".equals(fieldName)) { + step.requestParameters = reader.readMap(JsonReader::readUntyped); + } else if ("description".equals(fieldName)) { + step.description = reader.getString(); + } else { + reader.skipChildren(); + } + }); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/ScenarioTest.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/ScenarioTest.java new file mode 100644 index 0000000000..e15eb1ecfd --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/ScenarioTest.java @@ -0,0 +1,183 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.extension.model.codemodel; + +import com.microsoft.typespec.http.client.generator.core.extension.base.util.JsonUtils; +import com.azure.json.JsonReader; +import com.azure.json.JsonSerializable; +import com.azure.json.JsonWriter; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +/** + * Represents a scenario test. + * + * @see + * Api Scenario Definition Reference + * + */ +public class ScenarioTest implements JsonSerializable { + private String filePath; + private List requiredVariables; + private Map requiredVariablesDefault; + private List scenarios; + private ScenarioTestScope scope; + private Boolean useArmTemplate; + + /** + * Creates a new instance of the ScenarioTest class. + */ + public ScenarioTest() { + } + + /** + * Gets the file path of the scenario test. + * + * @return The file path of the scenario test. + */ + @YamlProperty("_filePath") + public String getFilePath() { + return filePath; + } + + /** + * Sets the file path of the scenario test. + * + * @param filePath The file path of the scenario test. + */ + public void setFilePath(String filePath) { + this.filePath = filePath; + } + + /** + * Gets the required variables. + * + * @return The required variables. + */ + public List getRequiredVariables() { + return requiredVariables; + } + + /** + * Sets the required variables. + * + * @param requiredVariables The required variables. + */ + public void setRequiredVariables(List requiredVariables) { + this.requiredVariables = requiredVariables; + } + + /** + * Gets the default values for the required variables. + * + * @return The default values for the required variables. + */ + public Map getRequiredVariablesDefault() { + return requiredVariablesDefault; + } + + /** + * Sets the default values for the required variables. + * + * @param requiredVariablesDefault The default values for the required variables. + */ + public void setRequiredVariablesDefault(Map requiredVariablesDefault) { + this.requiredVariablesDefault = requiredVariablesDefault; + } + + /** + * Gets the scenarios. + * + * @return The scenarios. + */ + public List getScenarios() { + return scenarios; + } + + /** + * Sets the scenarios. + * + * @param scenarios The scenarios. + */ + public void setScenarios(List scenarios) { + this.scenarios = scenarios; + } + + /** + * Gets the scope of the scenario test. + * + * @return The scope of the scenario test. + */ + public ScenarioTestScope getScope() { + return scope; + } + + /** + * Sets the scope of the scenario test. + * + * @param scope The scope of the scenario test. + */ + public void setScope(ScenarioTestScope scope) { + this.scope = scope; + } + + /** + * Gets whether to use an ARM template. + * + * @return Whether to use an ARM template. + */ + public Boolean getUseArmTemplate() { + return useArmTemplate; + } + + /** + * Sets whether to use an ARM template. + * + * @param useArmTemplate Whether to use an ARM template. + */ + public void setUseArmTemplate(Boolean useArmTemplate) { + this.useArmTemplate = useArmTemplate; + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + return jsonWriter.writeStartObject() + .writeStringField("_filePath", filePath) + .writeArrayField("requiredVariables", requiredVariables, JsonWriter::writeString) + .writeMapField("requiredVariablesDefault", requiredVariablesDefault, JsonWriter::writeString) + .writeArrayField("scenarios", scenarios, JsonWriter::writeJson) + .writeStringField("scope", scope == null ? null : scope.toString()) + .writeBooleanField("useArmTemplate", useArmTemplate) + .writeEndObject(); + } + + /** + * Deserializes a ScenarioTest instance from the JSON data. + * + * @param jsonReader The JSON reader to deserialize from. + * @return A ScenarioTest instance deserialized from the JSON data. + * @throws IOException If an error occurs during deserialization. + */ + public static ScenarioTest fromJson(JsonReader jsonReader) throws IOException { + return JsonUtils.readObject(jsonReader, ScenarioTest::new, (test, fieldName, reader) -> { + if ("_filePath".equals(fieldName)) { + test.filePath = reader.getString(); + } else if ("requiredVariables".equals(fieldName)) { + test.requiredVariables = reader.readArray(JsonReader::getString); + } else if ("requiredVariablesDefault".equals(fieldName)) { + test.requiredVariablesDefault = reader.readMap(JsonReader::getString); + } else if ("scenarios".equals(fieldName)) { + test.scenarios = reader.readArray(TestScenario::fromJson); + } else if ("scope".equals(fieldName)) { + test.scope = ScenarioTestScope.fromValue(reader.getString()); + } else if ("useArmTemplate".equals(fieldName)) { + test.useArmTemplate = reader.getNullable(JsonReader::getBoolean); + } else { + reader.skipChildren(); + } + }); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/ScenarioTestScope.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/ScenarioTestScope.java new file mode 100644 index 0000000000..eb865b57e6 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/ScenarioTestScope.java @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.extension.model.codemodel; + +/** + * Represents the scope of a test scenario. + */ +public enum ScenarioTestScope { + + /** + * All the following API scenario and steps should be under some resourceGroup. It means: + * The consumer (API scenario runner or anything consumes API scenario) SHOULD maintain the resource group itself. + * Usually it requires user to input the subscriptionId/location, then it creates the resource group before test + * running, and deletes the resource group after running + * The consumer SHOULD set the following variables: + * - subscriptionId + * - resourceGroupName + * - location + */ + RESOURCE_GROUP("ResourceGroup"); + + private final String value; + + ScenarioTestScope(String value) { + this.value = value; + } + + /** + * Gets the ScenarioTestScope from its value. + * + * @param value The value. + * @return The ScenarioTestScope. + * @throws IllegalArgumentException If the value is invalid. + */ + public static ScenarioTestScope fromValue(String value) { + if ("ResourceGroup".equals(value)) { + return RESOURCE_GROUP; + } else { + throw new IllegalArgumentException(value); + } + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/Schema.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/Schema.java new file mode 100644 index 0000000000..7e146bb66c --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/Schema.java @@ -0,0 +1,570 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.extension.model.codemodel; + +import com.microsoft.typespec.http.client.generator.core.extension.base.util.JsonUtils; +import com.azure.json.JsonReader; +import com.azure.json.JsonWriter; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Represents a schema. + */ +public class Schema extends Metadata { + private Schema.AllSchemaTypes type; + private String summary; + private Object example; + private Object defaultValue; + private SerializationFormats serialization; + private Set serializationFormats; + private Set usage; + private String uid; + private String $key; + private String description; + private List apiVersions = new ArrayList<>(); + private Deprecation deprecated; + private ExternalDocumentation externalDocs; + + /** + * Creates a new instance of the Schema class. + */ + public Schema() { + } + + /** + * Gets the all schema types. (Required) + * + * @return The all schema types. + */ + public Schema.AllSchemaTypes getType() { + return type; + } + + /** + * Sets the all schema types. (Required) + * + * @param type The all schema types. + */ + public void setType(Schema.AllSchemaTypes type) { + this.type = type; + } + + /** + * Gets a short description. + * + * @return The short description. + */ + public String getSummary() { + return summary; + } + + /** + * Sets a short description. + * + * @param summary The short description. + */ + public void setSummary(String summary) { + this.summary = summary; + } + + /** + * Gets example information. + * + * @return The example information. + */ + public Object getExample() { + return example; + } + + /** + * Sets example information. + * + * @param example The example information. + */ + public void setExample(Object example) { + this.example = example; + } + + /** + * Gets the default value. + * + * @return The default value. + */ + public Object getDefaultValue() { + return defaultValue; + } + + /** + * Sets the default value. + * + * @param defaultValue The default value. + */ + public void setDefaultValue(Object defaultValue) { + this.defaultValue = defaultValue; + } + + /** + * Gets the serialization formats. + * + * @return The serialization formats. + */ + public SerializationFormats getSerialization() { + return serialization; + } + + /** + * Sets the serialization formats. + * + * @param serialization The serialization formats. + */ + public void setSerialization(SerializationFormats serialization) { + this.serialization = serialization; + } + + /** + * Gets the set of serialization formats this Schema is used with, ex. JSON, XML, etc. + * + * @return The serialization formats. + */ + public Set getSerializationFormats() { + return serializationFormats; + } + + /** + * Sets the set of serialization formats this Schema is used with, ex. JSON, XML, etc. + * + * @param serializationFormats The serialization formats. + */ + public void setSerializationFormats(Set serializationFormats) { + this.serializationFormats = serializationFormats; + } + + /** + * Gets the UID of the schema. (Required) + * + * @return The UID of the schema. + */ + public String getUid() { + return uid; + } + + /** + * Sets the UID of the schema. (Required) + * + * @param uid The UID of the schema. + */ + public void setUid(String uid) { + this.uid = uid; + } + + /** + * Gets the key of the schema. (Required) + * + * @return The key of the schema. + */ + public String get$key() { + return $key; + } + + /** + * Sets the key of the schema. (Required) + * + * @param $key The key of the schema. + */ + public void set$key(String $key) { + this.$key = $key; + } + + /** + * Gets the description of the schema. (Required) + * + * @return The description of the schema. + */ + public String getDescription() { + return description; + } + + /** + * Sets the description of the schema. (Required) + * + * @param description The description of the schema. + */ + public void setDescription(String description) { + this.description = description; + } + + /** + * Gets the API versions that this applies to. Undefined means all versions. + * + * @return The API versions that this applies to. Undefined means all versions. + */ + public List getApiVersions() { + return apiVersions; + } + + /** + * Sets the API versions that this applies to. Undefined means all versions. + * + * @param apiVersions The API versions that this applies to. Undefined means all versions. + */ + public void setApiVersions(List apiVersions) { + this.apiVersions = apiVersions; + } + + /** + * Gets the deprecation information for the schema. + * + * @return The deprecation information for the schema. + */ + public Deprecation getDeprecated() { + return deprecated; + } + + /** + * Sets the deprecation information for the schema. + * + * @param deprecated The deprecation information for the schema. + */ + public void setDeprecated(Deprecation deprecated) { + this.deprecated = deprecated; + } + + /** + * Gets a reference to external documentation. + * + * @return A reference to external documentation. + */ + public ExternalDocumentation getExternalDocs() { + return externalDocs; + } + + /** + * Sets a reference to external documentation. + * + * @param externalDocs A reference to external documentation. + */ + public void setExternalDocs(ExternalDocumentation externalDocs) { + this.externalDocs = externalDocs; + } + + /** + * Gets the usage of the schema. + * + * @return The usage of the schema. + */ + public Set getUsage() { + return usage; + } + + /** + * Sets the usage of the schema. + * + * @param usage The usage of the schema. + */ + public void setUsage(Set usage) { + this.usage = usage; + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + return writeParentProperties(jsonWriter.writeStartObject()).writeEndObject(); + } + + JsonWriter writeParentProperties(JsonWriter jsonWriter) throws IOException { + return super.writeParentProperties(jsonWriter) + .writeStringField("type", type == null ? null : type.toString()) + .writeStringField("summary", summary) + .writeUntypedField("example", example) + .writeUntypedField("defaultValue", defaultValue) + .writeJsonField("serialization", serialization) + .writeArrayField("serializationFormats", serializationFormats, JsonWriter::writeString) + .writeArrayField("usage", usage, (writer, element) -> writer.writeString(element == null ? null : element.toString())) + .writeStringField("uid", uid) + .writeStringField("$key", $key) + .writeStringField("description", description) + .writeArrayField("apiVersions", apiVersions, JsonWriter::writeJson) + .writeJsonField("deprecated", deprecated) + .writeJsonField("externalDocs", externalDocs); + } + + /** + * Deserializes a Schema instance from the JSON data. + * + * @param jsonReader The JSON reader to deserialize from. + * @return A Schema instance deserialized from the JSON data. + * @throws IOException If an error occurs during deserialization. + */ + public static Schema fromJson(JsonReader jsonReader) throws IOException { + return JsonUtils.readObject(jsonReader, Schema::new, (schema, fieldName, reader) -> { + if (!schema.tryConsumeParentProperties(schema, fieldName, reader)) { + reader.skipChildren(); + } + }); + } + + boolean tryConsumeParentProperties(Schema schema, String fieldName, JsonReader reader) throws IOException { + if (super.tryConsumeParentProperties(schema, fieldName, reader)) { + return true; + } else if ("type".equals(fieldName)) { + schema.type = Schema.AllSchemaTypes.fromValue(reader.getString()); + return true; + } else if ("summary".equals(fieldName)) { + schema.summary = reader.getString(); + return true; + } else if ("example".equals(fieldName)) { + schema.example = reader.readUntyped(); + return true; + } else if ("defaultValue".equals(fieldName)) { + schema.defaultValue = reader.readUntyped(); + return true; + } else if ("serialization".equals(fieldName)) { + schema.serialization = SerializationFormats.fromJson(reader); + return true; + } else if ("serializationFormats".equals(fieldName)) { + List formats = reader.readArray(JsonReader::getString); + schema.serializationFormats = formats == null ? null : new HashSet<>(formats); + return true; + } else if ("usage".equals(fieldName)) { + List usage = reader.readArray(element -> SchemaContext.fromValue(element.getString())); + schema.usage = usage == null ? null : new HashSet<>(usage); + return true; + } else if ("uid".equals(fieldName)) { + schema.uid = reader.getString(); + return true; + } else if ("$key".equals(fieldName)) { + schema.$key = reader.getString(); + return true; + } else if ("description".equals(fieldName)) { + schema.description = reader.getString(); + return true; + } else if ("apiVersions".equals(fieldName)) { + schema.apiVersions = reader.readArray(ApiVersion::fromJson); + return true; + } else if ("deprecated".equals(fieldName)) { + schema.deprecated = Deprecation.fromJson(reader); + return true; + } else if ("externalDocs".equals(fieldName)) { + schema.externalDocs = ExternalDocumentation.fromJson(reader); + return true; + } + + return false; + } + + /** + * Represents all schema types. + */ + public enum AllSchemaTypes { + /** + * Represents any type. + */ + ANY("any"), + + /** + * Represents any object. + */ + ANY_OBJECT("any-object"), + + /** + * Represents AND logic. + */ + AND("and"), + + /** + * Represents arm-id. + */ + ARM_ID("arm-id"), + + /** + * Represents array. + */ + ARRAY("array"), + + /** + * Represents binary. + */ + BINARY("binary"), + + /** + * Represents boolean. + */ + BOOLEAN("boolean"), + + /** + * Represents byte array. + */ + BYTE_ARRAY("byte-array"), + + /** + * Represents char. + */ + CHAR("char"), + + /** + * Represents choice. + */ + CHOICE("choice"), + + /** + * Represents constant. + */ + CONSTANT("constant"), + + /** + * Represents credential. + */ + CREDENTIAL("credential"), + + /** + * Represents date. + */ + DATE("date"), + + /** + * Represents date-time. + */ + DATE_TIME("date-time"), + + /** + * Represents dictionary. + */ + DICTIONARY("dictionary"), + + /** + * Represents duration. + */ + DURATION("duration"), + + /** + * Represents flag. + */ + FLAG("flag"), + + /** + * Represents float. + */ + GROUP("group"), + + /** + * Represents integer. + */ + INTEGER("integer"), + + /** + * Represents NOT logic. + */ + NOT("not"), + + /** + * Represents number. + */ + NUMBER("number"), + + /** + * Represents object. + */ + OBJECT("object"), + + /** + * Represents odata-query. + */ + ODATA_QUERY("odata-query"), + + /** + * Represents OR logic. + */ + OR("or"), + + /** + * Represents parameter-group. + */ + PARAMETER_GROUP("parameter-group"), + + /** + * Represents sealed-choice. + */ + SEALED_CHOICE("sealed-choice"), + + /** + * Represents string. + */ + STRING("string"), + + /** + * Represents time. + */ + TIME("time"), + + /** + * Represents unixtime. + */ + UNIXTIME("unixtime"), + + /** + * Represents uri. + */ + URI("uri"), + + /** + * Represents uuid. + */ + UUID("uuid"), + + /** + * Represents XOR logic. + */ + XOR("xor"); + private final String value; + private final static Map CONSTANTS = new HashMap<>(); + + static { + for (Schema.AllSchemaTypes c : values()) { + CONSTANTS.put(c.value, c); + } + } + + AllSchemaTypes(String value) { + this.value = value; + } + + @Override + public String toString() { + return this.value; + } + + /** + * Gets the value of the schema type. + * + * @return The value of the schema type. + */ + public String value() { + return this.value; + } + + /** + * Returns the enum constant of this type with the specified value. + * + * @param value The value of the constant. + * @return The enum constant of this type with the specified value. + * @throws IllegalArgumentException If the specified value does not map to one of the constants in the enum. + */ + public static Schema.AllSchemaTypes fromValue(String value) { + Schema.AllSchemaTypes constant = CONSTANTS.get(value); + if (constant == null) { + throw new IllegalArgumentException(value); + } else { + return constant; + } + } + + } + +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/SchemaContext.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/SchemaContext.java new file mode 100644 index 0000000000..b1efd5b9ce --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/SchemaContext.java @@ -0,0 +1,100 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.extension.model.codemodel; + +import java.util.HashMap; +import java.util.Map; + +/** + * The context in which a schema is used. + */ +public enum SchemaContext { + /** + * The schema is used as an input. + */ + INPUT("input"), + + /** + * The schema is used as an output. + */ + OUTPUT("output"), + + /** + * The schema is used as an exception. + */ + EXCEPTION("exception"), + + /** + * The schema is used publicly. + */ + PUBLIC("public"), + + /** + * The schema is used as a paged result. + */ + PAGED("paged"), + + /** + * The schema is used as an anonymous type. + */ + ANONYMOUS("anonymous"), + + /** + * The schema is used internally. + */ + INTERNAL("internal"), + + /** + * The schema is used as a JSON merge patch. + */ + JSON_MERGE_PATCH("json-merge-patch"), + + /** + * The schema is used as options group. + */ + OPTIONS_GROUP("options-group"); + + private final String value; + private final static Map CONSTANTS = new HashMap<>(); + + static { + for (SchemaContext c : values()) { + CONSTANTS.put(c.value, c); + } + } + + SchemaContext(String value) { + this.value = value; + } + + @Override + public String toString() { + return this.value; + } + + /** + * Gets the value of the schema context. + * + * @return The value of the schema context. + */ + public String value() { + return this.value; + } + + /** + * Returns the enum constant of this type with the specified value. + * + * @param value The value of the constant. + * @return The enum constant of this type with the specified value. + * @throws IllegalArgumentException If the specified value does not map to one of the constants in the enum. + */ + public static SchemaContext fromValue(String value) { + SchemaContext constant = CONSTANTS.get(value); + if (constant == null) { + throw new IllegalArgumentException(value); + } else { + return constant; + } + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/SchemaResponse.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/SchemaResponse.java new file mode 100644 index 0000000000..42fafb3595 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/SchemaResponse.java @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.extension.model.codemodel; + +import com.microsoft.typespec.http.client.generator.core.extension.base.util.JsonUtils; +import com.azure.json.JsonReader; +import com.azure.json.JsonWriter; + +import java.io.IOException; + +/** + * Represents a response from a service. + */ +public class SchemaResponse extends Response { + private Schema schema; + + /** + * Creates a new instance of the SchemaResponse class. + */ + public SchemaResponse() { + } + + /** + * Gets the schema of the response. + * + * @return The schema of the response. + */ + public Schema getSchema() { + return schema; + } + + /** + * Sets the schema of the response. + * + * @param schema The schema of the response. + */ + public void setSchema(Schema schema) { + this.schema = schema; + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + return super.writeParentProperties(jsonWriter.writeStartObject()) + .writeJsonField("schema", schema) + .writeEndObject(); + } + + /** + * Deserializes a SchemaResponse instance from the JSON data. + * + * @param jsonReader The JSON reader to deserialize from. + * @return A SchemaResponse instance deserialized from the JSON data. + * @throws IOException If an error occurs during deserialization. + */ + public static SchemaResponse fromJson(JsonReader jsonReader) throws IOException { + return JsonUtils.readObject(jsonReader, SchemaResponse::new, (response, fieldName, reader) -> { + if (response.tryConsumeParentProperties(response, fieldName, reader)) { + return; + } + + if ("schema".equals(fieldName)) { + response.schema = Schema.fromJson(reader); + } else { + reader.skipChildren(); + } + }); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/Schemas.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/Schemas.java new file mode 100644 index 0000000000..94378c7a5f --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/Schemas.java @@ -0,0 +1,662 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.extension.model.codemodel; + +import com.microsoft.typespec.http.client.generator.core.extension.base.util.JsonUtils; +import com.azure.json.JsonReader; +import com.azure.json.JsonSerializable; +import com.azure.json.JsonWriter; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * Represents the full set of schemas for a given service, categorized into convenient collections. + */ +public class Schemas implements JsonSerializable { + private List arrays = new ArrayList<>(); + private List dictionaries = new ArrayList<>(); + private List binaries = new ArrayList<>(); + private List groups = new ArrayList<>(); + private List booleans = new ArrayList<>(); + private List numbers = new ArrayList<>(); + private List objects = new ArrayList<>(); + private List strings = new ArrayList<>(); + private List unixtimes = new ArrayList<>(); + private List byteArrays = new ArrayList<>(); + private List streams = new ArrayList<>(); + private List chars = new ArrayList<>(); + private List dates = new ArrayList<>(); + private List dateTimes = new ArrayList<>(); + private List durations = new ArrayList<>(); + private List uuids = new ArrayList<>(); + private List uris = new ArrayList<>(); + private List credentials = new ArrayList<>(); + private List odataQueries = new ArrayList<>(); + private List choices = new ArrayList<>(); + private List sealedChoices = new ArrayList<>(); + private List flags = new ArrayList<>(); + private List constants = new ArrayList<>(); + private List ands = new ArrayList<>(); + private List ors = new ArrayList<>(); + private List xors = new ArrayList<>(); + private List unknowns = new ArrayList<>(); + private List parameterGroups = new ArrayList<>(); + + /** + * Creates a new instance of the Schemas class. + */ + public Schemas() { + } + + /** + * Gets the list of array schemas. + * + * @return The list of array schemas. + */ + public List getArrays() { + return arrays; + } + + /** + * Sets the list of array schemas. + * + * @param arrays The list of array schemas. + */ + public void setArrays(List arrays) { + this.arrays = arrays; + } + + /** + * Gets the list of dictionary schemas. + * + * @return The list of dictionary schemas. + */ + public List getDictionaries() { + return dictionaries; + } + + /** + * Sets the list of dictionary schemas. + * + * @param dictionaries The list of dictionary schemas. + */ + public void setDictionaries(List dictionaries) { + this.dictionaries = dictionaries; + } + + /** + * Gets the list of boolean schemas. + * + * @return The list of boolean schemas. + */ + public List getBooleans() { + return booleans; + } + + /** + * Sets the list of boolean schemas. + * + * @param booleans The list of boolean schemas. + */ + public void setBooleans(List booleans) { + this.booleans = booleans; + } + + /** + * Gets the list of number schemas. + * + * @return The list of number schemas. + */ + public List getNumbers() { + return numbers; + } + + /** + * Sets the list of number schemas. + * + * @param numbers The list of number schemas. + */ + public void setNumbers(List numbers) { + this.numbers = numbers; + } + + /** + * Gets the list of object schemas. + * + * @return The list of object schemas. + */ + public List getObjects() { + return objects; + } + + /** + * Sets the list of object schemas. + * + * @param objects The list of object schemas. + */ + public void setObjects(List objects) { + this.objects = objects; + } + + /** + * Gets the list of string schemas. + * + * @return The list of string schemas. + */ + public List getStrings() { + return strings; + } + + /** + * Sets the list of string schemas. + * + * @param strings The list of string schemas. + */ + public void setStrings(List strings) { + this.strings = strings; + } + + /** + * Gets the list of UnixTime schemas. + * + * @return The list of UnixTime schemas. + */ + public List getUnixtimes() { + return unixtimes; + } + + /** + * Sets the list of UnixTime schemas. + * + * @param unixtimes The list of UnixTime schemas. + */ + public void setUnixtimes(List unixtimes) { + this.unixtimes = unixtimes; + } + + /** + * Gets the list of ByteArray schemas. + * + * @return The list of ByteArray schemas. + */ + public List getByteArrays() { + return byteArrays; + } + + /** + * Sets the list of ByteArray schemas. + * + * @param byteArrays The list of ByteArray schemas. + */ + public void setByteArrays(List byteArrays) { + this.byteArrays = byteArrays; + } + + /** + * Gets the list of streams. + * + * @return The list of streams. + */ + public List getStreams() { + return streams; + } + + /** + * Sets the list of streams. + * + * @param streams The list of streams. + */ + public void setStreams(List streams) { + this.streams = streams; + } + + /** + * Gets the list of Char schemas. + * + * @return The list of Char schemas. + */ + public List getChars() { + return chars; + } + + /** + * Sets the list of Char schemas. + * + * @param chars The list of Char schemas. + */ + public void setChars(List chars) { + this.chars = chars; + } + + /** + * Gets the list of Date schemas. + * + * @return The list of Date schemas. + */ + public List getDates() { + return dates; + } + + /** + * Sets the list of Date schemas. + * + * @param dates The list of Date schemas. + */ + public void setDates(List dates) { + this.dates = dates; + } + + /** + * Gets the list of DateTime schemas. + * + * @return The list of DateTime schemas. + */ + public List getDateTimes() { + return dateTimes; + } + + /** + * Sets the list of DateTime schemas. + * + * @param dateTimes The list of DateTime schemas. + */ + public void setDateTimes(List dateTimes) { + this.dateTimes = dateTimes; + } + + /** + * Gets the list of Duration schemas. + * + * @return The list of Duration schemas. + */ + public List getDurations() { + return durations; + } + + /** + * Sets the list of Duration schemas. + * + * @param durations The list of Duration schemas. + */ + public void setDurations(List durations) { + this.durations = durations; + } + + /** + * Gets the list of Uuid schemas. + * + * @return The list of Uuid schemas. + */ + public List getUuids() { + return uuids; + } + + /** + * Sets the list of Uuid schemas. + * + * @param uuids The list of Uuid schemas. + */ + public void setUuids(List uuids) { + this.uuids = uuids; + } + + /** + * Gets the list of Uri schemas. + * + * @return The list of Uri schemas. + */ + public List getUris() { + return uris; + } + + /** + * Sets the list of Uri schemas. + * + * @param uris The list of Uri schemas. + */ + public void setUris(List uris) { + this.uris = uris; + } + + /** + * Gets the list of Credential schemas. + * + * @return The list of Credential schemas. + */ + public List getCredentials() { + return credentials; + } + + /** + * Sets the list of Credential schemas. + * + * @param credentials The list of Credential schemas. + */ + public void setCredentials(List credentials) { + this.credentials = credentials; + } + + /** + * Gets the list of ODataQuery schemas. + * + * @return The list of ODataQuery schemas. + */ + public List getOdataQueries() { + return odataQueries; + } + + /** + * Sets the list of ODataQuery schemas. + * + * @param odataQueries The list of ODataQuery schemas. + */ + public void setOdataQueries(List odataQueries) { + this.odataQueries = odataQueries; + } + + /** + * Gets the list of Choice schemas. + * + * @return The list of Choice schemas. + */ + public List getChoices() { + return choices; + } + + /** + * Sets the list of Choice schemas. + * + * @param choices The list of Choice schemas. + */ + public void setChoices(List choices) { + this.choices = choices; + } + + /** + * Gets the list of SealedChoice schemas. + * + * @return The list of SealedChoice schemas. + */ + public List getSealedChoices() { + return sealedChoices; + } + + /** + * Sets the list of SealedChoice schemas. + * + * @param sealedChoices The list of SealedChoice schemas. + */ + public void setSealedChoices(List sealedChoices) { + this.sealedChoices = sealedChoices; + } + + /** + * Gets the list of Flag schemas. + * + * @return The list of Flag schemas. + */ + public List getFlags() { + return flags; + } + + /** + * Sets the list of Flag schemas. + * + * @param flags The list of Flag schemas. + */ + public void setFlags(List flags) { + this.flags = flags; + } + + /** + * Gets the list of Constant schemas. + * + * @return The list of Constant schemas. + */ + public List getConstants() { + return constants; + } + + /** + * Sets the list of Constant schemas. + * + * @param constants The list of Constant schemas. + */ + public void setConstants(List constants) { + this.constants = constants; + } + + /** + * Gets the list of And schemas. + * + * @return The list of And schemas. + */ + public List getAnds() { + return ands; + } + + /** + * Sets the list of And schemas. + * + * @param ands The list of And schemas. + */ + public void setAnds(List ands) { + this.ands = ands; + } + + /** + * Gets the list of Or schemas. + * + * @return The list of Or schemas. + */ + public List getOrs() { + return ors; + } + + /** + * Sets the list of Or schemas. + * + * @param ors The list of Or schemas. + */ + public void setOrs(List ors) { + this.ors = ors; + } + + /** + * Gets the list of Xor schemas. + * + * @return The list of Xor schemas. + */ + public List getXors() { + return xors; + } + + /** + * Sets the list of Xor schemas. + * + * @param xors The list of Xor schemas. + */ + public void setXors(List xors) { + this.xors = xors; + } + + /** + * Gets the list of unknown schemas. + * + * @return The list of unknown schemas. + */ + public List getUnknowns() { + return unknowns; + } + + /** + * Sets the list of unknown schemas. + * + * @param unknowns The list of unknown schemas. + */ + public void setUnknowns(List unknowns) { + this.unknowns = unknowns; + } + + /** + * Gets the list of parameter group schemas. + * + * @return The list of parameter group schemas. + */ + public List getParameterGroups() { + return parameterGroups; + } + + /** + * Sets the list of parameter group schemas. + * + * @param parameterGroups The list of parameter group schemas. + */ + public void setParameterGroups(List parameterGroups) { + this.parameterGroups = parameterGroups; + } + + /** + * Gets the list of group schemas. + * + * @return The list of group schemas. + */ + public List getGroups() { + return groups; + } + + /** + * Sets the list of group schemas. + * + * @param groups The list of group schemas. + */ + public void setGroups(List groups) { + this.groups = groups; + } + + /** + * Gets the list of binary schemas. + * + * @return The list of binary schemas. + */ + public List getBinaries() { + return binaries; + } + + /** + * Sets the list of binary schemas. + * + * @param binaries The list of binary schemas. + */ + public void setBinaries(List binaries) { + this.binaries = binaries; + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + return jsonWriter.writeStartObject() + .writeArrayField("arrays", arrays, JsonWriter::writeJson) + .writeArrayField("dictionaries", dictionaries, JsonWriter::writeJson) + .writeArrayField("binaries", binaries, JsonWriter::writeJson) + .writeArrayField("groups", groups, JsonWriter::writeJson) + .writeArrayField("booleans", booleans, JsonWriter::writeJson) + .writeArrayField("numbers", numbers, JsonWriter::writeJson) + .writeArrayField("objects", objects, JsonWriter::writeJson) + .writeArrayField("strings", strings, JsonWriter::writeJson) + .writeArrayField("unixtimes", unixtimes, JsonWriter::writeJson) + .writeArrayField("byteArrays", byteArrays, JsonWriter::writeJson) + .writeArrayField("streams", streams, JsonWriter::writeJson) + .writeArrayField("chars", chars, JsonWriter::writeJson) + .writeArrayField("dates", dates, JsonWriter::writeJson) + .writeArrayField("dateTimes", dateTimes, JsonWriter::writeJson) + .writeArrayField("durations", durations, JsonWriter::writeJson) + .writeArrayField("uuids", uuids, JsonWriter::writeJson) + .writeArrayField("uris", uris, JsonWriter::writeJson) + .writeArrayField("credentials", credentials, JsonWriter::writeJson) + .writeArrayField("odataQueries", odataQueries, JsonWriter::writeJson) + .writeArrayField("choices", choices, JsonWriter::writeJson) + .writeArrayField("sealedChoices", sealedChoices, JsonWriter::writeJson) + .writeArrayField("flags", flags, JsonWriter::writeJson) + .writeArrayField("constants", constants, JsonWriter::writeJson) + .writeArrayField("ands", ands, JsonWriter::writeJson) + .writeArrayField("ors", ors, JsonWriter::writeJson) + .writeArrayField("xors", xors, JsonWriter::writeJson) + .writeArrayField("unknowns", unknowns, JsonWriter::writeJson) + .writeArrayField("parameterGroups", parameterGroups, JsonWriter::writeJson) + .writeEndObject(); + } + + /** + * Deserializes a Schemas instance from the JSON data. + * + * @param jsonReader The JSON reader to deserialize from. + * @return A Schemas instance deserialized from the JSON data. + * @throws IOException If an error occurs during deserialization. + */ + public static Schemas fromJson(JsonReader jsonReader) throws IOException { + return JsonUtils.readObject(jsonReader, Schemas::new, (schemas, fieldName, reader) -> { + if ("arrays".equals(fieldName)) { + schemas.arrays = reader.readArray(ArraySchema::fromJson); + } else if ("dictionaries".equals(fieldName)) { + schemas.dictionaries = reader.readArray(DictionarySchema::fromJson); + } else if ("binaries".equals(fieldName)) { + schemas.binaries = reader.readArray(BinarySchema::fromJson); + } else if ("groups".equals(fieldName)) { + schemas.groups = reader.readArray(ObjectSchema::fromJson); + } else if ("booleans".equals(fieldName)) { + schemas.booleans = reader.readArray(BooleanSchema::fromJson); + } else if ("numbers".equals(fieldName)) { + schemas.numbers = reader.readArray(NumberSchema::fromJson); + } else if ("objects".equals(fieldName)) { + schemas.objects = reader.readArray(ObjectSchema::fromJson); + } else if ("strings".equals(fieldName)) { + schemas.strings = reader.readArray(StringSchema::fromJson); + } else if ("unixtimes".equals(fieldName)) { + schemas.unixtimes = reader.readArray(UnixTimeSchema::fromJson); + } else if ("byteArrays".equals(fieldName)) { + schemas.byteArrays = reader.readArray(ByteArraySchema::fromJson); + } else if ("streams".equals(fieldName)) { + schemas.streams = reader.readArray(Schema::fromJson); + } else if ("chars".equals(fieldName)) { + schemas.chars = reader.readArray(CharSchema::fromJson); + } else if ("dates".equals(fieldName)) { + schemas.dates = reader.readArray(DateSchema::fromJson); + } else if ("dateTimes".equals(fieldName)) { + schemas.dateTimes = reader.readArray(DateTimeSchema::fromJson); + } else if ("durations".equals(fieldName)) { + schemas.durations = reader.readArray(DurationSchema::fromJson); + } else if ("uuids".equals(fieldName)) { + schemas.uuids = reader.readArray(UuidSchema::fromJson); + } else if ("uris".equals(fieldName)) { + schemas.uris = reader.readArray(UriSchema::fromJson); + } else if ("credentials".equals(fieldName)) { + schemas.credentials = reader.readArray(CredentialSchema::fromJson); + } else if ("odataQueries".equals(fieldName)) { + schemas.odataQueries = reader.readArray(ODataQuerySchema::fromJson); + } else if ("choices".equals(fieldName)) { + schemas.choices = reader.readArray(ChoiceSchema::fromJson); + } else if ("sealedChoices".equals(fieldName)) { + schemas.sealedChoices = reader.readArray(SealedChoiceSchema::fromJson); + } else if ("flags".equals(fieldName)) { + schemas.flags = reader.readArray(FlagSchema::fromJson); + } else if ("constants".equals(fieldName)) { + schemas.constants = reader.readArray(ConstantSchema::fromJson); + } else if ("ands".equals(fieldName)) { + schemas.ands = reader.readArray(AndSchema::fromJson); + } else if ("ors".equals(fieldName)) { + schemas.ors = reader.readArray(OrSchema::fromJson); + } else if ("xors".equals(fieldName)) { + schemas.xors = reader.readArray(XorSchema::fromJson); + } else if ("unknowns".equals(fieldName)) { + schemas.unknowns = reader.readArray(Schema::fromJson); + } else if ("parameterGroups".equals(fieldName)) { + schemas.parameterGroups = reader.readArray(ParameterGroupSchema::fromJson); + } else { + reader.skipChildren(); + } + }); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/Scheme.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/Scheme.java new file mode 100644 index 0000000000..7c7eebd523 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/Scheme.java @@ -0,0 +1,212 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.extension.model.codemodel; + +import com.microsoft.typespec.http.client.generator.core.extension.base.util.JsonUtils; +import com.azure.json.JsonReader; +import com.azure.json.JsonSerializable; +import com.azure.json.JsonWriter; + +import java.io.IOException; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Represents a security scheme. + */ +public class Scheme implements JsonSerializable { + private Scheme.SecuritySchemeType type; + // OAuth2 + private Set scopes = new HashSet<>(); + // Key + private String name; + private String in; + private String prefix; + + /** + * Creates a new instance of the Scheme class. + */ + public Scheme() { + } + + /** + * Gets the type of the security scheme. + * + * @return The type of the security scheme. + */ + public Scheme.SecuritySchemeType getType() { + return type; + } + + /** + * Sets the type of the security scheme. + * + * @param type The type of the security scheme. + */ + public void setType(Scheme.SecuritySchemeType type) { + this.type = type; + } + + /** + * Gets the scopes of the security scheme. + * + * @return The scopes of the security scheme. + */ + public Set getScopes() { + return scopes; + } + + /** + * Sets the scopes of the security scheme. + * + * @param scopes The scopes of the security scheme. + */ + public void setScopes(Set scopes) { + this.scopes = scopes; + } + + /** + * Gets the name of the security scheme. + * + * @return The name of the security scheme. + */ + public String getName() { + return name; + } + + /** + * Sets the name of the security scheme. + * + * @param name The name of the security scheme. + */ + public void setName(String name) { + this.name = name; + } + + /** + * Gets the location of the security scheme. + * + * @return The location of the security scheme. + */ + public String getIn() { + return in; + } + + /** + * Sets the location of the security scheme. + * + * @param in The location of the security scheme. + */ + public void setIn(String in) { + this.in = in; + } + + /** + * Gets the prefix of the security scheme. + * + * @return The prefix of the security scheme. + */ + public String getPrefix() { + return prefix; + } + + /** + * Sets the prefix of the security scheme. + * + * @param prefix The prefix of the security scheme. + */ + public void setPrefix(String prefix) { + this.prefix = prefix; + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + return jsonWriter.writeStartObject() + .writeStringField("type", type == null ? null : type.toString()) + .writeArrayField("scopes", scopes, JsonWriter::writeString) + .writeStringField("name", name) + .writeStringField("in", in) + .writeStringField("prefix", prefix) + .writeEndObject(); + } + + /** + * Deserializes a Scheme instance from the JSON data. + * + * @param jsonReader The JSON reader to deserialize from. + * @return A Scheme instance deserialized from the JSON data. + * @throws IOException If an error occurs during deserialization. + */ + public static Scheme fromJson(JsonReader jsonReader) throws IOException { + return JsonUtils.readObject(jsonReader, Scheme::new, (scheme, fieldName, reader) -> { + if ("type".equals(fieldName)) { + scheme.type = SecuritySchemeType.fromValue(reader.getString()); + } else if ("scopes".equals(fieldName)) { + List scopes = reader.readArray(JsonReader::getString); + scheme.scopes = scopes == null ? null : new HashSet<>(scopes); + } else if ("name".equals(fieldName)) { + scheme.name = reader.getString(); + } else if ("in".equals(fieldName)) { + scheme.in = reader.getString(); + } else if ("prefix".equals(fieldName)) { + scheme.prefix = reader.getString(); + } else { + reader.skipChildren(); + } + }); + } + + /** + * The type of the security scheme. + */ + public enum SecuritySchemeType { + /** + * OAuth2 security scheme. + */ + OAUTH2("OAuth2"), + + /** + * Key security scheme. + */ + KEY("Key"); + + private final String value; + + SecuritySchemeType(String value) { + this.value = value; + } + + @Override + public String toString() { + return this.value; + } + + /** + * Get the value of the security scheme type. + * + * @return The value of the security scheme type. + */ + public String value() { + return this.value; + } + + /** + * Get the security scheme type from the value. + * + * @param value The value of the security scheme type. + * @return The security scheme type. + * @throws IllegalArgumentException thrown if the value does not match any of the security scheme types. + */ + public static Scheme.SecuritySchemeType fromValue(String value) { + if ("OAuth2".equals(value)) { + return OAUTH2; + } else if ("Key".equals(value)) { + return KEY; + } else { + throw new IllegalArgumentException(value); + } + } + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/SealedChoiceSchema.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/SealedChoiceSchema.java new file mode 100644 index 0000000000..1466305732 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/SealedChoiceSchema.java @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.extension.model.codemodel; + +import com.microsoft.typespec.http.client.generator.core.extension.base.util.JsonUtils; +import com.azure.json.JsonReader; +import com.azure.json.JsonWriter; + +import java.io.IOException; + +/** + * Represents a choice of several values (ie, an 'enum'). + */ +public class SealedChoiceSchema extends ChoiceSchema { + + /** + * Creates a new instance of the SealedChoiceSchema class. + */ + public SealedChoiceSchema() { + super(); + } + + @Override + public String toString() { + return sharedToString(this, SealedChoiceSchema.class.getName()); + } + + @Override + public int hashCode() { + return sharedHashCode(this); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + if (!(other instanceof SealedChoiceSchema)) { + return false; + } + + return sharedEquals(this, (SealedChoiceSchema) other); + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + return super.toJson(jsonWriter); + } + + /** + * Deserializes a SealedChoiceSchema instance from the JSON data. + * + * @param jsonReader The JSON reader to deserialize from. + * @return A SealedChoiceSchema instance deserialized from the JSON data. + * @throws IOException If an error occurs during deserialization. + */ + public static SealedChoiceSchema fromJson(JsonReader jsonReader) throws IOException { + return JsonUtils.readObject(jsonReader, SealedChoiceSchema::new, (schema, fieldName, reader) -> { + if (!schema.tryConsumeParentProperties(schema, fieldName, reader)) { + reader.skipChildren(); + } + }); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/Security.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/Security.java new file mode 100644 index 0000000000..c24a6e4ed9 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/Security.java @@ -0,0 +1,90 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.extension.model.codemodel; + +import com.microsoft.typespec.http.client.generator.core.extension.base.util.JsonUtils; +import com.azure.json.JsonReader; +import com.azure.json.JsonSerializable; +import com.azure.json.JsonWriter; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * Represents security information. + */ +public class Security implements JsonSerializable { + private boolean authenticationRequired; + private List schemes = new ArrayList<>(); + + /** + * Creates a new instance of the Security class. + */ + public Security() { + } + + /** + * Gets whether authentication is required. + * + * @return Whether authentication is required. + */ + public boolean isAuthenticationRequired() { + return authenticationRequired; + } + + /** + * Sets whether authentication is required. + * + * @param authenticationRequired Whether authentication is required. + */ + public void setAuthenticationRequired(boolean authenticationRequired) { + this.authenticationRequired = authenticationRequired; + } + + /** + * Gets the security schemes. + * + * @return The security schemes. + */ + public List getSchemes() { + return schemes; + } + + /** + * Sets the security schemes. + * + * @param schemes The security schemes. + */ + public void setSchemes(List schemes) { + this.schemes = schemes; + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + return jsonWriter.writeStartObject() + .writeBooleanField("authenticationRequired", authenticationRequired) + .writeArrayField("schemes", schemes, JsonWriter::writeJson) + .writeEndObject(); + } + + /** + * Deserializes a Security instance from the JSON data. + * + * @param jsonReader The JSON reader to deserialize from. + * @return A Security instance deserialized from the JSON data. + * @throws IOException If an error occurs during deserialization. + */ + public static Security fromJson(JsonReader jsonReader) throws IOException { + return JsonUtils.readObject(jsonReader, Security::new, (security, fieldName, reader) -> { + if ("authenticationRequired".equals(fieldName)) { + security.authenticationRequired = reader.getBoolean(); + } else if ("schemes".equals(fieldName)) { + security.schemes = reader.readArray(Scheme::fromJson); + } else { + reader.skipChildren(); + } + }); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/SerializationFormat.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/SerializationFormat.java new file mode 100644 index 0000000000..406818083b --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/SerializationFormat.java @@ -0,0 +1,92 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.extension.model.codemodel; + +import com.microsoft.typespec.http.client.generator.core.extension.base.util.JsonUtils; +import com.azure.json.JsonReader; +import com.azure.json.JsonSerializable; +import com.azure.json.JsonWriter; + +import java.io.IOException; +import java.util.Objects; + +/** + * Represents a serialization format. + */ +public class SerializationFormat implements JsonSerializable { + private DictionaryAny extensions; + + /** + * Creates a new instance of the SerializationFormat class. + */ + public SerializationFormat() { + } + + /** + * Gets the extensions. + * + * @return The extensions. + */ + public DictionaryAny getExtensions() { + return extensions; + } + + /** + * Sets the extensions. + * + * @param extensions The extensions. + */ + public void setExtensions(DictionaryAny extensions) { + this.extensions = extensions; + } + + @Override + public String toString() { + return SerializationFormat.class.getName() + "@" + Integer.toHexString(System.identityHashCode(this)) + + "[extensions=" + Objects.toString(extensions, "") + "]"; + } + + @Override + public int hashCode() { + return Objects.hashCode(extensions); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof SerializationFormat)) { + return false; + } + + SerializationFormat rhs = ((SerializationFormat) other); + return Objects.equals(extensions, rhs.extensions); + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + return jsonWriter.writeStartObject() + .writeJsonField("extensions", extensions) + .writeEndObject(); + } + + /** + * Deserializes a SerializationFormat instance from the JSON data. + * + * @param jsonReader The JSON reader to deserialize from. + * @return A SerializationFormat instance deserialized from the JSON data. + * @throws IOException If an error occurs during deserialization. + */ + public static SerializationFormat fromJson(JsonReader jsonReader) throws IOException { + return JsonUtils.readObject(jsonReader, SerializationFormat::new, (format, fieldName, reader) -> { + if ("extensions".equals(fieldName)) { + format.extensions = DictionaryAny.fromJson(reader); + } else { + reader.skipChildren(); + } + }); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/SerializationFormats.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/SerializationFormats.java new file mode 100644 index 0000000000..058c68f03b --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/SerializationFormats.java @@ -0,0 +1,137 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.extension.model.codemodel; + +import com.microsoft.typespec.http.client.generator.core.extension.base.util.JsonUtils; +import com.azure.json.JsonReader; +import com.azure.json.JsonSerializable; +import com.azure.json.JsonWriter; + +import java.io.IOException; +import java.util.Objects; + +/** + * Represents individual serialization formats. + */ +public class SerializationFormats implements JsonSerializable { + private SerializationFormat json; + private XmlSerializationFormat xml; + private SerializationFormat protobuf; + + /** + * Creates a new instance of the SerializationFormats class. + */ + public SerializationFormats() { + } + + /** + * Gets the JSON serialization format. + * + * @return The JSON serialization format. + */ + public SerializationFormat getJson() { + return json; + } + + /** + * Sets the JSON serialization format. + * + * @param json The JSON serialization format. + */ + public void setJson(SerializationFormat json) { + this.json = json; + } + + /** + * Gets the XML serialization format. + * + * @return The XML serialization format. + */ + public XmlSerializationFormat getXml() { + return xml; + } + + /** + * Sets the XML serialization format. + * + * @param xml The XML serialization format. + */ + public void setXml(XmlSerializationFormat xml) { + this.xml = xml; + } + + /** + * Gets the Protobuf serialization format. + * + * @return The Protobuf serialization format. + */ + public SerializationFormat getProtobuf() { + return protobuf; + } + + /** + * Sets the Protobuf serialization format. + * + * @param protobuf The Protobuf serialization format. + */ + public void setProtobuf(SerializationFormat protobuf) { + this.protobuf = protobuf; + } + + @Override + public String toString() { + return SerializationFormats.class.getName() + "@" + Integer.toHexString(System.identityHashCode(this)) + + "[json=" + Objects.toString(json, "") + ",xml=" + Objects.toString(xml, "") + ",protobuf=" + + Objects.toString(protobuf, "") + "]"; + } + + @Override + public int hashCode() { + return Objects.hash(json, protobuf, xml); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof SerializationFormats)) { + return false; + } + + SerializationFormats rhs = ((SerializationFormats) other); + return Objects.equals(json, rhs.json) && Objects.equals(protobuf, rhs.protobuf) && Objects.equals(xml, rhs.xml); + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + return jsonWriter.writeStartObject() + .writeJsonField("json", json) + .writeJsonField("xml", xml) + .writeJsonField("protobuf", protobuf) + .writeEndObject(); + } + + /** + * Deserializes a SerializationFormats instance from the JSON data. + * + * @param jsonReader The JSON reader to deserialize from. + * @return A SerializationFormats instance deserialized from the JSON data. + * @throws IOException If an error occurs during deserialization. + */ + public static SerializationFormats fromJson(JsonReader jsonReader) throws IOException { + return JsonUtils.readObject(jsonReader, SerializationFormats::new, (formats, fieldName, reader) -> { + if ("json".equals(fieldName)) { + formats.json = SerializationFormat.fromJson(reader); + } else if ("xml".equals(fieldName)) { + formats.xml = XmlSerializationFormat.fromJson(reader); + } else if ("protobuf".equals(fieldName)) { + formats.protobuf = SerializationFormat.fromJson(reader); + } else { + reader.skipChildren(); + } + }); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/SerializationStyle.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/SerializationStyle.java new file mode 100644 index 0000000000..8127f2870e --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/SerializationStyle.java @@ -0,0 +1,114 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.extension.model.codemodel; + +import java.util.HashMap; +import java.util.Map; + +/** + * Represents individual serialization styles. + */ +public enum SerializationStyle { + /** + * The serialization style is binary. + */ + BINARY("binary"), + + /** + * The serialization style is deep object. + */ + DEEP_OBJECT("deepObject"), + + /** + * The serialization style is form. + */ + FORM("form"), + + /** + * The serialization style is JSON. + */ + JSON("json"), + + /** + * The serialization style is label. + */ + LABEL("label"), + + /** + * The serialization style is matrix. + */ + MATRIX("matrix"), + + /** + * The serialization style is pipe delimited. + */ + PIPE_DELIMITED("pipeDelimited"), + + /** + * The serialization style is simple. + */ + SIMPLE("simple"), + + /** + * The serialization style is space delimited. + */ + SPACE_DELIMITED("spaceDelimited"), + + /** + * The serialization style is tab delimited. + */ + TAB_DELIMITED("tabDelimited"), + + /** + * The serialization style is XML. + */ + XML("xml"); + + private final String value; + private final static Map CONSTANTS = new HashMap<>(); + + static { + for (SerializationStyle c : values()) { + CONSTANTS.put(c.value, c); + } + } + + SerializationStyle(String value) { + this.value = value; + } + + @Override + public String toString() { + if ("uri".equals(this.value)) { + return "host"; + } else { + return this.value; + } + } + + /** + * Gets the value of the serialization style. + * + * @return The value of the serialization style. + */ + public String value() { + return this.value; + } + + /** + * Gets the serialization style from its value. + * + * @param value The value of the serialization style. + * @return The serialization style. + * @throws IllegalArgumentException If the value is not a valid serialization style. + */ + public static SerializationStyle fromValue(String value) { + SerializationStyle constant = CONSTANTS.get(value); + if (constant == null) { + throw new IllegalArgumentException(value); + } else { + return constant; + } + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/Server.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/Server.java new file mode 100644 index 0000000000..9c8b8e9ca5 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/Server.java @@ -0,0 +1,111 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.extension.model.codemodel; + +import com.microsoft.typespec.http.client.generator.core.extension.base.util.JsonUtils; +import com.azure.json.JsonReader; +import com.azure.json.JsonSerializable; +import com.azure.json.JsonWriter; + +import java.io.IOException; +import java.util.List; + +/** + * Represents a server. + */ +public class Server implements JsonSerializable { + private String url; + private Languages language; + private List variables; + + /** + * Creates a new instance of the Server class. + */ + public Server() { + } + + /** + * Gets the URL of the server. + * + * @return The URL of the server. + */ + public String getUrl() { + return url; + } + + /** + * Sets the URL of the server. + * + * @param url The URL of the server. + */ + public void setUrl(String url) { + this.url = url; + } + + /** + * Gets the language-specific information for the server. + * + * @return The language-specific information for the server. + */ + public Languages getLanguage() { + return language; + } + + /** + * Sets the language-specific information for the server. + * + * @param language The language-specific information for the server. + */ + public void setLanguage(Languages language) { + this.language = language; + } + + /** + * Gets the variables for the server. + * + * @return The variables for the server. + */ + public List getVariables() { + return variables; + } + + /** + * Sets the variables for the server. + * + * @param variables The variables for the server. + */ + public void setVariables(List variables) { + this.variables = variables; + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + return jsonWriter.writeStartObject() + .writeStringField("url", url) + .writeJsonField("language", language) + .writeArrayField("variables", variables, JsonWriter::writeJson) + .writeEndObject(); + } + + /** + * Deserializes a Server instance from the JSON data. + * + * @param jsonReader The JSON reader to deserialize from. + * @return A Server instance deserialized from the JSON data. + * @throws IOException If an error occurs during deserialization. + */ + public static Server fromJson(JsonReader jsonReader) throws IOException { + return JsonUtils.readObject(jsonReader, Server::new, (server, fieldName, reader) -> { + if ("url".equals(fieldName)) { + server.url = reader.getString(); + } else if ("language".equals(fieldName)) { + server.language = Languages.fromJson(reader); + } else if ("variables".equals(fieldName)) { + server.variables = reader.readArray(Value::fromJson); + } else { + reader.skipChildren(); + } + }); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/ServiceVersion.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/ServiceVersion.java new file mode 100644 index 0000000000..8cd488d949 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/ServiceVersion.java @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.extension.model.codemodel; + +import com.microsoft.typespec.http.client.generator.core.extension.base.util.JsonUtils; +import com.azure.json.JsonReader; +import com.azure.json.JsonWriter; + +import java.io.IOException; + +/** + * Represents a service version. + */ +public class ServiceVersion extends Metadata { + /** + * Creates a new instance of the ServiceVersion class. + */ + public ServiceVersion() { + super(); + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + return super.toJson(jsonWriter); + } + + /** + * Deserializes a ServiceVersion instance from the JSON data. + * + * @param jsonReader The JSON reader to deserialize from. + * @return A ServiceVersion instance deserialized from the JSON data. + * @throws IOException If an error occurs during deserialization. + */ + public static ServiceVersion fromJson(JsonReader jsonReader) throws IOException { + return JsonUtils.readObject(jsonReader, ServiceVersion::new, (serviceVersion, fieldName, reader) -> { + if (!serviceVersion.tryConsumeParentProperties(serviceVersion, fieldName, reader)) { + reader.skipChildren(); + } + }); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/StreamResponse.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/StreamResponse.java new file mode 100644 index 0000000000..090d791cde --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/StreamResponse.java @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.extension.model.codemodel; + +import com.microsoft.typespec.http.client.generator.core.extension.base.util.JsonUtils; +import com.azure.json.JsonReader; +import com.azure.json.JsonWriter; + +import java.io.IOException; + +/** + * Represents a stream response. + */ +public class StreamResponse extends Response { + /** + * Creates a new instance of the StreamResponse class. + */ + public StreamResponse() { + super(); + } + + /** + * Whether the response is a stream. + * + * @return Whether the response is a stream. + */ + public boolean isStream() { + return true; + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + return super.toJson(jsonWriter); + } + + /** + * Deserializes a StreamResponse instance from the JSON data. + * + * @param jsonReader The JSON reader to deserialize from. + * @return A StreamResponse instance deserialized from the JSON data. + * @throws IOException If an error occurs during deserialization. + */ + public static StreamResponse fromJson(JsonReader jsonReader) throws IOException { + return JsonUtils.readObject(jsonReader, StreamResponse::new, (response, fieldName, reader) -> { + if (!response.tryConsumeParentProperties(response, fieldName, reader)) { + reader.skipChildren(); + } + }); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/StringSchema.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/StringSchema.java new file mode 100644 index 0000000000..dc76917a48 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/StringSchema.java @@ -0,0 +1,139 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.extension.model.codemodel; + +import com.microsoft.typespec.http.client.generator.core.extension.base.util.JsonUtils; +import com.azure.json.JsonReader; +import com.azure.json.JsonWriter; + +import java.io.IOException; +import java.util.Objects; + +/** + * Represents a string value. + */ +public class StringSchema extends PrimitiveSchema { + private double maxLength; + private double minLength; + private String pattern; + + /** + * Creates a new instance of the StringSchema class. + */ + public StringSchema() { + super(); + } + + /** + * Gets the maximum length of the string. + * + * @return The maximum length of the string. + */ + public double getMaxLength() { + return maxLength; + } + + /** + * Sets the maximum length of the string. + * + * @param maxLength The maximum length of the string. + */ + public void setMaxLength(double maxLength) { + this.maxLength = maxLength; + } + + /** + * Gets the minimum length of the string. + * + * @return The minimum length of the string. + */ + public double getMinLength() { + return minLength; + } + + /** + * Sets the minimum length of the string. + * + * @param minLength The minimum length of the string. + */ + public void setMinLength(double minLength) { + this.minLength = minLength; + } + + /** + * Gets a regular expression that the string must be validated against. + * + * @return A regular expression that the string must be validated against. + */ + public String getPattern() { + return pattern; + } + + /** + * Sets a regular expression that the string must be validated against. + * + * @param pattern A regular expression that the string must be validated against. + */ + public void setPattern(String pattern) { + this.pattern = pattern; + } + + @Override + public String toString() { + return StringSchema.class.getName() + "@" + Integer.toHexString(System.identityHashCode(this)) + "[maxLength=" + + maxLength + ",minLength=" + minLength + ",pattern=" + Objects.toString(pattern, "") + "]"; + } + + @Override + public int hashCode() { + return Objects.hash(pattern, maxLength, minLength); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof StringSchema)) { + return false; + } + + StringSchema rhs = ((StringSchema) other); + return maxLength == rhs.maxLength && minLength == rhs.minLength && Objects.equals(pattern, rhs.pattern); + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + return super.writeParentProperties(jsonWriter.writeStartObject()) + .writeDoubleField("maxLength", maxLength) + .writeDoubleField("minLength", minLength) + .writeStringField("pattern", pattern) + .writeEndObject(); + } + + /** + * Deserializes a StringSchema instance from the JSON data. + * + * @param jsonReader The JSON reader to deserialize from. + * @return A StringSchema instance deserialized from the JSON data. + * @throws IOException If an error occurs during deserialization. + */ + public static StringSchema fromJson(JsonReader jsonReader) throws IOException { + return JsonUtils.readObject(jsonReader, StringSchema::new, (schema, fieldName, reader) -> { + if (schema.tryConsumeParentProperties(schema, fieldName, reader)) { + return; + } + if ("maxLength".equals(fieldName)) { + schema.maxLength = reader.getDouble(); + } else if ("minLength".equals(fieldName)) { + schema.minLength = reader.getDouble(); + } else if ("pattern".equals(fieldName)) { + schema.pattern = reader.getString(); + } else { + reader.skipChildren(); + } + }); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/TestModel.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/TestModel.java new file mode 100644 index 0000000000..4955de4707 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/TestModel.java @@ -0,0 +1,67 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.extension.model.codemodel; + +import com.microsoft.typespec.http.client.generator.core.extension.base.util.JsonUtils; +import com.azure.json.JsonReader; +import com.azure.json.JsonSerializable; +import com.azure.json.JsonWriter; + +import java.io.IOException; +import java.util.List; + +/** + * Represents a test model. + */ +public class TestModel implements JsonSerializable { + private List scenarioTests; + + /** + * Creates a new instance of the TestModel class. + */ + public TestModel() { + } + + /** + * Gets the API scenario definitions. + * + * @return The API scenario definitions. + */ + public List getScenarioTests() { + return scenarioTests; + } + + /** + * Sets the API scenario definitions. + * + * @param scenarioTests The API scenario definitions. + */ + public void setScenarioTests(List scenarioTests) { + this.scenarioTests = scenarioTests; + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + return jsonWriter.writeStartObject() + .writeArrayField("scenarioTests", scenarioTests, JsonWriter::writeJson) + .writeEndObject(); + } + + /** + * Deserializes a TestModel instance from the JSON data. + * + * @param jsonReader The JSON reader to deserialize from. + * @return A TestModel instance deserialized from the JSON data. + * @throws IOException If an error occurs during deserialization. + */ + public static TestModel fromJson(JsonReader jsonReader) throws IOException { + return JsonUtils.readObject(jsonReader, TestModel::new, (model, fieldName, reader) -> { + if ("scenarioTests".equals(fieldName)) { + model.scenarioTests = reader.readArray(ScenarioTest::fromJson); + } else { + reader.skipChildren(); + } + }); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/TestScenario.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/TestScenario.java new file mode 100644 index 0000000000..64c45e054d --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/TestScenario.java @@ -0,0 +1,179 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.extension.model.codemodel; + +import com.microsoft.typespec.http.client.generator.core.extension.base.util.JsonUtils; +import com.azure.json.JsonReader; +import com.azure.json.JsonSerializable; +import com.azure.json.JsonWriter; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +/** + * Represents a test scenario. + */ +public class TestScenario implements JsonSerializable { + private String description; + private List requiredVariables; + private Map requiredVariablesDefault; + private String scenario; + private Boolean shareScope; + private List resolvedSteps; + + /** + * Creates a new instance of the TestScenario class. + */ + public TestScenario() { + } + + /** + * Gets the description of the scenario. + * + * @return The description of the scenario. + */ + public String getDescription() { + return description; + } + + /** + * Sets the description of the scenario. + * + * @param description The description of the scenario. + */ + public void setDescription(String description) { + this.description = description; + } + + /** + * Gets the required variables for the scenario. + * + * @return The required variables for the scenario. + */ + public List getRequiredVariables() { + return requiredVariables; + } + + /** + * Sets the required variables for the scenario. + * + * @param requiredVariables The required variables for the scenario. + */ + public void setRequiredVariables(List requiredVariables) { + this.requiredVariables = requiredVariables; + } + + /** + * Gets the default values of the required variables. + * + * @return The default values of the required variables. + */ + public Map getRequiredVariablesDefault() { + return requiredVariablesDefault; + } + + /** + * Sets the default values of the required variables. + * + * @param requiredVariablesDefault The default values of the required variables. + */ + public void setRequiredVariablesDefault(Map requiredVariablesDefault) { + this.requiredVariablesDefault = requiredVariablesDefault; + } + + /** + * Gets the scenario. + * + * @return The scenario. + */ + public String getScenario() { + return scenario; + } + + /** + * Sets the scenario. + * + * @param scenario The scenario. + */ + public void setScenario(String scenario) { + this.scenario = scenario; + } + + /** + * Gets whether to share the scope and prepareSteps with other scenarios. + * + * @return Whether to share the scope and prepareSteps with other scenarios. + */ + public Boolean getShareScope() { + return shareScope; + } + + /** + * Sets whether to share the scope and prepareSteps with other scenarios. + * + * @param shareScope Whether to share the scope and prepareSteps with other scenarios. + */ + public void setShareScope(Boolean shareScope) { + this.shareScope = shareScope; + } + + /** + * Gets the resolved steps. + * + * @return The resolved steps. + */ + @YamlProperty("_resolvedSteps") + public List getResolvedSteps() { + return resolvedSteps; + } + + /** + * Sets the resolved steps. + * + * @param resolvedSteps The resolved steps. + */ + public void setResolvedSteps(List resolvedSteps) { + this.resolvedSteps = resolvedSteps; + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + return jsonWriter.writeStartObject() + .writeStringField("description", description) + .writeArrayField("requiredVariables", requiredVariables, JsonWriter::writeString) + .writeMapField("requiredVariablesDefault", requiredVariablesDefault, JsonWriter::writeString) + .writeStringField("scenario", scenario) + .writeBooleanField("shareScope", shareScope) + .writeArrayField("_resolvedSteps", resolvedSteps, JsonWriter::writeJson) + .writeEndObject(); + } + + /** + * Deserializes a TestScenario instance from the JSON data. + * + * @param jsonReader The JSON reader to deserialize from. + * @return A TestScenario instance deserialized from the JSON data. + * @throws IOException If an error occurs during deserialization. + */ + public static TestScenario fromJson(JsonReader jsonReader) throws IOException { + return JsonUtils.readObject(jsonReader, TestScenario::new, (scenario, fieldName, reader) -> { + if ("description".equals(fieldName)) { + scenario.description = reader.getString(); + } else if ("requiredVariables".equals(fieldName)) { + scenario.requiredVariables = reader.readArray(JsonReader::getString); + } else if ("requiredVariablesDefault".equals(fieldName)) { + scenario.requiredVariablesDefault = reader.readMap(JsonReader::getString); + } else if ("scenario".equals(fieldName)) { + scenario.scenario = reader.getString(); + } else if ("shareScope".equals(fieldName)) { + scenario.shareScope = reader.getNullable(JsonReader::getBoolean); + } else if ("_resolvedSteps".equals(fieldName)) { + scenario.resolvedSteps = reader.readArray(ScenarioStep::fromJson); + } else { + reader.skipChildren(); + } + }); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/TestScenarioStepType.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/TestScenarioStepType.java new file mode 100644 index 0000000000..c295992900 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/TestScenarioStepType.java @@ -0,0 +1,63 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.extension.model.codemodel; + +import java.util.HashMap; +import java.util.Map; + +/** + * Represents a test scenario step type. + */ +public enum TestScenarioStepType { + + /** + * Step to run a swagger operation defined rest call. This may not be just one http call. + *

+ * If the operation is a long-running operation (LRO), then follow the LRO polling strategy. + * Response statusCode must be 200 if the LRO succeeded, no matter what code the initial response is. + * If the LRO is PUT/PATCH, the runner should automatically insert a GET after the polling to verify the resource + * update result. + * If the operation is DELETE, then after the operation, the runner should automatically insert a GET to verify + * resource cannot be found. + * Rest call step could be defined either by an example file, or by resourceName tracking and update. + */ + REST_CALL("restCall"), + /** + * Step to deploy ARM template to the scope. + */ + STEP_ARM_TEMPLATE("armTemplateDeployment"), + /** + * Step to deploy ARM deployment script to the scope. + */ + STEP_ARM_DEPLOYMENT_SCRIPT("stepArmDeploymentScript"); + + private final String value; + private static final Map CONSTANTS = new HashMap<>(); + + static { + for (TestScenarioStepType stepType : TestScenarioStepType.values()) { + CONSTANTS.put(stepType.value, stepType); + } + } + + TestScenarioStepType(String value) { + this.value = value; + } + + /** + * Gets the test scenario step type from its value. + * + * @param value The value of the test scenario step type. + * @return The test scenario step type. + * @throws IllegalArgumentException If the value is not a known test scenario step type. + */ + public static TestScenarioStepType fromValue(String value) { + TestScenarioStepType stepType = CONSTANTS.get(value); + if (stepType == null) { + throw new IllegalArgumentException(value); + } + return stepType; + } + +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/TimeSchema.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/TimeSchema.java new file mode 100644 index 0000000000..8b52d03f1f --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/TimeSchema.java @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.extension.model.codemodel; + +import com.microsoft.typespec.http.client.generator.core.extension.base.util.JsonUtils; +import com.azure.json.JsonReader; +import com.azure.json.JsonWriter; + +import java.io.IOException; + +/** + * Represents a time schema. + */ +public class TimeSchema extends PrimitiveSchema { + /** + * Creates a new instance of the TimeSchema class. + */ + public TimeSchema() { + super(); + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + return super.toJson(jsonWriter); + } + + /** + * Deserializes a TimeSchema instance from the JSON data. + * + * @param jsonReader The JSON reader to deserialize from. + * @return A TimeSchema instance deserialized from the JSON data. + * @throws IOException If an error occurs during deserialization. + */ + public static TimeSchema fromJson(JsonReader jsonReader) throws IOException { + return JsonUtils.readObject(jsonReader, TimeSchema::new, (schema, fieldName, reader) -> { + if (!schema.tryConsumeParentProperties(schema, fieldName, reader)) { + reader.skipChildren(); + } + }); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/UnixTimeSchema.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/UnixTimeSchema.java new file mode 100644 index 0000000000..f1b485e662 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/UnixTimeSchema.java @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.extension.model.codemodel; + +import com.microsoft.typespec.http.client.generator.core.extension.base.util.JsonUtils; +import com.azure.json.JsonReader; +import com.azure.json.JsonWriter; + +import java.io.IOException; + +/** + * Represents a UnixTime value. + */ +public class UnixTimeSchema extends PrimitiveSchema { + + /** + * Creates a new instance of the UnixTimeSchema class. + */ + public UnixTimeSchema() { + super(); + } + + @Override + public String toString() { + return UnixTimeSchema.class.getName() + "@" + Integer.toHexString(System.identityHashCode(this)) + "[]"; + } + + @Override + public int hashCode() { + return 1; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + return other instanceof UnixTimeSchema; + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + return super.toJson(jsonWriter); + } + + /** + * Deserializes a UnixTimeSchema instance from the JSON data. + * + * @param jsonReader The JSON reader to deserialize from. + * @return A UnixTimeSchema instance deserialized from the JSON data. + * @throws IOException If an error occurs during deserialization. + */ + public static UnixTimeSchema fromJson(JsonReader jsonReader) throws IOException { + return JsonUtils.readObject(jsonReader, UnixTimeSchema::new, (schema, fieldName, reader) -> { + if (!schema.tryConsumeParentProperties(schema, fieldName, reader)) { + reader.skipChildren(); + } + }); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/UriSchema.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/UriSchema.java new file mode 100644 index 0000000000..68474a4856 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/UriSchema.java @@ -0,0 +1,140 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.extension.model.codemodel; + +import com.microsoft.typespec.http.client.generator.core.extension.base.util.JsonUtils; +import com.azure.json.JsonReader; +import com.azure.json.JsonWriter; + +import java.io.IOException; +import java.util.Objects; + +/** + * Represents a Uri value. + */ +public class UriSchema extends PrimitiveSchema { + private double maxLength; + private double minLength; + private String pattern; + + /** + * Creates a new instance of the UriSchema class. + */ + public UriSchema() { + super(); + } + + /** + * Get the maximum length of the URI. + * + * @return The maximum length of the URI. + */ + public double getMaxLength() { + return maxLength; + } + + /** + * Set the maximum length of the URI. + * + * @param maxLength The maximum length of the URI. + */ + public void setMaxLength(double maxLength) { + this.maxLength = maxLength; + } + + /** + * Get the minimum length of the URI. + * + * @return The minimum length of the URI. + */ + public double getMinLength() { + return minLength; + } + + /** + * Set the minimum length of the URI. + * + * @param minLength The minimum length of the URI. + */ + public void setMinLength(double minLength) { + this.minLength = minLength; + } + + /** + * Get a regular expression that the URI must be validated against. + * + * @return A regular expression that the URI must be validated against. + */ + public String getPattern() { + return pattern; + } + + /** + * Set a regular expression that the URI must be validated against. + * + * @param pattern A regular expression that the URI must be validated against. + */ + public void setPattern(String pattern) { + this.pattern = pattern; + } + + @Override + public String toString() { + return UriSchema.class.getName() + "@" + Integer.toHexString(System.identityHashCode(this)) + "[maxLength=" + + maxLength + ",minLength=" + minLength + ",pattern=" + Objects.toString(pattern, "") + "]"; + } + + @Override + public int hashCode() { + return Objects.hash(pattern, maxLength, minLength); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof UriSchema)) { + return false; + } + + UriSchema rhs = ((UriSchema) other); + return maxLength == rhs.maxLength && minLength == rhs.minLength && Objects.equals(pattern, rhs.pattern); + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + return super.writeParentProperties(jsonWriter.writeStartObject()) + .writeDoubleField("maxLength", maxLength) + .writeDoubleField("minLength", minLength) + .writeStringField("pattern", pattern) + .writeEndObject(); + } + + /** + * Deserializes a UriSchema instance from the JSON data. + * + * @param jsonReader The JSON reader to deserialize from. + * @return A UriSchema instance deserialized from the JSON data. + * @throws IOException If an error occurs during deserialization. + */ + public static UriSchema fromJson(JsonReader jsonReader) throws IOException { + return JsonUtils.readObject(jsonReader, UriSchema::new, (schema, fieldName, reader) -> { + if (schema.tryConsumeParentProperties(schema, fieldName, reader)) { + return; + } + + if ("maxLength".equals(fieldName)) { + schema.maxLength = reader.getDouble(); + } else if ("minLength".equals(fieldName)) { + schema.minLength = reader.getDouble(); + } else if ("pattern".equals(fieldName)) { + schema.pattern = reader.getString(); + } else { + reader.skipChildren(); + } + }); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/UuidSchema.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/UuidSchema.java new file mode 100644 index 0000000000..aa5426a96c --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/UuidSchema.java @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.extension.model.codemodel; + +import com.microsoft.typespec.http.client.generator.core.extension.base.util.JsonUtils; +import com.azure.json.JsonReader; +import com.azure.json.JsonWriter; + +import java.io.IOException; + +/** + * Represents a Uuid value. + */ +public class UuidSchema extends PrimitiveSchema { + + /** + * Creates a new instance of the UuidSchema class. + */ + public UuidSchema() { + super(); + } + + @Override + public String toString() { + return UuidSchema.class.getName() + '@' + Integer.toHexString(System.identityHashCode(this)) + "[]"; + } + + @Override + public int hashCode() { + return 1; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + return other instanceof UuidSchema; + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + return super.toJson(jsonWriter); + } + + /** + * Deserializes a UuidSchema instance from the JSON data. + * + * @param jsonReader The JSON reader to deserialize from. + * @return A UuidSchema instance deserialized from the JSON data. + * @throws IOException If an error occurs during deserialization. + */ + public static UuidSchema fromJson(JsonReader jsonReader) throws IOException { + return JsonUtils.readObject(jsonReader, UuidSchema::new, (schema, fieldName, reader) -> { + if (!schema.tryConsumeParentProperties(schema, fieldName, reader)) { + reader.skipChildren(); + } + }); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/Value.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/Value.java new file mode 100644 index 0000000000..0c5329d495 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/Value.java @@ -0,0 +1,296 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.extension.model.codemodel; + +import com.microsoft.typespec.http.client.generator.core.extension.base.util.JsonUtils; +import com.azure.json.JsonReader; +import com.azure.json.JsonWriter; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * Represents a value. + */ +public class Value extends Metadata { + private Schema schema; + private boolean required; + private boolean nullable; + private String $key; + private String description; + private String uid; + private String summary; + private List apiVersions = new ArrayList<>(); + private Deprecation deprecated; + private ExternalDocumentation externalDocs; + + /** + * Creates a new instance of the Value class. + */ + public Value() { + } + + /** + * Gets the summary of the value. + * + * @return The summary of the value. + */ + public String getSummary() { + return summary; + } + + /** + * Sets the summary of the value. + * + * @param summary The summary of the value. + */ + public void setSummary(String summary) { + this.summary = summary; + } + + /** + * Gets the unique identifier of the value. (Required) + * + * @return The unique identifier of the value. + */ + public String getUid() { + return uid; + } + + /** + * Sets the unique identifier of the value. (Required) + * + * @param uid The unique identifier of the value. + */ + public void setUid(String uid) { + this.uid = uid; + } + + /** + * Gets the key of the value. (Required) + * + * @return The key of the value. + */ + public String get$key() { + return $key; + } + + /** + * Sets the key of the value. (Required) + * + * @param $key The key of the value. + */ + public void set$key(String $key) { + this.$key = $key; + } + + /** + * Gets the description of the value. (Required) + * + * @return The description of the value. + */ + public String getDescription() { + return description; + } + + /** + * Sets the description of the value. (Required) + * + * @param description The description of the value. + */ + public void setDescription(String description) { + this.description = description; + } + + /** + * Gets the API versions that this applies to. Undefined means all versions. + * + * @return The API versions that this applies to. Undefined means all versions. + */ + public List getApiVersions() { + return apiVersions; + } + + /** + * Sets the API versions that this applies to. Undefined means all versions. + * + * @param apiVersions The API versions that this applies to. Undefined means all versions. + */ + public void setApiVersions(List apiVersions) { + this.apiVersions = apiVersions; + } + + /** + * Gets the deprecation information for the value. + * + * @return The deprecation information for the value. + */ + public Deprecation getDeprecated() { + return deprecated; + } + + /** + * Sets the deprecation information for the value. + * + * @param deprecated The deprecation information for the value. + */ + public void setDeprecated(Deprecation deprecated) { + this.deprecated = deprecated; + } + + /** + * Gets a reference to external documentation. + * + * @return A reference to external documentation. + */ + public ExternalDocumentation getExternalDocs() { + return externalDocs; + } + + /** + * Sets a reference to external documentation. + * + * @param externalDocs A reference to external documentation. + */ + public void setExternalDocs(ExternalDocumentation externalDocs) { + this.externalDocs = externalDocs; + } + + /** + * Gets the schema of the value. (Required) + * + * @return The schema of the value. + */ + public Schema getSchema() { + return schema; + } + + /** + * Sets the schema of the value. (Required) + * + * @param schema The schema of the value. + */ + public void setSchema(Schema schema) { + // This should ideally be done by the modeler if the header collection prefix is set. + // The schema should be of type dictionary + // TODO: remove this when modeler is fixed. + if (this.getExtensions() != null && this.getExtensions().getXmsHeaderCollectionPrefix() != null + && schema instanceof StringSchema) { + DictionarySchema dictionarySchema = new DictionarySchema(); + dictionarySchema.setElementType(schema); + this.schema = dictionarySchema; + // return; + } + this.schema = schema; + } + + /** + * Gets whether the value is required. + * + * @return Whether the value is required. + */ + public boolean isRequired() { + return required; + } + + /** + * Sets whether the value is required. + * + * @param required Whether the value is required. + */ + public void setRequired(boolean required) { + this.required = required; + } + + /** + * Gets whether the value is nullable. + * + * @return Whether the value is nullable. + */ + public boolean isNullable() { + return nullable; + } + + /** + * Sets whether the value is nullable. + * + * @param nullable Whether the value is nullable. + */ + public void setNullable(boolean nullable) { + this.nullable = nullable; + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + return writeParentProperties(jsonWriter.writeStartObject()).writeEndObject(); + } + + JsonWriter writeParentProperties(JsonWriter jsonWriter) throws IOException { + return super.writeParentProperties(jsonWriter) + .writeJsonField("schema", schema) + .writeBooleanField("required", required) + .writeBooleanField("nullable", nullable) + .writeStringField("$key", $key) + .writeStringField("description", description) + .writeStringField("uid", uid) + .writeStringField("summary", summary) + .writeArrayField("apiVersions", apiVersions, JsonWriter::writeJson) + .writeJsonField("deprecated", deprecated) + .writeJsonField("externalDocs", externalDocs); + } + + /** + * Deserializes a Value instance from the JSON data. + * + * @param jsonReader The JSON reader to deserialize from. + * @return A Value instance deserialized from the JSON data. + * @throws IOException If an error occurs during deserialization. + */ + public static Value fromJson(JsonReader jsonReader) throws IOException { + return JsonUtils.readObject(jsonReader, Value::new, (value, fieldName, reader) -> { + if (!value.tryConsumeParentProperties(value, fieldName, reader)) { + reader.skipChildren(); + } + }); + } + + boolean tryConsumeParentProperties(Value value, String fieldName, JsonReader reader) throws IOException { + if (super.tryConsumeParentProperties(value, fieldName, reader)) { + return true; + } else if ("schema".equals(fieldName)) { + value.schema = Schema.fromJson(reader); + return true; + } else if ("required".equals(fieldName)) { + value.required = reader.getBoolean(); + return true; + } else if ("nullable".equals(fieldName)) { + value.nullable = reader.getBoolean(); + return true; + } else if ("$key".equals(fieldName)) { + value.$key = reader.getString(); + return true; + } else if ("description".equals(fieldName)) { + value.description = reader.getString(); + return true; + } else if ("uid".equals(fieldName)) { + value.uid = reader.getString(); + return true; + } else if ("summary".equals(fieldName)) { + value.summary = reader.getString(); + return true; + } else if ("apiVersions".equals(fieldName)) { + value.apiVersions = reader.readArray(ApiVersion::fromJson); + return true; + } else if ("deprecated".equals(fieldName)) { + value.deprecated = Deprecation.fromJson(reader); + return true; + } else if ("externalDocs".equals(fieldName)) { + value.externalDocs = ExternalDocumentation.fromJson(reader); + return true; + } + + return false; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/ValueSchema.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/ValueSchema.java new file mode 100644 index 0000000000..78bc342c02 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/ValueSchema.java @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.extension.model.codemodel; + +import com.microsoft.typespec.http.client.generator.core.extension.base.util.JsonUtils; +import com.azure.json.JsonReader; +import com.azure.json.JsonWriter; + +import java.io.IOException; + +/** + * Schema types that are non-object or complex types. + */ +public class ValueSchema extends Schema { + /** + * Creates a new instance of the ValueSchema class. + */ + public ValueSchema() { + super(); + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + return super.toJson(jsonWriter); + } + + /** + * Deserializes a ValueSchema instance from the JSON data. + * + * @param jsonReader The JSON reader to deserialize from. + * @return A ValueSchema instance deserialized from the JSON data. + * @throws IOException If an error occurs during deserialization. + */ + public static ValueSchema fromJson(JsonReader jsonReader) throws IOException { + return JsonUtils.readObject(jsonReader, ValueSchema::new, (schema, fieldName, reader) -> { + if (!schema.tryConsumeParentProperties(schema, fieldName, reader)) { + reader.skipChildren(); + } + }); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/XmlSerializationFormat.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/XmlSerializationFormat.java new file mode 100644 index 0000000000..7dad4af0e7 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/XmlSerializationFormat.java @@ -0,0 +1,206 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.extension.model.codemodel; + +import com.microsoft.typespec.http.client.generator.core.extension.base.util.JsonUtils; +import com.azure.json.JsonReader; +import com.azure.json.JsonWriter; + +import java.io.IOException; +import java.util.Objects; + +/** + * Represents the XML serialization format. + */ +public class XmlSerializationFormat extends SerializationFormat { + private String name; + private String namespace; + private String prefix; + private boolean attribute; + private boolean wrapped; + private boolean text; + + /** + * Creates a new instance of the XmlSerializationFormat class. + */ + public XmlSerializationFormat() { + } + + /** + * Gets the name of the XML element. + * + * @return The name of the XML element. + */ + public String getName() { + return name; + } + + /** + * Sets the name of the XML element. + * + * @param name The name of the XML element. + */ + public void setName(String name) { + this.name = name; + } + + /** + * Gets the namespace of the XML element. + * + * @return The namespace of the XML element. + */ + public String getNamespace() { + return namespace; + } + + /** + * Sets the namespace of the XML element. + * + * @param namespace The namespace of the XML element. + */ + public void setNamespace(String namespace) { + this.namespace = namespace; + } + + /** + * Gets the prefix of the XML element. + * + * @return The prefix of the XML element. + */ + public String getPrefix() { + return prefix; + } + + /** + * Sets the prefix of the XML element. + * + * @param prefix The prefix of the XML element. + */ + public void setPrefix(String prefix) { + this.prefix = prefix; + } + + /** + * Gets whether the XML element is an attribute. + * + * @return Whether the XML element is an attribute. + */ + public boolean isAttribute() { + return attribute; + } + + /** + * Sets whether the XML element is an attribute. + * + * @param attribute Whether the XML element is an attribute. + */ + public void setAttribute(boolean attribute) { + this.attribute = attribute; + } + + /** + * Gets whether the XML element is wrapped. + * + * @return Whether the XML element is wrapped. + */ + public boolean isWrapped() { + return wrapped; + } + + /** + * Sets whether the XML element is wrapped. + * + * @param wrapped Whether the XML element is wrapped. + */ + public void setWrapped(boolean wrapped) { + this.wrapped = wrapped; + } + + /** + * Gets whether the XML element is text. + * + * @return Whether the XML element is text. + */ + public boolean isText() { + return text; + } + + /** + * Sets whether the XML element is text. + * + * @param text Whether the XML element is text. + */ + public void setText(boolean text) { + this.text = text; + } + + @Override + public String toString() { + return XmlSerializationFormat.class.getName() + "@" + Integer.toHexString(System.identityHashCode(this)) + + "[name=" + Objects.toString(name, "") + ",namespace=" + Objects.toString(namespace, "") + + ",prefix=" + Objects.toString(prefix, "") + ",attribute=" + attribute + ",wrapped=" + wrapped + + ",text=" + text + "]"; + } + + @Override + public int hashCode() { + return Objects.hash(name, namespace, attribute, wrapped, prefix, text); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof XmlSerializationFormat)) { + return false; + } + XmlSerializationFormat rhs = ((XmlSerializationFormat) other); + return Objects.equals(name, rhs.name) && Objects.equals(namespace, rhs.namespace) && attribute == rhs.attribute + && wrapped == rhs.wrapped && Objects.equals(prefix, rhs.prefix) && text == rhs.text; + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + return jsonWriter.writeStartObject() + .writeJsonField("extensions", getExtensions()) + .writeStringField("name", name) + .writeStringField("namespace", namespace) + .writeStringField("prefix", prefix) + .writeBooleanField("attribute", attribute) + .writeBooleanField("wrapped", wrapped) + .writeBooleanField("text", text) + .writeEndObject(); + } + + /** + * Deserializes a XmlSerializationFormat instance from the JSON data. + * + * @param jsonReader The JSON reader to deserialize from. + * @return A XmlSerializationFormat instance deserialized from the JSON data. + * @throws IOException If an error occurs during deserialization. + */ + public static XmlSerializationFormat fromJson(JsonReader jsonReader) throws IOException { + return JsonUtils.readObject(jsonReader, XmlSerializationFormat::new, (format, fieldName, reader) -> { + if ("extensions".equals(fieldName)) { + format.setExtensions(DictionaryAny.fromJson(reader)); + } else if ("name".equals(fieldName)) { + format.name = reader.getString(); + } else if ("namespace".equals(fieldName)) { + format.namespace = reader.getString(); + } else if ("prefix".equals(fieldName)) { + format.prefix = reader.getString(); + } else if ("attribute".equals(fieldName)) { + format.attribute = reader.getBoolean(); + } else if ("wrapped".equals(fieldName)) { + format.wrapped = reader.getBoolean(); + } else if ("text".equals(fieldName)) { + format.text = reader.getBoolean(); + } else { + reader.skipChildren(); + } + }); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/XorSchema.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/XorSchema.java new file mode 100644 index 0000000000..0e7515bb2d --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/XorSchema.java @@ -0,0 +1,97 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.extension.model.codemodel; + +import com.microsoft.typespec.http.client.generator.core.extension.base.util.JsonUtils; +import com.azure.json.JsonReader; +import com.azure.json.JsonWriter; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + * Represents an XOR relationship between several schemas + */ +public class XorSchema extends ComplexSchema { + private List oneOf = new ArrayList<>(); + + /** + * Creates a new instance of the XorSchema class. + */ + public XorSchema() { + super(); + } + + /** + * Gets the set of schemas that this must be one and only one of. (Required) + * + * @return The set of schemas that this must be one and only one of. + */ + public List getOneOf() { + return oneOf; + } + + /** + * Sets the set of schemas that this must be one and only one of. (Required) + * + * @param oneOf The set of schemas that this must be one and only one of. + */ + public void setOneOf(List oneOf) { + this.oneOf = oneOf; + } + + @Override + public String toString() { + return XorSchema.class.getName() + "@" + Integer.toHexString(System.identityHashCode(this)) + "[oneOf=" + + Objects.toString(oneOf, "") + "]"; + } + + @Override + public int hashCode() { + return Objects.hashCode(oneOf); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof XorSchema)) { + return false; + } + + XorSchema rhs = ((XorSchema) other); + return Objects.equals(oneOf, rhs.oneOf); + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + return super.writeParentProperties(jsonWriter.writeStartObject()) + .writeArrayField("oneOf", oneOf, JsonWriter::writeJson) + .writeEndObject(); + } + /** + * Deserializes a XorSchema instance from the JSON data. + * + * @param jsonReader The JSON reader to deserialize from. + * @return A XorSchema instance deserialized from the JSON data. + * @throws IOException If an error occurs during deserialization. + */ + public static XorSchema fromJson(JsonReader jsonReader) throws IOException { + return JsonUtils.readObject(jsonReader, XorSchema::new, (schema, fieldName, reader) -> { + if (schema.tryConsumeParentProperties(schema, fieldName, reader)) { + return; + } + + if ("oneOf".equals(fieldName)) { + schema.oneOf = reader.readArray(Schema::fromJson); + } else { + reader.skipChildren(); + } + }); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/YamlProperty.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/YamlProperty.java new file mode 100644 index 0000000000..16bcf4db3b --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/YamlProperty.java @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.extension.model.codemodel; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Customize a property mapping between a yaml property and a java property + * It's currently a hint to let specific yaml constructor to decide whether serialize or deserialize + * according to this annotation. + * + * @see AnnotatedPropertyUtils + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface YamlProperty { + + /** + * The property name to read from yaml. + * @return the property name to read from yaml + */ + String value(); +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/package-info.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/package-info.java new file mode 100644 index 0000000000..a6ea77b64a --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/codemodel/package-info.java @@ -0,0 +1,7 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/** + * Contains models for code models. + */ +package com.microsoft.typespec.http.client.generator.core.extension.model.codemodel; diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/extensionmodel/AllowedResource.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/extensionmodel/AllowedResource.java new file mode 100644 index 0000000000..e85b045599 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/extensionmodel/AllowedResource.java @@ -0,0 +1,90 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.extension.model.extensionmodel; + +import com.azure.json.JsonReader; +import com.azure.json.JsonSerializable; +import com.azure.json.JsonWriter; + +import java.io.IOException; +import java.util.List; + +import static com.microsoft.typespec.http.client.generator.core.extension.base.util.JsonUtils.readObject; + +/** + * Represents a resource that is allowed to be accessed. + */ +public class AllowedResource implements JsonSerializable { + private List scopes; + private String type; + + /** + * Creates a new instance of the AllowedResource class. + */ + public AllowedResource() { + } + + /** + * Gets the scopes that are allowed to access the resource. + * + * @return The scopes that are allowed to access the resource. + */ + public List getScopes() { + return scopes; + } + + /** + * Sets the scopes that are allowed to access the resource. + * + * @param scopes The scopes that are allowed to access the resource. + */ + public void setScopes(List scopes) { + this.scopes = scopes; + } + + /** + * Gets the type of the resource. + * + * @return The type of the resource. + */ + public String getType() { + return type; + } + + /** + * Sets the type of the resource. + * + * @param type The type of the resource. + */ + public void setType(String type) { + this.type = type; + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + return jsonWriter.writeStartObject() + .writeArrayField("scopes", scopes, JsonWriter::writeString) + .writeStringField("type", type) + .writeEndObject(); + } + + /** + * Deserializes an AllowedResource instance from the JSON data. + * + * @param jsonReader The JSON reader to deserialize from. + * @return An AllowedResource instance deserialized from the JSON data. + * @throws IOException If an error occurs during deserialization. + */ + public static AllowedResource fromJson(JsonReader jsonReader) throws IOException { + return readObject(jsonReader, AllowedResource::new, (allowedResource, fieldName, reader) -> { + if ("scopes".equals(fieldName)) { + allowedResource.scopes = reader.readArray(JsonReader::getString); + } else if ("type".equals(fieldName)) { + allowedResource.type = reader.getString(); + } else { + reader.skipChildren(); + } + }); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/extensionmodel/XmsArmIdDetails.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/extensionmodel/XmsArmIdDetails.java new file mode 100644 index 0000000000..248a6729da --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/extensionmodel/XmsArmIdDetails.java @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.extension.model.extensionmodel; + +import com.azure.json.JsonReader; +import com.azure.json.JsonSerializable; +import com.azure.json.JsonWriter; + +import java.io.IOException; +import java.util.List; + +import static com.microsoft.typespec.http.client.generator.core.extension.base.util.JsonUtils.readObject; + +/** + * Represents the details of an ARM ID. + */ +public class XmsArmIdDetails implements JsonSerializable { + private List allowedResources; + + /** + * Creates a new instance of the XmsArmIdDetails class. + */ + public XmsArmIdDetails() { + } + + /** + * Gets the resources that are allowed to be accessed. + * + * @return The resources that are allowed to be accessed. + */ + public List getAllowedResources() { + return allowedResources; + } + + /** + * Sets the resources that are allowed to be accessed. + * + * @param allowedResources The resources that are allowed to be accessed. + */ + public void setAllowedResources(List allowedResources) { + this.allowedResources = allowedResources; + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + return jsonWriter.writeStartObject() + .writeArrayField("allowedResources", allowedResources, JsonWriter::writeJson) + .writeEndObject(); + } + + /** + * Deserializes an XmsArmIdDetails instance from the JSON data. + * + * @param jsonReader The JSON reader to deserialize from. + * @return An XmsArmIdDetails instance deserialized from the JSON data. + * @throws IOException If an error occurs during deserialization. + */ + public static XmsArmIdDetails fromJson(JsonReader jsonReader) throws IOException { + return readObject(jsonReader, XmsArmIdDetails::new, (xmsArmIdDetails, fieldName, reader) -> { + if ("allowedResources".equals(fieldName)) { + xmsArmIdDetails.allowedResources = reader.readArray(AllowedResource::fromJson); + } else { + reader.skipChildren(); + } + }); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/extensionmodel/XmsEnum.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/extensionmodel/XmsEnum.java new file mode 100644 index 0000000000..b0778caa9b --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/extensionmodel/XmsEnum.java @@ -0,0 +1,211 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.extension.model.extensionmodel; + +import com.azure.json.JsonReader; +import com.azure.json.JsonSerializable; +import com.azure.json.JsonWriter; + +import java.io.IOException; +import java.util.List; + +import static com.microsoft.typespec.http.client.generator.core.extension.base.util.JsonUtils.readObject; + +/** + * Represents an enum. + */ +public class XmsEnum implements JsonSerializable { + private String name; + private boolean modelAsString = false; + private List values; + + /** + * Creates a new instance of the XmsEnum class. + */ + public XmsEnum() { + } + + /** + * Gets the name of the enum. + * + * @return The name of the enum. + */ + public String getName() { + return name; + } + + /** + * Sets the name of the enum. + * + * @param name The name of the enum. + */ + public void setName(String name) { + this.name = name; + } + + /** + * Gets whether the enum is represented as a string. + * + * @return Whether the enum is represented as a string. + */ + public boolean isModelAsString() { + return modelAsString; + } + + /** + * Sets whether the enum is represented as a string. + * + * @param modelAsString Whether the enum is represented as a string. + */ + public void setModelAsString(boolean modelAsString) { + this.modelAsString = modelAsString; + } + + /** + * Gets the values of the enum. + * + * @return The values of the enum. + */ + public List getValues() { + return values; + } + + /** + * Sets the values of the enum. + * + * @param values The values of the enum. + */ + public void setValues(List values) { + this.values = values; + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + return jsonWriter.writeStartObject() + .writeStringField("name", name) + .writeBooleanField("modelAsString", modelAsString) + .writeArrayField("values", values, JsonWriter::writeJson) + .writeEndObject(); + } + + /** + * Deserializes an XmsEnum instance from the JSON data. + * + * @param jsonReader The JSON reader to deserialize from. + * @return An XmsEnum instance deserialized from the JSON data. + * @throws IOException If an error occurs during deserialization. + */ + public static XmsEnum fromJson(JsonReader jsonReader) throws IOException { + return readObject(jsonReader, XmsEnum::new, (xmsEnum, fieldName, reader) -> { + if ("name".equals(fieldName)) { + xmsEnum.name = reader.getString(); + } else if ("modelAsString".equals(fieldName)) { + xmsEnum.modelAsString = reader.getBoolean(); + } else if ("values".equals(fieldName)) { + xmsEnum.values = reader.readArray(Value::fromJson); + } else { + reader.skipChildren(); + } + }); + } + + /** + * Represents a value of the enum. + */ + public static class Value implements JsonSerializable { + private String value; + private String description; + private String name; + + /** + * Creates a new instance of the Value class. + */ + public Value() { + } + + /** + * Gets the value of the enum. + * + * @return The value of the enum. + */ + public String getValue() { + return value; + } + + /** + * Sets the value of the enum. + * + * @param value The value of the enum. + */ + public void setValue(String value) { + this.value = value; + } + + /** + * Gets the description of the value. + * + * @return The description of the value. + */ + public String getDescription() { + return description; + } + + /** + * Sets the description of the value. + * + * @param description The description of the value. + */ + public void setDescription(String description) { + this.description = description; + } + + /** + * Gets the name of the value. + * + * @return The name of the value. + */ + public String getName() { + return name; + } + + /** + * Sets the name of the value. + * + * @param name The name of the value. + */ + public void setName(String name) { + this.name = name; + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + return jsonWriter.writeStartObject() + .writeStringField("value", value) + .writeStringField("description", description) + .writeStringField("name", name) + .writeEndObject(); + } + + /** + * Deserializes a Value instance from the JSON data. + * + * @param jsonReader The JSON reader to deserialize from. + * @return A Value instance deserialized from the JSON data. + * @throws IOException If an error occurs during deserialization. + */ + public static Value fromJson(JsonReader jsonReader) throws IOException { + return readObject(jsonReader, Value::new, (value, fieldName, reader) -> { + if ("value".equals(fieldName)) { + value.value = reader.getString(); + } else if ("description".equals(fieldName)) { + value.description = reader.getString(); + } else if ("name".equals(fieldName)) { + value.name = reader.getString(); + } else { + reader.skipChildren(); + } + }); + } + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/extensionmodel/XmsExamples.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/extensionmodel/XmsExamples.java new file mode 100644 index 0000000000..e5456ad41f --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/extensionmodel/XmsExamples.java @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.extension.model.extensionmodel; + +import com.azure.json.JsonReader; +import com.azure.json.JsonSerializable; +import com.azure.json.JsonWriter; + +import java.io.IOException; +import java.util.Map; + +import static com.microsoft.typespec.http.client.generator.core.extension.base.util.JsonUtils.readObject; + +/** + * Represents the examples of a model. + */ +public class XmsExamples implements JsonSerializable { + private Map examples; + + /** + * Creates a new instance of the XmsExamples class. + */ + public XmsExamples() { + } + + /** + * Gets the examples of the model. + * + * @return The examples of the model. + */ + public Map getExamples() { + return examples; + } + + /** + * Sets the examples of the model. + * + * @param examples The examples of the model. + */ + public void setExamples(Map examples) { + this.examples = examples; + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + return jsonWriter.writeStartObject() + .writeMapField("examples", examples, JsonWriter::writeUntyped) + .writeEndObject(); + } + + /** + * Deserializes an XmsExamples instance from the JSON data. + * + * @param jsonReader The JSON reader to deserialize from. + * @return An XmsExamples instance deserialized from the JSON data. + * @throws IOException If an error occurs during deserialization. + */ + public static XmsExamples fromJson(JsonReader jsonReader) throws IOException { + return readObject(jsonReader, XmsExamples::new, (xmsExamples, fieldName, reader) -> { + if ("examples".equals(fieldName)) { + xmsExamples.examples = reader.readMap(JsonReader::readUntyped); + } else { + reader.skipChildren(); + } + }); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/extensionmodel/XmsExtensions.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/extensionmodel/XmsExtensions.java new file mode 100644 index 0000000000..fb8b3de5b9 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/extensionmodel/XmsExtensions.java @@ -0,0 +1,397 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.extension.model.extensionmodel; + +import com.microsoft.typespec.http.client.generator.core.extension.base.util.JsonUtils; +import com.azure.json.JsonReader; +import com.azure.json.JsonSerializable; +import com.azure.json.JsonWriter; + +import java.io.IOException; +import java.util.List; + +/** + * Represents the x-ms-extensions of a model. + */ +public class XmsExtensions implements JsonSerializable { + private XmsEnum xmsEnum; + private String xmsClientName; + private XmsPageable xmsPageable; + private boolean xmsSkipUrlEncoding; + private boolean xmsClientFlatten; + private boolean xmsLongRunningOperation; + private XmsLongRunningOperationOptions xmsLongRunningOperationOptions; + private boolean xmsFlattened; + private boolean xmsAzureResource; + private List xmsMutability; + private String xmsHeaderCollectionPrefix; + private XmsInternalAutorestAnonymousSchema xmsInternalAutorestAnonymousSchema; + private XmsArmIdDetails xmsArmIdDetails; + private XmsExamples xmsExamples; + private Boolean xmsSecret; + private List xmsVersioningAdded; + + /** + * Creates a new instance of the XmsExtensions class. + */ + public XmsExtensions() { + } + + /** + * Gets the enum of the model. + * + * @return The enum of the model. + */ + public XmsEnum getXmsEnum() { + return xmsEnum; + } + + /** + * Sets the enum of the model. + * + * @param xmsEnum The enum of the model. + */ + public void setXmsEnum(XmsEnum xmsEnum) { + this.xmsEnum = xmsEnum; + } + + /** + * Gets the client name of the model. + * + * @return The client name of the model. + */ + public String getXmsClientName() { + return xmsClientName; + } + + /** + * Sets the client name of the model. + * + * @param xmsClientName The client name of the model. + */ + public void setXmsClientName(String xmsClientName) { + this.xmsClientName = xmsClientName; + } + + /** + * Gets the pageable of the model. + * + * @return The pageable of the model. + */ + public XmsPageable getXmsPageable() { + return xmsPageable; + } + + /** + * Sets the pageable of the model. + * + * @param xmsPageable The pageable of the model. + */ + public void setXmsPageable(XmsPageable xmsPageable) { + this.xmsPageable = xmsPageable; + } + + /** + * Gets the skip URL encoding of the model. + * + * @return The skip URL encoding of the model. + */ + public boolean isXmsSkipUrlEncoding() { + return xmsSkipUrlEncoding; + } + + /** + * Sets the skip URL encoding of the model. + * + * @param xmsSkipUrlEncoding The skip URL encoding of the model. + */ + public void setXmsSkipUrlEncoding(boolean xmsSkipUrlEncoding) { + this.xmsSkipUrlEncoding = xmsSkipUrlEncoding; + } + + /** + * Gets the client flatten of the model. + * + * @return The client flatten of the model. + */ + public boolean isXmsClientFlatten() { + return xmsClientFlatten; + } + + /** + * Sets the client flatten of the model. + * + * @param xmsClientFlatten The client flatten of the model. + */ + public void setXmsClientFlatten(boolean xmsClientFlatten) { + this.xmsClientFlatten = xmsClientFlatten; + } + + /** + * Gets the long-running operation of the model. + * + * @return The long-running operation of the model. + */ + public boolean isXmsLongRunningOperation() { + return xmsLongRunningOperation; + } + + /** + * Sets the long-running operation of the model. + * + * @param xmsLongRunningOperation The long-running operation of the model. + */ + public void setXmsLongRunningOperation(boolean xmsLongRunningOperation) { + this.xmsLongRunningOperation = xmsLongRunningOperation; + } + + /** + * Gets the flattened of the model. + * + * @return The flattened of the model. + */ + public boolean isXmsFlattened() { + return xmsFlattened; + } + + /** + * Sets the flattened of the model. + * + * @param xmsFlattened The flattened of the model. + */ + public void setXmsFlattened(boolean xmsFlattened) { + this.xmsFlattened = xmsFlattened; + } + + /** + * Gets the Azure resource of the model. + * + * @return The Azure resource of the model. + */ + public boolean isXmsAzureResource() { + return xmsAzureResource; + } + + /** + * Sets the Azure resource of the model. + * + * @param xmsAzureResource The Azure resource of the model. + */ + public void setXmsAzureResource(boolean xmsAzureResource) { + this.xmsAzureResource = xmsAzureResource; + } + + /** + * Gets the mutability of the model. + * + * @return The mutability of the model. + */ + public List getXmsMutability() { + return xmsMutability; + } + + /** + * Sets the mutability of the model. + * + * @param xmsMutability The mutability of the model. + */ + public void setXmsMutability(List xmsMutability) { + this.xmsMutability = xmsMutability; + } + + /** + * Gets the header collection prefix of the model. + * + * @return The header collection prefix of the model. + */ + public String getXmsHeaderCollectionPrefix() { + return xmsHeaderCollectionPrefix; + } + + /** + * Sets the header collection prefix of the model. + * + * @param xmsHeaderCollectionPrefix The header collection prefix of the model. + */ + public void setXmsHeaderCollectionPrefix(String xmsHeaderCollectionPrefix) { + this.xmsHeaderCollectionPrefix = xmsHeaderCollectionPrefix; + } + + /** + * Gets the internal autorest anonymous schema of the model. + * + * @return The internal autorest anonymous schema of the model. + */ + public XmsInternalAutorestAnonymousSchema getXmsInternalAutorestAnonymousSchema() { + return xmsInternalAutorestAnonymousSchema; + } + + /** + * Sets the internal autorest anonymous schema of the model. + * + * @param xmsInternalAutorestAnonymousSchema The internal autorest anonymous schema of the model. + */ + public void setXmsInternalAutorestAnonymousSchema(XmsInternalAutorestAnonymousSchema xmsInternalAutorestAnonymousSchema) { + this.xmsInternalAutorestAnonymousSchema = xmsInternalAutorestAnonymousSchema; + } + + /** + * Gets the long-running operation options of the model. + * + * @return The long-running operation options of the model. + */ + public XmsLongRunningOperationOptions getXmsLongRunningOperationOptions() { + return xmsLongRunningOperationOptions; + } + + /** + * Sets the long-running operation options of the model. + * + * @param xmsLongRunningOperationOptions The long-running operation options of the model. + */ + public void setXmsLongRunningOperationOptions(XmsLongRunningOperationOptions xmsLongRunningOperationOptions) { + this.xmsLongRunningOperationOptions = xmsLongRunningOperationOptions; + } + + /** + * Gets the ARM ID details of the model. + * + * @return The ARM ID details of the model. + */ + public XmsArmIdDetails getXmsArmIdDetails() { + return xmsArmIdDetails; + } + + /** + * Sets the ARM ID details of the model. + * + * @param xmsArmIdDetails The ARM ID details of the model. + */ + public void setXmsArmIdDetails(XmsArmIdDetails xmsArmIdDetails) { + this.xmsArmIdDetails = xmsArmIdDetails; + } + + /** + * Gets the examples of the model. + * + * @return The examples of the model. + */ + public XmsExamples getXmsExamples() { + return xmsExamples; + } + + /** + * Sets the examples of the model. + * + * @param xmsExamples The examples of the model. + */ + public void setXmsExamples(XmsExamples xmsExamples) { + this.xmsExamples = xmsExamples; + } + + /** + * Gets the secret of the model. + * + * @return The secret of the model. + */ + public Boolean getXmsSecret() { + return xmsSecret; + } + + /** + * Sets the secret of the model. + * + * @param xmsSecret The secret of the model. + */ + public void setXmsSecret(Boolean xmsSecret) { + this.xmsSecret = xmsSecret; + } + + /** + * Gets the versioning added of the model. + * + * @return The versioning added of the model. + */ + public List getXmsVersioningAdded() { + return xmsVersioningAdded; + } + + /** + * Sets the versioning added of the model. + * + * @param xmsVersioningAdded The versioning added of the model. + */ + public void setXmsVersioningAdded(List xmsVersioningAdded) { + this.xmsVersioningAdded = xmsVersioningAdded; + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + return jsonWriter.writeStartObject() + .writeJsonField("xmsEnum", xmsEnum) + .writeStringField("xmsClientName", xmsClientName) + .writeJsonField("xmsPageable", xmsPageable) + .writeBooleanField("xmsSkipUrlEncoding", xmsSkipUrlEncoding) + .writeBooleanField("xmsClientFlatten", xmsClientFlatten) + .writeBooleanField("xmsLongRunningOperation", xmsLongRunningOperation) + .writeJsonField("xmsLongRunningOperationOptions", xmsLongRunningOperationOptions) + .writeBooleanField("xmsFlattened", xmsFlattened) + .writeBooleanField("xmsAzureResource", xmsAzureResource) + .writeArrayField("xmsMutability", xmsMutability, JsonWriter::writeString) + .writeStringField("xmsHeaderCollectionPrefix", xmsHeaderCollectionPrefix) + .writeJsonField("xmsInternalAutorestAnonymousSchema", xmsInternalAutorestAnonymousSchema) + .writeJsonField("xmsArmIdDetails", xmsArmIdDetails) + .writeJsonField("xmsExamples", xmsExamples) + .writeBooleanField("xmsSecret", xmsSecret) + .writeArrayField("xmsVersioningAdded", xmsVersioningAdded, JsonWriter::writeString) + .writeEndObject(); + } + + /** + * Deserializes an XmsExtensions instance from the JSON data. + * + * @param jsonReader The JSON reader to deserialize from. + * @return An XmsExtensions instance deserialized from the JSON data. + * @throws IOException If an error occurs during deserialization. + */ + public static XmsExtensions fromJson(JsonReader jsonReader) throws IOException { + return JsonUtils.readObject(jsonReader, XmsExtensions::new, (extensions, fieldName, reader) -> { + if ("xmsEnum".equals(fieldName)) { + extensions.xmsEnum = XmsEnum.fromJson(reader); + } else if ("xmsClientName".equals(fieldName)) { + extensions.xmsClientName = reader.getString(); + } else if ("xmsPageable".equals(fieldName)) { + extensions.xmsPageable = XmsPageable.fromJson(reader); + } else if ("xmsSkipUrlEncoding".equals(fieldName)) { + extensions.xmsSkipUrlEncoding = reader.getBoolean(); + } else if ("xmsClientFlatten".equals(fieldName)) { + extensions.xmsClientFlatten = reader.getBoolean(); + } else if ("xmsLongRunningOperation".equals(fieldName)) { + extensions.xmsLongRunningOperation = reader.getBoolean(); + } else if ("xmsLongRunningOperationOptions".equals(fieldName)) { + extensions.xmsLongRunningOperationOptions = XmsLongRunningOperationOptions.fromJson(reader); + } else if ("xmsFlattened".equals(fieldName)) { + extensions.xmsFlattened = reader.getBoolean(); + } else if ("xmsAzureResource".equals(fieldName)) { + extensions.xmsAzureResource = reader.getBoolean(); + } else if ("xmsMutability".equals(fieldName)) { + extensions.xmsMutability = reader.readArray(JsonReader::getString); + } else if ("xmsHeaderCollectionPrefix".equals(fieldName)) { + extensions.xmsHeaderCollectionPrefix = reader.getString(); + } else if ("xmsInternalAutorestAnonymousSchema".equals(fieldName)) { + extensions.xmsInternalAutorestAnonymousSchema = XmsInternalAutorestAnonymousSchema.fromJson(reader); + } else if ("xmsArmIdDetails".equals(fieldName)) { + extensions.xmsArmIdDetails = XmsArmIdDetails.fromJson(reader); + } else if ("xmsExamples".equals(fieldName)) { + extensions.xmsExamples = XmsExamples.fromJson(reader); + } else if ("xmsSecret".equals(fieldName)) { + extensions.xmsSecret = reader.getNullable(JsonReader::getBoolean); + } else if ("xmsVersioningAdded".equals(fieldName)) { + extensions.xmsVersioningAdded = reader.readArray(JsonReader::getString); + } else { + reader.skipChildren(); + } + }); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/extensionmodel/XmsInternalAutorestAnonymousSchema.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/extensionmodel/XmsInternalAutorestAnonymousSchema.java new file mode 100644 index 0000000000..2e2e474f16 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/extensionmodel/XmsInternalAutorestAnonymousSchema.java @@ -0,0 +1,67 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.extension.model.extensionmodel; + +import com.azure.json.JsonReader; +import com.azure.json.JsonSerializable; +import com.azure.json.JsonWriter; + +import java.io.IOException; + +import static com.microsoft.typespec.http.client.generator.core.extension.base.util.JsonUtils.readObject; + +/** + * Represents an anonymous schema. + */ +public class XmsInternalAutorestAnonymousSchema implements JsonSerializable { + private boolean anonymous = true; + + /** + * Creates a new instance of the XmsInternalAutorestAnonymousSchema class. + */ + public XmsInternalAutorestAnonymousSchema() { + } + + /** + * Gets whether the schema is anonymous. + * + * @return Whether the schema is anonymous. + */ + public boolean isAnonymous() { + return anonymous; + } + + /** + * Sets whether the schema is anonymous. + * + * @param anonymous Whether the schema is anonymous. + */ + public void setAnonymous(boolean anonymous) { + this.anonymous = anonymous; + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + return jsonWriter.writeStartObject() + .writeBooleanField("anonymous", anonymous) + .writeEndObject(); + } + + /** + * Deserializes an XmsInternalAutorestAnonymousSchema instance from the JSON data. + * + * @param jsonReader The JSON reader to deserialize from. + * @return An XmsInternalAutorestAnonymousSchema instance deserialized from the JSON data. + * @throws IOException If an error occurs during deserialization. + */ + public static XmsInternalAutorestAnonymousSchema fromJson(JsonReader jsonReader) throws IOException { + return readObject(jsonReader, XmsInternalAutorestAnonymousSchema::new, (anonymousSchema, fieldName, reader) -> { + if ("anonymous".equals(fieldName)) { + anonymousSchema.anonymous = reader.getBoolean(); + } else { + reader.skipChildren(); + } + }); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/extensionmodel/XmsLongRunningOperationOptions.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/extensionmodel/XmsLongRunningOperationOptions.java new file mode 100644 index 0000000000..d6198b0fd6 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/extensionmodel/XmsLongRunningOperationOptions.java @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.extension.model.extensionmodel; + +import com.azure.json.JsonReader; +import com.azure.json.JsonSerializable; +import com.azure.json.JsonWriter; + +import java.io.IOException; + +import static com.microsoft.typespec.http.client.generator.core.extension.base.util.JsonUtils.readObject; + +/** + * Represents the options for a long-running operation. + */ +public class XmsLongRunningOperationOptions implements JsonSerializable { + // azure-async-operation + // location + // original-uri + private String finalStateVia; + + /** + * Creates a new instance of the XmsLongRunningOperationOptions class. + */ + public XmsLongRunningOperationOptions() { + } + + /** + * Gets the final state via. + * + * @return The final state via. + */ + public String getFinalStateVia() { + return finalStateVia; + } + + /** + * Sets the final state via. + * + * @param finalStateVia The final state via. + */ + public void setFinalStateVia(String finalStateVia) { + this.finalStateVia = finalStateVia; + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + return jsonWriter.writeStartObject() + .writeStringField("finalStateVia", finalStateVia) + .writeEndObject(); + } + + /** + * Deserializes an XmsLongRunningOperationOptions instance from the JSON data. + * + * @param jsonReader The JSON reader to deserialize from. + * @return An XmsLongRunningOperationOptions instance deserialized from the JSON data. + * @throws IOException If an error occurs during deserialization. + */ + public static XmsLongRunningOperationOptions fromJson(JsonReader jsonReader) throws IOException { + return readObject(jsonReader, XmsLongRunningOperationOptions::new, (lroOptions, fieldName, reader) -> { + if ("finalStateVia".equals(fieldName)) { + lroOptions.finalStateVia = reader.getString(); + } else { + reader.skipChildren(); + } + }); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/extensionmodel/XmsPageable.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/extensionmodel/XmsPageable.java new file mode 100644 index 0000000000..20f2116d1a --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/extensionmodel/XmsPageable.java @@ -0,0 +1,133 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.extension.model.extensionmodel; + +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Operation; +import com.microsoft.typespec.http.client.generator.core.extension.base.util.JsonUtils; +import com.azure.json.JsonReader; +import com.azure.json.JsonSerializable; +import com.azure.json.JsonWriter; + +import java.io.IOException; + +/** + * Represents the pageable settings of a model. + */ +public class XmsPageable implements JsonSerializable { + private String itemName = "value"; + private String nextLinkName; + private String operationName; + private Operation nextOperation; + + /** + * Creates a new instance of the XmsPageable class. + */ + public XmsPageable() { + } + + /** + * Gets the name of the item in the pageable response. + * + * @return The name of the item in the pageable response. + */ + public String getItemName() { + return itemName; + } + + /** + * Sets the name of the item in the pageable response. + * + * @param itemName The name of the item in the pageable response. + */ + public void setItemName(String itemName) { + this.itemName = itemName; + } + + /** + * Gets the name of the next link in the pageable response. + * + * @return The name of the next link in the pageable response. + */ + public String getNextLinkName() { + return nextLinkName; + } + + /** + * Sets the name of the next link in the pageable response. + * + * @param nextLinkName The name of the next link in the pageable response. + */ + public void setNextLinkName(String nextLinkName) { + this.nextLinkName = nextLinkName; + } + + /** + * Gets the name of the operation that retrieves the next page of items. + * + * @return The name of the operation that retrieves the next page of items. + */ + public String getOperationName() { + return operationName; + } + + /** + * Sets the name of the operation that retrieves the next page of items. + * + * @param operationName The name of the operation that retrieves the next page of items. + */ + public void setOperationName(String operationName) { + this.operationName = operationName; + } + + /** + * Gets the operation that retrieves the next page of items. + * + * @return The operation that retrieves the next page of items. + */ + public Operation getNextOperation() { + return nextOperation; + } + + /** + * Sets the operation that retrieves the next page of items. + * + * @param nextOperation The operation that retrieves the next page of items. + */ + public void setNextOperation(Operation nextOperation) { + this.nextOperation = nextOperation; + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + return jsonWriter.writeStartObject() + .writeStringField("itemName", itemName) + .writeStringField("nextLinkName", nextLinkName) + .writeStringField("operationName", operationName) + .writeJsonField("nextOperation", nextOperation) + .writeEndObject(); + } + + /** + * Deserializes an XmsPageable instance from the JSON data. + * + * @param jsonReader The JSON reader to deserialize from. + * @return An XmsPageable instance deserialized from the JSON data. + * @throws IOException If an error occurs during deserialization. + */ + public static XmsPageable fromJson(JsonReader jsonReader) throws IOException { + return JsonUtils.readObject(jsonReader, XmsPageable::new, (pageable, fieldName, reader) -> { + if ("itemName".equals(fieldName)) { + pageable.itemName = reader.getString(); + } else if ("nextLinkName".equals(fieldName)) { + pageable.nextLinkName = reader.getString(); + } else if ("operationName".equals(fieldName)) { + pageable.operationName = reader.getString(); + } else if ("nextOperation".equals(fieldName)) { + pageable.nextOperation = Operation.fromJson(reader); + } else { + reader.skipChildren(); + } + }); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/extensionmodel/package-info.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/extensionmodel/package-info.java new file mode 100644 index 0000000000..aeefa39af6 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/extensionmodel/package-info.java @@ -0,0 +1,7 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/** + * Contains models for Swagger code model extensions. + */ +package com.microsoft.typespec.http.client.generator.core.extension.model.extensionmodel; diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/package-info.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/package-info.java new file mode 100644 index 0000000000..3a4ccf0d0c --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/model/package-info.java @@ -0,0 +1,7 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/** + * Contains base models for extension base. + */ +package com.microsoft.typespec.http.client.generator.core.extension.model; diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/plugin/AutorestSettings.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/plugin/AutorestSettings.java new file mode 100644 index 0000000000..baf82b156b --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/plugin/AutorestSettings.java @@ -0,0 +1,194 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.extension.plugin; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + * The settings for the AutoRest extension. + */ +public class AutorestSettings { + private String title; + private String tag; + private String baseFolder; + private String outputFolder; + private List security = new ArrayList<>(); + private List securityScopes = new ArrayList<>(); + private String securityHeaderName; + private String javaSdksFolder; + private final List inputFiles = new ArrayList<>(); + private final List require = new ArrayList<>(); + + /** + * Creates a new instance of the AutorestSettings class. + */ + public AutorestSettings() { + } + + /** + * Gets the title of what Autorest is generation. + * + * @return The title of what Autorest is generating. + */ + public String getTitle() { + return this.title; + } + + /** + * Set the title of what Autorest is generating. + * + * @param title The title of what Autorest is generating. + */ + public void setTitle(String title) { + this.title = title; + } + + /** + * Gets the tag for what Autorest is generating. + * + * @return The tag for what Autorest is generating. + */ + public String getTag() { + return tag; + } + + /** + * Sets the tag for what Autorest is generating. + * + * @param tag The tag for what Autorest is generating. + */ + public void setTag(String tag) { + this.tag = tag; + } + + /** + * Gets the base folder for the generation. + * + * @return The base folder for the generation. + */ + public String getBaseFolder() { + return baseFolder; + } + + /** + * Sets the base folder for the generation. + * + * @param baseFolder The base folder for the generation. + */ + public void setBaseFolder(String baseFolder) { + this.baseFolder = baseFolder; + } + + /** + * Gets the output folder for the generation. + * + * @return The output folder for the generation. + */ + public String getOutputFolder() { + return outputFolder; + } + + /** + * Sets the output folder for the generation. + * + * @param outputFolder The output folder for the generation. + */ + public void setOutputFolder(String outputFolder) { + this.outputFolder = outputFolder; + } + + /** + * Gets the security settings for the generation. + * + * @return The security settings for the generation. + */ + public List getSecurity() { + return this.security; + } + + /** + * Sets the security settings for the generation. + * + * @param security The security settings for the generation. + * @throws NullPointerException If {@code security} is null. + */ + public void setSecurity(List security) { + this.security = Objects.requireNonNull(security); + } + + /** + * Gets the security scopes for the generation. + * + * @return The security scopes for the generation. + */ + public List getSecurityScopes() { + return this.securityScopes; + } + + /** + * Sets the security scopes for the generation. + * + * @param securityScopes The security scopes for the generation. + * @throws NullPointerException If {@code securityScopes} is null. + */ + public void setSecurityScopes(List securityScopes) { + this.securityScopes = Objects.requireNonNull(securityScopes); + } + + /** + * Gets the security header name for the generation. + * + * @return The security header name for the generation. + */ + public String getSecurityHeaderName() { + return this.securityHeaderName; + } + + /** + * Sets the security header name for the generation. + * + * @param securityHeaderName The security header name for the generation. + */ + public void setSecurityHeaderName(String securityHeaderName) { + this.securityHeaderName = securityHeaderName; + } + + /** + * Gets the folder containing the Java SDKs for the generation. + * + * @return The folder containing the Java SDKs for the generation. + */ + public String getJavaSdksFolder() { + return javaSdksFolder; + } + + /** + * Sets the folder containing the Java SDKs for the generation. + * + * @param javaSdksFolder The folder containing the Java SDKs for the generation. + */ + public void setJavaSdksFolder(String javaSdksFolder) { + this.javaSdksFolder = javaSdksFolder; + } + + /** + * Gets the input files for the generation. + * + * @return The input files for the generation. + */ + public List getInputFiles() { + return inputFiles; + } + + /** + * Gets the required plugins for the generation. + * + * @return The required plugins for the generation. + */ + public List getRequire() { + return require; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/plugin/JavaSettings.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/plugin/JavaSettings.java new file mode 100644 index 0000000000..25fa19a5cc --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/plugin/JavaSettings.java @@ -0,0 +1,1731 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.extension.plugin; + +import com.azure.json.JsonProviders; +import com.azure.json.JsonReader; +import com.azure.json.JsonSerializable; +import com.azure.json.JsonToken; +import com.azure.json.JsonWriter; +import org.slf4j.Logger; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +/** + * Settings that are used by the Java AutoRest Generator. + */ +public class JavaSettings { + private static final String VERSION = "4.0.0"; + private static JavaSettings instance; + private static NewPlugin host; + private static String header; + private static final Map SIMPLE_JAVA_SETTINGS = new HashMap<>(); + private static Logger logger; + private final boolean useKeyCredential; + private final String flavor; + private final boolean noCustomHeaders; + private final boolean disableTypedHeadersMethods; + + static void setHeader(String value) { + if ("MICROSOFT_MIT".equals(value)) { + header = MICROSOFT_MIT_LICENSE_HEADER + "\n" + String.format(DEFAULT_CODE_GENERATION_HEADER, VERSION); + } else if ("MICROSOFT_APACHE".equals(value)) { + header = MICROSOFT_APACHE_LICENSE_HEADER + "\n" + String.format(DEFAULT_CODE_GENERATION_HEADER, VERSION); + } else if ("MICROSOFT_MIT_NO_VERSION".equals(value)) { + header = MICROSOFT_MIT_LICENSE_HEADER + "\n" + DEFAULT_CODE_GENERATION_HEADER_WITHOUT_VERSION; + } else if ("MICROSOFT_MIT_SMALL_NO_VERSION".equals(value)) { + header = MICROSOFT_MIT_SMALL_LICENSE_HEADER + "\n" + DEFAULT_CODE_GENERATION_HEADER_WITHOUT_VERSION; + } else if ("MICROSOFT_APACHE_NO_VERSION".equals(value)) { + header = MICROSOFT_APACHE_LICENSE_HEADER + "\n" + DEFAULT_CODE_GENERATION_HEADER_WITHOUT_VERSION; + } else if ("MICROSOFT_MIT_NO_CODEGEN".equals(value)) { + header = MICROSOFT_MIT_LICENSE_HEADER + "\n" + "Code generated by Microsoft (R) AutoRest Code Generator."; + } else if ("NONE".equals(value)) { + header = ""; + } else if ("MICROSOFT_MIT_SMALL".equals(value)) { + header = MICROSOFT_MIT_SMALL_LICENSE_HEADER + "Code generated by Microsoft (R) AutoRest Code Generator."; + } else if ("MICROSOFT_MIT_SMALL_TYPESPEC".equals(value)) { + header = MICROSOFT_MIT_SMALL_LICENSE_HEADER + "Code generated by Microsoft (R) TypeSpec Code Generator."; + } else if ("SMALL_TYPESPEC".equals(value)) { + header = "Code generated by Microsoft (R) TypeSpec Code Generator."; + } else if ("MICROSOFT_MIT_SMALL_NO_CODEGEN".equals(value)) { + header = MICROSOFT_MIT_SMALL_LICENSE_HEADER; + } else { + header = value; + } + } + + static void setHost(NewPlugin host) { + JavaSettings.host = host; + logger = new PluginLogger(host, JavaSettings.class); + } + + /** + * Clear the JavaSettings instance. + */ + public static void clear() { + instance = null; + } + + /** + * Get the JavaSettings instance. + * + * @return The JavaSettings instance. + */ + public static JavaSettings getInstance() { + if (instance == null) { + AutorestSettings autorestSettings = new AutorestSettings(); + loadStringSetting("title", autorestSettings::setTitle); + loadStringOrArraySettingAsArray("security", autorestSettings::setSecurity); + loadStringOrArraySettingAsArray("security-scopes", autorestSettings::setSecurityScopes); + loadStringSetting("security-header-name", autorestSettings::setSecurityHeaderName); + + loadStringSetting("tag", autorestSettings::setTag); + loadStringSetting("base-folder", autorestSettings::setBaseFolder); + loadStringSetting("output-folder", autorestSettings::setOutputFolder); + loadStringSetting("java-sdks-folder", autorestSettings::setJavaSdksFolder); + // input-file + List inputFiles = host.getValueWithJsonReader("input-file", + jsonReader -> jsonReader.readArray(JsonReader::getString)); + if (inputFiles != null) { + autorestSettings.getInputFiles().addAll(inputFiles); + logger.debug("List of input files : {}", autorestSettings.getInputFiles()); + } + // require (readme.md etc.) + List require = host.getValueWithJsonReader("require", + jsonReader -> jsonReader.readArray(JsonReader::getString)); + if (require != null) { + autorestSettings.getRequire().addAll(require); + logger.debug("List of require : {}", autorestSettings.getRequire()); + } + + String fluent = getStringValue(host, "fluent"); + + setHeader(getStringValue(host, "license-header")); + instance = new JavaSettings( + autorestSettings, + host.getValueWithJsonReader("modelerfour", jsonReader -> jsonReader.readMap(JsonReader::readUntyped)), + getBooleanValue(host, "azure-arm", false), + getBooleanValue(host, "sdk-integration", false), + fluent, + getBooleanValue(host, "regenerate-pom", false), + header, + getStringValue(host, "service-name"), + getStringValue(host, "namespace", "com.azure.app").toLowerCase(), + getBooleanValue(host, "client-side-validations", false), + getStringValue(host, "client-type-prefix"), + getBooleanValue(host, "generate-client-interfaces", false), + getBooleanValue(host, "generate-client-as-impl", false), + getStringValue(host, "implementation-subpackage", "implementation"), + getStringValue(host, "models-subpackage", "models"), + getStringValue(host, "custom-types", ""), + getStringValue(host, "custom-types-subpackage", ""), + getStringValue(host, "fluent-subpackage", "fluent"), + getBooleanValue(host, "required-parameter-client-methods", false), + getBooleanValue(host, "generate-sync-async-clients", false), + getBooleanValue(host, "generate-builder-per-client", false), + getStringValue(host, "sync-methods", "essential"), + getBooleanValue(host, "client-logger", false), + getBooleanValue(host, "required-fields-as-ctor-args", false), + getBooleanValue(host, "service-interface-as-public", true), + getStringValue(host, "artifact-id", ""), + getStringValue(host, "credential-types", "none"), + getStringValue(host, "credential-scopes"), + getStringValue(host, "customization-jar-path"), + getStringValue(host, "customization-class"), + getBooleanValue(host, "optional-constant-as-enum", false), + getBooleanValue(host, "data-plane", false), + getBooleanValue(host, "use-iterable", false), + host.getValueWithJsonReader("service-versions", jsonReader -> jsonReader.readArray(JsonReader::getString)), + getStringValue(host, "client-flattened-annotation-target", ""), + getStringValue(host, "key-credential-header-name", ""), + getBooleanValue(host, "disable-client-builder", false), + host.getValueWithJsonReader("polling", jsonReader -> jsonReader.readMap(PollingDetails::fromJson)), + getBooleanValue(host, "generate-samples", false), + getBooleanValue(host, "generate-tests", false), + false, //getBooleanValue(host, "generate-send-request-method", false), + getBooleanValue(host, "annotate-getters-and-setters-for-serialization", false), + getStringValue(host, "default-http-exception-type"), + getBooleanValue(host, "use-default-http-status-code-to-exception-type-mapping", false), + host.getValueWithJsonReader("http-status-code-to-exception-type-mapping", JavaSettings::parseStatusCodeMapping), + getBooleanValue(host, "partial-update", false), + // If fluent default to false, this is because the automated test generation ends up with invalid code. + // Once that is fixed, this can be switched over to true. + getBooleanValue(host, "generic-response-type", fluent == null), + getBooleanValue(host, "stream-style-serialization", true), + getBooleanValue(host, "enable-sync-stack", false), + getBooleanValue(host, "output-model-immutable", false), + getBooleanValue(host, "use-input-stream-for-binary", false), + getBooleanValue(host, "no-custom-headers", true), + getBooleanValue(host, "include-read-only-in-constructor-args", false), + // setting the default as true as the Java design guideline recommends using String for URLs. + getBooleanValue(host, "url-as-string", true), + getBooleanValue(host, "uuid-as-string", false), + + // setting this to false by default as a lot of existing libraries still use swagger and + // were generated with required = true set in JsonProperty annotation + getBooleanValue(host, "disable-required-property-annotation", false), + getBooleanValue(host, "enable-page-size", false), + getBooleanValue(host, "use-key-credential", false), + getBooleanValue(host, "null-byte-array-maps-to-empty-array", false), + getBooleanValue(host, "graal-vm-config", false), + getStringValue(host, "flavor", "Azure"), + getBooleanValue(host, "disable-typed-headers-methods", false) + ); + } + return instance; + } + + private static Map parseStatusCodeMapping(JsonReader jsonReader) throws IOException { + return jsonReader.readObject(reader -> { + Map mapping = new HashMap<>(); + while (reader.nextToken() != JsonToken.END_OBJECT) { + int key = Integer.parseInt(reader.getFieldName()); + reader.nextToken(); + mapping.put(key, reader.getString()); + } + + return mapping; + }); + } + + /** + * Create a new JavaSettings object with the provided properties. + * + * @param autorestSettings The autorest settings. + * @param modelerSettings The modeler settings. + * @param azure Whether to generate the Azure. + * @param sdkIntegration Whether to generate the SDK integration. + * @param fluent The fluent generation mode. + * @param regeneratePom Whether to regenerate the POM. + * @param fileHeaderText The file header text. + * @param serviceName The service name. + * @param packageKeyword The package keyword. + * @param clientSideValidations Whether to add client-side validations to the generated clients. + * @param clientTypePrefix The prefix that will be added to each generated client type. + * @param generateClientInterfaces Whether interfaces will be generated for Service and Method Group clients. + * @param generateClientAsImpl Whether Service and Method Group clients will be generated as implementation + * @param implementationSubpackage The sub-package that the Service and Method Group client implementation classes + * will be put into. + * @param modelsSubpackage The sub-package that Enums, Exceptions, and Model types will be put into. + * @param customTypes The custom types that will be generated. + * @param customTypesSubpackage The sub-package that custom types will be put into. + * @param fluentSubpackage The sub-package that Fluent interfaces will be put into. + * @param requiredParameterClientMethods Whether Service and Method Group client method overloads that omit optional + * parameters will be created. + * @param generateSyncAsyncClients Whether Service and Method Group clients will be generated with both synchronous + * and asynchronous methods. + * @param generateBuilderPerClient Whether a builder will be generated for each Service and Method Group client. + * @param syncMethods The sync methods generation mode. + * @param clientLogger Whether to add a logger to the generated clients. + * @param requiredFieldsAsConstructorArgs Whether required fields will be included in the constructor arguments for + * generated models. + * @param serviceInterfaceAsPublic If set to true, proxy method service interface will be marked as public. + * @param artifactId The artifactId for the generated project. + * @param credentialType The type of credential to generate. + * @param credentialScopes The scopes for the generated credential. + * @param customizationJarPath The path to the customization jar. + * @param customizationClass The class to use for customization. + * @param optionalConstantAsEnum Whether to generate optional constants as enums. + * @param dataPlaneClient Whether to generate a data plane client. + * @param useIterable Whether to use Iterable instead of List for collection types. + * @param serviceVersions The versions of the service. + * @param clientFlattenAnnotationTarget The target for the @JsonFlatten annotation for + * x-ms-client-flatten. + * @param keyCredentialHeaderName The header name for the key credential. + * @param clientBuilderDisabled Whether to disable the client builder. + * @param pollingConfig The polling configuration. + * @param generateSamples Whether to generate samples. + * @param generateTests Whether to generate tests. + * @param generateSendRequestMethod Whether to generate the send request method. + * @param annotateGettersAndSettersForSerialization If set to true, Jackson JsonGetter and JsonSetter will annotate + * getters and setters in generated models to handle serialization and deserialization. For now, fields will + * continue being annotated to ensure that there are no backwards compatibility breaks. + * @param defaultHttpExceptionType The fully-qualified class that should be used as the default exception type. This + * class must extend from HttpResponseException. + * @param useDefaultHttpStatusCodeToExceptionTypeMapping Determines whether a well-known HTTP status code to + * exception type mapping should be used if an HTTP status code-exception mapping isn't provided. + * @param httpStatusCodeToExceptionTypeMapping A mapping of HTTP response status code to the exception type that + * should be thrown if that status code is seen. All exception types must be fully-qualified and extend from + * HttpResponseException. + * @param handlePartialUpdate If set to true, the generated model will handle partial updates. + * @param genericResponseTypes If set to true, responses will only use Response, ResponseBase, PagedResponse, and + * PagedResponseBase types with generics instead of creating a specific named type that extends one of those types. + * @param streamStyleSerialization If set to true, models will handle serialization themselves using stream-style + * serialization instead of relying on Jackson Databind. + * @param isSyncStackEnabled If set to true, sync methods are generated using sync stack. i.e these methods do + * not use sync-over-async stack. + * @param outputModelImmutable If set to true, the models that are determined as output only models will be made + * immutable without any public constructors or setter methods. + * @param streamResponseInputStream If set to true, sync methods will use {@code InputStream} for binary responses. + * @param noCustomHeaders If set to true, methods that have custom header types will also have an equivalent + * method that returns just the response with untyped headers. + * @param includeReadOnlyInConstructorArgs If set to true, read-only required properties will be included in the + * constructor if {@code requiredFieldsAsConstructorArgs} is true. This is a backwards compatibility flag as + * previously read-only required were included in constructors. + * @param urlAsString This generates all URLs as String type. This is enabled by default as required by the Java + * design guidelines. For backward compatibility, this can be set to false. + * @param disableRequiredPropertyAnnotation If set to true, the required property annotation will be disabled. + * @param pageSizeEnabled If set to true, the generated client will have support for page size. + * @param useKeyCredential If set to true, the generated client will have support for key credential. + * @param nullByteArrayMapsToEmptyArray If set to true, {@code ArrayType.BYTE_ARRAY} will return an empty array + * instead of null when the default value expression is null. + * @param generateGraalVmConfig If set to true, the generated client will have support for GraalVM. + * @param flavor The brand name we use to generate SDK. + * @param disableTypedHeadersMethods Prevents generating REST API methods that include typed headers. If set to + * true, {@code noCustomHeaders} will be ignored as no REST APIs with typed headers will be generated. + */ + private JavaSettings(AutorestSettings autorestSettings, + Map modelerSettings, + boolean azure, + boolean sdkIntegration, + String fluent, + boolean regeneratePom, + String fileHeaderText, + String serviceName, + String packageKeyword, + boolean clientSideValidations, + String clientTypePrefix, + boolean generateClientInterfaces, + boolean generateClientAsImpl, + String implementationSubpackage, + String modelsSubpackage, + String customTypes, + String customTypesSubpackage, + String fluentSubpackage, + boolean requiredParameterClientMethods, + boolean generateSyncAsyncClients, + boolean generateBuilderPerClient, + String syncMethods, + boolean clientLogger, + boolean requiredFieldsAsConstructorArgs, + boolean serviceInterfaceAsPublic, + String artifactId, + String credentialType, + String credentialScopes, + String customizationJarPath, + String customizationClass, + boolean optionalConstantAsEnum, + boolean dataPlaneClient, + boolean useIterable, + List serviceVersions, + String clientFlattenAnnotationTarget, + String keyCredentialHeaderName, + boolean clientBuilderDisabled, + Map pollingConfig, + boolean generateSamples, + boolean generateTests, + boolean generateSendRequestMethod, + boolean annotateGettersAndSettersForSerialization, + String defaultHttpExceptionType, + boolean useDefaultHttpStatusCodeToExceptionTypeMapping, + Map httpStatusCodeToExceptionTypeMapping, + boolean handlePartialUpdate, + boolean genericResponseTypes, + boolean streamStyleSerialization, + boolean isSyncStackEnabled, + boolean outputModelImmutable, + boolean streamResponseInputStream, + boolean noCustomHeaders, + boolean includeReadOnlyInConstructorArgs, + boolean urlAsString, + boolean uuidAsString, + boolean disableRequiredPropertyAnnotation, + boolean pageSizeEnabled, + boolean useKeyCredential, + boolean nullByteArrayMapsToEmptyArray, + boolean generateGraalVmConfig, + String flavor, + boolean disableTypedHeadersMethods) { + + this.autorestSettings = autorestSettings; + this.modelerSettings = new ModelerSettings(modelerSettings); + this.azure = azure; + this.sdkIntegration = sdkIntegration; + this.fluent = fluent == null ? Fluent.NONE : (fluent.isEmpty() || fluent.equalsIgnoreCase("true") + ? Fluent.PREMIUM : Fluent.valueOf(fluent.toUpperCase(Locale.ROOT))); + this.regeneratePom = regeneratePom; + this.fileHeaderText = fileHeaderText; + this.serviceName = serviceName; + this.packageName = packageKeyword; + this.clientSideValidations = clientSideValidations; + this.clientTypePrefix = clientTypePrefix; + this.generateClientInterfaces = generateClientInterfaces; + this.generateClientAsImpl = generateClientAsImpl || generateSyncAsyncClients || generateClientInterfaces; + this.implementationSubpackage = implementationSubpackage; + this.modelsSubpackage = modelsSubpackage; + this.customTypes = (customTypes == null || customTypes.isEmpty()) + ? new ArrayList<>() : Arrays.asList(customTypes.split(",")); + this.customTypesSubpackage = customTypesSubpackage; + this.fluentSubpackage = fluentSubpackage; + this.requiredParameterClientMethods = requiredParameterClientMethods; + this.generateSyncAsyncClients = generateSyncAsyncClients; + this.generateBuilderPerClient = generateBuilderPerClient; + this.syncMethods = SyncMethodsGeneration.fromValue(syncMethods); + this.clientLogger = clientLogger; + this.requiredFieldsAsConstructorArgs = requiredFieldsAsConstructorArgs; + this.serviceInterfaceAsPublic = serviceInterfaceAsPublic; + this.artifactId = artifactId; + this.optionalConstantAsEnum = optionalConstantAsEnum; + this.dataPlaneClient = dataPlaneClient; + this.useIterable = useIterable; + this.serviceVersions = serviceVersions; + this.clientFlattenAnnotationTarget = + (clientFlattenAnnotationTarget == null || clientFlattenAnnotationTarget.isEmpty()) + ? ClientFlattenAnnotationTarget.TYPE + : ClientFlattenAnnotationTarget.valueOf(clientFlattenAnnotationTarget.toUpperCase(Locale.ROOT)); + + if (credentialType != null) { + String[] splits = credentialType.split(","); + this.credentialTypes = Arrays.stream(splits) + .map(String::trim) + .map(CredentialType::fromValue) + .collect(Collectors.toSet()); + } + if (credentialScopes != null) { + String[] splits = credentialScopes.split(","); + this.credentialScopes = Arrays.stream(splits) + .map(String::trim) + .map(split -> { + if (!split.startsWith("\"")) { + split = "\"" + split + "\""; + } + return split; + }) + .collect(Collectors.toSet()); + } + this.customizationJarPath = customizationJarPath; + this.customizationClass = customizationClass; + this.keyCredentialHeaderName = keyCredentialHeaderName; + this.clientBuilderDisabled = clientBuilderDisabled; + if (pollingConfig != null) { + if (!pollingConfig.containsKey("default")) { + pollingConfig.put("default", new PollingDetails()); + } + } + this.pollingConfig = pollingConfig; + this.generateSamples = generateSamples; + this.generateTests = generateTests; + this.generateSendRequestMethod = generateSendRequestMethod; + this.annotateGettersAndSettersForSerialization = annotateGettersAndSettersForSerialization; + + // Error HTTP status code exception type handling. + this.defaultHttpExceptionType = defaultHttpExceptionType; + this.useDefaultHttpStatusCodeToExceptionTypeMapping = useDefaultHttpStatusCodeToExceptionTypeMapping; + this.httpStatusCodeToExceptionTypeMapping = httpStatusCodeToExceptionTypeMapping; + + this.handlePartialUpdate = handlePartialUpdate; + + this.genericResponseTypes = genericResponseTypes; + + this.streamStyleSerialization = streamStyleSerialization; + this.syncStackEnabled = isSyncStackEnabled; + + this.outputModelImmutable = outputModelImmutable; + + this.isInputStreamForBinary = streamResponseInputStream; + this.noCustomHeaders = noCustomHeaders; + this.includeReadOnlyInConstructorArgs = includeReadOnlyInConstructorArgs; + this.urlAsString = urlAsString; + this.uuidAsString = uuidAsString; + this.disableRequiredJsonAnnotation = disableRequiredPropertyAnnotation; + this.pageSizeEnabled = pageSizeEnabled; + this.useKeyCredential = useKeyCredential; + this.nullByteArrayMapsToEmptyArray = nullByteArrayMapsToEmptyArray; + this.generateGraalVmConfig = generateGraalVmConfig; + this.flavor = flavor; + this.disableTypedHeadersMethods = disableTypedHeadersMethods; + } + + /** + * Whether to generate with Azure branding. + * + * @return Whether to generate with Azure branding. + */ + public boolean isBranded() { + return "azure".equalsIgnoreCase(this.flavor); + } + + private final String keyCredentialHeaderName; + + /** + * The header name for the key credential. + * + * @return The header name for the key credential. + */ + public String getKeyCredentialHeaderName() { + return this.keyCredentialHeaderName; + } + + + private Set credentialTypes; + + /** + * The types of credentials to generate. + * + * @return The types of credentials to generate. + */ + public Set getCredentialTypes() { + return credentialTypes; + } + + + private Set credentialScopes; + + /** + * The scopes for the generated credential. + * + * @return The scopes for the generated credential. + */ + public Set getCredentialScopes() { + return credentialScopes; + } + + + private final boolean azure; + + /** + * Whether to generate the Azure. + * + * @return Whether to generate the Azure. + */ + public final boolean isAzure() { + return azure; + } + + + private final String artifactId; + + /** + * The artifactId for the generated project. + * + * @return The artifactId for the generated project. + */ + public String getArtifactId() { + return artifactId; + } + + /** + * Whether to disable custom headers type generation. + * + * @return Whether to disable custom headers type generation. + */ + public boolean isNoCustomHeaders() { + return noCustomHeaders; + } + + + private final boolean urlAsString; + + /** + * Whether to generate all URLs as String type. This is enabled by default as required by the Java design + * guidelines. For backward compatibility, this can be set to false. + * + * @return Whether to generate all URLs as String type. + */ + public boolean urlAsString() { + return urlAsString; + } + + private final boolean uuidAsString; + + /** + * Whether to use string for uuid. + * + * @return Whether to use string for uuid. + */ + public boolean uuidAsString() { + return uuidAsString; + } + + private final boolean disableRequiredJsonAnnotation; + + /** + * Whether to disable the required property annotation. + * + * @return Whether to disable the required property annotation. + */ + public boolean isDisableRequiredJsonAnnotation() { + return disableRequiredJsonAnnotation; + } + + /** + * Represents Fluent generation mode. + */ + public enum Fluent { + /** + * No Fluent generation. + */ + NONE, + /** + * Fluent Lite generation. + */ + LITE, + /** + * Fluent Premium generation. + */ + PREMIUM; + + /** + * Gets a {@link Fluent} value for the give {@code value} string. + * + * @param value The value to parse. + * @return The {@link Fluent} value. + */ + public static Fluent fromString(String value) { + if (value == null || value.isEmpty()) { + return null; + } + + if ("none".equalsIgnoreCase(value)) { + return NONE; + } else if ("lite".equalsIgnoreCase(value)) { + return LITE; + } else if ("premium".equalsIgnoreCase(value)) { + return PREMIUM; + } else { + return null; + } + } + } + + private final Fluent fluent; + + /** + * Whether to generate the Fluent. + * + * @return Whether to generate the Fluent. + */ + public final boolean isFluent() { + return fluent != Fluent.NONE; + } + + /** + * Whether to generate Fluent Lite. + * + * @return Whether to generate Fluent Lite. + */ + public final boolean isFluentLite() { + return fluent == Fluent.LITE; + } + + /** + * Whether to generate Fluent Premium. + * + * @return Whether to generate Fluent Premium. + */ + public final boolean isFluentPremium() { + return fluent == Fluent.PREMIUM; + } + + /** + * Whether to generate the Azure or Fluent. + * + * @return Whether to generate the Azure or Fluent. + */ + public final boolean isAzureOrFluent() { + return isAzure() || isFluent(); + } + + // configure for model flatten in client + + /** + * Represents the target for the @JsonFlatten annotation for x-ms-client-flatten. + */ + public enum ClientFlattenAnnotationTarget { + /** + * JsonFlatten on class + */ + TYPE, + /** + * JsonFlatten on class variable + */ + FIELD, + /** + * Do not use @JsonFlatten. The model flatten is implemented as class variable getter/setter access the + * flattened properties. + */ + NONE, + /** + * Disable the model flatten + */ + DISABLED + } + + // target for @JsonFlatten annotation for x-ms-client-flatten + private final ClientFlattenAnnotationTarget clientFlattenAnnotationTarget; + + /** + * When flatten client mode, where to put the @JsonFlatten annotation. If NONE, flatten at + * getter/setter methods via codegen. + * + * @return When flatten client mode, where to put the @JsonFlatten annotation. If NONE, flatten at + * getter/setter methods via codegen. + */ + public ClientFlattenAnnotationTarget getClientFlattenAnnotationTarget() { + return this.clientFlattenAnnotationTarget; + } + + /** + * Represents the settings that are used by the modeler. + */ + public static class ModelerSettings { + private final Map settings; + + /** + * Create a new ModelerSettings object with the provided settings. + * + * @param settings The settings that are used by the modeler. + */ + public ModelerSettings(Map settings) { + this.settings = settings == null ? Collections.emptyMap() : settings; + } + + /** + * Get the settings that are used by the modeler. + * + * @return The settings that are used by the modeler. + */ + public Map getSettings() { + return settings; + } + + /** + * If false, use client-flattened-annotation-target = TYPE for no flatten; client-flattened-annotation-target = + * NONE for flatten at getter/setter methods via codegen. + *

+ * If true, use client-flattened-annotation-target = TYPE for @JsonFlatten on type (i.e. on class); + * client-flattened-annotation-target = FIELD for @JsonFlatten on field. + *

+ * modelerfour.flatten-models = false and client-flattened-annotation-target = NONE would require + * modelerfour.flatten-payloads = false. + * + * @return value of modelerfour.flatten-models + */ + public boolean isFlattenModel() { + return settings.containsKey("flatten-models") && (boolean) settings.get("flatten-models"); + } + } + + private final ModelerSettings modelerSettings; + + /** + * The settings that are used by the modeler. + * + * @return The settings that are used by the modeler. + */ + public ModelerSettings getModelerSettings() { + return modelerSettings; + } + + private final AutorestSettings autorestSettings; + + /** + * The settings that are used by the AutoRest generator. + * + * @return The settings that are used by the AutoRest generator. + */ + public AutorestSettings getAutorestSettings() { + return autorestSettings; + } + + /** + * The settings that are used by the AutoRest generator. + * + * @return The settings that are used by the AutoRest generator. + */ + public Map getSimpleJavaSettings() { + return SIMPLE_JAVA_SETTINGS; + } + + private final boolean sdkIntegration; + + /** + * Whether to generate the SDK integration. + * + * @return Whether to generate the SDK integration. + */ + public boolean isSdkIntegration() { + return sdkIntegration; + } + + private final boolean regeneratePom; + + /** + * Whether to regenerate the pom file. + * + * @return Whether to regenerate the pom file. + */ + public final boolean isRegeneratePom() { + return regeneratePom; + } + + private final String fileHeaderText; + + /** + * Get the file header text. + * + * @return The file header text. + */ + public final String getFileHeaderText() { + return fileHeaderText; + } + + private final String serviceName; + + /** + * Get the service name. + * + * @return The service name. + */ + public final String getServiceName() { + return serviceName; + } + + private final String packageName; + + /** + * Get the package name. + * + * @return The package name. + */ + public final String getPackage() { + return packageName; + } + + /** + * Get the package name with the provided package suffixes appended. + * + * @param packageSuffixes The package suffixes to append to the package name. + * @return The package name with the provided package suffixes appended. + */ + public final String getPackage(String... packageSuffixes) { + StringBuilder packageBuilder = new StringBuilder(packageName); + if (packageSuffixes != null) { + for (String packageSuffix : packageSuffixes) { + if (packageSuffix != null && !packageSuffix.isEmpty()) { + // Cleanse the package suffix to remove leading and trailing periods. + boolean startsWithPeriod = packageSuffix.startsWith("."); + boolean endsWithPeriod = packageSuffix.endsWith("."); + + String cleansedPackageSuffix; + if (startsWithPeriod && endsWithPeriod) { + cleansedPackageSuffix = packageSuffix.substring(1, packageSuffix.length() - 1); + } else if (startsWithPeriod) { + cleansedPackageSuffix = packageSuffix.substring(1); + } else if (endsWithPeriod) { + cleansedPackageSuffix = packageSuffix.substring(0, packageSuffix.length() - 1); + } else { + cleansedPackageSuffix = packageSuffix; + } + + packageBuilder.append(".").append(cleansedPackageSuffix); + } + } + } + return packageBuilder.toString(); + } + + private final boolean clientSideValidations; + + /** + * Whether to add client side validations to the generated clients. + * + * @return Whether to add client side validations to the generated clients. + */ + public final boolean isClientSideValidations() { + return clientSideValidations; + } + + /** + * The prefix that will be added to each generated client type. + */ + private final String clientTypePrefix; + + /** + * The prefix that will be added to each generated client type. + * + * @return The prefix that will be added to each generated client type. + */ + public final String getClientTypePrefix() { + return clientTypePrefix; + } + + /** + * Whether interfaces will be generated for Service and Method Group clients. + */ + private final boolean generateClientInterfaces; + + /** + * Whether interfaces will be generated for Service and Method Group clients. + * + * @return Whether interfaces will be generated for Service and Method Group clients. + */ + public final boolean isGenerateClientInterfaces() { + return generateClientInterfaces; + } + + /** + * Whether interfaces will be generated for Service and Method Group clients. + */ + private final boolean generateClientAsImpl; + + /** + * Whether interfaces will be generated for Service and Method Group clients. + * + * @return Whether interfaces will be generated for Service and Method Group clients. + */ + public final boolean isGenerateClientAsImpl() { + return generateClientAsImpl; + } + + /** + * The sub-package that the Service and Method Group client implementation classes will be put into. + */ + private final String implementationSubpackage; + + /** + * The sub-package that the Service and Method Group client implementation classes will be put into. + * + * @return The sub-package that the Service and Method Group client implementation classes will be put into. + */ + public final String getImplementationSubpackage() { + return implementationSubpackage; + } + + /** + * The sub-package that Enums, Exceptions, and Model types will be put into. + */ + private final String modelsSubpackage; + + /** + * The sub-package that Enums, Exceptions, and Model types will be put into. + * + * @return The sub-package that Enums, Exceptions, and Model types will be put into. + */ + public final String getModelsSubpackage() { + return modelsSubpackage; + } + + private final String fluentSubpackage; + + /** + * The sub-package for Fluent SDK, that contains Client and Builder types, which is not recommended to be used + * directly. + * + * @return The sub-package for Fluent SDK, that contains Client and Builder types, which is not recommended to be + * used directly. + */ + public final String getFluentSubpackage() { + return fluentSubpackage; + } + + /** + * The sub-package for Fluent SDK, that contains Enums, Exceptions, and Model types, which is not recommended being + * used directly. + * + * @return The sub-package for Fluent SDK, that contains Enums, Exceptions, and Model types, which is not + * recommended being used directly. + */ + public final String getFluentModelsSubpackage() { + if (modelsSubpackage.contains(".")) { + return fluentSubpackage + "." + modelsSubpackage.substring(modelsSubpackage.lastIndexOf(".") + 1); + } else { + return fluentSubpackage + "." + modelsSubpackage; + } + } + + /** + * Whether Service and Method Group client method overloads that omit optional parameters will be created. + */ + private final boolean requiredParameterClientMethods; + + /** + * Whether Service and Method Group client method overloads that omit optional parameters will be created. + * + * @return Whether Service and Method Group client method overloads that omit optional parameters will be created. + */ + public final boolean isRequiredParameterClientMethods() { + return requiredParameterClientMethods; + } + + private final boolean generateSyncAsyncClients; + + /** + * Whether sync methods are generated using sync stack. i.e these methods do not use sync-over-async stack. + * + * @return Whether sync methods are generated using sync stack. + */ + public final boolean isGenerateSyncAsyncClients() { + return generateSyncAsyncClients; + } + + /** + * Whether sync methods are generated using sync stack. i.e these methods do not use sync-over-async stack. + * + * @return Whether sync methods are generated using sync stack. + */ + public final boolean isSyncClientWrapAsyncClient() { + return !syncStackEnabled; + } + + private final SyncMethodsGeneration syncMethods; + + /** + * Get the sync methods generation. + * + * @return The sync methods generation. + */ + public final SyncMethodsGeneration getSyncMethods() { + return syncMethods; + } + + /** + * Whether to generate async methods. + * + * @return Whether to generate async methods. + */ + public final boolean isGenerateAsyncMethods() { + SyncMethodsGeneration syncMethodsGeneration = getSyncMethods(); + return syncMethodsGeneration == SyncMethodsGeneration.ALL + || syncMethodsGeneration == SyncMethodsGeneration.ESSENTIAL; + } + + /** + * Whether to generate sync methods. + * + * @return Whether to generate sync methods. + */ + public final boolean isGenerateSyncMethods() { + SyncMethodsGeneration syncMethodsGeneration = getSyncMethods(); + return syncMethodsGeneration == SyncMethodsGeneration.ALL + || syncMethodsGeneration == SyncMethodsGeneration.SYNC_ONLY; + } + + private final boolean requiredFieldsAsConstructorArgs; + + /** + * Whether required fields will be included as constructor arguments. + * + * @return Whether required fields will be included as constructor arguments. + */ + public boolean isRequiredFieldsAsConstructorArgs() { + return requiredFieldsAsConstructorArgs; + } + + private final boolean serviceInterfaceAsPublic; + + /** + * Whether proxy method service interface will be marked as public. + * + * @return Whether proxy method service interface will be marked as public. + */ + public boolean isServiceInterfaceAsPublic() { + return serviceInterfaceAsPublic; + } + + /** + * Represents sync methods generation. + */ + public enum SyncMethodsGeneration { + /** + * Generate all methods. + */ + ALL, + + /** + * Generate only essential methods. + */ + ESSENTIAL, + + /** + * Generate only sync methods. + */ + SYNC_ONLY, // SYNC_ONLY requires "enable-sync-stack" + + /** + * Generate no methods. + */ + NONE; + + /** + * Convert the string value to the enum value. + * + * @param value The string value. + * @return The enum value. + */ + public static SyncMethodsGeneration fromValue(String value) { + if (value == null) { + return null; + } else if (value.equals("all")) { + return ALL; + } else if (value.equals("essential")) { + return ESSENTIAL; + } else if (value.equals("none")) { + return NONE; + } else if (value.equals("sync-only")) { + return SYNC_ONLY; + } + return null; + } + } + + private final List customTypes; + + /** + * The list of custom types. + * + * @return The list of custom types. + */ + public List getCustomTypes() { + return customTypes; + } + + /** + * Whether the given type name is a custom type. + * + * @param typeName The type name. + * @return Whether the given type name is a custom type. + */ + public boolean isCustomType(String typeName) { + return customTypes.contains(typeName); + } + + private final String customTypesSubpackage; + + /** + * The sub-package that custom types will be put into. + * + * @return The sub-package that custom types will be put into. + */ + public final String getCustomTypesSubpackage() { + return customTypesSubpackage; + } + + /** + * Represents the type of credential. + */ + public enum CredentialType { + /** + * Token credential. + */ + TOKEN_CREDENTIAL, + + /** + * Azure key credential. + */ + AZURE_KEY_CREDENTIAL, + + /** + * No credential. + */ + NONE; + + /** + * Convert the string value to the enum value. + * + * @param value The string value. + * @return The enum value. + */ + public static CredentialType fromValue(String value) { + if (value == null) { + return null; + } else if (value.equals("tokencredential")) { + return TOKEN_CREDENTIAL; + } else if (value.equals("azurekeycredential")) { + return AZURE_KEY_CREDENTIAL; + } else if (value.equals("none")) { + return NONE; + } + return NONE; + } + } + + private final boolean clientLogger; + + /** + * Whether to use client logger. + * + * @return Whether to use client logger. + */ + public final boolean isUseClientLogger() { + return clientLogger; + } + + private final String customizationJarPath; + + /** + * The path to the customization jar. + * + * @return The path to the customization jar. + */ + public final String getCustomizationJarPath() { + return customizationJarPath; + } + + private final String customizationClass; + + /** + * The fully qualified class name of the customization class. + * + * @return The fully qualified class name of the customization class. + */ + public final String getCustomizationClass() { + return customizationClass; + } + + private final boolean optionalConstantAsEnum; + + /** + * Whether to generate optional constants as enum. + * + * @return Whether to generate optional constants as enum. + */ + public boolean isOptionalConstantAsEnum() { + return optionalConstantAsEnum; + } + + private final boolean dataPlaneClient; + + /** + * Whether the client is a data plane client. + * + * @return Whether the client is a data plane client. + */ + + public boolean isDataPlaneClient() { + return dataPlaneClient; + } + + private final boolean useIterable; + + /** + * Whether to use Iterable for list type properties. + * + * @return whether to use Iterable for list type properties. + */ + public boolean isUseIterable() { + return useIterable; + } + + /** + * Service version list. It maps to api-version parameter in swagger. Last one is the latest version, also default + * version + */ + private final List serviceVersions; + + /** + * Service version list. It maps to api-version parameter in swagger. Last one is the latest version, also + * default version + * + * @return Service version list. It maps to api-version parameter in swagger. Last one is the latest version, also + * default version + */ + public List getServiceVersions() { + return serviceVersions; + } + + private final boolean generateSamples; + + /** + * Whether to generate samples. + * + * @return Whether to generate samples. + */ + public boolean isGenerateSamples() { + return generateSamples; + } + + private final boolean generateTests; + + /** + * Whether to generate tests. + * + * @return Whether to generate tests. + */ + public boolean isGenerateTests() { + return generateTests; + } + + private final boolean generateSendRequestMethod; + + /** + * Whether to generate the send request method. + * + * @return Whether to generate the send request method. + */ + public boolean isGenerateSendRequestMethod() { + return generateSendRequestMethod; + } + + private final boolean syncStackEnabled; + + /** + * Whether to enable sync stack. + * + * @return Whether to enable sync stack. + */ + public boolean isSyncStackEnabled() { + return syncStackEnabled; + } + + private final boolean clientBuilderDisabled; + + /** + * Whether to disable the client builder. + * + * @return Whether to disable the client builder. + */ + public boolean clientBuilderDisabled() { + return clientBuilderDisabled; + } + + private final boolean outputModelImmutable; + + /** + * Whether the models that are determined as output only models will be made immutable without any public + * constructors or setter methods. + * + * @return Whether the models that are determined as output only models will be made immutable without any public + * constructors or setter methods. + */ + public boolean isOutputModelImmutable() { + return outputModelImmutable; + } + + private final boolean pageSizeEnabled; + + /** + * Whether to enable page size. + * + * @return Whether to enable page size. + */ + public boolean isPageSizeEnabled() { + return pageSizeEnabled; + } + + private final boolean generateGraalVmConfig; + + /** + * Whether to generate a GraalVM configuration file. + * + * @return Whether to generate a GraalVM configuration file. + */ + public boolean isGenerateGraalVmConfig() { + return generateGraalVmConfig; + } + + /** + * Represents the details of polling for a long-running operation. + */ + public static class PollingDetails implements JsonSerializable { + private String strategy; + private String syncStrategy; + private String intermediateType; + private String finalType; + private String pollInterval; + + /** + * Creates a new PollingDetails object. + */ + public PollingDetails() { + } + + /** + * The default polling strategy format. + */ + public static final String DEFAULT_POLLING_STRATEGY_FORMAT = String.join("\n", + "new %s<>(new PollingStrategyOptions({httpPipeline})", + " .setEndpoint({endpoint})", + " .setContext({context})", + " .setServiceVersion({serviceVersion}))"); + + private static final String DEFAULT_POLLING_CODE + = String.format(DEFAULT_POLLING_STRATEGY_FORMAT, "DefaultPollingStrategy"); + + private static final String DEFAULT_SYNC_POLLING_CODE + = String.format(DEFAULT_POLLING_STRATEGY_FORMAT, "SyncDefaultPollingStrategy"); + + /** + * Gets the strategy for polling. + * + * @return The strategy for polling. + */ + public String getStrategy() { + if (strategy == null || "default".equalsIgnoreCase(strategy)) { + return DEFAULT_POLLING_CODE; + } else { + return strategy; + } + } + + /** + * Gets the sync strategy for polling. + * + * @return The sync strategy for polling. + */ + public String getSyncStrategy() { + if (syncStrategy == null || "default".equalsIgnoreCase(syncStrategy)) { + return DEFAULT_SYNC_POLLING_CODE; + } else { + return syncStrategy; + } + } + + /** + * Gets the intermediate type for polling. + * + * @return The intermediate type for polling. + */ + public String getIntermediateType() { + return intermediateType; + } + + /** + * Gets the final type for polling. + * + * @return The final type for polling. + */ + public String getFinalType() { + return finalType; + } + + /** + * Gets the polling interval in seconds. + * + * @return The polling interval in seconds. + */ + public int getPollIntervalInSeconds() { + return pollInterval != null ? Integer.parseInt(pollInterval) : 1; + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + return jsonWriter.writeStartObject() + .writeStringField("strategy", strategy) + .writeStringField("sync-strategy", syncStrategy) + .writeStringField("intermediate-type", intermediateType) + .writeStringField("final-type", finalType) + .writeStringField("poll-interval", pollInterval) + .writeEndObject(); + } + + /** + * Deserializes a PollingDetails instance from the JSON data. + * + * @param jsonReader The JSON reader to deserialize from. + * @return A PollingDetails instance deserialized from the JSON data. + * @throws IOException If an error occurs during deserialization. + */ + public static PollingDetails fromJson(JsonReader jsonReader) throws IOException { + return jsonReader.readObject(reader -> { + PollingDetails pollingDetails = new PollingDetails(); + + while (reader.nextToken() != JsonToken.END_OBJECT) { + String fieldName = reader.getFieldName(); + reader.nextToken(); + + if ("strategy".equals(fieldName)) { + pollingDetails.strategy = reader.getString(); + } else if ("sync-strategy".equals(fieldName)) { + pollingDetails.syncStrategy = reader.getString(); + } else if ("intermediate-type".equals(fieldName)) { + pollingDetails.intermediateType = reader.getString(); + } else if ("final-type".equals(fieldName)) { + pollingDetails.finalType = reader.getString(); + } else if ("poll-interval".equals(fieldName)) { + pollingDetails.pollInterval = reader.getString(); + } else { + reader.skipChildren(); + } + } + + return pollingDetails; + }); + } + } + + private final Map pollingConfig; + + /** + * Gets the polling configuration for the specified operation. + * + * @param operation The operation name. + * @return The polling configuration for the specified operation, or the default polling configuration if no + * configuration is specified for the operation. + */ + public PollingDetails getPollingConfig(String operation) { + if (pollingConfig == null) { + return null; + } + for (String key : pollingConfig.keySet()) { + if (key.equalsIgnoreCase(operation)) { + return pollingConfig.get(key); + } + } + return pollingConfig.get("default"); + } + + private final boolean annotateGettersAndSettersForSerialization; + + /** + * Whether model getters and setters should be annotated with Jackson JsonGetter and JsonSetter to handle + * serialization. + *

+ * For now, Fields will continue to be annotated with JsonProperty to ensure there are no backwards compatibility + * breaking changes. + * + * @return Whether model getters and setters should be annotated to handle serialization. + */ + public boolean isGettersAndSettersAnnotatedForSerialization() { + return annotateGettersAndSettersForSerialization; + } + + private final String defaultHttpExceptionType; + + /** + * Gets the fully-qualified exception type that is used for error HTTP status codes. + * + * @return The fully-qualified exception type. + */ + public String getDefaultHttpExceptionType() { + return defaultHttpExceptionType; + } + + private final boolean useDefaultHttpStatusCodeToExceptionTypeMapping; + + /** + * Whether to use the default error HTTP status code to exception type mapping. + * + * @return Whether to use the default error HTTP status code to exception type mapping. + */ + public boolean isUseDefaultHttpStatusCodeToExceptionTypeMapping() { + return useDefaultHttpStatusCodeToExceptionTypeMapping; + } + + private final Map httpStatusCodeToExceptionTypeMapping; + + /** + * Gets a read-only view of the custom error HTTP status code to exception type mapping. + * + * @return A read-only view of the custom error HTTP status code to exception type mapping. + */ + public Map getHttpStatusCodeToExceptionTypeMapping() { + return httpStatusCodeToExceptionTypeMapping == null + ? null : Collections.unmodifiableMap(httpStatusCodeToExceptionTypeMapping); + } + + private final boolean generateBuilderPerClient; + + /** + * Whether a builder will be generated for each client. + * + * @return Whether a builder will be generated for each client. + */ + public boolean isGenerateBuilderPerClient() { + return generateBuilderPerClient; + } + + private final boolean handlePartialUpdate; + + /** + * Whether partial updates are handled. + * + * @return Whether partial updates are handled. + */ + public boolean isHandlePartialUpdate() { + return handlePartialUpdate; + } + + private final boolean genericResponseTypes; + + /** + * Whether Response, ResponseBase, PagedResponse, or PagedResponseBase will be used directly with generics instead + * of creating a named type that extends one of those type. + * + * @return Whether generic response types are used instead of named types that extend the generic type. + */ + public boolean isGenericResponseTypes() { + return genericResponseTypes; + } + + private final boolean streamStyleSerialization; + + /** + * Whether models will handle serialization themselves using stream-style serialization instead of relying on + * Jackson Databind. + * + * @return Whether models will handle serialization themselves. + */ + public boolean isStreamStyleSerialization() { + return streamStyleSerialization; + } + + private final boolean isInputStreamForBinary; + + /** + * Whether to use InputStream for binary in response body. + * + * @return If true, return InputStream for binary in response body. If false, return + * BinaryData. + */ + public boolean isInputStreamForBinary() { + return isInputStreamForBinary; + } + + private final boolean includeReadOnlyInConstructorArgs; + + /** + * Whether required read-only properties will be included in constructor arguments. + *

+ * In the past, required read-only properties were included in constructor arguments when + * {@link #isRequiredFieldsAsConstructorArgs()} was true. This flag re-enables that capability when the property is + * required and read-only and {@link #isRequiredFieldsAsConstructorArgs()} is true. + *

+ * If this returns true but {@link #isRequiredFieldsAsConstructorArgs()} returns false this configuration does + * nothing. + * + * @return Whether required read-only properties will be included in constructor arguments. + */ + public boolean isIncludeReadOnlyInConstructorArgs() { + return includeReadOnlyInConstructorArgs; + } + + /** + * Whether to use key credential for authentication. + * + * @return Whether to use key credential for authentication. + */ + public boolean isUseKeyCredential() { + return this.useKeyCredential; + } + + private final boolean nullByteArrayMapsToEmptyArray; + + /** + * Whether {@code ArrayType.BYTE_ARRAY} will return an empty array instead of null when the default value expression + * is null. + *

+ * Set this to true to ensure backwards compatibility with previous versions of the Java generator. + * + * @return Whether {@code ArrayType.BYTE_ARRAY} will return an empty array instead of null when the default value + * expression is null. + */ + public boolean isNullByteArrayMapsToEmptyArray() { + return nullByteArrayMapsToEmptyArray; + } + + /** + * Determines whether REST API methods returning typed headers will be generated. + *

+ * If set to true, {@link #isNoCustomHeaders()} and {@link #isGenericResponseTypes()} will be ignored as no REST + * APIs returning typed headers will be generated. Meaning overloads without typed headers won't be generated and + * since no methods with typed headers will exist there won't be usage of {@code ResponseBase}, or named subtypes of + * it. + *

+ * No matter the value, typed header classes will be generated. This can be useful if a majority of API calls don't + * returned the typed headers at all but there are still cases where it would be useful to have them. Typed header + * classes are simply created with {@code HttpHeaders} from {@code azure-core}. + * + * @return Whether REST APIs including typed headers should be generated. + */ + public boolean isDisableTypedHeadersMethods() { + return disableTypedHeadersMethods; + } + + private static final String DEFAULT_CODE_GENERATION_HEADER = String.join("\n", + "Code generated by Microsoft (R) AutoRest Code Generator %s", + "Changes may cause incorrect behavior and will be lost if the code is regenerated."); + + private static final String DEFAULT_CODE_GENERATION_HEADER_WITHOUT_VERSION = String.join("\n", + "Code generated by Microsoft (R) AutoRest Code Generator.", + "Changes may cause incorrect behavior and will be lost if the code is regenerated."); + + private static final String MICROSOFT_APACHE_LICENSE_HEADER = String.join("\n", + "Copyright (c) Microsoft and contributors. All rights reserved.\n", + "Licensed under the Apache License, Version 2.0 (the \"License\");", + "you may not use this file except in compliance with the License.", + "You may obtain a copy of the License at", + " https://www.apache.org/licenses/LICENSE-2.0\n", + "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.\n", + "See the License for the specific language governing permissions and", + "limitations under the License.", + ""); + + private static final String MICROSOFT_MIT_LICENSE_HEADER = String.join("\n", + "Copyright (c) Microsoft Corporation. All rights reserved.", + "Licensed under the MIT License. See License.txt in the project root for license information.", + ""); + + private static final String MICROSOFT_MIT_SMALL_LICENSE_HEADER = String.join("\n", + "Copyright (c) Microsoft Corporation. All rights reserved.", + "Licensed under the MIT License.", + ""); + + private static void loadStringSetting(String settingName, Consumer action) { + String settingValue = host.getStringValue(settingName); + if (settingValue != null) { + logger.debug("Option, string, {} : {}", settingName, settingValue); + action.accept(settingValue); + } + } + + private static String getStringValue(NewPlugin host, String settingName) { + return getStringValue(host, settingName, null); + } + + private static String getStringValue(NewPlugin host, String settingName, String defaultValue) { + String ret = host.getStringValue(settingName); + if (ret == null) { + return defaultValue; + } else { + logger.debug("Option, string, {} : {}", settingName, ret); + SIMPLE_JAVA_SETTINGS.put(settingName, ret); + return ret; + } + } + + private static boolean getBooleanValue(NewPlugin host, String settingName, boolean defaultValue) { + Boolean ret = host.getBooleanValue(settingName); + if (ret == null) { + return defaultValue; + } else { + logger.debug("Option, boolean, {} : {}", settingName, ret); + SIMPLE_JAVA_SETTINGS.put(settingName, ret); + return ret; + } + } + + private static void loadStringOrArraySettingAsArray(String settingName, Consumer> action) { + host.getValue(settingName, jsonString -> { + if (jsonString == null) { + return null; + } else if (jsonString.startsWith("[")) { + // Array values will need to be parsed. + try (JsonReader jsonReader = JsonProviders.createReader(jsonString)) { + List settingValueList = jsonReader.readArray(JsonReader::getString); + logger.debug("Option, array, {} : {}", settingName, settingValueList); + action.accept(settingValueList); + } + } else { + // Single values will be returned as the string representation. + logger.debug("Option, string, {} : {}", settingName, jsonString); + action.accept(Collections.singletonList(jsonString)); + } + + return null; + }); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/plugin/NewPlugin.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/plugin/NewPlugin.java new file mode 100644 index 0000000000..bdb9b176a3 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/plugin/NewPlugin.java @@ -0,0 +1,346 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.extension.plugin; + +import com.microsoft.typespec.http.client.generator.core.extension.jsonrpc.Connection; +import com.microsoft.typespec.http.client.generator.core.extension.model.Message; +import com.microsoft.typespec.http.client.generator.core.extension.model.MessageChannel; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.AnnotatedPropertyUtils; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.CodeModelCustomConstructor; +import com.azure.json.JsonProviders; +import com.azure.json.JsonReader; +import com.azure.json.ReadValueCallback; +import org.yaml.snakeyaml.DumperOptions; +import org.yaml.snakeyaml.LoaderOptions; +import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.constructor.Constructor; +import org.yaml.snakeyaml.inspector.TrustedTagInspector; +import org.yaml.snakeyaml.representer.Representer; + +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.io.UncheckedIOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** + * Represents a plugin that can be run by AutoRest. + */ +public abstract class NewPlugin { + /** + * The Yaml used to serialize and deserialize YAML. + */ + protected final Yaml yamlMapper; + + /** + * The connection to the AutoRest extension. + */ + protected final Connection connection; + + /** + * The name of the plugin. + */ + protected final String pluginName; + + /** + * The session id. + */ + protected final String sessionId; + + /** + * Reads the content of a file. + * + * @param fileName The name of the file. + * @return The content of the file. + */ + public String readFile(String fileName) { + return connection.request("ReadFile", sessionId, fileName); + } + + /** + * Gets the value of a key. + * + * @param The type of the value. + * @param key The key. + * @param converter The converter to convert the value to the desired type. + * @return The value of the key. + */ + public T getValue(String key, ReadValueCallback converter) { + try { + return converter.read(getValueString(key)); + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + + /** + * Gets the value of a key as a JSON object. + * + * @param key The key. + * @param converter The converter to convert the value to the desired type. + * @return The value of the key. + * @param The type of the value. + */ + public T getValueWithJsonReader(String key, ReadValueCallback converter) { + String valueString = getValueString(key); + if (valueString == null) { + return null; + } + + try (JsonReader jsonReader = JsonProviders.createReader(valueString)) { + return converter.read(jsonReader); + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + + private String getValueString(String key) { + return connection.request("GetValue", sessionId, key); + } + + /** + * Gets the value of a key. + * + * @param key The key. + * @return The value of the key. + */ + public String getStringValue(String key) { + return getValue(key, json -> json); + } + + /** + * Gets the value of a key. + * + * @param key The key. + * @param defaultValue The default value if the key doesn't have a value. + * @return The value of the key. + */ + public String getStringValue(String key, String defaultValue) { + String ret = getStringValue(key); + return (ret == null) ? defaultValue : ret; + } + + /** + * Gets the value of a key. + * + * @param key The key. + * @return The value of the key. + */ + public Boolean getBooleanValue(String key) { + return getValue(key, + json -> json == null ? null : (!json.equals("0") && !json.equals("false") && !json.isEmpty())); + } + + /** + * Gets the value of a key. + * + * @param key The key. + * @param defaultValue The default value if the key doesn't have a value. + * @return The value of the key. + */ + public boolean getBooleanValue(String key, boolean defaultValue) { + Boolean ret = getBooleanValue(key); + return (ret == null) ? defaultValue : ret; + } + + /** + * Gets the input files. + * + * @return The input files. + */ + public List listInputs() { + return listInputs(null); + } + + /** + * Gets the input files of a specific type. + * + * @param artifactType The type of the input files. + * @return The input files of the specific type. + */ + public List listInputs(String artifactType) { + String jsonResponse = connection.request("ListInputs", sessionId, artifactType); + try (JsonReader jsonReader = JsonProviders.createReader(jsonResponse)) { + return jsonReader.readArray(JsonReader::getString); + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + + /** + * Sends a message to the AutoRest extension. + * + * @param message The message to send. + */ + public void message(Message message) { + connection.notify("Message", sessionId, message); + } + + /** + * Sends a message to the AutoRest extension. + * + * @param channel The channel of the message. + * @param text The text of the message. + * @param error The error of the message. + * @param keys The keys of the message. + */ + public void message(MessageChannel channel, String text, Throwable error, List keys) { + Message message = new Message(); + message.setChannel(channel); + message.setKey(keys); + message.setSource(Collections.emptyList()); + if (error != null) { + text += "\n" + formatThrowableMessage(error); + } + message.setText(text); + message(message); + } + + /** + * Writes the content to a file. + * + * @param fileName The name of the file. + * @param content The content of the file. + * @param sourceMap The source map of the file. + */ + public void writeFile(String fileName, String content, List sourceMap) { + connection.notify("WriteFile", sessionId, fileName, content, sourceMap); + } + + /** + * Writes the content to a file. + * + * @param fileName The name of the file. + * @param content The content of the file. + * @param sourceMap The source map of the file. + * @param artifactType The type of the file. + */ + public void writeFile(String fileName, String content, List sourceMap, String artifactType) { + Message message = new Message(); + message.setChannel(MessageChannel.FILE); + if (sourceMap == null) { + message.setDetails(Map.of("content", content, "type", artifactType, "uri", fileName)); + } else { + message.setDetails( + Map.of("content", content, "type", artifactType, "uri", fileName, "sourceMap", sourceMap)); + } + message.setText(content); + message.setKey(Arrays.asList(artifactType, fileName)); + connection.notify("Message", sessionId, message); + } + + /** + * Protects the files from being overwritten. + * + * @param path The path to the files to protect. + */ + public void protectFiles(String path) { + List items = listInputs(path); + if (items != null && !items.isEmpty()) { + for (String item : items) { + String content = readFile(item); + writeFile(item, content, null, "preserved-files"); + } + } + String contentSingle = readFile(path); + writeFile(path, contentSingle, null, "preserved-files"); + } + + /** + * Gets the configuration file. + * + * @param fileName The name of the configuration file. + * @return The content of the configuration file. + */ + public String getConfigurationFile(String fileName) { + Map configurations = getValueWithJsonReader("configurationFiles", + jsonReader -> jsonReader.readMap(JsonReader::getString)); + + if (configurations != null) { + Iterator it = configurations.keySet().iterator(); + if (it.hasNext()) { + String first = it.next(); + first = first.substring(0, first.lastIndexOf('/')); + for (String configFile : configurations.keySet()) { + if (Objects.equals(configFile, first + "/" + fileName)) { + return configurations.get(configFile); + } + } + } + } + return ""; + } + + /** + * Updates the configuration file. + * + * @param filename The name of the configuration file. + * @param content The content of the configuration file. + */ + public void updateConfigurationFile(String filename, String content) { + Message message = new Message(); + message.setChannel(MessageChannel.CONFIGURATION); + message.setKey(List.of(filename)); + message.setText(content); + connection.notify("Message", sessionId, message); + } + + /** + * Initializes a new instance of the NewPlugin class. + * + * @param connection The connection to the AutoRest extension. + * @param pluginName The name of the plugin. + * @param sessionId The session id. + */ + public NewPlugin(Connection connection, String pluginName, String sessionId) { + this.connection = connection; + this.pluginName = pluginName; + this.sessionId = sessionId; + Representer representer = new Representer(new DumperOptions()); + representer.setPropertyUtils(new AnnotatedPropertyUtils()); + representer.getPropertyUtils().setSkipMissingProperties(true); + LoaderOptions loaderOptions = new LoaderOptions(); + loaderOptions.setCodePointLimit(50 * 1024 * 1024); + loaderOptions.setMaxAliasesForCollections(Integer.MAX_VALUE); + loaderOptions.setNestingDepthLimit(Integer.MAX_VALUE); + loaderOptions.setTagInspector(new TrustedTagInspector()); + Constructor constructor = new CodeModelCustomConstructor(loaderOptions); + yamlMapper = new Yaml(constructor, representer, new DumperOptions(), loaderOptions); + } + + /** + * The method that is called to run the plugin. + * + * @return Whether the plugin ran successfully. + */ + public boolean process() { + try { + JavaSettings.setHost(this); + return processInternal(); + } catch (Throwable t) { + message(MessageChannel.FATAL, "Unhandled error: " + t.getMessage(), t, List.of(getClass().getSimpleName())); + return false; + } + } + + /** + * The method that is called to run the plugin. + * + * @return Whether the plugin ran successfully. + */ + public abstract boolean processInternal(); + + private String formatThrowableMessage(Throwable t) { + StringWriter stringWriter = new StringWriter(); + PrintWriter printWriter = new PrintWriter(stringWriter); + + t.printStackTrace(printWriter); + return stringWriter.toString(); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/plugin/PluginLogger.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/plugin/PluginLogger.java new file mode 100644 index 0000000000..1c86d85158 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/plugin/PluginLogger.java @@ -0,0 +1,339 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.extension.plugin; + +import com.microsoft.typespec.http.client.generator.core.extension.model.MessageChannel; +import org.slf4j.helpers.FormattingTuple; +import org.slf4j.helpers.MarkerIgnoringBase; +import org.slf4j.helpers.MessageFormatter; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * A logger for AutoRest plugins. + */ +public final class PluginLogger extends MarkerIgnoringBase { + /** + * The AutoRest plugin. + */ + private final NewPlugin plugin; + + /** + * The keys for logging. + */ + private final List keys; + + /** + * Whether tracing is enabled. + */ + private final boolean isTracingEnabled; + + /** + * Whether debugging is enabled. + */ + private final boolean isDebugEnabled; + + /** + * Construct DefaultLogger for the given class. + * + * @param plugin the AutoRest plugin + * @param clazz the class + * @param labels the labels for logging + */ + public PluginLogger(NewPlugin plugin, Class clazz, String... labels) { + this.plugin = plugin; + this.isTracingEnabled = plugin.getBooleanValue("verbose", false); + this.isDebugEnabled = plugin.getBooleanValue("debug", false) || plugin.getBooleanValue("debugger", false); + + this.keys = new ArrayList<>(); + if (clazz != null) { + keys.add(clazz.getSimpleName()); + } + + if (labels != null && labels.length > 0) { + this.keys.addAll(Arrays.asList(labels)); + } + } + + /** + * Construct DefaultLogger for the given class name. + * + * @param plugin the AutoRest plugin + * @param labels the labels for logging + */ + public PluginLogger(NewPlugin plugin, String... labels) { + this(plugin, null, labels); + } + + /** + * {@inheritDoc} + */ + @Override + public String getName() { + return String.join("/", keys); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isTraceEnabled() { + return isTracingEnabled; + } + + /** + * {@inheritDoc} + */ + @Override + public void trace(final String msg) { + logMessageWithFormat(MessageChannel.VERBOSE, msg); + } + + /** + * {@inheritDoc} + */ + @Override + public void trace(final String format, final Object arg1) { + logMessageWithFormat(MessageChannel.VERBOSE, format, arg1); + } + + /** + * {@inheritDoc} + */ + @Override + public void trace(final String format, final Object arg1, final Object arg2) { + logMessageWithFormat(MessageChannel.VERBOSE, format, arg1, arg2); + } + + /** + * {@inheritDoc} + */ + @Override + public void trace(final String format, final Object... arguments) { + logMessageWithFormat(MessageChannel.VERBOSE, format, arguments); + } + + /** + * {@inheritDoc} + */ + @Override + public void trace(final String msg, final Throwable t) { + log(MessageChannel.VERBOSE, msg, t); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isDebugEnabled() { + return isDebugEnabled; + } + + @Override + public void debug(final String msg) { + logMessageWithFormat(MessageChannel.DEBUG, msg); + } + + /** + * {@inheritDoc} + */ + @Override + public void debug(String format, Object arg) { + logMessageWithFormat(MessageChannel.DEBUG, format, arg); + } + + /** + * {@inheritDoc} + */ + @Override + public void debug(final String format, final Object arg1, final Object arg2) { + logMessageWithFormat(MessageChannel.DEBUG, format, arg1, arg2); + } + + /** + * {@inheritDoc} + */ + @Override + public void debug(String format, Object... args) { + logMessageWithFormat(MessageChannel.DEBUG, format, args); + } + + /** + * {@inheritDoc} + */ + @Override + public void debug(final String msg, final Throwable t) { + log(MessageChannel.DEBUG, msg, t); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isInfoEnabled() { + return true; + } + + + /** + * {@inheritDoc} + */ + @Override + public void info(final String msg) { + logMessageWithFormat(MessageChannel.INFORMATION, msg); + } + + /** + * {@inheritDoc} + */ + @Override + public void info(String format, Object arg) { + logMessageWithFormat(MessageChannel.INFORMATION, format, arg); + } + + /** + * {@inheritDoc} + */ + @Override + public void info(final String format, final Object arg1, final Object arg2) { + logMessageWithFormat(MessageChannel.INFORMATION, format, arg1, arg2); + } + + /** + * {@inheritDoc} + */ + @Override + public void info(String format, Object... args) { + logMessageWithFormat(MessageChannel.INFORMATION, format, args); + } + + /** + * {@inheritDoc} + */ + @Override + public void info(final String msg, final Throwable t) { + log(MessageChannel.INFORMATION, msg, t); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isWarnEnabled() { + return true; + } + + /** + * {@inheritDoc} + */ + @Override + public void warn(final String msg) { + logMessageWithFormat(MessageChannel.WARNING, msg); + } + + /** + * {@inheritDoc} + */ + @Override + public void warn(String format, Object arg) { + logMessageWithFormat(MessageChannel.WARNING, format, arg); + } + + /** + * {@inheritDoc} + */ + @Override + public void warn(final String format, final Object arg1, final Object arg2) { + logMessageWithFormat(MessageChannel.WARNING, format, arg1, arg2); + } + + /** + * {@inheritDoc} + */ + @Override + public void warn(String format, Object... args) { + logMessageWithFormat(MessageChannel.WARNING, format, args); + } + + /** + * {@inheritDoc} + */ + @Override + public void warn(final String msg, final Throwable t) { + log(MessageChannel.WARNING, msg, t); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isErrorEnabled() { + return true; + } + + /** + * {@inheritDoc} + */ + @Override + public void error(String format, Object arg) { + logMessageWithFormat(MessageChannel.ERROR, format, arg); + } + + /** + * {@inheritDoc} + */ + @Override + public void error(final String msg) { + logMessageWithFormat(MessageChannel.ERROR, msg); + } + + /** + * {@inheritDoc} + */ + @Override + public void error(final String format, final Object arg1, final Object arg2) { + logMessageWithFormat(MessageChannel.ERROR, format, arg1, arg2); + } + + /** + * {@inheritDoc} + */ + @Override + public void error(String format, Object... args) { + logMessageWithFormat(MessageChannel.ERROR, format, args); + } + + /** + * {@inheritDoc} + */ + @Override + public void error(final String msg, final Throwable t) { + log(MessageChannel.ERROR, msg, t); + } + + /** + * Format and write the message according to the {@code MESSAGE_TEMPLATE}. + * + * @param messageChannel The level to log. + * @param format The log message format. + * @param arguments a list of arbitrary arguments taken in by format. + */ + private void logMessageWithFormat(MessageChannel messageChannel, String format, Object... arguments) { + FormattingTuple tp = MessageFormatter.arrayFormat(format, arguments); + log(messageChannel, tp.getMessage(), tp.getThrowable()); + } + + /** + * Format and write the message according to the {@code MESSAGE_TEMPLATE}. + * + * @param messageChannel log level + * @param message The message itself + * @param t The exception whose stack trace should be logged + */ + private void log(MessageChannel messageChannel, String message, Throwable t) { + plugin.message(messageChannel, message, t, keys); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/plugin/package-info.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/plugin/package-info.java new file mode 100644 index 0000000000..9e64cb12b4 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/extension/plugin/package-info.java @@ -0,0 +1,7 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/** + * Contains classes for managing AutoRest plugins. + */ +package com.microsoft.typespec.http.client.generator.core.extension.plugin; diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/implementation/ClientModelPropertiesManager.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/implementation/ClientModelPropertiesManager.java new file mode 100644 index 0000000000..0962619cc9 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/implementation/ClientModelPropertiesManager.java @@ -0,0 +1,703 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.implementation; + +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientModel; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientModelProperty; +import com.microsoft.typespec.http.client.generator.core.util.ClientModelUtil; +import com.azure.core.util.CoreUtils; +import com.azure.json.JsonReader; +import com.azure.json.JsonSerializable; +import com.azure.xml.XmlReader; +import com.azure.xml.XmlSerializable; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +/** + * Manages metadata about properties in a {@link ClientModel} and how they correlate with model class generation. + *

+ * This will bucket all properties, and super type properties, into the buckets of required, optional, and additional + * properties. In addition to bucketing, each property will be checked if it requires flattening and be used to generate + * the flattened properties structure for the model. + *

+ * This will also handle getting the discriminator property and the expected value for the field. + */ +public final class ClientModelPropertiesManager { + private final ClientModel model; + private final String deserializedModelName; + private final boolean hasRequiredProperties; + private final boolean hasConstructorArguments; + private final int requiredPropertiesCount; + private final int setterPropertiesCount; + private final int readOnlyPropertiesCount; + private final LinkedHashMap superConstructorProperties; + private final LinkedHashMap superRequiredProperties; + private final LinkedHashMap superSetterProperties; + private final LinkedHashMap superReadOnlyProperties; + private final ClientModelProperty superAdditionalPropertiesProperty; + private final LinkedHashMap constructorProperties; + private final LinkedHashMap requiredProperties; + private final LinkedHashMap setterProperties; + private final LinkedHashMap readOnlyProperties; + private final ClientModelProperty additionalProperties; + private final ClientModelPropertyWithMetadata discriminatorProperty; + private final String expectedDiscriminator; + private final JsonFlattenedPropertiesTree jsonFlattenedPropertiesTree; + private final String jsonReaderFieldNameVariableName; + + private final String xmlRootElementName; + private final String xmlRootElementNamespace; + private final boolean hasXmlElements; + private final boolean hasXmlTexts; + private final String xmlReaderNameVariableName; + private final List superXmlAttributes; + private final List xmlAttributes; + private final List superXmlTexts; + private final List xmlTexts; + private final List superXmlElements; + private final List xmlElements; + private final Map xmlNamespaceWithPrefix; + private final Map xmlNamespaceToConstantMapping; + + /** + * Creates a new instance of {@link ClientModelPropertiesManager}. + * + * @param model The {@link ClientModel}. + */ + public ClientModelPropertiesManager(ClientModel model, JavaSettings settings) { + // The reader name variable needs to be mutable as it may match a property name in the class. + Set possibleReaderFieldNameVariableNames = new LinkedHashSet<>(Arrays.asList( + "fieldName", "jsonFieldName", "deserializationFieldName")); + Set possibleXmlNameVariableNames = new LinkedHashSet<>(Arrays.asList( + "elementName", "xmlElementName", "deserializationElementName")); + this.model = model; + this.deserializedModelName = "deserialized" + model.getName(); + this.expectedDiscriminator = model.getSerializedName(); + ClientModelPropertyWithMetadata discriminatorProperty = null; + + Map flattenedProperties = new LinkedHashMap<>(); + boolean hasRequiredProperties = false; + boolean hasConstructorArguments = false; + superConstructorProperties = new LinkedHashMap<>(); + superRequiredProperties = new LinkedHashMap<>(); + superSetterProperties = new LinkedHashMap<>(); + superReadOnlyProperties = new LinkedHashMap<>(); + ClientModelProperty superAdditionalProperties = null; + boolean hasXmlElements = false; + boolean hasXmlTexts = false; + xmlNamespaceWithPrefix = new LinkedHashMap<>(); + superXmlAttributes = new ArrayList<>(); + xmlAttributes = new ArrayList<>(); + superXmlTexts = new ArrayList<>(); + xmlTexts = new ArrayList<>(); + superXmlElements = new ArrayList<>(); + xmlElements = new ArrayList<>(); + + if (model.isPolymorphic()) { + ClientModel superTypeModel = model; + ClientModel parentModel = ClientModelUtil.getClientModel(model.getParentModelName()); + while (parentModel != null) { + superTypeModel = parentModel; + parentModel = ClientModelUtil.getClientModel(superTypeModel.getParentModelName()); + } + + xmlRootElementName = superTypeModel.getXmlName(); + xmlRootElementNamespace = superTypeModel.getXmlNamespace(); + } else { + xmlRootElementName = model.getXmlName(); + xmlRootElementNamespace = model.getXmlNamespace(); + } + + for (ClientModelProperty property : ClientModelUtil.getParentProperties(model)) { + // Ignore additional properties from parent types as it will be handled specifically in the subtype. + if (property.isAdditionalProperties()) { + superAdditionalProperties = property; + continue; + } + + if (property.isPolymorphicDiscriminator()) { + discriminatorProperty = new ClientModelPropertyWithMetadata(model, property, true); + } + + if (!property.isPolymorphicDiscriminator()) { + superPropertyConsumer(property, superRequiredProperties, superConstructorProperties, + superReadOnlyProperties, superSetterProperties, settings); + hasRequiredProperties |= property.isRequired(); + hasConstructorArguments |= ClientModelUtil.includePropertyInConstructor(property, settings); + } + + if (property.getNeedsFlatten()) { + flattenedProperties.put(property.getName(), new ClientModelPropertyWithMetadata(model, property, true)); + } + + possibleReaderFieldNameVariableNames.remove(property.getName()); + possibleXmlNameVariableNames.remove(property.getName()); + + if (property.isXmlAttribute()) { + if (!property.isPolymorphicDiscriminator()) { + superXmlAttributes.add(property); + } + } else if (property.isXmlText()) { + hasXmlTexts = true; + superXmlTexts.add(property); + } else { + hasXmlElements = true; + superXmlElements.add(property); + } + + if (!CoreUtils.isNullOrEmpty(property.getXmlPrefix())) { + xmlNamespaceWithPrefix.put(property.getXmlPrefix(), property.getXmlNamespace()); + } + } + + this.superAdditionalPropertiesProperty = superAdditionalProperties; + + constructorProperties = new LinkedHashMap<>(); + requiredProperties = new LinkedHashMap<>(); + setterProperties = new LinkedHashMap<>(); + readOnlyProperties = new LinkedHashMap<>(); + ClientModelProperty additionalProperties = null; + for (ClientModelProperty property : model.getProperties()) { + if (property.isRequired()) { + hasRequiredProperties = true; + requiredProperties.put(property.getSerializedName(), property); + + if (!property.isConstant()) { + if (ClientModelUtil.includePropertyInConstructor(property, settings)) { + constructorProperties.put(property.getSerializedName(), property); + hasConstructorArguments = true; + } else { + readOnlyProperties.put(property.getSerializedName(), property); + } + } + } else if (property.isAdditionalProperties()) { + // Extract the additionalProperties property as this will need to be passed into all deserialization + // logic creation calls. + additionalProperties = property; + } else { + setterProperties.put(property.getSerializedName(), property); + } + + if (property.isPolymorphicDiscriminator()) { + if (discriminatorProperty == null) { + discriminatorProperty = new ClientModelPropertyWithMetadata(model, property, false); + } else if (Objects.equals(discriminatorProperty.getProperty().getSerializedName(), property.getSerializedName())) { + discriminatorProperty = new ClientModelPropertyWithMetadata(model, property, true); + } else { + discriminatorProperty = new ClientModelPropertyWithMetadata(model, property, false); + } + } + + if (property.getNeedsFlatten()) { + flattenedProperties.put(property.getName(), new ClientModelPropertyWithMetadata(model, property, false)); + } + + possibleReaderFieldNameVariableNames.remove(property.getName()); + possibleXmlNameVariableNames.remove(property.getName()); + + if (property.isXmlAttribute()) { + if (!property.isPolymorphicDiscriminator()) { + xmlAttributes.add(property); + } + } else if (property.isXmlText()) { + hasXmlTexts = true; + xmlTexts.add(property); + } else { + hasXmlElements = true; + xmlElements.add(property); + } + + if (!CoreUtils.isNullOrEmpty(property.getXmlPrefix())) { + xmlNamespaceWithPrefix.put(property.getXmlPrefix(), property.getXmlNamespace()); + } + } + + this.hasRequiredProperties = hasRequiredProperties; + this.requiredPropertiesCount = requiredProperties.size() + superRequiredProperties.size(); + this.setterPropertiesCount = setterProperties.size() + superSetterProperties.size(); + this.readOnlyPropertiesCount = readOnlyProperties.size() + superReadOnlyProperties.size(); + this.hasConstructorArguments = hasConstructorArguments; + this.hasXmlElements = hasXmlElements; + this.hasXmlTexts = hasXmlTexts; + this.discriminatorProperty = discriminatorProperty; + this.additionalProperties = additionalProperties; + this.jsonFlattenedPropertiesTree = getFlattenedPropertiesHierarchy(model.getPolymorphicDiscriminatorName(), + flattenedProperties); + Iterator possibleReaderFieldNameVariableNamesIterator = possibleReaderFieldNameVariableNames.iterator(); + if (possibleReaderFieldNameVariableNamesIterator.hasNext()) { + this.jsonReaderFieldNameVariableName = possibleReaderFieldNameVariableNamesIterator.next(); + } else { + throw new IllegalStateException("Model properties exhausted all possible JsonReader field name variables. " + + "Add additional possible JsonReader field name variables to resolve this issue."); + } + + Iterator possibleXmlNameVariableNamesIterator = possibleXmlNameVariableNames.iterator(); + if (possibleXmlNameVariableNamesIterator.hasNext()) { + this.xmlReaderNameVariableName = possibleXmlNameVariableNamesIterator.next(); + } else { + throw new IllegalStateException("Model properties exhausted all possible XmlReader name variables. " + + "Add additional possible XmlReader name variables to resolve this issue."); + } + + this.xmlNamespaceToConstantMapping = model.getXmlName() == null + ? Collections.emptyMap() : ClientModelUtil.xmlNamespaceToConstantMapping(model); + } + + private static void superPropertyConsumer(ClientModelProperty property, + Map superRequiredProperties, + Map superConstructorProperties, + Map superReadOnlyProperties, + Map superSetterProperties, JavaSettings settings) { + if (property.isRequired()) { + superRequiredProperties.put(property.getSerializedName(), property); + + if (!property.isConstant()) { + if (ClientModelUtil.includePropertyInConstructor(property, settings)) { + superConstructorProperties.put(property.getSerializedName(), property); + } else { + superReadOnlyProperties.put(property.getSerializedName(), property); + } + } + } else { + superSetterProperties.put(property.getSerializedName(), property); + } + } + + /** + * Gets the {@link ClientModel} that the properties are based on. + * + * @return The {@link ClientModel} that the properties are based on. + */ + public ClientModel getModel() { + return model; + } + + /** + * Gets the name of the variable used when deserializing an instance of the {@link #getModel() model}. + * + * @return The name of the variable used during deserialization. + */ + public String getDeserializedModelName() { + return deserializedModelName; + } + + /** + * Whether the {@link #getModel() model} contains required properties, either directly or through super classes. + * + * @return Whether the {@link #getModel() model} contains required properties. + */ + public boolean hasRequiredProperties() { + return hasRequiredProperties; + } + + /** + * Gets the number of required properties in the {@link #getModel() model}. + * + * @return The number of required properties in the {@link #getModel() model}. + */ + public int getRequiredPropertiesCount() { + return requiredPropertiesCount; + } + + /** + * Gets the number of setter properties in the {@link #getModel() model}. + * + * @return The number of setter properties in the {@link #getModel() model}. + */ + public int getSetterPropertiesCount() { + return setterPropertiesCount; + } + + /** + * Gets the number of read-only properties in the {@link #getModel() model}. + * + * @return The number of read-only properties in the {@link #getModel() model}. + */ + public int getReadOnlyPropertiesCount() { + return readOnlyPropertiesCount; + } + + /** + * Whether the {@link #getModel() model} has constructor arguments, either directly or required through super + * classes. + * + * @return Whether the {@link #getModel() model} contains constructor arguments. + */ + public boolean hasConstructorArguments() { + return hasConstructorArguments; + } + + /** + * Consumes each constructor {@link ClientModelProperty property} defined by super classes of the + * {@link #getModel() model}. + * + * @param consumer The {@link ClientModelProperty} consumer. + */ + public void forEachSuperConstructorProperty(Consumer consumer) { + superConstructorProperties.values().forEach(consumer); + } + + /** + * Consumes each required {@link ClientModelProperty property} defined by super classes of the + * {@link #getModel() model}. + * + * @param consumer The {@link ClientModelProperty} consumer. + */ + public void forEachSuperRequiredProperty(Consumer consumer) { + superRequiredProperties.values().forEach(consumer); + } + + /** + * Consumes each non-required {@link ClientModelProperty property} defined by super classes of the + * {@link #getModel() model}. + * + * @param consumer The {@link ClientModelProperty} consumer. + */ + public void forEachSuperSetterProperty(Consumer consumer) { + superSetterProperties.values().forEach(consumer); + } + + /** + * Consumes each read-only {@link ClientModelProperty property} defined by super classes of the + * {@link #getModel() model}. + * + * @param consumer The {@link ClientModelProperty} consumer. + */ + public void forEachSuperReadOnlyProperty(Consumer consumer) { + superReadOnlyProperties.values().forEach(consumer); + } + + /** + * Consumes each constructor {@link ClientModelProperty property} defined by the {@link #getModel() model}. + * + * @param consumer The {@link ClientModelProperty} consumer. + */ + public void forEachConstructorProperty(Consumer consumer) { + constructorProperties.values().forEach(consumer); + } + + /** + * Consumes each required {@link ClientModelProperty property} defined by the {@link #getModel() model}. + * + * @param consumer The {@link ClientModelProperty} consumer. + */ + public void forEachRequiredProperty(Consumer consumer) { + requiredProperties.values().forEach(consumer); + } + + /** + * Consumes each non-required {@link ClientModelProperty property} defined by the {@link #getModel() model}. + * + * @param consumer The {@link ClientModelProperty} consumer. + */ + public void forEachSetterProperty(Consumer consumer) { + setterProperties.values().forEach(consumer); + } + + /** + * Consumes each read-only {@link ClientModelProperty property} defined by the {@link #getModel() model}. + * + * @param consumer The {@link ClientModelProperty} consumer. + */ + public void forEachReadOnlyProperty(Consumer consumer) { + readOnlyProperties.values().forEach(consumer); + } + + /** + * Gets the {@link ClientModelProperty} that defines the additional properties property. + *

+ * If the model doesn't contain additional properties this will return null. + * + * @return The {@link ClientModelProperty} that defines the additional properties property, or null if the model + * doesn't define additional properties. + */ + public ClientModelProperty getAdditionalProperties() { + return additionalProperties; + } + + /** + * Gets the {@link ClientModelProperty} that defines the additional properties property in superclass. + *

+ * If the no superclass contain additional properties, this will return null. + * + * @return The {@link ClientModelProperty} that defines the additional properties property in superclass, or null if + * no superclass defines additional properties. + */ + public ClientModelProperty getSuperAdditionalPropertiesProperty() { + return superAdditionalPropertiesProperty; + } + + /** + * Gets the {@link ClientModelPropertyWithMetadata} that defines the discriminator property for polymorphic types. + *

+ * If the model isn't polymorphic this will return null. + * + * @return The {@link ClientModelPropertyWithMetadata} that defines the discriminator property for polymorphic + * types, or null if the model isn't a polymorphic type. + */ + public ClientModelPropertyWithMetadata getDiscriminatorProperty() { + return discriminatorProperty; + } + + /** + * Gets the expected discriminator value for the polymorphic model. + *

+ * If the model isn't polymorphic this will return null. + * + * @return The expected discriminator value for the polymorphic model, or null if the model isn't a polymorphic + * type. + */ + public String getExpectedDiscriminator() { + return expectedDiscriminator; + } + + /** + * Gets the JSON flattened properties tree for the model. + *

+ * If the model doesn't contain any JSON flattening this will return null. + * + * @return The JSON flattened properties tree for the model, or null if the model doesn't contain any JSON + * flattening. + */ + public JsonFlattenedPropertiesTree getJsonFlattenedPropertiesTree() { + return jsonFlattenedPropertiesTree; + } + + /** + * Gets the variable name for {@link JsonReader#getFieldName()} in {@link JsonSerializable#fromJson(JsonReader)} + * implementations. + *

+ * This is used instead of a static variable name as deserialization maintains holders for required properties which + * could conflict with the static variable name. The constructor manages determination of the variable name by + * tracking a set of possible names, if all possible names are exhausted the constructor will throw an exception to + * indicate more possible names need to be added to support all code generation expectations. + * + * @return The variable name that tracks the current JSON field name. + */ + public String getJsonReaderFieldNameVariableName() { + return jsonReaderFieldNameVariableName; + } + + /** + * Gets the variable name for {@link XmlReader#getElementName()} in {@link XmlSerializable#fromXml(XmlReader)} + * implementations. + *

+ * This is used instead of a static variable name as deserialization maintains holders for required properties which + * could conflict with the static variable name. The constructor manages determination of the variable name by + * tracking a set of possible names, if all possible names are exhausted the constructor will throw an exception to + * indicate more possible names need to be added to support all code generation expectations. + * + * @return The variable name that tracks the current XML name. + */ + public String getXmlReaderNameVariableName() { + return xmlReaderNameVariableName; + } + + /** + * Gets the default XML root element name for the model. + *

+ * Polymorphism for XML works differently from JSON where the discriminator to determine which type to deserialize + * is determined by an attribute rather than a special property. This results in the super type and all subtypes + * using the same root element name determined by the super type. + * + * @return The default XML root element name. + */ + public String getXmlRootElementName() { + return xmlRootElementName; + } + + /** + * Gets the XML root element namespace for the model. + *

+ * Polymorphism for XML has the super type define the XML namespace. + * + * @return The XML root element namespace. + */ + public String getXmlRootElementNamespace() { + return xmlRootElementNamespace; + } + + /** + * Whether the {@link #getModel() model} defines XML elements, XML properties that aren't + * {@link ClientModelProperty#isXmlAttribute() attributes} or {@link ClientModelProperty#isXmlText() text}. + * + * @return Whether the {@link #getModel() model} defines XML elements + */ + public boolean hasXmlElements() { + return hasXmlElements; + } + + /** + * Whether the {@link #getModel() model} defines XML texts, XML properties that are + * {@link ClientModelProperty#isXmlText() text}. + * + * @return Whether the {@link #getModel() model} defines XML texts + */ + public boolean hasXmlTexts() { + return hasXmlTexts; + } + + /** + * Consumes each XML namespace that has a prefix. + * + * @param consumer XML namespace with prefix consumer. + */ + public void forEachXmlNamespaceWithPrefix(BiConsumer consumer) { + xmlNamespaceWithPrefix.forEach(consumer); + } + + /** + * Consumes each XML attribute {@link ClientModelProperty property} defined by super classes of the + * {@link #getModel() model}. + * + * @param consumer The {@link ClientModelProperty} consumer. + */ + public void forEachSuperXmlAttribute(Consumer consumer) { + superXmlAttributes.forEach(consumer); + } + + /** + * Consumes each XML attribute {@link ClientModelProperty property} defined by the {@link #getModel() model}. + * + * @param consumer The {@link ClientModelProperty} consumer. + */ + public void forEachXmlAttribute(Consumer consumer) { + xmlAttributes.forEach(consumer); + } + + /** + * Consumes each XML text {@link ClientModelProperty property} defined by super classes of the + * {@link #getModel() model}. + * + * @param consumer The {@link ClientModelProperty} consumer. + */ + public void forEachSuperXmlText(Consumer consumer) { + superXmlTexts.forEach(consumer); + } + + /** + * Consumes each XML text {@link ClientModelProperty property} defined by the {@link #getModel() model}. + * + * @param consumer The {@link ClientModelProperty} consumer. + */ + public void forEachXmlText(Consumer consumer) { + xmlTexts.forEach(consumer); + } + + /** + * Consumes each XML element {@link ClientModelProperty property} defined by super classes of the + * {@link #getModel() model}. + * + * @param consumer The {@link ClientModelProperty} consumer. + */ + public void forEachSuperXmlElement(Consumer consumer) { + superXmlElements.forEach(consumer); + } + + /** + * Consumes each XML element {@link ClientModelProperty property} defined by the {@link #getModel() model}. + * + * @param consumer The {@link ClientModelProperty} consumer. + */ + public void forEachXmlElement(Consumer consumer) { + xmlElements.forEach(consumer); + } + + /** + * Gets the XML namespace constant for the given XML namespace. + * + * @param xmlNamespace The XML namespace. + * @return The XML namespace constant. + */ + public String getXmlNamespaceConstant(String xmlNamespace) { + return xmlNamespaceToConstantMapping.get(xmlNamespace); + } + + /** + * Takes all properties that will be included in a {@code fromJson(JsonReader)} call and for all properties that are + * flattened creates a tree representation of their paths. + *

+ * Flattened properties require additional processing as they must be handled at the same time. For example if a + * model has three flattened properties with JSON paths "im.flattened", "im.deeper.flattened", and + * "im.deeper.flattenedtoo" this will create the following structure: + * + *

+     * im -> flattened
+     *     | deeper -> flattened
+     *               | flattenedtoo
+     * 
+ * + * This structure is then used while generating deserialization logic to ensure that when the "im" JSON sub-object + * is found that it'll look for both "flattened" and the "deeper" JSON sub-object before either reading or skipping + * unknown properties. If this isn't done and deserialization logic is generated on a property-by-property basis, + * this could result in the "im.flattened" check skipping the "deeper" JSON sub-object. + * + * @param discriminatorProperty A potential discriminator property for hierarchical models. + * @param flattenedProperties All flattened properties that are part of a model's deserialization. + * @return The flattened JSON properties structure, or an empty structure if the model doesn't contained flattened + * properties. + */ + private static JsonFlattenedPropertiesTree getFlattenedPropertiesHierarchy(String discriminatorProperty, + Map flattenedProperties) { + JsonFlattenedPropertiesTree structure = JsonFlattenedPropertiesTree.createBaseNode(); + + if (!CoreUtils.isNullOrEmpty(discriminatorProperty)) { + List propertyHierarchy = ClientModelUtil.splitFlattenedSerializedName(discriminatorProperty); + if (!propertyHierarchy.isEmpty()) { + structure = JsonFlattenedPropertiesTree.createBaseNode(); + } + } + + for (ClientModelPropertyWithMetadata property : flattenedProperties.values()) { + if (!property.getProperty().getNeedsFlatten()) { + // Property doesn't need flattening, ignore it. + continue; + } + + // Splits the flattened property into the individual properties in the JSON path. + // For example "im.deeper.flattened" becomes ["im", "deeper", "flattened"]. + List propertyHierarchy = ClientModelUtil.splitFlattenedSerializedName(property.getProperty().getSerializedName()); + + if (propertyHierarchy.size() == 1) { + // Property is marked for flattening but points directly to its JSON path, ignore it. + continue; + } + + // Loop over all the property names in the JSON path, either getting or creating that node in the + // flattened JSON properties structure. + JsonFlattenedPropertiesTree pointer = structure; + for (int i = 0; i < propertyHierarchy.size(); i++) { + String nodeName = propertyHierarchy.get(i); + + // Structure doesn't contain the flattened property. + if (!pointer.hasChildNode(nodeName)) { + // Depending on whether this is the last property in the flattened property either a terminal + // or intermediate node will be inserted. + JsonFlattenedPropertiesTree newPointer = (i == propertyHierarchy.size() - 1) + ? JsonFlattenedPropertiesTree.createTerminalNode(nodeName, property) + : JsonFlattenedPropertiesTree.createIntermediateNode(nodeName); + + pointer.addChildNode(newPointer); + pointer = newPointer; + } else { + pointer = pointer.getChildNode(nodeName); + } + } + } + + return structure; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/implementation/ClientModelPropertyWithMetadata.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/implementation/ClientModelPropertyWithMetadata.java new file mode 100644 index 0000000000..47064edec5 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/implementation/ClientModelPropertyWithMetadata.java @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.implementation; + +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientModel; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientModelProperty; + +/** + * A {@link ClientModelProperty} with additional metadata specific to a given {@link ClientModel}. + *

+ * For example this class could contain information such as whether the {@link ClientModelProperty} is from a super + * type of the given {@link ClientModel}. + */ +public final class ClientModelPropertyWithMetadata { + private final ClientModel model; + private final ClientModelProperty property; + private final boolean fromSuperClass; + + /** + * Creates a new instance of {@link ClientModelPropertyWithMetadata}. + * + * @param model The {@link ClientModel}. + * @param property The {@link ClientModelProperty}. + * @param fromSuperClass Whether the property is from a super class of the specific {@link ClientModel}. + */ + public ClientModelPropertyWithMetadata(ClientModel model, ClientModelProperty property, boolean fromSuperClass) { + this.model = model; + this.property = property; + this.fromSuperClass = fromSuperClass; + } + + /** + * Gets the {@link ClientModel} that the {@link #getProperty() ClientModelProperty} is from. + * + * @return The containing {@link ClientModel}. + */ + public ClientModel getModel() { + return model; + } + + /** + * Gets the {@link ClientModelProperty} that the metadata is based on. + * + * @return The {@link ClientModelProperty} that the metadata is based on. + */ + public ClientModelProperty getProperty() { + return property; + } + + /** + * Whether the {@link ClientModelProperty} is from a super class of the {@link ClientModel}. + * + * @return Whether the {@link ClientModelProperty} is from a super class of the {@link ClientModel}. + */ + public boolean isFromSuperClass() { + return fromSuperClass; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/implementation/JsonFlattenedPropertiesTree.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/implementation/JsonFlattenedPropertiesTree.java new file mode 100644 index 0000000000..a23f23cccc --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/implementation/JsonFlattenedPropertiesTree.java @@ -0,0 +1,141 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.implementation; + +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientModel; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientModelProperty; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Manages the structure of JSON flattened {@link ClientModelProperty ClientModelProperties} in a {@link ClientModel}. + *

+ * This structure determines how model types are generated when handling JSON flattened stream-style serialization in + * regard to where properties are stored. + */ +public final class JsonFlattenedPropertiesTree { + // A property being set indicates that this is a terminal value. + private final ClientModelPropertyWithMetadata property; + private final String nodeName; + private final Map nodes; + + /** + * Creates a base node for a JSON flattened tree. + * + * @return A new base node for a JSON flattened tree. + */ + public static JsonFlattenedPropertiesTree createBaseNode() { + return new JsonFlattenedPropertiesTree(null, null); + } + + /** + * Creates an intermediate node for a JSON flattened tree. + *

+ * Intermediate nodes manage steps in JSON flattened structures, for example {@code a.flattened.json} + * would have two intermediate nodes in {@code a} and {@code flattened} where {@code json} would be a terminal + * node. + * + * @param nodeName Name of the node, correlates to the JSON property name. + * @return A new intermediate node for a JSON flattened tree. + */ + public static JsonFlattenedPropertiesTree createIntermediateNode(String nodeName) { + return new JsonFlattenedPropertiesTree(null, nodeName); + } + + /** + * Creates a terminal node for a JSON flattened tree. + *

+ * Terminal nodes manage end points in JSON flattened structures, for example {@code a.flattened.json} + * would have two intermediate nodes in {@code a} and {@code flattened} where {@code json} would be a terminal + * node. + * + * @param nodeName Name of the node, correlates to the JSON property name. + * @param property The {@link ClientModelProperty} which contains information about the JSON property. + * @return A new terminal node for a JSON flattened tree. + */ + public static JsonFlattenedPropertiesTree createTerminalNode(String nodeName, + ClientModelPropertyWithMetadata property) { + return new JsonFlattenedPropertiesTree(property, nodeName); + } + + /** + * Adds a new child node into the JSON flattened tree. + *

+ * An exception will be thrown if the node having the child node added is a terminal node. Terminal nodes cannot + * have children otherwise the JSON structure would be invalid. + * + * @param childNode The child node. + * @throws IllegalStateException If the node having the child node added is a terminal node. + */ + public void addChildNode(JsonFlattenedPropertiesTree childNode) { + if (property != null) { + throw new IllegalStateException("JSON flatten structure contains a terminal node and intermediate " + + "node with the same JSON property. This isn't valid as it would require the JSON property " + + "to be both a sub-object and a value node."); + } + + nodes.put(childNode.nodeName, childNode); + } + + /** + * Whether this node has a child node with the specified name. + * + * @param childNodeName The child node name. + * @return Whether this node has a child node with the specified name. + */ + public boolean hasChildNode(String childNodeName) { + return nodes.containsKey(childNodeName); + } + + /** + * Gets the child not with the specified name. + *

+ * If this node doesn't contain a child node with the specified name null will be returned. + * + * @param childNodeName The child node name. + * @return The child name with the specified name, or null if it doesn't exist. + */ + public JsonFlattenedPropertiesTree getChildNode(String childNodeName) { + return nodes.get(childNodeName); + } + + /** + * Gets the children nodes for this node. + * + * @return The children nodes for this node. + */ + public Map getChildrenNodes() { + return nodes; + } + + /** + * Gets the name of this node. + *

+ * If this is the root node this will be null. + * + * @return The name of this node, or null if it's the root node. + */ + public String getNodeName() { + return nodeName; + } + + /** + * Gets the {@link ClientModelProperty} associated to this node. + *

+ * If this is the root or an intermediate node this will be null. + * + * @return The {@link ClientModelProperty} associated to this node, or null if this is the root or an intermediate + * node. + */ + public ClientModelPropertyWithMetadata getProperty() { + return property; + } + + private JsonFlattenedPropertiesTree(ClientModelPropertyWithMetadata property, String nodeName) { + this.property = property; + this.nodeName = nodeName; + this.nodes = new LinkedHashMap<>(); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/mapper/AnyMapper.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/mapper/AnyMapper.java new file mode 100644 index 0000000000..e792fd1a87 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/mapper/AnyMapper.java @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.mapper; + +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.AnySchema; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClassType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.IType; + +/** + * A mapper that maps an {@link AnySchema} to {@link ClassType#OBJECT}, always. + */ +public class AnyMapper implements IMapper { + + private static final AnyMapper INSTANCE = new AnyMapper(); + + private AnyMapper() { + // private constructor + } + + /** + * Gets the global {@link AnyMapper} instance. + * + * @return The global {@link AnyMapper} instance. + */ + public static AnyMapper getInstance() { + return INSTANCE; + } + + @Override + public IType map(AnySchema anySchema) { + return ClassType.OBJECT; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/mapper/ArrayMapper.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/mapper/ArrayMapper.java new file mode 100644 index 0000000000..2e337ac5d2 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/mapper/ArrayMapper.java @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.mapper; + +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.ArraySchema; +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.IType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.IterableType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ListType; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * A mapper that maps an {@link ArraySchema} to either an {@link IterableType} or {@link ListType}. + */ +public class ArrayMapper implements IMapper { + private static final ArrayMapper INSTANCE = new ArrayMapper(); + Map parsed = new ConcurrentHashMap<>(); + + private ArrayMapper() { + } + + /** + * Gets the global {@link ArrayMapper} instance. + * + * @return The global {@link ArrayMapper} instance. + */ + public static ArrayMapper getInstance() { + return INSTANCE; + } + + @Override + public IType map(ArraySchema sequenceType) { + if (sequenceType == null) { + return null; + } + + IType arrayType = parsed.get(sequenceType); + if (arrayType != null) { + return arrayType; + } + + IType mappedType = Mappers.getSchemaMapper().map(sequenceType.getElementType()); + + // Choose IterableType or ListType depending on whether arrays should use Iterable. + arrayType = JavaSettings.getInstance().isUseIterable() + ? new IterableType(mappedType) + : new ListType(mappedType); + + parsed.put(sequenceType, arrayType); + return arrayType; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/mapper/BinaryMapper.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/mapper/BinaryMapper.java new file mode 100644 index 0000000000..7a72096f54 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/mapper/BinaryMapper.java @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.mapper; + +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.BinarySchema; +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClassType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.GenericType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.IType; + +/** + * A mapper that maps a {@link BinarySchema} to {@link GenericType#FLUX_BYTE_BUFFER} when the schema isn't null. + */ +public class BinaryMapper implements IMapper { + + private static final BinaryMapper INSTANCE = new BinaryMapper(); + + /** + * Gets the global {@link BinaryMapper} instance. + * + * @return The global {@link BinaryMapper} instance. + */ + public static BinaryMapper getInstance() { + return INSTANCE; + } + + @Override + public IType map(BinarySchema binarySchema) { + if (binarySchema == null) { + return null; + } + return JavaSettings.getInstance().isDataPlaneClient() + ? ClassType.BINARY_DATA + : GenericType.FLUX_BYTE_BUFFER; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/mapper/ChoiceMapper.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/mapper/ChoiceMapper.java new file mode 100644 index 0000000000..7d6051263b --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/mapper/ChoiceMapper.java @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.mapper; + +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.ChoiceSchema; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.EnumType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.IType; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * A mapper that maps a {@link ChoiceSchema} to an {@link EnumType}. + */ +public class ChoiceMapper implements IMapper { + private static final ChoiceMapper INSTANCE = new ChoiceMapper(); + Map parsed = new ConcurrentHashMap<>(); + + private ChoiceMapper() { + } + + /** + * Gets the global {@link ChoiceMapper} instance. + * + * @return The global {@link ChoiceMapper} instance. + */ + public static ChoiceMapper getInstance() { + return INSTANCE; + } + + @Override + public IType map(ChoiceSchema enumType) { + if (enumType == null) { + return null; + } + + IType choiceType = parsed.get(enumType); + if (choiceType != null) { + return choiceType; + } + + choiceType = createChoiceType(enumType); + parsed.put(enumType, choiceType); + + return choiceType; + } + + private IType createChoiceType(ChoiceSchema enumType) { + return MapperUtils.createEnumType(enumType, true); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/mapper/ClientMapper.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/mapper/ClientMapper.java new file mode 100644 index 0000000000..ff3eb439db --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/mapper/ClientMapper.java @@ -0,0 +1,714 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.mapper; + +import com.microsoft.typespec.http.client.generator.core.Javagen; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.ArraySchema; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.ChoiceSchema; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.CodeModel; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.DictionarySchema; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Header; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Language; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Languages; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.ObjectSchema; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Operation; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.OperationGroup; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Parameter; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Property; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Request; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Response; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Schema; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.SchemaContext; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Scheme; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.SealedChoiceSchema; +import com.microsoft.typespec.http.client.generator.core.extension.model.extensionmodel.XmsExtensions; +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.AsyncSyncClient; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClassType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.Client; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientBuilder; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientBuilderTrait; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientException; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethod; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethodExample; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethodType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientModel; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientModels; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientResponse; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ConvenienceMethod; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.EnumType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ExternalPackage; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.IType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ModuleInfo; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.PackageInfo; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ProtocolExample; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ServiceClient; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.XmlSequenceWrapper; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaVisibility; +import com.microsoft.typespec.http.client.generator.core.template.Templates; +import com.microsoft.typespec.http.client.generator.core.util.ClientModelUtil; +import com.microsoft.typespec.http.client.generator.core.util.CodeNamer; +import com.microsoft.typespec.http.client.generator.core.util.SchemaUtil; +import com.azure.core.util.CoreUtils; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * A mapper that maps a {@link CodeModel} to a {@link Client}. + */ +public class ClientMapper implements IMapper { + private static final ClientMapper INSTANCE = new ClientMapper(); + + protected ClientMapper() { + } + + /** + * Gets the global {@link ClientMapper} instance. + * + * @return The global {@link ClientMapper} instance. + */ + public static ClientMapper getInstance() { + return INSTANCE; + } + + @Override + public Client map(CodeModel codeModel) { + JavaSettings settings = JavaSettings.getInstance(); + Client.Builder builder = new Client.Builder(); + + // enum model + final List enumTypes = new ArrayList<>(); + Set enumNames = new HashSet<>(); + for (ChoiceSchema choiceSchema : codeModel.getSchemas().getChoices()) { + IType iType = Mappers.getChoiceMapper().map(choiceSchema); + if (iType != ClassType.STRING) { + EnumType enumType = (EnumType) iType; + if (!enumNames.contains(enumType.getName())) { + enumTypes.add(enumType); + enumNames.add(enumType.getName()); + } + } + } + for (SealedChoiceSchema choiceSchema : codeModel.getSchemas().getSealedChoices()) { + IType iType = Mappers.getSealedChoiceMapper().map(choiceSchema); + if (iType != ClassType.STRING) { + EnumType enumType = (EnumType) iType; + if (!enumNames.contains(enumType.getName())) { + enumTypes.add(enumType); + enumNames.add(enumType.getName()); + } + } + } + builder.enums(enumTypes); + + // exception + List exceptions = codeModel.getOperationGroups().stream() + .flatMap(og -> og.getOperations().stream()) + .flatMap(o -> o.getExceptions().stream()) + .map(Response::getSchema) + .distinct() + .filter(s -> s instanceof ObjectSchema) + .map(s -> Mappers.getExceptionMapper().map((ObjectSchema) s)) + .filter(Objects::nonNull) + .distinct() + .collect(Collectors.toList()); + builder.exceptions(exceptions); + + builder.xmlSequenceWrappers(parseXmlSequenceWrappers(codeModel, settings)); + + // class model + Stream autoRestModelTypes = Stream.concat( + codeModel.getSchemas().getObjects().stream(), + codeModel.getOperationGroups().stream().flatMap(og -> og.getOperations().stream()) + .map(o -> parseHeader(o, settings)).filter(Objects::nonNull)); + + List clientModelsFromCodeModel = autoRestModelTypes + .distinct() + .map(autoRestCompositeType -> Mappers.getModelMapper().map(autoRestCompositeType)) + .filter(Objects::nonNull) + .distinct() + .collect(Collectors.toList()); + // append some models not from CodeModel (currently, only for ##FileDetails models for multipart/form-data request) + // TODO (weidxu): we can remove this code block, if ##FileDetails moves to azure-core + final List clientModels = Stream.concat(clientModelsFromCodeModel.stream(), ClientModels.getInstance().getModels().stream()) + .distinct() + .collect(Collectors.toList()); + builder.models(clientModels); + + // union model (class) + builder.unionModels(codeModel.getSchemas().getOrs().stream().distinct() + .flatMap(schema -> Mappers.getUnionModelMapper().map(schema).stream()) + .filter(Objects::nonNull) + .distinct() + .collect(Collectors.toList())); + + // response model (subclass of Response with headers) + final List responseModels = codeModel.getOperationGroups().stream() + .flatMap(og -> og.getOperations().stream()) + .distinct() + .map(m -> parseResponse(m, clientModels, settings)) + .filter(Objects::nonNull) + .distinct() + .collect(Collectors.toList()); + builder.responseModels(responseModels); + + String serviceClientName = codeModel.getLanguage().getJava().getName(); + String serviceClientDescription = codeModel.getInfo().getDescription(); + builder.clientName(serviceClientName).clientDescription(serviceClientDescription); + + Map serviceClientsMap = new LinkedHashMap<>(); + + boolean multipleClientsWithOperationsPresent = codeModel.getClients() + .stream() + .flatMap(client -> client.getOperationGroups().stream()) + .flatMap(og -> og.getOperations().stream()) + .findAny() + .isPresent(); + + boolean singleClientOperationsPresent = codeModel.getOperationGroups() + .stream() + .flatMap(og -> og.getOperations().stream()) + .findAny() + .isPresent(); + + if (multipleClientsWithOperationsPresent || singleClientOperationsPresent) { + // set the service clients only if there are client operations present + if (!CoreUtils.isNullOrEmpty(codeModel.getClients())) { + serviceClientsMap = processClients(codeModel.getClients(), codeModel); + builder.serviceClients(new ArrayList(serviceClientsMap.keySet())); + } else { + // service client + ServiceClient serviceClient = Mappers.getServiceClientMapper().map(codeModel); + builder.serviceClient(serviceClient); + + serviceClientsMap.put(serviceClient, codeModel); + } + } + + // package info + // client + Map packageInfos = new HashMap<>(); + if (settings.isGenerateClientInterfaces() || !settings.isGenerateClientAsImpl() + || settings.getImplementationSubpackage() == null || settings.getImplementationSubpackage().isEmpty() + || settings.isFluent() || settings.isGenerateSyncAsyncClients() || settings.isDataPlaneClient()) { + packageInfos.put(settings.getPackage(), new PackageInfo( + settings.getPackage(), + String.format("Package containing the classes for %s.\n%s", serviceClientName, + serviceClientDescription))); + } + if (settings.isFluent()) { + if (settings.isFluentLite() && !CoreUtils.isNullOrEmpty(settings.getImplementationSubpackage())) { + String implementationPackage = settings.getPackage(settings.getImplementationSubpackage()); + if (!packageInfos.containsKey(implementationPackage)) { + packageInfos.put(implementationPackage, new PackageInfo( + implementationPackage, + String.format("Package containing the implementations for %s.\n%s", + serviceClientName, serviceClientDescription))); + } + } + if (!CoreUtils.isNullOrEmpty(settings.getFluentSubpackage())) { + String fluentPackage = settings.getPackage(settings.getFluentSubpackage()); + if (!packageInfos.containsKey(fluentPackage)) { + packageInfos.put(fluentPackage, new PackageInfo( + fluentPackage, + String.format("Package containing the service clients for %s.\n%s", + serviceClientName, serviceClientDescription))); + } + String fluentInnerPackage = settings.getPackage(settings.getFluentModelsSubpackage()); + if (!packageInfos.containsKey(fluentInnerPackage)) { + packageInfos.put(fluentInnerPackage, new PackageInfo( + fluentInnerPackage, + String.format("Package containing the inner data models for %s.\n%s", + serviceClientName, serviceClientDescription))); + } + } + } else { + if (settings.isGenerateClientAsImpl() && settings.getImplementationSubpackage() != null + && !settings.getImplementationSubpackage().isEmpty()) { + + String implementationPackage = settings.getPackage(settings.getImplementationSubpackage()); + if (!packageInfos.containsKey(implementationPackage)) { + packageInfos.put(implementationPackage, new PackageInfo( + implementationPackage, + String.format("Package containing the implementations for %s.\n%s", + serviceClientName, serviceClientDescription))); + } + } + } + // client in different packages + for (ServiceClient client : serviceClientsMap.keySet()) { + if (client.getBuilderPackageName() != null && !packageInfos.containsKey(client.getBuilderPackageName())) { + packageInfos.put(client.getBuilderPackageName(), new PackageInfo( + client.getBuilderPackageName(), + String.format("Package containing the classes for %s.\n%s", client.getInterfaceName(), + serviceClientDescription))); + } + } + // model + final List modelsPackages = getModelsPackages(clientModels, enumTypes, responseModels); + for (String modelsPackage : modelsPackages) { + if (!packageInfos.containsKey(modelsPackage)) { + packageInfos.put(modelsPackage, new PackageInfo( + modelsPackage, + String.format("Package containing the data models for %s.\n%s", serviceClientName, + serviceClientDescription))); + } + } + if (settings.getCustomTypes() != null && !settings.getCustomTypes().isEmpty() + && settings.getCustomTypesSubpackage() != null && !settings.getCustomTypesSubpackage().isEmpty()) { + String customTypesPackage = settings.getPackage(settings.getCustomTypesSubpackage()); + if (!packageInfos.containsKey(customTypesPackage)) { + packageInfos.put(customTypesPackage, new PackageInfo( + customTypesPackage, + String.format("Package containing the data models for %s.\n%s", serviceClientName, + serviceClientDescription))); + } + } + builder.packageInfos(new ArrayList<>(packageInfos.values())); + + // module info + builder.moduleInfo(getModuleInfo(modelsPackages, serviceClientsMap.keySet())); + + // async/sync service client (wrapper for the ServiceClient) + List syncClients = new ArrayList<>(); + List asyncClients = new ArrayList<>(); + List clientBuilders = new ArrayList<>(); + for (Map.Entry entry : serviceClientsMap.entrySet()) { + List syncClientsLocal = new ArrayList<>(); + List asyncClientsLocal = new ArrayList<>(); + + ServiceClient serviceClient = entry.getKey(); + com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Client client = entry.getValue(); + if (settings.isGenerateSyncAsyncClients()) { + ClientModelUtil.getAsyncSyncClients(client, serviceClient, asyncClientsLocal, syncClientsLocal); + } + builder.syncClients(syncClients); + builder.asyncClients(asyncClients); + + // service client builder + if (!serviceClient.isBuilderDisabled()) { + String builderSuffix = ClientModelUtil.getBuilderSuffix(); + String builderName = serviceClient.getInterfaceName() + builderSuffix; + String builderPackage = ClientModelUtil.getServiceClientBuilderPackageName(serviceClient); + if (settings.isGenerateSyncAsyncClients() && settings.isGenerateBuilderPerClient()) { + // service client builder per service client + for (int i = 0; i < asyncClientsLocal.size(); ++i) { + AsyncSyncClient asyncClient = asyncClientsLocal.get(i); + AsyncSyncClient syncClient = (i >= syncClientsLocal.size()) ? null : syncClientsLocal.get(i); + String clientName = ((syncClient != null) + ? syncClient.getClassName() + : asyncClient.getClassName().replace("AsyncClient", "Client")); + String clientBuilderName = clientName + builderSuffix; + ClientBuilder clientBuilder = new ClientBuilder( + builderPackage, clientBuilderName, serviceClient, + (syncClient == null) ? Collections.emptyList() : Collections.singletonList(syncClient), + Collections.singletonList(asyncClient), serviceClient.getCrossLanguageDefinitionId()); + + addBuilderTraits(clientBuilder, serviceClient); + clientBuilders.add(clientBuilder); + + // there is a cross-reference between service client and service client builder + asyncClient.setClientBuilder(clientBuilder); + if (syncClient != null) { + syncClient.setClientBuilder(clientBuilder); + } + } + } else { + // service client builder + ClientBuilder clientBuilder = new ClientBuilder(builderPackage, builderName, + serviceClient, syncClientsLocal, asyncClientsLocal, serviceClient.getCrossLanguageDefinitionId()); + addBuilderTraits(clientBuilder, serviceClient); + clientBuilders.add(clientBuilder); + + // there is a cross-reference between service client and service client builder + asyncClientsLocal.forEach(c -> c.setClientBuilder(clientBuilder)); + syncClientsLocal.forEach(c -> c.setClientBuilder(clientBuilder)); + } + } + + syncClients.addAll(syncClientsLocal); + asyncClients.addAll(asyncClientsLocal); + } + builder.clientBuilders(clientBuilders); + builder.crossLanguageDefinitionId(codeModel.getLanguage().getJava().getName()); + + // example/test + if (settings.isDataPlaneClient() && (settings.isGenerateSamples() || settings.isGenerateTests())) { + addProtocolExamples(builder, syncClients); + addConvenienceExamples(builder, syncClients); + } + + if (settings.isGenerateTests() && codeModel.getTestModel() != null) { + builder.liveTests(LiveTestsMapper.getInstance().map(codeModel.getTestModel())); + } + + builder.graalVmConfig(Mappers.getGraalVmConfigMapper() + .map(new GraalVmConfigMapper.ServiceAndModel( + serviceClientsMap.keySet(), + exceptions, + clientModels, + enumTypes))); + + return builder.build(); + } + + private void addConvenienceExamples(Client.Builder builder, List syncClients) { + // convenience examples + List convenienceExamples = new ArrayList<>(); + Set convenienceExampleNameSet = new HashSet<>(); + + BiConsumer handleConvenienceExample = (c, convenienceMethod) -> { + ClientBuilder clientBuilder = c.getClientBuilder(); + if (clientBuilder != null && convenienceMethod.getProtocolMethod().getProxyMethod().getExamples() != null) { + // only generate sample for convenience methods with max overload parameters + convenienceMethod.getConvenienceMethods().stream() + .filter(clientMethod -> clientMethod.getMethodVisibility() == JavaVisibility.Public && clientMethod.getMethodVisibilityInWrapperClient() == JavaVisibility.Public) + .filter(clientMethod -> Templates.getClientMethodSampleTemplate() + .isExampleIncluded(clientMethod, convenienceMethod)) + .max((clientMethod1, clientMethod2) -> { + int m1ParameterCount = clientMethod1.getMethodInputParameters().size(); + int m2ParameterCount = clientMethod2.getMethodInputParameters().size(); + return m1ParameterCount - m2ParameterCount; + }) + .ifPresent(clientMethod -> + clientMethod.getProxyMethod().getExamples().forEach((name, example) -> { + String filename = CodeNamer.toPascalCase(CodeNamer.removeInvalidCharacters(name)); + if (!convenienceExampleNameSet.contains(filename)) { + ClientMethodExample convenienceExample = + new ClientMethodExample(clientMethod, c, clientBuilder, filename, example); + convenienceExamples.add(convenienceExample); + convenienceExampleNameSet.add(filename); + } + })); + } + }; + + // convenience examples + syncClients.stream().filter(c -> !CoreUtils.isNullOrEmpty(c.getConvenienceMethods())) + .forEach(c -> c.getConvenienceMethods() + .forEach(m -> handleConvenienceExample.accept(c, m))); + builder.clientMethodExamples(convenienceExamples); + } + + private void addProtocolExamples(Client.Builder builder, List syncClients) { + List protocolExamples = new ArrayList<>(); + Set protocolExampleNameSet = new HashSet<>(); + + BiConsumer handleExample = (c, m) -> { + if (m.getMethodVisibility() == JavaVisibility.Public + && m.getMethodVisibilityInWrapperClient() == JavaVisibility.Public + && !m.isImplementationOnly() && + (m.getType() == ClientMethodType.SimpleSyncRestResponse + || m.getType() == ClientMethodType.PagingSync + || m.getType() == ClientMethodType.LongRunningBeginSync)) { + ClientBuilder clientBuilder = c.getClientBuilder(); + if (clientBuilder != null && m.getProxyMethod().getExamples() != null) { + m.getProxyMethod().getExamples().forEach((name, example) -> { + String filename = CodeNamer.toPascalCase(CodeNamer.removeInvalidCharacters(name)); + if (!protocolExampleNameSet.contains(filename)) { + ProtocolExample protocolExample = new ProtocolExample(m, c, clientBuilder, filename, example); + protocolExamples.add(protocolExample); + protocolExampleNameSet.add(filename); + } + }); + } + } + }; + + // protocol examples, exclude those that have convenience methods + syncClients.stream().filter(c -> c.getServiceClient() != null) + .forEach(c -> { + Set convenienceProxyMethodNames = new HashSet<>(); + if (c.getConvenienceMethods() != null) { + convenienceProxyMethodNames.addAll(c.getConvenienceMethods().stream() + .map(convenienceMethod -> convenienceMethod + .getProtocolMethod().getProxyMethod().getBaseName()) + .collect(Collectors.toSet())); + } + c.getServiceClient().getClientMethods() + .stream() + .filter(m -> !convenienceProxyMethodNames.contains(m.getProxyMethod().getBaseName())) + .forEach(m -> handleExample.accept(c, m)); + }); + syncClients.stream().filter(c -> c.getMethodGroupClient() != null) + .forEach(c -> { + Set convenienceProxyMethodNames = new HashSet<>(); + if (c.getConvenienceMethods() != null) { + convenienceProxyMethodNames.addAll(c.getConvenienceMethods().stream() + .map(convenienceMethod -> convenienceMethod + .getProtocolMethod().getProxyMethod().getBaseName()) + .collect(Collectors.toSet())); + } + c.getMethodGroupClient().getClientMethods() + .stream() + .filter(m -> !convenienceProxyMethodNames.contains(m.getProxyMethod().getBaseName())) + .forEach(m -> handleExample.accept(c, m)); + }); + builder.protocolExamples(protocolExamples); + } + + /** + * Extension for processing multi-client. Supported in Cadl. + * + * @param clients List of clients. + * @return List of service clients. + */ + protected Map processClients(List clients, CodeModel codeModel) { + return Collections.emptyMap(); + } + + private void addBuilderTraits(ClientBuilder clientBuilder, ServiceClient serviceClient) { + clientBuilder.addBuilderTrait(ClientBuilderTrait.HTTP_TRAIT); + if (!JavaSettings.getInstance().isBranded()) { + clientBuilder.addBuilderTrait(ClientBuilderTrait.PROXY_TRAIT); + } + + clientBuilder.addBuilderTrait(ClientBuilderTrait.CONFIGURATION_TRAIT); + if (serviceClient.getSecurityInfo().getSecurityTypes().contains(Scheme.SecuritySchemeType.OAUTH2)) { + clientBuilder.addBuilderTrait(ClientBuilderTrait.TOKEN_CREDENTIAL_TRAIT); + } + if (serviceClient.getSecurityInfo().getSecurityTypes().contains(Scheme.SecuritySchemeType.KEY)) { + if (!JavaSettings.getInstance().isBranded() || JavaSettings.getInstance().isUseKeyCredential()) { + clientBuilder.addBuilderTrait(ClientBuilderTrait.KEY_CREDENTIAL_TRAIT); + } else { + clientBuilder.addBuilderTrait(ClientBuilderTrait.AZURE_KEY_CREDENTIAL_TRAIT); + } + } + serviceClient.getProperties().stream() + .map(property -> { + Javagen.getPluginInstance().getLogger().info("Client property name " + property.getName()); + return property; + }) + .filter(property -> property.getName().equals("endpoint")) + .findFirst() + .ifPresent(property -> clientBuilder.addBuilderTrait(ClientBuilderTrait.getEndpointTrait(property))); + } + + private List parseXmlSequenceWrappers(CodeModel codeModel, JavaSettings settings) { + Map xmlSequenceWrappers = new LinkedHashMap<>(); + for (OperationGroup operationGroup : codeModel.getOperationGroups()) { + for (Operation operation : operationGroup.getOperations()) { + Schema responseBodySchema = SchemaUtil.getLowestCommonParent(operation.getResponses().stream() + .map(Response::getSchema) + .filter(Objects::nonNull) + .iterator()); + + if (responseBodySchema instanceof ArraySchema) { + parseXmlSequenceWrappers((ArraySchema) responseBodySchema, xmlSequenceWrappers, settings); + } + + for (Parameter parameter : operation.getParameters()) { + if (!(parameter.getSchema() instanceof ArraySchema)) { + continue; + } + parseXmlSequenceWrappers((ArraySchema) parameter.getSchema(), xmlSequenceWrappers, settings); + } + + for (Request request : operation.getRequests()) { + for (Parameter parameter : request.getParameters()) { + if (!(parameter.getSchema() instanceof ArraySchema)) { + continue; + } + parseXmlSequenceWrappers((ArraySchema) parameter.getSchema(), xmlSequenceWrappers, settings); + } + } + } + } + + return new ArrayList<>(xmlSequenceWrappers.values()); + } + + private static void parseXmlSequenceWrappers(ArraySchema arraySchema, + Map xmlSequenceWrappers, JavaSettings settings) { + if (!SchemaUtil.treatAsXml(arraySchema)) { + return; + } + + String modelTypeName = arraySchema.getElementType().getLanguage().getJava() != null + ? arraySchema.getElementType().getLanguage().getJava().getName() + : arraySchema.getElementType().getLanguage().getDefault().getName(); + + xmlSequenceWrappers.computeIfAbsent(modelTypeName, name -> new XmlSequenceWrapper(name, arraySchema, settings)); + } + + static ObjectSchema parseHeader(Operation operation, JavaSettings settings) { + if (!SchemaUtil.responseContainsHeaderSchemas(operation, settings)) { + return null; + } + + String name = CodeNamer.getPlural(operation.getOperationGroup().getLanguage().getJava().getName()) + + CodeNamer.toPascalCase(operation.getLanguage().getJava().getName()) + "Headers"; + Map headerMap = new HashMap<>(); + Map headerExtensions = new HashMap<>(); + for (Response response : operation.getResponses()) { + if (response.getProtocol().getHttp().getHeaders() != null) { + for (Header header : response.getProtocol().getHttp().getHeaders()) { + headerExtensions.put(header.getHeader(), header.getExtensions()); + headerMap.put(header.getHeader(), header.getSchema()); + } + } + } + if (headerMap.isEmpty()) { + return null; + } + ObjectSchema headerSchema = new ObjectSchema(); + headerSchema.setLanguage(new Languages()); + headerSchema.getLanguage().setJava(new Language()); + headerSchema.getLanguage().getJava().setName(name); + headerSchema.setProperties(new ArrayList<>()); + headerSchema.setStronglyTypedHeader(true); + headerSchema.setUsage(new HashSet<>(Collections.singletonList(SchemaContext.OUTPUT))); + + // TODO (weidxu): at present we do not generate convenience API with Header model +// if (operation.getConvenienceApi() != null) { +// headerSchema.getUsage().add(SchemaContext.CONVENIENCE_API); +// } + + for (Map.Entry header : headerMap.entrySet()) { + Property property = new Property(); + property.setSerializedName(header.getKey()); + property.setLanguage(new Languages()); + property.getLanguage().setJava(new Language()); + property.getLanguage().getJava().setName(CodeNamer.getPropertyName(header.getKey())); + property.getLanguage().getJava().setDescription(header.getValue().getDescription()); + property.setSchema(header.getValue()); + property.setDescription(header.getValue().getDescription()); + if (headerExtensions.get(header.getKey()) != null) { + property.setExtensions(headerExtensions.get(header.getKey())); + if (property.getExtensions().getXmsHeaderCollectionPrefix() != null) { + property.setSerializedName(property.getExtensions().getXmsHeaderCollectionPrefix()); + DictionarySchema dictionarySchema = new DictionarySchema(); + dictionarySchema.setElementType(header.getValue()); + property.setSchema(header.getValue()); + } + } + headerSchema.getProperties().add(property); + } + return headerSchema; + } + + private ClientResponse parseResponse(Operation method, List models, JavaSettings settings) { + ClientResponse.Builder builder = new ClientResponse.Builder(); + ObjectSchema headerSchema = parseHeader(method, settings); + if (headerSchema == null || settings.isGenericResponseTypes() || settings.isDisableTypedHeadersMethods()) { + return null; + } + + ClassType classType = ClientMapper.getClientResponseClassType(method, models, settings); + return builder.name(classType.getName()) + .packageName(classType.getPackage()) + .description(String.format("Contains all response data for the %s operation.", method.getLanguage().getJava().getName())) + .headersType(Mappers.getSchemaMapper().map(headerSchema)) + .bodyType(SchemaUtil.getOperationResponseType(method, settings)) + .build(); + } + + private static ModuleInfo getModuleInfo(List modelsPackages, Collection clients) { + // WARNING: Only tested for low level clients + JavaSettings settings = JavaSettings.getInstance(); + ModuleInfo moduleInfo = new ModuleInfo(settings.getPackage()); + + List requireModules = moduleInfo.getRequireModules(); + requireModules.add(new ModuleInfo.RequireModule(ExternalPackage.CORE.getPackageName(), true)); + + // export packages that contain Client, ClientBuilder, ServiceVersion + List exportModules = moduleInfo.getExportModules(); + exportModules.add(new ModuleInfo.ExportModule(settings.getPackage())); + for (ServiceClient client : clients) { + String builderPackageName = client.getBuilderPackageName(); + if (builderPackageName != null + && exportModules.stream().noneMatch(exportModule -> exportModule.getModuleName().equals(builderPackageName))) { + exportModules.add(new ModuleInfo.ExportModule(builderPackageName)); + } + } + + final String implementationSubpackagePrefix = settings.getPackage(settings.getImplementationSubpackage()) + "."; + for (String modelsPackage : modelsPackages) { + // export if models is not in implementation + if (!modelsPackage.startsWith(implementationSubpackagePrefix)) { + exportModules.add(new ModuleInfo.ExportModule(modelsPackage)); + } + + // open models package to azure-core and jackson + List openToModules = new ArrayList<>(); + openToModules.add(ExternalPackage.CORE.getPackageName()); + if (!settings.isStreamStyleSerialization()) { + openToModules.add("com.fasterxml.jackson.databind"); + } + List openModules = moduleInfo.getOpenModules(); + openModules.add(new ModuleInfo.OpenModule(modelsPackage, openToModules)); + } + + return moduleInfo; + } + + /** + * Extension for the list of "models" package (it could contain "implementation.models" and that of + * custom-types-subpackage), that need to have "exports" or "opens" in "module-info.java", and have + * "package-info.java" + * + * @param clientModels the list of client models (ObjectSchema). + * @param enumTypes the list of enum models (ChoiceSchema and SealedChoiceSchema). + * @param responseModels the list of client response models (for responses that contains headers). + * @return whether SDK contains "models" package, + */ + protected List getModelsPackages(List clientModels, List enumTypes, List responseModels) { + + List ret = Collections.emptyList(); + + JavaSettings settings = JavaSettings.getInstance(); + boolean hasModels = !settings.isDataPlaneClient() // not DPG + // defined models package (it is defined by default) + && (settings.getModelsSubpackage() != null && !settings.getModelsSubpackage().isEmpty()) + // models package is not same as implementation package + && !settings.getModelsSubpackage().equals(settings.getImplementationSubpackage()); + + if (hasModels) { + Set packages = clientModels.stream() + .map(ClientModel::getPackage) + .collect(Collectors.toSet()); + + packages.addAll(enumTypes.stream() + .map(EnumType::getPackage) + .collect(Collectors.toSet())); + packages.addAll(responseModels.stream() + .map(ClientResponse::getPackage) + .collect(Collectors.toSet())); + + ret = new ArrayList<>(packages); + } + + return ret; + } + + static ClassType getClientResponseClassType(Operation method, List models, JavaSettings settings) { + String name = CodeNamer.getPlural(method.getOperationGroup().getLanguage().getJava().getName()) + + CodeNamer.toPascalCase(method.getLanguage().getJava().getName()) + "Response"; + String packageName = settings.getPackage(settings.getModelsSubpackage()); + if (settings.isCustomType(name)) { + packageName = settings.getPackage(settings.getCustomTypesSubpackage()); + } + + // deduplicate from model name + for (ClientModel model : models) { + if (model.getName().equalsIgnoreCase(name) && model.getPackage().equals(packageName)) { + name = name + "Response"; + } + } + + return new ClassType.Builder().packageName(packageName).name(name).build(); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/mapper/ClientMethodMapper.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/mapper/ClientMethodMapper.java new file mode 100644 index 0000000000..0c1af50271 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/mapper/ClientMethodMapper.java @@ -0,0 +1,1739 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.mapper; + +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.ConstantSchema; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.ConvenienceApi; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.LongRunningMetadata; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.ObjectSchema; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Operation; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Parameter; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Request; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.RequestParameterLocation; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Response; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Schema; +import com.microsoft.typespec.http.client.generator.core.extension.model.extensionmodel.XmsPageable; +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings.SyncMethodsGeneration; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClassType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethod; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethodParameter; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethodType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientModel; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientModelProperty; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientModels; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ExternalDocumentation; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.GenericType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.IType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ImplementationDetails; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ListType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.MethodPageDetails; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.MethodParameter; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.MethodPollingDetails; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.MethodTransformationDetail; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ParameterMapping; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.PrimitiveType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ProxyMethod; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ProxyMethodParameter; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ReturnValue; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaVisibility; +import com.microsoft.typespec.http.client.generator.core.util.ClientModelUtil; +import com.microsoft.typespec.http.client.generator.core.util.CodeNamer; +import com.microsoft.typespec.http.client.generator.core.util.MethodNamer; +import com.microsoft.typespec.http.client.generator.core.util.MethodUtil; +import com.microsoft.typespec.http.client.generator.core.util.ReturnTypeDescriptionAssembler; +import com.microsoft.typespec.http.client.generator.core.util.SchemaUtil; +import com.azure.core.http.HttpMethod; +import com.azure.core.util.CoreUtils; + +import java.util.ArrayList; +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.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * A mapper that maps an {@link Operation} to a lit of {@link ClientMethod ClientMethods}. + */ +public class ClientMethodMapper implements IMapper> { + private static final ClientMethodMapper INSTANCE = new ClientMethodMapper(); + + private static final Pattern ANYTHING_THEN_PERIOD = Pattern.compile(".*\\."); + + private final Map> parsed = new ConcurrentHashMap<>(); + + private static class CacheKey { + private final Operation operation; + private final boolean isProtocolMethod; + + public CacheKey(Operation operation, boolean isProtocolMethod) { + this.operation = operation; + this.isProtocolMethod = isProtocolMethod; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + CacheKey cacheKey = (CacheKey) o; + return isProtocolMethod == cacheKey.isProtocolMethod && operation.equals(cacheKey.operation); + } + + @Override + public int hashCode() { + return Objects.hash(operation, isProtocolMethod); + } + } + + /** + * Creates a new instance of {@link ClientMethodMapper}. + */ + protected ClientMethodMapper() { + } + + /** + * Gets the global {@link ClientMethodMapper} instance. + * + * @return The global {@link ClientMethodMapper} instance. + */ + public static ClientMethodMapper getInstance() { + return INSTANCE; + } + + @Override + public List map(Operation operation) { + return map(operation, JavaSettings.getInstance().isDataPlaneClient()); + } + + /** + * Maps an {@link Operation} to a list of {@link ClientMethod ClientMethods}. + * + * @param operation The {@link Operation} being mapped. + * @param isProtocolMethod Whether the operation is a protocol method. + * @return The list of {@link ClientMethod ClientMethods}. + */ + public List map(Operation operation, boolean isProtocolMethod) { + CacheKey cacheKey = new CacheKey(operation, isProtocolMethod); + List clientMethods = parsed.get(cacheKey); + if (clientMethods != null) { + return clientMethods; + } + + clientMethods = createClientMethods(operation, isProtocolMethod); + parsed.put(cacheKey, clientMethods); + + return clientMethods; + } + + /** + * Creates the client methods for the operation. + * + * @param operation the operation. + * @param isProtocolMethod whether the client method to be simplified for resilience to API changes. + * @return the client methods created. + */ + private List createClientMethods(Operation operation, boolean isProtocolMethod) { + JavaSettings settings = JavaSettings.getInstance(); + + // With the introduction of "enable-sync-stack" data plane clients now have two distinct ways of creating + // synchronous implementation client methods. + // + // 1. Configure "enable-sync-stack" which will create synchronous proxy methods that will use a fully + // synchronous code path. + // 2. Configure "sync-methods" which will create synchronous implementation client methods that will block + // on the asynchronous proxy method. + // + // If both are support "enable-sync-stack" take precedent. This required substantial changes to the follow code + // as before asynchronous proxy methods would generate synchronous implementation client methods which + // shouldn't eagerly be done anymore as it would have resulted in erroneous synchronous implementation client + // methods. + + Map> proxyMethodsMap = Mappers.getProxyMethodMapper().map(operation); + + List methods = new ArrayList<>(); + + // If this operation is part of a group it'll need to be referenced with a more specific target. + ClientMethod.Builder builder = getClientMethodBuilder() + .clientReference((operation.getOperationGroup() == null || operation.getOperationGroup().getLanguage().getJava().getName().isEmpty()) ? "this" : "this.client") + .setCrossLanguageDefinitionId(operation.getCrossLanguageDefinitionId()); + + // merge summary and description + String summary = operation.getSummary(); + if (summary == null) { + // summary from m4 is under language + summary = operation.getLanguage().getDefault() == null ? null : operation.getLanguage().getDefault().getSummary(); + } + String description = operation.getLanguage().getJava() == null ? null : operation.getLanguage().getJava().getDescription(); + if (CoreUtils.isNullOrEmpty(summary) && CoreUtils.isNullOrEmpty(description)) { + builder.description(String.format("The %s operation.", operation.getLanguage().getJava().getName())); + } else { + builder.description(SchemaUtil.mergeSummaryWithDescription(summary, description)); + } + + // API comment + ImplementationDetails.Builder implDetailsBuilder = null; + if (operation.getLanguage().getJava() != null && !CoreUtils.isNullOrEmpty(operation.getLanguage().getJava().getComment())) { + implDetailsBuilder = new ImplementationDetails.Builder().comment(operation.getLanguage().getJava().getComment()); + builder.implementationDetails(implDetailsBuilder.build()); + } + + // map externalDocs property + if (operation.getExternalDocs() != null) { + ExternalDocumentation externalDocumentation = new ExternalDocumentation.Builder() + .description(operation.getExternalDocs().getDescription()) + .url(operation.getExternalDocs().getUrl()) + .build(); + builder.methodDocumentation(externalDocumentation); + } + + List requests = getCodeModelRequests(operation, isProtocolMethod, proxyMethodsMap); + for (Request request : requests) { + List proxyMethods = proxyMethodsMap.get(request); + for (ProxyMethod proxyMethod : proxyMethods) { + ReturnTypeHolder returnTypeHolder = getReturnTypes(operation, isProtocolMethod, settings, proxyMethod.isCustomHeaderIgnored()); + builder.proxyMethod(proxyMethod); + List parameters = new ArrayList<>(); + List requiredParameterExpressions = new ArrayList<>(); + Map validateExpressions = new HashMap<>(); + List methodTransformationDetails = new ArrayList<>(); + + List codeModelParameters = getCodeModelParameters(request, isProtocolMethod); + + final boolean isPageable = + operation.getExtensions() != null && operation.getExtensions().getXmsPageable() != null + && shouldGeneratePagingMethods(); + if (isPageable && JavaSettings.getInstance().isPageSizeEnabled()) { + // remove maxpagesize parameter from client method API, it would be in e.g. PagedIterable.iterableByPage(int) + codeModelParameters = codeModelParameters.stream() + .filter(p -> !MethodUtil.isMaxPageSizeParameter(p)) + .collect(Collectors.toList()); + } + + final boolean isJsonPatch = MethodUtil.isContentTypeInRequest(request, "application/json-patch+json"); + + final boolean proxyMethodUsesBinaryData = proxyMethod.getParameters().stream() + .anyMatch(proxyMethodParameter -> proxyMethodParameter.getClientType() == ClassType.BINARY_DATA); + final boolean proxyMethodUsesFluxByteBuffer = proxyMethod.getParameters().stream() + .anyMatch(proxyMethodParameter -> proxyMethodParameter.getClientType() == GenericType.FLUX_BYTE_BUFFER); + + Set originalParameters = new HashSet<>(); + for (Parameter parameter : codeModelParameters) { + ClientMethodParameter clientMethodParameter = Mappers.getClientParameterMapper() + .map(parameter, isProtocolMethod); + + if (isJsonPatch) { + clientMethodParameter = CustomClientParameterMapper.getInstance().map(parameter, isProtocolMethod); + } + + // If the codemodel parameter and proxy method parameter types don't match, update the client + // method param to use proxy method parameter type. + if (proxyMethodUsesBinaryData && clientMethodParameter.getClientType() == GenericType.FLUX_BYTE_BUFFER) { + clientMethodParameter = updateClientMethodParameter(clientMethodParameter); + } + + if (request.getSignatureParameters().contains(parameter)) { + parameters.add(clientMethodParameter); + } + + if (!(parameter.getSchema() instanceof ConstantSchema) && parameter.getGroupedBy() == null) { + MethodParameter methodParameter; + String expression; + if (parameter.getImplementation() != Parameter.ImplementationLocation.CLIENT) { + methodParameter = clientMethodParameter; + expression = clientMethodParameter.getName(); + } else { + ProxyMethodParameter proxyParameter = Mappers.getProxyParameterMapper().map(parameter); + methodParameter = proxyParameter; + expression = proxyParameter.getParameterReference(); + } + + // Validations + if (methodParameter.isRequired() && !(methodParameter.getClientType() instanceof PrimitiveType)) { + requiredParameterExpressions.add(expression); + } + String validation = methodParameter.getClientType().validate(expression); + if (validation != null) { + validateExpressions.put(expression, validation); + } + } + + // Transformations + if ((parameter.getOriginalParameter() != null || parameter.getGroupedBy() != null) + && !(parameter.getSchema() instanceof ConstantSchema) && !isProtocolMethod) { + + processParameterTransformations( + methodTransformationDetails, originalParameters, + parameter, clientMethodParameter, isProtocolMethod); + } + } + + // handle the case that the flattened parameter is model with all its properties read-only + // in this case, it is not original parameter from any other parameters + for (Parameter parameter : request.getParameters().stream() + .filter(p -> p.isFlattened() && p.getProtocol() != null && p.getProtocol().getHttp() != null) // flattened proxy parameter + .filter(p -> !originalParameters.contains(p)) // but not original parameter from any other parameters + .collect(Collectors.toList())) { + ClientMethodParameter outParameter = Mappers.getClientParameterMapper().map(parameter); + methodTransformationDetails.add(new MethodTransformationDetail(outParameter, new ArrayList<>())); + } + + final MethodOverloadType defaultOverloadType = hasNonRequiredParameters(parameters) ? MethodOverloadType.OVERLOAD_MAXIMUM : MethodOverloadType.OVERLOAD_MINIMUM_MAXIMUM; + final boolean generateOnlyRequiredParameters = settings.isRequiredParameterClientMethods() && defaultOverloadType == MethodOverloadType.OVERLOAD_MAXIMUM; + + JavaVisibility methodVisibilityInWrapperClient = JavaVisibility.Public; + if (operation.getInternalApi() == Boolean.TRUE + || (isProtocolMethod && operation.getGenerateProtocolApi() == Boolean.FALSE)) { + // Client method is package private in wrapper client, so that the client or developer can still invoke it. + methodVisibilityInWrapperClient = JavaVisibility.PackagePrivate; + } + + builder.parameters(parameters) + .requiredNullableParameterExpressions(requiredParameterExpressions) + .validateExpressions(validateExpressions) + .methodTransformationDetails(methodTransformationDetails) + .methodVisibilityInWrapperClient(methodVisibilityInWrapperClient) + .methodPageDetails(null); + + if (isPageable) { + String pageableItemName = getPageableItemName(operation.getExtensions().getXmsPageable(), proxyMethod.getRawResponseBodyType() != null ? proxyMethod.getRawResponseBodyType() : proxyMethod.getResponseBodyType()); + if (pageableItemName == null) { + // There is no pageable item name for this operation, skip it. + continue; + } + + // If the ProxyMethod is synchronous perform a complete generation of synchronous pageable APIs. + if (proxyMethod.isSync()) { + createSyncPageableClientMethods(operation, isProtocolMethod, settings, methods, builder, + returnTypeHolder, proxyMethod, parameters, pageableItemName, + generateOnlyRequiredParameters, defaultOverloadType); + } else { + // Otherwise, perform a complete generation of asynchronous pageable APIs. + // Then if SyncMethodsGeneration is enabled and Sync Stack is not perform synchronous pageable + // API generation based on SyncMethodsGeneration configuration. + createAsyncPageableClientMethods(operation, isProtocolMethod, settings, methods, builder, + returnTypeHolder, proxyMethod, parameters, pageableItemName, + generateOnlyRequiredParameters, defaultOverloadType); + + if (settings.isGenerateSyncMethods() && !settings.isSyncStackEnabled()) { + createSyncPageableClientMethods(operation, isProtocolMethod, settings, methods, builder, + returnTypeHolder, proxyMethod, parameters, pageableItemName, + generateOnlyRequiredParameters, defaultOverloadType); + } + } + } else if (operation.getExtensions() != null && operation.getExtensions().isXmsLongRunningOperation() + && (settings.isFluent() || settings.getPollingConfig("default") != null) + && !returnTypeHolder.syncReturnType.equals(ClassType.INPUT_STREAM)) { + // temporary skip InputStream, no idea how to do this in PollerFlux + // Skip sync ProxyMethods for polling as sync polling isn't ready yet. + if (proxyMethod.isSync()) { + continue; + } + + JavaVisibility simpleAsyncMethodVisibility = + methodVisibility(ClientMethodType.SimpleAsyncRestResponse, defaultOverloadType, false, isProtocolMethod); + JavaVisibility simpleAsyncMethodVisibilityWithContext = + methodVisibility(ClientMethodType.SimpleAsyncRestResponse, defaultOverloadType, true, isProtocolMethod); + + JavaVisibility simpleSyncMethodVisibility = + methodVisibility(ClientMethodType.SimpleSyncRestResponse, defaultOverloadType, false, + isProtocolMethod); + JavaVisibility simpleSyncMethodVisibilityWithContext = + methodVisibility(ClientMethodType.SimpleSyncRestResponse, defaultOverloadType, true, + isProtocolMethod); + // for vanilla and fluent, the SimpleAsyncRestResponse is VISIBLE, so that they can be used for possible customization on LRO + + // there is ambiguity of RestResponse from simple API and from LRO API + // e.g. SimpleAsyncRestResponse without Context in simple API should be VISIBLE + // hence override here for DPG + if (settings.isDataPlaneClient()) { + simpleAsyncMethodVisibility = NOT_GENERATE; + simpleAsyncMethodVisibilityWithContext = NOT_VISIBLE; + simpleSyncMethodVisibility = NOT_GENERATE; + simpleSyncMethodVisibilityWithContext = NOT_VISIBLE; + } + + // WithResponseAsync, with required and optional parameters + methods.add(builder + .returnValue(createSimpleAsyncRestResponseReturnValue(operation, + returnTypeHolder.asyncRestResponseReturnType, returnTypeHolder.syncReturnType)) + .name(proxyMethod.getSimpleAsyncRestResponseMethodName()) + .onlyRequiredParameters(false) + .type(ClientMethodType.SimpleAsyncRestResponse) + .groupedParameterRequired(false) + .methodVisibility(simpleAsyncMethodVisibility) + .hasWithContextOverload(simpleAsyncMethodVisibilityWithContext != NOT_GENERATE) + .build()); + + builder.methodVisibility(simpleAsyncMethodVisibilityWithContext); + addClientMethodWithContext(methods, builder, parameters, getContextParameter(isProtocolMethod)); + + if (JavaSettings.getInstance().isSyncStackEnabled() && !proxyMethodUsesFluxByteBuffer) { + // WithResponseAsync, with required and optional parameters + methods.add(builder + .returnValue(createSimpleSyncRestResponseReturnValue(operation, + returnTypeHolder.syncReturnWithResponse, returnTypeHolder.syncReturnType)) + .name(proxyMethod.getSimpleRestResponseMethodName()) + .onlyRequiredParameters(false) + .type(ClientMethodType.SimpleSyncRestResponse) + .groupedParameterRequired(false) + .methodVisibility(simpleSyncMethodVisibility) + .proxyMethod(proxyMethod.toSync()) + .build()); + + builder.methodVisibility(simpleSyncMethodVisibilityWithContext); + addClientMethodWithContext(methods, builder, parameters, getContextParameter(isProtocolMethod)); + + // reset builder + builder + .returnValue(createSimpleAsyncRestResponseReturnValue(operation, + returnTypeHolder.asyncRestResponseReturnType, returnTypeHolder.syncReturnType)) + .name(proxyMethod.getSimpleAsyncRestResponseMethodName()) + .onlyRequiredParameters(false) + .type(ClientMethodType.SimpleAsyncRestResponse) + .groupedParameterRequired(false) + .proxyMethod(proxyMethod) + .methodVisibility(simpleAsyncMethodVisibility); + } + + JavaSettings.PollingDetails pollingDetails = settings.getPollingConfig(proxyMethod.getOperationId()); + + MethodPollingDetails methodPollingDetails = null; + MethodPollingDetails dpgMethodPollingDetailsWithModel = null; // for additional LRO methods + + if (pollingDetails != null) { + // try lroMetadata + methodPollingDetails = methodPollingDetailsFromMetadata(operation, pollingDetails); + + // result from methodPollingDetails already handled JavaSettings.PollingDetails (as well as LongRunningMetadata) + if (methodPollingDetails == null) { + methodPollingDetails = new MethodPollingDetails( + pollingDetails.getStrategy(), + pollingDetails.getSyncStrategy(), + getPollingIntermediateType(pollingDetails, returnTypeHolder.syncReturnType), + getPollingFinalType(pollingDetails, returnTypeHolder.syncReturnType, MethodUtil.getHttpMethod(operation)), + pollingDetails.getPollIntervalInSeconds()); + } + } + + if (methodPollingDetails != null && isProtocolMethod + // models of LRO configured + && !(ClassType.BINARY_DATA.equals(methodPollingDetails.getIntermediateType()) + && (ClassType.BINARY_DATA.equals(methodPollingDetails.getFinalType()) || ClassType.VOID.equals(methodPollingDetails.getFinalType().asNullable())))) { + + // a new method to be added as implementation only (not exposed to client) for developer + dpgMethodPollingDetailsWithModel = methodPollingDetails; + + // keep consistency with DPG from Swagger, see getPollingFinalType + IType resultType = ClassType.BINARY_DATA; + // DELETE would not have final response as resource is deleted + if (MethodUtil.getHttpMethod(operation) == HttpMethod.DELETE) { + resultType = PrimitiveType.VOID; + } + + // DPG keep the method with BinaryData + methodPollingDetails = new MethodPollingDetails( + dpgMethodPollingDetailsWithModel.getPollingStrategy(), + dpgMethodPollingDetailsWithModel.getSyncPollingStrategy(), + ClassType.BINARY_DATA, + resultType, + dpgMethodPollingDetailsWithModel.getPollIntervalInSeconds()); + } + + MethodNamer methodNamer = resolveMethodNamer(proxyMethod, operation.getConvenienceApi(), isProtocolMethod); + + createLroMethods(operation, builder, methods, + methodNamer.getLroBeginAsyncMethodName(), + methodNamer.getLroBeginMethodName(), + parameters, returnTypeHolder.syncReturnType, methodPollingDetails, isProtocolMethod, + generateOnlyRequiredParameters, defaultOverloadType, proxyMethod); + + if (dpgMethodPollingDetailsWithModel != null) { + // additional LRO method for data-plane, with intermediate/final type, for convenience of grow-up + // it is public in implementation, but not exposed in wrapper client + + if (implDetailsBuilder == null) { + implDetailsBuilder = new ImplementationDetails.Builder(); + } + implDetailsBuilder.implementationOnly(true); + + builder = builder.implementationDetails(implDetailsBuilder.build()); + + createLroMethods(operation, builder, methods, + methodNamer.getLroModelBeginAsyncMethodName(), + methodNamer.getLroModelBeginMethodName(), + parameters, returnTypeHolder.syncReturnType, dpgMethodPollingDetailsWithModel, isProtocolMethod, + generateOnlyRequiredParameters, defaultOverloadType, proxyMethod); + + builder = builder.implementationDetails(implDetailsBuilder.implementationOnly(false).build()); + } + + this.createAdditionalLroMethods(operation, builder, methods, isProtocolMethod, + returnTypeHolder.asyncReturnType, returnTypeHolder.syncReturnType, proxyMethod, parameters, + generateOnlyRequiredParameters, defaultOverloadType); + } else { + if (proxyMethod.isSync()) { + // If the ProxyMethod is synchronous perform a complete generation of synchronous simple APIs. + + createSimpleSyncClientMethods(operation, isProtocolMethod, settings, methods, builder, + returnTypeHolder, proxyMethod, parameters, generateOnlyRequiredParameters, defaultOverloadType); + } else { + // Otherwise, perform a complete generation of asynchronous simple APIs. + // Then if SyncMethodsGeneration is enabled and Sync Stack is not perform synchronous simple + // API generation based on SyncMethodsGeneration configuration. + + if (settings.getSyncMethods() != SyncMethodsGeneration.SYNC_ONLY) { + // SyncMethodsGeneration.NONE would still generate these + createSimpleAsyncClientMethods(operation, isProtocolMethod, settings, methods, builder, + returnTypeHolder, proxyMethod, parameters, generateOnlyRequiredParameters, defaultOverloadType); + } + + if (settings.isGenerateSyncMethods() && !settings.isSyncStackEnabled()) { + createSimpleSyncClientMethods(operation, isProtocolMethod, settings, methods, builder, + returnTypeHolder, proxyMethod, parameters, generateOnlyRequiredParameters, defaultOverloadType); + } + } + } + } + } + + return methods.stream() + .filter(m -> m.getMethodVisibility() != NOT_GENERATE) + .distinct() + .collect(Collectors.toList()); + } + + private void processParameterTransformations( + List methodTransformationDetails, + Set originalParameters, + Parameter parameter, ClientMethodParameter clientMethodParameter, + boolean isProtocolMethod) { + + ClientMethodParameter outParameter; + if (parameter.getOriginalParameter() != null) { + originalParameters.add(parameter.getOriginalParameter()); + outParameter = Mappers.getClientParameterMapper().map(parameter.getOriginalParameter()); + } else { + outParameter = clientMethodParameter; + } + MethodTransformationDetail detail = methodTransformationDetails.stream() + .filter(d -> outParameter.getName().equals(d.getOutParameter().getName())) + .findFirst().orElse(null); + if (detail == null) { + detail = new MethodTransformationDetail(outParameter, new ArrayList<>()); + methodTransformationDetails.add(detail); + } + ParameterMapping mapping = new ParameterMapping(); + if (parameter.getGroupedBy() != null) { + mapping.setInputParameter(Mappers.getClientParameterMapper().map(parameter.getGroupedBy(), isProtocolMethod)); + ClientModel groupModel = Mappers.getModelMapper().map((ObjectSchema) parameter.getGroupedBy().getSchema()); + ClientModelProperty inputProperty = groupModel.getProperties().stream() + .filter(p -> parameter.getLanguage().getJava().getName().equals(p.getName())) + .findFirst().get(); + mapping.setInputParameterProperty(inputProperty); + } else { + mapping.setInputParameter(clientMethodParameter); + } + if (parameter.getOriginalParameter() != null) { + mapping.setOutputParameterProperty(Mappers.getModelPropertyMapper().map(parameter.getTargetProperty())); + mapping.setOutputParameterPropertyName(parameter.getTargetProperty().getLanguage().getJava().getName()); + } + detail.getParameterMappings().add(mapping); + } + + private ReturnTypeHolder getReturnTypes(Operation operation, boolean isProtocolMethod, JavaSettings settings, + boolean isCustomHeaderIgnored) { + ReturnTypeHolder returnTypeHolder = new ReturnTypeHolder(); + + if (operation.getExtensions() != null && operation.getExtensions().getXmsPageable() != null) { + // Mono> + Schema responseBodySchema = SchemaUtil.getLowestCommonParent(operation.getResponses().stream() + .map(Response::getSchema).filter(Objects::nonNull).iterator()); + if (!(responseBodySchema instanceof ObjectSchema)) { + throw new IllegalArgumentException(String.format("[JavaCheck/SchemaError] no common parent found for client models %s", + operation.getResponses().stream().map(Response::getSchema).filter(Objects::nonNull) + .map(SchemaUtil::getJavaName).collect(Collectors.toList()))); + } + ClientModel responseBodyModel = Mappers.getModelMapper().map((ObjectSchema) responseBodySchema); + Stream.concat(responseBodyModel.getProperties().stream(), ClientModelUtil.getParentProperties(responseBodyModel).stream()) + .filter(p -> p.getSerializedName().equals(operation.getExtensions().getXmsPageable().getItemName())) + .findFirst() + .ifPresentOrElse(itemProperty -> { + IType listType = itemProperty.getWireType(); + IType elementType = ((ListType) listType).getElementType(); + if (isProtocolMethod) { + returnTypeHolder.asyncRestResponseReturnType = createProtocolPagedRestResponseReturnType(); + returnTypeHolder.asyncReturnType = createProtocolPagedAsyncReturnType(); + returnTypeHolder.syncReturnType = createProtocolPagedSyncReturnType(); + returnTypeHolder.syncReturnWithResponse = createProtocolPagedRestResponseReturnTypeSync(); + } else { + returnTypeHolder.asyncRestResponseReturnType = createPagedRestResponseReturnType(elementType); + returnTypeHolder.asyncReturnType = createPagedAsyncReturnType(elementType); + returnTypeHolder.syncReturnType = createPagedSyncReturnType(elementType); + returnTypeHolder.syncReturnWithResponse = createPagedRestResponseReturnTypeSync(elementType); + } + }, () -> { + throw new IllegalArgumentException(String.format("[JavaCheck/SchemaError] item name %s not found among properties of client model %s", + operation.getExtensions().getXmsPageable().getItemName(), responseBodyModel.getName())); + }); + + return returnTypeHolder; + } + + IType responseBodyType = MapperUtils.handleResponseSchema(operation, settings); + if (isProtocolMethod && JavaSettings.getInstance().isBranded()) { + responseBodyType = SchemaUtil.removeModelFromResponse(responseBodyType, operation); + } + + returnTypeHolder.asyncRestResponseReturnType = Mappers.getProxyMethodMapper() + .getAsyncRestResponseReturnType(operation, responseBodyType, isProtocolMethod, settings, isCustomHeaderIgnored) + .getClientType(); + + IType restAPIMethodReturnBodyClientType = responseBodyType.getClientType(); + if (responseBodyType.equals(ClassType.INPUT_STREAM)) { + returnTypeHolder.asyncReturnType = createAsyncBinaryReturnType(); + returnTypeHolder.syncReturnType = responseBodyType.getClientType(); + } else { + if (restAPIMethodReturnBodyClientType != PrimitiveType.VOID) { + returnTypeHolder.asyncReturnType = createAsyncBodyReturnType(restAPIMethodReturnBodyClientType); + } else { + returnTypeHolder.asyncReturnType = createAsyncVoidReturnType(); + } + returnTypeHolder.syncReturnType = responseBodyType.getClientType(); + if (responseBodyType == GenericType.FLUX_BYTE_BUFFER && !settings.isFluent()) { + returnTypeHolder.syncReturnType = ClassType.BINARY_DATA; + } + } + + returnTypeHolder.syncReturnWithResponse = createSyncReturnWithResponseType(returnTypeHolder.syncReturnType, + operation, isProtocolMethod, settings, isCustomHeaderIgnored); + + return returnTypeHolder; + } + + private static List getCodeModelRequests(Operation operation, boolean isProtocolMethod, + Map> proxyMethodsMap) { + if (!isProtocolMethod && operation.getConvenienceApi() != null && operation.getConvenienceApi().getRequests() != null) { + // convenience API of a protocol API + List requests = operation.getConvenienceApi().getRequests(); + for (Request request : requests) { + // at present, just set the proxy methods + proxyMethodsMap.put(request, proxyMethodsMap.values().iterator().next()); + } + return requests; + } else { + return operation.getRequests(); + } + } + + private static List getCodeModelParameters(Request request, boolean isProtocolMethod) { + if (isProtocolMethod) { + // Required path, body, header and query parameters are allowed + return request.getParameters().stream().filter(p -> { + RequestParameterLocation location = p.getProtocol().getHttp().getIn(); + + return p.isRequired() && (location == RequestParameterLocation.PATH + || location == RequestParameterLocation.BODY + || location == RequestParameterLocation.HEADER + || location == RequestParameterLocation.QUERY); + }) + .collect(Collectors.toList()); + } else { + return request.getParameters().stream().filter(p -> !p.isFlattened()).collect(Collectors.toList()); + } + } + + private void createAsyncPageableClientMethods(Operation operation, boolean isProtocolMethod, JavaSettings settings, + List methods, ClientMethod.Builder builder, ReturnTypeHolder returnTypeHolder, ProxyMethod proxyMethod, + List parameters, String pageableItemName, + boolean generateClientMethodWithOnlyRequiredParameters, MethodOverloadType defaultOverloadType) { + + ReturnValue singlePageReturnValue = createPagingAsyncSinglePageReturnValue(operation, + returnTypeHolder.asyncRestResponseReturnType, returnTypeHolder.syncReturnType); + ReturnValue nextPageReturnValue = createPagingAsyncReturnValue(operation, returnTypeHolder.asyncReturnType, + returnTypeHolder.syncReturnType); + MethodVisibilityFunction visibilityFunction = (firstPage, overloadType, includesContext) -> + methodVisibility(firstPage ? ClientMethodType.PagingAsyncSinglePage : ClientMethodType.PagingAsync, + overloadType, includesContext, isProtocolMethod); + + createPageableClientMethods(operation, isProtocolMethod, settings, methods, builder, proxyMethod, parameters, pageableItemName, + false, singlePageReturnValue, nextPageReturnValue, visibilityFunction, getContextParameter(isProtocolMethod), + generateClientMethodWithOnlyRequiredParameters, defaultOverloadType); + } + + private void createSyncPageableClientMethods(Operation operation, boolean isProtocolMethod, JavaSettings settings, + List methods, ClientMethod.Builder builder, ReturnTypeHolder returnTypeHolder, ProxyMethod proxyMethod, + List parameters, String pageableItemName, + boolean generateClientMethodWithOnlyRequiredParameters, MethodOverloadType defaultOverloadType) { + + ReturnValue singlePageReturnValue = createPagingSyncSinglePageReturnValue(operation, + returnTypeHolder.syncReturnWithResponse, returnTypeHolder.syncReturnType); + ReturnValue nextPageReturnValue = createPagingSyncReturnValue(operation, returnTypeHolder.syncReturnType); + MethodVisibilityFunction visibilityFunction = (firstPage, overloadType, includesContext) -> + methodVisibility(firstPage ? ClientMethodType.PagingSyncSinglePage : ClientMethodType.PagingSync, + overloadType, includesContext, isProtocolMethod); + + createPageableClientMethods(operation, isProtocolMethod, settings, methods, builder, proxyMethod, parameters, pageableItemName, + true, singlePageReturnValue, nextPageReturnValue, visibilityFunction, getContextParameter(isProtocolMethod), + generateClientMethodWithOnlyRequiredParameters, defaultOverloadType); + } + + private static void createPageableClientMethods(Operation operation, boolean isProtocolMethod, JavaSettings settings, + List methods, ClientMethod.Builder builder, ProxyMethod proxyMethod, List parameters, + String pageableItemName, boolean isSync, ReturnValue singlePageReturnValue, ReturnValue nextPageReturnValue, + MethodVisibilityFunction visibilityFunction, ClientMethodParameter contextParameter, + boolean generateClientMethodWithOnlyRequiredParameters, MethodOverloadType defaultOverloadType) { + + MethodNamer methodNamer = resolveMethodNamer(proxyMethod, operation.getConvenienceApi(), isProtocolMethod); + + Operation nextOperation = operation.getExtensions().getXmsPageable().getNextOperation(); + String nextLinkName = operation.getExtensions().getXmsPageable().getNextLinkName(); + String itemName = operation.getExtensions().getXmsPageable().getItemName(); + ClientMethodType nextMethodType = isSync ? ClientMethodType.PagingSyncSinglePage : ClientMethodType.PagingAsyncSinglePage; + + boolean isNextMethod = (nextOperation == operation); + + IType lroIntermediateType = null; + if (operation.getExtensions().isXmsLongRunningOperation() && !isNextMethod) { + lroIntermediateType = SchemaUtil.getOperationResponseType(operation, settings); + } + + List nextMethods = (isNextMethod || nextOperation == null) + ? null : Mappers.getClientMethodMapper().map(nextOperation); + + ClientMethod nextMethod = (nextMethods == null) ? null + : nextMethods.stream().filter(m -> m.getType() == nextMethodType).findFirst().orElse(null); + + IType nextLinkType = getPageableNextLinkType(operation.getExtensions().getXmsPageable(), + (proxyMethod.getRawResponseBodyType() != null ? proxyMethod.getRawResponseBodyType() : proxyMethod.getResponseBodyType()).toString()); + + MethodPageDetails details = new MethodPageDetails(CodeNamer.getPropertyName(nextLinkName), nextLinkType, pageableItemName, + nextMethod, lroIntermediateType, nextLinkName, itemName); + builder.methodPageDetails(details); + + String pageMethodName = isSync ? methodNamer.getPagingSinglePageMethodName() : methodNamer.getPagingAsyncSinglePageMethodName(); + ClientMethodType pageMethodType = isSync ? ClientMethodType.PagingSyncSinglePage : ClientMethodType.PagingAsyncSinglePage; + + // Only generate maximum overload of Paging###SinglePage API, and it should not be exposed to user. + + JavaVisibility methodVisibility = visibilityFunction.methodVisibility(true, defaultOverloadType, false); + builder.returnValue(singlePageReturnValue) + .onlyRequiredParameters(false) + .name(pageMethodName) + .type(pageMethodType) + .groupedParameterRequired(false) + .methodVisibility(methodVisibility); + + if (settings.isGenerateAsyncMethods()) { + methods.add(builder.build()); + } + + // Generate an overload with all parameters, optionally include context. + builder.methodVisibility(visibilityFunction.methodVisibility(true, defaultOverloadType, true)); + addClientMethodWithContext(methods, builder, parameters, pageMethodType, pageMethodName, + singlePageReturnValue, details, contextParameter); + + // If this was the next method there is no further work to be done. + if (isNextMethod) { + return; + } + + // Otherwise repeat what we just did but for next page client methods. + pageMethodName = isSync ? methodNamer.getMethodName() : methodNamer.getSimpleAsyncMethodName(); + pageMethodType = isSync ? ClientMethodType.PagingSync : ClientMethodType.PagingAsync; + + builder.returnValue(nextPageReturnValue) + .name(pageMethodName) + .type(pageMethodType) + .groupedParameterRequired(false) + .methodVisibility(visibilityFunction.methodVisibility(false, defaultOverloadType, false)); + + if (settings.isGenerateAsyncMethods()) { + methods.add(builder.build()); + + // overload for versioning + createOverloadForVersioning(isProtocolMethod, methods, builder, parameters); + } + + if (generateClientMethodWithOnlyRequiredParameters) { + methods.add(builder + .onlyRequiredParameters(true) + .methodVisibility(visibilityFunction.methodVisibility(false, MethodOverloadType.OVERLOAD_MINIMUM, false)) + .build()); + } + + MethodPageDetails detailsWithContext = details; + if (nextMethods != null) { + // Match to the nextMethod with Context + IType contextWireType = contextParameter.getWireType(); + nextMethod = nextMethods.stream() + .filter(m -> m.getType() == nextMethodType) + .filter(m -> m.getMethodParameters().stream().anyMatch(p -> contextWireType.equals(p.getClientType()))) + .findFirst() + .orElse(null); + + if (nextMethod != null) { + detailsWithContext = new MethodPageDetails(CodeNamer.getPropertyName(nextLinkName), nextLinkType, + pageableItemName, nextMethod, lroIntermediateType, nextLinkName, itemName); + } + } + + builder.methodVisibility(visibilityFunction.methodVisibility(false, defaultOverloadType, true)); + addClientMethodWithContext(methods, builder, parameters, pageMethodType, pageMethodName, + nextPageReturnValue, detailsWithContext, contextParameter); + } + + private void createSimpleAsyncClientMethods(Operation operation, boolean isProtocolMethod, JavaSettings settings, + List methods, ClientMethod.Builder builder, ReturnTypeHolder returnTypeHolder, ProxyMethod proxyMethod, + List parameters, boolean generateClientMethodWithOnlyRequiredParameters, MethodOverloadType defaultOverloadType) { + + ReturnValue responseReturnValue = createSimpleAsyncRestResponseReturnValue(operation, + returnTypeHolder.asyncRestResponseReturnType, returnTypeHolder.syncReturnType); + ReturnValue returnValue = createSimpleAsyncReturnValue(operation, returnTypeHolder.asyncReturnType, + returnTypeHolder.syncReturnType); + MethodVisibilityFunction visibilityFunction = (restResponse, overloadType, includesContext) -> + methodVisibility(restResponse ? ClientMethodType.SimpleAsyncRestResponse : ClientMethodType.SimpleAsync, + overloadType, includesContext, isProtocolMethod); + + createSimpleClientMethods(operation, isProtocolMethod, methods, builder, proxyMethod, parameters, false, responseReturnValue, + returnValue, visibilityFunction, getContextParameter(isProtocolMethod), generateClientMethodWithOnlyRequiredParameters, defaultOverloadType); + } + + private void createSimpleSyncClientMethods(Operation operation, boolean isProtocolMethod, JavaSettings settings, + List methods, ClientMethod.Builder builder, ReturnTypeHolder returnTypeHolder, ProxyMethod proxyMethod, + List parameters, boolean generateClientMethodWithOnlyRequiredParameters, MethodOverloadType defaultOverloadType) { + + ReturnValue responseReturnValue = createSimpleSyncRestResponseReturnValue(operation, + returnTypeHolder.syncReturnWithResponse, returnTypeHolder.syncReturnType); + ReturnValue returnValue = createSimpleSyncReturnValue(operation, returnTypeHolder.syncReturnType); + MethodVisibilityFunction visibilityFunction = (restResponse, overloadType, includesContext) -> + methodVisibility(restResponse ? ClientMethodType.SimpleSyncRestResponse : ClientMethodType.SimpleSync, + overloadType, includesContext, isProtocolMethod); + + createSimpleClientMethods(operation, isProtocolMethod, methods, builder, proxyMethod, parameters, true, responseReturnValue, + returnValue, visibilityFunction, getContextParameter(isProtocolMethod), generateClientMethodWithOnlyRequiredParameters, defaultOverloadType); + } + + private static void createSimpleClientMethods(Operation operation, boolean isProtocolMethod, + List methods, ClientMethod.Builder builder, + ProxyMethod proxyMethod, List parameters, boolean isSync, + ReturnValue responseReturnValue, ReturnValue returnValue, + MethodVisibilityFunction visibilityFunction, ClientMethodParameter contextParameter, + boolean generateClientMethodWithOnlyRequiredParameters, MethodOverloadType defaultOverloadType) { + + MethodNamer methodNamer = resolveMethodNamer(proxyMethod, operation.getConvenienceApi(), isProtocolMethod); + + String methodName = isSync ? methodNamer.getSimpleRestResponseMethodName() : methodNamer.getSimpleAsyncRestResponseMethodName(); + ClientMethodType methodType = isSync ? ClientMethodType.SimpleSyncRestResponse : ClientMethodType.SimpleAsyncRestResponse; + + JavaVisibility withContextVisibility = visibilityFunction.methodVisibility(true, defaultOverloadType, true); + builder.parameters(parameters) + .returnValue(responseReturnValue) + .onlyRequiredParameters(false) + .name(methodName) + .type(methodType) + .groupedParameterRequired(false) + .hasWithContextOverload(withContextVisibility != NOT_GENERATE) + .methodVisibility(visibilityFunction.methodVisibility(true, defaultOverloadType, false)); + // Always generate an overload of WithResponse with non-required parameters without Context. + // It is only for sync proxy method, and is usually filtered out in methodVisibility function. + methods.add(builder.build()); + + builder.methodVisibility(withContextVisibility); + addClientMethodWithContext(methods, builder, parameters, contextParameter); + + // Repeat the same but for simple returns. + if (proxyMethod.isCustomHeaderIgnored()) { + return; + } + methodName = isSync ? methodNamer.getMethodName() : methodNamer.getSimpleAsyncMethodName(); + methodType = isSync ? ClientMethodType.SimpleSync : ClientMethodType.SimpleAsync; + + builder.parameters(parameters) + .returnValue(returnValue) + .name(methodName) + .type(methodType) + .groupedParameterRequired(false) + .methodVisibility(visibilityFunction.methodVisibility(false, defaultOverloadType, false)); + methods.add(builder.build()); + + // overload for versioning + createOverloadForVersioning(isProtocolMethod, methods, builder, parameters); + + if (generateClientMethodWithOnlyRequiredParameters) { + methods.add(builder + .methodVisibility(visibilityFunction.methodVisibility(false, MethodOverloadType.OVERLOAD_MINIMUM, false)) + .onlyRequiredParameters(true) + .build()); + } + + builder.methodVisibility(visibilityFunction.methodVisibility(false, defaultOverloadType, true)); + addClientMethodWithContext(methods, builder, parameters, contextParameter); + } + + private static void createOverloadForVersioning( + boolean isProtocolMethod, + List methods, ClientMethod.Builder builder, + List parameters) { + + if (!isProtocolMethod && JavaSettings.getInstance().isDataPlaneClient()) { + if (parameters.stream().anyMatch(p -> p.getVersioning() != null && p.getVersioning().getAdded() != null)) { + List> signatures = findOverloadedSignatures(parameters); + for (List overloadedParameters : signatures) { + builder.parameters(overloadedParameters); + methods.add(builder.build()); + } + } + + builder.parameters(parameters); + } + } + + static List> findOverloadedSignatures(List parameters) { + List> signatures = new ArrayList<>(); + + List allParameters = parameters; + List requiredParameters = parameters.stream() + .filter(MethodParameter::isRequired) + .collect(Collectors.toList()); + + List versions = allParameters.stream() + .flatMap(p -> { + if (p.getVersioning() != null && p.getVersioning().getAdded() != null) { + return p.getVersioning().getAdded().stream(); + } else { + return Stream.empty(); + } + }).distinct().collect(Collectors.toList()); + versions.add(0, null); // for signature of no version + + for (String version : versions) { + List overloadedParameters = allParameters.stream() + .filter(p -> (p.getVersioning() == null || p.getVersioning().getAdded() == null) + || (p.getVersioning() != null && p.getVersioning().getAdded() != null && p.getVersioning().getAdded().contains(version))) + .collect(Collectors.toList()); + + if (!overloadedParameters.equals(allParameters) + && !overloadedParameters.equals(requiredParameters) + && !signatures.contains(overloadedParameters)) { + // take the signature not same as required-only, not same as full, not same as anything already there + signatures.add(overloadedParameters); + } + } + + return signatures; + } + + private static ClientMethodParameter updateClientMethodParameter(ClientMethodParameter clientMethodParameter) { + return clientMethodParameter.newBuilder() + .rawType(ClassType.BINARY_DATA) + .wireType(ClassType.BINARY_DATA) + .build(); + } + + /** + * Extension point of additional methods for LRO. + */ + protected void createAdditionalLroMethods( + Operation operation, ClientMethod.Builder builder, List methods, + boolean isProtocolMethod, IType asyncReturnType, IType syncReturnType, + ProxyMethod proxyMethod, List parameters, + boolean generateClientMethodWithOnlyRequiredParameters, MethodOverloadType defaultOverloadType) { + + } + + private void createLroMethods( + Operation operation, ClientMethod.Builder builder, List methods, + String asyncMethodName, String syncMethodName, List parameters, IType syncReturnType, + MethodPollingDetails methodPollingDetails, boolean isProtocolMethod, + boolean generateClientMethodWithOnlyRequiredParameters, MethodOverloadType defaultOverloadType, + ProxyMethod proxyMethod) { + + boolean proxyMethodUsesFluxByteBuffer = proxyMethod.getParameters().stream() + .anyMatch(proxyMethodParameter -> proxyMethodParameter.getClientType() == GenericType.FLUX_BYTE_BUFFER); + + builder.methodPollingDetails(methodPollingDetails); + if (JavaSettings.getInstance().isGenerateAsyncMethods()) { + // begin method async + methods.add(builder + .returnValue(createLongRunningBeginAsyncReturnValue(operation, syncReturnType, methodPollingDetails)) + .name(asyncMethodName) + .onlyRequiredParameters(false) + .type(ClientMethodType.LongRunningBeginAsync) + .groupedParameterRequired(false) + .methodVisibility(methodVisibility(ClientMethodType.LongRunningBeginAsync, defaultOverloadType, false, isProtocolMethod)) + .build()); + + // overload for versioning + createOverloadForVersioning(isProtocolMethod, methods, builder, parameters); + + if (generateClientMethodWithOnlyRequiredParameters) { + methods.add(builder + .onlyRequiredParameters(true) + .methodVisibility(methodVisibility(ClientMethodType.LongRunningBeginAsync, MethodOverloadType.OVERLOAD_MINIMUM, false, isProtocolMethod)) + .build()); + } + + builder.methodVisibility(methodVisibility(ClientMethodType.LongRunningBeginAsync, defaultOverloadType, true, isProtocolMethod)); + addClientMethodWithContext(methods, builder, parameters, getContextParameter(isProtocolMethod)); + } + + if (!proxyMethodUsesFluxByteBuffer && + (JavaSettings.getInstance().isGenerateSyncMethods() + || JavaSettings.getInstance().isSyncStackEnabled())) { + // begin method sync + methods.add(builder + .returnValue(createLongRunningBeginSyncReturnValue(operation, syncReturnType, methodPollingDetails)) + .name(syncMethodName) + .onlyRequiredParameters(false) + .type(ClientMethodType.LongRunningBeginSync) + .groupedParameterRequired(false) + .methodVisibility(methodVisibility(ClientMethodType.LongRunningBeginSync, defaultOverloadType, false, isProtocolMethod)) + .build()); + + // overload for versioning + createOverloadForVersioning(isProtocolMethod, methods, builder, parameters); + + if (generateClientMethodWithOnlyRequiredParameters) { + methods.add(builder + .onlyRequiredParameters(true) + .methodVisibility(methodVisibility(ClientMethodType.LongRunningBeginSync, MethodOverloadType.OVERLOAD_MINIMUM, false, isProtocolMethod)) + .build()); + } + + builder.methodVisibility(methodVisibility(ClientMethodType.LongRunningBeginSync, defaultOverloadType, true, isProtocolMethod)); + addClientMethodWithContext(methods, builder, parameters, getContextParameter(isProtocolMethod)); + } + } + + private ClientMethodParameter getContextParameter() { + return new ClientMethodParameter.Builder() + .description("The context to associate with this operation.") + .wireType(this.getContextType()) + .name("context") + .requestParameterLocation(RequestParameterLocation.NONE) + .annotations(Collections.emptyList()) + .constant(false) + .defaultValue(null) + .fromClient(false) + .finalParameter(false) + .required(false) + .build(); + } + + /** + * Gets the Context type. + * + * @return The Context type. + */ + protected IType getContextType() { + return ClassType.CONTEXT; + } + + /** + * Creates the synchronous {@code withResponse} type. + * + * @param syncReturnType The return type. + * @param operation The operation. + * @param isProtocolMethod Whether this is a protocol method. + * @param settings Autorest generation settings. + * @return The synchronous {@code withResponse} type. + */ + protected IType createSyncReturnWithResponseType(IType syncReturnType, Operation operation, + boolean isProtocolMethod, JavaSettings settings) { + return this.createSyncReturnWithResponseType(syncReturnType, operation, isProtocolMethod, settings, false); + } + + /** + * Creates the synchronous {@code withResponse} type. + * + * @param syncReturnType The return type. + * @param operation The operation. + * @param isProtocolMethod Whether this is a protocol method. + * @param settings Autorest generation settings. + * @param ignoreCustomHeaders Whether the custom header type is ignored. + * @return The synchronous {@code withResponse} type. + */ + protected IType createSyncReturnWithResponseType(IType syncReturnType, Operation operation, + boolean isProtocolMethod, JavaSettings settings, boolean ignoreCustomHeaders) { + boolean responseContainsHeaders = SchemaUtil.responseContainsHeaderSchemas(operation, settings); + + // If DPG is being generated or the response doesn't contain headers return Response + // If no named response types are being used return ResponseBase + // Else named response types are being used and return that. + if (isProtocolMethod || !responseContainsHeaders) { + return GenericType.Response(syncReturnType); + } else if (settings.isGenericResponseTypes()) { + if (ignoreCustomHeaders || settings.isDisableTypedHeadersMethods()) { + return GenericType.Response(syncReturnType); + } + return GenericType.RestResponse(Mappers.getSchemaMapper().map(ClientMapper.parseHeader(operation, settings)), + syncReturnType); + } else { + return ClientMapper.getClientResponseClassType(operation, ClientModels.getInstance().getModels(), settings); + } + } + + /** + * Creates a simple synchronous REST response {@link ReturnValue}. + * + * @param operation The operation. + * @param syncReturnWithResponse The synchronous {@code withResponse} return. + * @param syncReturnType The synchronous return type. + * @return The simple synchronous REST response {@link ReturnValue}. + */ + protected ReturnValue createSimpleSyncRestResponseReturnValue(Operation operation, IType syncReturnWithResponse, IType syncReturnType) { + return new ReturnValue(returnTypeDescription(operation, syncReturnWithResponse, syncReturnType), + syncReturnWithResponse); + } + + /** + * Creates a simple asynchronous REST response {@link ReturnValue}. + * + * @param operation The operation. + * @param asyncRestResponseReturnType The asynchronous {@code withResponse} return. + * @param syncReturnType The synchronous return type. + * @return The simple asynchronous REST response {@link ReturnValue}. + */ + protected ReturnValue createSimpleAsyncRestResponseReturnValue(Operation operation, IType asyncRestResponseReturnType, IType syncReturnType) { + return new ReturnValue(returnTypeDescription(operation, asyncRestResponseReturnType, syncReturnType), + asyncRestResponseReturnType); + } + + /** + * Creates a simple synchronous return value. + * + * @param operation The operation. + * @param syncReturnType The synchronous return value. + * @return The simple synchronous return value. + */ + protected ReturnValue createSimpleSyncReturnValue(Operation operation, IType syncReturnType) { + return new ReturnValue(returnTypeDescription(operation, syncReturnType, syncReturnType), + syncReturnType); + } + + /** + * Creates a simple asynchronous return value. + * + * @param operation The operation. + * @param asyncReturnType The asynchronous return type. + * @param syncReturnType The synchronous return type. + * @return The simple asynchronous return value. + */ + protected ReturnValue createSimpleAsyncReturnValue(Operation operation, IType asyncReturnType, IType syncReturnType) { + return new ReturnValue(returnTypeDescription(operation, asyncReturnType, syncReturnType), + asyncReturnType); + } + + /** + * Creates a synchronous long-running return value. + * + * @param operation The operation. + * @param syncReturnType The synchronous return type. + * @return The synchronous long-running return value. + */ + protected ReturnValue createLongRunningSyncReturnValue(Operation operation, IType syncReturnType) { + return new ReturnValue(returnTypeDescription(operation, syncReturnType, syncReturnType), + syncReturnType); + } + + /** + * Creates an asynchronous long-running return value. + * + * @param operation The operation. + * @param asyncReturnType The asynchronous return type. + * @param syncReturnType The synchronous return type. + * @return The asynchronous long-running return value. + */ + protected ReturnValue createLongRunningAsyncReturnValue(Operation operation, IType asyncReturnType, IType syncReturnType) { + return new ReturnValue(returnTypeDescription(operation, asyncReturnType, syncReturnType), + asyncReturnType); + } + + private ReturnValue createLongRunningBeginSyncReturnValue(Operation operation, IType syncReturnType, MethodPollingDetails pollingDetails) { + if (JavaSettings.getInstance().isFluent()) { + IType returnType = GenericType.SyncPoller(GenericType.PollResult(syncReturnType.asNullable()), syncReturnType.asNullable()); + return new ReturnValue(returnTypeDescription(operation, returnType, syncReturnType), returnType); + } else { + IType returnType = GenericType.SyncPoller(pollingDetails.getIntermediateType(), pollingDetails.getFinalType()); + return new ReturnValue(returnTypeDescription(operation, returnType, pollingDetails.getFinalType()), returnType); + } + } + + /** + * Creates an asynchronous long-running begin return value. + * + * @param operation The operation. + * @param syncReturnType The synchronous return type. + * @param pollingDetails The polling details. + * @return The asynchronous long-running begin return value. + */ + protected ReturnValue createLongRunningBeginAsyncReturnValue(Operation operation, IType syncReturnType, MethodPollingDetails pollingDetails) { + if (JavaSettings.getInstance().isFluent()) { + IType returnType = GenericType.PollerFlux(GenericType.PollResult(syncReturnType.asNullable()), syncReturnType.asNullable()); + return new ReturnValue(returnTypeDescription(operation, returnType, syncReturnType), returnType); + } else { + IType returnType = GenericType.PollerFlux(pollingDetails.getIntermediateType(), pollingDetails.getFinalType()); + return new ReturnValue(returnTypeDescription(operation, returnType, pollingDetails.getFinalType()), returnType); + } + } + + /** + * Creates a synchronous paging return value. + * + * @param operation The operation. + * @param syncReturnType The synchronous return type. + * @return The synchronous paging return value. + */ + protected ReturnValue createPagingSyncReturnValue(Operation operation, IType syncReturnType) { + return new ReturnValue(returnTypeDescription(operation, syncReturnType, syncReturnType), + syncReturnType); + } + + /** + * Creates an asynchronous paging return value. + * + * @param operation The operation. + * @param asyncReturnType The asynchronous return type. + * @param syncReturnType The synchronous return type. + * @return The asynchronous paging return value. + */ + protected ReturnValue createPagingAsyncReturnValue(Operation operation, IType asyncReturnType, IType syncReturnType) { + return new ReturnValue(returnTypeDescription(operation, asyncReturnType, syncReturnType), + asyncReturnType); + } + + /** + * Creates an asynchronous single page paging return value. + * + * @param operation The operation. + * @param asyncRestResponseReturnType The asynchronous REST response return type. + * @param syncReturnType The synchronous return type. + * @return The asynchronous single page paging return value. + */ + protected ReturnValue createPagingAsyncSinglePageReturnValue(Operation operation, IType asyncRestResponseReturnType, IType syncReturnType) { + return new ReturnValue(returnTypeDescription(operation, asyncRestResponseReturnType, syncReturnType), + asyncRestResponseReturnType); + } + + /** + * Creates a synchronous single page paging return value. + * + * @param operation The operation. + * @param syncRestResponseReturnType The synchronous REST response return type. + * @param syncReturnType The synchronous return type. + * @return The synchronous single page paging return value. + */ + protected ReturnValue createPagingSyncSinglePageReturnValue(Operation operation, + IType syncRestResponseReturnType, IType syncReturnType) { + return new ReturnValue(returnTypeDescription(operation, syncRestResponseReturnType, syncReturnType), + syncRestResponseReturnType); + } + + /** + * Whether paging methods should be generated. + * + * @return Whether paging methods should be generated. + */ + protected boolean shouldGeneratePagingMethods() { + return true; + } + + /** + * Creates an asynchronous void return type. + * + * @return The asynchronous void return type. + */ + protected IType createAsyncVoidReturnType() { + return GenericType.Mono(ClassType.VOID); + } + + /** + * Creates an asynchronous body return type. + * + * @param restAPIMethodReturnBodyClientType The type of the body. + * @return The asynchronous body return type. + */ + protected IType createAsyncBodyReturnType(IType restAPIMethodReturnBodyClientType) { + return GenericType.Mono(restAPIMethodReturnBodyClientType); + } + + /** + * Creates an asynchronous binary return type. + * + * @return The asynchronous binary return type. + */ + protected IType createAsyncBinaryReturnType() { + return GenericType.Flux(ClassType.BYTE_BUFFER); + } + + /** + * Creates a synchronous paged return type. + * + * @param elementType The element type of the page. + * @return The synchronous paged return type. + */ + protected IType createPagedSyncReturnType(IType elementType) { + return GenericType.PagedIterable(elementType); + } + + /** + * Creates an asynchronous paged return type. + * + * @param elementType The element type of the page. + * @return The asynchronous paged return type. + */ + protected IType createPagedAsyncReturnType(IType elementType) { + return GenericType.PagedFlux(elementType); + } + + /** + * Creates an asynchronous paged REST response return type. + * + * @param elementType The element type of the page. + * @return The asynchronous paged REST response return type. + */ + protected IType createPagedRestResponseReturnType(IType elementType) { + return GenericType.Mono(GenericType.PagedResponse(elementType)); + } + + /** + * Creates a synchronous paged REST response return type. + * + * @param elementType The element type of the page. + * @return The synchronous paged REST response return type. + */ + protected IType createPagedRestResponseReturnTypeSync(IType elementType) { + return GenericType.PagedResponse(elementType); + } + + /** + * Creates a synchronous paged protocol return type. + * + * @return The synchronous paged protocol return type. + */ + protected IType createProtocolPagedSyncReturnType() { + return GenericType.PagedIterable(ClassType.BINARY_DATA); + } + + /** + * Creates an asynchronous paged protocol return type. + * + * @return The asynchronous paged protocol return type. + */ + protected IType createProtocolPagedAsyncReturnType() { + return GenericType.PagedFlux(ClassType.BINARY_DATA); + } + + /** + * Creates an asynchronous paged protocol REST response return type. + * + * @return The asynchronous paged protocol REST response return type. + */ + protected IType createProtocolPagedRestResponseReturnType() { + return GenericType.Mono(GenericType.PagedResponse(ClassType.BINARY_DATA)); + } + + /** + * Creates a synchronous paged protocol REST response return type. + * + * @return The synchronous paged protocol REST response return type. + */ + protected IType createProtocolPagedRestResponseReturnTypeSync() { + return GenericType.PagedResponse(ClassType.BINARY_DATA); + } + + /** + * Gets a {@link ClientMethod.Builder}. + * + * @return A {@link ClientMethod.Builder}. + */ + protected ClientMethod.Builder getClientMethodBuilder() { + return new ClientMethod.Builder(); + } + + /** + * A {@link JavaVisibility} where the method isn't visible in public API. + */ + protected static final JavaVisibility NOT_VISIBLE = JavaVisibility.Private; + + /** + * A {@link JavaVisibility} where the method is visible in public API. + */ + protected static final JavaVisibility VISIBLE = JavaVisibility.Public; + + /** + * A {@link JavaVisibility} where the method shouldn't be generated. + */ + protected static final JavaVisibility NOT_GENERATE = null; + + /** + * Enum describing the type of method overload. + */ + protected enum MethodOverloadType { + // minimum overload, only required parameters + OVERLOAD_MINIMUM(0x01), + // maximum overload, required parameters and optional parameters + OVERLOAD_MAXIMUM(0x10), + // both a minimum overload and maximum overload, usually because of no optional parameters in API + OVERLOAD_MINIMUM_MAXIMUM(0x11); + + private final int value; + + MethodOverloadType(int value) { + this.value = value; + } + + public int value() { + return value; + } + } + + /** + * Extension for configuration on method visibility. + *

+ * ClientMethodTemplate.writeMethod (and whether it is called) would also decide the visibility in generated code. + * + * @param methodType the type of the client method. + * @param methodOverloadType type of method overload. + * @param hasContextParameter whether the method has Context parameter. + * @param isProtocolMethod whether the client method to be simplified for resilience to API changes. + * @return method visibility, null if do not generate. + */ + protected JavaVisibility methodVisibility(ClientMethodType methodType, MethodOverloadType methodOverloadType, + boolean hasContextParameter, boolean isProtocolMethod) { + + JavaSettings settings = JavaSettings.getInstance(); + if (settings.isDataPlaneClient()) { + if (isProtocolMethod) { + /* + Rule for DPG protocol method + + 1. Only generate "WithResponse" method for simple API (hence exclude SimpleAsync and SimpleSync). + 2. For sync method, Context is included in "RequestOptions", hence do not generate method with Context parameter. + 3. For async method, Context is not included in method (this rule is valid for all clients). + */ + if (methodType == ClientMethodType.SimpleAsync + || methodType == ClientMethodType.SimpleSync + || !hasContextParameter + || (methodType == ClientMethodType.PagingSyncSinglePage && !settings.isSyncStackEnabled())) { + return NOT_GENERATE; + } + + if (methodType == ClientMethodType.PagingAsyncSinglePage + || (methodType == ClientMethodType.PagingSyncSinglePage && settings.isSyncStackEnabled())) { + return NOT_VISIBLE; + } + return VISIBLE; + } else { + // at present, only generate convenience method for simple API and pageable API (no LRO) + return ((methodType == ClientMethodType.SimpleAsync && !hasContextParameter) + || (methodType == ClientMethodType.SimpleSync && !hasContextParameter) + || (methodType == ClientMethodType.PagingAsync && !hasContextParameter) + || (methodType == ClientMethodType.PagingSync && !hasContextParameter) + || (methodType == ClientMethodType.LongRunningBeginAsync && !hasContextParameter) + || (methodType == ClientMethodType.LongRunningBeginSync && !hasContextParameter)) + // || (methodType == ClientMethodType.SimpleSyncRestResponse && hasContextParameter)) + ? VISIBLE + : NOT_GENERATE; + } + } else { + if (methodType == ClientMethodType.SimpleSyncRestResponse && !hasContextParameter) { + return NOT_GENERATE; + } else if (methodType == ClientMethodType.SimpleSync && hasContextParameter) { + return NOT_GENERATE; + } + return VISIBLE; + } + } + + @FunctionalInterface + private interface MethodVisibilityFunction { + JavaVisibility methodVisibility(boolean isRestResponseOrIsFirstPage, MethodOverloadType methodOverloadType, boolean hasContextParameter); + } + + private static void addClientMethodWithContext(List methods, ClientMethod.Builder builder, + List parameters, ClientMethodType clientMethodType, String proxyMethodName, + ReturnValue returnValue, MethodPageDetails details, ClientMethodParameter contextParameter) { + + List updatedParams = new ArrayList<>(parameters); + if (JavaSettings.getInstance().isBranded() || contextParameter.getClientType().equals(ClassType.REQUEST_OPTIONS)) { + updatedParams.add(contextParameter); + } + + methods.add(builder + .parameters(updatedParams) // update builder parameters to include context + .returnValue(returnValue) + .name(proxyMethodName) + .onlyRequiredParameters(false) + .type(clientMethodType) + .groupedParameterRequired(false) + .methodPageDetails(details) + .build()); + // reset the parameters to original params + builder.parameters(parameters); + } + + /** + * Gets the Context parameter. + * + * @param isProtocolMethod Whether the method is a protocol method. + * @return The Context parameter. + */ + protected ClientMethodParameter getContextParameter(boolean isProtocolMethod) { + return isProtocolMethod + ? ClientMethodParameter.REQUEST_OPTIONS_PARAMETER + : getContextParameter(); + } + + /** + * Adds a {@link ClientMethod} that has a Context parameter included. + * + * @param methods The list of {@link ClientMethod ClientMethods} already created. + * @param builder The builder for the {@link ClientMethod}. + * @param parameters Parameters of the method. + * @param contextParameter The Context parameter. + */ + protected static void addClientMethodWithContext(List methods, ClientMethod.Builder builder, + List parameters, ClientMethodParameter contextParameter) { + List updatedParams = new ArrayList<>(parameters); + if (JavaSettings.getInstance().isBranded() || contextParameter.getClientType().equals(ClassType.REQUEST_OPTIONS)) { + updatedParams.add(contextParameter); + } + + methods.add(builder + .parameters(updatedParams) // update builder parameters to include context + .onlyRequiredParameters(false) + .hasWithContextOverload(false) // WithContext overload doesn't have a withContext overload + .build()); + // reset the parameters to original params + builder.parameters(parameters); + } + + private static String getPageableItemName(XmsPageable xmsPageable, IType responseBodyType) { + ClientModel responseBodyModel = ClientModelUtil.getClientModel(responseBodyType.toString()); + return Stream.concat(responseBodyModel.getProperties().stream(), ClientModelUtil.getParentProperties(responseBodyModel).stream()) + .filter(p -> p.getSerializedName().equals(xmsPageable.getItemName())) + .map(ClientModelProperty::getName).findAny().orElse(null); + } + + private static IType getPageableNextLinkType(XmsPageable xmsPageable, String clientModelName) { + ClientModel responseBodyModel = ClientModelUtil.getClientModel(clientModelName); + IType nextLinkType = responseBodyModel.getProperties().stream() + .filter(p -> p.getSerializedName().equals(xmsPageable.getNextLinkName())) + .map(ClientModelProperty::getClientType).findAny().orElse(null); + if (nextLinkType == null && !CoreUtils.isNullOrEmpty(responseBodyModel.getParentModelName())) { + // try find nextLink property in parent model + nextLinkType = getPageableNextLinkType(xmsPageable, responseBodyModel.getParentModelName()); + } + return nextLinkType; + } + + private IType getPollingIntermediateType(JavaSettings.PollingDetails details, IType syncReturnType) { + IType pollResponseType = syncReturnType.asNullable(); + if (JavaSettings.getInstance().isFluent()) { + return pollResponseType; + } + if (details != null && details.getIntermediateType() != null) { + pollResponseType = createTypeFromModelName(details.getIntermediateType(), JavaSettings.getInstance()); + } + // azure-core wants poll response to be non-null + if (pollResponseType.asNullable() == ClassType.VOID) { + pollResponseType = ClassType.BINARY_DATA; + } + + return pollResponseType; + } + + private IType getPollingFinalType(JavaSettings.PollingDetails details, IType syncReturnType, HttpMethod httpMethod) { + IType resultType = syncReturnType.asNullable(); + if (JavaSettings.getInstance().isFluent()) { + return resultType; + } + if (details != null && details.getFinalType() != null) { + resultType = createTypeFromModelName(details.getFinalType(), JavaSettings.getInstance()); + } + // azure-core wants poll response to be non-null + if (resultType.asNullable() == ClassType.VOID) { + resultType = ClassType.BINARY_DATA; + } + // DELETE would not have final response as resource is deleted + if (httpMethod == HttpMethod.DELETE) { + resultType = PrimitiveType.VOID; + } + + return resultType; + } + + private static boolean hasNonRequiredParameters(List parameters) { + return parameters.stream().anyMatch(p -> !p.isRequired() && !p.isConstant()); + } + + /** + * Creates the return type Javadoc description. + * + * @param operation The operation. + * @param returnType The return type. + * @param baseType The base type. + * @return The return type Javadoc description. + */ + protected static String returnTypeDescription(Operation operation, IType returnType, IType baseType) { + if (returnType == PrimitiveType.VOID) { + // void methods don't have a return value, therefore no return Javadoc. + return null; + } + String description = null; + // try the description of the operation + if (operation.getLanguage() != null && operation.getLanguage().getDefault() != null) { + String operationDescription = operation.getLanguage().getDefault().getDescription(); + if (!CoreUtils.isNullOrEmpty(operationDescription)) { + if (operationDescription.toLowerCase().startsWith("get ") || operationDescription.toLowerCase().startsWith("gets ")) { + int startIndex = operationDescription.indexOf(" ") + 1; + description = formatReturnTypeDescription(operationDescription.substring(startIndex)); + } + } + } + + // try the description on the schema of return type + if (description == null && operation.getResponses() != null && !operation.getResponses().isEmpty()) { + Schema responseSchema = operation.getResponses().get(0).getSchema(); + if (responseSchema != null && !CoreUtils.isNullOrEmpty(responseSchema.getSummary())) { + description = formatReturnTypeDescription(responseSchema.getSummary()); + } else if (responseSchema != null && responseSchema.getLanguage() != null && responseSchema.getLanguage().getDefault() != null) { + String responseSchemaDescription = responseSchema.getLanguage().getDefault().getDescription(); + if (!CoreUtils.isNullOrEmpty(responseSchemaDescription)) { + description = formatReturnTypeDescription(responseSchemaDescription); + } + } + } + + // Mono of HEAD method + if (description == null + && baseType == PrimitiveType.BOOLEAN + && HttpMethod.HEAD == MethodUtil.getHttpMethod(operation)) { + description = "whether resource exists"; + } + + description = ReturnTypeDescriptionAssembler.assemble(description, returnType, baseType); + + return description == null ? "the response" : description; + } + + private static String formatReturnTypeDescription(String description) { + description = description.trim(); + int endIndex = description.indexOf(". "); // Get 1st sentence. + if (endIndex == -1 && description.length() > 0 && description.charAt(description.length() - 1) == '.') { + // Remove last period. + endIndex = description.length() - 1; + } + if (endIndex != -1) { + description = description.substring(0, endIndex); + } + if (description.length() > 0 && Character.isUpperCase(description.charAt(0))) { + description = description.substring(0, 1).toLowerCase() + description.substring(1); + } + return description; + } + + private static MethodPollingDetails methodPollingDetailsFromMetadata( + Operation operation, + JavaSettings.PollingDetails pollingDetails) { + + if (pollingDetails == null || operation.getConvenienceApi() == null) { + return null; + } + + MethodPollingDetails methodPollingDetails = null; + if (operation.getLroMetadata() != null) { + // only TypeSpec would have LongRunningMetadata + LongRunningMetadata metadata = operation.getLroMetadata(); + ObjectMapper objectMapper = Mappers.getObjectMapper(); + IType intermediateType = objectMapper.map(metadata.getPollResultType()); + IType finalType = metadata.getFinalResultType() == null + ? PrimitiveType.VOID + : objectMapper.map(metadata.getFinalResultType()); + + // PollingDetails would override LongRunningMetadata + if (pollingDetails.getIntermediateType() != null) { + intermediateType = createTypeFromModelName(pollingDetails.getIntermediateType(), JavaSettings.getInstance()); + } + if (pollingDetails.getFinalType() != null) { + finalType = createTypeFromModelName(pollingDetails.getFinalType(), JavaSettings.getInstance()); + } + + // PollingStrategy + JavaSettings settings = JavaSettings.getInstance(); + final String packageName = settings.getPackage(settings.getImplementationSubpackage()); + String pollingStrategy = metadata.getPollingStrategy() == null + ? pollingDetails.getStrategy() + : String.format(JavaSettings.PollingDetails.DEFAULT_POLLING_STRATEGY_FORMAT, packageName + "." + metadata.getPollingStrategy().getLanguage().getJava().getName()); + String syncPollingStrategy = metadata.getPollingStrategy() == null + ? pollingDetails.getSyncStrategy() + : String.format(JavaSettings.PollingDetails.DEFAULT_POLLING_STRATEGY_FORMAT, packageName + ".Sync" + metadata.getPollingStrategy().getLanguage().getJava().getName()); + if (metadata.getPollingStrategy() != null && metadata.getFinalResultPropertySerializedName() != null) { + // add "" argument to polling strategy constructor + Function addPropertyNameToArguments = (strategy) -> { + strategy = strategy.substring(0, strategy.length() - 1) + ", "; + strategy += ClassType.STRING.defaultValueExpression(metadata.getFinalResultPropertySerializedName()); + strategy += ")"; + return strategy; + }; + pollingStrategy = addPropertyNameToArguments.apply(pollingStrategy); + syncPollingStrategy = addPropertyNameToArguments.apply(syncPollingStrategy); + } + + methodPollingDetails = new MethodPollingDetails( + pollingStrategy, + syncPollingStrategy, + intermediateType, + finalType, + pollingDetails.getPollIntervalInSeconds()); + } + return methodPollingDetails; + } + + /** + * Create IType from model name (full name or simple name). + * + * @param modelName the model name. If it is simple name, package name from JavaSetting will be used. + * @return IType of the model + */ + private static IType createTypeFromModelName(String modelName, JavaSettings settings) { + String finalTypeName; + String finalTypePackage; + if (modelName.contains(".")) { + finalTypeName = ANYTHING_THEN_PERIOD.matcher(modelName).replaceAll(""); + finalTypePackage = modelName.replace("." + finalTypeName, ""); + } else { + finalTypeName = modelName; + finalTypePackage = JavaSettings.getInstance().getPackage(); + } + return new ClassType.Builder().packageName(finalTypePackage).name(finalTypeName).build(); + } + + private static MethodNamer resolveMethodNamer(ProxyMethod proxyMethod, ConvenienceApi convenienceApi, boolean isProtocolMethod) { + if (!isProtocolMethod && convenienceApi != null) { + return new MethodNamer(SchemaUtil.getJavaName(convenienceApi)); + } else { + if (proxyMethod.isSync()) { + return new MethodNamer(proxyMethod.getBaseName()); + } + return new MethodNamer(proxyMethod.getName()); + } + } + + private static final class ReturnTypeHolder { + IType asyncRestResponseReturnType; + IType asyncReturnType; + IType syncReturnType; + IType syncReturnWithResponse; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/mapper/ClientParameterMapper.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/mapper/ClientParameterMapper.java new file mode 100644 index 0000000000..65abc04318 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/mapper/ClientParameterMapper.java @@ -0,0 +1,96 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.mapper; + +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.ConstantSchema; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Parameter; +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethodParameter; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.IType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.Versioning; +import com.microsoft.typespec.http.client.generator.core.util.CodeNamer; +import com.microsoft.typespec.http.client.generator.core.util.MethodUtil; +import com.microsoft.typespec.http.client.generator.core.util.SchemaUtil; + +import java.util.ArrayList; + +/** + * A mapper that maps an {@link Parameter} to a {@link ClientMethodParameter}. + */ +public class ClientParameterMapper implements IMapper { + private static final ClientParameterMapper INSTANCE = new ClientParameterMapper(); + + private ClientParameterMapper() { + } + + /** + * Gets the global {@link ClientParameterMapper} instance. + * + * @return The global {@link ClientParameterMapper} instance. + */ + public static ClientParameterMapper getInstance() { + return INSTANCE; + } + + @Override + public ClientMethodParameter map(Parameter parameter) { + return map(parameter, JavaSettings.getInstance().isDataPlaneClient()); + } + + /** + * Maps an {@link Parameter} to a {@link ClientMethodParameter}. + * + * @param parameter The {@link Parameter} being mapped. + * @param isProtocolMethod Whether the parameter is being used in a protocol method. + * @return The {@link ClientMethodParameter}. + */ + public ClientMethodParameter map(Parameter parameter, boolean isProtocolMethod) { + String name = parameter.getOriginalParameter() != null && parameter.getLanguage().getJava().getName().equals(parameter.getOriginalParameter().getLanguage().getJava().getName()) + ? CodeNamer.toCamelCase(parameter.getOriginalParameter().getSchema().getLanguage().getJava().getName()) + CodeNamer.toPascalCase(parameter.getLanguage().getJava().getName()) + : parameter.getLanguage().getJava().getName(); + name = CodeNamer.getEscapedReservedClientMethodParameterName(name); + + ClientMethodParameter.Builder builder = new ClientMethodParameter.Builder() + .name(name) + .required(parameter.isRequired()) + .fromClient(parameter.getImplementation() == Parameter.ImplementationLocation.CLIENT); + if (parameter.getProtocol() != null && parameter.getProtocol().getHttp() != null) { + builder.requestParameterLocation(parameter.getProtocol().getHttp().getIn()); + } + + IType wireType = Mappers.getSchemaMapper().map(parameter.getSchema()); + if (parameter.isNullable() || !parameter.isRequired()) { + wireType = wireType.asNullable(); + } + builder.rawType(wireType); + + if (isProtocolMethod) { + wireType = SchemaUtil.removeModelFromParameter(parameter.getProtocol().getHttp().getIn(), wireType); + } + + builder.wireType(wireType) + .annotations(new ArrayList<>()); + + boolean isConstant = false; + String defaultValue = null; + if (parameter.getSchema() instanceof ConstantSchema) { + isConstant = true; + Object objValue = ((ConstantSchema) parameter.getSchema()).getValue().getValue(); + defaultValue = objValue == null ? null : String.valueOf(objValue); + } + builder.constant(isConstant).defaultValue(defaultValue); + + builder.description(MethodUtil.getMethodParameterDescription(parameter, name, isProtocolMethod)); + + if (parameter.getExtensions() != null) { + if (parameter.getExtensions().getXmsVersioningAdded() != null) { + builder.versioning(new Versioning.Builder() + .added(parameter.getExtensions().getXmsVersioningAdded()) + .build()); + } + } + + return builder.build(); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/mapper/ConstantMapper.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/mapper/ConstantMapper.java new file mode 100644 index 0000000000..54095120e9 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/mapper/ConstantMapper.java @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.mapper; + +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.ConstantSchema; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.IType; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * A mapper that maps a {@link ConstantSchema} to a type. + */ +public class ConstantMapper implements IMapper { + private static final ConstantMapper INSTANCE = new ConstantMapper(); + Map parsed = new ConcurrentHashMap<>(); + + private ConstantMapper() { + } + + /** + * Gets the global {@link ConstantMapper} instance. + * + * @return The global {@link ConstantMapper} instance. + */ + public static ConstantMapper getInstance() { + return INSTANCE; + } + + @Override + public IType map(ConstantSchema constantSchema) { + if (constantSchema == null) { + return null; + } + + IType constantType = parsed.get(constantSchema); + if (constantType != null) { + return constantType; + } + + constantType = Mappers.getSchemaMapper().map(constantSchema.getValueType()); + parsed.put(constantSchema, constantType); + + return constantType; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/mapper/CustomClientParameterMapper.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/mapper/CustomClientParameterMapper.java new file mode 100644 index 0000000000..1c6ca81ad6 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/mapper/CustomClientParameterMapper.java @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.mapper; + +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.AnySchema; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.ArraySchema; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.ConstantSchema; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Parameter; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClassType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethodParameter; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.IType; +import com.microsoft.typespec.http.client.generator.core.util.CodeNamer; +import com.microsoft.typespec.http.client.generator.core.util.MethodUtil; +import com.microsoft.typespec.http.client.generator.core.util.SchemaUtil; + +import java.util.ArrayList; + +public class CustomClientParameterMapper implements IMapper { + + private static final CustomClientParameterMapper INSTANCE = new CustomClientParameterMapper(); + + private CustomClientParameterMapper() { + } + + public static CustomClientParameterMapper getInstance() { + return INSTANCE; + } + + @Override + public ClientMethodParameter map(Parameter parameter) { + return map(parameter, false); + } + + public ClientMethodParameter map(Parameter parameter, boolean isProtocolMethod) { + String name = parameter.getOriginalParameter() != null && parameter.getLanguage().getJava().getName().equals(parameter.getOriginalParameter().getLanguage().getJava().getName()) + ? CodeNamer.toCamelCase(parameter.getOriginalParameter().getSchema().getLanguage().getJava().getName()) + CodeNamer.toPascalCase(parameter.getLanguage().getJava().getName()) + : parameter.getLanguage().getJava().getName(); + + ClientMethodParameter.Builder builder = new ClientMethodParameter.Builder() + .name(name) + .required(parameter.isRequired()) + .fromClient(parameter.getImplementation() == Parameter.ImplementationLocation.CLIENT); + + IType wireType = Mappers.getSchemaMapper().map(parameter.getSchema()); + if (parameter.getSchema() instanceof ArraySchema) { + ArraySchema arraySchema = (ArraySchema) parameter.getSchema(); + if (arraySchema.getElementType() instanceof AnySchema) { + wireType = ClassType.JSON_PATCH_DOCUMENT; + } + } + + if (isProtocolMethod) { + wireType = SchemaUtil.removeModelFromParameter(parameter.getProtocol().getHttp().getIn(), wireType); + } + + if (parameter.isNullable() || !parameter.isRequired()) { + wireType = wireType.asNullable(); + } + builder.wireType(wireType); + + builder.annotations(new ArrayList<>()); + + boolean isConstant = false; + String defaultValue = null; + if (parameter.getSchema() instanceof ConstantSchema) { + isConstant = true; + Object objValue = ((ConstantSchema) parameter.getSchema()).getValue().getValue(); + defaultValue = objValue == null ? null : String.valueOf(objValue); + } + builder.constant(isConstant).defaultValue(defaultValue); + + builder.description(MethodUtil.getMethodParameterDescription(parameter, name, isProtocolMethod)); + + return builder.build(); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/mapper/CustomProxyParameterMapper.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/mapper/CustomProxyParameterMapper.java new file mode 100644 index 0000000000..cea003f533 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/mapper/CustomProxyParameterMapper.java @@ -0,0 +1,169 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.mapper; + +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.AnySchema; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.ArraySchema; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.ConstantSchema; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Parameter; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.RequestParameterLocation; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Schema; +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ArrayType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClassType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.IType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ListType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.PrimitiveType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ProxyMethodParameter; +import com.microsoft.typespec.http.client.generator.core.util.CodeNamer; +import com.microsoft.typespec.http.client.generator.core.util.SchemaUtil; +import com.azure.core.util.serializer.CollectionFormat; + +public class CustomProxyParameterMapper implements IMapper { + + private static final CustomProxyParameterMapper INSTANCE = new CustomProxyParameterMapper(); + + private CustomProxyParameterMapper() { + } + + public static CustomProxyParameterMapper getInstance() { + return INSTANCE; + } + + + @Override + public ProxyMethodParameter map(Parameter parameter) { + JavaSettings settings = JavaSettings.getInstance(); + + ProxyMethodParameter.Builder builder = new ProxyMethodParameter.Builder() + .requestParameterName(parameter.getLanguage().getDefault().getSerializedName()) + .name(parameter.getLanguage().getJava().getName()) + .required(parameter.isRequired()) + .nullable(parameter.isNullable()); + + String headerCollectionPrefix = null; + if (parameter.getExtensions() != null && parameter.getExtensions().getXmsHeaderCollectionPrefix() != null) { + headerCollectionPrefix = parameter.getExtensions().getXmsHeaderCollectionPrefix(); + } + builder.headerCollectionPrefix(headerCollectionPrefix); + + + Schema parameterJvWireType = parameter.getSchema(); + + IType wireType = Mappers.getSchemaMapper().map(parameterJvWireType); + + if (parameterJvWireType instanceof ArraySchema) { + ArraySchema arraySchema = (ArraySchema) parameterJvWireType; + if (arraySchema.getElementType() instanceof AnySchema) { + wireType = ClassType.JSON_PATCH_DOCUMENT; + } + } + + if (parameter.isNullable() || !parameter.isRequired()) { + wireType = wireType.asNullable(); + } + IType clientType = wireType.getClientType(); + builder.clientType(clientType); + + RequestParameterLocation parameterRequestLocation = parameter.getProtocol().getHttp().getIn(); + builder.requestParameterLocation(parameterRequestLocation); + + boolean parameterIsServiceClientProperty = parameter.getImplementation() == Parameter.ImplementationLocation.CLIENT; + builder.fromClient(parameterIsServiceClientProperty); + + if (wireType instanceof ListType && SchemaUtil.treatAsXml(parameterJvWireType) + && parameterRequestLocation == RequestParameterLocation.BODY) { + String modelTypeName = ((ArraySchema) parameterJvWireType).getElementType().getLanguage().getJava().getName(); + boolean isCustomType = settings.isCustomType(CodeNamer.toPascalCase(modelTypeName + "Wrapper")); + String packageName = isCustomType + ? settings.getPackage(settings.getCustomTypesSubpackage()) + : settings.getPackage(settings.getImplementationSubpackage() + ".models"); + wireType = new ClassType.Builder() + .packageName(packageName) + .name(modelTypeName + "Wrapper") + .usedInXml(true) + .build(); + } else if (wireType == ArrayType.BYTE_ARRAY) { + if (parameterRequestLocation != RequestParameterLocation.BODY /*&& parameterRequestLocation != RequestParameterLocation.FormData*/) { + wireType = ClassType.STRING; + } + } else if (wireType instanceof ListType && parameter.getProtocol().getHttp().getIn() != RequestParameterLocation.BODY /*&& parameter.getProtocol().getHttp().getIn() != RequestParameterLocation.FormData*/) { + if (parameter.getProtocol().getHttp().getExplode()) { + wireType = new ListType(ClassType.STRING); + } else { + wireType = ClassType.STRING; + } + } else if (settings.isDataPlaneClient() && !(wireType instanceof PrimitiveType)) { + wireType = ClassType.STRING; + } + if (parameter.getProtocol().getHttp().getExplode()) { + builder.alreadyEncoded(true); + } + builder.wireType(wireType); + + String parameterDescription = parameter.getDescription(); + if (parameterDescription == null || parameterDescription.isEmpty()) { + parameterDescription = String.format("the %s value", clientType); + } + builder.description(parameterDescription); + + if (parameter.getExtensions() != null) { + builder.alreadyEncoded(parameter.getExtensions().isXmsSkipUrlEncoding()); + } + + if (parameter.getSchema() instanceof ConstantSchema){ + builder.constant(true); + Object objValue = ((ConstantSchema) parameter.getSchema()).getValue().getValue(); + builder.defaultValue(objValue == null ? null : String.valueOf(objValue)); + } + + String parameterReference = parameter.getLanguage().getJava().getName(); + if (Parameter.ImplementationLocation.CLIENT.equals(parameter.getImplementation())) { + String operationGroupName = parameter.getOperation().getOperationGroup().getLanguage().getJava().getName(); + String caller = (operationGroupName == null || operationGroupName.isEmpty()) ? "this" : "this.client"; + String clientPropertyName = parameter.getLanguage().getJava().getName(); + if (clientPropertyName != null && !clientPropertyName.isEmpty()) { + clientPropertyName = CodeNamer.toPascalCase(CodeNamer.removeInvalidCharacters(clientPropertyName)); + } + String prefix = "get"; + if (clientType == PrimitiveType.BOOLEAN || clientType == ClassType.BOOLEAN) { + prefix = "is"; + if (CodeNamer.toCamelCase(parameterReference).startsWith(prefix)) { + prefix = ""; + clientPropertyName = CodeNamer.toCamelCase(clientPropertyName); + } + } + parameterReference = String.format("%s.%s%s()", caller, prefix, clientPropertyName); + } + builder.parameterReference(parameterReference); + + CollectionFormat collectionFormat = null; + if (parameter.getProtocol().getHttp().getStyle() != null) { + switch (parameter.getProtocol().getHttp().getStyle()) { + case SIMPLE: + collectionFormat = CollectionFormat.CSV; + break; + case SPACE_DELIMITED: + collectionFormat = CollectionFormat.SSV; + break; + case PIPE_DELIMITED: + collectionFormat = CollectionFormat.PIPES; + break; + case TAB_DELIMITED: + collectionFormat = CollectionFormat.TSV; + break; + default: + collectionFormat = CollectionFormat.CSV; + } + } + if (collectionFormat == null && clientType instanceof ListType + && ClassType.STRING == wireType) { + collectionFormat = CollectionFormat.CSV; + } + builder.collectionFormat(collectionFormat); + builder.explode(parameter.getProtocol().getHttp().getExplode()); + + return builder.build(); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/mapper/DefaultMapperFactory.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/mapper/DefaultMapperFactory.java new file mode 100644 index 0000000000..c7d28a5aba --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/mapper/DefaultMapperFactory.java @@ -0,0 +1,126 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.mapper; + +public class DefaultMapperFactory implements MapperFactory { + @Override + public ChoiceMapper getChoiceMapper() { + return ChoiceMapper.getInstance(); + } + + @Override + public SealedChoiceMapper getSealedChoiceMapper() { + return SealedChoiceMapper.getInstance(); + } + + @Override + public PrimitiveMapper getPrimitiveMapper() { + return PrimitiveMapper.getInstance(); + } + + @Override + public SchemaMapper getSchemaMapper() { + return SchemaMapper.getInstance(); + } + + @Override + public ArrayMapper getArrayMapper() { + return ArrayMapper.getInstance(); + } + + @Override + public DictionaryMapper getDictionaryMapper() { + return DictionaryMapper.getInstance(); + } + + @Override + public ObjectMapper getObjectMapper() { + return ObjectMapper.getInstance(); + } + + @Override + public ConstantMapper getConstantMapper() { + return ConstantMapper.getInstance(); + } + + @Override + public ModelPropertyMapper getModelPropertyMapper() { + return ModelPropertyMapper.getInstance(); + } + + @Override + public ModelMapper getModelMapper() { + return ModelMapper.getInstance(); + } + + @Override + public ProxyParameterMapper getProxyParameterMapper() { + return ProxyParameterMapper.getInstance(); + } + + @Override + public ProxyMethodMapper getProxyMethodMapper() { + return ProxyMethodMapper.getInstance(); + } + + @Override + public ProxyMethodExampleMapper getProxyMethodExampleMapper() { + return ProxyMethodExampleMapper.getInstance(); + } + + @Override + public MethodGroupMapper getMethodGroupMapper() { + return MethodGroupMapper.getInstance(); + } + + @Override + public ClientParameterMapper getClientParameterMapper() { + return ClientParameterMapper.getInstance(); + } + + @Override + public ClientMethodMapper getClientMethodMapper() { + return ClientMethodMapper.getInstance(); + } + + @Override + public ExceptionMapper getExceptionMapper() { + return ExceptionMapper.getInstance(); + } + + @Override + public ServiceClientMapper getServiceClientMapper() { + return ServiceClientMapper.getInstance(); + } + + @Override + public ClientMapper getClientMapper() { + return ClientMapper.getInstance(); + } + + @Override + public AnyMapper getAnyMapper() { + return AnyMapper.getInstance(); + } + + @Override + public BinaryMapper getBinaryMapper() { + return BinaryMapper.getInstance(); + } + + @Override + public UnionMapper getUnionMapper() { + return UnionMapper.getInstance(); + } + + @Override + public UnionModelMapper getUnionModelMapper() { + return UnionModelMapper.getInstance(); + } + + @Override + public GraalVmConfigMapper getGraalVmConfigMapper() { + return GraalVmConfigMapper.getInstance(); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/mapper/DictionaryMapper.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/mapper/DictionaryMapper.java new file mode 100644 index 0000000000..fded6dc2c7 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/mapper/DictionaryMapper.java @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.mapper; + +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.DictionarySchema; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.IType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.MapType; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class DictionaryMapper implements IMapper { + private static final DictionaryMapper INSTANCE = new DictionaryMapper(); + Map parsed = new ConcurrentHashMap<>(); + + private DictionaryMapper() { + } + + public static DictionaryMapper getInstance() { + return INSTANCE; + } + + @Override + public IType map(DictionarySchema dictionaryType) { + if (dictionaryType == null) { + return null; + } + + IType dictType = parsed.get(dictionaryType); + if (dictType != null) { + return dictType; + } + + IType elementType = Mappers.getSchemaMapper().map(dictionaryType.getElementType()); + boolean elementNullable = dictionaryType.getNullableItems() != null && dictionaryType.getNullableItems(); + if (elementNullable) { + elementType = elementType.asNullable(); + } + dictType = new MapType(elementType, elementNullable); + parsed.put(dictionaryType, dictType); + + return dictType; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/mapper/ExceptionMapper.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/mapper/ExceptionMapper.java new file mode 100644 index 0000000000..0f2a3a656c --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/mapper/ExceptionMapper.java @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.mapper; + +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.ObjectSchema; +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientException; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class ExceptionMapper implements IMapper { + private static final ExceptionMapper INSTANCE = new ExceptionMapper(); + Map parsed = new ConcurrentHashMap<>(); + + protected ExceptionMapper() { + } + + public static ExceptionMapper getInstance() { + return INSTANCE; + } + + @Override + public ClientException map(ObjectSchema compositeType) { + if (compositeType == null + // there is no need to generate Exception class, if we use Exceptions from azure-core + || (JavaSettings.getInstance().isDataPlaneClient() && JavaSettings.getInstance().isUseDefaultHttpStatusCodeToExceptionTypeMapping())) { + return null; + } + + return parsed.computeIfAbsent(compositeType, cType -> buildException(cType, JavaSettings.getInstance())); + } + + protected ClientException buildException(ObjectSchema compositeType, JavaSettings settings) { + String errorName = compositeType.getLanguage().getJava().getName(); + String methodOperationExceptionTypeName = errorName + "Exception"; + + if (compositeType.getExtensions() != null && compositeType.getExtensions().getXmsClientName() != null) { + methodOperationExceptionTypeName = compositeType.getExtensions().getXmsClientName(); + } + + boolean isCustomType = settings.isCustomType(methodOperationExceptionTypeName); + String exceptionSubPackage = isCustomType + ? settings.getCustomTypesSubpackage() + : settings.getModelsSubpackage(); + String packageName = settings.getPackage(exceptionSubPackage); + + return createClientExceptionBuilder() + .packageName(packageName) + .name(methodOperationExceptionTypeName) + .errorName(errorName) + .build(); + } + + protected ClientException.Builder createClientExceptionBuilder() { + return new ClientException.Builder(); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/mapper/GraalVmConfigMapper.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/mapper/GraalVmConfigMapper.java new file mode 100644 index 0000000000..a416737c29 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/mapper/GraalVmConfigMapper.java @@ -0,0 +1,86 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.mapper; + +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientException; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientModel; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.EnumType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.GraalVmConfig; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ServiceClient; + +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class GraalVmConfigMapper implements IMapper { + + public static class ServiceAndModel { + private final Collection serviceClients; + private final Collection exceptions; + private final Collection models; + private final Collection enums; + + public ServiceAndModel(Collection serviceClients, + Collection exceptions, + Collection models, + Collection enums) { + this.serviceClients = serviceClients; + this.exceptions = exceptions; + this.models = models; + this.enums = enums; + } + } + + private static final GraalVmConfigMapper INSTANCE = new GraalVmConfigMapper(); + + protected GraalVmConfigMapper() { + } + + public static GraalVmConfigMapper getInstance() { + return INSTANCE; + } + + @Override + public GraalVmConfig map(ServiceAndModel data) { + List proxies; + List reflects; + + final boolean streamStyle = JavaSettings.getInstance().isStreamStyleSerialization(); + + // Reflect + // Exception and error model is still created by reflection in azure-core + reflects = data.exceptions.stream() + .map(e -> e.getPackage() + "." + e.getName()) + .collect(Collectors.toList()); + reflects.addAll(data.models.stream() + .filter(m -> !streamStyle || (m.getImplementationDetails() != null && m.getImplementationDetails().isException())) + .map(m -> m.getPackage() + "." + m.getName()) + .collect(Collectors.toList())); + reflects.addAll(data.enums.stream() + .filter(m -> !streamStyle || (m.getImplementationDetails() != null && m.getImplementationDetails().isException())) + .map(m -> m.getPackage() + "." + m.getName()) + .collect(Collectors.toList())); + + // Proxy + proxies = data.serviceClients.stream() + .flatMap(sc -> { + if (sc.getMethodGroupClients() != null) { + return sc.getMethodGroupClients().stream(); + } else { + return Stream.empty(); + } + }) + .filter(m -> m.getProxy() != null) + .map(m -> m.getPackage() + "." + m.getClassName() + "$" + m.getProxy().getName()) + .collect(Collectors.toList()); + proxies.addAll(data.serviceClients.stream() + .filter(sc -> sc.getProxy() != null) + .map(sc -> sc.getPackage() + "." + sc.getClassName() + "$" + sc.getProxy().getName()) + .collect(Collectors.toList())); + + return new GraalVmConfig(proxies, reflects, JavaSettings.getInstance().isFluent()); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/mapper/IMapper.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/mapper/IMapper.java new file mode 100644 index 0000000000..91f8d9dd3f --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/mapper/IMapper.java @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.mapper; + +/** + * Interface for mapping from a value to another value. + * + * @param The from type. + * @param The to type. + */ +public interface IMapper { + /** + * Maps the from value. + * + * @param fromT The from value. + * @return The mapped to value. + */ + ToT map(FromT fromT); +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/mapper/LiveTestsMapper.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/mapper/LiveTestsMapper.java new file mode 100644 index 0000000000..6d35a0c88d --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/mapper/LiveTestsMapper.java @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.mapper; + +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.ScenarioStep; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.TestModel; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.TestScenarioStepType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ExampleLiveTestStep; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.LiveTestCase; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.LiveTestStep; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.LiveTests; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ProxyMethodExample; +import com.microsoft.typespec.http.client.generator.core.util.CodeNamer; +import com.microsoft.typespec.http.client.generator.core.util.XmsExampleWrapper; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * A mapper to map test model to live tests. + */ +public class LiveTestsMapper implements IMapper>{ + + private static final LiveTestsMapper INSTANCE = new LiveTestsMapper(); + + public static LiveTestsMapper getInstance() { + return INSTANCE; + } + + @Override + public List map(TestModel testModel) { + if (testModel.getScenarioTests() == null) { + return new ArrayList<>(); + } + return testModel.getScenarioTests().stream().map(scenarioTest -> { + LiveTests liveTests = new LiveTests(getFilename(scenarioTest.getFilePath())); + liveTests.addTestCases(scenarioTest.getScenarios().stream().map(testScenario -> { + LiveTestCase liveTestCase = new LiveTestCase(CodeNamer.toCamelCase(testScenario.getScenario()), testScenario.getDescription()); + liveTestCase.addTestSteps(testScenario.getResolvedSteps().stream() + // future work: support other step types, for now only support example file + .filter(scenarioStep -> scenarioStep.getType() == TestScenarioStepType.REST_CALL && + scenarioStep.getExampleFile() != null) + .map((Function) scenarioStep -> { + Map example = new HashMap<>(); + example.put("parameters", scenarioStep.getRequestParameters()); + XmsExampleWrapper exampleWrapper = new XmsExampleWrapper(example, scenarioStep.getOperationId(), scenarioStep.getExampleName()); + ProxyMethodExample proxyMethodExample = Mappers.getProxyMethodExampleMapper().map(exampleWrapper); + return ExampleLiveTestStep.newBuilder() + .operationId(scenarioStep.getOperationId()) + .description(scenarioStep.getDescription()) + .example(proxyMethodExample) + .build(); + }) + .collect(Collectors.toList())); + return liveTestCase; + }).collect(Collectors.toList())); + return liveTests; + }).collect(Collectors.toList()); + } + + private static String getFilename(String filePath) { + String[] split = filePath.replace("\\\\", "/").split("/"); + String filename = split[split.length - 1]; + filename = filename.split("\\.")[0]; + return CodeNamer.toPascalCase(filename); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/mapper/MapperFactory.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/mapper/MapperFactory.java new file mode 100644 index 0000000000..b034ab5596 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/mapper/MapperFactory.java @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.mapper; + +public interface MapperFactory { + + ChoiceMapper getChoiceMapper(); + + SealedChoiceMapper getSealedChoiceMapper(); + + PrimitiveMapper getPrimitiveMapper(); + + SchemaMapper getSchemaMapper(); + + ArrayMapper getArrayMapper(); + + DictionaryMapper getDictionaryMapper(); + + ObjectMapper getObjectMapper(); + + ConstantMapper getConstantMapper(); + + ModelPropertyMapper getModelPropertyMapper(); + + ModelMapper getModelMapper(); + + ProxyParameterMapper getProxyParameterMapper(); + + ProxyMethodMapper getProxyMethodMapper(); + + ProxyMethodExampleMapper getProxyMethodExampleMapper(); + + MethodGroupMapper getMethodGroupMapper(); + + ClientParameterMapper getClientParameterMapper(); + + ClientMethodMapper getClientMethodMapper(); + + ExceptionMapper getExceptionMapper(); + + ServiceClientMapper getServiceClientMapper(); + + ClientMapper getClientMapper(); + + AnyMapper getAnyMapper(); + + BinaryMapper getBinaryMapper(); + + UnionMapper getUnionMapper(); + + UnionModelMapper getUnionModelMapper(); + + GraalVmConfigMapper getGraalVmConfigMapper(); +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/mapper/MapperUtils.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/mapper/MapperUtils.java new file mode 100644 index 0000000000..244cee2184 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/mapper/MapperUtils.java @@ -0,0 +1,120 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.mapper; + +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.ArraySchema; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.ChoiceSchema; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.ChoiceValue; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Operation; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Response; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Schema; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.SchemaContext; +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClassType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientEnumValue; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.EnumType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.IType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ImplementationDetails; +import com.microsoft.typespec.http.client.generator.core.util.CodeNamer; +import com.microsoft.typespec.http.client.generator.core.util.SchemaUtil; +import com.azure.core.util.CoreUtils; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + * Contains utility methods to help map from modelerfour to Java Autorest. + */ +final class MapperUtils { + static IType createEnumType(ChoiceSchema enumType, boolean expandable) { + JavaSettings settings = JavaSettings.getInstance(); + String enumTypeName = enumType.getLanguage().getJava().getName(); + + if (enumTypeName == null || enumTypeName.isEmpty() || enumTypeName.equals("enum")) { + return ClassType.STRING; + } else { + String enumPackage = settings.getPackage(settings.getModelsSubpackage()); + if (settings.isCustomType(enumTypeName)) { + enumPackage = settings.getPackage(settings.getCustomTypesSubpackage()); + } else if (settings.isDataPlaneClient() && (enumType.getUsage() != null && enumType.getUsage().contains(SchemaContext.INTERNAL))) { + // internal type, which is not exposed to user + enumPackage = settings.getPackage(settings.getImplementationSubpackage(), settings.getModelsSubpackage()); + } + + String summary = enumType.getSummary(); + String description = enumType.getLanguage().getJava() == null ? null : enumType.getLanguage().getJava().getDescription(); + description = SchemaUtil.mergeSummaryWithDescription(summary, description); + if (CoreUtils.isNullOrEmpty(description)) { + description = "Defines values for " + enumTypeName + "."; + } + + List enumValues = new ArrayList<>(); + for (ChoiceValue enumValue : enumType.getChoices()) { + String enumName = enumValue.getValue(); + String enumDescription = null; + if (!settings.isFluent()) { + if (enumValue.getLanguage() != null && enumValue.getLanguage().getJava() != null + && enumValue.getLanguage().getJava().getName() != null) { + enumName = enumValue.getLanguage().getJava().getName(); + enumDescription = enumValue.getLanguage().getJava().getDescription(); + } else if (enumValue.getLanguage() != null && enumValue.getLanguage().getDefault() != null + && enumValue.getLanguage().getDefault().getName() != null) { + enumName = enumValue.getLanguage().getDefault().getName(); + enumDescription = enumValue.getLanguage().getDefault().getDescription(); + } + } + final String memberName = CodeNamer.getEnumMemberName(enumName); + long counter = enumValues.stream().filter(v -> v.getName().equals(memberName)).count(); + if (counter > 0) { + enumValues.add(new ClientEnumValue(memberName + "_" + counter, enumValue.getValue(), enumDescription)); + } else { + enumValues.add(new ClientEnumValue(memberName, enumValue.getValue(), enumDescription)); + } + } + + return new EnumType.Builder() + .packageName(enumPackage) + .name(enumTypeName) + .description(description) + .expandable(expandable) + .values(enumValues) + .elementType(Mappers.getSchemaMapper().map(enumType.getChoiceType())) + .implementationDetails(new ImplementationDetails.Builder() + .usages(SchemaUtil.mapSchemaContext(enumType.getUsage())) + .build()) + .crossLanguageDefinitionId(enumType.getCrossLanguageDefinitionId()) + .build(); + } + } + + static IType handleResponseSchema(Operation operation, JavaSettings settings) { + Schema responseBodySchema = SchemaUtil.getLowestCommonParent(operation.getResponses().stream() + .map(Response::getSchema).filter(Objects::nonNull).iterator()); + boolean xmlWrapperResponse = responseBodySchema != null && responseBodySchema.getSerialization() != null + && responseBodySchema.getSerialization().getXml() != null + && responseBodySchema.getSerialization().getXml().isWrapped(); + + if (!xmlWrapperResponse) { + return SchemaUtil.getOperationResponseType(responseBodySchema, operation, settings); + } + + // XML wrapped response types are tricky as they're defined as ArraySchema but in reality it's a specialized + // ObjectSchema. + ArraySchema arraySchema = (ArraySchema) responseBodySchema; + String className = arraySchema.getElementType().getLanguage().getJava().getName() + "Wrapper"; + String classPackage = settings.isCustomType(className) + ? settings.getPackage(className) + : settings.getPackage(settings.getImplementationSubpackage() + ".models"); + + return new ClassType.Builder() + .packageName(classPackage) + .name(className) + .extensions(responseBodySchema.getExtensions()) + .build(); + } + + private MapperUtils() { + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/mapper/Mappers.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/mapper/Mappers.java new file mode 100644 index 0000000000..4c8c66252b --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/mapper/Mappers.java @@ -0,0 +1,109 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.mapper; + +public class Mappers { + + private static MapperFactory factory = new DefaultMapperFactory(); + + public static void setFactory(MapperFactory mapperFactory) { + factory = mapperFactory; + } + + public static ChoiceMapper getChoiceMapper() { + return factory.getChoiceMapper(); + } + + public static SealedChoiceMapper getSealedChoiceMapper() { + return factory.getSealedChoiceMapper(); + } + + public static PrimitiveMapper getPrimitiveMapper() { + return factory.getPrimitiveMapper(); + } + + public static SchemaMapper getSchemaMapper() { + return factory.getSchemaMapper(); + } + + public static ArrayMapper getArrayMapper() { + return factory.getArrayMapper(); + } + + public static DictionaryMapper getDictionaryMapper() { + return factory.getDictionaryMapper(); + } + + public static ObjectMapper getObjectMapper() { + return factory.getObjectMapper(); + } + + public static ConstantMapper getConstantMapper() { + return factory.getConstantMapper(); + } + + public static ModelPropertyMapper getModelPropertyMapper() { + return factory.getModelPropertyMapper(); + } + + public static ModelMapper getModelMapper() { + return factory.getModelMapper(); + } + + public static ProxyParameterMapper getProxyParameterMapper() { + return factory.getProxyParameterMapper(); + } + + public static ProxyMethodMapper getProxyMethodMapper() { + return factory.getProxyMethodMapper(); + } + + public static ProxyMethodExampleMapper getProxyMethodExampleMapper() { + return factory.getProxyMethodExampleMapper(); + } + + public static MethodGroupMapper getMethodGroupMapper() { + return factory.getMethodGroupMapper(); + } + + public static ClientParameterMapper getClientParameterMapper() { + return factory.getClientParameterMapper(); + } + + public static ClientMethodMapper getClientMethodMapper() { + return factory.getClientMethodMapper(); + } + + public static ExceptionMapper getExceptionMapper() { + return factory.getExceptionMapper(); + } + + public static ServiceClientMapper getServiceClientMapper() { + return factory.getServiceClientMapper(); + } + + public static ClientMapper getClientMapper() { + return factory.getClientMapper(); + } + + public static AnyMapper getAnyMapper() { + return factory.getAnyMapper(); + } + + public static BinaryMapper getBinaryMapper() { + return factory.getBinaryMapper(); + } + + public static UnionMapper getUnionMapper() { + return factory.getUnionMapper(); + } + + public static UnionModelMapper getUnionModelMapper() { + return factory.getUnionModelMapper(); + } + + public static GraalVmConfigMapper getGraalVmConfigMapper() { + return factory.getGraalVmConfigMapper(); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/mapper/MethodGroupMapper.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/mapper/MethodGroupMapper.java new file mode 100644 index 0000000000..65fd80c975 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/mapper/MethodGroupMapper.java @@ -0,0 +1,167 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.mapper; + +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Operation; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.OperationGroup; +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethod; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientModels; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.IType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.MethodGroupClient; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.Proxy; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ProxyMethod; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ServiceClientProperty; +import com.microsoft.typespec.http.client.generator.core.util.ClientModelUtil; +import com.microsoft.typespec.http.client.generator.core.util.CodeNamer; +import com.microsoft.typespec.http.client.generator.core.util.MethodUtil; +import com.azure.core.util.CoreUtils; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +public class MethodGroupMapper implements IMapper { + private static final MethodGroupMapper INSTANCE = new MethodGroupMapper(); + private final Map parsed = new ConcurrentHashMap<>(); + + protected MethodGroupMapper() { + } + + public static MethodGroupMapper getInstance() { + return INSTANCE; + } + + @Override + public MethodGroupClient map(OperationGroup methodGroup) { + return this.map(methodGroup, null); + } + + public MethodGroupClient map(OperationGroup methodGroup, List parentClientProperties) { + MethodGroupClient methodGroupClient = parsed.get(methodGroup); + if (methodGroupClient != null) { + return methodGroupClient; + } + + methodGroupClient = createMethodGroupClient(methodGroup, parentClientProperties); + parsed.put(methodGroup, methodGroupClient); + + return methodGroupClient; + } + + private MethodGroupClient createMethodGroupClient(OperationGroup methodGroup, List parentClientProperties) { + JavaSettings settings = JavaSettings.getInstance(); + MethodGroupClient.Builder builder = createMethodGroupClientBuilder(); + + String classBaseName = methodGroup.getLanguage().getJava().getName(); + builder.classBaseName(classBaseName); + String interfaceName = CodeNamer.getPlural(classBaseName); + final String interfaceNameForCheckDeduplicate = interfaceName; + if (ClientModels.getInstance().getModels().stream().anyMatch(cm -> interfaceNameForCheckDeduplicate.equals(cm.getName())) + || parsed.values().stream().anyMatch(mg -> interfaceNameForCheckDeduplicate.equals(mg.getInterfaceName()))) { + interfaceName += "Operations"; + } + builder.interfaceName(interfaceName); + String className = interfaceName; + if (settings.isFluent()) { + if (settings.isGenerateClientAsImpl()) { + className += "ClientImpl"; + } else { + className += "Client"; + } + } else if (settings.isGenerateClientAsImpl()) { + className += "Impl"; + } + builder.className(className); + + if (!CoreUtils.isNullOrEmpty(methodGroup.getOperations())) { + Proxy.Builder proxyBuilder = createProxyBuilder(); + + String restAPIName = CodeNamer.toPascalCase(CodeNamer.getPlural(methodGroup.getLanguage().getJava().getName())); + restAPIName += "Service"; + String serviceClientName = methodGroup.getCodeModel().getLanguage().getJava().getName(); + // TODO: Assume all operations share the same base url + proxyBuilder.name(restAPIName) + .clientTypeName(serviceClientName + interfaceName) + .baseURL(methodGroup.getOperations().get(0).getRequests().get(0).getProtocol().getHttp().getUri()); + + List restAPIMethods = new ArrayList<>(); + for (Operation method : methodGroup.getOperations()) { + if (settings.isDataPlaneClient()) { + MethodUtil.tryMergeBinaryRequestsAndUpdateOperation(method.getRequests(), method); + } + restAPIMethods.addAll(Mappers.getProxyMethodMapper().map(method).values().stream().flatMap(Collection::stream).collect(Collectors.toList())); + } + proxyBuilder.methods(restAPIMethods); + + builder.proxy(proxyBuilder.build()); + } + + String serviceClientName = ClientModelUtil.getClientImplementClassName(methodGroup.getCodeModel()); + builder.serviceClientName(serviceClientName); + + builder.variableName(CodeNamer.toCamelCase(interfaceName)); + + if (settings.isFluent() && settings.isGenerateClientInterfaces()) { + interfaceName += "Client"; + builder.interfaceName(interfaceName); + } + + builder.variableType(settings.isGenerateClientInterfaces() ? interfaceName : className); + + List implementedInterfaces = new ArrayList<>(); + if (settings.isGenerateClientInterfaces()) { + implementedInterfaces.add(interfaceName); + } + builder.implementedInterfaces(implementedInterfaces); + + String packageName; + if (settings.isFluent()) { + packageName = settings.getPackage(settings.isGenerateClientAsImpl() ? settings.getImplementationSubpackage() : settings.getFluentSubpackage()); + } else { + boolean isCustomType = settings.isCustomType(className); + packageName = settings.getPackage(isCustomType ? settings.getCustomTypesSubpackage() : (settings.isGenerateClientAsImpl() ? settings.getImplementationSubpackage() : null)); + } + builder.packageName(packageName); + + List clientMethods = new ArrayList<>(); + for (Operation operation : methodGroup.getOperations()) { + clientMethods.addAll(Mappers.getClientMethodMapper().map(operation)); + } + if (settings.isGenerateSendRequestMethod()) { + clientMethods.add(ClientMethod.getAsyncSendRequestClientMethod(true)); + if (settings.getSyncMethods() != JavaSettings.SyncMethodsGeneration.NONE) { + clientMethods.add(ClientMethod.getSyncSendRequestClientMethod(true)); + } + } + builder.clientMethods(clientMethods); + builder.supportedInterfaces(supportedInterfaces(methodGroup, clientMethods)); + + if (!CoreUtils.isNullOrEmpty(parentClientProperties) && settings.isGenerateClientAsImpl()) { + // filter for serviceVersion + builder.properties(parentClientProperties.stream() + .filter(p -> Objects.equals("serviceVersion", p.getName())) + .collect(Collectors.toList())); + } + + return builder.build(); + } + + protected MethodGroupClient.Builder createMethodGroupClientBuilder() { + return new MethodGroupClient.Builder(); + } + + protected Proxy.Builder createProxyBuilder() { + return new Proxy.Builder(); + } + + protected List supportedInterfaces(OperationGroup operationGroup, List clientMethods) { + return Collections.emptyList(); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/mapper/ModelMapper.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/mapper/ModelMapper.java new file mode 100644 index 0000000000..746674be03 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/mapper/ModelMapper.java @@ -0,0 +1,661 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.mapper; + +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.ArraySchema; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.DictionarySchema; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Language; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Languages; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.ObjectSchema; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Property; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Schema; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.XmlSerializationFormat; +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClassType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientModel; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientModelProperty; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientModelPropertyReference; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientModels; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ExternalPackage; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.IType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ImplementationDetails; +import com.microsoft.typespec.http.client.generator.core.util.CodeNamer; +import com.microsoft.typespec.http.client.generator.core.util.SchemaUtil; +import com.azure.core.util.CoreUtils; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class ModelMapper implements IMapper, NeedsPlainObjectCheck { + private static final ModelMapper INSTANCE = new ModelMapper(); + private final ClientModels serviceModels = ClientModels.getInstance(); + + private final static String PROPERTY_NAME_ADDITIONAL_PROPERTIES = "additionalProperties"; + + protected ModelMapper() { + } + + public static ModelMapper getInstance() { + return INSTANCE; + } + + @Override + public ClientModel map(ObjectSchema compositeType) { + JavaSettings settings = JavaSettings.getInstance(); + ObjectMapper objectMapper = Mappers.getObjectMapper(); + + ClassType modelType = objectMapper.map(compositeType); + String modelName = modelType.getName(); + ClientModel result = serviceModels.getModel(modelType.getName()); + if (result == null && !isPlainObject(compositeType)) { + Set usages = SchemaUtil.mapSchemaContext(compositeType.getUsage()); + if (isPredefinedModel(modelType)) { + // TODO (weidxu): a more consistent handling of external model for all data-plane + if (settings.isDataPlaneClient()) { + usages = new HashSet<>(usages); + usages.add(ImplementationDetails.Usage.EXTERNAL); + } else { + // abort handling external model, if not DPG + // vanilla and fluent currently does not have mechanism to handle model that not to be outputted. + return result; + } + } + + if (usages.contains(ImplementationDetails.Usage.JSON_MERGE_PATCH) + && !usages.contains(ImplementationDetails.Usage.INPUT)) { + // Remove the usage of JSON merge patch if the model isn't used as INPUT to the service. JSON merge + // patch logic is only used for INPUT. + usages.remove(ImplementationDetails.Usage.JSON_MERGE_PATCH); + } + + ClientModel.Builder builder = createModelBuilder().name(modelName) + .packageName(modelType.getPackage()) + .type(modelType) + .stronglyTypedHeader(compositeType.isStronglyTypedHeader()) + .usedInXml(SchemaUtil.treatAsXml(compositeType)) + .serializationFormats(compositeType.getSerializationFormats()) + .implementationDetails(new ImplementationDetails.Builder().usages(usages).build()); + + boolean isPolymorphic = compositeType.getDiscriminator() != null + || compositeType.getDiscriminatorValue() != null; + builder.polymorphic(isPolymorphic); + + HashSet modelImports = new HashSet<>(); + + String parentModelName = null; + boolean hasAdditionalProperties = false; + List parentsNeedFlatten = Collections.emptyList(); + if (compositeType.getParents() != null && compositeType.getParents().getImmediate() != null) { + hasAdditionalProperties = compositeType.getParents() + .getImmediate() + .stream() + .anyMatch(s -> s instanceof DictionarySchema); + + ParentSchemaInfo parentSchemaInfo = getParentSchemaInfo(compositeType); + if (parentSchemaInfo.hasParentSchema()) { + parentsNeedFlatten = parentSchemaInfo.getFlattenedParentSchemas(); + + ClassType parentType = objectMapper.map(parentSchemaInfo.getParentSchema()); + parentModelName = parentType.getName(); + modelImports.add(parentType.getPackage() + "." + parentModelName); + } + } + builder.parentModelName(parentModelName); + + List compositeTypeProperties = compositeType.getProperties() + .stream() + .filter(p -> !p.isIsDiscriminator()) + .collect(Collectors.toList()); + if (!parentsNeedFlatten.isEmpty()) { + // Take properties from base class of multiple inheritance as properties of this class. + for (ObjectSchema parent : parentsNeedFlatten) { + compositeTypeProperties.addAll(parent.getProperties() + .stream() + .filter(p -> !p.isIsDiscriminator()) + .collect(Collectors.toList())); + if (parent.getParents() != null) { + compositeTypeProperties.addAll(parent.getParents() + .getAll() + .stream() + .filter(s -> s instanceof ObjectSchema) + .flatMap(s -> ((ObjectSchema) s).getProperties().stream()) + .filter(p -> !p.isIsDiscriminator()) + .collect(Collectors.toList())); + } + } + } + for (Property autoRestProperty : compositeTypeProperties) { + IType propertyType = Mappers.getSchemaMapper().map(autoRestProperty.getSchema()); + if (!autoRestProperty.isRequired()) { + propertyType = propertyType.asNullable(); + } + propertyType.addImportsTo(modelImports, false); + + IType propertyClientType = Mappers.getSchemaMapper().map(autoRestProperty.getSchema()).getClientType(); + propertyClientType.addImportsTo(modelImports, false); + } + + boolean compositeTypeUsedWithXml = SchemaUtil.treatAsXml(compositeType); + if (!compositeTypeProperties.isEmpty()) { + if (compositeTypeUsedWithXml) { + modelImports.add("com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement"); + + if (compositeTypeProperties.stream().anyMatch(p -> p.getSchema() instanceof ArraySchema)) { + modelImports.add(ArrayList.class.getName()); + } + + if (compositeTypeProperties.stream().anyMatch(p -> { + if (p.getSchema().getSerialization() == null + || p.getSchema().getSerialization().getXml() == null) { + return false; + } + + XmlSerializationFormat xmlSchema = p.getSchema().getSerialization().getXml(); + return xmlSchema.isAttribute() || xmlSchema.getNamespace() != null; + })) { + modelImports.add("com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty"); + } + + if (compositeTypeProperties.stream().anyMatch(p -> { + if (p.getSchema().getSerialization() == null + || p.getSchema().getSerialization().getXml() == null) { + return false; + } + + return p.getSchema().getSerialization().getXml().isText(); + })) { + modelImports.add("com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlText"); + } + + if (compositeTypeProperties.stream() + .anyMatch(p -> p.getSchema().getSerialization() == null + || p.getSchema().getSerialization().getXml() == null || !p.getSchema() + .getSerialization() + .getXml() + .isAttribute())) { + modelImports.add("com.fasterxml.jackson.annotation.JsonProperty"); + } + + if (compositeTypeProperties.stream() + .anyMatch(p -> p.getSchema().getSerialization() != null + && p.getSchema().getSerialization().getXml() != null && p.getSchema() + .getSerialization() + .getXml() + .isWrapped())) { + modelImports.add("com.fasterxml.jackson.annotation.JsonCreator"); + } + + } else { + modelImports.add("com.fasterxml.jackson.annotation.JsonProperty"); + } + } + if (hasAdditionalProperties) { + for (Property property : compositeTypeProperties) { + if (property.getLanguage().getJava().getName().equals(PROPERTY_NAME_ADDITIONAL_PROPERTIES)) { + property.getLanguage().getJava().setName(PROPERTY_NAME_ADDITIONAL_PROPERTIES + "Property"); + } + } + } + + String summary = compositeType.getSummary(); + String description = compositeType.getLanguage().getJava() == null + ? null + : compositeType.getLanguage().getJava().getDescription(); + if (CoreUtils.isNullOrEmpty(summary) && CoreUtils.isNullOrEmpty(description)) { + builder.description(String.format("The %s model.", compositeType.getLanguage().getJava().getName())); + } else { + builder.description(SchemaUtil.mergeSummaryWithDescription(summary, description)); + } + + String modelSerializedName = compositeType.getDiscriminatorValue(); + if (modelSerializedName == null && compositeType.getLanguage().getDefault() != null) { + modelSerializedName = compositeType.getLanguage().getDefault().getName(); + } + builder.serializedName(modelSerializedName); + + List derivedTypes = new ArrayList<>(); + boolean hasChildren = compositeType.getChildren() != null + && compositeType.getChildren().getImmediate() != null; + if (hasChildren) { + for (Schema childSchema : compositeType.getChildren().getImmediate()) { + if (childSchema instanceof ObjectSchema) { + ClientModel model = this.map((ObjectSchema) childSchema); + derivedTypes.add(model); + } else { + throw new RuntimeException( + "Wait what? How? Child is not an object but a " + childSchema.getClass() + "?"); + } + } + } + builder.derivedModels(derivedTypes); + + // Only configure XML information if XML is listed as one of the serialization formats in the ObjectSchema. + if (SchemaUtil.treatAsXml(compositeType)) { + boolean hasXmlFormat = compositeType.getSerialization() != null + && compositeType.getSerialization().getXml() != null; + if (hasXmlFormat) { + final XmlSerializationFormat xml = compositeType.getSerialization().getXml(); + String xmlName = CoreUtils.isNullOrEmpty(xml.getName()) ? compositeType.getLanguage() + .getDefault() + .getName() : xml.getName(); + builder.xmlName(xmlName); + builder.xmlNamespace(xml.getNamespace()); + } else { + builder.xmlName(compositeType.getLanguage().getDefault().getName()); + } + } + + List properties = new ArrayList<>(); + + boolean needsFlatten = false; + if (settings.getModelerSettings().isFlattenModel() // enabled by modelerfour + && settings.getClientFlattenAnnotationTarget() == JavaSettings.ClientFlattenAnnotationTarget.TYPE) { + needsFlatten = hasFlattenedProperty(compositeType, parentsNeedFlatten); + } + + String polymorphicDiscriminator = null; + if (isPolymorphic) { + String discriminatorSerializedName = SchemaUtil.getDiscriminatorSerializedName(compositeType); + // Only escape the discriminator if the model will be flattened. + polymorphicDiscriminator = needsFlatten + ? discriminatorSerializedName.replace(".", "\\\\.") + : discriminatorSerializedName; + + final String finalPolymorphicDiscriminator = polymorphicDiscriminator; + ClientModelProperty discriminatorProperty = createDiscriminatorProperty(settings, hasChildren, + compositeType, annotationArgs -> annotationArgs.replace(discriminatorSerializedName, + finalPolymorphicDiscriminator), polymorphicDiscriminator); + + if (discriminatorProperty != null) { + properties.add(discriminatorProperty); + + if (!settings.isStreamStyleSerialization()) { + modelImports.add("com.fasterxml.jackson.annotation.JsonTypeId"); + } + } + + builder.polymorphicDiscriminatorName(polymorphicDiscriminator) + .polymorphicDiscriminator(discriminatorProperty); + } + + builder.needsFlatten(needsFlatten); + builder.imports(new ArrayList<>(modelImports)); + + final boolean mutablePropertyAsOptional = usages.contains(ImplementationDetails.Usage.JSON_MERGE_PATCH) + && settings.isStreamStyleSerialization(); + List propertyReferences = new ArrayList<>(); + for (Property property : compositeTypeProperties) { + ClientModelProperty modelProperty = Mappers.getModelPropertyMapper() + .map(property, mutablePropertyAsOptional); + if (Objects.equals(polymorphicDiscriminator, modelProperty.getSerializedName())) { + // Discriminator is defined both as the discriminator and a property in the model. + // Make the discriminator property required if the property is required. But don't add the property + // again as it would result in two properties for the same serialized name. + properties.get(0).setRequired(modelProperty.isRequired()); + + // If the model has children models, copy the requirement logic to the children models with the same + // polymorphic discriminator. + // Passing from the parent is performed instead of children checking the parent as children will + // complete mapping before the parent. So, the parent is last to complete and the children models + // will be fully defined. If the inverse was done, children checking the parent, the parent would + // be null or an infinite loop would happen. + if (!CoreUtils.isNullOrEmpty(derivedTypes)) { + for (ClientModel derivedType : derivedTypes) { + if (Objects.equals(derivedType.getPolymorphicDiscriminator().getSerializedName(), + polymorphicDiscriminator)) { + derivedType.getPolymorphicDiscriminator().setRequired(modelProperty.isRequired()); + } + } + } + + continue; + } + + properties.add(modelProperty); + + if (modelProperty.getClientFlatten() + && settings.getClientFlattenAnnotationTarget() == JavaSettings.ClientFlattenAnnotationTarget.NONE) { + propertyReferences.addAll( + collectPropertiesFromFlattenedModel(compositeType, property, modelProperty, + propertyReferences)); + } + } + + if (hasAdditionalProperties) { + DictionarySchema schema = (DictionarySchema) compositeType.getParents() + .getImmediate() + .stream() + .filter(s -> s instanceof DictionarySchema) + .findFirst() + .orElseThrow(() -> new IllegalStateException( + "Unable to find DictionarySchema for additional properties property.")); + Property additionalProperties = new Property(); + additionalProperties.setReadOnly(false); + additionalProperties.setSchema(schema); + additionalProperties.setSerializedName(""); + + additionalProperties.setLanguage(new Languages()); + additionalProperties.getLanguage().setJava(new Language()); + additionalProperties.getLanguage().getJava().setName(PROPERTY_NAME_ADDITIONAL_PROPERTIES); + String additionalPropertiesDescription = schema.getLanguage().getJava().getDescription(); + if (CoreUtils.isNullOrEmpty(additionalPropertiesDescription)) { + additionalPropertiesDescription = "Additional properties"; + } + additionalProperties.getLanguage().getJava().setDescription(additionalPropertiesDescription); + + properties.add(Mappers.getModelPropertyMapper().map(additionalProperties)); + } + + builder.properties(properties); + builder.propertyReferences(propertyReferences); + builder.crossLanguageDefinitionId(compositeType.getCrossLanguageDefinitionId()); + + result = builder.build(); + + if (isPolymorphic && !CoreUtils.isNullOrEmpty(derivedTypes)) { + // Walk the polymorphic hierarchy finding places where the parent model and child model have different + // polymorphic discriminators. When this case is found add the parent polymorphic discriminator as a parent + // polymorphic discriminator to the child model. This is necessary to ensure that the child model generates + // the correct serialization in multi-level polymorphic structures. + for (ClientModel derivedType : derivedTypes) { + if (!Objects.equals(polymorphicDiscriminator, derivedType.getPolymorphicDiscriminatorName())) { + ClientModelProperty parentDiscriminator = result.getPolymorphicDiscriminator() + .newBuilder() + .defaultValue(result.getPolymorphicDiscriminator().getClientType() + .defaultValueExpression(derivedType.getSerializedName())) + .build(); + + passPolymorphicDiscriminatorToChildren(parentDiscriminator, derivedType); + } + } + } + + serviceModels.addModel(result); + } + + return result; + } + + private static void passPolymorphicDiscriminatorToChildren(ClientModelProperty parentDiscriminator, + ClientModel child) { + // Due to the execution order of ModelMapper, where children models complete mapping before the parent model, + // the parent polymorphic discriminator needs to be added at index 0. Reason, given an example where there are + // three models, where model #1 is the root parent with discriminator type, model #2 is a child of model #2 with + // discriminator kind, and model #3 is a child of model #3 with discriminator form. The order if this running + // will have model #2 add its discriminator to model #3 before model #1 runs adding its discriminator to #2 and + // #3. We want #3 to have the ordering of [type, kind], to represent the ordering of the parent models. + child.getParentPolymorphicDiscriminators().add(0, parentDiscriminator); + + for (ClientModel derived : child.getDerivedModels()) { + passPolymorphicDiscriminatorToChildren(parentDiscriminator, derived); + } + } + + private static class ParentSchemaInfo { + private final ObjectSchema parentSchema; + private final List flattenedParentSchemas; + + public ParentSchemaInfo(ObjectSchema parentSchema, List flattenedParentSchemas) { + this.parentSchema = parentSchema; + this.flattenedParentSchemas = flattenedParentSchemas; + } + + public boolean hasParentSchema() { + return parentSchema != null; + } + + /** + * @return the single parent schema to keep. + */ + public ObjectSchema getParentSchema() { + return parentSchema; + } + + /** + * @return the list of parent schemas to flatten into this schema. + */ + public List getFlattenedParentSchemas() { + return flattenedParentSchemas; + } + } + + /** + * Separate all immediate parents into: one to keep, and the rest to flatten. + *

+ * If schema is not polymorphic, keep the first parent of type ObjectSchema. If schema is polymorphic (but not the + * supertype), keep the parent in polymorphic hierarchy. + * + * @param compositeType the object schema + * @return the info on parent schema. + */ + private ParentSchemaInfo getParentSchemaInfo(ObjectSchema compositeType) { + ObjectSchema parentSchema = null; + List flattenedParentSchemas = new ArrayList<>(); + + if (compositeType.getDiscriminatorValue() != null) { + for (Schema parent : compositeType.getParents().getImmediate()) { + if (parent instanceof ObjectSchema) { + ObjectSchema parentAsObjectSchema = (ObjectSchema) parent; + if (parentSchema == null + && (parentAsObjectSchema.getDiscriminatorValue() != null || parentAsObjectSchema.getDiscriminator() != null)) { + parentSchema = parentAsObjectSchema; + } else { + flattenedParentSchemas.add((ObjectSchema) parent); + } + } + } + + if (parentSchema == null) { + // failed to find a parent being polymorphic + // clean up and fallback to flow without polymorphic + flattenedParentSchemas.clear(); + } + } + + if (parentSchema == null) { + for (Schema parent : compositeType.getParents().getImmediate()) { + if (parent instanceof ObjectSchema) { + if (parentSchema == null) { + parentSchema = (ObjectSchema) parent; + } else { + flattenedParentSchemas.add((ObjectSchema) parent); + } + } + } + } + + return new ParentSchemaInfo(parentSchema, flattenedParentSchemas); + } + + /** + * Creates a {@link ClientModelProperty} for the discriminator type in a polymorphic Swagger model. + *

+ * By default if the discriminator isn't passed to child type deserialization or if the type isn't a terminal, or + * leaf type, in the hierarchy no {@link ClientModelProperty} will be created. + *

+ * This method serves as an extension point for Fluent generator. + * + * @param settings The Autorest generation settings, used to determine whether a discriminator property should be + * created. + * @param hasChildren Flag indicating whether the Swagger model has children models. + * @param compositeType The Swagger schema of the model. + * @param annotationArgumentsMapper Function that maps the {@link ClientModelProperty#getAnnotationArguments()} of + * the {@code compositeType} into the attributes of {@code JsonProperty} for the discriminator property. + * @param serializedName The serialized name of the discriminator property. + * @return A {@link ClientModelProperty} that is the discriminator field property, or null if either the + * discriminator shouldn't be made into a property or if the model isn't a terminal, or leaf, type. + */ + protected ClientModelProperty createDiscriminatorProperty(JavaSettings settings, boolean hasChildren, + ObjectSchema compositeType, Function annotationArgumentsMapper, String serializedName) { + ClientModelProperty discriminatorProperty = Mappers.getModelPropertyMapper() + .map(SchemaUtil.getDiscriminatorProperty(compositeType)); + + return discriminatorProperty.newBuilder() + .annotationArguments(annotationArgumentsMapper.apply(discriminatorProperty.getAnnotationArguments())) + .serializedName(serializedName) + .defaultValue(discriminatorProperty.getClientType().defaultValueExpression(compositeType.getDiscriminatorValue())) + .readOnly(true) + .required(false) + .polymorphicDiscriminator(true) + .build(); + } + + protected ClientModel.Builder createModelBuilder() { + return new ClientModel.Builder(); + } + + /** + * Collect property reference from flattened model. + * + * @param compositeType the model + * @param property the property of the model that specified to be flattened + * @param modelProperty the ClientModelProperty of the property + * @param existingPropertyReferences the list of existing property references from previously flattened models, for + * disambiguate purpose + * @return the list of property references from flattened model. + */ + private List collectPropertiesFromFlattenedModel( + ObjectSchema compositeType, Property property, ClientModelProperty modelProperty, + List existingPropertyReferences) { + + List propertyReferences = new ArrayList<>(); + ObjectSchema targetModelSchema = (ObjectSchema) property.getSchema(); + String originalFlattenedPropertyName = property.getLanguage().getJava().getName(); // not modelProperty.getName() + ClientModel targetModel = this.map(targetModelSchema); + if (targetModel != null && targetModel.getProperties() != null) { + // gather this type and its parents + List objectSchemaAndParents = new ArrayList<>(); + objectSchemaAndParents.add(compositeType); + if (compositeType.getParents() != null && compositeType.getParents().getAll() != null) { + objectSchemaAndParents.addAll( + compositeType.getParents().getAll().stream() + .filter(p -> p instanceof ObjectSchema) + .map(p -> (ObjectSchema) p) + .collect(Collectors.toList())); + } + // gather property names for disambiguate + Set propertyNames = objectSchemaAndParents.stream() + .flatMap(o -> o.getProperties().stream()) + .filter(p -> p.getExtensions() == null || !p.getExtensions().isXmsClientFlatten()) + .map(p -> p.getLanguage().getJava().getName()) + .collect(Collectors.toSet()); + propertyNames.addAll(existingPropertyReferences.stream().map(ClientModelPropertyReference::getName).collect(Collectors.toList())); + // additional properties + if (compositeType.getParents() != null && compositeType.getParents().getAll() != null + && compositeType.getParents().getAll().stream().anyMatch(s -> s instanceof DictionarySchema)) { + propertyNames.add(PROPERTY_NAME_ADDITIONAL_PROPERTIES); + } + + Set referencePropertyNames = new HashSet<>(); + // properties from the target model + for (ClientModelProperty property1 : targetModel.getProperties()) { + if (!property1.getClientFlatten() && !property1.isAdditionalProperties()) { + String name = disambiguatePropertyNameOfFlattenedSchema(propertyNames, originalFlattenedPropertyName, property1.getName()); + if (!referencePropertyNames.contains(name)) { + propertyReferences.add(ClientModelPropertyReference.ofFlattenProperty(modelProperty, targetModel, property1, name)); + referencePropertyNames.add(name); + } + } + } + for (ClientModelPropertyReference property1 : targetModel.getPropertyReferences()) { + if (property1.isFromFlattenedProperty()) { + String name = disambiguatePropertyNameOfFlattenedSchema(propertyNames, originalFlattenedPropertyName, property1.getName()); + if (!referencePropertyNames.contains(name)) { + propertyReferences.add(ClientModelPropertyReference.ofFlattenProperty(modelProperty, targetModel, property1, name)); + referencePropertyNames.add(name); + } + } + } + // properties from the parents of the target model + if (targetModelSchema.getParents() != null && !CoreUtils.isNullOrEmpty(targetModelSchema.getParents().getAll())) { + // take parent of the target model, as rest parents (if any) is already flattened into the target model + ParentSchemaInfo parentSchemaInfo = getParentSchemaInfo(targetModelSchema); + if (parentSchemaInfo.hasParentSchema()) { + ObjectSchema parentSchema = parentSchemaInfo.getParentSchema(); + Stream.concat( + Stream.of(parentSchema), + parentSchema.getParents() != null && parentSchema.getParents().getAll() != null + ? parentSchema.getParents().getAll().stream() + : Stream.empty()) + .filter(o -> o instanceof ObjectSchema) + .map(o -> (ObjectSchema) o) + .forEach(objectSchema1 -> objectSchema1.getProperties().stream() + .filter(p -> !p.isIsDiscriminator()) + .forEach(property1 -> { + if (property1.getExtensions() == null || !property1.getExtensions().isXmsClientFlatten()) { + ClientModelProperty referenceProperty1 = Mappers.getModelPropertyMapper().map(property1); + String name = disambiguatePropertyNameOfFlattenedSchema(propertyNames, originalFlattenedPropertyName, referenceProperty1.getName()); + if (!referencePropertyNames.contains(name)) { + propertyReferences.add(ClientModelPropertyReference.ofFlattenProperty(modelProperty, targetModel, referenceProperty1, name)); + referencePropertyNames.add(name); + } + } else { + // nested flattened model + if (property1.getSchema() instanceof ObjectSchema && !isPlainObject((ObjectSchema) property.getSchema())) { + ClientModelProperty modelProperty1 = Mappers.getModelPropertyMapper().map(property1); + List nestedReferences = collectPropertiesFromFlattenedModel( + objectSchema1, property1, modelProperty1, existingPropertyReferences); + nestedReferences.forEach(property2 -> { + String name = disambiguatePropertyNameOfFlattenedSchema(propertyNames, originalFlattenedPropertyName, property2.getName()); + if (!referencePropertyNames.contains(name)) { + propertyReferences.add(ClientModelPropertyReference.ofFlattenProperty(modelProperty, targetModel, property2, name)); + referencePropertyNames.add(name); + } + }); + } + } + })); + } + } + } + return propertyReferences; + } + + private static boolean hasFlattenedProperty(ObjectSchema compositeType, Collection parentsNeedFlatten) { + boolean ret = compositeType.getProperties().stream() + .anyMatch(p -> p.getFlattenedNames() != null && !p.getFlattenedNames().isEmpty()); + if (!ret && !parentsNeedFlatten.isEmpty()) { + // Check properties from base class of multiple inheritance as properties of this class. + ret = parentsNeedFlatten.stream() + .flatMap(s -> (s.getParents() != null && s.getParents().getAll() != null) ? Stream.concat(Stream.of(s), s.getParents().getAll().stream()) : Stream.of(s)) + .filter(s -> s instanceof ObjectSchema) + .flatMap(s -> ((ObjectSchema) s).getProperties().stream()) + .anyMatch(p -> p.getFlattenedNames() != null && !p.getFlattenedNames().isEmpty()); + } + return ret; + } + + /** + * Extension for predefined types in azure-core. + * + * @param compositeType object type + * @return Whether the type is predefined. + */ + protected boolean isPredefinedModel(ClassType compositeType) { + if (JavaSettings.getInstance().isDataPlaneClient()) { + // see ObjectMapper.mapPredefinedModel + // this might be too simplified, and Android might require a different implementation + return compositeType.getPackage().startsWith(ExternalPackage.CORE.getPackageName() + "."); + } else { + return false; + } + } + + private static String disambiguatePropertyNameOfFlattenedSchema(Set propertyNames, String originalFlattenedPropertyName, String propertyName) { + String ret = propertyName; + if (propertyNames.contains(propertyName)) { + // follow pattern from m4 + ret = propertyName + CodeNamer.toPascalCase(originalFlattenedPropertyName) + CodeNamer.toPascalCase(propertyName); + } + return ret; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/mapper/ModelPropertyMapper.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/mapper/ModelPropertyMapper.java new file mode 100644 index 0000000000..c4a4079739 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/mapper/ModelPropertyMapper.java @@ -0,0 +1,224 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.mapper; + +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.ArraySchema; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.ConstantSchema; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.ObjectSchema; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Property; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Schema; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.XmlSerializationFormat; +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClassType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientModelProperty; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.EnumType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.IType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.PrimitiveType; +import com.microsoft.typespec.http.client.generator.core.util.CodeNamer; +import com.microsoft.typespec.http.client.generator.core.util.SchemaUtil; +import com.azure.core.util.CoreUtils; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.stream.Collectors; + +public class ModelPropertyMapper implements IMapper, NeedsPlainObjectCheck { + private static final ModelPropertyMapper INSTANCE = new ModelPropertyMapper(); + + public static ModelPropertyMapper getInstance() { + return INSTANCE; + } + + protected ModelPropertyMapper() { + } + + @Override + public ClientModelProperty map(Property property) { + return map(property, false); + } + + /** + * ClientModelProperty + * + * @param property the property + * @param mutableAsOptional make mutable property optional, for JSON Merge Patch + * @return ClientModelProperty + */ + public ClientModelProperty map(Property property, boolean mutableAsOptional) { + JavaSettings settings = JavaSettings.getInstance(); + + ClientModelProperty.Builder builder = new ClientModelProperty.Builder() + .name(property.getLanguage().getJava().getName()) + .required(property.isRequired()) + .readOnly(property.isReadOnly()); + + if (mutableAsOptional && !property.isReadOnly() && !property.isIsDiscriminator()) { + builder.required(false); + builder.requiredForCreate(property.isRequired()); + } + + String description; + String summaryInProperty = property.getSummary(); + String descriptionInProperty = property.getLanguage().getJava() == null ? null : property.getLanguage().getJava().getDescription(); + if (CoreUtils.isNullOrEmpty(summaryInProperty) && CoreUtils.isNullOrEmpty(descriptionInProperty)) { + description = String.format("The %s property.", property.getSerializedName()); + } else { + description = SchemaUtil.mergeSummaryWithDescription(summaryInProperty, descriptionInProperty); + } + builder.description(description); + + boolean flattened = false; + if (settings.getModelerSettings().isFlattenModel()) { // enabled by modelerfour + if (settings.getClientFlattenAnnotationTarget() == JavaSettings.ClientFlattenAnnotationTarget.TYPE) { + if (property.getParentSchema() != null) { + flattened = property.getParentSchema().getProperties().stream() + .anyMatch(p -> !CoreUtils.isNullOrEmpty(p.getFlattenedNames())); + if (!flattened) { + String discriminatorSerializedName = SchemaUtil.getDiscriminatorSerializedName(property.getParentSchema()); + flattened = discriminatorSerializedName.contains("."); + } + } else { + flattened = !CoreUtils.isNullOrEmpty(property.getFlattenedNames()); + } + } else if (settings.getClientFlattenAnnotationTarget() == JavaSettings.ClientFlattenAnnotationTarget.FIELD) { + flattened = !CoreUtils.isNullOrEmpty(property.getFlattenedNames()); + } + } + builder.needsFlatten(flattened); + + if (property.getExtensions() != null && property.getExtensions().isXmsClientFlatten() + // avoid non-object schema or a plain object schema without any properties + && property.getSchema() instanceof ObjectSchema && !isPlainObject((ObjectSchema) property.getSchema()) + && settings.getClientFlattenAnnotationTarget() == JavaSettings.ClientFlattenAnnotationTarget.NONE) { + // avoid naming conflict + builder.name("inner" + CodeNamer.toPascalCase(property.getLanguage().getJava().getName())); + builder.clientFlatten(true); + } + + StringBuilder serializedName = new StringBuilder(); + if (property.getFlattenedNames() != null && !property.getFlattenedNames().isEmpty()) { + for (String flattenedName : property.getFlattenedNames()) { + serializedName.append(flattenedName.replace(".", "\\\\.")).append("."); + } + serializedName.deleteCharAt(serializedName.length() - 1); + } else if (flattened) { + serializedName.append(property.getSerializedName().replace(".", "\\\\.")); + } else { + serializedName.append(property.getSerializedName()); + } + builder.serializedName(serializedName.toString()); + if (serializedName.toString().isEmpty() && "additionalProperties".equals(property.getLanguage().getJava().getName())) { + builder.additionalProperties(true); + } + + boolean propertyIsSecret = false; + if (property.getExtensions() != null) { + if (property.getExtensions().getXmsSecret() != null) { + propertyIsSecret = property.getExtensions().getXmsSecret(); + } + } + + XmlSerializationFormat xmlSerializationFormat = null; + if (property.getSchema().getSerialization() != null) { + xmlSerializationFormat = property.getSchema().getSerialization().getXml(); + } + + String xmlName = null; + String xmlNamespace = null; + boolean isXmlWrapper = false; + boolean isXmlAttribute = false; + boolean isXmlText = false; + String xmlPrefix = null; + if (xmlSerializationFormat != null) { + isXmlWrapper = xmlSerializationFormat.isWrapped(); + isXmlAttribute = xmlSerializationFormat.isAttribute(); + xmlName = xmlSerializationFormat.getName(); + xmlNamespace = xmlSerializationFormat.getNamespace(); + isXmlText = xmlSerializationFormat.isText(); + xmlPrefix = xmlSerializationFormat.getPrefix(); + } + + final String xmlParamName = xmlName == null ? serializedName.toString() : xmlName; + builder.xmlName(xmlParamName) + .xmlWrapper(isXmlWrapper) + .xmlAttribute(isXmlAttribute) + .xmlNamespace(xmlNamespace) + .xmlText(isXmlText) + .xmlPrefix(xmlPrefix); + + List annotationArgumentList = new ArrayList() {{ + add(String.format("value = \"%s\"", xmlParamName)); + }}; + + if (property.isRequired() && !propertyIsSecret && !settings.isDisableRequiredJsonAnnotation()) { + annotationArgumentList.add("required = true"); + } + + // Though this looks odd to add WRITE_ONLY access when the property is marked as read-only it is the correct + // behavior. The Swagger definition for read-only is from the perspective of the service which correlates to + // write-only behavior in an SDK. + if (property.isReadOnly()) { + annotationArgumentList.add("access = JsonProperty.Access.WRITE_ONLY"); + } + builder.annotationArguments(String.join(", ", annotationArgumentList)); + + String headerCollectionPrefix = null; + if (property.getExtensions() != null && property.getExtensions().getXmsHeaderCollectionPrefix() != null) { + headerCollectionPrefix = property.getExtensions().getXmsHeaderCollectionPrefix(); + } + builder.headerCollectionPrefix(headerCollectionPrefix); + + IType propertyWireType = Mappers.getSchemaMapper().map(property.getSchema()); + if (property.isNullable() || !property.isRequired()) { + propertyWireType = propertyWireType.asNullable(); + } + // Invariant: clientType == wireType.getClientType() + IType propertyClientType = propertyWireType.getClientType(); + builder.wireType(propertyWireType).clientType(propertyClientType); + + Schema autoRestPropertyModelType = property.getSchema(); + if (autoRestPropertyModelType instanceof ArraySchema) { + ArraySchema sequence = (ArraySchema) autoRestPropertyModelType; + if (sequence.getElementType().getSerialization() != null + && sequence.getElementType().getSerialization().getXml() != null + && sequence.getElementType().getSerialization().getXml().getName() != null) { + builder.xmlListElementName(sequence.getElementType().getSerialization().getXml().getName()); + builder.xmlListElementNamespace(sequence.getElementType().getSerialization().getXml().getNamespace()); + builder.xmlListElementPrefix(sequence.getElementType().getSerialization().getXml().getPrefix()); + } else { + builder.xmlListElementName(sequence.getElementType().getLanguage().getDefault().getName()); + builder.xmlListElementNamespace(sequence.getElementType().getLanguage().getDefault().getNamespace()); + } + } + + if (property.getSchema() instanceof ConstantSchema) { + Object objValue = ((ConstantSchema) property.getSchema()).getValue().getValue(); + builder.constant(true); + builder.defaultValue(objValue == null ? null : propertyClientType.defaultValueExpression(String.valueOf(objValue))); + } + + // x-ms-mutability + if (property.getExtensions() != null) { + List xmsMutability = property.getExtensions().getXmsMutability(); + if (xmsMutability != null) { + List mutabilities = xmsMutability.stream() + .map(m -> ClientModelProperty.Mutability.valueOf(m.toUpperCase(Locale.ROOT))) + .collect(Collectors.toList()); + builder.mutabilities(mutabilities); + } + } + + // handle x-ms-client-default for primitive type, enum, boxed type and string + if (property.getClientDefaultValue() != null && + (propertyWireType instanceof PrimitiveType || propertyWireType instanceof EnumType || + (propertyWireType instanceof ClassType && ((ClassType) propertyWireType).isBoxedType()) || + propertyWireType.equals(ClassType.STRING))) { + String autoRestPropertyDefaultValueExpression = propertyWireType.defaultValueExpression(property.getClientDefaultValue()); + builder.defaultValue(autoRestPropertyDefaultValueExpression); + } + + return builder.build(); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/mapper/NeedsPlainObjectCheck.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/mapper/NeedsPlainObjectCheck.java new file mode 100644 index 0000000000..0ddf8faf3d --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/mapper/NeedsPlainObjectCheck.java @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.mapper; + +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.ObjectSchema; +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; + +public interface NeedsPlainObjectCheck { + /** + * Check that the type can be regarded as a plain java.lang.Object. + * + * @param compositeType The type to check. + */ + default boolean isPlainObject(ObjectSchema compositeType) { + return !JavaSettings.getInstance().isDataPlaneClient() + && compositeType.getProperties().isEmpty() && compositeType.getDiscriminator() == null + && compositeType.getParents() == null && compositeType.getChildren() == null + && (compositeType.getExtensions() == null || compositeType.getExtensions().getXmsEnum() == null); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/mapper/ObjectMapper.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/mapper/ObjectMapper.java new file mode 100644 index 0000000000..6263a2c291 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/mapper/ObjectMapper.java @@ -0,0 +1,116 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.mapper; + +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.ObjectSchema; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.SchemaContext; +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClassType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.IType; +import com.microsoft.typespec.http.client.generator.core.util.SchemaUtil; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class ObjectMapper implements IMapper, NeedsPlainObjectCheck { + private static final ObjectMapper INSTANCE = new ObjectMapper(); + Map parsed = new ConcurrentHashMap<>(); + + protected ObjectMapper() { + } + + public static ObjectMapper getInstance() { + return INSTANCE; + } + + @Override + public ClassType map(ObjectSchema compositeType) { + if (compositeType == null) { + return null; + } + + return parsed.computeIfAbsent(compositeType, this::createClassType); + } + + private ClassType createClassType(ObjectSchema compositeType) { + JavaSettings settings = JavaSettings.getInstance(); + + ClassType result = mapPredefinedModel(compositeType); + if (result != null) { + return result; + } + + if (isPlainObject(compositeType)) { + return ClassType.OBJECT; + } + + String classPackage; + String className = compositeType.getLanguage().getJava().getName(); + if (settings.isCustomType(compositeType.getLanguage().getJava().getName())) { + classPackage = settings.getPackage(settings.getCustomTypesSubpackage()); + } else if (settings.isFluent() && isInnerModel(compositeType)) { + className += "Inner"; + classPackage = settings.getPackage(settings.getFluentModelsSubpackage()); + } else if (settings.isFluent() && compositeType.isFlattenedSchema()) { + // put class of flattened type to implementation package + classPackage = settings.getPackage(settings.getFluentModelsSubpackage()); + } else if (settings.isDataPlaneClient() && isInternalModel(compositeType)) { + // internal type is not exposed to user + classPackage = settings.getPackage(settings.getImplementationSubpackage(), settings.getModelsSubpackage()); + } else if (isPageModel(compositeType)) { + // put class of Page<> type to implementation package + // for DPG from TypeSpec, these are not generated to class + + // this would not affect mgmt from Swagger, as the "usage" from m4 does not have this information. + + classPackage = settings.getPackage(settings.getImplementationSubpackage(), settings.getModelsSubpackage()); + } else { + classPackage = settings.getPackage(settings.getModelsSubpackage()); + } + + return new ClassType.Builder() + .packageName(classPackage) + .name(className) + .extensions(compositeType.getExtensions()) + .usedInXml(SchemaUtil.treatAsXml(compositeType)) + .build(); + } + + /** + * Extension for predefined types in azure-core. + * + * @param compositeType object type + * @return The predefined type. + */ + protected ClassType mapPredefinedModel(ObjectSchema compositeType) { + return SchemaUtil.mapExternalModel(compositeType); + } + + /** + * Extension for Fluent inner model. + * + * @param compositeType object type + * @return whether the type should be treated as inner model + */ + protected boolean isInnerModel(ObjectSchema compositeType) { + return false; + } + + /** + * Extension for Page model. + *

+ * Page model does not need to be exposed to user, as it is internal wire data that will be converted to PagedFlux or PagedIterable. + * Check in TypeSpec. + * + * @param compositeType object type + * @return whether the type is a Page model. + */ + private static boolean isPageModel(ObjectSchema compositeType) { + return compositeType.getUsage() != null && compositeType.getUsage().contains(SchemaContext.PAGED); + } + + private static boolean isInternalModel(ObjectSchema compositeType) { + return compositeType.getUsage() != null && compositeType.getUsage().contains(SchemaContext.INTERNAL); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/mapper/PomMapper.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/mapper/PomMapper.java new file mode 100644 index 0000000000..d6d93ea21e --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/mapper/PomMapper.java @@ -0,0 +1,115 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.mapper; + +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.Pom; +import com.microsoft.typespec.http.client.generator.core.model.projectmodel.Project; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +public class PomMapper implements IMapper { + + protected static final String TEST_SUFFIX = ":test"; + + @Override + public Pom map(Project project) { + if (!JavaSettings.getInstance().isBranded()) { + return createGenericPom(project); + } else { + return createAzurePom(project); + } + } + + private Pom createAzurePom(Project project) { + Pom pom = new Pom(); + pom.setGroupId(project.getGroupId()); + pom.setArtifactId(project.getArtifactId()); + pom.setVersion(project.getVersion()); + + pom.setServiceName(project.getServiceName()); + pom.setServiceDescription(project.getServiceDescriptionForPom()); + + Set addedDependencyPrefixes = new HashSet<>(); + List dependencyIdentifiers = new ArrayList<>(); + if (JavaSettings.getInstance().isStreamStyleSerialization()) { + addDependencyIdentifier(dependencyIdentifiers, addedDependencyPrefixes, + Project.Dependency.AZURE_JSON, false); + addDependencyIdentifier(dependencyIdentifiers, addedDependencyPrefixes, + Project.Dependency.AZURE_XML, false); + } + addDependencyIdentifier(dependencyIdentifiers, addedDependencyPrefixes, + Project.Dependency.AZURE_CORE, false); + addDependencyIdentifier(dependencyIdentifiers, addedDependencyPrefixes, + Project.Dependency.AZURE_CORE_HTTP_NETTY, false); + addDependencyIdentifier(dependencyIdentifiers, addedDependencyPrefixes, + Project.Dependency.JUNIT_JUPITER_API, true); + addDependencyIdentifier(dependencyIdentifiers, addedDependencyPrefixes, + Project.Dependency.JUNIT_JUPITER_ENGINE, true); + addDependencyIdentifier(dependencyIdentifiers, addedDependencyPrefixes, + Project.Dependency.AZURE_CORE_TEST, true); + addDependencyIdentifier(dependencyIdentifiers, addedDependencyPrefixes, + Project.Dependency.AZURE_IDENTITY, true); + addDependencyIdentifier(dependencyIdentifiers, addedDependencyPrefixes, + Project.Dependency.SLF4J_SIMPLE, true); + + // merge dependencies in POM and dependencies added above + dependencyIdentifiers.addAll(project.getPomDependencyIdentifiers().stream() + .filter(dependencyIdentifier -> addedDependencyPrefixes.stream().noneMatch(dependencyIdentifier::startsWith)) + .collect(Collectors.toList())); + + pom.setDependencyIdentifiers(dependencyIdentifiers); + + if (project.isIntegratedWithSdk()) { + pom.setParentIdentifier(Project.Dependency.AZURE_CLIENT_SDK_PARENT.getDependencyIdentifier()); + pom.setParentRelativePath("../../parents/azure-client-sdk-parent"); + } + + pom.setRequireCompilerPlugins(!project.isIntegratedWithSdk()); + + return pom; + } + + private Pom createGenericPom(Project project) { + Pom pom = new Pom(); + pom.setGroupId(project.getGroupId()); + pom.setArtifactId(project.getArtifactId()); + pom.setVersion(project.getVersion()); + + pom.setServiceName(project.getServiceName()); + pom.setServiceDescription(project.getServiceDescriptionForPom()); + Map repositories = new HashMap<>(); + repositories.put("clientcore", "https://clientcore.blob.core.windows.net/artifacts"); + pom.setRepositories(repositories); + + Set addedDependencyPrefixes = new HashSet<>(); + List dependencyIdentifiers = new ArrayList<>(); + // for generic pom, stream style is always true + addDependencyIdentifier(dependencyIdentifiers, addedDependencyPrefixes, + Project.Dependency.CLIENTCORE, false); + addDependencyIdentifier(dependencyIdentifiers, addedDependencyPrefixes, + Project.Dependency.CLIENTCORE_JSON, false); + + // merge dependencies in POM and dependencies added above + dependencyIdentifiers.addAll(project.getPomDependencyIdentifiers().stream() + .filter(dependencyIdentifier -> addedDependencyPrefixes.stream().noneMatch(dependencyIdentifier::startsWith)) + .collect(Collectors.toList())); + + pom.setDependencyIdentifiers(dependencyIdentifiers); + pom.setRequireCompilerPlugins(true); + return pom; + } + + protected static void addDependencyIdentifier(List dependencyIdentifiers, Set prefixes, + Project.Dependency dependency, boolean isTestScope) { + prefixes.add(dependency.getGroupId() + ":" + dependency.getArtifactId() + ":"); + dependencyIdentifiers.add(dependency.getDependencyIdentifier() + (isTestScope ? TEST_SUFFIX : "")); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/mapper/PrimitiveMapper.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/mapper/PrimitiveMapper.java new file mode 100644 index 0000000000..06362527b4 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/mapper/PrimitiveMapper.java @@ -0,0 +1,118 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.mapper; + +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.ByteArraySchema; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.DateTimeSchema; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.DurationSchema; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.NumberSchema; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.PrimitiveSchema; +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ArrayType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClassType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.IType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.PrimitiveType; + +import java.util.HashMap; +import java.util.Map; + +public class PrimitiveMapper implements IMapper { + private static final PrimitiveMapper INSTANCE = new PrimitiveMapper(); + protected Map parsed = new HashMap<>(); + + protected PrimitiveMapper() { + } + + public static PrimitiveMapper getInstance() { + return INSTANCE; + } + + @Override + public IType map(PrimitiveSchema primaryType) { + if (primaryType == null) { + return null; + } + + return parsed.computeIfAbsent(primaryType, this::createPrimitiveType); + } + + /** + * Extension. + * + * @param primaryType the primitive schema. + * @return the client model type. + */ + protected IType createPrimitiveType(PrimitiveSchema primaryType) { + boolean isLowLevelClient = JavaSettings.getInstance().isDataPlaneClient(); + boolean urlAsString = JavaSettings.getInstance().urlAsString(); + boolean uuidAsString = JavaSettings.getInstance().uuidAsString(); + + switch (primaryType.getType()) { +// case null: +// iType = PrimitiveType.Void; +// break; + case BOOLEAN: return PrimitiveType.BOOLEAN; + case BYTE_ARRAY: + ByteArraySchema byteArraySchema = (ByteArraySchema) primaryType; + return (byteArraySchema.getFormat() == ByteArraySchema.Format.BASE_64_URL) + ? ClassType.BASE_64_URL + : ArrayType.BYTE_ARRAY; + case CHAR: return PrimitiveType.CHAR; + case DATE: return isLowLevelClient ? ClassType.STRING : ClassType.LOCAL_DATE; + case DATE_TIME: + DateTimeSchema dateTimeSchema = (DateTimeSchema) primaryType; + return (dateTimeSchema.getFormat() == DateTimeSchema.Format.DATE_TIME_RFC_1123) + ? ClassType.DATE_TIME_RFC_1123 + : ClassType.DATE_TIME; + case TIME: +// TimeSchema timeSchema = (TimeSchema) primaryType; + return ClassType.STRING; +// case KnownPrimaryType.DateTimeRfc1123: +// iType = ClassType.DateTimeRfc1123; +// break; + case NUMBER: + NumberSchema numberSchema = (NumberSchema) primaryType; + if (numberSchema.getPrecision() == 64) { + return PrimitiveType.DOUBLE; + } else if (numberSchema.getPrecision() == 32) { + return PrimitiveType.FLOAT; + } else { + return ClassType.BIG_DECIMAL; + } + case INTEGER: + NumberSchema intSchema = (NumberSchema) primaryType; + return (intSchema.getPrecision() == 64) + ? PrimitiveType.LONG + : PrimitiveType.INT; +// case KnownPrimaryType.Long: +// iType = PrimitiveType.Long; +// break; +// case KnownPrimaryType.Stream: +// iType = GenericType.FluxByteBuffer; +// break; + case STRING: return ClassType.STRING; + case ARM_ID: return ClassType.STRING; + case URI: return isLowLevelClient || urlAsString ? ClassType.STRING : ClassType.URL; + case DURATION: + DurationSchema durationSchema = (DurationSchema) primaryType; + IType durationType = ClassType.DURATION; + if (durationSchema.getFormat() != null) { + switch (durationSchema.getFormat()) { + case SECONDS_INTEGER: + return PrimitiveType.DURATION_LONG; + case SECONDS_NUMBER: + return PrimitiveType.DURATION_DOUBLE; + } + } + return durationType; + case UNIXTIME: return isLowLevelClient ? PrimitiveType.LONG : PrimitiveType.UNIX_TIME_LONG; + case UUID: return isLowLevelClient || uuidAsString ? ClassType.STRING : ClassType.UUID; + case OBJECT: return ClassType.OBJECT; + case CREDENTIAL: return ClassType.TOKEN_CREDENTIAL; + default: + throw new UnsupportedOperationException(String.format("Unrecognized AutoRest Primitive Type: %s", + primaryType.getType())); + } + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/mapper/ProxyMethodExampleMapper.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/mapper/ProxyMethodExampleMapper.java new file mode 100644 index 0000000000..3a9392c4f0 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/mapper/ProxyMethodExampleMapper.java @@ -0,0 +1,73 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.mapper; + +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ProxyMethodExample; +import com.microsoft.typespec.http.client.generator.core.preprocessor.namer.CodeNamer; +import com.microsoft.typespec.http.client.generator.core.util.XmsExampleWrapper; + +import java.util.Locale; +import java.util.Map; + +public class ProxyMethodExampleMapper implements IMapper { + + private static final ProxyMethodExampleMapper INSTANCE = new ProxyMethodExampleMapper(); + + protected ProxyMethodExampleMapper() { + } + + public static ProxyMethodExampleMapper getInstance() { + return INSTANCE; + } + + // https://azure.github.io/autorest/extensions/#x-ms-examples + // https://github.com/Azure/azure-rest-api-specs/blob/main/documentation/x-ms-examples.md + + @SuppressWarnings("unchecked") + @Override + public ProxyMethodExample map(XmsExampleWrapper exampleWrapper) { + ProxyMethodExample.Builder builder = new ProxyMethodExample.Builder().name(exampleWrapper.getExampleName()); + + Object xmsExample = exampleWrapper.getXmsExample(); + if (xmsExample instanceof Map) { + // parameters + Object parameters = ((Map) xmsExample).get("parameters"); + if (parameters instanceof Map) { + for (Map.Entry entry : ((Map) parameters).entrySet()) { + builder.parameter(entry.getKey(), entry.getValue()); + } + } + + // responses + Object responses = ((Map) xmsExample).get("responses"); + if (responses instanceof Map) { + for (Map.Entry entry : ((Map) responses).entrySet()) { + try { + Integer statusCode = Integer.valueOf(entry.getKey()); + builder.response(statusCode, entry.getValue()); + } catch (NumberFormatException numberFormatException) { + // ignore the response + } + } + } + + // x-ms-original-file + String xmsOriginalFile = (String) ((Map) xmsExample).get("x-ms-original-file"); + builder.originalFile(xmsOriginalFile); + if (exampleWrapper.getOperationId() != null) { + builder.codeSnippetIdentifier(buildCodeSnippetIdentifier(exampleWrapper.getOperationId(), exampleWrapper.getExampleName())); + } + } + return builder.build(); + } + + private String buildCodeSnippetIdentifier(String operationId, String exampleName) { + return String.format("%s.generated.%s.%s", JavaSettings.getInstance().getPackage(), getValidName(operationId), getValidName(exampleName)).toLowerCase(Locale.ROOT); + } + + private String getValidName(String exampleName) { + return CodeNamer.getValidName(exampleName).replace("_", ""); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/mapper/ProxyMethodMapper.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/mapper/ProxyMethodMapper.java new file mode 100644 index 0000000000..6567bedea8 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/mapper/ProxyMethodMapper.java @@ -0,0 +1,771 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.mapper; + +import com.microsoft.typespec.http.client.generator.core.Javagen; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Operation; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Parameter; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Request; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.RequestParameterLocation; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Response; +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; +import com.microsoft.typespec.http.client.generator.core.extension.plugin.PluginLogger; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClassType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientModels; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.GenericType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.IType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ParameterSynthesizedOrigin; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.PrimitiveType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ProxyMethod; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ProxyMethodExample; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ProxyMethodParameter; +import com.microsoft.typespec.http.client.generator.core.util.ClientModelUtil; +import com.microsoft.typespec.http.client.generator.core.util.CodeNamer; +import com.microsoft.typespec.http.client.generator.core.util.MethodUtil; +import com.microsoft.typespec.http.client.generator.core.util.SchemaUtil; +import com.microsoft.typespec.http.client.generator.core.util.XmsExampleWrapper; +import com.azure.core.http.HttpMethod; +import com.azure.core.util.CoreUtils; +import org.slf4j.Logger; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * Maps Swagger definition into the interface methods that RestProxy consumes. + */ +public class ProxyMethodMapper implements IMapper>> { + + private final Logger logger = new PluginLogger(Javagen.getPluginInstance(), ProxyMethodMapper.class); + + private static final List RETURN_VALUE_WIRE_TYPE_OPTIONS = Arrays.asList(ClassType.BASE_64_URL, + ClassType.DATE_TIME_RFC_1123, PrimitiveType.DURATION_LONG, PrimitiveType.DURATION_DOUBLE, + ClassType.DURATION_LONG, ClassType.DURATION_DOUBLE, PrimitiveType.UNIX_TIME_LONG, ClassType.UNIX_TIME_LONG, + ClassType.UNIX_TIME_DATE_TIME); + + private static final ProxyMethodMapper INSTANCE = new ProxyMethodMapper(); + + private final Map> parsed = new ConcurrentHashMap<>(); + + protected ProxyMethodMapper() { + } + + public static ProxyMethodMapper getInstance() { + return INSTANCE; + } + + @Override + public Map> map(Operation operation) { + JavaSettings settings = JavaSettings.getInstance(); + Map> result = new LinkedHashMap<>(); + + String operationName = operation.getLanguage().getJava().getName(); + ProxyMethod.Builder builder = createProxyMethodBuilder().description(operation.getDescription()) + .name(operationName) + .isResumable(false); + + String operationId = operation.getOperationId(); + if (CoreUtils.isNullOrEmpty(operationId) && operation.getLanguage() != null + && operation.getLanguage().getDefault() + != null) { // operationId or language.default could be null for generated method like "listNext" + if (operationGroupNotNull(operation, settings)) { + operationId = operation.getOperationGroup().getLanguage().getDefault().getName() + "_" + + operation.getLanguage().getDefault().getName(); + } else { + operationId = operation.getLanguage().getDefault().getName(); + } + } + builder.operationId(operationId); + + List expectedStatusCodes = operation.getResponses() + .stream() + .flatMap(r -> r.getProtocol().getHttp().getStatusCodes().stream()) + .map(s -> s.replace("'", "")) + .map(Integer::parseInt) + .sorted() + .collect(Collectors.toList()); + builder.responseExpectedStatusCodes(expectedStatusCodes); + + IType responseBodyType = MapperUtils.handleResponseSchema(operation, settings); + if (settings.isDataPlaneClient() && settings.isBranded()) { + builder.rawResponseBodyType(responseBodyType); + responseBodyType = SchemaUtil.removeModelFromResponse(responseBodyType, operation); + } + builder.responseBodyType(responseBodyType); + IType asyncRestResponseReturnType = getAsyncRestResponseReturnType(operation, responseBodyType, + settings.isDataPlaneClient(), settings); + builder.returnType(asyncRestResponseReturnType); + + buildUnexpectedResponseExceptionTypes(builder, operation, expectedStatusCodes, settings); + + AtomicReference responseBodyTypeReference = new AtomicReference<>(responseBodyType); + builder.returnValueWireType(RETURN_VALUE_WIRE_TYPE_OPTIONS.stream() + .filter(type -> responseBodyTypeReference.get().contains(type)) + .findFirst() + .orElse(null)); + + Set responseContentTypes = operation.getResponses() + .stream() + .filter(r -> r.getProtocol() != null && r.getProtocol().getHttp() != null + && r.getProtocol().getHttp().getMediaTypes() != null) + .flatMap(r -> r.getProtocol().getHttp().getMediaTypes().stream()) + .filter(s -> !s.isEmpty()) + .collect(Collectors.toSet()); + if (!responseContentTypes.contains("application/json")) { + responseContentTypes.add(MethodUtil.CONTENT_TYPE_APPLICATION_JSON_ERROR_WEIGHT); + } + builder.responseContentTypes(responseContentTypes); + + List requests = operation.getRequests(); + // Used to deduplicate method with same signature. + // E.g. one request takes "application/json" and another takes "text/plain", which both are String type + Set> methodSignatures = new HashSet<>(); + + for (Request request : requests) { + if (parsed.containsKey(request)) { + result.put(request, parsed.get(request)); + continue; + } + + String requestContentType = "application/json"; + + // check for mediaTypes first as that is more specific than the knownMediaType + // if there are multiple, we'll use the generic type + if (request.getProtocol().getHttp().getMediaTypes() != null + && request.getProtocol().getHttp().getMediaTypes().size() == 1) { + requestContentType = request.getProtocol().getHttp().getMediaTypes().get(0); + } else if (request.getProtocol().getHttp().getKnownMediaType() != null) { + requestContentType = request.getProtocol().getHttp().getKnownMediaType().getContentType(); + } + builder.requestContentType(requestContentType); + builder.baseURL(request.getProtocol().getHttp().getUri()); + builder.urlPath(request.getProtocol().getHttp().getPath()); + builder.httpMethod(HttpMethod.valueOf(request.getProtocol().getHttp().getMethod().toUpperCase())); + + List parameters = new ArrayList<>(); + List allParameters = new ArrayList<>(); + List proxyMethods = new ArrayList<>(); + // add content-type parameter to allParameters when body is optional and there is single content type + if (settings.isDataPlaneClient() + // only if "content-type" is not already defined in parameters + && request.getParameters() + .stream() + .noneMatch(p -> p.getProtocol() != null && p.getProtocol().getHttp() != null + && p.getProtocol().getHttp().getIn() == RequestParameterLocation.HEADER + && "content-type".equalsIgnoreCase(p.getLanguage().getDefault().getSerializedName()))) { + boolean isBodyParamRequired = request.getParameters() + .stream() + .filter(p -> p.getProtocol() != null && p.getProtocol().getHttp() != null + && p.getProtocol().getHttp().getIn() == RequestParameterLocation.BODY) + .map(Parameter::isRequired) + .findFirst() + .orElse(false); + if (MethodUtil.getContentTypeCount(operation.getRequests()) == 1 && !isBodyParamRequired) { + Parameter contentTypeParameter = MethodUtil.createContentTypeParameter(request, operation); + allParameters.add(Mappers.getProxyParameterMapper().map(contentTypeParameter)); + } + } + + for (Parameter parameter : request.getParameters() + .stream() + .filter(p -> p.getProtocol() != null && p.getProtocol().getHttp() != null) + .collect(Collectors.toList())) { + parameter.setOperation(operation); + ProxyMethodParameter proxyMethodParameter = Mappers.getProxyParameterMapper().map(parameter); + if (requestContentType.startsWith("application/json-patch+json")) { + proxyMethodParameter = CustomProxyParameterMapper.getInstance().map(parameter); + } + allParameters.add(proxyMethodParameter); + if (!settings.isDataPlaneClient()) { + parameters.add(proxyMethodParameter); + } else { + // LLC will put required path, body, query, header parameters to method signature + final boolean parameterIsRequired = parameter.isRequired(); + final boolean parameterIsClientOrApiVersion = + ClientModelUtil.getClientDefaultValueOrConstantValue(parameter) != null + && ParameterSynthesizedOrigin.fromValue(parameter.getOrigin()) + == ParameterSynthesizedOrigin.API_VERSION; + final boolean parameterIsConstantOrFromClient = proxyMethodParameter.isConstant() + || proxyMethodParameter.isFromClient(); + if (parameterIsRequired || parameterIsConstantOrFromClient || parameterIsClientOrApiVersion) { + parameters.add(proxyMethodParameter); + } + } + } + List specialParameters = getSpecialParameters(operation); + if (!CoreUtils.isNullOrEmpty(specialParameters)) { + builder.specialHeaders(specialParameters.stream() + .map(ProxyMethodParameter::getRequestParameterName) + .collect(Collectors.toList())); + } + if (!settings.isDataPlaneClient()) { + parameters.addAll(specialParameters); + } + allParameters.addAll(specialParameters); + + String name = deduplicateMethodName(operationName, parameters, requestContentType, methodSignatures); + builder.name(name); + + if (settings.isDataPlaneClient()) { + ProxyMethodParameter requestOptions = ProxyMethodParameter.REQUEST_OPTIONS_PARAMETER; + allParameters.add(requestOptions); + parameters.add(requestOptions); + } + + if (JavaSettings.getInstance().isBranded()) { + ProxyMethodParameter contextParameter = getContextParameter(); + allParameters.add(contextParameter); + parameters.add(contextParameter); + } + + appendCallbackParameter(parameters, responseBodyType); + builder.allParameters(allParameters); + builder.parameters(parameters); + + if (operation.getExtensions() != null && operation.getExtensions().getXmsExamples() != null + && operation.getExtensions().getXmsExamples().getExamples() != null && !operation.getExtensions() + .getXmsExamples() + .getExamples() + .isEmpty()) { + String operationIdLocal = operationId; + Map examples = operation.getExtensions() + .getXmsExamples() + .getExamples() + .entrySet() + .stream() + .collect(Collectors.toMap(Map.Entry::getKey, e -> Mappers.getProxyMethodExampleMapper() + .map(new XmsExampleWrapper(e.getValue(), operationIdLocal, e.getKey())))); + builder.examples(examples); + } + + ProxyMethod proxyMethod = builder.build(); + proxyMethods.add(proxyMethod); + + addNoCustomHeaderProxyMethod(operation, settings, operationName, builder, responseBodyType, + asyncRestResponseReturnType, proxyMethods); + + ProxyMethodParameter fluxByteBufferParam = parameters.stream() + .filter(parameter -> parameter.getClientType() == GenericType.FLUX_BYTE_BUFFER) + .findFirst() + .orElse(null); + + if (fluxByteBufferParam != null) { + List proxyMethodParameters = new ArrayList<>(parameters); + int i = parameters.indexOf(fluxByteBufferParam); + proxyMethodParameters.remove(i); + + ProxyMethodParameter binaryDataParam = fluxByteBufferParam.newBuilder() + .wireType(ClassType.BINARY_DATA) + .rawType(ClassType.BINARY_DATA) + .clientType(ClassType.BINARY_DATA) + .build(); + + proxyMethodParameters.add(i, binaryDataParam); + builder.parameters(proxyMethodParameters); + proxyMethods.add(builder.build()); + + addNoCustomHeaderProxyMethod(operation, settings, operationName, builder, responseBodyType, + asyncRestResponseReturnType, proxyMethods); + } + + final List asyncProxyMethods = new ArrayList<>(proxyMethods); + if (settings.isSyncStackEnabled()) { + addSyncProxyMethods(proxyMethods); + } + if (settings.getSyncMethods() == JavaSettings.SyncMethodsGeneration.SYNC_ONLY) { + proxyMethods.removeAll(asyncProxyMethods); + } + result.put(request, proxyMethods); + parsed.put(request, proxyMethods); + } + return result; + } + + private void addNoCustomHeaderProxyMethod(Operation operation, JavaSettings settings, String operationName, + ProxyMethod.Builder builder, IType responseBodyType, IType asyncRestResponseReturnType, + List proxyMethods) { + if (settings.isDisableTypedHeadersMethods()) { + return; + } + + if (settings.isNoCustomHeaders() && asyncRestResponseReturnType instanceof GenericType + && ((GenericType) asyncRestResponseReturnType).getTypeArguments()[0] instanceof GenericType + && ((GenericType) ((GenericType) asyncRestResponseReturnType).getTypeArguments()[0]).getName() + .equals("ResponseBase")) { + IType asyncResponseWithNoHeaders = getAsyncRestResponseReturnType(operation, responseBodyType, + settings.isDataPlaneClient(), settings, true); + builder.returnType(asyncResponseWithNoHeaders); + builder.name(operationName + "NoCustomHeaders"); + builder.customHeaderIgnored(true); + + proxyMethods.add(builder.build()); + + // reset builder state + // TODO (srnagar): add a clone method to proxy method builder. Each proxy method should use it's own + // builder instance to maintain its state separately. + builder.returnType(asyncRestResponseReturnType); + builder.name(operationName); + builder.customHeaderIgnored(false); + } + } + + private void addSyncProxyMethods(List proxyMethods) { + List syncProxyMethods = new ArrayList<>(); + for (ProxyMethod asyncProxyMethod : proxyMethods) { + if (asyncProxyMethod.getParameters() + .stream() + .anyMatch(param -> param.getClientType() == GenericType.FLUX_BYTE_BUFFER)) { + continue; + } + syncProxyMethods.add(asyncProxyMethod.toSync()); + } + proxyMethods.addAll(syncProxyMethods); + } + + protected boolean operationGroupNotNull(Operation operation, JavaSettings settings) { + return operation.getOperationGroup() != null && operation.getOperationGroup().getLanguage() != null + && operation.getOperationGroup().getLanguage().getDefault() != null && !CoreUtils.isNullOrEmpty( + operation.getOperationGroup().getLanguage().getDefault().getName()); + } + + private ProxyMethodParameter getContextParameter() { + return new ProxyMethodParameter.Builder().description("The context to associate with this operation.") + .wireType(getContextClass()) + .clientType(getContextClass()) + .name("context") + .requestParameterLocation(RequestParameterLocation.NONE) + .requestParameterName("context") + .alreadyEncoded(true) + .constant(false) + .required(false) + .nullable(false) + .fromClient(false) + .parameterReference("context") + .origin(ParameterSynthesizedOrigin.CONTEXT) + .build(); + + } + + protected ClassType getContextClass() { + return ClassType.CONTEXT; + } + + protected void appendCallbackParameter(List parameters, IType responseBodyType) { + } + + /** + * Gets the type for AsyncRestResponse. + * + * @param operation the operation. + * @param responseBodyType the type of the response body. + * @param isProtocolMethod whether the client method to be simplified for resilience to API changes. + * @param settings the JavaSettings. + * @return the type for AsyncRestResponse. + */ + protected IType getAsyncRestResponseReturnType(Operation operation, IType responseBodyType, + boolean isProtocolMethod, JavaSettings settings) { + return this.getAsyncRestResponseReturnType(operation, responseBodyType, isProtocolMethod, settings, false); + } + + /** + * Gets the type for AsyncRestResponse. + * + * @param operation the operation. + * @param responseBodyType the type of the response body. + * @param isProtocolMethod whether the client method to be simplified for resilience to API changes. + * @param settings the JavaSettings. + * @param ignoreTypedHeaders Ignores typed headers when creating the return type, if this is set to true. + * @return the type for AsyncRestResponse. + */ + protected IType getAsyncRestResponseReturnType(Operation operation, IType responseBodyType, + boolean isProtocolMethod, JavaSettings settings, boolean ignoreTypedHeaders) { + if (isProtocolMethod) { + IType singleValueType; + if (responseBodyType.equals(PrimitiveType.VOID)) { + singleValueType = GenericType.Response(ClassType.VOID); + } else { + singleValueType = GenericType.Response(responseBodyType); + } + return createSingleValueAsyncReturnType(singleValueType); + } else if (operation.getExtensions() != null && operation.getExtensions().isXmsLongRunningOperation() + && settings.isFluent() && (operation.getExtensions().getXmsPageable() == null || !( + operation.getExtensions().getXmsPageable().getNextOperation() == operation))) { + // LRO in fluent uses Flux for PollerFactory in azure-core-management + return createBinaryContentAsyncReturnType(); + } else if (SchemaUtil.responseContainsHeaderSchemas(operation, settings)) { + // SchemaResponse + // method with schema in headers would require a ClientResponse + if (settings.isGenericResponseTypes()) { + // If the response body type is InputStream it needs to be converted to Flux to be + // asynchronous, unless this is sync-stack. + if (responseBodyType == ClassType.INPUT_STREAM) { + responseBodyType = GenericType.FLUX_BYTE_BUFFER; + } + IType genericResponseType = GenericType.RestResponse( + Mappers.getSchemaMapper().map(ClientMapper.parseHeader(operation, settings)), responseBodyType); + + if (ignoreTypedHeaders || settings.isDisableTypedHeadersMethods()) { + if (responseBodyType == GenericType.FLUX_BYTE_BUFFER) { + return createStreamContentAsyncReturnType(); + } + genericResponseType = GenericType.Response(responseBodyType); + } + return createSingleValueAsyncReturnType(genericResponseType); + } else { + ClassType clientResponseClassType = ClientMapper.getClientResponseClassType(operation, + ClientModels.getInstance().getModels(), settings); + return createClientResponseAsyncReturnType(clientResponseClassType); + } + } else { + if ((!settings.isDataPlaneClient() && !settings.isSyncStackEnabled() && settings.isInputStreamForBinary() + && responseBodyType.equals(ClassType.BINARY_DATA)) || responseBodyType.equals(ClassType.INPUT_STREAM)) { + return createStreamContentAsyncReturnType(); + } else if (responseBodyType.equals(PrimitiveType.VOID)) { + IType singleValueType = GenericType.Response(ClassType.VOID); + return createSingleValueAsyncReturnType(singleValueType); + } else { + IType singleValueType = GenericType.Response(responseBodyType); + return createSingleValueAsyncReturnType(singleValueType); + } + } + } + + protected IType createSingleValueAsyncReturnType(IType singleValueType) { + return GenericType.Mono(singleValueType); + } + + protected IType createClientResponseAsyncReturnType(ClassType clientResponseClassType) { + return GenericType.Mono(clientResponseClassType); + } + + protected IType createStreamContentAsyncReturnType() { + IType singleValueType = ClassType.STREAM_RESPONSE; + return GenericType.Mono(singleValueType); + } + + protected IType createBinaryContentAsyncReturnType() { + IType returnType = GenericType.Response(GenericType.FLUX_BYTE_BUFFER); // raw response for LRO + return GenericType.Mono(returnType); + } + + protected ProxyMethod.Builder createProxyMethodBuilder() { + return new ProxyMethod.Builder(); + } + + /** + * Extension for configure on unexpected response exception types to builder. + * + * @param builder the ProxyMethod builder + * @param operation the operation + * @param expectedStatusCodes the expected status codes + * @param settings the settings + */ + protected void buildUnexpectedResponseExceptionTypes(ProxyMethod.Builder builder, Operation operation, + List expectedStatusCodes, JavaSettings settings) { + SwaggerExceptionDefinitions swaggerExceptionDefinitions = getSwaggerExceptionDefinitions(operation, settings); + ClassType settingsDefaultExceptionType = getDefaultHttpExceptionTypeFromSettings(settings); + + // Use the settings defined default exception type over the Swagger defined default exception type. + ClassType defaultErrorType = (settingsDefaultExceptionType == null) + ? swaggerExceptionDefinitions.defaultExceptionType + : settingsDefaultExceptionType; + + if (defaultErrorType != null) { + builder.unexpectedResponseExceptionType(defaultErrorType); + } else { + builder.unexpectedResponseExceptionType(getHttpResponseExceptionType()); + } + + Map settingsExceptionTypeMap = getHttpStatusToExceptionTypeMappingFromSettings(settings); + + // Initialize the merged map with the Swagger defined configurations so that the settings configurations + // overrides it. + Map mergedExceptionTypeMapping = new HashMap<>( + swaggerExceptionDefinitions.exceptionTypeMapping); + mergedExceptionTypeMapping.putAll(settingsExceptionTypeMap); + + // remove expected status codes + expectedStatusCodes.forEach(mergedExceptionTypeMapping::remove); + + // Convert the exception type mapping into what code generation uses elsewhere. + Map> processedMapping = new HashMap<>(); + for (Map.Entry kvp : mergedExceptionTypeMapping.entrySet()) { + processedMapping.compute(kvp.getValue(), (errorType, statuses) -> { + if (statuses == null) { + List statusList = new ArrayList<>(); + statusList.add(kvp.getKey()); + return statusList; + } + + statuses.add(kvp.getKey()); + return statuses; + }); + } + + if (!processedMapping.isEmpty()) { + builder.unexpectedResponseExceptionTypes(processedMapping); + } + } + + private SwaggerExceptionDefinitions getSwaggerExceptionDefinitions(Operation operation, JavaSettings settings) { + + SwaggerExceptionDefinitions exceptionDefinitions = new SwaggerExceptionDefinitions(); + ClassType swaggerDefaultExceptionType = null; + Map swaggerExceptionTypeMap = new HashMap<>(); + + if (settings.isDataPlaneClient()) { + // LLC does not use model, hence exception from swagger + swaggerDefaultExceptionType = ClassType.HTTP_RESPONSE_EXCEPTION; + exceptionDefinitions.defaultExceptionType = swaggerDefaultExceptionType; + exceptionDefinitions.exceptionTypeMapping = swaggerExceptionTypeMap; + } else { + /* + 1. If exception has valid numeric status codes, group them to unexpectedResponseExceptionTypes + 2. If exception does not have status codes, or have 'default' or invalid number, put the first to unexpectedResponseExceptionType, ignore the rest + 3. After processing, if no model in unexpectedResponseExceptionType, take any from unexpectedResponseExceptionTypes and put it to unexpectedResponseExceptionType + */ + if (operation.getExceptions() != null && !operation.getExceptions().isEmpty()) { + for (Response exception : operation.getExceptions()) { + // Exception doesn't have HTTP configurations, skip it. + if (exception.getProtocol() == null || exception.getProtocol().getHttp() == null) { + continue; + } + + boolean isDefaultError = true; + List statusCodes = exception.getProtocol().getHttp().getStatusCodes(); + if (statusCodes != null && !statusCodes.isEmpty()) { + try { + ClassType exceptionType = getExceptionType(exception, settings); + statusCodes.stream() + .map(Integer::parseInt) + .forEach(status -> swaggerExceptionTypeMap.put(status, exceptionType)); + + isDefaultError = false; + } catch (NumberFormatException ex) { + // statusCodes can be 'default' + //logger.warn("Failed to parse status code, exception {}", ex.toString()); + } + } + + if (swaggerDefaultExceptionType == null && isDefaultError && exception.getSchema() != null) { + swaggerDefaultExceptionType = processExceptionClassType( + (ClassType) Mappers.getSchemaMapper().map(exception.getSchema()), settings); + } + } + + if (swaggerDefaultExceptionType == null && !CoreUtils.isNullOrEmpty(operation.getExceptions()) + // m4 could return Response without schema, when the Swagger uses e.g. "produces: [ application/x-rdp ]" + && operation.getExceptions().get(0).getSchema() != null) { + // no default error, use the 1st to keep backward compatibility + swaggerDefaultExceptionType = processExceptionClassType( + (ClassType) Mappers.getSchemaMapper().map(operation.getExceptions().get(0).getSchema()), + settings); + } + } + + exceptionDefinitions.defaultExceptionType = swaggerDefaultExceptionType; + exceptionDefinitions.exceptionTypeMapping = swaggerExceptionTypeMap; + } + + return exceptionDefinitions; + } + + private static final class SwaggerExceptionDefinitions { + private ClassType defaultExceptionType; + private Map exceptionTypeMapping; + } + + private ClassType getExceptionType(Response exception, JavaSettings settings) { + ClassType exceptionType = getHttpResponseExceptionType(); // default as HttpResponseException + + if (exception != null && exception.getSchema() != null) { + ClassType errorType = (ClassType) Mappers.getSchemaMapper().map(exception.getSchema()); + if (errorType != null) { + exceptionType = processExceptionClassType(errorType, settings); + } + } + + return exceptionType; + } + + /** + * Extension for map error ClassType to exception ClassType. + * + * @param errorType the error class. + * @param settings the Java settings. + * @return the exception ClassType. + */ + protected ClassType processExceptionClassType(ClassType errorType, JavaSettings settings) { + if (errorType == null) { + return null; + } + + String exceptionName = errorType.getExtensions() == null ? null : errorType.getExtensions().getXmsClientName(); + if (exceptionName == null || exceptionName.isEmpty()) { + exceptionName = errorType.getName(); + exceptionName += "Exception"; + } + + String exceptionPackage = (settings.isCustomType(exceptionName)) ? settings.getPackage( + settings.getCustomTypesSubpackage()) : settings.getPackage(settings.getModelsSubpackage()); + + return new ClassType.Builder().packageName(exceptionPackage).name(exceptionName).build(); + } + + private String deduplicateMethodName(String operationName, List parameters, + String requestContentType, Set> methodSignatures) { + String name = operationName; + List methodSignature = new ArrayList<>(); + methodSignature.add(operationName); + methodSignature.addAll( + parameters.stream().map(p -> p.getWireType().toString()) // simple class name should be enough? + .collect(Collectors.toList())); + if (methodSignatures.contains(methodSignature)) { + // got a conflict on method signature + String conflictMethodSignature = methodSignature.toString(); + + // first try to append media type + if (!CoreUtils.isNullOrEmpty(requestContentType)) { + methodSignature.set(0, + operationName + CodeNamer.toPascalCase(CodeNamer.removeInvalidCharacters(requestContentType))); + } + + // if not working, then just append increasing index no. + int indexNo = 1; + while (methodSignatures.contains(methodSignature)) { + methodSignature.set(0, operationName + indexNo); + ++indexNo; + } + + // let's hope the new name does not conflict with name from another operation + name = methodSignature.get(0); + logger.warn("Rename method to '{}', due to conflict on method signature {}", name, conflictMethodSignature); + } + methodSignatures.add(methodSignature); + return name; + } + + private static ClassType getDefaultHttpExceptionTypeFromSettings(JavaSettings settings) { + String defaultHttpExceptionType = settings.getDefaultHttpExceptionType(); + + return CoreUtils.isNullOrEmpty(defaultHttpExceptionType) + ? null + : createExceptionTypeFromFullyQualifiedClass(defaultHttpExceptionType); + } + + private Map getHttpStatusToExceptionTypeMappingFromSettings(JavaSettings settings) { + // Use a status code to error type mapping initial so that the custom mapping can override the default mapping, + // if the default mapping is being used. + Map exceptionMapping = new HashMap<>(); + + if (settings.isUseDefaultHttpStatusCodeToExceptionTypeMapping()) { + exceptionMapping.putAll(getDefaultHttpStatusCodeToExceptionTypeMapping()); + } + + Map customExceptionMapping = settings.getHttpStatusCodeToExceptionTypeMapping(); + if (!CoreUtils.isNullOrEmpty(customExceptionMapping)) { + customExceptionMapping.forEach( + (key, value) -> exceptionMapping.put(key, createExceptionTypeFromFullyQualifiedClass(value))); + } + + return exceptionMapping; + } + + private static ClassType createExceptionTypeFromFullyQualifiedClass(String fullyQualifiedClass) { + int classStart = fullyQualifiedClass.lastIndexOf("."); + return new ClassType.Builder().packageName(fullyQualifiedClass.substring(0, classStart)) + .name(fullyQualifiedClass.substring(classStart + 1)) + .build(); + } + + /** + * Gets the default HTTP status code to exception type mapping. + *

+ * This is only used when {@link JavaSettings#isUseDefaultHttpStatusCodeToExceptionTypeMapping()} is true. The + * values in this mapping may also be overridden if {@link JavaSettings#getHttpStatusCodeToExceptionTypeMapping()} + * is configured. + * + * @return The default HTTP status code to exception type mapping. + */ + protected Map getDefaultHttpStatusCodeToExceptionTypeMapping() { + Map defaultMapping = new HashMap<>(); + defaultMapping.put(401, ClassType.CLIENT_AUTHENTICATION_EXCEPTION); + defaultMapping.put(404, ClassType.RESOURCE_NOT_FOUND_EXCEPTION); + defaultMapping.put(409, ClassType.RESOURCE_MODIFIED_EXCEPTION); + + return defaultMapping; + } + + /** + * Gets the default HTTP response exception type. + *

+ * The returned exception type is used as the default HTTP exception when both the Swagger doesn't define an HTTP + * exception type and {@link JavaSettings} doesn't contain {@link JavaSettings#getDefaultHttpExceptionType()}. + * + * @return The default HTTP response exception type. + */ + protected ClassType getHttpResponseExceptionType() { + return ClassType.HTTP_RESPONSE_EXCEPTION; + } + + /** + * Gets the special parameters. + * + * @param operation the operation + * @return the special parameters. + */ + protected List getSpecialParameters(Operation operation) { + List specialParameters = new ArrayList<>(); + if (!CoreUtils.isNullOrEmpty(operation.getSpecialHeaders()) && !CoreUtils.isNullOrEmpty( + operation.getRequests())) { + HttpMethod httpMethod = MethodUtil.getHttpMethod(operation); + if (MethodUtil.isHttpMethodSupportRepeatableRequestHeaders(httpMethod)) { + List specialHeaders = operation.getSpecialHeaders() + .stream() + .map(s -> s.toLowerCase(Locale.ROOT)) + .collect(Collectors.toList()); + boolean supportRepeatabilityRequest = specialHeaders.contains( + MethodUtil.REPEATABILITY_REQUEST_ID_HEADER); + if (supportRepeatabilityRequest) { + Function commonBuilderSetting + = builder -> { + builder.rawType(ClassType.STRING) + .wireType(ClassType.STRING) + .clientType(ClassType.STRING) + .requestParameterLocation(RequestParameterLocation.HEADER) + .required(false) + .nullable(true) + .fromClient(false); + return builder; + }; + + specialParameters.add(commonBuilderSetting.apply( + new ProxyMethodParameter.Builder().name(MethodUtil.REPEATABILITY_REQUEST_ID_VARIABLE_NAME) + .parameterReference(MethodUtil.REPEATABILITY_REQUEST_ID_EXPRESSION) + .requestParameterName(MethodUtil.REPEATABILITY_REQUEST_ID_HEADER) + .description("Repeatability request ID header")).build()); + if (specialHeaders.contains(MethodUtil.REPEATABILITY_FIRST_SENT_HEADER)) { + specialParameters.add(commonBuilderSetting.apply( + new ProxyMethodParameter.Builder().name(MethodUtil.REPEATABILITY_FIRST_SENT_VARIABLE_NAME) + .parameterReference(MethodUtil.REPEATABILITY_FIRST_SENT_EXPRESSION) + .requestParameterName(MethodUtil.REPEATABILITY_FIRST_SENT_HEADER) + .description("Repeatability first sent header as HTTP-date")).build()); + } + } + } + } + return specialParameters; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/mapper/ProxyParameterMapper.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/mapper/ProxyParameterMapper.java new file mode 100644 index 0000000000..470643425b --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/mapper/ProxyParameterMapper.java @@ -0,0 +1,177 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.mapper; + +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.ArraySchema; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.ConstantSchema; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Parameter; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.RequestParameterLocation; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Schema; +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ArrayType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClassType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.IType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ListType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ParameterSynthesizedOrigin; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.PrimitiveType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ProxyMethodParameter; +import com.microsoft.typespec.http.client.generator.core.util.CodeNamer; +import com.microsoft.typespec.http.client.generator.core.util.MethodUtil; +import com.microsoft.typespec.http.client.generator.core.util.SchemaUtil; +import com.azure.core.util.serializer.CollectionFormat; + +public class ProxyParameterMapper implements IMapper { + private static final ProxyParameterMapper INSTANCE = new ProxyParameterMapper(); + + protected ProxyParameterMapper() { + } + + public static ProxyParameterMapper getInstance() { + return INSTANCE; + } + + @Override + public ProxyMethodParameter map(Parameter parameter) { + JavaSettings settings = JavaSettings.getInstance(); + + String name = parameter.getLanguage().getJava().getName(); + + ProxyMethodParameter.Builder builder = createProxyMethodParameterBuilder() + .requestParameterName(parameter.getLanguage().getDefault().getSerializedName()) + .name(name) + .required(parameter.isRequired()) + .nullable(parameter.isNullable()) + .origin(ParameterSynthesizedOrigin.fromValue(parameter.getOrigin())); + + String headerCollectionPrefix = null; + if (parameter.getExtensions() != null && parameter.getExtensions().getXmsHeaderCollectionPrefix() != null) { + headerCollectionPrefix = parameter.getExtensions().getXmsHeaderCollectionPrefix(); + } + builder.headerCollectionPrefix(headerCollectionPrefix); + + RequestParameterLocation parameterRequestLocation = parameter.getProtocol().getHttp().getIn(); + builder.requestParameterLocation(parameterRequestLocation); + + boolean parameterIsServiceClientProperty = parameter.getImplementation() == Parameter.ImplementationLocation.CLIENT; + builder.fromClient(parameterIsServiceClientProperty); + + Schema parameterJvWireType = parameter.getSchema(); + IType wireType = Mappers.getSchemaMapper().map(parameterJvWireType); + if (parameter.isNullable() || !parameter.isRequired()) { + wireType = wireType.asNullable(); + } + builder.rawType(wireType); + + IType clientType = wireType.getClientType(); + + if (settings.isDataPlaneClient()) { + clientType = SchemaUtil.removeModelFromParameter(parameterRequestLocation, clientType); + } + builder.clientType(clientType); + + if (wireType instanceof ListType + && SchemaUtil.treatAsXml(parameterJvWireType) + && parameterRequestLocation == RequestParameterLocation.BODY) { + String modelTypeName = ((ArraySchema) parameterJvWireType).getElementType().getLanguage().getJava().getName(); + boolean isCustomType = settings.isCustomType(CodeNamer.toPascalCase(modelTypeName + "Wrapper")); + String packageName = isCustomType + ? settings.getPackage(settings.getCustomTypesSubpackage()) + : settings.getPackage(settings.getImplementationSubpackage() + ".models"); + wireType = new ClassType.Builder() + .packageName(packageName) + .name(modelTypeName + "Wrapper") + .usedInXml(true) + .build(); + } else if (wireType == ArrayType.BYTE_ARRAY) { + if (parameterRequestLocation != RequestParameterLocation.BODY /*&& parameterRequestLocation != RequestParameterLocation.FormData*/) { + wireType = ClassType.STRING; + } else if (settings.isDataPlaneClient()) { + wireType = SchemaUtil.removeModelFromParameter(parameterRequestLocation, wireType); + } + } else if (wireType instanceof ListType && parameter.getProtocol().getHttp().getIn() != RequestParameterLocation.BODY /*&& parameter.getProtocol().getHttp().getIn() != RequestParameterLocation.FormData*/) { + if (parameter.getProtocol().getHttp().getExplode()) { + wireType = new ListType(ClassType.STRING); + } else { + wireType = ClassType.STRING; + } + } else if (settings.isDataPlaneClient()) { + wireType = SchemaUtil.removeModelFromParameter(parameterRequestLocation, wireType); + } + builder.wireType(wireType); + + builder.description(MethodUtil.getMethodParameterDescription(parameter, name, settings.isDataPlaneClient())); + + if (parameter.getExtensions() != null) { + builder.alreadyEncoded(parameter.getExtensions().isXmsSkipUrlEncoding()); + } + + if (parameter.getSchema() instanceof ConstantSchema){ + builder.constant(true); + Object objValue = ((ConstantSchema) parameter.getSchema()).getValue().getValue(); + builder.defaultValue(objValue == null ? null : String.valueOf(objValue)); + } + + // parameterReference is what ClientMethod calls the ProxyMethod + String parameterReference = CodeNamer.getEscapedReservedClientMethodParameterName(name); + if (Parameter.ImplementationLocation.CLIENT.equals(parameter.getImplementation())) { + String operationGroupName = parameter.getOperation().getOperationGroup().getLanguage().getJava().getName(); + String caller = (operationGroupName == null || operationGroupName.isEmpty()) ? "this" : "this.client"; + String clientPropertyName = parameter.getLanguage().getJava().getName(); + boolean isServiceVersion = false; + if (settings.isDataPlaneClient() && ParameterSynthesizedOrigin.fromValue(parameter.getOrigin()) == ParameterSynthesizedOrigin.API_VERSION) { + isServiceVersion = true; + clientPropertyName = "serviceVersion"; + } + if (clientPropertyName != null && !clientPropertyName.isEmpty()) { + clientPropertyName = CodeNamer.toPascalCase(CodeNamer.removeInvalidCharacters(clientPropertyName)); + } + String prefix = "get"; + if (clientType == PrimitiveType.BOOLEAN || clientType == ClassType.BOOLEAN) { + prefix = "is"; + if (CodeNamer.toCamelCase(parameterReference).startsWith(prefix)) { + prefix = ""; + clientPropertyName = CodeNamer.toCamelCase(clientPropertyName); + } + } + parameterReference = String.format("%s.%s%s()", caller, prefix, clientPropertyName); + if (isServiceVersion) { + parameterReference += ".getVersion()"; + } + } + builder.parameterReference(parameterReference); + + CollectionFormat collectionFormat = null; + if (parameter.getProtocol().getHttp().getStyle() != null) { + switch (parameter.getProtocol().getHttp().getStyle()) { + case SIMPLE: + collectionFormat = CollectionFormat.CSV; + break; + case SPACE_DELIMITED: + collectionFormat = CollectionFormat.SSV; + break; + case PIPE_DELIMITED: + collectionFormat = CollectionFormat.PIPES; + break; + case TAB_DELIMITED: + collectionFormat = CollectionFormat.TSV; + break; + default: + collectionFormat = CollectionFormat.CSV; + } + } + if (collectionFormat == null && clientType instanceof ListType + && ClassType.STRING == wireType) { + collectionFormat = CollectionFormat.CSV; + } + builder.collectionFormat(collectionFormat); + builder.explode(parameter.getProtocol().getHttp().getExplode()); + + return builder.build(); + } + + protected ProxyMethodParameter.Builder createProxyMethodParameterBuilder() { + return new ProxyMethodParameter.Builder(); + } + +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/mapper/SchemaMapper.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/mapper/SchemaMapper.java new file mode 100644 index 0000000000..573947b0d4 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/mapper/SchemaMapper.java @@ -0,0 +1,76 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.mapper; + +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.AnySchema; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.ArraySchema; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.BinarySchema; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.ChoiceSchema; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.ConstantSchema; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.DictionarySchema; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.ObjectSchema; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.OrSchema; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.PrimitiveSchema; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Schema; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.SealedChoiceSchema; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.IType; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class SchemaMapper implements IMapper { + private static final SchemaMapper INSTANCE = new SchemaMapper(); + Map parsed = new ConcurrentHashMap<>(); + + private SchemaMapper() { + } + + public static SchemaMapper getInstance() { + return INSTANCE; + } + + @Override + public IType map(Schema value) { + if (value == null) { + return null; + } + + IType schemaType = parsed.get(value); + if (schemaType != null) { + return schemaType; + } + + schemaType = createSchemaType(value); + parsed.put(value, schemaType); + + return schemaType; + } + + private IType createSchemaType(Schema value) { + if (value instanceof PrimitiveSchema) { + return Mappers.getPrimitiveMapper().map((PrimitiveSchema) value); + } else if (value instanceof ChoiceSchema) { + return Mappers.getChoiceMapper().map((ChoiceSchema) value); + } else if (value instanceof SealedChoiceSchema) { + return Mappers.getSealedChoiceMapper().map((SealedChoiceSchema) value); + } else if (value instanceof ArraySchema) { + return Mappers.getArrayMapper().map((ArraySchema) value); + } else if (value instanceof DictionarySchema) { + return Mappers.getDictionaryMapper().map((DictionarySchema) value); + } else if (value instanceof ObjectSchema) { + return Mappers.getObjectMapper().map((ObjectSchema) value); + } else if (value instanceof ConstantSchema) { + return Mappers.getConstantMapper().map((ConstantSchema) value); + } else if(value instanceof AnySchema) { + return Mappers.getAnyMapper().map((AnySchema) value); + } else if(value instanceof BinarySchema) { + return Mappers.getBinaryMapper().map((BinarySchema) value); + } else if(value instanceof OrSchema) { + return Mappers.getUnionMapper().map((OrSchema) value); + } else { + throw new UnsupportedOperationException("Cannot find a mapper for schema type " + value.getClass() + + ". Key: " + value.get$key()); + } + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/mapper/SealedChoiceMapper.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/mapper/SealedChoiceMapper.java new file mode 100644 index 0000000000..6ed293e4c7 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/mapper/SealedChoiceMapper.java @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.mapper; + +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.SealedChoiceSchema; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.IType; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class SealedChoiceMapper implements IMapper { + private static final SealedChoiceMapper INSTANCE = new SealedChoiceMapper(); + Map parsed = new ConcurrentHashMap<>(); + + private SealedChoiceMapper() { + } + + public static SealedChoiceMapper getInstance() { + return INSTANCE; + } + + @Override + public IType map(SealedChoiceSchema enumType) { + if (enumType == null) { + return null; + } + + IType sealedChoiceType = parsed.get(enumType); + if (sealedChoiceType != null) { + return sealedChoiceType; + } + + sealedChoiceType = createSealedChoiceType(enumType); + parsed.put(enumType, sealedChoiceType); + + return sealedChoiceType; + } + + private IType createSealedChoiceType(SealedChoiceSchema enumType) { + return MapperUtils.createEnumType(enumType, false); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/mapper/ServiceClientMapper.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/mapper/ServiceClientMapper.java new file mode 100644 index 0000000000..6aa444d80e --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/mapper/ServiceClientMapper.java @@ -0,0 +1,453 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.mapper; + +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Client; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.CodeModel; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.ConstantSchema; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Operation; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.OperationGroup; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Parameter; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.RequestParameterLocation; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Scheme; +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClassType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethod; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethodParameter; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.Constructor; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.IType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.MethodGroupClient; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ParameterSynthesizedOrigin; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.Proxy; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ProxyMethod; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.SecurityInfo; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ServiceClient; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ServiceClientProperty; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaVisibility; +import com.microsoft.typespec.http.client.generator.core.util.ClientModelUtil; +import com.microsoft.typespec.http.client.generator.core.util.CodeNamer; +import com.microsoft.typespec.http.client.generator.core.util.MethodUtil; +import com.microsoft.typespec.http.client.generator.core.util.SchemaUtil; +import com.azure.core.util.CoreUtils; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class ServiceClientMapper implements IMapper { + private static final ServiceClientMapper INSTANCE = new ServiceClientMapper(); + + private static final Pattern TRAILING_FORWARD_SLASH = Pattern.compile("/+$"); + private static final Pattern URL_PATH = Pattern.compile("(? codeModelRestAPIMethods = codeModel.getOperationGroups().stream() + .filter(og -> CoreUtils.isNullOrEmpty(og.getLanguage().getJava().getName())) + .flatMap(og -> og.getOperations().stream()) + .collect(Collectors.toList()); + + Proxy proxy = null; + if (!codeModelRestAPIMethods.isEmpty()) { + proxy = processClientOperations(builder, codeModelRestAPIMethods, serviceClientInterfaceName); + } else { + builder.clientMethods(Collections.emptyList()); + } + + List properties = processClientProperties(codeModel, ClientModelUtil.getServiceVersionClassName(serviceClientInterfaceName)); + + List serviceClientMethodGroupClients = new ArrayList<>(); + List codeModelMethodGroups = codeModel.getOperationGroups().stream() + .filter(og -> og.getLanguage().getJava().getName() != null && + !og.getLanguage().getJava().getName().isEmpty()) + .collect(Collectors.toList()); + for (OperationGroup codeModelMethodGroup : codeModelMethodGroups) { + serviceClientMethodGroupClients.add(Mappers.getMethodGroupMapper().map(codeModelMethodGroup, properties)); + } + builder.methodGroupClients(serviceClientMethodGroupClients); + + if (proxy == null && !serviceClientMethodGroupClients.isEmpty()) { + proxy = serviceClientMethodGroupClients.iterator().next().getProxy(); + } + + processParametersAndConstructors(builder, codeModel, codeModel, properties, proxy); + + return builder.build(); + } + + protected Proxy.Builder getProxyBuilder() { + return new Proxy.Builder(); + } + + protected ClientMethodParameter createSerializerAdapterParameter() { + return new ClientMethodParameter.Builder() + .description("The serializer to serialize an object into a string") + .finalParameter(false) + .wireType(ClassType.SERIALIZER_ADAPTER) + .name("serializerAdapter") + .required(true) + .constant(false) + .fromClient(true) + .defaultValue(null) + .annotations(new ArrayList<>()) + .build(); + } + + protected IType getHttpPipelineClassType() { + return ClassType.HTTP_PIPELINE; + } + + protected void addSerializerAdapterProperty(List serviceClientProperties, JavaSettings settings) { + if (settings.isBranded()) { + serviceClientProperties.add(new ServiceClientProperty("The serializer to serialize an object into a string.", + ClassType.SERIALIZER_ADAPTER, "serializerAdapter", true, null, + settings.isFluent() ? JavaVisibility.PackagePrivate : JavaVisibility.Public)); + } + } + + protected void addHttpPipelineProperty(List serviceClientProperties) { + serviceClientProperties.add(new ServiceClientProperty("The HTTP pipeline to send requests through.", + ClassType.HTTP_PIPELINE, "httpPipeline", true, null)); + } + + protected ServiceClient.Builder createClientBuilder() { + return new ServiceClient.Builder(); + } + + protected static String getBaseUrl(CodeModel codeModel) { + // assume all operations share the same base url + return codeModel.getOperationGroups().get(0).getOperations().get(0).getRequests().get(0) + .getProtocol().getHttp().getUri(); + } + + protected static String getBaseUrl(Operation operation) { + // assume all operations share the same base url + return operation.getRequests().get(0) + .getProtocol().getHttp().getUri(); + } + + protected Proxy processClientOperations(ServiceClient.Builder builder, List operations, String baseName) { + JavaSettings settings = JavaSettings.getInstance(); + + // TODO: Assume all operations share the same base url + Proxy.Builder proxyBuilder = getProxyBuilder() + .name(baseName + "Service") + .clientTypeName(baseName) + .baseURL(getBaseUrl(operations.iterator().next())); + List restAPIMethods = new ArrayList<>(); + for (Operation operation : operations) { + if (settings.isDataPlaneClient()) { + MethodUtil.tryMergeBinaryRequestsAndUpdateOperation(operation.getRequests(), operation); + } + restAPIMethods.addAll(Mappers.getProxyMethodMapper().map(operation).values().stream().flatMap(Collection::stream).collect(Collectors.toList())); + } + proxyBuilder.methods(restAPIMethods); + Proxy proxy = proxyBuilder.build(); + builder.proxy(proxy); + List clientMethods = operations.stream() + .flatMap(m -> Mappers.getClientMethodMapper().map(m).stream()) + .collect(Collectors.toList()); + if (settings.isGenerateSendRequestMethod()) { + clientMethods.add(ClientMethod.getAsyncSendRequestClientMethod(false)); + if (settings.getSyncMethods() != JavaSettings.SyncMethodsGeneration.NONE) { + clientMethods.add(ClientMethod.getSyncSendRequestClientMethod(false)); + } + } + builder.clientMethods(clientMethods); + + return proxy; + } + + protected List processClientProperties(Client client, String serviceVersionClassName) { + JavaSettings settings = JavaSettings.getInstance(); + + List serviceClientProperties = new ArrayList<>(); + List clientParameters = Stream.concat(client.getGlobalParameters().stream(), + client.getOperationGroups().stream() + .flatMap(og -> og.getOperations().stream()) + .flatMap(o -> o.getRequests().stream()) + .flatMap(r -> r.getParameters().stream())) + .filter(p -> p.getImplementation() == Parameter.ImplementationLocation.CLIENT) + .distinct() + .collect(Collectors.toList()); + String apiVersionSerializedName = "api-version"; + for (Parameter p : clientParameters) { + String serializedName = p.getLanguage().getDefault().getSerializedName(); + + if (settings.isDataPlaneClient() && ParameterSynthesizedOrigin.fromValue(p.getOrigin()) == ParameterSynthesizedOrigin.API_VERSION) { + // skip api-version, ServiceVersion will always be added to client for DPG + apiVersionSerializedName = serializedName; + continue; + } + + String serviceClientPropertyDescription = + p.getDescription() != null ? p.getDescription() : p.getLanguage().getJava().getDescription(); + + String serviceClientPropertyName = CodeNamer.getPropertyName(p.getLanguage().getJava().getName()); + + IType serviceClientPropertyClientType = Mappers.getSchemaMapper().map(p.getSchema()); + if (settings.isDataPlaneClient()) { + // mostly for Enum to String + serviceClientPropertyClientType = SchemaUtil.removeModelFromParameter(RequestParameterLocation.URI, serviceClientPropertyClientType); + } + if (p.isNullable() && serviceClientPropertyClientType != null) { + serviceClientPropertyClientType = serviceClientPropertyClientType.asNullable(); + } + + boolean serviceClientPropertyIsReadOnly = p.getSchema() instanceof ConstantSchema; + if (!settings.isFluent()) { + serviceClientPropertyIsReadOnly = false; + } + String serviceClientPropertyDefaultValueExpression = serviceClientPropertyClientType.defaultValueExpression(ClientModelUtil.getClientDefaultValueOrConstantValue(p)); + boolean serviceClientPropertyRequired = p.isRequired(); + + if (serviceClientPropertyClientType != ClassType.TOKEN_CREDENTIAL) { + ServiceClientProperty serviceClientProperty = + new ServiceClientProperty.Builder() + .description(serviceClientPropertyDescription) + .type(serviceClientPropertyClientType) + .name(serviceClientPropertyName) + .readOnly(serviceClientPropertyIsReadOnly) + .defaultValueExpression(serviceClientPropertyDefaultValueExpression) + .required(serviceClientPropertyRequired) + .requestParameterName(serializedName) + .build(); + if (!serviceClientProperties.contains(serviceClientProperty)) { + // Ignore duplicate client property. + serviceClientProperties.add(serviceClientProperty); + } + } + } + + if (settings.isDataPlaneClient() && serviceVersionClassName != null) { + // Always add a ServiceVersion parameter for DPG + serviceClientProperties.add(new ServiceClientProperty.Builder() + .description("Service version") + .type(new ClassType.Builder() + .name(serviceVersionClassName) + .packageName(settings.getPackage()) + .build()) + .name("serviceVersion") + .readOnly(false) + .defaultValueExpression(serviceVersionClassName + ".getLatest()") + .required(false) + .requestParameterName(apiVersionSerializedName) + .build()); + } + + return serviceClientProperties; + } + + protected void processParametersAndConstructors(ServiceClient.Builder builder, Client client, CodeModel codeModel, List serviceClientProperties, Proxy proxy) { + JavaSettings settings = JavaSettings.getInstance(); + + List clientParameters = Stream.concat(client.getGlobalParameters().stream(), + client.getOperationGroups().stream() + .flatMap(og -> og.getOperations().stream()) + .flatMap(o -> o.getRequests().stream()) + .flatMap(r -> r.getParameters().stream())) + .filter(p -> p.getImplementation() == Parameter.ImplementationLocation.CLIENT) + .distinct() + .collect(Collectors.toList()); + + addHttpPipelineProperty(serviceClientProperties); + addSerializerAdapterProperty(serviceClientProperties, settings); + if (settings.isFluent()) { + serviceClientProperties.add(new ServiceClientProperty.Builder() + .description("The default poll interval for long-running operation.") + .type(ClassType.DURATION) + .name("defaultPollInterval") + .readOnly(true) + .build()); + } + + builder.properties(serviceClientProperties); + + ClientMethodParameter tokenCredentialParameter = new ClientMethodParameter.Builder() + .description("the credentials for Azure") + .finalParameter(false) + .wireType(ClassType.TOKEN_CREDENTIAL) + .name("credential") + .required(true) + .constant(false) + .fromClient(true) + .defaultValue(null) + .annotations(new ArrayList<>()) + .build(); + + ClientMethodParameter httpPipelineParameter = new ClientMethodParameter.Builder() + .description("The HTTP pipeline to send requests through") + .finalParameter(false) + .wireType(getHttpPipelineClassType()) + .name("httpPipeline") + .required(true) + .constant(false) + .fromClient(true) + .defaultValue(null) + .annotations(new ArrayList<>()) + .build(); + + ClientMethodParameter serializerAdapterParameter = createSerializerAdapterParameter(); + + // map security information in code model to ServiceClient.SecurityInfo + SecurityInfo securityInfo = new SecurityInfo(); + if (codeModel.getSecurity() != null && + codeModel.getSecurity().getSchemes() != null && + codeModel.getSecurity().isAuthenticationRequired()) { + final String userImpersonationScope = "user_impersonation"; + + SecurityInfo securityInfoInCodeModel = new SecurityInfo(); + codeModel.getSecurity().getSchemes().forEach(securityScheme -> { + // hack, ignore "user_impersonation", as these non-AADToken appears in modelerfour 4.23.0+ + if (securityScheme.getType() == Scheme.SecuritySchemeType.OAUTH2 + && securityScheme.getScopes().size() == 1 + && userImpersonationScope.equals(securityScheme.getScopes().iterator().next())) { + return; + } + + securityInfoInCodeModel.getSecurityTypes().add(securityScheme.getType()); + if (securityScheme.getType().equals(Scheme.SecuritySchemeType.OAUTH2)) { + Set credentialScopes = securityScheme.getScopes().stream() + .filter(s -> !userImpersonationScope.equals(s)) // hack, filter out "user_impersonation" + .map(scope -> { + if (!scope.startsWith("\"")) { + return "\"" + scope + "\""; + } else { + return scope; + } + }).collect(Collectors.toSet()); + securityInfoInCodeModel.setScopes(credentialScopes); + } + if (securityScheme.getType().equals(Scheme.SecuritySchemeType.KEY)) { + securityInfoInCodeModel.setHeaderName(securityScheme.getName()); + securityInfoInCodeModel.setHeaderValuePrefix(securityScheme.getPrefix()); + } + }); + securityInfo = securityInfoInCodeModel; + } + + // overwrite securityInfo using JavaSettings + if (settings.getCredentialTypes() != null && !settings.getCredentialTypes().isEmpty() && + !settings.getCredentialTypes().contains(JavaSettings.CredentialType.NONE)) { + SecurityInfo securityInfoInJavaSettings = new SecurityInfo(); + if (settings.getCredentialTypes().contains(JavaSettings.CredentialType.TOKEN_CREDENTIAL)) { + securityInfoInJavaSettings.getSecurityTypes().add(Scheme.SecuritySchemeType.OAUTH2); + securityInfoInJavaSettings.setScopes(settings.getCredentialScopes()); + } + if (settings.getCredentialTypes().contains(JavaSettings.CredentialType.AZURE_KEY_CREDENTIAL)) { + securityInfoInJavaSettings.getSecurityTypes().add(Scheme.SecuritySchemeType.KEY); + securityInfoInJavaSettings.setHeaderName(settings.getKeyCredentialHeaderName()); + } + securityInfo = securityInfoInJavaSettings; + } + builder.securityInfo(securityInfo); + + if (securityInfo.getSecurityTypes().contains(Scheme.SecuritySchemeType.OAUTH2)) { + Set scopes = securityInfo.getScopes(); + String scopeParams; + if (scopes != null && !scopes.isEmpty()) { + scopeParams = "DEFAULT_SCOPES"; + } else { + // Remove trailing / and all relative paths + String host = TRAILING_FORWARD_SLASH.matcher(proxy.getBaseURL()).replaceAll(""); + host = URL_PATH.matcher(host).replaceAll(""); + List parameters = new ArrayList<>(); + int start = host.indexOf("{"); + while (start >= 0) { + int end = host.indexOf("}", start); + String serializedName = host.substring(start + 1, end); + Optional hostParam = clientParameters.stream().filter(p -> serializedName.equals(p.getLanguage().getJava().getSerializedName())).findFirst(); + if (hostParam.isPresent()) { + parameters.add(hostParam.get().getLanguage().getJava().getName()); + host = host.substring(0, start) + "%s" + host.substring(end + 1); + } + start = host.indexOf("{", start + 1); + } + if (parameters.isEmpty()) { + scopeParams = String.format("\"%s/.default\"", host); + } else { + scopeParams = String.format("String.format(\"%s/.default\", %s)", host, String.join(", ", parameters)); + } + } + builder.defaultCredentialScopes(scopeParams); + } + + List serviceClientConstructors = new ArrayList<>(); + + if (!settings.isBranded()) { + serviceClientConstructors.add(new Constructor(Collections.singletonList(httpPipelineParameter))); + builder.tokenCredentialParameter(tokenCredentialParameter) + .httpPipelineParameter(httpPipelineParameter) + .constructors(serviceClientConstructors); + } else if (settings.isFluent()) { + ClientMethodParameter azureEnvironmentParameter = new ClientMethodParameter.Builder() + .description("The Azure environment") + .finalParameter(false) + .wireType(ClassType.AZURE_ENVIRONMENT) + .name("environment") + .required(true) + .constant(false) + .fromClient(true) + .defaultValue("AzureEnvironment.AZURE") + .annotations(new ArrayList<>()) + .build(); + + ClientMethodParameter defaultPollIntervalParameter = new ClientMethodParameter.Builder() + .description("The default poll interval for long-running operation") + .finalParameter(false) + .wireType(ClassType.DURATION) + .name("defaultPollInterval") + .required(true) + .constant(false) + .fromClient(true) + .defaultValue("Duration.ofSeconds(30)") + .annotations(new ArrayList<>()) + .build(); + + serviceClientConstructors.add(new Constructor(Arrays.asList(httpPipelineParameter, serializerAdapterParameter, defaultPollIntervalParameter, azureEnvironmentParameter))); + builder.tokenCredentialParameter(tokenCredentialParameter) + .httpPipelineParameter(httpPipelineParameter) + .serializerAdapterParameter(serializerAdapterParameter) + .defaultPollIntervalParameter(defaultPollIntervalParameter) + .azureEnvironmentParameter(azureEnvironmentParameter) + .constructors(serviceClientConstructors); + } else { + serviceClientConstructors.add(new Constructor(new ArrayList<>())); + serviceClientConstructors.add(new Constructor(Collections.singletonList(httpPipelineParameter))); + serviceClientConstructors.add(new Constructor(Arrays.asList(httpPipelineParameter, serializerAdapterParameter))); + builder.tokenCredentialParameter(tokenCredentialParameter) + .httpPipelineParameter(httpPipelineParameter) + .serializerAdapterParameter(serializerAdapterParameter) + .constructors(serviceClientConstructors); + } + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/mapper/UnionMapper.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/mapper/UnionMapper.java new file mode 100644 index 0000000000..f18719ae66 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/mapper/UnionMapper.java @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.mapper; + +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.OrSchema; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.SchemaContext; +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClassType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.IType; +import com.microsoft.typespec.http.client.generator.core.util.SchemaUtil; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class UnionMapper implements IMapper { + private static final UnionMapper INSTANCE = new UnionMapper(); + Map parsed = new ConcurrentHashMap<>(); + + protected UnionMapper() { + } + + public static UnionMapper getInstance() { + return INSTANCE; + } + + @Override + public ClassType map(OrSchema compositeType) { + return ClassType.BINARY_DATA; +// if (compositeType == null) { +// return null; +// } +// +// return parsed.computeIfAbsent(compositeType, this::createClassType); + } + + private ClassType createClassType(OrSchema compositeType) { + JavaSettings settings = JavaSettings.getInstance(); + + String className = compositeType.getLanguage().getJava().getName(); + String classPackage = settings.isCustomType(className) + ? settings.getPackage(settings.getCustomTypesSubpackage()) + : settings.getPackage(settings.getModelsSubpackage()); + + if (settings.isDataPlaneClient() && (compositeType.getUsage() != null && compositeType.getUsage().contains(SchemaContext.INTERNAL))) { + // internal type, which is not exposed to user + classPackage = settings.getPackage(settings.getImplementationSubpackage(), settings.getModelsSubpackage()); + } + + return new ClassType.Builder() + .packageName(classPackage) + .name(className) + .extensions(compositeType.getExtensions()) + .usedInXml(SchemaUtil.treatAsXml(compositeType)) + .build(); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/mapper/UnionModelMapper.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/mapper/UnionModelMapper.java new file mode 100644 index 0000000000..44dfb3feee --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/mapper/UnionModelMapper.java @@ -0,0 +1,96 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.mapper; + +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.ObjectSchema; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.OrSchema; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Property; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Schema; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClassType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientModelProperty; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ImplementationDetails; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.UnionModel; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.UnionModels; +import com.microsoft.typespec.http.client.generator.core.util.SchemaUtil; +import com.azure.core.util.CoreUtils; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class UnionModelMapper implements IMapper> { + + private static final UnionModelMapper INSTANCE = new UnionModelMapper(); + private final UnionModels serviceModels = UnionModels.getInstance(); + + protected UnionModelMapper() { + } + + public static UnionModelMapper getInstance() { + return INSTANCE; + } + + @Override + public List map(OrSchema type) { + return Collections.emptyList(); + } + + private List createSubClasses(OrSchema type) { + ClassType baseModelType = Mappers.getUnionMapper().map(type); + String baseModelName = baseModelType.getName(); + List models = serviceModels.getModel(baseModelType.getName()); + if (models == null) { + models = new ArrayList<>(); + + // superclass + UnionModel.Builder builder = new UnionModel.Builder() + .name(baseModelName) + .packageName(baseModelType.getPackage()) + .implementationDetails(new ImplementationDetails.Builder() + .usages(SchemaUtil.mapSchemaContext(type.getUsage())) + .build()); + processDescription(builder, type); + + models.add(builder.build()); + + // subclasses + for (ObjectSchema subtype : type.getAnyOf()) { + String name = subtype.getLanguage().getJava().getName(); + builder.name(name) + .parentModelName(baseModelName); + processDescription(builder, subtype); + + // import + Set imports = new HashSet<>(); + imports.add(baseModelType.getFullName()); + builder.imports(new ArrayList<>(imports)); + + // property + List properties = new ArrayList<>(); + for (Property property : subtype.getProperties()) { + ClientModelProperty modelProperty = Mappers.getModelPropertyMapper().map(property); + properties.add(modelProperty); + } + builder.properties(properties); + + models.add(builder.build()); + } + + serviceModels.addModel(models); + } + return models; + } + + private static void processDescription(UnionModel.Builder builder, Schema type) { + String summary = type.getSummary(); + String description = type.getLanguage().getJava() == null ? null : type.getLanguage().getJava().getDescription(); + if (CoreUtils.isNullOrEmpty(summary) && CoreUtils.isNullOrEmpty(description)) { + builder.description(String.format("The %s model.", type.getLanguage().getJava().getName())); + } else { + builder.description(SchemaUtil.mergeSummaryWithDescription(summary, description)); + } + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/Annotation.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/Annotation.java new file mode 100644 index 0000000000..5949d92359 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/Annotation.java @@ -0,0 +1,120 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.model.clientmodel; + +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; + +import java.util.Set; + +public class Annotation { + private static final String CLIENTCORE_PACKAGE = "io.clientcore.core.http.annotation"; + + public static final Annotation GENERATED = new Annotation.Builder().knownClass( + com.azure.core.annotation.Generated.class).build(); + + public static final Annotation HOST = new Annotation.Builder().knownClass(com.azure.core.annotation.Host.class) + .build(); + + public static final Annotation SERVICE_INTERFACE = new Annotation.Builder().knownClass( + com.azure.core.annotation.ServiceInterface.class).build(); + + public static final Annotation SERVICE_CLIENT = new Annotation.Builder().knownClass( + com.azure.core.annotation.ServiceClient.class).build(); + + public static final Annotation SERVICE_METHOD = new Annotation.Builder().knownClass( + com.azure.core.annotation.ServiceMethod.class).build(); + + public static final Annotation SERVICE_CLIENT_BUILDER = new Annotation.Builder().knownClass( + com.azure.core.annotation.ServiceClientBuilder.class).build(); + + public static final Annotation UNEXPECTED_RESPONSE_EXCEPTION_TYPE = new Annotation.Builder().knownClass( + com.azure.core.annotation.UnexpectedResponseExceptionType.class).build(); + + public static final Annotation EXPECTED_RESPONSE = new Annotation.Builder().knownClass( + com.azure.core.annotation.ExpectedResponses.class).build(); + + public static final Annotation HEADERS = new Annotation.Builder().knownClass( + com.azure.core.annotation.Headers.class).build(); + + public static final Annotation FORM_PARAM = new Annotation.Builder().knownClass( + com.azure.core.annotation.FormParam.class).build(); + + public static final Annotation RETURN_VALUE_WIRE_TYPE = new Annotation.Builder().knownClass( + com.azure.core.annotation.ReturnValueWireType.class).build(); + + public static final Annotation RETURN_TYPE = new Annotation.Builder().knownClass( + com.azure.core.annotation.ReturnType.class).build(); + + public static final Annotation IMMUTABLE = new Annotation.Builder().knownClass( + com.azure.core.annotation.Immutable.class).build(); + + public static final Annotation FLUENT = new Annotation.Builder().knownClass(com.azure.core.annotation.Fluent.class) + .build(); + + public static final Annotation HEADER_COLLECTION = new Annotation.Builder().knownClass( + com.azure.core.annotation.HeaderCollection.class).build(); + + public static final Annotation METADATA = new Annotation("io.clientcore.core.annotation", "Metadata"); + + public static final Annotation HTTP_REQUEST_INFORMATION = new Annotation(CLIENTCORE_PACKAGE, + "HttpRequestInformation"); + public static final Annotation UNEXPECTED_RESPONSE_EXCEPTION_INFORMATION = new Annotation(CLIENTCORE_PACKAGE, + "UnexpectedResponseExceptionDetail"); + public static final Annotation TYPE_CONDITIONS = new Annotation(CLIENTCORE_PACKAGE, "TypeConditions"); + + private final String fullName; + private final String packageName; + private final String name; + + private Annotation(String packageName, String name) { + this.packageName = packageName; + this.name = name; + this.fullName = packageName + "." + name; + } + + public final String getPackage() { + return packageName; + } + + public final String getName() { + return name; + } + + public final void addImportsTo(Set imports) { + imports.add(fullName); + } + + public static class Builder { + + private String packageName; + private String name; + + public Builder packageName(String packageName) { + this.packageName = packageName; + return this; + } + + public Builder name(String name) { + this.name = name; + return this; + } + + public Builder knownClass(Class clazz) { + this.packageName(clazz.getPackage().getName()).name(clazz.getSimpleName()); + + if (!JavaSettings.getInstance().isBranded()) { + this.packageName(clazz.getPackage() + .getName() + .replace(ExternalPackage.AZURE_CORE_PACKAGE_NAME, ExternalPackage.CLIENTCORE_PACKAGE_NAME) + .replace(ExternalPackage.AZURE_JSON_PACKAGE_NAME, ExternalPackage.CLIENTCORE_JSON_PACKAGE_NAME)); + } + + return this; + } + + public Annotation build() { + return new Annotation(packageName, name); + } + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/ArrayType.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/ArrayType.java new file mode 100644 index 0000000000..609bdd6f20 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/ArrayType.java @@ -0,0 +1,143 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.model.clientmodel; + +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; + +import java.util.Set; +import java.util.function.Function; + + +/** + * The details of an array type that is used by a client. + */ +public class ArrayType implements IType { + /** + * The {@code byte[]} type. + */ + public static final ArrayType BYTE_ARRAY = new ArrayType(PrimitiveType.BYTE, + defaultValueExpression -> { + if (defaultValueExpression != null) { + return String.format("\"%1$s\".getBytes()", defaultValueExpression); + } else { + return JavaSettings.getInstance().isNullByteArrayMapsToEmptyArray() ? "EMPTY_BYTE_ARRAY" : "null"; + } + }); + + private final String toStringValue; + private final IType elementType; + private final Function defaultValueExpressionConverter; + + private ArrayType(IType elementType, Function defaultValueExpressionConverter) { + this.toStringValue = elementType + "[]"; + this.elementType = elementType; + this.defaultValueExpressionConverter = defaultValueExpressionConverter; + } + + /** + * Gets the element type of the array. + * + * @return The element type of the array. + */ + public final IType getElementType() { + return elementType; + } + + @Override + public String toString() { + return toStringValue; + } + + @Override + public final IType asNullable() { + return this; + } + + @Override + public final boolean contains(IType type) { + return this == type || getElementType().contains(type); + } + + @Override + public final void addImportsTo(Set imports, boolean includeImplementationImports) { + getElementType().addImportsTo(imports, includeImplementationImports); + } + + @Override + public final String defaultValueExpression(String sourceExpression) { + return defaultValueExpressionConverter.apply(sourceExpression); + } + + @Override + public String defaultValueExpression() { + return defaultValueExpression(null); + } + + @Override + public final IType getClientType() { + // The only supported array type is byte[] + return this; + } + + @Override + public final String convertToClientType(String expression) { + // The only supported array type is byte[] + return expression; + } + + @Override + public final String convertFromClientType(String expression) { + // The only supported array type is byte[] + return expression; + } + + @Override + public String validate(String expression) { + return null; + } + + @Override + public String jsonToken() { + return "JsonToken.START_ARRAY"; + } + + @Override + public String jsonDeserializationMethod(String jsonReaderName) { + return jsonReaderName + ".getBinary()"; + } + + @Override + public String jsonSerializationMethodCall(String jsonWriterName, String fieldName, String valueGetter, + boolean jsonMergePatch) { + return fieldName == null + ? String.format("%s.writeBinary(%s)", jsonWriterName, valueGetter) + : String.format("%s.writeBinaryField(\"%s\", %s)", jsonWriterName, fieldName, valueGetter); + } + + @Override + public String xmlDeserializationMethod(String xmlReaderName, String attributeName, String attributeNamespace, + boolean namespaceIsConstant) { + if (attributeName == null) { + return xmlReaderName + ".getBinaryElement()"; + } else if (attributeNamespace == null) { + return xmlReaderName + ".getBinaryAttribute(null, \"" + attributeName + "\")"; + } else { + return namespaceIsConstant + ? xmlReaderName + ".getBinaryAttribute(" + attributeNamespace + ", \"" + attributeName + "\")" + : xmlReaderName + ".getBinaryAttribute(\"" + attributeNamespace + "\", \"" + attributeName + "\")"; + } + } + + @Override + public String xmlSerializationMethodCall(String xmlWriterName, String attributeOrElementName, String namespaceUri, + String valueGetter, boolean isAttribute, boolean nameIsVariable, boolean namespaceIsConstant) { + return ClassType.xmlSerializationCallHelper(xmlWriterName, "writeBinary", attributeOrElementName, namespaceUri, + valueGetter, isAttribute, nameIsVariable, namespaceIsConstant); + } + + @Override + public boolean isUsedInXml() { + return false; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/AsyncSyncClient.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/AsyncSyncClient.java new file mode 100644 index 0000000000..025b3c287d --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/AsyncSyncClient.java @@ -0,0 +1,201 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.model.clientmodel; + +import java.util.Collections; +import java.util.List; +import java.util.Set; + +/** + * An asynchronous and/or synchronous client. + */ +public class AsyncSyncClient { + + private final String className; + private final String packageName; + + private final MethodGroupClient methodGroupClient; + + private final ServiceClient serviceClient; + + private final List convenienceMethods; + private final String crossLanguageDefinitionId; + + // There is also reference from Client to ClientBuilder via "@ServiceClient(builder = ClientBuilder.class)" + // clientBuilder can be null, if builder is disabled via "disable-client-builder" + private ClientBuilder clientBuilder; + + private AsyncSyncClient(String packageName, String className, + MethodGroupClient methodGroupClient, ServiceClient serviceClient, + List convenienceMethods, String crossLanguageDefinitionId) { + this.packageName = packageName; + this.className = className; + this.methodGroupClient = methodGroupClient; + this.serviceClient = serviceClient; + this.convenienceMethods = convenienceMethods; + this.crossLanguageDefinitionId = crossLanguageDefinitionId; + } + + public String getCrossLanguageDefinitionId() { + return crossLanguageDefinitionId; + } + + /** + * Get the package name. + * + * @return the package name. + */ + public String getPackageName() { + return packageName; + } + + /** + * Get the class name. + * + * @return the class name. + */ + public String getClassName() { + return className; + } + + /** + * Get the method group client. + * + * @return the method group client. + */ + public MethodGroupClient getMethodGroupClient() { + return methodGroupClient; + } + + /** + * Get the service client. + * + * @return the service client. + */ + public ServiceClient getServiceClient() { + return serviceClient; + } + + /** + * Gets the list of convenience methods. + * + * @return the list of convenience methods. + */ + public List getConvenienceMethods() { + return convenienceMethods; + } + + /** + * Adds the imports required by the client to the set of imports. + * + * @param imports The imports being added to. + * @param includeImplementationImports Whether implementation imports should be included. + */ + public void addImportsTo(Set imports, boolean includeImplementationImports) { + imports.add(packageName + "." + className); + } + + /** + * Gets the {@link Builder} associated with this client. + * + * @return The {@link Builder} associated with this client. + */ + public ClientBuilder getClientBuilder() { + return clientBuilder; + } + + /** + * Sets the {@link Builder} to associate with this client. + * + * @param clientBuilder The {@link Builder} to associate with this client. + */ + public void setClientBuilder(ClientBuilder clientBuilder) { + this.clientBuilder = clientBuilder; + } + + /** + * A builder that is used to create instance of {@link AsyncSyncClient}. + */ + public static class Builder { + + private String className; + private String packageName; + + private MethodGroupClient methodGroupClient; + + private ServiceClient serviceClient; + + private List convenienceMethods = Collections.emptyList(); + private String crossLanguageDefinitionId; + + /** + * Sets the class name. + * + * @param className The class name. + * @return This builder. + */ + public Builder className(String className) { + this.className = className; + return this; + } + + /** + * Sets the package name. + * + * @param packageName The package name. + * @return This builder. + */ + public Builder packageName(String packageName) { + this.packageName = packageName; + return this; + } + + /** + * Sets the {@link MethodGroupClient}. + * + * @param methodGroupClient The {@link MethodGroupClient}. + * @return This builder. + */ + public Builder methodGroupClient(MethodGroupClient methodGroupClient) { + this.methodGroupClient = methodGroupClient; + return this; + } + + /** + * Sets the {@link ServiceClient}. + * + * @param serviceClient The {@link ServiceClient}. + * @return This builder. + */ + public Builder serviceClient(ServiceClient serviceClient) { + this.serviceClient = serviceClient; + return this; + } + + /** + * Sets the list of {@link ConvenienceMethod ConvenienceMethods}. + * + * @param convenienceMethods The list of {@link ConvenienceMethod ConvenienceMethods}. + * @return This builder. + */ + public Builder convenienceMethods(List convenienceMethods) { + this.convenienceMethods = convenienceMethods; + return this; + } + + public Builder crossLanguageDefinitionId(String crossLanguageDefinitionId) { + this.crossLanguageDefinitionId = crossLanguageDefinitionId; + return this; + } + + /** + * Builds an instance of {@link AsyncSyncClient}. + * + * @return The instance of {@link AsyncSyncClient}. + */ + public AsyncSyncClient build() { + return new AsyncSyncClient(packageName, className, methodGroupClient, serviceClient, convenienceMethods, crossLanguageDefinitionId); + } + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/ClassType.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/ClassType.java new file mode 100644 index 0000000000..144c3515e4 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/ClassType.java @@ -0,0 +1,935 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.model.clientmodel; + +import com.microsoft.typespec.http.client.generator.core.extension.model.extensionmodel.XmsExtensions; +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; +import com.microsoft.typespec.http.client.generator.core.util.TemplateUtil; +import com.azure.core.client.traits.ConfigurationTrait; +import com.azure.core.client.traits.EndpointTrait; +import com.azure.core.client.traits.HttpTrait; +import com.azure.core.client.traits.KeyCredentialTrait; +import com.azure.core.credential.AzureKeyCredential; +import com.azure.core.credential.KeyCredential; +import com.azure.core.credential.TokenCredential; +import com.azure.core.exception.ClientAuthenticationException; +import com.azure.core.exception.HttpResponseException; +import com.azure.core.exception.ResourceExistsException; +import com.azure.core.exception.ResourceModifiedException; +import com.azure.core.exception.ResourceNotFoundException; +import com.azure.core.exception.TooManyRedirectsException; +import com.azure.core.http.HttpClient; +import com.azure.core.http.HttpHeaderName; +import com.azure.core.http.HttpHeaders; +import com.azure.core.http.HttpPipeline; +import com.azure.core.http.HttpPipelineBuilder; +import com.azure.core.http.HttpRequest; +import com.azure.core.http.MatchConditions; +import com.azure.core.http.ProxyOptions; +import com.azure.core.http.RequestConditions; +import com.azure.core.http.policy.HttpLogOptions; +import com.azure.core.http.policy.HttpPipelinePolicy; +import com.azure.core.http.policy.KeyCredentialPolicy; +import com.azure.core.http.policy.RedirectPolicy; +import com.azure.core.http.policy.RetryOptions; +import com.azure.core.http.policy.RetryPolicy; +import com.azure.core.http.rest.RequestOptions; +import com.azure.core.http.rest.Response; +import com.azure.core.http.rest.RestProxy; +import com.azure.core.http.rest.SimpleResponse; +import com.azure.core.http.rest.StreamResponse; +import com.azure.core.models.JsonPatchDocument; +import com.azure.core.models.ResponseError; +import com.azure.core.util.Base64Url; +import com.azure.core.util.BinaryData; +import com.azure.core.util.ClientOptions; +import com.azure.core.util.Configuration; +import com.azure.core.util.Context; +import com.azure.core.util.CoreUtils; +import com.azure.core.util.DateTimeRfc1123; +import com.azure.core.util.ExpandableStringEnum; +import com.azure.core.util.logging.ClientLogger; +import com.azure.core.util.logging.LogLevel; +import com.azure.core.util.polling.PollOperationDetails; +import com.azure.core.util.serializer.JsonSerializer; +import com.azure.core.util.serializer.SerializerAdapter; +import com.azure.core.util.serializer.TypeReference; +import com.azure.json.JsonReader; +import com.azure.json.JsonSerializable; +import com.azure.json.JsonToken; +import com.azure.json.JsonWriter; + +import java.io.InputStream; +import java.math.BigDecimal; +import java.nio.ByteBuffer; +import java.time.Duration; +import java.time.Instant; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.Function; + +/** + * The details of a class type that is used by a client. + */ +public class ClassType implements IType { + + + private static class ClassDetails { + + private final Class azureClass; + private final String genericClass; + + public ClassDetails(Class azureClass, String genericClass) { + this.azureClass = azureClass; + this.genericClass = genericClass; + } + + public String getAzureClass() { + return azureClass.getName(); + } + + public String getGenericClass() { + return genericClass; + } + + } + + private static final Map, ClassDetails> CLASS_TYPE_MAPPING = new HashMap, ClassDetails>() {{ + put(RestProxy.class, new ClassDetails(RestProxy.class, "io.clientcore.core.http.RestProxy")); + put(HttpPipeline.class, new ClassDetails(HttpPipeline.class, "io.clientcore.core.http.pipeline.HttpPipeline")); + put(HttpPipelineBuilder.class, new ClassDetails(HttpPipelineBuilder.class, "io.clientcore.core.http.pipeline.HttpPipelineBuilder")); + put(Context.class, new ClassDetails(Context.class, "io.clientcore.core.util.Context")); + put(HttpClient.class, new ClassDetails(HttpClient.class, "io.clientcore.core.http.client.HttpClient")); + put(HttpLogOptions.class, new ClassDetails(HttpLogOptions.class, "io.clientcore.core.http.models.HttpLogOptions")); + put(HttpPipelinePolicy.class, new ClassDetails(HttpPipelinePolicy.class, "io.clientcore.core.http.pipeline.HttpPipelinePolicy")); + put(KeyCredentialPolicy.class, new ClassDetails(KeyCredentialPolicy.class, "io.clientcore.core.http.pipeline.KeyCredentialPolicy")); + put(RetryPolicy.class, new ClassDetails(RetryPolicy.class, "io.clientcore.core.http.pipeline.HttpRetryPolicy")); + put(RedirectPolicy.class, new ClassDetails(RedirectPolicy.class, "io.clientcore.core.http.pipeline.HttpRedirectPolicy")); + put(Configuration.class, new ClassDetails(Configuration.class, "io.clientcore.core.util.configuration.Configuration")); + put(HttpHeaders.class, new ClassDetails(HttpHeaders.class, "io.clientcore.core.models.Headers")); + put(HttpHeaderName.class, new ClassDetails(HttpHeaderName.class, "io.clientcore.core.http.models.HttpHeaderName")); + put(HttpRequest.class, new ClassDetails(HttpRequest.class, "io.clientcore.core.http.models.HttpRequest")); + put(RequestOptions.class, new ClassDetails(RequestOptions.class, "io.clientcore.core.http.models.RequestOptions")); + put(BinaryData.class, new ClassDetails(BinaryData.class, "io.clientcore.core.util.binarydata.BinaryData")); + put(RetryOptions.class, new ClassDetails(RetryOptions.class, "io.clientcore.core.http.models.HttpRetryOptions")); + put(ProxyOptions.class, new ClassDetails(ProxyOptions.class, "io.clientcore.core.http.models.ProxyOptions")); + put(Response.class, new ClassDetails(Response.class, "io.clientcore.core.http.models.Response")); + put(SimpleResponse.class, new ClassDetails(SimpleResponse.class, "io.clientcore.core.http.SimpleResponse")); + put(ExpandableStringEnum.class, new ClassDetails(ExpandableStringEnum.class, "io.clientcore.core.util.ExpandableEnum")); + put(HttpResponseException.class, new ClassDetails(HttpResponseException.class, "io.clientcore.core.http.exception.HttpResponseException")); + put(HttpTrait.class, new ClassDetails(HttpTrait.class, "io.clientcore.core.models.traits.HttpTrait")); + put(ConfigurationTrait.class, new ClassDetails(ConfigurationTrait.class, "io.clientcore.core.models.traits.ConfigurationTrait")); + put(EndpointTrait.class, new ClassDetails(EndpointTrait.class, "io.clientcore.core.models.traits.EndpointTrait")); + put(KeyCredentialTrait.class, new ClassDetails(KeyCredentialTrait.class, "io.clientcore.core.models.traits.KeyCredentialTrait")); + put(TypeReference.class, new ClassDetails(TypeReference.class, "io.clientcore.core.models.TypeReference")); + put(ClientLogger.class, new ClassDetails(ClientLogger.class, "io.clientcore.core.util.ClientLogger")); + put(LogLevel.class, new ClassDetails(LogLevel.class, "io.clientcore.core.util.ClientLogger.LogLevel")); + }}; + + private static ClassType.Builder getClassTypeBuilder(Class classKey) { + if (!JavaSettings.getInstance().isBranded()) { + if (CLASS_TYPE_MAPPING.containsKey(classKey)) { + return new ClassType.Builder(false).knownClass(CLASS_TYPE_MAPPING.get(classKey).getGenericClass()); + } else { + return new Builder(false).packageName(classKey.getPackage().getName() + .replace(ExternalPackage.AZURE_CORE_PACKAGE_NAME, ExternalPackage.CLIENTCORE_PACKAGE_NAME) + .replace(ExternalPackage.AZURE_JSON_PACKAGE_NAME, ExternalPackage.CLIENTCORE_JSON_PACKAGE_NAME)) + .name(classKey.getSimpleName()); + } + } else { + if (CLASS_TYPE_MAPPING.containsKey(classKey)) { + return new ClassType.Builder(false).knownClass(CLASS_TYPE_MAPPING.get(classKey).getAzureClass()); + } else { + return new Builder(false).packageName(classKey.getPackage().getName()).name(classKey.getSimpleName()); + } + } + } + + public static final ClassType REQUEST_CONDITIONS = new Builder().knownClass(RequestConditions.class).build(); + public static final ClassType MATCH_CONDITIONS = new Builder().knownClass(MatchConditions.class).build(); + public static final ClassType CORE_UTILS = getClassTypeBuilder(CoreUtils.class).build(); + public static final ClassType RESPONSE = getClassTypeBuilder(Response.class).build(); + public static final ClassType SIMPLE_RESPONSE = getClassTypeBuilder(SimpleResponse.class).build(); + public static final ClassType EXPANDABLE_STRING_ENUM = getClassTypeBuilder(ExpandableStringEnum.class).build(); + public static final ClassType HTTP_PIPELINE_BUILDER = getClassTypeBuilder(HttpPipelineBuilder.class).build(); + public static final ClassType KEY_CREDENTIAL_POLICY = getClassTypeBuilder(KeyCredentialPolicy.class).build(); + public static final ClassType KEY_CREDENTIAL_TRAIT = getClassTypeBuilder(KeyCredentialTrait.class).build(); + public static final ClassType ENDPOINT_TRAIT = getClassTypeBuilder(EndpointTrait.class).build(); + public static final ClassType HTTP_TRAIT = getClassTypeBuilder(HttpTrait.class).build(); + public static final ClassType CONFIGURATION_TRAIT = getClassTypeBuilder(ConfigurationTrait.class).build(); + public static final ClassType PROXY_TRAIT = new ClassType.Builder(false) + .packageName("io.clientcore.core.models.traits").name("ProxyTrait") + .build(); + public static final ClassType POLL_OPERATION_DETAILS = getClassTypeBuilder(PollOperationDetails.class).build(); + public static final ClassType JSON_SERIALIZABLE = getClassTypeBuilder(JsonSerializable.class).build(); + public static final ClassType JSON_WRITER = getClassTypeBuilder(JsonWriter.class).build(); + public static final ClassType JSON_READER = getClassTypeBuilder(JsonReader.class).build(); + public static final ClassType JSON_TOKEN = getClassTypeBuilder(JsonToken.class).build(); + + public static final ClassType VOID = new ClassType.Builder(false).knownClass(Void.class).build(); + + public static final ClassType BOOLEAN = new Builder(false).knownClass(Boolean.class) + .defaultValueExpressionConverter(String::toLowerCase) + .jsonToken("JsonToken.BOOLEAN") + .jsonDeserializationMethod("getNullable(JsonReader::getBoolean)") + .serializationMethodBase("writeBoolean") + .xmlElementDeserializationMethod("getNullableElement(Boolean::parseBoolean)") + .xmlAttributeDeserializationTemplate("%s.getNullableAttribute(%s, %s, Boolean::parseBoolean)") + .build(); + + public static final ClassType BYTE = new Builder(false).knownClass(Byte.class) + .jsonDeserializationMethod("getNullable(JsonReader::getInt)") + .jsonToken("JsonToken.NUMBER") + .serializationMethodBase("writeNumber") + .xmlElementDeserializationMethod("getNullableElement(Byte::parseByte)") + .xmlAttributeDeserializationTemplate("%s.getNullableAttribute(%s, %s, Byte::parseByte)") + .build(); + + public static final ClassType INTEGER = new Builder(false).knownClass(Integer.class) + .defaultValueExpressionConverter(Function.identity()) + .jsonToken("JsonToken.NUMBER") + .jsonDeserializationMethod("getNullable(JsonReader::getInt)") + .serializationMethodBase("writeNumber") + .xmlElementDeserializationMethod("getNullableElement(Integer::parseInt)") + .xmlAttributeDeserializationTemplate("%s.getNullableAttribute(%s, %s, Integer::parseInt)") + .build(); + + public static final ClassType LONG = new Builder(false) + .prototypeAsLong() + .build(); + + public static final ClassType FLOAT = new Builder(false).knownClass(Float.class) + .defaultValueExpressionConverter(defaultValueExpression -> Float.parseFloat(defaultValueExpression) + "F") + .jsonToken("JsonToken.NUMBER") + .jsonDeserializationMethod("getNullable(JsonReader::getFloat)") + .serializationMethodBase("writeNumber") + .xmlElementDeserializationMethod("getNullableElement(Float::parseFloat)") + .xmlAttributeDeserializationTemplate("%s.getNullableAttribute(%s, %s, Float::parseFloat)") + .build(); + + public static final ClassType DOUBLE = new Builder(false).knownClass(Double.class) + .prototypeAsDouble() + .build(); + + public static final ClassType CHARACTER = new Builder(false).knownClass(Character.class) + .defaultValueExpressionConverter(defaultValueExpression -> String.valueOf((defaultValueExpression.charAt(0)))) + .jsonToken("JsonToken.STRING") + .serializationValueGetterModifier(valueGetter -> "Objects.toString(" + valueGetter + ", null)") + .jsonDeserializationMethod("getNullable(nonNullReader -> nonNullReader.getString().charAt(0))") + .serializationMethodBase("writeString") + .xmlElementDeserializationMethod("getNullableElement(nonNullString -> nonNullString.charAt(0))") + .xmlAttributeDeserializationTemplate("%s.getNullableAttribute(%s, %s, nonNullString -> nonNullString.charAt(0))") + .build(); + + public static final ClassType STRING = new Builder(false).knownClass(String.class) + .defaultValueExpressionConverter(defaultValueExpression -> "\"" + TemplateUtil.escapeString(defaultValueExpression) + "\"") + .jsonToken("JsonToken.STRING") + .jsonDeserializationMethod("getString()") + .serializationMethodBase("writeString") + .xmlElementDeserializationMethod("getStringElement()") + .xmlAttributeDeserializationTemplate("%s.getStringAttribute(%s, %s)") + .build(); + + public static final ClassType BASE_64_URL = getClassTypeBuilder(Base64Url.class) + .serializationValueGetterModifier(valueGetter -> "Objects.toString(" + valueGetter + ", null)") + .jsonToken("JsonToken.STRING") + .jsonDeserializationMethod("getNullable(nonNullReader -> new Base64Url(nonNullReader.getString()))") + .serializationMethodBase("writeString") + .xmlElementDeserializationMethod("getNullableElement(Base64Url::new)") + .xmlAttributeDeserializationTemplate("%s.getNullableAttribute(%s, %s, Base64Url::new)") + .build(); + + public static final ClassType ANDROID_BASE_64_URL = new ClassType.Builder(false) + .packageName("com.azure.android.core.util").name("Base64Url") + .build(); + + public static final ClassType LOCAL_DATE = new Builder(false).knownClass(java.time.LocalDate.class) + .defaultValueExpressionConverter(defaultValueExpression -> "LocalDate.parse(\"" + defaultValueExpression + "\")") + .jsonToken("JsonToken.STRING") + .serializationValueGetterModifier(valueGetter -> "Objects.toString(" + valueGetter + ", null)") + .jsonDeserializationMethod("getNullable(nonNullReader -> LocalDate.parse(nonNullReader.getString()))") + .serializationMethodBase("writeString") + .xmlElementDeserializationMethod("getNullableElement(LocalDate::parse)") + .xmlAttributeDeserializationTemplate("%s.getNullableAttribute(%s, %s, LocalDate::parse)") + .build(); + + public static final ClassType ANDROID_LOCAL_DATE = new ClassType.Builder(false) + .packageName("org.threeten.bp").name("LocalDate") + .build(); + + public static final ClassType DATE_TIME = new Builder(false).knownClass(OffsetDateTime.class) + .defaultValueExpressionConverter(defaultValueExpression -> "OffsetDateTime.parse(\"" + defaultValueExpression + "\")") + .jsonToken("JsonToken.STRING") + .serializationValueGetterModifier(valueGetter -> valueGetter + " == null ? null : DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(" + valueGetter + ")") + .jsonDeserializationMethod("getNullable(nonNullReader -> " + CORE_UTILS.getName() + ".parseBestOffsetDateTime(nonNullReader.getString()))") + .serializationMethodBase("writeString") + .xmlElementDeserializationMethod("getNullableElement(dateString -> " + CORE_UTILS.getName() + ".parseBestOffsetDateTime(dateString))") + .xmlAttributeDeserializationTemplate("%s.getNullableAttribute(%s, %s, dateString -> " + CORE_UTILS.getName() + ".parseBestOffsetDateTime(dateString))") + .build(); + + public static final ClassType DURATION = new Builder(false).knownClass(Duration.class) + .defaultValueExpressionConverter(defaultValueExpression -> "Duration.parse(\"" + defaultValueExpression + "\")") + .jsonToken("JsonToken.STRING") + .serializationValueGetterModifier(valueGetter -> CORE_UTILS.getName() + ".durationToStringWithDays(" + valueGetter + ")") + .jsonDeserializationMethod("getNullable(nonNullReader -> Duration.parse(nonNullReader.getString()))") + .serializationMethodBase("writeString") + .xmlElementDeserializationMethod("getNullableElement(Duration::parse)") + .xmlAttributeDeserializationTemplate("%s.getNullableAttribute(%s, %s, Duration::parse)") + .build(); + + public static final ClassType ANDROID_DURATION = new ClassType.Builder(false) + .packageName("org.threeten.bp").name("Duration") + .build(); + + public static final ClassType DATE_TIME_RFC_1123 = getClassTypeBuilder(DateTimeRfc1123.class) + .defaultValueExpressionConverter(defaultValueExpression -> "new DateTimeRfc1123(\"" + defaultValueExpression + "\")") + .jsonToken("JsonToken.STRING") + .serializationValueGetterModifier(valueGetter -> "Objects.toString(" + valueGetter + ", null)") + .jsonDeserializationMethod("getNullable(nonNullReader -> new DateTimeRfc1123(nonNullReader.getString()))") + .serializationMethodBase("writeString") + .xmlElementDeserializationMethod("getNullableElement(DateTimeRfc1123::new)") + .xmlAttributeDeserializationTemplate("%s.getNullableAttribute(%s, %s, DateTimeRfc1123::new)") + .build(); + + public static final ClassType ANDROID_DATE_TIME_RFC_1123 = new ClassType.Builder(false) + .packageName("com.azure.android.core.util").name("DateTimeRfc1123") + .build(); + + public static final ClassType BIG_DECIMAL = new Builder(false).knownClass(BigDecimal.class) + .defaultValueExpressionConverter(defaultValueExpression -> "new BigDecimal(\"" + defaultValueExpression + "\")") + .jsonToken("JsonToken.NUMBER") + .serializationMethodBase("writeNumber") + .jsonDeserializationMethod("getNullable(nonNullReader -> new BigDecimal(nonNullReader.getString()))") + .xmlElementDeserializationMethod("getNullableElement(BigDecimal::new)") + .xmlAttributeDeserializationTemplate("%s.getNullableAttribute(%s, %s, BigDecimal::new)") + .build(); + + public static final ClassType UUID = new Builder(false).knownClass(java.util.UUID.class) + .defaultValueExpressionConverter(defaultValueExpression -> "UUID.fromString(\"" + defaultValueExpression + "\")") + .jsonToken("JsonToken.STRING") + .serializationValueGetterModifier(valueGetter -> "Objects.toString(" + valueGetter + ", null)") + .jsonDeserializationMethod("getNullable(nonNullReader -> UUID.fromString(nonNullReader.getString()))") + .serializationMethodBase("writeString") + .xmlElementDeserializationMethod("getNullableElement(UUID::fromString)") + .xmlAttributeDeserializationTemplate("%s.getNullableAttribute(%s, %s, UUID::fromString)") + .build(); + + public static final ClassType OBJECT = new ClassType.Builder(false) + .knownClass(Object.class) + .build(); + + public static final ClassType TOKEN_CREDENTIAL = new ClassType.Builder(false).knownClass(TokenCredential.class) + .build(); + + public static final ClassType ANDROID_HTTP_RESPONSE_EXCEPTION = new ClassType.Builder(false) + .packageName("com.azure.android.core.http.exception").name("HttpResponseException") + .build(); + + public static final ClassType UNIX_TIME_DATE_TIME = new ClassType.Builder(false) + .defaultValueExpressionConverter(defaultValueExpression -> "OffsetDateTime.parse(\"" + defaultValueExpression + "\")") + .jsonToken("JsonToken.STRING") + .knownClass(OffsetDateTime.class) + .serializationValueGetterModifier(valueGetter -> "Objects.toString(" + valueGetter + ", null)") + .jsonDeserializationMethod("getNullable(nonNullReader -> OffsetDateTime.parse(nonNullReader.getString()))") + .serializationMethodBase("writeString") + .xmlElementDeserializationMethod("getNullableElement(OffsetDateTime::parse)") + .xmlAttributeDeserializationTemplate("%s.getNullableAttribute(%s, %s, OffsetDateTime::parse)") + .build(); + + public static final ClassType ANDROID_DATE_TIME = new ClassType.Builder(false) + .packageName("org.threeten.bp").name("OffsetDateTime") + .build(); + + public static final ClassType UNIX_TIME_LONG = new ClassType.Builder(false) + .prototypeAsLong() + .build(); + + public static final ClassType DURATION_LONG = new ClassType.Builder(false) + .prototypeAsLong() + .build(); + + public static final ClassType DURATION_DOUBLE = new ClassType.Builder(false) + .prototypeAsDouble() + .build(); + + public static final ClassType HTTP_PIPELINE = getClassTypeBuilder(HttpPipeline.class).build(); + + public static final ClassType ANDROID_HTTP_PIPELINE = new ClassType.Builder(false) + .packageName("com.azure.android.core.http").name("HttpPipeline") + .build(); + + public static final ClassType REST_PROXY = getClassTypeBuilder(RestProxy.class).build(); + + public static final ClassType ANDROID_REST_PROXY = new ClassType.Builder(false) + .packageName("com.azure.android.core.rest").name("RestProxy") + .build(); + + public static final ClassType SERIALIZER_ADAPTER = new ClassType.Builder(false).knownClass(SerializerAdapter.class) + .build(); + public static final ClassType JSON_SERIALIZER = getClassTypeBuilder(JsonSerializer.class) + .build(); + + public static final ClassType ANDROID_JACKSON_SERDER = new ClassType.Builder(false) + .packageName("com.azure.android.core.serde.jackson").name("JacksonSerder") + .build(); + + public static final ClassType FUNCTION = new ClassType.Builder(false).knownClass(Function.class).build(); + + public static final ClassType BYTE_BUFFER = new ClassType.Builder(false).knownClass(ByteBuffer.class).build(); + + public static final ClassType URL = new Builder(false) + .defaultValueExpressionConverter(defaultValueExpression -> "new URL(\"" + defaultValueExpression + "\")") + .knownClass(java.net.URL.class) + .jsonToken("JsonToken.STRING") + .serializationValueGetterModifier(valueGetter -> "Objects.toString(" + valueGetter + ", null)") + .jsonDeserializationMethod("getNullable(nonNullReader -> new URL(nonNullReader.getString()))") + .serializationMethodBase("writeString") + .xmlElementDeserializationMethod("getNullableElement(urlString -> { try { return new URL(urlString); } catch (MalformedURLException e) { throw new XMLStreamException(e); } })") + .xmlAttributeDeserializationTemplate("%s.getNullableAttribute(%s, %s, URL::new)") + .build(); + + public static final ClassType STREAM_RESPONSE = new ClassType.Builder(false).knownClass(StreamResponse.class) + .build(); + + public static final ClassType INPUT_STREAM = new ClassType.Builder(false).knownClass(InputStream.class) + .build(); + + public static final ClassType CONTEXT = ClassType.getClassTypeBuilder(Context.class) + .defaultValueExpressionConverter(epr -> "com.azure.core.util.Context.NONE") + .build(); + + public static final ClassType ANDROID_CONTEXT = new ClassType.Builder(false) + .packageName("com.azure.android.core.util").name("Context") + .build(); + + public static final ClassType CLIENT_LOGGER = ClassType.getClassTypeBuilder(ClientLogger.class).build(); + public static final ClassType LOG_LEVEL = ClassType.getClassTypeBuilder(LogLevel.class).build(); + + public static final ClassType AZURE_ENVIRONMENT = new ClassType.Builder(false) + .packageName("com.azure.core.management").name("AzureEnvironment") + .build(); + + public static final ClassType HTTP_CLIENT = getClassTypeBuilder(HttpClient.class).build(); + + public static final ClassType ANDROID_HTTP_CLIENT = new ClassType.Builder(false) + .packageName("com.azure.android.core.http").name("HttpClient") + .build(); + + public static final ClassType HTTP_PIPELINE_POLICY = getClassTypeBuilder(HttpPipelinePolicy.class).build(); + + public static final ClassType ANDROID_HTTP_PIPELINE_POLICY = new ClassType.Builder(false) + .packageName("com.azure.android.core.http").name("HttpPipelinePolicy") + .build(); + + public static final ClassType HTTP_LOG_OPTIONS = getClassTypeBuilder(HttpLogOptions.class).build(); + + public static final ClassType ANDROID_HTTP_LOG_OPTIONS = new ClassType.Builder(false) + .packageName("com.azure.android.core.http.policy").name("HttpLogOptions") + .build(); + + public static final ClassType CONFIGURATION = getClassTypeBuilder(Configuration.class).build(); + + public static final ClassType SERVICE_VERSION = new ClassType.Builder(false).knownClass(ServiceVersion.class) + .build(); + + public static final ClassType AZURE_KEY_CREDENTIAL = new ClassType.Builder(false) + .knownClass(AzureKeyCredential.class) + .build(); + + public static final ClassType KEY_CREDENTIAL = getClassTypeBuilder(KeyCredential.class).build(); + + public static final ClassType RETRY_POLICY = getClassTypeBuilder(RetryPolicy.class).build(); + public static final ClassType REDIRECT_POLICY = getClassTypeBuilder(RedirectPolicy.class).build(); + + public static final ClassType RETRY_OPTIONS = getClassTypeBuilder(RetryOptions.class).build(); + + public static final ClassType REDIRECT_OPTIONS = new ClassType.Builder(false) + .packageName("io.clientcore.core.http.models").name("HttpRedirectOptions") + .build(); + + public static final ClassType ANDROID_RETRY_POLICY = new ClassType.Builder(false) + .packageName("com.azure.android.core.http.policy").name("RetryPolicy") + .build(); + + public static final ClassType JSON_PATCH_DOCUMENT = new ClassType.Builder(false).knownClass(JsonPatchDocument.class) + .jsonToken("JsonToken.START_OBJECT") + .build(); + + public static final ClassType BINARY_DATA = getClassTypeBuilder(BinaryData.class) + .defaultValueExpressionConverter(defaultValueExpression -> "BinaryData.fromObject(\"" + defaultValueExpression + "\")") + // When used as model property, serialization code will not use the "writeUntyped(nullableVar)", + // because some backend would fail the request on "null" value. + .serializationMethodBase("writeUntyped") + .serializationValueGetterModifier(valueGetter -> valueGetter + " == null ? null : " + valueGetter + ".toObject(Object.class)") + .jsonDeserializationMethod("getNullable(nonNullReader -> BinaryData.fromObject(nonNullReader.readUntyped()))") + .xmlElementDeserializationMethod("getNullableElement(BinaryData::fromObject)") + .xmlAttributeDeserializationTemplate("%s.getNullableAttribute(%s, %s, BinaryData::fromObject)") + .build(); + + public static final ClassType REQUEST_OPTIONS = getClassTypeBuilder(RequestOptions.class).build(); + public static final ClassType PROXY_OPTIONS = getClassTypeBuilder(ProxyOptions.class).build(); + public static final ClassType CLIENT_OPTIONS = getClassTypeBuilder(ClientOptions.class).build(); + public static final ClassType HTTP_REQUEST = getClassTypeBuilder(HttpRequest.class).build(); + public static final ClassType HTTP_HEADERS = getClassTypeBuilder(HttpHeaders.class).build(); + public static final ClassType HTTP_HEADER_NAME = getClassTypeBuilder(HttpHeaderName.class).build(); + + // Java exception types + public static final ClassType HTTP_RESPONSE_EXCEPTION = getClassTypeBuilder(HttpResponseException.class).build(); + public static final ClassType CLIENT_AUTHENTICATION_EXCEPTION = getClassTypeBuilder(ClientAuthenticationException.class) + .build(); + public static final ClassType RESOURCE_EXISTS_EXCEPTION = getClassTypeBuilder(ResourceExistsException.class) + .build(); + public static final ClassType RESOURCE_MODIFIED_EXCEPTION = getClassTypeBuilder(ResourceModifiedException.class) + .build(); + public static final ClassType RESOURCE_NOT_FOUND_EXCEPTION = getClassTypeBuilder(ResourceNotFoundException.class) + .build(); + public static final ClassType TOO_MANY_REDIRECTS_EXCEPTION = getClassTypeBuilder(TooManyRedirectsException.class) + .build(); + public static final ClassType RESPONSE_ERROR = new Builder() + .knownClass(ResponseError.class) + .jsonToken("JsonToken.START_OBJECT") + .build(); + public static final ClassType RESPONSE_INNER_ERROR = new Builder() + .packageName("com.azure.core.models").name("ResponseInnerError") + .jsonToken("JsonToken.START_OBJECT") + .build(); + + private final String fullName; + private final String packageName; + private final String name; + private final List implementationImports; + private final XmsExtensions extensions; + private final Function defaultValueExpressionConverter; + private final boolean isSwaggerType; + private final Function serializationValueGetterModifier; + private final String jsonToken; + private final String serializationMethodBase; + private final String jsonDeserializationMethod; + private final String xmlAttributeDeserializationTemplate; + private final String xmlElementDeserializationMethod; + private final boolean usedInXml; + + private ClassType(String packageKeyword, String name, List implementationImports, XmsExtensions extensions, + Function defaultValueExpressionConverter, boolean isSwaggerType, String jsonToken, + String serializationMethodBase, Function serializationValueGetterModifier, + String jsonDeserializationMethod, String xmlAttributeDeserializationTemplate, + String xmlElementDeserializationMethod, boolean usedInXml) { + this.fullName = packageKeyword + "." + name; + this.packageName = packageKeyword; + this.name = name; + this.implementationImports = implementationImports; + this.extensions = extensions; + this.defaultValueExpressionConverter = defaultValueExpressionConverter; + this.isSwaggerType = isSwaggerType; + this.jsonToken = jsonToken; + this.serializationMethodBase = serializationMethodBase; + this.serializationValueGetterModifier = serializationValueGetterModifier; + this.jsonDeserializationMethod = jsonDeserializationMethod; + this.xmlAttributeDeserializationTemplate = xmlAttributeDeserializationTemplate; + this.xmlElementDeserializationMethod = xmlElementDeserializationMethod; + this.usedInXml = usedInXml; + } + + public final String getPackage() { + return packageName; + } + + public final String getName() { + return name; + } + + private List getImplementationImports() { + return implementationImports; + } + + public XmsExtensions getExtensions() { + return extensions; + } + + private Function getDefaultValueExpressionConverter() { + return defaultValueExpressionConverter; + } + + public final boolean isBoxedType() { + // TODO (alzimmer): This should be a property on the ClassType + return this.equals(ClassType.VOID) + || this.equals(ClassType.BOOLEAN) + || this.equals(ClassType.BYTE) + || this.equals(ClassType.INTEGER) + || this.equals(ClassType.LONG) + || this.equals(ClassType.FLOAT) + || this.equals(ClassType.DOUBLE); + } + + @Override + public String toString() { + return name; + } + + @Override + public boolean equals(final Object other) { + if (this == other) { + return true; + } + if (!(other instanceof ClassType)) { + return false; + } + ClassType that = (ClassType) other; + return Objects.equals(this.name, that.name) && Objects.equals(this.packageName, that.packageName); + } + + @Override + public int hashCode() { + return Objects.hash(packageName, name); + } + + public final IType asNullable() { + return this; + } + + public final boolean contains(IType type) { + return this.equals(type); + } + + public final String getFullName() { + return fullName; + } + + public final void addImportsTo(Set imports, boolean includeImplementationImports) { + if (!getPackage().equals("java.lang")) { + imports.add(fullName); + } + + if (this == ClassType.UNIX_TIME_LONG) { + imports.add(Instant.class.getName()); + imports.add(ZoneOffset.class.getName()); + } + + if (this == ClassType.DATE_TIME) { + imports.add(DateTimeFormatter.class.getName()); + } + + if (this == ClassType.URL) { + imports.add(java.net.URL.class.getName()); + imports.add(java.net.MalformedURLException.class.getName()); + } + + if (includeImplementationImports && getImplementationImports() != null) { + imports.addAll(getImplementationImports()); + } + } + + public final String defaultValueExpression(String sourceExpression) { + String result = sourceExpression; + if (result != null) { + if (getDefaultValueExpressionConverter() != null) { + result = defaultValueExpressionConverter.apply(sourceExpression); + } else { + result = "new " + this + "()"; + } + } + return result; + } + + @Override + public String defaultValueExpression() { + return "null"; + } + + public final IType getClientType() { + IType clientType = this; + if (this == ClassType.DATE_TIME_RFC_1123) { + clientType = ClassType.DATE_TIME; + } else if (this == ClassType.UNIX_TIME_LONG) { + clientType = ClassType.DATE_TIME; + } else if (this == ClassType.BASE_64_URL) { + clientType = ArrayType.BYTE_ARRAY; + } else if (this == ClassType.DURATION_LONG) { + clientType = ClassType.DURATION; + } else if (this == ClassType.DURATION_DOUBLE) { + clientType = ClassType.DURATION; + } + return clientType; + } + + public String convertToClientType(String expression) { + if (this == ClassType.DATE_TIME_RFC_1123 || this == ClassType.ANDROID_DATE_TIME_RFC_1123) { + expression = expression + ".getDateTime()"; + } else if (this == ClassType.UNIX_TIME_LONG) { + expression = "OffsetDateTime.ofInstant(Instant.ofEpochSecond(" + expression + "), ZoneOffset.UTC)"; + } else if (this == ClassType.BASE_64_URL) { + expression = expression + ".decodedBytes()"; + } else if (this == ClassType.URL) { + expression = "new URL(" + expression + ")"; + } else if (this == ClassType.DURATION_LONG) { + expression = "Duration.ofSeconds(" + expression + ")"; + } else if (this == ClassType.DURATION_DOUBLE) { + expression = "Duration.ofNanos((long) (" + expression + " * 1000_000_000L))"; + } + + return expression; + } + + public String convertFromClientType(String expression) { + if (this == ClassType.DATE_TIME_RFC_1123 || this == ClassType.ANDROID_DATE_TIME_RFC_1123) { + expression = "new DateTimeRfc1123(" + expression + ")"; + } else if (this == ClassType.UNIX_TIME_LONG) { + expression = expression + ".toEpochSecond()"; + } else if (this == ClassType.BASE_64_URL) { + expression = "Base64Url.encode(" + expression + ")"; + } else if (this == ClassType.URL) { + expression = expression + ".toString()"; + } else if (this == ClassType.DURATION_LONG) { + expression = expression + ".getSeconds()"; + } else if (this == ClassType.DURATION_DOUBLE) { + expression = "(double) " + expression + ".toNanos() / 1000_000_000L"; + } + + return expression; + } + + public String validate(String expression) { + if (packageName.startsWith(JavaSettings.getInstance().getPackage())) { + return expression + ".validate()"; + } else { + return null; + } + } + + public boolean isSwaggerType() { + return isSwaggerType; + } + + @Override + public String jsonToken() { + return jsonToken; + } + + @Override + public String jsonDeserializationMethod(String jsonReaderName) { + if (jsonDeserializationMethod == null) { + return null; + } + + return jsonReaderName + "." + jsonDeserializationMethod; + } + + @Override + public String jsonSerializationMethodCall(String jsonWriterName, String fieldName, String valueGetter, + boolean jsonMergePatch) { + if (!isSwaggerType && CoreUtils.isNullOrEmpty(serializationMethodBase)) { + return null; + } + + String methodBase = isSwaggerType ? "writeJson" : serializationMethodBase; + String value = serializationValueGetterModifier != null + ? serializationValueGetterModifier.apply(valueGetter) : valueGetter; + + return fieldName == null + ? jsonWriterName + "." + methodBase + "(" + value + ")" + : jsonWriterName + "." + methodBase + "Field(\"" + fieldName + "\", " + value + ")"; + } + + @Override + public String xmlDeserializationMethod(String xmlReaderName, String attributeName, String attributeNamespace, + boolean namespaceIsConstant) { + if (attributeName == null) { + return xmlReaderName + "." + xmlElementDeserializationMethod; + } else if (attributeNamespace == null) { + return String.format(xmlAttributeDeserializationTemplate, xmlReaderName, "null", + "\"" + attributeName + "\""); + } else { + String namespace = namespaceIsConstant ? attributeNamespace : "\"" + attributeNamespace + "\""; + return String.format(xmlAttributeDeserializationTemplate, xmlReaderName, namespace, + "\"" + attributeName + "\""); + } + } + + @Override + public String xmlSerializationMethodCall(String xmlWriterName, String attributeOrElementName, String namespaceUri, + String valueGetter, boolean isAttribute, boolean nameIsVariable, boolean namespaceIsConstant) { + if (isSwaggerType) { + if (isAttribute) { + throw new RuntimeException("Swagger types cannot be written as attributes."); + } + + return xmlWriterName + ".writeXml(" + valueGetter + ", \"" + attributeOrElementName + "\")"; + } + + String value = serializationValueGetterModifier != null + ? serializationValueGetterModifier.apply(valueGetter) : valueGetter; + return xmlSerializationCallHelper(xmlWriterName, serializationMethodBase, attributeOrElementName, namespaceUri, + value, isAttribute, nameIsVariable, namespaceIsConstant); + } + + @Override + public boolean isUsedInXml() { + return usedInXml; + } + + public static class Builder { + /* + * Used to indicate if the class type is generated based on a Swagger definition and isn't a pre-defined, + * handwritten type. + */ + private final boolean isSwaggerType; + + private String packageName; + private String name; + private List implementationImports; + private XmsExtensions extensions; + private Function defaultValueExpressionConverter; + private Function serializationValueGetterModifier; + private String jsonToken; + private String jsonDeserializationMethod; + private String serializationMethodBase; + private String xmlAttributeDeserializationTemplate; + private String xmlElementDeserializationMethod; + private boolean usedInXml; + + public Builder() { + this(true); + } + + private Builder(boolean isSwaggerType) { + this.isSwaggerType = isSwaggerType; + } + + public Builder packageName(String packageName) { + this.packageName = packageName; + return this; + } + + public Builder name(String name) { + this.name = name; + return this; + } + + public Builder prototypeAsLong() { + return this.knownClass(Long.class) + .defaultValueExpressionConverter(defaultValueExpression -> defaultValueExpression + 'L') + .jsonToken("JsonToken.NUMBER") + .serializationMethodBase("writeNumber") + .jsonDeserializationMethod("getNullable(JsonReader::getLong)") + .xmlElementDeserializationMethod("getNullableElement(Long::parseLong)") + .xmlAttributeDeserializationTemplate("%s.getNullableAttribute(%s, %s, Long::parseLong)"); + } + + public Builder prototypeAsDouble() { + return this.knownClass(Double.class) + .defaultValueExpressionConverter(defaultValueExpression -> java.lang.String.valueOf(java.lang.Double.parseDouble(defaultValueExpression)) + 'D') + .jsonToken("JsonToken.NUMBER") + .serializationMethodBase("writeNumber") + .jsonDeserializationMethod("getNullable(JsonReader::getDouble)") + .xmlElementDeserializationMethod("getNullableElement(Double::parseDouble)") + .xmlAttributeDeserializationTemplate("%s.getNullableAttribute(%s, %s, Double::parseDouble)"); + } + + public Builder knownClass(Class clazz) { + return packageName(clazz.getPackage().getName()) + .name(clazz.getSimpleName()); + } + + private Builder knownClass(String fullName) { + int index = fullName.lastIndexOf("."); + return packageName(fullName.substring(0, index)) + .name(fullName.substring(index + 1)); + } + + public Builder implementationImports(String... implementationImports) { + this.implementationImports = Arrays.asList(implementationImports); + return this; + } + + public Builder extensions(XmsExtensions extensions) { + this.extensions = extensions; + return this; + } + + public Builder defaultValueExpressionConverter(Function defaultValueExpressionConverter) { + this.defaultValueExpressionConverter = defaultValueExpressionConverter; + return this; + } + + public Builder jsonToken(String jsonToken) { + this.jsonToken = jsonToken; + return this; + } + + public Builder serializationValueGetterModifier(Function serializationValueGetterModifier) { + this.serializationValueGetterModifier = serializationValueGetterModifier; + return this; + } + + public Builder jsonDeserializationMethod(String jsonDeserializationMethod) { + this.jsonDeserializationMethod = jsonDeserializationMethod; + return this; + } + + public Builder serializationMethodBase(String serializationMethodBase) { + this.serializationMethodBase = serializationMethodBase; + return this; + } + + public Builder xmlAttributeDeserializationTemplate(String xmlAttributeDeserializationTemplate) { + this.xmlAttributeDeserializationTemplate = xmlAttributeDeserializationTemplate; + return this; + } + + public Builder xmlElementDeserializationMethod(String xmlElementDeserializationMethod) { + this.xmlElementDeserializationMethod = xmlElementDeserializationMethod; + return this; + } + + public Builder usedInXml(boolean usedInXml) { + this.usedInXml = usedInXml; + return this; + } + + public ClassType build() { + // Deserialization of Swagger types needs to be handled differently as the named reader needs + // to be passed to the deserialization method and the reader name cannot be determined here. + String jsonDeserializationMethod = isSwaggerType ? null : this.jsonDeserializationMethod; + String xmlAttributeDeserializationTemplate = isSwaggerType + ? null : this.xmlAttributeDeserializationTemplate; + String xmlElementDeserializationMethod = isSwaggerType ? null : this.xmlElementDeserializationMethod; + + return new ClassType(packageName, name, implementationImports, extensions, defaultValueExpressionConverter, + isSwaggerType, jsonToken, serializationMethodBase, serializationValueGetterModifier, + jsonDeserializationMethod, xmlAttributeDeserializationTemplate, xmlElementDeserializationMethod, + usedInXml); + } + } + + static String xmlSerializationCallHelper(String writer, String method, String xmlName, String namespace, + String value, boolean isAttribute, boolean nameIsVariable, boolean namespaceIsConstant) { + String name = (xmlName == null) ? null + : nameIsVariable ? xmlName : "\"" + xmlName + "\""; + namespace = (namespace == null) ? null + : namespaceIsConstant ? namespace : "\"" + namespace + "\""; + + if (isAttribute) { + method = method + "Attribute"; + return (namespace == null) + ? writer + "." + method + "(" + name + ", " + value + ")" + : writer + "." + method + "(" + namespace + ", " + name + ", " + value + ")"; + } + + if (name == null) { + return writer + "." + method + "(" + value + ")"; + } else { + method = method + "Element"; + return (namespace == null) + ? writer + "." + method + "(" + name + ", " + value + ")" + : writer + "." + method + "(" + namespace + ", " + name + ", " + value + ")"; + } + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/Client.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/Client.java new file mode 100644 index 0000000000..b50353c407 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/Client.java @@ -0,0 +1,453 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.model.clientmodel; + +import java.util.Collections; +import java.util.List; + +/** + * A container for the types associated for accessing a specific service. + */ +public class Client { + private final String crossLanguageDefinitionId; + + /** + * The name of this service client. + */ + private String clientName; + /** + * The description of this service. + */ + private String clientDescription; + /** + * Get the enum types that are used by this service. + */ + private List enums; + /** + * Get the exception types that are used by this service. + */ + private List exceptions; + /** + * Get the XML sequence wrappers that are used by this service. + */ + private List xmlSequenceWrappers; + /** + * Get the response models which contain the response status code, headers and body for each service method. + */ + private List responseModels; + /** + * Get the model types that are used by this service. + */ + private List models; + /** + * Get the package infos. + */ + private List packageInfos; + /** + * Get the manager for this service. + */ + private Manager manager; + /** + * The serviceClient for this service. + */ + private ServiceClient serviceClient; + private List serviceClients; + /** + * Get the module info. + */ + private ModuleInfo moduleInfo; + private final List syncClients; + + private final List asyncClients; + private final List clientBuilders; + private final List protocolExamples; + private final List liveTests; + private final List unionModels; + private final List clientMethodExamples; + private final GraalVmConfig graalVmConfig; + + /** + * Create a new Client with the provided values. + * @param clientName The name of the service client. + * @param clientDescription The description of the service client. + * @param enums The enum types that are used by the client. + * @param exceptions The exception types that are used by the client. + * @param xmlSequenceWrappers the xml wrapper types that are used by the client. + * @param responseModels the models for response. + * @param models the client models that are used by the client. + * @param packageInfos the package-info classes that are used by the client. + * @param manager the manager class that is used by the client. + * @param serviceClient the service client that is used by the client. + * @param moduleInfo the module-info. + * @param syncClients sync service clients. + * @param asyncClients async service clients. + * @param clientBuilders service client builders. + * @param protocolExamples examples for DPG. + */ + private Client(String clientName, String clientDescription, List enums, List exceptions, + List xmlSequenceWrappers, List responseModels, + List models, List packageInfos, Manager manager, + ServiceClient serviceClient, List serviceClients, ModuleInfo moduleInfo, + List syncClients, List asyncClients, + List clientBuilders, List protocolExamples, + List liveTests, List unionModels, List clientMethodExamples, String crossLanguageDefinitionId, + GraalVmConfig graalVmConfig + ) { + this.clientName = clientName; + this.clientDescription = clientDescription; + this.enums = enums; + this.exceptions = exceptions; + this.xmlSequenceWrappers = xmlSequenceWrappers; + this.responseModels = responseModels; + this.models = models; + this.packageInfos = packageInfos; + this.manager = manager; + this.serviceClient = serviceClient; + this.serviceClients = serviceClients; + this.moduleInfo = moduleInfo; + this.syncClients = syncClients; + this.asyncClients = asyncClients; + this.clientBuilders = clientBuilders; + this.protocolExamples = protocolExamples; + this.liveTests = liveTests; + this.unionModels = unionModels; + this.clientMethodExamples = clientMethodExamples; + this.crossLanguageDefinitionId = crossLanguageDefinitionId; + this.graalVmConfig = graalVmConfig; + } + + public String getCrossLanguageDefinitionId() { + return crossLanguageDefinitionId; + } + + public final String getClientName() { + return clientName; + } + + public final String getClientDescription() { + return clientDescription; + } + + public final List getEnums() { + return enums; + } + + public final List getExceptions() { + return exceptions; + } + + public final List getXmlSequenceWrappers() { + return xmlSequenceWrappers; + } + + public final List getResponseModels() { + return responseModels; + } + + public final List getModels() { + return models; + } + + public final List getPackageInfos() { + return packageInfos; + } + + public final ModuleInfo getModuleInfo() { + return moduleInfo; + } + + public final Manager getManager() { + return manager; + } + + public final ServiceClient getServiceClient() { + return serviceClient; + } + + public final List getServiceClients() { + return serviceClients; + } + + /** @return the sync service clients */ + public List getSyncClients() { + return syncClients; + } + + /** @return the async service clients */ + public List getAsyncClients() { + return asyncClients; + } + + /** @return the service client builders */ + public List getClientBuilders() { + return clientBuilders; + } + + /** @return the examples for DPG */ + public List getProtocolExamples() { + return protocolExamples; + } + + /** @return the live tests */ + public List getLiveTests() { + return liveTests; + } + + public List getUnionModels() { + return unionModels; + } + + /** @return the examples for vanilla client methods */ + public List getClientMethodExamples() { + return clientMethodExamples; + } + + /** @return the Graal VM config */ + public GraalVmConfig getGraalVmConfig() { + return graalVmConfig; + } + + public static class Builder { + private String clientName; + private String clientDescription; + private List enums; + private List exceptions; + private List xmlSequenceWrappers; + private List responseModels; + private List models; + private List packageInfos; + private Manager manager; + private ServiceClient serviceClient; + private List serviceClients = Collections.emptyList(); + private ModuleInfo moduleInfo; + private List syncClients = Collections.emptyList(); + private List asyncClients = Collections.emptyList(); + private List clientBuilders = Collections.emptyList(); + private List protocolExamples = Collections.emptyList(); + private List liveTests = Collections.emptyList(); + private List unionModels = Collections.emptyList(); + private List clientMethodExamples = Collections.emptyList(); + private GraalVmConfig graalVmConfig; + private String crossLanguageDefinitionId; + + + public Builder crossLanguageDefinitionId(String crossLanguageDefinitionId) { + this.crossLanguageDefinitionId = crossLanguageDefinitionId; + return this; + } + + /** + * Sets the name of this service client. + * @param clientName the name of this service client + * @return the Builder itself + */ + public Builder clientName(String clientName) { + this.clientName = clientName; + return this; + } + + /** + * Sets the description of this service. + * @param clientDescription the description of this service + * @return the Builder itself + */ + public Builder clientDescription(String clientDescription) { + this.clientDescription = clientDescription; + return this; + } + + /** + * Sets the enum types that are used by this service. + * @param enums the enum types that are used by this service + * @return the Builder itself + */ + public Builder enums(List enums) { + this.enums = enums; + return this; + } + + /** + * Sets the exception types that are used by this service. + * @param exceptions the exception types that are used by this service + * @return the Builder itself + */ + public Builder exceptions(List exceptions) { + this.exceptions = exceptions; + return this; + } + + /** + * Sets the XML sequence wrappers that are used by this service. + * @param xmlSequenceWrappers the XML sequence wrappers that are used by this service + * @return the Builder itself + */ + public Builder xmlSequenceWrappers(List xmlSequenceWrappers) { + this.xmlSequenceWrappers = xmlSequenceWrappers; + return this; + } + + /** + * Sets the response models which contain the response status code, headers and body for each service method. + * @param responseModels the response models which contain the response status code, headers and body for each service method + * @return the Builder itself + */ + public Builder responseModels(List responseModels) { + this.responseModels = responseModels; + return this; + } + + /** + * Sets the model types that are used by this service. + * @param models the model types that are used by this service + * @return the Builder itself + */ + public Builder models(List models) { + this.models = models; + return this; + } + + public Builder unionModels(List unionModels) { + this.unionModels = unionModels; + return this; + } + + /** + * Sets the package infos. + * @param packageInfos the package infos + * @return the Builder itself + */ + public Builder packageInfos(List packageInfos) { + this.packageInfos = packageInfos; + return this; + } + + /** + * Sets the manager for this service. + * @param manager the manager for this service + * @return the Builder itself + */ + public Builder manager(Manager manager) { + this.manager = manager; + return this; + } + + /** + * Sets the serviceClient for this service. + * @param serviceClient the serviceClient for this service + * @return the Builder itself + */ + public Builder serviceClient(ServiceClient serviceClient) { + this.serviceClient = serviceClient; + return this; + } + + public Builder serviceClients(List serviceClients) { + this.serviceClients = serviceClients; + return this; + } + + /** + * Sets the module info for this client. + * @param moduleInfo the module info + * @return the Builder itself + */ + public Builder moduleInfo(ModuleInfo moduleInfo) { + this.moduleInfo = moduleInfo; + return this; + } + + /** + * Sets the module info for this client. + * @param syncClients the sync service clients + * @return the Builder itself + */ + public Builder syncClients(List syncClients) { + this.syncClients = syncClients; + return this; + } + + /** + * Sets the module info for this client. + * @param asyncClients async service clients + * @return the Builder itself + */ + public Builder asyncClients(List asyncClients) { + this.asyncClients = asyncClients; + return this; + } + + /** + * Sets the module info for this client. + * @param clientBuilders the service client builders + * @return the Builder itself + */ + public Builder clientBuilders(List clientBuilders) { + this.clientBuilders = clientBuilders; + return this; + } + + /** + * Sets the examples for this client. + * @param protocolExamples the examples for DPG + * @return the Builder itself + */ + public Builder protocolExamples(List protocolExamples) { + this.protocolExamples = protocolExamples; + return this; + } + + /** + * Sets the client method examples for this client. + * @param clientMethodExamples the examples for vanilla client methods + * @return the Builder itself + */ + public Builder clientMethodExamples(List clientMethodExamples) { + this.clientMethodExamples = clientMethodExamples; + return this; + } + + /** + * Sets the live tests for this client. + * @param liveTests live tests + * @return the Builder itself + */ + public Builder liveTests(List liveTests) { + this.liveTests = liveTests; + return this; + } + + public Builder graalVmConfig(GraalVmConfig graalVmConfig) { + this.graalVmConfig = graalVmConfig; + return this; + } + + public Client build() { + if (serviceClient == null && !serviceClients.isEmpty()) { + serviceClient = serviceClients.iterator().next(); + } + return new Client(clientName, + clientDescription, + enums, + exceptions, + xmlSequenceWrappers, + responseModels, + models, + packageInfos, + manager, + serviceClient, + serviceClients, + moduleInfo, + syncClients, + asyncClients, + clientBuilders, + protocolExamples, + liveTests, + unionModels, + clientMethodExamples, + crossLanguageDefinitionId, + graalVmConfig + ); + } + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/ClientBuilder.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/ClientBuilder.java new file mode 100644 index 0000000000..0e2ff073fb --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/ClientBuilder.java @@ -0,0 +1,90 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.model.clientmodel; + +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +public class ClientBuilder { + + private final String packageName; + private final String className; + private final ServiceClient serviceClient; + + // There is naturally ClientBuilder to Client reference, via "buildClient" method and via "@ServiceClientBuilder(serviceClients = {Client.class, AsyncClient.class})" + // syncClients and asyncClients can be empty. In this case, ClientBuilder build serviceClient directly. Note this usually is only used for internal implementation, as this pattern does not match Java guidelines. + private final List syncClients; + private final List asyncClients; + private final List builderTraits = new ArrayList<>(); + private String crossLanguageDefinitionId; + + public ClientBuilder(String packageName, String className, + ServiceClient serviceClient, + List syncClients, List asyncClients, String crossLanguageDefinitionId) { + this.packageName = Objects.requireNonNull(packageName); + this.className = Objects.requireNonNull(className); + this.serviceClient = Objects.requireNonNull(serviceClient); + this.syncClients = Objects.requireNonNull(syncClients); + this.asyncClients = Objects.requireNonNull(asyncClients); + this.crossLanguageDefinitionId = crossLanguageDefinitionId; + } + + public String getPackageName() { + return packageName; + } + + public String getClassName() { + return className; + } + + public ServiceClient getServiceClient() { + return serviceClient; + } + + public List getSyncClients() { + return syncClients; + } + + public List getAsyncClients() { + return asyncClients; + } + + public String getBuilderMethodNameForSyncClient(AsyncSyncClient syncClient) { + boolean singleClient = asyncClients.size() == 1 || syncClient.getMethodGroupClient() == null; + return singleClient + ? "buildClient" + : ("build" + syncClient.getClassName()); + } + + public String getBuilderMethodNameForAsyncClient(AsyncSyncClient asyncClient) { + boolean singleClient = asyncClients.size() == 1 || asyncClient.getMethodGroupClient() == null; + return singleClient + ? "buildAsyncClient" + : ("build" + asyncClient.getClassName()); + } + + public void addImportsTo(Set imports, boolean includeImplementationImports) { + JavaSettings settings = JavaSettings.getInstance(); + imports.add(String.format("%1$s.%2$s", getPackageName(), getClassName())); + serviceClient.addImportsTo(imports, includeImplementationImports, true, settings); + getSyncClients().forEach(c -> c.addImportsTo(imports, includeImplementationImports)); + getAsyncClients().forEach(c -> c.addImportsTo(imports, includeImplementationImports)); + } + + public void addBuilderTrait(ClientBuilderTrait trait) { + this.builderTraits.add(trait); + } + + public List getBuilderTraits() { + return this.builderTraits; + } + + public String getCrossLanguageDefinitionId() { + return this.crossLanguageDefinitionId; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/ClientBuilderTrait.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/ClientBuilderTrait.java new file mode 100644 index 0000000000..b84c77e92f --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/ClientBuilderTrait.java @@ -0,0 +1,426 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.model.clientmodel; + +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaBlock; +import com.azure.core.client.traits.AzureKeyCredentialTrait; +import com.azure.core.client.traits.EndpointTrait; +import com.azure.core.client.traits.KeyCredentialTrait; +import com.azure.core.client.traits.TokenCredentialTrait; +import com.azure.core.credential.AzureKeyCredential; +import com.azure.core.credential.TokenCredential; +import com.azure.core.util.logging.LogLevel; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +/** + * Class representing a builder trait that adds additional context to the {@link ClientBuilder} client model. + */ +public class ClientBuilderTrait { + + public static final ClientBuilderTrait HTTP_TRAIT = createHttpTrait(); + + public static final ClientBuilderTrait CONFIGURATION_TRAIT = createConfigurationTrait(); + + public static final ClientBuilderTrait AZURE_KEY_CREDENTIAL_TRAIT = createAzureKeyCredentialTrait(); + + public static final ClientBuilderTrait KEY_CREDENTIAL_TRAIT = createKeyCredentialTrait(); + + public static final ClientBuilderTrait TOKEN_CREDENTIAL_TRAIT = createTokenCredentialTrait(); + + public static final ClientBuilderTrait PROXY_TRAIT = createProxyTrait(); + + private static ClientBuilderTrait endpointTrait; + + private String traitInterfaceName; + private List importPackages; + private List clientBuilderTraitMethods; + + /** + * Returns the trait interface name. + * @return the trait interface name. + */ + public String getTraitInterfaceName() { + return traitInterfaceName; + } + + /** + * Sets the trait interface name. + * @param traitInterfaceName the trait interface name. + */ + public void setTraitInterfaceName(String traitInterfaceName) { + this.traitInterfaceName = traitInterfaceName; + } + + /** + * Returns the list of packages that needs to be imported for this trait. + * @return The list of packages that needs to be imported for this trait. + */ + public List getImportPackages() { + return importPackages; + } + + /** + * Sets the list of packages that needs to be imported for this trait. + * @param importPackages the list of packages that needs to be imported for this trait. + */ + public void setImportPackages(List importPackages) { + this.importPackages = importPackages; + } + + /** + * Returns the list of methods that this trait interface contains. + * @return the list of methods that this trait interface contains. + */ + public List getTraitMethods() { + return clientBuilderTraitMethods; + } + + /** + * Sets the list of methods that this trait interface contains. + * @param clientBuilderTraitMethods the list of methods that this trait interface contains. + */ + public void setTraitMethods(List clientBuilderTraitMethods) { + this.clientBuilderTraitMethods = clientBuilderTraitMethods; + } + + private static ClientBuilderTrait createHttpTrait() { + boolean isBranded = JavaSettings.getInstance().isBranded(); + + ClientBuilderTrait httpTrait = new ClientBuilderTrait(); + httpTrait.setTraitInterfaceName("HttpTrait"); + List importPackages = new ArrayList<>(); + httpTrait.setImportPackages(importPackages); + importPackages.add(ClassType.HTTP_TRAIT.getFullName()); + List httpClientBuilderTraitMethods = new ArrayList<>(); + httpTrait.setTraitMethods(httpClientBuilderTraitMethods); + + // pipeline + String pipelineMethodName = isBranded ? "pipeline" : "httpPipeline"; + ServiceClientProperty pipelineProperty = new ServiceClientProperty("The HTTP pipeline to send requests " + + "through.", ClassType.HTTP_PIPELINE, "pipeline", false, + JavaSettings.getInstance().isAzureOrFluent() + ? "new HttpPipelineBuilder().policies(new UserAgentPolicy(), new RetryPolicy()).build()" + : "createHttpPipeline()"); + importPackages.add(ClassType.LOG_LEVEL.getFullName()); + Consumer pipelineMethodImpl = function -> { + final String pipelineVarName = "pipeline"; + if (JavaSettings.getInstance().isUseClientLogger()) { + function.ifBlock(String.format("this.%1$s != null && %1$s == null", pipelineVarName), ifBlock -> { + function.line(addLogging(LogLevel.INFORMATIONAL, "HttpPipeline is being set to 'null' when it was previously configured.")); + }); + } + function.line(String.format("this.%1$s = %2$s;", pipelineVarName, pipelineVarName)); + function.methodReturn("this"); + }; + ClientBuilderTraitMethod pipelineMethod = createTraitMethod(pipelineMethodName, "pipeline", ClassType.HTTP_PIPELINE, + pipelineProperty, "{@inheritDoc}", pipelineMethodImpl); + importPackages.add(ClassType.HTTP_PIPELINE.getFullName()); + + httpClientBuilderTraitMethods.add(pipelineMethod); + + // httpClient + ServiceClientProperty httpClientProperty = new ServiceClientProperty("The HTTP client used to send the request.", + ClassType.HTTP_CLIENT, "httpClient", false, null); + Consumer httpClientMethodImpl = function -> { + function.line(String.format("this.%1$s = %2$s;", "httpClient", "httpClient")); + function.methodReturn("this"); + }; + ClientBuilderTraitMethod httpClientMethod = createTraitMethod("httpClient", "httpClient", ClassType.HTTP_CLIENT, + httpClientProperty, "{@inheritDoc}", httpClientMethodImpl); + importPackages.add(ClassType.HTTP_CLIENT.getFullName()); + + httpClientBuilderTraitMethods.add(httpClientMethod); + + // httpLogOptions + ServiceClientProperty httpLogOptionsProperty = new ServiceClientProperty("The logging configuration for HTTP " + + "requests and responses.", + ClassType.HTTP_LOG_OPTIONS, "httpLogOptions", false, null); + Consumer httpLogOptionsMethodImpl = function -> { + function.line(String.format("this.%1$s = %2$s;", "httpLogOptions", "httpLogOptions")); + function.methodReturn("this"); + }; + ClientBuilderTraitMethod httpLogOptionsMethod = createTraitMethod("httpLogOptions", "httpLogOptions", ClassType.HTTP_LOG_OPTIONS, + httpLogOptionsProperty, "{@inheritDoc}", httpLogOptionsMethodImpl); + importPackages.add(ClassType.HTTP_LOG_OPTIONS.getFullName()); + + httpClientBuilderTraitMethods.add(httpLogOptionsMethod); + + // clientOptions + if (isBranded) { + ServiceClientProperty clientOptionsProperty = new ServiceClientProperty("The client options such as application ID and custom headers to set on a request.", + ClassType.CLIENT_OPTIONS, "clientOptions", false, null); + Consumer clientOptionsMethodImpl = function -> { + function.line(String.format("this.%1$s = %2$s;", "clientOptions", "clientOptions")); + function.methodReturn("this"); + }; + ClientBuilderTraitMethod clientOptionsMethod = createTraitMethod("clientOptions", "clientOptions", ClassType.CLIENT_OPTIONS, + clientOptionsProperty, "{@inheritDoc}", clientOptionsMethodImpl); + importPackages.add(ClassType.CLIENT_OPTIONS.getFullName()); + + httpClientBuilderTraitMethods.add(clientOptionsMethod); + } + + // retryOptions + ServiceClientProperty retryOptionsProperty = + new ServiceClientProperty("The retry options to configure retry policy for failed " + + "requests.", + ClassType.RETRY_OPTIONS, "retryOptions", false, null); + Consumer retryOptionsMethodImpl = function -> { + function.line(String.format("this.%1$s = %2$s;", "retryOptions", "retryOptions")); + function.methodReturn("this"); + }; + String retryOptionsMethodName = isBranded ? "retryOptions" : "httpRetryOptions"; + ClientBuilderTraitMethod retryOptionsMethod = createTraitMethod(retryOptionsMethodName, "retryOptions", ClassType.RETRY_OPTIONS, + retryOptionsProperty, "{@inheritDoc}", retryOptionsMethodImpl); + importPackages.add(ClassType.RETRY_OPTIONS.getFullName()); + httpClientBuilderTraitMethods.add(retryOptionsMethod); + + // addPolicy + Consumer addPolicyMethodImpl = function -> { + function.line("Objects.requireNonNull(customPolicy, \"'customPolicy' cannot be null.\");"); + function.line("pipelinePolicies.add(customPolicy);"); + function.methodReturn("this"); + }; + String addPolicyMethodName = isBranded ? "addPolicy" : "addHttpPipelinePolicy"; + ClientBuilderTraitMethod addPolicyMethod = createTraitMethod(addPolicyMethodName, "customPolicy", ClassType.HTTP_PIPELINE_POLICY, + null, "{@inheritDoc}", addPolicyMethodImpl); + importPackages.add(ClassType.HTTP_PIPELINE_POLICY.getFullName()); + httpClientBuilderTraitMethods.add(addPolicyMethod); + + + if (!isBranded) { + // redirectOptions + ServiceClientProperty redirectOptionsProperty = + new ServiceClientProperty("The redirect options to configure redirect policy", + ClassType.REDIRECT_OPTIONS, "redirectOptions", false, null); + Consumer redirectOptionsMethodImpl = function -> { + function.line(String.format("this.%1$s = %2$s;", "redirectOptions", "redirectOptions")); + function.methodReturn("this"); + }; + ClientBuilderTraitMethod redirectOptionsMethod = createTraitMethod("httpRedirectOptions", "redirectOptions", ClassType.REDIRECT_OPTIONS, + redirectOptionsProperty, "{@inheritDoc}", redirectOptionsMethodImpl); + importPackages.add(ClassType.REDIRECT_OPTIONS.getFullName()); + httpClientBuilderTraitMethods.add(redirectOptionsMethod); + } + + return httpTrait; + } + + private static String addLogging(LogLevel level, String message) { + + if (JavaSettings.getInstance().isBranded()) { + switch (level) { + case VERBOSE: + return String.format("LOGGER.atVerbose().log(\"%s\");", message); + case INFORMATIONAL: + return String.format("LOGGER.atInfo().log(\"%s\");", message); + case WARNING: + return String.format("LOGGER.atWarning().log(\"%s\");", message); + case ERROR: + return String.format("LOGGER.atError().log(\"%s\");", message); + default: + return String.format("LOGGER.atInfo().log(\"%s\");", message); + } + } else { + switch (level) { + case VERBOSE: + return String.format("LOGGER.atVerbose().log(\"%s\");", message); + case INFORMATIONAL: + return String.format("LOGGER.atInfo().log(\"%s\");", message); + case WARNING: + return String.format("LOGGER.atWarning().log(\"%s\");", message); + case ERROR: + return String.format("LOGGER.atError().log(\"%s\");", message); + default: + return String.format("LOGGER.atInfo().log(\"%s\");", message); + } + } + } + + private static ClientBuilderTrait createConfigurationTrait() { + ClientBuilderTrait configurationTrait = new ClientBuilderTrait(); + configurationTrait.setTraitInterfaceName("ConfigurationTrait"); + List importPackages = new ArrayList<>(); + configurationTrait.setImportPackages(importPackages); + importPackages.add(ClassType.CONFIGURATION_TRAIT.getFullName()); + + List configurationClientBuilderTraitMethods = new ArrayList<>(); + configurationTrait.setTraitMethods(configurationClientBuilderTraitMethods); + + String propertyName = "configuration"; + ServiceClientProperty configurationProperty = new ServiceClientProperty("The configuration store that is used" + + " during construction of the service client.", + ClassType.CONFIGURATION, propertyName, false, null); + + Consumer configurationMethodImpl = function -> { + function.line(String.format("this.%1$s = %2$s;", propertyName, propertyName)); + function.methodReturn("this"); + }; + ClientBuilderTraitMethod configurationMethod = createTraitMethod(propertyName, propertyName, ClassType.CONFIGURATION, + configurationProperty, "{@inheritDoc}", configurationMethodImpl); + importPackages.add(ClassType.CONFIGURATION.getFullName()); + + configurationClientBuilderTraitMethods.add(configurationMethod); + return configurationTrait; + } + + private static ClientBuilderTrait createProxyTrait() { + ClientBuilderTrait proxyTrait = new ClientBuilderTrait(); + proxyTrait.setTraitInterfaceName("ProxyTrait"); + List importPackages = new ArrayList<>(); + proxyTrait.setImportPackages(importPackages); + importPackages.add(ClassType.PROXY_TRAIT.getFullName()); + + List proxyClientBuilderTraitMethods = new ArrayList<>(); + proxyTrait.setTraitMethods(proxyClientBuilderTraitMethods); + + String propertyName = "proxyOptions"; + ServiceClientProperty proxyOptionsProperty = new ServiceClientProperty("The proxy options used" + + " during construction of the service client.", + ClassType.PROXY_OPTIONS, propertyName, false, null); + + Consumer proxyMethodImpl = function -> { + function.line(String.format("this.%1$s = %2$s;", propertyName, propertyName)); + function.methodReturn("this"); + }; + ClientBuilderTraitMethod proxyMethod = createTraitMethod(propertyName, propertyName, ClassType.PROXY_OPTIONS, + proxyOptionsProperty, "{@inheritDoc}", proxyMethodImpl); + importPackages.add(ClassType.PROXY_OPTIONS.getFullName()); + + proxyClientBuilderTraitMethods.add(proxyMethod); + return proxyTrait; + } + + public static ClientBuilderTrait getEndpointTrait(ServiceClientProperty property) { + ClientBuilderTrait endpointTrait = ClientBuilderTrait.endpointTrait; + if (endpointTrait == null) { + endpointTrait = new ClientBuilderTrait(); + endpointTrait.setTraitInterfaceName(EndpointTrait.class.getSimpleName()); + + List importPackages = new ArrayList<>(); + endpointTrait.setImportPackages(importPackages); + importPackages.add(ClassType.ENDPOINT_TRAIT.getFullName()); + + List endpointClientBuilderTraitMethods = new ArrayList<>(); + endpointTrait.setTraitMethods(endpointClientBuilderTraitMethods); + + String propertyName = "endpoint"; + ServiceClientProperty endpointProperty = new ServiceClientProperty.Builder() + .name(propertyName) + .type(ClassType.STRING) + .description("The service endpoint") + .readOnly(false) + .required(property.isRequired()) + .defaultValueExpression(property.getDefaultValueExpression()) + .requestParameterName(property.getRequestParameterName()) + .build(); + + Consumer endpointMethodImpl = function -> { + function.line(String.format("this.%1$s = %2$s;", propertyName, propertyName)); + function.methodReturn("this"); + }; + ClientBuilderTraitMethod endpointMethod = createTraitMethod(propertyName, propertyName, ClassType.STRING, + endpointProperty, "{@inheritDoc}", endpointMethodImpl); + + endpointClientBuilderTraitMethods.add(endpointMethod); + ClientBuilderTrait.endpointTrait = endpointTrait; + } + return endpointTrait; + } + + private static ClientBuilderTrait createTokenCredentialTrait() { + ClientBuilderTrait tokenCredentialTrait = new ClientBuilderTrait(); + tokenCredentialTrait.setTraitInterfaceName(TokenCredentialTrait.class.getSimpleName()); + List importPackages = new ArrayList<>(); + tokenCredentialTrait.setImportPackages(importPackages); + importPackages.add(TokenCredentialTrait.class.getName()); + + List clientBuilderTraitMethods = new ArrayList<>(); + tokenCredentialTrait.setTraitMethods(clientBuilderTraitMethods); + + String propertyName = "tokenCredential"; + ServiceClientProperty property = new ServiceClientProperty("The TokenCredential used for authentication.", + ClassType.TOKEN_CREDENTIAL, propertyName, false, null); + + Consumer methodImpl = function -> { + function.line(String.format("this.%1$s = %2$s;", propertyName, propertyName)); + function.methodReturn("this"); + }; + ClientBuilderTraitMethod clientMethod = createTraitMethod("credential", propertyName, ClassType.TOKEN_CREDENTIAL, + property, "{@inheritDoc}", methodImpl); + importPackages.add(TokenCredential.class.getName()); + + clientBuilderTraitMethods.add(clientMethod); + return tokenCredentialTrait; + } + + private static ClientBuilderTrait createAzureKeyCredentialTrait() { + ClientBuilderTrait azureKeyCredentialTrait = new ClientBuilderTrait(); + azureKeyCredentialTrait.setTraitInterfaceName(AzureKeyCredentialTrait.class.getSimpleName()); + List importPackages = new ArrayList<>(); + azureKeyCredentialTrait.setImportPackages(importPackages); + importPackages.add(AzureKeyCredentialTrait.class.getName()); + + List clientBuilderTraitMethods = new ArrayList<>(); + azureKeyCredentialTrait.setTraitMethods(clientBuilderTraitMethods); + + String propertyName = "azureKeyCredential"; + ServiceClientProperty property = new ServiceClientProperty("The AzureKeyCredential used for authentication.", + ClassType.AZURE_KEY_CREDENTIAL, propertyName, false, null); + + Consumer methodImpl = function -> { + function.line(String.format("this.%1$s = %2$s;", propertyName, propertyName)); + function.methodReturn("this"); + }; + ClientBuilderTraitMethod clientMethod = createTraitMethod("credential", propertyName, ClassType.AZURE_KEY_CREDENTIAL, + property, "{@inheritDoc}", methodImpl); + importPackages.add(AzureKeyCredential.class.getName()); + + clientBuilderTraitMethods.add(clientMethod); + return azureKeyCredentialTrait; + } + + private static ClientBuilderTrait createKeyCredentialTrait() { + ClientBuilderTrait keyCredentialTrait = new ClientBuilderTrait(); + keyCredentialTrait.setTraitInterfaceName(KeyCredentialTrait.class.getSimpleName()); + List importPackages = new ArrayList<>(); + keyCredentialTrait.setImportPackages(importPackages); + importPackages.add(ClassType.KEY_CREDENTIAL_TRAIT.getFullName()); + + List clientBuilderTraitMethods = new ArrayList<>(); + keyCredentialTrait.setTraitMethods(clientBuilderTraitMethods); + + String propertyName = "keyCredential"; + ServiceClientProperty property = new ServiceClientProperty("The KeyCredential used for authentication.", + ClassType.KEY_CREDENTIAL, propertyName, false, null); + + Consumer methodImpl = function -> { + function.line(String.format("this.%1$s = %2$s;", propertyName, propertyName)); + function.methodReturn("this"); + }; + ClientBuilderTraitMethod clientMethod = createTraitMethod("credential", propertyName, ClassType.KEY_CREDENTIAL, + property, "{@inheritDoc}", methodImpl); + importPackages.add(ClassType.KEY_CREDENTIAL.getFullName()); + + clientBuilderTraitMethods.add(clientMethod); + return keyCredentialTrait; + } + + private static ClientBuilderTraitMethod createTraitMethod(String methodName, String methodParamName, ClassType paramType, + ServiceClientProperty property, + String documentation, Consumer methodImpl) { + ClientBuilderTraitMethod pipelineMethod = new ClientBuilderTraitMethod(); + pipelineMethod.setMethodName(methodName); + pipelineMethod.setMethodParamName(methodParamName); + pipelineMethod.setMethodParamType(paramType); + pipelineMethod.setProperty(property); + pipelineMethod.setDocumentation(documentation); + pipelineMethod.setMethodImpl(methodImpl); + return pipelineMethod; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/ClientBuilderTraitMethod.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/ClientBuilderTraitMethod.java new file mode 100644 index 0000000000..e906764558 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/ClientBuilderTraitMethod.java @@ -0,0 +1,116 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.model.clientmodel; + +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaBlock; + +import java.util.function.Consumer; + +/** + * Class containing the details of a method in a trait interface. + */ +public class ClientBuilderTraitMethod { + private String methodName; + private ClassType methodParamType; + private String methodParamName; + private String documentation; + private ServiceClientProperty property; + private Consumer methodImpl; + + /** + * Returns the name of the method defined in the trait interface. + * @return The name of the trait method. + */ + public String getMethodName() { + return methodName; + } + + /** + * Sets the name of the method defined in the trait interface. + * @param methodName The method name. + */ + public void setMethodName(String methodName) { + this.methodName = methodName; + } + + /** + * Return the type of the method param. + * @return The type of the method param. + */ + public ClassType getMethodParamType() { + return methodParamType; + } + + /** + * Sets the type of the method param. + * @param methodParamType The type of the method param. + */ + public void setMethodParamType(ClassType methodParamType) { + this.methodParamType = methodParamType; + } + + /** + * Returns the name of the method param. + * @return The name of the method param. + */ + public String getMethodParamName() { + return methodParamName; + } + + /** + * Sets the name of the method param. + * @param methodParamName The name of the method param. + */ + public void setMethodParamName(String methodParamName) { + this.methodParamName = methodParamName; + } + + /** + * Returns the JavaDoc string for this trait method. + * @return The JavaDoc string for this trait method. + */ + public String getDocumentation() { + return documentation; + } + + /** + * Sets the JavaDoc string for this trait method. + * @param documentation The JavaDoc string for this trait method. + */ + public void setDocumentation(String documentation) { + this.documentation = documentation; + } + + /** + * Returns the property this trait method is applicable to. + * @return the property this trait method is applicable to. + */ + public ServiceClientProperty getProperty() { + return property; + } + + /** + * Set the property this trait method is applicable to. + * @param property the property this trait method is applicable to. + */ + public void setProperty(ServiceClientProperty property) { + this.property = property; + } + + /** + * Returns the callback that provides the implementation of this trait method. + * @return the callback that provides the implementation of this trait method. + */ + public Consumer getMethodImpl() { + return methodImpl; + } + + /** + * Sets the callback that provides the implementation of this trait method. + * @param methodImpl the callback that provides the implementation of this trait method. + */ + public void setMethodImpl(Consumer methodImpl) { + this.methodImpl = methodImpl; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/ClientEnumValue.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/ClientEnumValue.java new file mode 100644 index 0000000000..ef07eb0293 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/ClientEnumValue.java @@ -0,0 +1,63 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.model.clientmodel; + +/** + * An individual value within an enumerated type. + */ +public class ClientEnumValue { + private final String name; + private final String value; + private final String description; + + /** + * Create a new EnumValue with the provided name and value. + * + * @param name The name of this EnumValue. + * @param value The value of this EnumValue. + */ + public ClientEnumValue(String name, String value) { + this(name, value, null); + } + + /** + * Create a new EnumValue with the provided name, value, and description. + * + * @param name The name of this EnumValue. + * @param value The value of this EnumValue. + * @param description The description of this EnumValue. + */ + public ClientEnumValue(String name, String value, String description) { + this.name = name; + this.value = value; + this.description = description; + } + + /** + * Gets the name of this EnumValue. + * + * @return The name of this EnumValue. + */ + public final String getName() { + return name; + } + + /** + * Gets the value of this EnumValue. + * + * @return The value of this EnumValue. + */ + public final String getValue() { + return value; + } + + /** + * Gets the description of this EnumValue. + * + * @return The description of this EnumValue. + */ + public final String getDescription() { + return description; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/ClientException.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/ClientException.java new file mode 100644 index 0000000000..c8f758aba5 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/ClientException.java @@ -0,0 +1,117 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.model.clientmodel; + +/** + * The details of an exception type that is used by a client. + */ +public class ClientException { + private final String name; + private final String errorName; + private final String packageKeyword; + private final IType parentType; + + /** + * Create a new ServiceException with the provided properties. + * @param packageKeyword The package that this Exception will appear in. + * @param name The name of the ServiceException type. + * @param errorName The name of the error type contained by the ServiceException. + * @param parentType The type of parent exception. + */ + protected ClientException(String packageKeyword, String name, String errorName, IType parentType) { + this.packageKeyword = packageKeyword; + this.name = name; + this.errorName = errorName; + this.parentType = parentType; + } + + /** + * @return The name of the ServiceException type. + */ + public final String getName() { + return name; + } + + /** + * @return The name of the error type contained by the ServiceException. + */ + public final String getErrorName() { + return errorName; + } + + /** + * @return type of parent exception.The package that this Enum will appear in. + */ + public final String getPackage() { + return packageKeyword; + } + + /** + * @return The type of parent exception. + */ + public IType getParentType() { + return parentType; + } + + /** + * Builder for ClientException. + */ + public static class Builder { + protected String name; + protected String errorName; + protected String packageName; + protected IType parentType = ClassType.HTTP_RESPONSE_EXCEPTION; + + /** + * Sets exception name. + * @param name exception name. + * @return the Builder. + */ + public Builder name(String name) { + this.name = name; + return this; + } + + /** + * Sets error name. + * @param errorName error name. + * @return the Builder. + */ + public Builder errorName(String errorName) { + this.errorName = errorName; + return this; + } + + /** + * Sets package name. + * @param packageName package name. + * @return the Builder. + */ + public Builder packageName(String packageName) { + this.packageName = packageName; + return this; + } + + /** + * Sets parent exception IType. + * @param parentType parent exception IType. + * @return the Builder. + */ + public Builder parentType(IType parentType) { + this.parentType = parentType; + return this; + } + + /** + * Builds ClientException + * @return the new ClientException instance. + */ + public ClientException build() { + return new ClientException(packageName, + name, + errorName, + parentType); + } + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/ClientMethod.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/ClientMethod.java new file mode 100644 index 0000000000..2394aa101b --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/ClientMethod.java @@ -0,0 +1,799 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.model.clientmodel; + +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.RequestParameterLocation; +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaVisibility; +import com.microsoft.typespec.http.client.generator.core.util.CodeNamer; +import com.microsoft.typespec.http.client.generator.core.util.MethodUtil; +import com.azure.core.http.rest.SimpleResponse; +import com.azure.core.util.CoreUtils; +import com.azure.core.util.UrlBuilder; +import com.azure.core.util.polling.PollingStrategyOptions; +import com.azure.core.util.serializer.TypeReference; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * A ClientMethod that exists on a ServiceClient or MethodGroupClient that eventually will call a ProxyMethod. + */ +public class ClientMethod { + private static final List KNOWN_POLLING_STRATEGIES = Arrays.asList("DefaultPollingStrategy", + "ChainedPollingStrategy", "OperationResourcePollingStrategy", "LocationPollingStrategy", + "StatusCheckPollingStrategy", "SyncDefaultPollingStrategy", "SyncChainedPollingStrategy", + "SyncOperationResourcePollingStrategy", "SyncLocationPollingStrategy", "SyncStatusCheckPollingStrategy"); + + private final String crossLanguageDefinitionId; + /** + * The description of this ClientMethod. + */ + private final String description; + /** + * The return value of this ClientMethod. + */ + private final ReturnValue returnValue; + /** + * The name of this ClientMethod. + */ + private final String name; + /** + * The parameters of this ClientMethod. + */ + private final List parameters; + private final List methodParameters; + private final List methodRequiredParameters; + + /** + * Whether this ClientMethod has omitted optional parameters. + */ + private final boolean onlyRequiredParameters; + /** + * The type of this ClientMethod. + */ + private ClientMethodType type = ClientMethodType.values()[0]; + /** + * The RestAPIMethod that this ClientMethod eventually calls. + */ + private final ProxyMethod proxyMethod; + /** + * The expressions (parameters and service client properties) that need to be validated in this ClientMethod. + */ + private final Map validateExpressions; + /** + * The reference to the service client. + */ + private final String clientReference; + /** + * The parameter expressions which are required. + */ + private final List requiredNullableParameterExpressions; + /** + * The parameter that needs to transformed before pagination. + */ + private final boolean isGroupedParameterRequired; + /** + * The type name of groupedParameter. + */ + private final String groupedParameterTypeName; + /** + * The pagination information if this is a paged method. + */ + private final MethodPageDetails methodPageDetails; + /** + * The parameter transformations before calling ProxyMethod. + */ + private final List methodTransformationDetails; + + private final JavaVisibility methodVisibility; + + private final JavaVisibility methodVisibilityInWrapperClient; + + private final ImplementationDetails implementationDetails; + + private final MethodPollingDetails methodPollingDetails; + + private final ExternalDocumentation externalDocumentation; + + private final boolean hasWithContextOverload; + private final String parametersDeclaration; + private final String argumentList; + + /** + * Create a new ClientMethod with the provided properties. + * + * @param description The description of this ClientMethod. + * @param returnValue The return value of this ClientMethod. + * @param name The name of this ClientMethod. + * @param parameters The parameters of this ClientMethod. + * @param onlyRequiredParameters Whether this ClientMethod has omitted optional parameters. + * @param type The type of this ClientMethod. + * @param proxyMethod The ProxyMethod that this ClientMethod eventually calls. + * @param validateExpressions The expressions (parameters and service client properties) that need to be validated + * in this ClientMethod. + * @param clientReference The reference to the service client. + * @param requiredNullableParameterExpressions The parameter expressions which are required. + * @param isGroupedParameterRequired The parameter that needs to transformed before pagination. + * @param groupedParameterTypeName The type name of groupedParameter. + * @param methodPageDetails The pagination information if this is a paged method. + * @param methodTransformationDetails The parameter transformations before calling ProxyMethod. + * @param externalDocumentation The external documentation. + * @param hasWithContextOverload Whether this method has a corresponding {@code Context}-based overload. + */ + protected ClientMethod(String description, ReturnValue returnValue, String name, + List parameters, boolean onlyRequiredParameters, ClientMethodType type, + ProxyMethod proxyMethod, Map validateExpressions, String clientReference, + List requiredNullableParameterExpressions, boolean isGroupedParameterRequired, + String groupedParameterTypeName, MethodPageDetails methodPageDetails, + List methodTransformationDetails, JavaVisibility methodVisibility, + JavaVisibility methodVisibilityInWrapperClient, ImplementationDetails implementationDetails, + MethodPollingDetails methodPollingDetails, ExternalDocumentation externalDocumentation, + String crossLanguageDefinitionId, boolean hasWithContextOverload) { + this.description = description; + this.returnValue = returnValue; + this.name = name; + this.parameters = List.copyOf(parameters); + this.methodParameters = parameters.stream() + .filter(parameter -> !parameter.isFromClient() && parameter.getName() != null && !parameter.getName().trim().isEmpty()) + .sorted((p1, p2) -> Boolean.compare(!p1.isRequired(), !p2.isRequired())) + .collect(Collectors.toUnmodifiableList()); + this.methodRequiredParameters = methodParameters.stream() + .filter(param -> !param.isConstant() && param.isRequired()) + .collect(Collectors.toUnmodifiableList()); + this.onlyRequiredParameters = onlyRequiredParameters; + this.type = type; + this.proxyMethod = proxyMethod; + this.validateExpressions = validateExpressions; + this.clientReference = clientReference; + this.requiredNullableParameterExpressions = requiredNullableParameterExpressions; + this.isGroupedParameterRequired = isGroupedParameterRequired; + this.groupedParameterTypeName = groupedParameterTypeName; + this.methodPageDetails = methodPageDetails; + this.methodTransformationDetails = methodTransformationDetails; + this.methodVisibility = methodVisibility; + this.implementationDetails = implementationDetails; + this.methodPollingDetails = methodPollingDetails; + this.externalDocumentation = externalDocumentation; + this.methodVisibilityInWrapperClient = methodVisibilityInWrapperClient; + this.crossLanguageDefinitionId = crossLanguageDefinitionId; + this.hasWithContextOverload = hasWithContextOverload; + this.parametersDeclaration = getMethodInputParameters().stream().map(ClientMethodParameter::getDeclaration) + .collect(Collectors.joining(", ")); + this.argumentList = getMethodParameters().stream().map(ClientMethodParameter::getName) + .collect(Collectors.joining(", ")); + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + ClientMethod that = (ClientMethod) o; + return onlyRequiredParameters == that.onlyRequiredParameters + && isGroupedParameterRequired == that.isGroupedParameterRequired && Objects.equals(returnValue.getType(), + that.returnValue.getType()) && Objects.equals(name, that.name) && Objects.equals(getParametersDeclaration(), + that.getParametersDeclaration()) && type == that.type && Objects.equals( + requiredNullableParameterExpressions, that.requiredNullableParameterExpressions) && Objects.equals( + groupedParameterTypeName, that.groupedParameterTypeName) && Objects.equals(methodTransformationDetails, + that.methodTransformationDetails) && methodVisibility == that.methodVisibility; + } + + @Override + public int hashCode() { + return Objects.hash(returnValue.getType(), name, getParametersDeclaration(), onlyRequiredParameters, type, + requiredNullableParameterExpressions, isGroupedParameterRequired, groupedParameterTypeName, + methodTransformationDetails, methodVisibility); + } + + public String getCrossLanguageDefinitionId() { + return crossLanguageDefinitionId; + } + + public final String getDescription() { + return description; + } + + public final ReturnValue getReturnValue() { + return returnValue; + } + + public final String getName() { + return name; + } + + public final List getParameters() { + return parameters; + } + + public final boolean getOnlyRequiredParameters() { + return onlyRequiredParameters; + } + + public final ClientMethodType getType() { + return type; + } + + public final ProxyMethod getProxyMethod() { + return proxyMethod; + } + + public final Map getValidateExpressions() { + return validateExpressions; + } + + public final String getClientReference() { + return clientReference; + } + + /** + * Get the comma-separated list of parameter declarations for this ClientMethod. + */ + public final String getParametersDeclaration() { + return parametersDeclaration; + } + + /** + * Get the comma-separated list of parameter names for this ClientMethod. + */ + public final String getArgumentList() { + return argumentList; + } + + public final String getArgumentListWithoutRequestOptions() { + return getMethodParameters().stream() + .map(ClientMethodParameter::getName) + .map(name -> name.equals("requestOptions") ? "null" : name) + .collect(Collectors.joining(", ")); + } + + /** + * The full declaration of this ClientMethod. + */ + public final String getDeclaration() { + return getReturnValue().getType() + " " + getName() + "(" + getParametersDeclaration() + ")"; + } + + /** + * Get the input parameters of the client method, taking configure of onlyRequiredParameters. + */ + public final List getMethodInputParameters() { + return onlyRequiredParameters ? getMethodRequiredParameters() : getMethodParameters(); + } + + public final List getMethodParameters() { + return methodParameters; + } + + public final List getMethodRequiredParameters() { + return methodRequiredParameters; + } + + public final List getRequiredNullableParameterExpressions() { + return requiredNullableParameterExpressions; + } + + public final boolean isGroupedParameterRequired() { + return isGroupedParameterRequired; + } + + public final String getGroupedParameterTypeName() { + return groupedParameterTypeName; + } + + public final MethodPageDetails getMethodPageDetails() { + return methodPageDetails; + } + + public final List getMethodTransformationDetails() { + return methodTransformationDetails; + } + + public ExternalDocumentation getMethodDocumentation() { + return externalDocumentation; + } + + public final List getProxyMethodArguments(JavaSettings settings) { + List restAPIMethodArguments = getProxyMethod().getParameters().stream().map(parameter -> { + String parameterName = parameter.getParameterReference(); + IType parameterWireType = parameter.getWireType(); + if (parameter.isNullable()) { + parameterWireType = parameterWireType.asNullable(); + } + IType parameterClientType = parameter.getClientType(); + + if (parameterClientType != ClassType.BASE_64_URL && parameter.getRequestParameterLocation() + != RequestParameterLocation.BODY /*&& parameter.getRequestParameterLocation() != RequestParameterLocation.FormData*/ + && (parameterClientType instanceof ArrayType || parameterClientType instanceof ListType)) { + parameterWireType = ClassType.STRING; + } + + String parameterWireName = (parameterClientType != parameterWireType) + ? CodeNamer.toCamelCase(CodeNamer.removeInvalidCharacters(parameterName)) + "Converted" + : parameterName; + + String result; + if (getMethodTransformationDetails().stream() + .anyMatch(d -> d.getOutParameter().getName().equals(parameterName + "1"))) { + result = getMethodTransformationDetails().stream() + .filter(d -> d.getOutParameter().getName().equals(parameterName + "1")) + .findFirst() + .get() + .getOutParameter() + .getName(); + } else { + result = parameterWireName; + } + return result; + }).collect(Collectors.toList()); + return restAPIMethodArguments; + } + + public JavaVisibility getMethodVisibility() { + return methodVisibility; + } + + public JavaVisibility getMethodVisibilityInWrapperClient() { + return methodVisibilityInWrapperClient; + } + + public ImplementationDetails getImplementationDetails() { + return implementationDetails; + } + + public boolean isImplementationOnly() { + return implementationDetails != null && implementationDetails.isImplementationOnly(); + } + + public MethodPollingDetails getMethodPollingDetails() { + return methodPollingDetails; + } + + /** + * Whether this {@link ClientMethod} has a corresponding {@link ClientMethod} that has an equivalent overload that + * contains an additional {@code Context} parameter. + * + * @return whether this method has a corresponding {@code Context}-based overload + */ + public boolean hasWithContextOverload() { + return hasWithContextOverload; + } + + /** + * Add this ClientMethod's imports to the provided set of imports. + * + * @param imports The set of imports to add to. + * @param includeImplementationImports Whether to include imports that are only necessary for method + * implementations. + */ + public void addImportsTo(Set imports, boolean includeImplementationImports, JavaSettings settings) { + + Annotation.SERVICE_METHOD.addImportsTo(imports); + Annotation.RETURN_TYPE.addImportsTo(imports); + + imports.add("java.util.Objects"); + imports.add("java.util.stream.Collectors"); + ClassType.RESPONSE.addImportsTo(imports, includeImplementationImports); + ClassType.SIMPLE_RESPONSE.addImportsTo(imports, includeImplementationImports); + + if (settings.isDataPlaneClient()) { + // for some processing on RequestOptions (get/set header) + ClassType.HTTP_HEADER_NAME.addImportsTo(imports, false); + + // for query parameter modification in RequestOptions (UrlBuilder.parse) + imports.add(UrlBuilder.class.getName()); + } + + getReturnValue().addImportsTo(imports, includeImplementationImports); + + for (ClientMethodParameter parameter : getParameters()) { + parameter.addImportsTo(imports, includeImplementationImports); + } + + if (includeImplementationImports) { + ClassType.CONTEXT.addImportsTo(imports, false); + + if (proxyMethod != null) { + proxyMethod.addImportsTo(imports, includeImplementationImports, settings); + for (ProxyMethodParameter parameter : proxyMethod.getParameters()) { + parameter.getClientType().addImportsTo(imports, true); + + if (parameter.getExplode()) { + imports.add("java.util.Optional"); + imports.add("java.util.stream.Stream"); + imports.add(ArrayList.class.getName()); + imports.add("java.util.Collection"); + } + } + } + + if (getReturnValue().getType() == ClassType.INPUT_STREAM) { + imports.add("com.fasterxml.jackson.databind.util.ByteBufferBackedInputStream"); + imports.add("java.io.SequenceInputStream"); + imports.add("java.util.Enumeration"); + imports.add("java.util.Iterator"); + } + + // Add FluxUtil as an import if this is an asynchronous method and the last parameter isn't the Context + // parameter. + if (proxyMethod != null && !proxyMethod.isSync() && (CoreUtils.isNullOrEmpty(parameters) + || parameters.get(parameters.size() - 1) != ClientMethodParameter.CONTEXT_PARAMETER)) { + imports.add("com.azure.core.util.FluxUtil"); + } + + if (getMethodPageDetails() != null) { + imports.add("com.azure.core.http.rest.PagedResponseBase"); + + if (settings.isDataPlaneClient()) { + imports.add("java.util.List"); + imports.add("java.util.Map"); + ClassType.BINARY_DATA.addImportsTo(imports, includeImplementationImports); + } + } + + if (type == ClientMethodType.LongRunningBeginAsync || type == ClientMethodType.LongRunningBeginSync) { + if (settings.isFluent()) { + if (((GenericType) this.getReturnValue() + .getType() + .getClientType()).getTypeArguments()[0] instanceof GenericType) { + // pageable LRO + if (settings.isStreamStyleSerialization()) { + imports.add(TypeReference.class.getName()); + } else { + imports.add("com.fasterxml.jackson.core.type.TypeReference"); + } + } + } else { + imports.add(TypeReference.class.getName()); + if (!JavaSettings.getInstance().isBranded()) { + imports.add(Type.class.getName()); + imports.add(ParameterizedType.class.getName()); + } + + imports.add("java.time.Duration"); + imports.add(PollingStrategyOptions.class.getName()); + + if (getMethodPollingDetails() != null) { + for (String pollingStrategy : KNOWN_POLLING_STRATEGIES) { + if (getMethodPollingDetails().getPollingStrategy().contains(pollingStrategy) + || getMethodPollingDetails().getSyncPollingStrategy().contains(pollingStrategy)) { + imports.add("com.azure.core.util.polling." + pollingStrategy); + } + } + } + } + } + + if (type == ClientMethodType.PagingAsyncSinglePage + || type == ClientMethodType.PagingSyncSinglePage && this.getMethodPageDetails() != null) { + if (this.getMethodPageDetails() != null + && this.getMethodPageDetails().getLroIntermediateType() != null) { + // pageable + LRO + this.getMethodPageDetails() + .getLroIntermediateType() + .addImportsTo(imports, includeImplementationImports); + } + } + + if (MethodUtil.isMethodIncludeRepeatableRequestHeaders(this.proxyMethod)) { + // Repeatable Requests + ClassType.CORE_UTILS.addImportsTo(imports, false); + ClassType.DATE_TIME.addImportsTo(imports, false); + ClassType.DATE_TIME_RFC_1123.addImportsTo(imports, false); + } + + if (type == ClientMethodType.SendRequestAsync || type == ClientMethodType.SendRequestSync) { + imports.add(SimpleResponse.class.getName()); + ClassType.BINARY_DATA.addImportsTo(imports, false); + ClassType.HTTP_REQUEST.addImportsTo(imports, false); + } + } + } + + public static ClientMethod getAsyncSendRequestClientMethod(boolean isInMethodGroup) { + return new Builder().name("sendRequestAsync") + .description("Sends the {@code httpRequest}.") + .clientReference(isInMethodGroup ? "this.client" : "this") + .methodVisibility(JavaVisibility.Public) + .onlyRequiredParameters(false) + .type(ClientMethodType.SendRequestAsync) + .parameters(ClientMethodParameter.HTTP_REQUEST_PARAMETER) + .returnValue(new ReturnValue("the response body on successful completion of {@link Mono}", + GenericType.Mono(GenericType.Response(ClassType.BINARY_DATA)))) + .build(); + } + + public static ClientMethod getSyncSendRequestClientMethod(boolean isInMethodGroup) { + return new Builder().name("sendRequest") + .description("Sends the {@code httpRequest}.") + .clientReference(isInMethodGroup ? "this.client" : "this") + .methodVisibility(JavaVisibility.Public) + .onlyRequiredParameters(false) + .type(ClientMethodType.SendRequestSync) + .parameters(ClientMethodParameter.HTTP_REQUEST_PARAMETER, ClientMethodParameter.CONTEXT_PARAMETER) + .returnValue(new ReturnValue("the response body along with {@link Response}", + GenericType.Response(ClassType.BINARY_DATA))) + .build(); + } + + public static class Builder { + protected String description; + protected ReturnValue returnValue; + protected String name; + protected List parameters; + protected boolean onlyRequiredParameters; + protected ClientMethodType type = ClientMethodType.values()[0]; + protected ProxyMethod proxyMethod; + protected Map validateExpressions; + protected String clientReference; + protected List requiredNullableParameterExpressions; + protected boolean isGroupedParameterRequired; + protected String groupedParameterTypeName; + protected MethodPageDetails methodPageDetails; + protected List methodTransformationDetails; + protected JavaVisibility methodVisibility = JavaVisibility.Public; + protected JavaVisibility methodVisibilityInWrapperClient = JavaVisibility.Public; + protected ImplementationDetails implementationDetails; + protected MethodPollingDetails methodPollingDetails; + protected ExternalDocumentation externalDocumentation; + protected String crossLanguageDefinitionId; + protected boolean hasWithContextOverload; + + public Builder setCrossLanguageDefinitionId(String crossLanguageDefinitionId) { + this.crossLanguageDefinitionId = crossLanguageDefinitionId; + return this; + } + + /** + * Sets the description of this ClientMethod. + * + * @param description the description of this ClientMethod + * @return the Builder itself + */ + public Builder description(String description) { + this.description = description; + return this; + } + + /** + * Sets the return value of this ClientMethod. + * + * @param returnValue the return value of this ClientMethod + * @return the Builder itself + */ + public Builder returnValue(ReturnValue returnValue) { + this.returnValue = returnValue; + return this; + } + + /** + * Sets the name of this ClientMethod. + * + * @param name the name of this ClientMethod + * @return the Builder itself + */ + public Builder name(String name) { + this.name = name; + return this; + } + + /** + * Sets the parameters of this ClientMethod. + * + * @param parameters the parameters of this ClientMethod + * @return the Builder itself + */ + public Builder parameters(List parameters) { + this.parameters = parameters; + return this; + } + + private Builder parameters(ClientMethodParameter... parameters) { + this.parameters = CoreUtils.isNullOrEmpty(parameters) ? null : Arrays.asList(parameters); + return this; + } + + /** + * Sets whether this ClientMethod has omitted optional parameters. + * + * @param onlyRequiredParameters whether this ClientMethod has omitted optional parameters + * @return the Builder itself + */ + public Builder onlyRequiredParameters(boolean onlyRequiredParameters) { + this.onlyRequiredParameters = onlyRequiredParameters; + return this; + } + + /** + * Sets the type of this ClientMethod. + * + * @param type the type of this ClientMethod + * @return the Builder itself + */ + public Builder type(ClientMethodType type) { + this.type = type; + return this; + } + + /** + * Sets the RestAPIMethod that this ClientMethod eventually calls. + * + * @param proxyMethod the RestAPIMethod that this ClientMethod eventually calls + * @return the Builder itself + */ + public Builder proxyMethod(ProxyMethod proxyMethod) { + this.proxyMethod = proxyMethod; + return this; + } + + /** + * Sets the expressions ( (parameters and service client properties) that need to be validated in this + * ClientMethod. + * + * @param validateExpressions the expressions (parameters and service client properties) that need to be + * validated in this ClientMethod + * @return the Builder itself + */ + public Builder validateExpressions(Map validateExpressions) { + this.validateExpressions = validateExpressions; + return this; + } + + /** + * Sets the reference to the service client. + * + * @param clientReference the reference to the service client + * @return the Builder itself + */ + public Builder clientReference(String clientReference) { + this.clientReference = clientReference; + return this; + } + + /** + * Sets the parameter expressions which are required. + * + * @param requiredNullableParameterExpressions the parameter expressions which are required + * @return the Builder itself + */ + public Builder requiredNullableParameterExpressions(List requiredNullableParameterExpressions) { + this.requiredNullableParameterExpressions = requiredNullableParameterExpressions; + return this; + } + + /** + * Sets the parameter that needs to transformed before pagination. + * + * @param isGroupedParameterRequired the parameter that needs to transformed before pagination + * @return the Builder itself + */ + public Builder groupedParameterRequired(boolean isGroupedParameterRequired) { + this.isGroupedParameterRequired = isGroupedParameterRequired; + return this; + } + + /** + * Sets the type name of groupedParameter. + * + * @param groupedParameterTypeName the type name of groupedParameter + * @return the Builder itself + */ + public Builder groupedParameterTypeName(String groupedParameterTypeName) { + this.groupedParameterTypeName = groupedParameterTypeName; + return this; + } + + /** + * Sets the pagination information if this is a paged method. + * + * @param methodPageDetails the pagination information if this is a paged method + * @return the Builder itself + */ + public Builder methodPageDetails(MethodPageDetails methodPageDetails) { + this.methodPageDetails = methodPageDetails; + return this; + } + + /** + * Sets the parameter transformations before calling ProxyMethod. + * + * @param methodTransformationDetails the parameter transformations before calling ProxyMethod + * @return the Builder itself + */ + public Builder methodTransformationDetails(List methodTransformationDetails) { + this.methodTransformationDetails = methodTransformationDetails; + return this; + } + + /** + * Sets the parameter method visibility. + * + * @param methodVisibility the method visibility, default is Public. + * @return the Builder itself + */ + public Builder methodVisibility(JavaVisibility methodVisibility) { + this.methodVisibility = methodVisibility; + return this; + } + + /** + * Sets the parameter method visibility in wrapper client. + * + * @param methodVisibilityInWrapperClient the method visibility in wrapper client, default is Public. + * @return the Builder itself + */ + public Builder methodVisibilityInWrapperClient(JavaVisibility methodVisibilityInWrapperClient) { + this.methodVisibilityInWrapperClient = methodVisibilityInWrapperClient; + return this; + } + + /** + * Sets the polling information if this is a long running method. + * + * @param methodPollingDetails the polling information + * @return the Builder itself + */ + public Builder methodPollingDetails(MethodPollingDetails methodPollingDetails) { + this.methodPollingDetails = methodPollingDetails; + return this; + } + + /** + * Sets the implementation details for the method. + * + * @param implementationDetails the implementation details. + * @return the Builder itself + */ + public Builder implementationDetails(ImplementationDetails implementationDetails) { + this.implementationDetails = implementationDetails; + return this; + } + + /** + * Sets method documentation + * + * @param externalDocumentation method level documentation + * @return the Builder itself + */ + public Builder methodDocumentation(ExternalDocumentation externalDocumentation) { + this.externalDocumentation = externalDocumentation; + return this; + } + + /** + * Whether this {@link ClientMethod} has a corresponding {@link ClientMethod} that has an additional + * {@code Context} parameter. + *

+ * When this is true, when this method generates its client method it will call into the {@code Context}-based + * overload instead of generating a method body. This helps to avoid generating duplicate method bodies that + * only differ by the presence of a {@code Context} parameter. + * + * @param hasWithContextOverload whether this method has a corresponding {@code Context}-based overload + * @return the Builder itself + */ + public Builder hasWithContextOverload(boolean hasWithContextOverload) { + this.hasWithContextOverload = hasWithContextOverload; + return this; + } + + /** + * @return an immutable ClientMethod instance with the configurations on this builder. + */ + public ClientMethod build() { + return new ClientMethod(description, returnValue, name, parameters, onlyRequiredParameters, type, + proxyMethod, validateExpressions, clientReference, requiredNullableParameterExpressions, + isGroupedParameterRequired, groupedParameterTypeName, methodPageDetails, methodTransformationDetails, + methodVisibility, methodVisibilityInWrapperClient, implementationDetails, methodPollingDetails, + externalDocumentation, crossLanguageDefinitionId, hasWithContextOverload); + } + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/ClientMethodExample.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/ClientMethodExample.java new file mode 100644 index 0000000000..486b20d493 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/ClientMethodExample.java @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.model.clientmodel; + +public class ClientMethodExample { + private final ClientMethod clientMethod; + + private final AsyncSyncClient syncClient; + + private final ClientBuilder clientBuilder; + + private final String filename; + + private final ProxyMethodExample proxyMethodExample; + + public ClientMethodExample( + ClientMethod clientMethod, + AsyncSyncClient syncClient, + ClientBuilder clientBuilder, + String filename, + ProxyMethodExample proxyMethodExample) { + this.clientMethod = clientMethod; + this.syncClient = syncClient; + this.clientBuilder = clientBuilder; + this.filename = filename; + this.proxyMethodExample = proxyMethodExample; + } + + public ClientMethod getClientMethod() { + return clientMethod; + } + + public AsyncSyncClient getSyncClient() { + return syncClient; + } + + public ClientBuilder getClientBuilder() { + return clientBuilder; + } + + public String getFilename() { + return filename; + } + + public ProxyMethodExample getProxyMethodExample() { + return proxyMethodExample; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/ClientMethodParameter.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/ClientMethodParameter.java new file mode 100644 index 0000000000..44c4b03314 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/ClientMethodParameter.java @@ -0,0 +1,296 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.model.clientmodel; + +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.RequestParameterLocation; + +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * A parameter for a method. + */ +public class ClientMethodParameter extends MethodParameter { + + public static final ClientMethodParameter CONTEXT_PARAMETER = new ClientMethodParameter.Builder() + .description("The context to associate with this operation.") + .wireType(ClassType.CONTEXT) + .name("context") + .requestParameterLocation(RequestParameterLocation.NONE) + .annotations(Collections.emptyList()) + .constant(false) + .defaultValue(null) + .fromClient(false) + .finalParameter(false) + .required(false) + .build(); + + public static final ClientMethodParameter HTTP_REQUEST_PARAMETER = new ClientMethodParameter.Builder() + .description("The HTTP request to send.") + .wireType(ClassType.HTTP_REQUEST) + .name("httpRequest") + .requestParameterLocation(RequestParameterLocation.NONE) + .annotations(Collections.emptyList()) + .constant(false) + .defaultValue(null) + .fromClient(false) + .finalParameter(false) + .required(true) + .build(); + + public static final ClientMethodParameter REQUEST_OPTIONS_PARAMETER = new ClientMethodParameter.Builder() + .description("The options to configure the HTTP request before HTTP client sends it.") + .wireType(ClassType.REQUEST_OPTIONS) + .name("requestOptions") + .requestParameterLocation(RequestParameterLocation.NONE) + .constant(false) + .required(false) + .fromClient(false) + .annotations(Collections.emptyList()) + .build(); + + /** + * Whether this parameter is final. + */ + private final boolean isFinal; + /** + * The annotations that should be part of this Parameter's declaration. + */ + private final List annotations; + + private final Versioning versioning; + + /** + * Create a new Parameter with the provided properties. + * @param description The description of this parameter. + * @param isFinal Whether this parameter is final. + * @param wireType The type of this parameter. + * @param rawType The raw type of this parameter. Result of SchemaMapper. + * @param name The name of this parameter. + * @param isRequired Whether this parameter is required. + * @param isConstant Whether this parameter has a constant value. + * @param fromClient Whether this parameter is from a client property. + * @param annotations The annotations that should be part of this Parameter's declaration. + */ + private ClientMethodParameter(String description, boolean isFinal, IType wireType, IType rawType, String name, + boolean isRequired, boolean isConstant, boolean fromClient, String defaultValue, List annotations, + RequestParameterLocation location, Versioning versioning) { + super(description, wireType, rawType, wireType.getClientType(), name, location, isConstant, isRequired, + fromClient, defaultValue); + this.isFinal = isFinal; + this.annotations = annotations; + this.versioning = versioning; + } + + public final boolean isFinal() { + return isFinal; + } + + public final List getAnnotations() { + return annotations; + } + + public Versioning getVersioning() { + return versioning; + } + + /** + * Creates a builder that is initialized with all the builder properties set to current values of this instance. + * @return A new builder instance initialized with properties values of this instance. + */ + public ClientMethodParameter.Builder newBuilder() { + return new Builder(this); + } + + /** + * The full declaration of this parameter as it appears in a method signature. + */ + public final String getDeclaration() { + return getAnnotations().stream().map(annotation -> "@" + annotation.getName()).collect(Collectors.joining("")) + + (isFinal() ? "final " : "") + String.format("%1$s %2$s", getClientType(), getName()); + } + + /** + * Add this parameter's imports to the provided set of imports. + * @param imports The set of imports to add to. + * @param includeImplementationImports Whether to include imports that are only necessary for method implementations. + */ + public void addImportsTo(Set imports, boolean includeImplementationImports) { + for (ClassType annotation : getAnnotations()) { + annotation.addImportsTo(imports, includeImplementationImports); + } + getClientType().addImportsTo(imports, includeImplementationImports); + if (includeImplementationImports && getRawType() != null) { + getRawType().addImportsTo(imports, includeImplementationImports); + } + } + + public static class Builder { + private String description; + private boolean isFinal; + private IType wireType; + private IType rawType; + private String name; + private boolean isRequired; + private boolean isConstant; + private boolean fromClient; + private String defaultValue; + private List annotations; + private RequestParameterLocation requestParameterLocation; + private Versioning versioning; + + /** + * Sets the description of this parameter. + * @param description the description of this parameter + * @return the Builder itself + */ + public Builder description(String description) { + this.description = description; + return this; + } + + /** + * Sets whether this parameter is final. + * @param isFinal whether this parameter is final + * @return the Builder itself + */ + public Builder finalParameter(boolean isFinal) { + this.isFinal = isFinal; + return this; + } + + /** + * Sets the type of this parameter. + * @param wireType the type of this parameter + * @return the Builder itself + */ + public Builder wireType(IType wireType) { + this.wireType = wireType; + return this; + } + + /** + * Sets the raw type of this parameter. Result of SchemaMapper. + * @param rawType the raw type of this parameter + * @return the Builder itself + */ + public Builder rawType(IType rawType) { + this.rawType = rawType; + return this; + } + + /** + * Sets the name of this parameter. + * @param name the name of this parameter + * @return the Builder itself + */ + public Builder name(String name) { + this.name = name; + return this; + } + + /** + * Sets whether this parameter is required. + * @param isRequired whether this parameter is required + * @return the Builder itself + */ + public Builder required(boolean isRequired) { + this.isRequired = isRequired; + return this; + } + + /** + * Sets whether this parameter has a constant value. + * @param isConstant whether this parameter has a constant value + * @return the Builder itself + */ + public Builder constant(boolean isConstant) { + this.isConstant = isConstant; + return this; + } + + /** + * Sets whether this parameter is from a client property. + * @param fromClient whether this parameter is from a client property + * @return the Builder itself + */ + public Builder fromClient(boolean fromClient) { + this.fromClient = fromClient; + return this; + } + + /** + * Sets the default value for the parameter. + * @param defaultValue the default value for the parameter + * @return the Builder itself + */ + public Builder defaultValue(String defaultValue) { + this.defaultValue = defaultValue; + return this; + } + + /** + * Sets the annotations that should be part of this Parameter's declaration. + * @param annotations the annotations that should be part of this Parameter's declaration + * @return the Builder itself + */ + public Builder annotations(List annotations) { + this.annotations = annotations; + return this; + } + + /** + * Sets the location of the parameter. + * @param requestParameterLocation the location of the parameter + * @return the Builder itself + */ + public Builder requestParameterLocation(RequestParameterLocation requestParameterLocation) { + this.requestParameterLocation = requestParameterLocation; + return this; + } + + public Builder versioning(Versioning versioning) { + this.versioning = versioning; + return this; + } + + /** + * Creates a new instance of Builder. + */ + public Builder() { + } + + private Builder(ClientMethodParameter parameter) { + this.description = parameter.getDescription(); + this.isFinal = parameter.isFinal(); + this.wireType = parameter.getWireType(); + this.rawType = parameter.getRawType(); + this.name = parameter.getName(); + this.isRequired = parameter.isRequired(); + this.isConstant = parameter.isConstant(); + this.fromClient = parameter.isFromClient(); + this.defaultValue = parameter.getDefaultValue(); + this.annotations = parameter.getAnnotations(); + this.requestParameterLocation = parameter.getRequestParameterLocation(); + this.versioning = parameter.getVersioning(); + } + + public ClientMethodParameter build() { + return new ClientMethodParameter(description, + isFinal, + wireType, + rawType, + name, + isRequired, + isConstant, + fromClient, + defaultValue, + annotations, + requestParameterLocation, + versioning); + } + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/ClientMethodType.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/ClientMethodType.java new file mode 100644 index 0000000000..1270274825 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/ClientMethodType.java @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.model.clientmodel; + +import java.util.HashMap; +import java.util.Map; + +/** + * The different types of ClientMethod overloads that can exist in a client. + */ +public enum ClientMethodType { + PagingSync(0, true, false, true), + PagingAsync(1, true, false, false), + PagingAsyncSinglePage(2, true, false, false), + + SimulatedPagingSync(3, false, false, true), + SimulatedPagingAsync(4, false, false, false), + + LongRunningSync(5, false, true, true), + LongRunningAsync(6, false, true, false), + LongRunningBeginSync(7, false, true, true), + LongRunningBeginAsync(8, false, true, false), + + SimpleSync(9, false, false, true), + // will not generate when sync-methods=none, will generate when sync-methods=essential, + SimpleAsync(10, false, false, false), + SimpleAsyncRestResponse(11, false, false, false), + SimpleSyncRestResponse(12, false, false, true), + + Resumable(13, false, false, false), + + SendRequestSync(14, false, false, true), + SendRequestAsync(15, false, false, false), + PagingSyncSinglePage(16, true, false, true),; + + private static final Map MAPPINGS; + + static { + MAPPINGS = new HashMap<>(); + for (ClientMethodType methodType : ClientMethodType.values()) { + MAPPINGS.put(methodType.intValue, methodType); + } + } + + private final int intValue; + private final boolean isPaging; + private final boolean isLongRunning; + private final boolean isSync; + + ClientMethodType(int value, boolean isPaging, boolean isLongRunning, boolean isSync) { + intValue = value; + this.isPaging = isPaging; + this.isLongRunning = isLongRunning; + this.isSync = isSync; + } + + public static ClientMethodType forValue(int value) { + return MAPPINGS.get(value); + } + + public int getValue() { + return intValue; + } + + public boolean isPaging() { + return isPaging; + } + + public boolean isLongRunning() { + return isLongRunning; + } + + public boolean isSync() { + return isSync; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/ClientModel.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/ClientModel.java new file mode 100644 index 0000000000..0a0a5f1f10 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/ClientModel.java @@ -0,0 +1,777 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.model.clientmodel; + +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; +import com.azure.core.util.CoreUtils; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; + +/** + * A model that is defined by the client. + */ +public class ClientModel { + /** + * The package that this model class belongs to. + */ + private final String packageName; + + /** + * Get the name of this model. + */ + private final String name; + + private final String fullName; + + /** + * Get the imports for this model. + */ + private final List imports; + + /** + * Get the description of this model. + */ + private final String description; + + /** + * Get whether this model is part of a polymorphic hierarchy. + */ + private final boolean isPolymorphic; + + /** + * Get whether this model is a parent in a polymorphic hierarchy. + */ + private final boolean isPolymorphicParent; + + /** + * Get the properties used by parent models as polymorphic discriminators. + */ + private final List parentPolymorphicDiscriminators; + /** + * Get the property that determines which polymorphic model type to create. + */ + private final ClientModelProperty polymorphicDiscriminator; + + /** + * Get the name of the property that determines which polymorphic model type to create. + */ + private final String polymorphicDiscriminatorName; + + /** + * Get the name that is used for this model when it is serialized. + */ + private final String serializedName; + + /** + * Get whether this model needs serialization flattening. + */ + private final boolean needsFlatten; + + /** + * Get the parent model of this model. + */ + private final String parentModelName; + + /** + * Get the models that derive from this model. + */ + private final List derivedModels; + + /** + * Get the name that will be used for this model's XML element representation. + */ + private final String xmlName; + + /** + * The xml namespace for a model. + */ + private final String xmlNamespace; + + /** + * Get the properties for this model. + */ + private final List properties; + + /** + * Get the property references for this model. They are used to call property method from i.e. a parent model. + */ + private final List propertyReferences; + + /** + * The type of the model. + */ + private final IType modelType; + + /** + * Whether this model is a strongly-typed HTTP headers class. + */ + private final boolean stronglyTypedHeader; + + /** + * The implementation details for the model. + */ + private final ImplementationDetails implementationDetails; + + /** + * Whether the model is used in XML serialization. + */ + private final boolean usedInXml; + + private final Set serializationFormats; + + /** + * The cross language definition id for the model. + */ + private final String crossLanguageDefinitionId; + + /** + * Create a new ServiceModel with the provided properties. + * + * @param packageKeyword The package that this model class belongs to. + * @param name The name of this model. + * @param imports The imports for this model. + * @param description The description of this model. + * @param isPolymorphic Whether this model has model types that derive from it. + * @param polymorphicDiscriminator The property that determines which polymorphic model type to create. + * @param polymorphicDiscriminatorName The name of the property that determines which polymorphic model type to + * create. + * @param serializedName The name that is used for this model when it is serialized. + * @param needsFlatten Whether this model needs serialization flattening. + * @param parentModelName The parent model of this model. + * @param derivedModels The models that derive from this model. + * @param xmlName The name that will be used for this model's XML element representation. + * @param xmlNamespace The XML namespace that will be used for this model's XML element representation. + * @param properties The properties for this model. + * @param propertyReferences The property references for this model. + * @param modelType the type of the model. + * @param stronglyTypedHeader Whether this model is a strongly-typed HTTP headers class. + * @param implementationDetails The implementation details for the model. + * @param usedInXml Whether the model is used in XML serialization. + * @param crossLanguageDefinitionId The cross language definition id for the model. + */ + protected ClientModel(String packageKeyword, String name, List imports, String description, + boolean isPolymorphic, ClientModelProperty polymorphicDiscriminator, String polymorphicDiscriminatorName, + String serializedName, boolean needsFlatten, String parentModelName, List derivedModels, + String xmlName, String xmlNamespace, List properties, + List propertyReferences, IType modelType, boolean stronglyTypedHeader, + ImplementationDetails implementationDetails, boolean usedInXml, Set serializationFormats, + String crossLanguageDefinitionId) { + this.packageName = packageKeyword; + this.name = name; + this.fullName = packageName + "." + name; + this.imports = imports; + this.description = description; + this.isPolymorphic = isPolymorphic; + this.isPolymorphicParent = isPolymorphic && !CoreUtils.isNullOrEmpty(derivedModels); + this.parentPolymorphicDiscriminators = new ArrayList<>(); + this.polymorphicDiscriminator = polymorphicDiscriminator; + this.polymorphicDiscriminatorName = polymorphicDiscriminatorName; + this.serializedName = serializedName; + this.needsFlatten = needsFlatten; + this.parentModelName = parentModelName; + this.derivedModels = derivedModels; + this.xmlName = xmlName; + this.xmlNamespace = xmlNamespace; + this.properties = properties; + this.propertyReferences = propertyReferences; + this.modelType = modelType; + this.stronglyTypedHeader = stronglyTypedHeader; + this.implementationDetails = implementationDetails; + this.usedInXml = usedInXml; + this.crossLanguageDefinitionId = crossLanguageDefinitionId; + this.serializationFormats = serializationFormats; + } + + /** + * Get the cross language definition id for the model. + * + * @return the cross language definition id for the model. + */ + public String getCrossLanguageDefinitionId() { + return crossLanguageDefinitionId; + } + + /** + * Gets the package that this model class belongs to. + * + * @return The package that this model class belongs to. + */ + public final String getPackage() { + return packageName; + } + + /** + * Gets the name of this model. + * + * @return The name of this model. + */ + public final String getName() { + return name; + } + + /** + * Gets the fully qualified name of this model. + * + * @return The fully qualified name of this model. + */ + public final String getFullName() { + return fullName; + } + + /** + * Gets the imports for this model. + * + * @return The imports for this model. + */ + public final List getImports() { + return imports; + } + + /** + * Gets the description of this model. + * + * @return The description of this model. + */ + public final String getDescription() { + return description; + } + + /** + * Gets whether this model is part of a polymorphic hierarchy. + * + * @return Whether this model is part of a polymorphic hierarchy. + */ + public final boolean isPolymorphic() { + return isPolymorphic; + } + + /** + * Gets whether this model is a parent in a polymorphic hierarchy. + * + * @return Whether this model is a parent in a polymorphic hierarchy. + */ + public final boolean isPolymorphicParent() { + return isPolymorphicParent; + } + + /** + * Gets the properties used by parent models as polymorphic discriminators. + *

+ * The only time this will return a non-empty list is when this model is used in a multi-level polymorphic + * hierarchy. Or, as an example, if the root model uses a polymorphic discriminator of {@code kind} and this model + * uses a polymorphic discriminator of {@code type} this will have a single property where the serialized name is + * {@code kind} and the default value for it will be what to root model uses to determine that this is the model + * that should be deserialized. Continuing this example, the third level models, or those that are determined by the + * {@code type} value, will also have a single property where the serialized name is {@code kind} and the default + * value will be what the second level model uses to determine that this is the model that should be deserialized. + * This is because the {@code kind} property will always need to be present for these models. If there are even + * deeper levels of polymorphism, the same pattern will continue. So, if in the third level there is a model that + * introduces another polymorphic discriminator of {@code format} that model would have two properties in this list, + * one with {@code kind} with a default that determined the second level model and one with {@code type} with a + * default that determined the third level model. The fourth level model would then have both as well. + * + * @return The properties used by parent models as polymorphic discriminators. + */ + public final List getParentPolymorphicDiscriminators() { + return parentPolymorphicDiscriminators; + } + + /** + * Gets the property that determines which polymorphic model type to create. + * + * @return The property that determines which polymorphic model type to create. + */ + public final ClientModelProperty getPolymorphicDiscriminator() { + return polymorphicDiscriminator; + } + + /** + * Gets the name of the property that determines which polymorphic model type to create. + * + * @return The name of the property that determines which polymorphic model type to create. + */ + public final String getPolymorphicDiscriminatorName() { + return polymorphicDiscriminatorName; + } + + /** + * Gets the name that is used for this model when it is serialized. + * + * @return The name that is used for this model when it is serialized. + */ + public final String getSerializedName() { + return serializedName; + } + + /** + * Gets whether this model needs serialization flattening. + * + * @return Whether this model needs serialization flattening. + */ + public final boolean getNeedsFlatten() { + return needsFlatten; + } + + /** + * Gets the parent model of this model. + * + * @return The parent model of this model. + */ + public final String getParentModelName() { + return parentModelName; + } + + /** + * Gets the models that derive from this model. + * + * @return The models that derive from this model. + */ + public final List getDerivedModels() { + return derivedModels; + } + + /** + * Gets the name that will be used for this model's XML element representation. + * + * @return The name that will be used for this model's XML element representation. + */ + public final String getXmlName() { + return xmlName; + } + + /** + * Gets the XML namespace that will be used for this model's XML element representation. + * + * @return The XML namespace that will be used for this model's XML element representation. + */ + public String getXmlNamespace() { + return xmlNamespace; + } + + /** + * Gets the properties for this model. + * + * @return The properties for this model. + */ + public final List getProperties() { + return properties; + } + + /** + * Gets the type of the model. + * + * @return The type of the model. + */ + public IType getType() { + return modelType; + } + + /** + * Gets the property references for this model. They are used to call property method from i.e. a parent model. + * + * @return The property references for this model. + */ + public List getPropertyReferences() { + return propertyReferences == null ? Collections.emptyList() : propertyReferences; + } + + /** + * Whether this model is a strongly-typed HTTP headers class. + * + * @return Whether this model is a strongly-typed HTTP headers class. + */ + public boolean isStronglyTypedHeader() { + return stronglyTypedHeader; + } + + /** + * Gets the implementation details for the model. + * + * @return The implementation details for the model. + */ + public ImplementationDetails getImplementationDetails() { + return implementationDetails; + } + + /** + * List the properties that have access (getter or setter) methods. + *

+ * It does not include properties from superclass (even though they can be accessed via inheritance). It does not + * include properties that only have private access (e.g. property of a flattened model). It includes properties + * that can be accessed from the model but not declared in this model (e.g. properties from a flattened model). + * + * @return The properties that have access (getter or setter) methods. + */ + public List getAccessibleProperties() { + List propertyAccesses = new ArrayList<>(); + if (properties != null) { + for (ClientModelProperty property : properties) { + if (!property.getClientFlatten()) { + propertyAccesses.add(property); + } + } + } + + for (ClientModelPropertyReference clientModelPropertyReference : getPropertyReferences()) { + if (clientModelPropertyReference.isFromFlattenedProperty()) { + propertyAccesses.add(clientModelPropertyReference); + } + } + + return propertyAccesses; + } + + /** + * Add this ServiceModel's imports to the provided set of imports. + * + * @param imports The set of imports to add to. + * @param settings The settings for this Java generator session. + */ + public void addImportsTo(Set imports, JavaSettings settings) { + // whether annotated as Immutable or Fluent is also determined by its superclass + imports.add(this.getFullName()); + addFluentAnnotationImport(imports); + addImmutableAnnotationImport(imports); + + if (settings.getClientFlattenAnnotationTarget() == JavaSettings.ClientFlattenAnnotationTarget.TYPE + && needsFlatten) { + addJsonFlattenAnnotationImport(imports); + } + + imports.addAll(getImports()); + + if (isPolymorphic()) { + imports.add("com.fasterxml.jackson.annotation.JsonTypeInfo"); + imports.add("com.fasterxml.jackson.annotation.JsonTypeName"); + + if (getDerivedModels() != null && getDerivedModels().size() > 0) { + imports.add("com.fasterxml.jackson.annotation.JsonSubTypes"); + getDerivedModels().forEach(m -> imports.add(m.getFullName())); + } + } + + for (ClientModelProperty property : getProperties()) { + property.addImportsTo(imports, usedInXml); + } + } + + /** + * Add the Fluent annotation import to the provided set of imports. + * + * @param imports The set of imports to add to. + */ + protected void addJsonFlattenAnnotationImport(Set imports) { + imports.add("com.azure.core.annotation.JsonFlatten"); + } + + /** + * Add the Immutable annotation import to the provided set of imports. + * + * @param imports The set of imports to add to. + */ + protected void addImmutableAnnotationImport(Set imports) { + Annotation.IMMUTABLE.addImportsTo(imports); + if (!JavaSettings.getInstance().isBranded()) { + Annotation.TYPE_CONDITIONS.addImportsTo(imports); + Annotation.METADATA.addImportsTo(imports); + } + } + + /** + * Add the Fluent annotation import to the provided set of imports. + * + * @param imports The set of imports to add to. + */ + protected void addFluentAnnotationImport(Set imports) { + Annotation.FLUENT.addImportsTo(imports); + if (!JavaSettings.getInstance().isBranded()) { + Annotation.METADATA.addImportsTo(imports); + } + } + + /** + * Whether the model is used in XML serialization. + * + * @return Whether the model is used in XML serialization. + */ + public final boolean isUsedInXml() { + return usedInXml; + } + + /** + * Gets the Set of serialization format of the model. + * + * @return the Set of serialization format of the model. + */ + public Set getSerializationFormats() { + return serializationFormats; + } + + /** + * A builder for building a new ClientModel. + */ + public static class Builder { + protected String packageName; + protected String name; + protected List imports = Collections.emptyList(); + protected String description; + protected boolean isPolymorphic; + protected ClientModelProperty polymorphicDiscriminator; + protected String polymorphicDiscriminatorName; + protected String serializedName; + protected boolean needsFlatten = false; + protected String parentModelName; + protected List derivedModels = Collections.emptyList(); + protected String xmlName; + protected List properties; + protected String xmlNamespace; + protected List propertyReferences; + protected IType modelType; + protected boolean stronglyTypedHeader; + protected ImplementationDetails implementationDetails; + protected boolean usedInXml; + protected String crossLanguageDefinitionId; + protected Set serializationFormats = Collections.emptySet(); + + /** + * Sets the package that this model class belongs to. + * + * @param packageName the package that this model class belongs to + * @return the Builder itself + */ + public Builder packageName(String packageName) { + this.packageName = packageName; + return this; + } + + /** + * Sets the name of this model. + * + * @param name the name of this model + * @return the Builder itself + */ + public Builder name(String name) { + this.name = name; + return this; + } + + /** + * Sets the imports for this model. + * + * @param imports the imports for this model + * @return the Builder itself + */ + public Builder imports(List imports) { + this.imports = imports; + return this; + } + + /** + * Sets the description of this model. + * + * @param description the description of this model + * @return the Builder itself + */ + public Builder description(String description) { + this.description = description; + return this; + } + + /** + * Sets whether this model has model types that derive from it. + * + * @param isPolymorphic whether this model has model types that derive from it + * @return the Builder itself + */ + public Builder polymorphic(boolean isPolymorphic) { + this.isPolymorphic = isPolymorphic; + return this; + } + + /** + * Sets the property that determines which polymorphic model type to create. + * + * @param polymorphicDiscriminator the property that determines which polymorphic model type to create + * @return the Builder itself + */ + public Builder polymorphicDiscriminator(ClientModelProperty polymorphicDiscriminator) { + this.polymorphicDiscriminator = polymorphicDiscriminator; + return this; + } + + public Builder polymorphicDiscriminatorName(String polymorphicDiscriminatorName) { + this.polymorphicDiscriminatorName = polymorphicDiscriminatorName; + return this; + } + + /** + * Sets the name that is used for this model when it is serialized. + * + * @param serializedName the name that is used for this model when it is serialized + * @return the Builder itself + */ + public Builder serializedName(String serializedName) { + this.serializedName = serializedName; + return this; + } + + /** + * Sets whether this model needs serialization flattening. + * + * @param needsFlatten whether this model needs serialization flattening + * @return the Builder itself + */ + public Builder needsFlatten(boolean needsFlatten) { + this.needsFlatten = needsFlatten; + return this; + } + + /** + * Sets the parent model of this model. + * + * @param parentModelName the parent model of this model + * @return the Builder itself + */ + public Builder parentModelName(String parentModelName) { + this.parentModelName = parentModelName; + return this; + } + + /** + * Sets the models that derive from this model. + * + * @param derivedModels the models that derive from this model + * @return the Builder itself + */ + public Builder derivedModels(List derivedModels) { + this.derivedModels = derivedModels; + return this; + } + + /** + * Sets the name that will be used for this model's XML element representation. + * + * @param xmlName the name that will be used for this model's XML element representation + * @return the Builder itself + */ + public Builder xmlName(String xmlName) { + this.xmlName = xmlName; + return this; + } + + /** + * Sets the XML namespace that will be used for this model's XML element representation. + * + * @param xmlNamespace the XML namespace that will be used for this model's XML element representation + * @return the Builder itself + */ + public Builder xmlNamespace(String xmlNamespace) { + this.xmlNamespace = xmlNamespace; + return this; + } + + /** + * Sets the properties for this model. + * + * @param properties the properties for this model + * @return the Builder itself + */ + public Builder properties(List properties) { + this.properties = properties; + return this; + } + + /** + * Sets the property references for this model. They are used to call property method from i.e. a parent model. + * + * @param propertyReferences the property references. + * @return the Builder itself + */ + public Builder propertyReferences(List propertyReferences) { + this.propertyReferences = propertyReferences; + return this; + } + + /** + * Sets the model type. + * + * @param modelType the model type. + * @return the Builder itself + */ + public Builder type(IType modelType) { + this.modelType = modelType; + return this; + } + + /** + * Sets whether the model is a strongly-typed HTTP headers class. + * + * @param stronglyTypedHeader Whether the model is a strongly-typed HTTP headers class. + * @return the Builder itself + */ + public Builder stronglyTypedHeader(boolean stronglyTypedHeader) { + this.stronglyTypedHeader = stronglyTypedHeader; + return this; + } + + /** + * Sets the implementation details for the model. + * + * @param implementationDetails the implementation details. + * @return the Builder itself + */ + public Builder implementationDetails(ImplementationDetails implementationDetails) { + this.implementationDetails = implementationDetails; + return this; + } + + /** + * Sets whether the model is used in XML serialization. + * + * @param usedInXml Whether the model is used in XML serialization. + * @return the Builder itself + */ + public Builder usedInXml(boolean usedInXml) { + this.usedInXml = usedInXml; + return this; + } + + /** + * Sets the cross language definition id for the model. + * + * @param crossLanguageDefinitionId the cross language definition id for the model. + * @return the Builder itself + */ + public Builder crossLanguageDefinitionId(String crossLanguageDefinitionId) { + this.crossLanguageDefinitionId = crossLanguageDefinitionId; + return this; + } + + /** + * Sets the Set of serialization format of this model. + * + * @param serializationFormats the Set of serialization format of this model. + * @return the Builder itself + */ + public Builder serializationFormats(Set serializationFormats) { + this.serializationFormats = serializationFormats == null ? Collections.emptySet() : serializationFormats; + return this; + } + + /** + * Build a new ClientModel instance with the provided properties. + * + * @return a new ClientModel instance with the provided properties + */ + public ClientModel build() { + return new ClientModel(packageName, name, imports, description, isPolymorphic, polymorphicDiscriminator, + polymorphicDiscriminatorName, serializedName, needsFlatten, parentModelName, derivedModels, xmlName, + xmlNamespace, properties, propertyReferences, modelType, stronglyTypedHeader, implementationDetails, + usedInXml, serializationFormats, crossLanguageDefinitionId); + } + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/ClientModelProperty.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/ClientModelProperty.java new file mode 100644 index 0000000000..57a116f596 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/ClientModelProperty.java @@ -0,0 +1,701 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.model.clientmodel; + +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; +import com.microsoft.typespec.http.client.generator.core.util.CodeNamer; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Set; + +/** + * A property that exists within a model defined by the client. + */ +public class ClientModelProperty implements ClientModelPropertyAccess { + /** + * Get the name of this property. + */ + private final String name; + /** + * Get the description of this property. + */ + private final String description; + /** + * Get the arguments that go into this property's JsonProperty annotation. + */ + private final String annotationArguments; + /** + * Get whether this property is an attribute when serialized to XML. + */ + private final boolean isXmlAttribute; + /** + * Get this property's name when serialized to XML. + */ + private final String xmlName; + private final String xmlNamespace; + /** + * Get this property's name when it is serialized. + */ + private final String serializedName; + /** + * Get whether this property is a container. + */ + private final boolean isXmlWrapper; + /** + * The name of each list element tag within an XML list property. + */ + private final String xmlListElementName; + /** + * The namespace of each list element tag within an XML list property. + */ + private final String xmlListElementNamespace; + private final String xmlListElementPrefix; + /** + * The type of this property as it is transmitted across the network (across the wire). + */ + private final IType wireType; + /** + * The type of this property as it will be exposed via the client. + */ + private final IType clientType; + /** + * Get whether this property has a constant value. + */ + private final boolean isConstant; + /** + * Get the default value expression of this property. + */ + private final String defaultValue; + /** + * Get whether this property's value can be changed by the client library. + */ + private final boolean isReadOnly; + /** + * Whether this property is required. + */ + private boolean isRequired; + /** + * The prefix of the headers that make up this property's values. + */ + private final String headerCollectionPrefix; + + private final boolean isAdditionalProperties; + + private final List mutabilities; + + private final boolean needsFlatten; + private final boolean clientFlatten; + private final boolean polymorphicDiscriminator; + private final boolean isXmlText; + private final String xmlPrefix; + + private final Boolean requiredForCreate; + + /** + * Create a new ClientModelProperty with the provided properties. + * @param name The name of this property. + * @param description The description of this property. + * @param annotationArguments The arguments that go into this property's JsonProperty annotation. + * @param isXmlAttribute Whether this property is an attribute when serialized to XML. + * @param xmlName This property's name when serialized to XML. + * @param xmlNamespace Namespace of the XML attribute or element this property represents. + * @param serializedName This property's name when it is serialized. + * @param isXmlWrapper Whether this property is a container. + * @param xmlListElementName The name of each list element tag within an XML list property. + * @param wireType The type of this property as it is transmitted across the network (across the wire). + * @param clientType The type of this property as it will be exposed via the client. + * @param isConstant Whether this property has a constant value. + * @param defaultValue The default value expression of this property. + * @param isReadOnly Whether this property's value can be changed by the client library. + * @param mutabilities List of property mutability. + * @param headerCollectionPrefix The prefix of the headers that make up this property's values. + * @param isAdditionalProperties Whether this property contain the additional properties. + * @param polymorphicDiscriminator Whether this property is a polymorphic discriminator. + * @param isXmlText Whether this property uses the value of an XML tag. + * @param xmlPrefix The prefix of the XML attribute or element this property represents. + */ + private ClientModelProperty(String name, String description, String annotationArguments, boolean isXmlAttribute, + String xmlName, String xmlNamespace, String serializedName, boolean isXmlWrapper, String xmlListElementName, + String xmlListElementNamespace, String xmlListElementPrefix, IType wireType, IType clientType, + boolean isConstant, String defaultValue, boolean isReadOnly, List mutabilities, boolean isRequired, + String headerCollectionPrefix, boolean isAdditionalProperties, boolean needsFlatten, boolean clientFlatten, + boolean polymorphicDiscriminator, boolean isXmlText, String xmlPrefix, Boolean requiredForCreate) { + this.name = name; + this.description = description; + this.annotationArguments = annotationArguments; + this.isXmlAttribute = isXmlAttribute; + this.xmlName = xmlName; + this.xmlNamespace = xmlNamespace; + this.serializedName = serializedName; + this.isXmlWrapper = isXmlWrapper; + this.xmlListElementName = xmlListElementName; + this.xmlListElementNamespace = xmlListElementNamespace; + this.xmlListElementPrefix = xmlListElementPrefix; + this.wireType = wireType; + this.clientType = clientType; + this.isConstant = isConstant; + this.defaultValue = defaultValue; + this.isReadOnly = isReadOnly; + this.mutabilities = mutabilities; + this.isRequired = isRequired; + this.headerCollectionPrefix = headerCollectionPrefix; + this.isAdditionalProperties = isAdditionalProperties; + this.needsFlatten = needsFlatten; + this.clientFlatten = clientFlatten; + this.polymorphicDiscriminator = polymorphicDiscriminator; + this.isXmlText = isXmlText; + this.xmlPrefix = xmlPrefix; + this.requiredForCreate = requiredForCreate; + } + + public final String getName() { + return name; + } + + public final String getGetterName() { + return CodeNamer.getModelNamer().modelPropertyGetterName(this); + } + + public final String getSetterName() { + return CodeNamer.getModelNamer().modelPropertySetterName(this); + } + + public final String getDescription() { + return description; + } + + public final String getAnnotationArguments() { + return annotationArguments; + } + + public final boolean isXmlAttribute() { + return isXmlAttribute; + } + + public final String getXmlName() { + return xmlName; + } + + public String getXmlNamespace() { + return xmlNamespace; + } + + public final String getSerializedName() { + return serializedName; + } + + public final boolean isXmlWrapper() { + return isXmlWrapper; + } + + public final String getXmlListElementName() { + return xmlListElementName; + } + + public final String getXmlListElementNamespace() { + return xmlListElementNamespace; + } + + public final String getXmlListElementPrefix() { + return xmlListElementPrefix; + } + + public final IType getWireType() { + return wireType; + } + + public final IType getClientType() { + return clientType; + } + + public final boolean isConstant() { + return isConstant; + } + + public final String getDefaultValue() { + return defaultValue; + } + + public final boolean isReadOnly() { + return isReadOnly; + } + + public final boolean isReadOnlyForCreate() { + return isReadOnly || (this.getMutabilities() != null && !this.getMutabilities().contains(Mutability.CREATE)); + } + + public final boolean isReadOnlyForUpdate() { + return isReadOnly || (this.getMutabilities() != null && !this.getMutabilities().contains(Mutability.UPDATE)); + } + + public final String getHeaderCollectionPrefix() { + return headerCollectionPrefix; + } + + /** + * @return List of property mutability. + */ + public List getMutabilities() { + return mutabilities; + } + + /** + * @return whether the property need to be flatten. + */ + public final boolean getNeedsFlatten() { + return needsFlatten; + } + + /** + * @return whether the property is required to be flattened. + */ + public final boolean getClientFlatten() { + return clientFlatten; + } + + /** + * @return whether the property is a polymorphic discriminator. + */ + public final boolean isPolymorphicDiscriminator() { + return polymorphicDiscriminator; + } + + /** + * @return whether this property uses the value of an XML tag. + */ + public final boolean isXmlText() { + return isXmlText; + } + + /** + * Gets the XML prefix for the attribute or element. + * + * @return The XML prefix for this property. + */ + public final String getXmlPrefix() { + return xmlPrefix; + } + + public boolean isRequiredForCreate() { + return requiredForCreate == null ? this.isRequired() : requiredForCreate; + } + + /** + * Add this ServiceModelProperty's imports to the provided set of imports. + * @param imports The set of imports to add to. + */ + public final void addImportsTo(Set imports, boolean shouldGenerateXmlSerialization) { + JavaSettings settings = JavaSettings.getInstance(); + + if (getHeaderCollectionPrefix() != null && !getHeaderCollectionPrefix().isEmpty()) { + Annotation.HEADER_COLLECTION.addImportsTo(imports); + } + if (isAdditionalProperties) { + imports.add("com.fasterxml.jackson.annotation.JsonIgnore"); + imports.add("com.fasterxml.jackson.annotation.JsonAnySetter"); + imports.add("com.fasterxml.jackson.annotation.JsonAnyGetter"); + imports.add(LinkedHashMap.class.getName()); + } + + if (settings.getClientFlattenAnnotationTarget() == JavaSettings.ClientFlattenAnnotationTarget.FIELD && needsFlatten) { + addJsonFlattenAnnotationImport(imports); + } + + if (!isAdditionalProperties && getClientType() instanceof MapType) { + // required for "@JsonInclude(value = JsonInclude.Include.NON_NULL, content = JsonInclude.Include.ALWAYS)" + imports.add("com.fasterxml.jackson.annotation.JsonInclude"); + } + + if (getWireType() != null) { + getWireType().addImportsTo(imports, false); + } + getClientType().addImportsTo(imports, false); + + if (getClientType().equals(ArrayType.BYTE_ARRAY)) { + imports.add(ClassType.CORE_UTILS.getFullName()); + } + + if (shouldGenerateXmlSerialization) { + imports.add("com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement"); + if (isXmlWrapper()) { + imports.add("com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty"); + } + if (isXmlText()) { + imports.add("com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlText"); + } + } else { + imports.add("com.fasterxml.jackson.annotation.JsonProperty"); + } + } + + public boolean isRequired() { + return isRequired; + } + + public void setRequired(boolean required) { + isRequired = required; + } + + public boolean isAdditionalProperties() { + return isAdditionalProperties; + } + + protected void addJsonFlattenAnnotationImport(Set imports) { + imports.add("com.azure.core.annotation.JsonFlatten"); + } + + /** + * Creates a builder that is initialized with all the builder properties set to current values of this instance. + * @return A new builder instance initialized with properties values of this instance. + */ + public Builder newBuilder() { + return new Builder(this); + } + + public enum Mutability { + CREATE, UPDATE, READ + } + + public static class Builder { + private String name; + private String description; + private String annotationArguments; + private boolean isXmlAttribute; + private String xmlName; + private String serializedName; + private boolean isXmlWrapper; + private String xmlListElementName; + private String xmlListElementNamespace; + private String xmlListElementPrefix; + private IType wireType; + private IType clientType; + private boolean isConstant = false; + private String defaultValue; + private boolean isReadOnly; + private boolean isRequired; + private String headerCollectionPrefix; + private boolean isAdditionalProperties = false; + private String xmlNamespace; + private List mutabilities; + private boolean needsFlatten = false; + private boolean clientFlatten = false; + private boolean polymorphicDiscriminator = false; + private boolean isXmlText = false; + private String xmlPrefix; + private Boolean requiredForCreate; + + /** + * Sets the name of this property. + * @param name the name of this property + * @return the Builder itself + */ + public Builder name(String name) { + this.name = name; + return this; + } + + /** + * Sets the description of this property. + * @param description the description of this property + * @return the Builder itself + */ + public Builder description(String description) { + this.description = description; + return this; + } + + /** + * Sets the arguments that go into this property's JsonProperty annotation. + * @param annotationArguments the arguments that go into this property's JsonProperty annotation + * @return the Builder itself + */ + public Builder annotationArguments(String annotationArguments) { + this.annotationArguments = annotationArguments; + return this; + } + + /** + * Sets whether this property is an attribute when serialized to XML. + * @param isXmlAttribute whether this property is an attribute when serialized to XML + * @return the Builder itself + */ + public Builder xmlAttribute(boolean isXmlAttribute) { + this.isXmlAttribute = isXmlAttribute; + return this; + } + + /** + * Sets this property's name when serialized to XML. + * @param xmlName this property's name when serialized to XML + * @return the Builder itself + */ + public Builder xmlName(String xmlName) { + this.xmlName = xmlName; + return this; + } + + /** + * Sets this property's namespace when serialized to XML. + * @param xmlNamespace this property's namespace when serialized to XML + * @return the Builder itself + */ + public Builder xmlNamespace(String xmlNamespace) { + this.xmlNamespace = xmlNamespace; + return this; + } + + /** + * Sets this property's name when it is serialized. + * @param serializedName this property's name when it is serialized + * @return the Builder itself + */ + public Builder serializedName(String serializedName) { + this.serializedName = serializedName; + return this; + } + + /** + * Sets whether this property is a container. + * @param isXmlWrapper whether this property is a container + * @return the Builder itself + */ + public Builder xmlWrapper(boolean isXmlWrapper) { + this.isXmlWrapper = isXmlWrapper; + return this; + } + + /** + * Sets the name of each list element tag within an XML list property. + * @param xmlListElementName the name of each list element tag within an XML list property + * @return the Builder itself + */ + public Builder xmlListElementName(String xmlListElementName) { + this.xmlListElementName = xmlListElementName; + return this; + } + + /** + * Sets the namespace of each list element tag within an XML list property. + * @param xmlListElementNamespace the namespace of each list element tag within an XML list property + * @return the Builder itself + */ + public Builder xmlListElementNamespace(String xmlListElementNamespace) { + this.xmlListElementNamespace = xmlListElementNamespace; + return this; + } + + /** + * Sets the prefix of each list element tag within an XML list property. + * @param xmlListElementPrefix the prefix of each list element tag within an XML list property + * @return the Builder itself + */ + public Builder xmlListElementPrefix(String xmlListElementPrefix) { + this.xmlListElementPrefix = xmlListElementPrefix; + return this; + } + + /** + * Sets the type of this property as it is transmitted across the network (across the wire). + * @param wireType the type of this property as it is transmitted across the network (across the wire) + * @return the Builder itself + */ + public Builder wireType(IType wireType) { + this.wireType = wireType; + return this; + } + + /** + * Sets the type of this property as it will be exposed via the client. + * @param clientType the type of this property as it will be exposed via the client + * @return the Builder itself + */ + public Builder clientType(IType clientType) { + this.clientType = clientType; + return this; + } + + /** + * Sets whether this property has a constant value. + * @param isConstant whether this property has a constant value + * @return the Builder itself + */ + public Builder constant(boolean isConstant) { + this.isConstant = isConstant; + return this; + } + + /** + * Sets the default value expression of this property. + * @param defaultValue the default value expression of this property + * @return the Builder itself + */ + public Builder defaultValue(String defaultValue) { + this.defaultValue = defaultValue; + return this; + } + + /** + * Sets whether this property's value can be changed by the client library. + * @param isReadOnly whether this property's value can be changed by the client library + * @return the Builder itself + */ + public Builder readOnly(boolean isReadOnly) { + this.isReadOnly = isReadOnly; + return this; + } + + /** + * Sets whether this property is required. + * @param isRequired whether this property is required + * @return the Builder itself + */ + public Builder required(boolean isRequired) { + this.isRequired = isRequired; + return this; + } + + /** + * Sets whether this property is required when create the resource. + * @param requiredForCreate whether this property is required when create the resource. + * @return the Builder itself + */ + public Builder requiredForCreate(boolean requiredForCreate) { + this.requiredForCreate = requiredForCreate; + return this; + } + + /** + * Sets the prefix of the headers that make up this property's values. + * @param headerCollectionPrefix the prefix of the headers that make up this property's values + * @return the Builder itself + */ + public Builder headerCollectionPrefix(String headerCollectionPrefix) { + this.headerCollectionPrefix = headerCollectionPrefix; + return this; + } + + /** + * Sets whether this property contain the additional properties. + * @param isAdditionalProperties whether this property contain the additional properties + * @return the Builder itself + */ + public Builder additionalProperties(boolean isAdditionalProperties) { + this.isAdditionalProperties = isAdditionalProperties; + return this; + } + + /** + * Sets list of property mutability. + * @param mutabilities list of mutability. + * @return the Builder itself + */ + public Builder mutabilities(List mutabilities) { + this.mutabilities = mutabilities; + return this; + } + + /** + * Sets whether this property needs serialization flattening. + * + * Code will add @JsonFlatten annotation, and escape the @JsonValue. + * + * @param needsFlatten whether this property needs serialization flattening + * @return the Builder itself + */ + public Builder needsFlatten(boolean needsFlatten) { + this.needsFlatten = needsFlatten; + return this; + } + + /** + * Sets whether this property is required to be flattened. + * + * Code will make the accessors to the property private to hide them from user. + * + * @param clientFlatten whether this property is required to be flattened + * @return the Builder itself + */ + public Builder clientFlatten(boolean clientFlatten) { + this.clientFlatten = clientFlatten; + return this; + } + + /** + * Sets whether this property is a polymorphic discriminator. + * + * @param polymorphicDiscriminator Whether this property is a polymorphic discriminator. + * @return the Builder itself + */ + public Builder polymorphicDiscriminator(boolean polymorphicDiscriminator) { + this.polymorphicDiscriminator = polymorphicDiscriminator; + return this; + } + + /** + * Sets whether this property uses the value of an XML tag. + * + * @param isXmlText Whether this property uses the value of an XML tag. + * @return the Builder itself + */ + public Builder xmlText(boolean isXmlText) { + this.isXmlText = isXmlText; + return this; + } + + /** + * Sets the XML prefix for the property being constructed. + * + * @param xmlPrefix The XML prefix for the attribute or element the property represents. + * @return the Builder itself + */ + public Builder xmlPrefix(String xmlPrefix) { + this.xmlPrefix = xmlPrefix; + return this; + } + + /** + * Creates a new instance of Builder. + */ + public Builder() { + } + + private Builder(ClientModelProperty property) { + this.name = property.getName(); + this.description = property.getDescription(); + this.annotationArguments = property.getAnnotationArguments(); + this.isXmlAttribute = property.isXmlAttribute(); + this.xmlName = property.getXmlName(); + this.xmlNamespace = property.getXmlNamespace(); + this.serializedName = property.getSerializedName(); + this.isXmlWrapper = property.isXmlWrapper(); + this.xmlListElementName = property.getXmlListElementName(); + this.xmlListElementNamespace = property.getXmlListElementNamespace(); + this.xmlListElementPrefix = property.getXmlListElementPrefix(); + this.wireType = property.getWireType(); + this.clientType = property.getClientType(); + this.isConstant = property.isConstant(); + this.defaultValue = property.getDefaultValue(); + this.isReadOnly = property.isReadOnly(); + this.mutabilities = property.getMutabilities(); + this.isRequired = property.isRequired(); + this.headerCollectionPrefix = property.getHeaderCollectionPrefix(); + this.isAdditionalProperties = property.isAdditionalProperties(); + this.needsFlatten = property.getNeedsFlatten(); + this.clientFlatten = property.getClientFlatten(); + this.polymorphicDiscriminator = property.isPolymorphicDiscriminator(); + this.isXmlText = property.isXmlText(); + this.xmlPrefix = property.getXmlPrefix(); + this.requiredForCreate = property.requiredForCreate; + } + + public ClientModelProperty build() { + return new ClientModelProperty(name, description, annotationArguments, isXmlAttribute, xmlName, + xmlNamespace, serializedName, isXmlWrapper, xmlListElementName, xmlListElementNamespace, + xmlListElementPrefix, wireType, clientType, isConstant, defaultValue, isReadOnly, mutabilities, + isRequired, headerCollectionPrefix, isAdditionalProperties, needsFlatten, clientFlatten, + polymorphicDiscriminator, isXmlText, xmlPrefix, requiredForCreate); + } + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/ClientModelPropertyAccess.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/ClientModelPropertyAccess.java new file mode 100644 index 0000000000..8a94684165 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/ClientModelPropertyAccess.java @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.model.clientmodel; + +import java.util.Set; + +/** + * Access to the client model property. + */ +public interface ClientModelPropertyAccess { + + String getName(); + + String getDescription(); + + String getGetterName(); + + String getSetterName(); + + IType getClientType(); + + IType getWireType(); + + boolean isReadOnly(); + + boolean isReadOnlyForCreate(); + + boolean isReadOnlyForUpdate(); + + boolean isRequired(); + boolean isRequiredForCreate(); + + boolean isConstant(); + + void addImportsTo(Set imports, boolean shouldGenerateXmlSerialization); +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/ClientModelPropertyReference.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/ClientModelPropertyReference.java new file mode 100644 index 0000000000..dc17d137ca --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/ClientModelPropertyReference.java @@ -0,0 +1,162 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.model.clientmodel; + +import com.microsoft.typespec.http.client.generator.core.util.CodeNamer; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +public class ClientModelPropertyReference implements ClientModelPropertyAccess { + + /* + Usage of the ClientModelPropertyReference + 1. Reference to property (or propertyReference) from superclass, which has non-null referenceProperty, i.e., super.referenceProperty + 2. Reference to property from a flattened client model (targetModel), which has non-null referenceProperty and targetProperty, i.e., targetProperty.referenceProperty + + This could be recursive, as + */ + + private final String name; + private final ClientModelPropertyAccess referenceProperty; + private final ClientModel targetModel; + private final ClientModelProperty targetProperty; + + private ClientModelPropertyReference(ClientModelProperty targetProperty, ClientModel targetModel, + ClientModelPropertyAccess referenceProperty, String name) { + this.targetProperty = targetProperty; + this.targetModel = targetModel; + this.referenceProperty = referenceProperty; + this.name = name; + } + + public static ClientModelPropertyReference ofParentProperty(ClientModelProperty property) { + return new ClientModelPropertyReference(null, null, property, null); + } + + public static ClientModelPropertyReference ofParentProperty(ClientModelPropertyReference referenceProperty) { + if (!referenceProperty.isFromFlattenedProperty()) { + throw new IllegalArgumentException("Property is not from flattened model: " + referenceProperty.getName()); + } + return new ClientModelPropertyReference(null, null, referenceProperty, null); + } + + public static ClientModelPropertyReference ofFlattenProperty(ClientModelProperty targetProperty, + ClientModel targetModel, ClientModelProperty property, String name) { + return new ClientModelPropertyReference(targetProperty, targetModel, property, name); + } + + public static ClientModelPropertyReference ofFlattenProperty(ClientModelProperty targetProperty, + ClientModel targetModel, ClientModelPropertyReference referenceProperty, String name) { + if (!referenceProperty.isFromFlattenedProperty()) { + throw new IllegalArgumentException("Property is not from flattened model: " + referenceProperty.getName()); + } + return new ClientModelPropertyReference(targetProperty, targetModel, referenceProperty, name); + } + + public boolean isFromFlattenedProperty() { + return this.targetProperty != null; + } + + public boolean isFromParentModel() { + return this.targetProperty == null; + } + + public ClientModelPropertyAccess getReferenceProperty() { + return referenceProperty; + } + + public ClientModelProperty getTargetProperty() { + return targetProperty; + } + + public List getAllProperties() { + List properties = new ArrayList<>(); + if (targetProperty != null) { + properties.add(targetProperty); + } + if (referenceProperty instanceof ClientModelProperty) { + properties.add((ClientModelProperty) referenceProperty); + } else if (referenceProperty instanceof ClientModelPropertyReference) { + properties.addAll(((ClientModelPropertyReference) referenceProperty).getAllProperties()); + } else { + throw new IllegalStateException( + "Unknown subclass of ClientModelPropertyAccess: " + referenceProperty.getClass().getName()); + } + return properties; + } + + public IType getTargetModelType() { + return targetModel.getType(); + } + + @Override + public String getName() { + return this.name == null ? this.referenceProperty.getName() : this.name; + } + + @Override + public String getDescription() { + return referenceProperty.getDescription(); + } + + @Override + public String getGetterName() { + return CodeNamer.getModelNamer() + .modelPropertyGetterName(this.referenceProperty.getClientType(), this.getName()); + } + + @Override + public String getSetterName() { + return CodeNamer.getModelNamer().modelPropertySetterName(this.getName()); + } + + @Override + public IType getClientType() { + return referenceProperty.getClientType(); + } + + @Override + public IType getWireType() { + return referenceProperty.getWireType(); + } + + @Override + public boolean isReadOnly() { + return (targetProperty != null && targetProperty.isReadOnly()) || referenceProperty.isReadOnly(); + } + + @Override + public boolean isReadOnlyForCreate() { + return (targetProperty != null && targetProperty.isReadOnly()) || referenceProperty.isReadOnlyForCreate(); + } + + @Override + public boolean isReadOnlyForUpdate() { + return (targetProperty != null && targetProperty.isReadOnly()) || referenceProperty.isReadOnlyForUpdate(); + } + + @Override + public boolean isRequired() { + return (targetProperty == null || targetProperty.isRequired()) && referenceProperty.isRequired(); + } + + @Override + public boolean isRequiredForCreate() { + return (targetProperty == null || targetProperty.isRequiredForCreate()) + && referenceProperty.isRequiredForCreate(); + } + + @Override + public boolean isConstant() { + // could we have the whole flattened model as constant? + return referenceProperty.isConstant(); + } + + @Override + public void addImportsTo(Set imports, boolean shouldGenerateXmlSerialization) { + referenceProperty.addImportsTo(imports, shouldGenerateXmlSerialization); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/ClientModels.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/ClientModels.java new file mode 100644 index 0000000000..b84a999387 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/ClientModels.java @@ -0,0 +1,67 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.model.clientmodel; + +import com.microsoft.typespec.http.client.generator.core.util.ClientModelUtil; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * The collection of all client models stored for inheritance lookup. + */ +public class ClientModels { + private static final ClientModels INSTANCE = new ClientModels(); + private final Map nameMap = new HashMap<>(); +// private final Map> derivedTypesMap = new HashMap>(); + private ClientModels() { + } + + public final void clear() { + nameMap.clear(); + } + + public static ClientModels getInstance() { + return INSTANCE; + } + + /** + * Gets the ClientModel instance from the name of the model. + *

+ * Use {@link ClientModelUtil#getClientModel(String)} unless ModelMapper or ClientModelUtil. + * + * @param modelName the name of the model. + * @return the ClientModel instance. + */ + public final ClientModel getModel(String modelName) { + return nameMap.get(modelName); + } + + public final void addModel(ClientModel model) { + nameMap.put(model.getName(), model); + +// String parentModel = model.getParentModelName(); +// if (parentModel != null) { +// ArrayList derivedTypesList = getDerivedTypeList(parentModel); +// derivedTypesList.add(model); +// } + } + +// public final List getDerivedTypes(String parentModelName) { +// return getDerivedTypeList(parentModelName); +// } + + public final List getModels() { + return new ArrayList<>(nameMap.values()); + } + +// private ArrayList getDerivedTypeList(String parentModelName) { +// if (!derivedTypesMap.containsKey(parentModelName)) { +// derivedTypesMap.put(parentModelName, new ArrayList()); +// } +// return derivedTypesMap.get(parentModelName); +// } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/ClientResponse.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/ClientResponse.java new file mode 100644 index 0000000000..0e0e3b7989 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/ClientResponse.java @@ -0,0 +1,94 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.model.clientmodel; + +/** + * The response that is returned by a ClientMethod. + */ +public final class ClientResponse { + private String name; + private String packageName; + private String description; + private IType headersType; + private IType bodyType; + private String crossLanguageDefinitionId; + + private ClientResponse(String name, String packageKeyword, String description, IType headersType, IType bodyType, String crossLanguageDefinitionId) { + this.name = name; + packageName = packageKeyword; + this.description = description; + this.headersType = headersType; + this.bodyType = bodyType; + this.crossLanguageDefinitionId = crossLanguageDefinitionId; + } + + public String getName() { + return name; + } + + public String getPackage() { + return packageName; + } + + public String getDescription() { + return description; + } + + public IType getHeadersType() { + return headersType; + } + + public IType getBodyType() { + return bodyType; + } + + public String getCrossLanguageDefinitionId() { + return crossLanguageDefinitionId; + } + + public static class Builder { + private String name; + private String packageName; + private String description; + private IType headersType; + private IType bodyType; + + private String crossLanguageDefinitionId; + + + public Builder name(String name) { + this.name = name; + return this; + } + + public Builder packageName(String packageName) { + this.packageName = packageName; + return this; + } + + public Builder description(String description) { + this.description = description; + return this; + } + + public Builder headersType(IType headersType) { + this.headersType = headersType; + return this; + } + + public Builder bodyType(IType bodyType) { + this.bodyType = bodyType; + return this; + } + + public Builder crossLanguageDefinitionId(String crossLanguageDefinitionId) { + this.crossLanguageDefinitionId = crossLanguageDefinitionId; + return this; + } + + public ClientResponse build() { + return new ClientResponse(name, packageName, description, headersType, bodyType, crossLanguageDefinitionId); + } + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/Constructor.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/Constructor.java new file mode 100644 index 0000000000..b644e8362e --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/Constructor.java @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.model.clientmodel; + +import java.util.List; +import java.util.Set; + +/** + * The constructor in a ServiceClient. + */ +public class Constructor { + /** + * The parameters of this constructor. + */ + private List parameters; + + public Constructor(List parameters) { + this.parameters = parameters; + } + + public final List getParameters() { + return parameters; + } + + public final void addImportsTo(Set imports, boolean includeImplementationImports) { + for (ClientMethodParameter parameter : getParameters()) { + parameter.addImportsTo(imports, includeImplementationImports); + } + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/ConvenienceMethod.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/ConvenienceMethod.java new file mode 100644 index 0000000000..bc42e24603 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/ConvenienceMethod.java @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.model.clientmodel; + +import java.util.List; +import java.util.Objects; + +public class ConvenienceMethod { + + private final ClientMethod protocolMethod; + private final List convenienceMethods; + + public ConvenienceMethod(ClientMethod clientMethod, List convenienceMethods) { + this.protocolMethod = clientMethod; + this.convenienceMethods = convenienceMethods; + } + + public ClientMethod getProtocolMethod() { + return protocolMethod; + } + + public List getConvenienceMethods() { + return convenienceMethods; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ConvenienceMethod that = (ConvenienceMethod) o; + return protocolMethod.equals(that.protocolMethod); + } + + @Override + public int hashCode() { + return Objects.hash(protocolMethod); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/EnumType.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/EnumType.java new file mode 100644 index 0000000000..010187d5f0 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/EnumType.java @@ -0,0 +1,328 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.model.clientmodel; + +import com.microsoft.typespec.http.client.generator.core.util.CodeNamer; + +import java.util.List; +import java.util.Set; + +/** + * The details of an enumerated type that is used by a service. + */ +public class EnumType implements IType { + /** + * The name of the new Enum. + */ + private final String name; + private final String description; + /** + * The package that this enumeration belongs to. + */ + private final String packageName; + /** + * Whether this will be an ExpandableStringEnum type. + */ + private final boolean expandable; + /** + * The values of the Enum. + */ + private final List values; + + private final IType elementType; + + private final ImplementationDetails implementationDetails; + + private String crossLanguageDefinitionId; + + /** + * Create a new Enum with the provided properties. + * @param name The name of the new Enum. + * @param description The description of the Enum. + * @param expandable Whether this will be an ExpandableStringEnum type. + * @param values The values of the Enum. + */ + private EnumType(String packageKeyword, String name, String description, + boolean expandable, List values, + IType elementType, + ImplementationDetails implementationDetails, + String crossLanguageDefinitionId) { + this.name = name; + this.packageName = packageKeyword; + this.description = description; + this.expandable = expandable; + this.values = values; + this.elementType = elementType; + this.implementationDetails = implementationDetails; + this.crossLanguageDefinitionId = crossLanguageDefinitionId; + } + + public String getCrossLanguageDefinitionId() { + return crossLanguageDefinitionId; + } + + public final String getName() { + return name; + } + + public final String getPackage() { + return packageName; + } + + public String getDescription() { + return description; + } + + public final boolean getExpandable() { + return expandable; + } + + public final List getValues() { + return values; + } + + public final IType getElementType() { + return elementType; + } + + public final void addImportsTo(Set imports, boolean includeImplementationImports) { + imports.add(getPackage() + "." + getName()); + + // EnumTypes may result in Collectors being used, if Collectors isn't used the unused import will be removed. + imports.add("java.util.stream.Collectors"); + } + + public final boolean isNullable() { + return true; + } + + public final IType asNullable() { + return this; + } + + public final boolean contains(IType type) { + return this == type; + } + + public final String defaultValueExpression(String sourceExpression) { + if (sourceExpression == null) { + return null; + } + if (this.getExpandable()) { + for (ClientEnumValue enumValue : this.getValues()) { + if (sourceExpression.equals(enumValue.getValue())) { + return getName() + "." + enumValue.getName(); + } + } + return String.format("%1$s.from%2$s(%3$s)", getName(), + CodeNamer.toPascalCase(this.getElementType().toString()), + this.getElementType().defaultValueExpression(sourceExpression)); + } else { + for (ClientEnumValue enumValue : this.getValues()) { + if (sourceExpression.equals(enumValue.getValue())) { + return getName() + "." + enumValue.getName(); + } + } + return null; + } + } + + /** + * Gets the method name used to convert JSON to the enum type. + * + * @return The method name used to convert JSON to the enum type. + */ + public final String getFromMethodName() { + return "from" + CodeNamer.toPascalCase(elementType.getClientType().toString()); + } + + /** + * Gets the method name used to convert the enum type to JSON. + * + * @return The method name used to convert the enum type to JSON. + */ + public final String getToMethodName() { + return "to" + CodeNamer.toPascalCase(elementType.getClientType().toString()); + } + + @Override + public String defaultValueExpression() { + return "null"; + } + + public final IType getClientType() { + return this; + } + + public final String convertToClientType(String expression) { + return expression; + } + + public final String convertFromClientType(String expression) { + return expression; + } + + public final String validate(String expression) { + return null; + } + + public ImplementationDetails getImplementationDetails() { + return implementationDetails; + } + + @Override + public String jsonToken() { + return null; + } + + @Override + public String jsonDeserializationMethod(String jsonReaderName) { + return name + "." + getFromMethodName() + "(" + elementType.jsonDeserializationMethod(jsonReaderName) + ")"; + } + + @Override + public String jsonSerializationMethodCall(String jsonWriterName, String fieldName, String valueGetter, + boolean jsonMergePatch) { + // When JSON merge patch is being used the valueGetter will already be null checked as JSON merge patch needs to + // explicitly handle null values. + String actualValueGetter = jsonMergePatch + ? valueGetter + "." + getToMethodName() + "()" + : valueGetter + " == null ? null : " + valueGetter + "." + getToMethodName() + "()"; + + return elementType.asNullable().jsonSerializationMethodCall(jsonWriterName, fieldName, actualValueGetter, + jsonMergePatch); + } + + @Override + public String xmlDeserializationMethod(String xmlReaderName, String attributeName, String attributeNamespace, + boolean namespaceIsConstant) { + String elementTypeXmlDeserialization = elementType.xmlDeserializationMethod(xmlReaderName, attributeName, + attributeNamespace, namespaceIsConstant); + return name + "." + getFromMethodName() + "(" + elementTypeXmlDeserialization + ")"; + } + + @Override + public String xmlSerializationMethodCall(String xmlWriterName, String attributeOrElementName, String namespaceUri, + String valueGetter, boolean isAttribute, boolean nameIsVariable, boolean namespaceIsConstant) { + String actualValueGetter = valueGetter + " == null ? null : " + valueGetter + "." + getToMethodName() + "()"; + return elementType.xmlSerializationMethodCall(xmlWriterName, attributeOrElementName, namespaceUri, + actualValueGetter, isAttribute, nameIsVariable, namespaceIsConstant); + } + + @Override + public boolean isUsedInXml() { + return false; + } + + @Override + public String toString() { + return getName(); + } + + public static class Builder { + private String name; + private String description; + private String packageName; + private boolean expandable; + private List values; + private IType elementType = ClassType.STRING; + + private ImplementationDetails implementationDetails; + + private String crossLanguageDefinitionId; + + /** + * Sets the name of the Enum. + * @param name the name of the Enum + * @return the Builder + */ + public Builder name(String name) { + this.name = name; + return this; + } + + /** + * Sets the package name of the Enum. + * @param packageName the package name of the Enum + * @return the Builder + */ + public Builder packageName(String packageName) { + this.packageName = packageName; + return this; + } + + /** + * Sets the description of the Enum. + * @param description the description of the Enum + * @return the Builder + */ + public Builder description(String description) { + this.description = description; + return this; + } + + /** + * Sets whether the Enum is expandable. + * @param expandable whether the Enum is expandable + * @return the Builder + */ + public Builder expandable(boolean expandable) { + this.expandable = expandable; + return this; + } + + /** + * Sets the values of the Enum. + * @param values the values of the Enum + * @return the Builder + */ + public Builder values(List values) { + this.values = values; + return this; + } + + /** + * Sets the type of elements of the Enum. + * @param elementType the type of elements of the Enum + * @return the Builder + */ + public Builder elementType(IType elementType) { + if (elementType != null) { + this.elementType = elementType; + } + return this; + } + + /** + * Sets the implementation details for the model. + * @param implementationDetails the implementation details. + * @return the Builder itself + */ + public Builder implementationDetails(ImplementationDetails implementationDetails) { + this.implementationDetails = implementationDetails; + return this; + } + + public Builder crossLanguageDefinitionId(String crossLanguageDefinitionId) { + this.crossLanguageDefinitionId = crossLanguageDefinitionId; + return this; + } + + /** + * @return an immutable EnumType instance with the configurations on this builder. + */ + public EnumType build() { + return new EnumType( + packageName, + name, + description, + expandable, + values, + elementType, + implementationDetails, + crossLanguageDefinitionId + ); + } + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/ExampleLiveTestStep.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/ExampleLiveTestStep.java new file mode 100644 index 0000000000..f9a7643c10 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/ExampleLiveTestStep.java @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.model.clientmodel; + +public class ExampleLiveTestStep extends LiveTestStep{ + + private String operationId; + private ProxyMethodExample example; + + public static ExampleLiveTestStep.Builder newBuilder(){ + return new ExampleLiveTestStep.Builder(); + } + + public static class Builder extends LiveTestStep.Builder{ + + private Builder(){ + super(new ExampleLiveTestStep()); + } + + public Builder operationId(String operationId) { + step.operationId = operationId; + return this; + } + + public Builder example(ProxyMethodExample example) { + step.example = example; + return this; + } + + @Override + protected Builder getThis() { + return this; + } + } + + public String getOperationId() { + return operationId; + } + + public ProxyMethodExample getExample() { + return example; + } + + public String getDescription() { + return description; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/ExternalDocumentation.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/ExternalDocumentation.java new file mode 100644 index 0000000000..8e45c05a5b --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/ExternalDocumentation.java @@ -0,0 +1,76 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.model.clientmodel; + +import java.net.MalformedURLException; +import java.net.URL; + +/** + * External documentation, can be used to link to rest API documentation + */ +public class ExternalDocumentation { + private String description; + private String url; + + protected ExternalDocumentation(String description, String url) { + this.description = description; + this.url = url; + } + + public String getDescription() { + return description; + } + + public String getUrl() { + return url; + } + + + @Override + public String toString() { + return "ExternalDocumentation{" + + "description='" + description + '\'' + + ", url='" + url + '\'' + + '}'; + } + + public static class Builder { + + private String description; + + private String url; + + /** + * Sets the description of this ExternalDocumentation. + * @param description the description of this ExternalDocumentation + * @return the Builder itself + */ + public ExternalDocumentation.Builder description(String description) { + this.description = description; + return this; + } + + /** + * Sets the url of this ExternalDocumentation. + * @param url of this ExternalDocumentation + * @return the Builder itself + */ + public ExternalDocumentation.Builder url(String url) { + try { + new URL(url); + this.url = url; + } catch (MalformedURLException e) { + throw new RuntimeException(e); + } + return this; + } + + public ExternalDocumentation build() { + return new ExternalDocumentation( + description, + url); + } + + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/ExternalPackage.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/ExternalPackage.java new file mode 100644 index 0000000000..4ea8861a15 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/ExternalPackage.java @@ -0,0 +1,90 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.model.clientmodel; + +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; + +public class ExternalPackage { + + public static final String CLIENTCORE_PACKAGE_NAME = "io.clientcore.core"; + public static final String CLIENTCORE_JSON_PACKAGE_NAME = "io.clientcore.core.json"; + + public static final String AZURE_CORE_PACKAGE_NAME = "com.azure.core"; + public static final String AZURE_JSON_PACKAGE_NAME = "com.azure.json"; + + public static final ExternalPackage CORE = new Builder().packageName(CLIENTCORE_PACKAGE_NAME) + .groupId("io.clientcore") + .artifactId("core") + .build(); + + public static final ExternalPackage JSON = new Builder().packageName(CLIENTCORE_JSON_PACKAGE_NAME) + .groupId("io.clientcore") + .artifactId("core-json") + .build(); + + private final String packageName; + private final String groupId; + private final String artifactId; + + private ExternalPackage(String packageName, String groupId, String artifactId) { + this.packageName = packageName; + this.groupId = groupId; + this.artifactId = artifactId; + } + + public String getPackageName() { + return packageName; + } + + public String getGroupId() { + return groupId; + } + + public String getArtifactId() { + return artifactId; + } + + public static final class Builder { + private String packageName; + private String groupId; + private String artifactId; + + public Builder() { + } + + public Builder packageName(String packageName) { + this.packageName = packageName; + return this; + } + + public Builder groupId(String groupId) { + this.groupId = groupId; + return this; + } + + public Builder artifactId(String artifactId) { + this.artifactId = artifactId; + return this; + } + + public ExternalPackage build() { + if (JavaSettings.getInstance().isBranded()) { + switch (packageName) { + case CLIENTCORE_PACKAGE_NAME: + packageName = AZURE_CORE_PACKAGE_NAME; + groupId = "com.azure"; + artifactId = "azure-core"; + break; + + case CLIENTCORE_JSON_PACKAGE_NAME: + packageName = AZURE_JSON_PACKAGE_NAME; + groupId = "com.azure"; + artifactId = "azure-json"; + break; + } + } + return new ExternalPackage(packageName, groupId, artifactId); + } + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/GenericType.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/GenericType.java new file mode 100644 index 0000000000..09e70f48f4 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/GenericType.java @@ -0,0 +1,331 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.model.clientmodel; + +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; + +import java.util.Arrays; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * A generic type that is used by the client. + */ +public class GenericType implements IType { + public static final GenericType FLUX_BYTE_BUFFER = Flux(ClassType.BYTE_BUFFER); + /** + * The main non-generic type of this generic type. + */ + private final String name; + /** + * The package that this type belongs to. + */ + private final String packageName; + /** + * The type arguments of this generic type. + */ + private final IType[] typeArguments; + + private final String jsonToken; + + /** + * Create a new GenericType from the provided properties. + * @param name The main non-generic type of this generic type. + * @param typeArguments The type arguments of this generic type. + */ + public GenericType(String packageKeyword, String name, IType... typeArguments) { + this(packageKeyword, name, null, typeArguments); + } + + public GenericType(String packageKeyword, String name, String jsonToken, IType... typeArguments) { + if (!JavaSettings.getInstance().isBranded()) { + if (Objects.equals(packageKeyword + "." + name, com.azure.core.http.rest.Response.class.getName())) { + packageKeyword = "io.clientcore.core.http"; + } else { + packageKeyword = packageKeyword + .replace(ExternalPackage.AZURE_CORE_PACKAGE_NAME, ExternalPackage.CLIENTCORE_PACKAGE_NAME); + } + } + + this.name = name; + this.packageName = packageKeyword; + this.typeArguments = typeArguments; + this.jsonToken = jsonToken; + } + + public static GenericType Flux(IType typeArgument) { + return new GenericType("reactor.core.publisher", "Flux", typeArgument); + } + + public static GenericType Mono(IType typeArgument) { + return new GenericType("reactor.core.publisher", "Mono", typeArgument); + } + + public static GenericType OperationStatus(IType typeArgument) { + return new GenericType("com.microsoft.azure.v3", "OperationStatus", typeArgument); + } + + public static GenericType Page(IType elementType) { + return new GenericType("com.microsoft.azure.v3", "Page", elementType); + } + + public static GenericType PagedList(IType elementType) { + return new GenericType("com.microsoft.azure.v3", "PagedList", elementType); + } + + public static GenericType Response(IType bodyType) { + return new GenericType(ClassType.RESPONSE.getPackage(), ClassType.RESPONSE.getName(), bodyType); + } + + public static GenericType RestResponse(IType headersType, IType bodyType) { + return new GenericType("com.azure.core.http.rest", "ResponseBase", headersType, bodyType); + } + + public static GenericType PagedResponse(IType bodyType) { + return new GenericType("com.azure.core.http.rest", "PagedResponse", bodyType); + } + + public static GenericType PagedFlux(IType bodyType) { + return new GenericType("com.azure.core.http.rest", "PagedFlux", bodyType); + } + + public static GenericType PagedIterable(IType bodyType) { + return new GenericType("com.azure.core.http.rest", "PagedIterable", bodyType); + } + + public static GenericType Function(IType inputType, IType outputType) { + return new GenericType("java.util", "Function", inputType, outputType); + } + + public static GenericType PollerFlux(IType pollResultType, IType finalResultType) { + return new GenericType("com.azure.core.util.polling", "PollerFlux", pollResultType, finalResultType); + } + + public static GenericType SyncPoller(IType pollResultType, IType finalResultType) { + return new GenericType("com.azure.core.util.polling", "SyncPoller", pollResultType, finalResultType); + } + + public static GenericType PollResult(IType pollResultType) { + return new GenericType("com.azure.core.management.polling", "PollResult", pollResultType); + } + + public static GenericType AndroidResponse(IType typeArgument) { + return new GenericType("com.azure.android.core.rest", "Response", typeArgument); + } + + public static GenericType AndroidPagedResponse(IType typeArgument) { + return new GenericType("com.azure.android.core.rest", "PagedResponse", typeArgument); + } + + public static GenericType AndroidCallback(IType typeArgument) { + return new GenericType("com.azure.android.core.rest", "Callback", typeArgument); + } + + public static GenericType AndroidCompletableFuture(IType typeArgument) { + return new GenericType("java9.util.concurrent", "CompletableFuture", typeArgument); + } + + public final String getName() { + return name; + } + + public final String getPackage() { + return packageName; + } + + public final IType[] getTypeArguments() { + return typeArguments; + } + + @Override + public String toString() { + return String.format("%1$s<%2$s>", getName(), Arrays.stream(getTypeArguments()).map(typeArgument -> typeArgument.asNullable().toString()).collect(Collectors.joining(", "))); + } + + /** + * Creates a String based on the generic type that can be used as a Java property name. + *

+ * For example {@code Map} would become {@code MapStringObject}. + * + * @return A String representation of the generic type that can be used as a Java property name. + */ + public String toJavaPropertyString() { + StringBuilder javaPropertyString = new StringBuilder(getName()); + + for (IType typeArgument : typeArguments) { + if (typeArgument instanceof GenericType) { + javaPropertyString.append(((GenericType) typeArgument).toJavaPropertyString()); + } else { + javaPropertyString.append(typeArgument.asNullable()); + } + } + + return javaPropertyString.toString(); + } + + @Override + public boolean equals(Object rhs) { + boolean tempVar = rhs instanceof GenericType; + GenericType genericTypeRhs = tempVar ? (GenericType) rhs : null; + return tempVar && getPackage().equals(genericTypeRhs.packageName) && getName().equals(genericTypeRhs.name) && Arrays.equals(getTypeArguments(), genericTypeRhs.typeArguments); + } + + @Override + public int hashCode() { + return getPackage().hashCode() + getName().hashCode() + Arrays.stream(getTypeArguments()).map(Object::hashCode).reduce(0, Integer::sum); + } + + public final IType asNullable() { + return this; + } + + public final boolean contains(IType type) { + return this == type || Arrays.stream(getTypeArguments()).anyMatch((IType typeArgument) -> typeArgument.contains(type)); + } + + public void addImportsTo(Set imports, boolean includeImplementationImports) { + imports.add(String.format("%1$s.%2$s", getPackage(), getName())); + for (IType typeArgument : getTypeArguments()) { + typeArgument.addImportsTo(imports, includeImplementationImports); + } + } + + public final String defaultValueExpression(String sourceExpression) { + return sourceExpression; + } + + @Override + public String defaultValueExpression() { + return "null"; + } + + public final IType getClientType() { + IType clientType = this; + + IType[] wireTypeArguments = getTypeArguments(); + IType[] clientTypeArguments = Arrays.stream(wireTypeArguments).map(IType::getClientType).toArray(IType[]::new); + + for (int i = 0; i < clientTypeArguments.length; ++i) { + if (clientTypeArguments[i] != wireTypeArguments[i]) { + if (this instanceof ListType) { + clientType = new ListType(clientTypeArguments[0]); + } else if (this instanceof IterableType) { + clientType = new IterableType(clientTypeArguments[0]); + } else if (this instanceof MapType) { + clientType = new MapType(clientTypeArguments[1]); + } else { + clientType = new GenericType(getPackage(), getName(), jsonToken(), clientTypeArguments); + } + break; + } + } + + return clientType; + } + + public final String convertToClientType(String expression) { + if (this == getClientType()) { + return expression; + } + + IType[] wireTypeArguments = getTypeArguments(); + IType[] clientTypeArguments = Arrays.stream(wireTypeArguments).map(IType::getClientType).toArray(IType[]::new); + + for (int i = 0; i < clientTypeArguments.length; ++i) { + if (clientTypeArguments[i] != wireTypeArguments[i]) { + if (this instanceof ListType) { + expression = String.format("%1$s.stream().map(el -> %2$s).collect(java.util.stream.Collectors.toList())", expression, wireTypeArguments[i].convertToClientType("el")); + } else if (this instanceof IterableType) { + expression = String.format("java.util.stream.StreamSupport.stream(%1$s.spliterator(), false).map" + + "(el -> %2$s).collect(java.util.stream.Collectors.toList())", + expression, wireTypeArguments[i].convertToClientType("el")); + } else if (this instanceof MapType) { + // Key is always String in Swagger 2 + expression = String.format("%1$s.entrySet().stream().collect(java.util.stream.Collectors.toMap(Map.Entry::getKey, el -> %2$s))", expression, wireTypeArguments[i].convertToClientType("el.getValue()")); + } else if (this.getPackage().equals("io.reactivex")) { + expression = String.format("%1$s.map(el => %2$s)", expression, wireTypeArguments[0].convertToClientType("el")); + } else { + throw new UnsupportedOperationException(String.format("Instance %1$s of generic type %2$s not supported for conversion to client type.", expression, toString())); + } + break; + } + } + + return expression; + } + + public final String convertFromClientType(String expression) { + if (this == getClientType()) { + return expression; + } + + IType[] wireTypeArguments = getTypeArguments(); + IType[] clientTypeArguments = Arrays.stream(wireTypeArguments).map(IType::getClientType).toArray(IType[]::new); + + for (int i = 0; i < clientTypeArguments.length; ++i) { + if (clientTypeArguments[i] != wireTypeArguments[i]) { + if (this instanceof ListType) { + expression = String.format("%1$s.stream().map(el -> %2$s).collect(java.util.stream.Collectors.toList())", expression, wireTypeArguments[i].convertFromClientType("el")); + } else if (this instanceof IterableType) { + expression = String.format("java.util.stream.StreamSupport.stream(%1$s.spliterator(), false).map" + + "(el -> %2$s).collect(java.util.stream.Collectors.toList())", + expression, wireTypeArguments[i].convertFromClientType("el")); + } else if (this instanceof MapType) { + // Key is always String in Swagger 2 + expression = String.format("%1$s.entrySet().stream().collect(java.util.stream.Collectors.toMap(Map.Entry::getKey, el -> %2$s))", expression, wireTypeArguments[i].convertFromClientType("el.getValue()")); + } else if (this.getPackage().equals("io.reactivex")) { + expression = String.format("%1$s.map(el => %2$s)", expression, wireTypeArguments[0].convertFromClientType("el")); + } else { + throw new UnsupportedOperationException(String.format("Instance %1$s of generic type %2$s not supported for conversion from client type.", expression, toString())); + } + break; + } + } + + return expression; + } + + @Override + public String jsonToken() { + return jsonToken; + } + + @Override + public final String jsonDeserializationMethod(String jsonReaderName) { + return null; + } + + @Override + public final String jsonSerializationMethodCall(String jsonWriterName, String fieldName, String valueGetter, + boolean jsonMergePatch) { + return null; + } + + @Override + public final String xmlDeserializationMethod(String xmlReaderName, String attributeName, String attributeNamespace, + boolean namespaceIsConstant) { + return null; + } + + @Override + public final String xmlSerializationMethodCall(String xmlWriterName, String attributeOrElementName, + String namespaceUri, String valueGetter, boolean isAttribute, boolean nameIsVariable, boolean namespaceIsConstant) { + return null; + } + + @Override + public boolean isUsedInXml() { + return false; + } + + @Override + public String validate(String expression) { + return null; + } + + public String validate(String expression, int depth) { + return validate(expression); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/GraalVmConfig.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/GraalVmConfig.java new file mode 100644 index 0000000000..cf8813656d --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/GraalVmConfig.java @@ -0,0 +1,115 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.model.clientmodel; + +import com.microsoft.typespec.http.client.generator.core.util.TemplateUtil; +import com.azure.json.JsonSerializable; +import com.azure.json.JsonWriter; + +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +public class GraalVmConfig { + + private final List proxies; + private final List reflects; + private final boolean fluent; + + public GraalVmConfig(List proxies, List reflects, boolean fluent) { + this.proxies = proxies; + this.reflects = reflects; + this.fluent = fluent; + + Collections.sort(this.proxies); + Collections.sort(this.reflects); + } + + private static class ReflectConfig implements JsonSerializable { + private final String name; + + private ReflectConfig(String name) { + this.name = name; + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + return jsonWriter.writeStartObject() + .writeStringField("name", name) + .writeBooleanField("allDeclaredConstructors", true) + .writeBooleanField("allDeclaredFields", true) + .writeBooleanField("allDeclaredMethods", true) + .writeEndObject(); + } + } + + private static class ResourceConfig implements JsonSerializable { + + private static class Pattern implements JsonSerializable { + private final String pattern; + + private Pattern(String pattern) { + this.pattern = pattern; + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + return jsonWriter.writeStartObject() + .writeStringField("pattern", pattern) + .writeEndObject(); + } + } + + private static class Resource implements JsonSerializable{ + private final List includes; + + public Resource(List includes) { + this.includes = includes; + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + return jsonWriter.writeStartObject() + .writeArrayField("includes", includes, JsonWriter::writeJson) + .writeEndObject(); + } + } + + private final Resource resources; + private final List bundles = Collections.emptyList(); + + private ResourceConfig(String artifactId) { + this.resources = new Resource(Collections.singletonList( + new Pattern("\\Q" + artifactId + ".properties" + "\\E"))); + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + return jsonWriter.writeStartObject() + .writeJsonField("resources", resources) + .writeArrayField("bundles", bundles, JsonWriter::writeUntyped) + .writeEndObject(); + } + } + + public boolean generateResourceConfig() { + return !this.fluent; + } + + // TODO: Template + public String toProxyConfigJson() { + List> result = proxies.stream().map(Collections::singletonList).collect(Collectors.toList()); + return TemplateUtil.prettyPrintToJson(result); + } + + public String toReflectConfigJson() { + List result = reflects.stream().map(ReflectConfig::new).collect(Collectors.toList()); + return TemplateUtil.prettyPrintToJson(result); + } + + public String toResourceConfigJson(String artifactId) { + return TemplateUtil.prettyPrintToJson(new ResourceConfig(artifactId)); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/IType.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/IType.java new file mode 100644 index 0000000000..8fed07be78 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/IType.java @@ -0,0 +1,190 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.model.clientmodel; + +import com.azure.json.JsonReader; +import com.azure.json.JsonToken; +import com.azure.json.JsonWriter; +import com.azure.xml.XmlWriter; + +import java.util.Set; + +/** + * A type used by a client. + */ +public interface IType { + /** + * The type variant that users interact with. + * + * @return The type's client-side variant. + */ + IType getClientType(); + + /** + * Convert this type to the type users interact with. + * + * @param expression The expression used to convert the type to a client type. + * @return Java code to convert an expression to client type. + */ + String convertToClientType(String expression); + + /** + * Convert the client type variant of this type to the original form that should be sent on the wire. + * + * @param expression The expression used to convert from the client type. + * @return Java code to convert a client type expression to wire format. + */ + String convertFromClientType(String expression); + + /** + * Indicates whether the type is nullable. + * + * @return Whether the type is nullable. + */ + default boolean isNullable() { + return true; + } + + /** + * Convert this IType to an IType that is nullable. + * + * @return A version of this IType that is nullable. + */ + IType asNullable(); + + /** + * Get whether this IType contains (or is) the provided type. + * + * @param type The type to search for. + * @return Whether this IType contains (or is) the provided type. + */ + boolean contains(IType type); + + /** + * Add this type's imports to the provided set of imports. + * + * @param imports The set of imports to add to. + * @param includeImplementationImports Whether to include imports that are only necessary for method + * implementations. + */ + void addImportsTo(Set imports, boolean includeImplementationImports); + + /** + * Convert the provided default value expression to this type's default value expression. + * + * @param sourceExpression The source expression to convert to this type's default value expression. + * @return This type's default value expression. + */ + String defaultValueExpression(String sourceExpression); + + /** + * The default value expression, when this type does not have data. + *

+ * This is the expression of the type provided as client. By default, the expression is "null" for class types. For + * primitive types, this would be the Java default value, usually "0". For some collection types, this could be the + * empty collection. + * + * @return the default value expression, when this type does not have data. + */ + String defaultValueExpression(); + + /** + * Gets the Java code used to validate the type. + * + * @param expression The expression used during validation. + * @return The Java code used to validate the type. + */ + String validate(String expression); + + /** + * Gets the {@link JsonToken} associated to the type. + *

+ * The following table shows what will be returned: + *

    + *
  • String, String-based object - JsonToken.STRING
  • + *
  • Primitive number, boxed number - JsonToken.NUMBER
  • + *
  • Complex object, Map - JsonToken.START_OBJECT
  • + *
  • Array, Collection - JsonToken.START_ARRAY
  • + *
+ * + * All other types will return null, such as Enums which don't have a specific type. In the case of Enums the value + * type should be inspected. + * + * @return The {@link JsonToken} associated to the type. + */ + String jsonToken(); + + /** + * Gets the method that handles JSON deserialization for the type. + *

+ * If null is returned it either means the type is complex, such as a List or Map, or doesn't have a JSON + * deserialization method and support needs to be added. + * + * @param jsonReaderName The name of the {@link JsonReader} performing deserialization. + * @return The JSON deserialization method, or null i it isn't supported directly. + */ + String jsonDeserializationMethod(String jsonReaderName); + + /** + * Gets the method call that will handle JSON serialization. + *

+ * If {@code fieldName} is null it is assumed that a JSON value is being serialized. + *

+ * If null is returned it either means the type is complex, such as a List or Map, or doesn't have a serialization + * method and support needs to be added. + * + * @param jsonWriterName The name of the {@link JsonWriter} performing serialization. + * @param fieldName The name of the JSON field, optional. + * @param valueGetter The value getter. + * @param jsonMergePatch Flag indicating if the serialization call is for a JSON merge patch operation. + * @return The method call that will handle JSON serialization, or null if it isn't supported directly. + */ + String jsonSerializationMethodCall(String jsonWriterName, String fieldName, String valueGetter, + boolean jsonMergePatch); + + /** + * Gets the method that handles XML deserialization for the type. + *

+ * This method handles both XML attributes and elements. If {@code attributeName} is null the XML element + * deserialization method is returned. + *

+ * If null is returned it either means the type is complex, such as a List or Map, or doesn't have an XML + * deserialization method and support needs to be added. + * + * @param xmlReaderName The name of the {@link com.azure.xml.XmlReader} performing deserialization. + * @param attributeName The attribute name, if null this is considered to be an element call. + * @param attributeNamespace The attribute namespace, optional, ignored if {@code attributeName} is null. + * @param namespaceIsConstant Whether the {@code attributeNamespace} is a constant instead of a string. + * @return The XML deserialization method, or null i it isn't supported directly. + */ + String xmlDeserializationMethod(String xmlReaderName, String attributeName, String attributeNamespace, + boolean namespaceIsConstant); + + /** + * Gets the method call that will handle XML serialization. + *

+ * If {@code attributeOrElementName} is null it is assumed that an XML value is being serialized. + *

+ * If null is returned it either means the type is complex, such as a List or Map, or doesn't have a serialization + * method and support needs to be added. + * + * @param xmlWriterName The name of the {@link XmlWriter} performing serialization. + * @param attributeOrElementName The name of the XML attribute or element, optional. + * @param namespaceUri The namespace URI of the XML attribute or element, optional. + * @param valueGetter The value getter. + * @param isAttribute Whether an attribute is being written, if true {@code attributeOrElementName} must be set. + * @param nameIsVariable Whether the {@code attributeOrElementName} is a variable instead of a string. + * @param namespaceIsConstant Whether the {@code namespaceUri} is a constant instead of a string. + * @return The method call that will handle XML serialization, or null if it isn't supported directly. + */ + String xmlSerializationMethodCall(String xmlWriterName, String attributeOrElementName, String namespaceUri, + String valueGetter, boolean isAttribute, boolean nameIsVariable, boolean namespaceIsConstant); + + /** + * Whether the type is used with XML serialization. + * + * @return Whether the type is used with XML serialization. + */ + boolean isUsedInXml(); +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/ImplementationDetails.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/ImplementationDetails.java new file mode 100644 index 0000000000..30a4542e85 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/ImplementationDetails.java @@ -0,0 +1,309 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.model.clientmodel; + +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.SchemaContext; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * An instance on implementation details of method or model. + */ +public class ImplementationDetails { + + /** + * Usage of the model or method. See {@link SchemaContext}. + */ + public enum Usage { + /** + * Model used in input of operation. + */ + INPUT("input"), + + /** + * Model used in output of operation. + */ + OUTPUT("output"), + + /** + * Model used in error output of operation. + */ + EXCEPTION("exception"), + + /** + * Public model. + *

+ * Usually it means that the model is used in input or output of methods marked as convenience API (and that API + * is not marked as internal). Codegen should generate the class in models package. + */ + PUBLIC("public"), + + /** + * Model used in paged response. + *

+ * Codegen may choose to not generate class for it, or generate class in implementation package. + */ + PAGED("paged"), + + /** + * Anonymous model. + *

+ * Codegen may choose to not generate class for it, or generate class in implementation package. + */ + ANONYMOUS("anonymous"), + + /** + * External model. + *

+ * Codegen should not generate the class. Javadoc or test/sample generation will still need to process the + * model. Codegen likely need to have additional "require" clause in module-info.java, and additional dependency + * in pom.xml. + */ + EXTERNAL("external"), + + /** + * Internal model. + *

+ * Codegen should generate the class in implementation package. + */ + INTERNAL("internal"), + + /** + * Model used in json-merge-patch operation + *

+ * Codegen should handle serialization and deserialization specially for json-merge-patch model + */ + JSON_MERGE_PATCH("json-merge-patch"), + + /** + * Model used in options group + *

+ * Serialization code will not be generated + */ + OPTIONS_GROUP("options-group"); + + private final static Map CONSTANTS = new HashMap<>(); + + static { + for (Usage c : values()) { + CONSTANTS.put(c.value, c); + } + } + + private final String value; + + Usage(String value) { + this.value = value; + } + + /** + * Get the string value of the usage. + * + * @return the string value of the usage. + */ + public String value() { + return this.value; + } + + /** + * Get the Usage instance from the string value. + * + * @param value the string value. + * @return the Usage instance. + * @throws IllegalArgumentException thrown if the string value doesn't match any Usage. + */ + public static Usage fromValue(String value) { + Usage constant = CONSTANTS.get(value); + if (constant == null) { + throw new IllegalArgumentException(value); + } else { + return constant; + } + } + + /** + * Get the Usage instance from the SchemaContext. + * + * @param schemaContext the SchemaContext. + * @return the Usage instance. + * @throws IllegalArgumentException thrown if the SchemaContext doesn't match any Usage. + */ + public static Usage fromSchemaContext(SchemaContext schemaContext) { + switch (schemaContext) { + case INPUT: + return INPUT; + case OUTPUT: + return OUTPUT; + case EXCEPTION: + return EXCEPTION; + case PUBLIC: + return PUBLIC; + case PAGED: + return PAGED; + case ANONYMOUS: + return ANONYMOUS; + case INTERNAL: + return INTERNAL; + case JSON_MERGE_PATCH: + return JSON_MERGE_PATCH; + case OPTIONS_GROUP: + return OPTIONS_GROUP; + default: + throw new IllegalArgumentException(schemaContext.toString()); + } + } + } + + private final boolean implementationOnly; + + private final Set usages; + + private final String comment; + + /** + * Usually on a method, that it is only required in implementation class (FooClientImpl), but not in public class + * (FooClient). + * + * @return whether only required in implementation class. + */ + public boolean isImplementationOnly() { + return implementationOnly; + } + + /** + * Usage of the model or method. See {@link SchemaContext}. + * + * @return the usage of the model or method. + */ + public Set getUsages() { + return usages; + } + + /** + * Whether the model need to be generated for public use. + * + * @return whether the model need to be generated for public use. + */ + public boolean isPublic() { + return usages.contains(Usage.PUBLIC); + } + + /** + * Whether the model need to be generated for internal use. + * + * @return whether the model need to be generated for internal use. + */ + public boolean isInternal() { + return usages.contains(Usage.INTERNAL); + } + + /** + * Whether the model need to be generated for input use. + * + * @return whether the model need to be generated for input use. + */ + public boolean isInput() { + return usages.contains(Usage.INPUT); + } + + /** + * Whether the model need to be generated for output use. + * + * @return whether the model need to be generated for output use. + */ + public boolean isOutput() { + return usages.contains(Usage.OUTPUT); + } + + /** + * Whether the model need to be generated for exception use. + * + * @return whether the model need to be generated for exception use. + */ + public boolean isException() { + return usages.contains(Usage.EXCEPTION); + } + + /** + * Get the API comment. + * + * @return API comment. + */ + public String getComment() { + return comment; + } + + /** + * Creates an instance of ImplementationDetails class. + * + * @param implementationOnly whether only required in implementation class. + * @param usages usage of the model or method. + * @param comment API comment. + */ + protected ImplementationDetails(boolean implementationOnly, Set usages, String comment) { + this.implementationOnly = implementationOnly; + this.usages = usages; + this.comment = comment; + } + + /** + * Builder for {@link ImplementationDetails}. + */ + public static final class Builder { + private boolean implementationOnly = false; + private Set usages = new HashSet<>(); + private String comment; + + /** + * Creates an instance of Builder class. + */ + public Builder() { + } + + /** + * Set whether only required in implementation class. + * + * @param implementationOnly whether only required in implementation class. + * @return the Builder itself. + */ + public Builder implementationOnly(boolean implementationOnly) { + this.implementationOnly = implementationOnly; + return this; + } + + /** + * Sets usage of the model or method. + * + * @param usages usage of the model or method. + * @return the Builder itself. + */ + public Builder usages(Set usages) { + this.usages = usages; + return this; + } + + /** + * Sets API comment. + * + * @param comment API comment. + * @return the Builder itself. + */ + public Builder comment(String comment) { + this.comment = comment; + return this; + } + + /** + * Builds an instance of ImplementationDetails class. + * + * @return the ImplementationDetails instance. + */ + public ImplementationDetails build() { + return new ImplementationDetails(implementationOnly, usages, comment); + } + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/IterableType.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/IterableType.java new file mode 100644 index 0000000000..05a48fa8ac --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/IterableType.java @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.model.clientmodel; + + +import java.util.Set; + +public class IterableType extends GenericType { + /** + * Create a new IterableType from the provided properties. + * @param elementType The type of elements that are stored in this sequence. + */ + public IterableType(IType elementType) { + super("java.lang", "Iterable", "JsonToken.START_ARRAY", elementType); + } + + IterableType(String packageName, String className, IType elementType) { + super(packageName, className, "JsonToken.START_ARRAY", elementType); + } + + /** + * The type of elements that are stored in this iterable. + */ + public final IType getElementType() { + return getTypeArguments()[0]; + } + + @Override + public final void addImportsTo(Set imports, boolean includeImplementationImports) { + super.addImportsTo(imports, includeImplementationImports); + } + + @Override + public final String validate(String expression) { + return validate(expression, 0); + } + + @Override + public final String validate(String expression, int depth) { + String var = depth == 0 ? "e" : "e" + depth; + String elementValidation = getElementType() instanceof GenericType + ? ((GenericType) getElementType()).validate(var, depth + 1) + : getElementType().validate(var); + if (elementValidation != null) { + return String.format("%s.forEach(%s -> %s)", expression, var, elementValidation); + } else { + return null; + } + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/ListType.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/ListType.java new file mode 100644 index 0000000000..f066953c15 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/ListType.java @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.model.clientmodel; + +/** + * A sequence type used by a client. + */ +public class ListType extends IterableType { + /** + * Create a new ListType from the provided properties. + * @param elementType The type of elements that are stored in this sequence. + */ + public ListType(IType elementType) { + super("java.util", "List", elementType); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/LiveTestCase.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/LiveTestCase.java new file mode 100644 index 0000000000..c3058fa6ff --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/LiveTestCase.java @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.model.clientmodel; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class LiveTestCase { + + private final String name; + private final List testSteps = new ArrayList<>(); + private final String description; + + public LiveTestCase(String name, String description) { + this.name = name; + this.description = description; + } + + public String getDescription() { + return description; + } + + public void addTestSteps(List testSteps) { + this.testSteps.addAll(testSteps); + } + + public List getTestSteps() { + return Collections.unmodifiableList(testSteps); + } + + public String getName() { + return name; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/LiveTestStep.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/LiveTestStep.java new file mode 100644 index 0000000000..301101a6da --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/LiveTestStep.java @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.model.clientmodel; + +public abstract class LiveTestStep { + + protected String description; + + public String getDescription() { + return description; + } + + void setDescription(String description) { + this.description = description; + } + + public static abstract class Builder { + protected final S step; + + abstract protected T getThis(); + + protected Builder(S step) { + this.step = step; + } + + public T description(String description) { + step.setDescription(description); + return getThis(); + } + + public S build() { + return step; + } + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/LiveTests.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/LiveTests.java new file mode 100644 index 0000000000..0fcc79567f --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/LiveTests.java @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.model.clientmodel; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class LiveTests { + + private final String filename; + private final List testCases = new ArrayList<>(); + + public LiveTests(String filename) { + this.filename = filename; + } + + public String getFilename() { + return filename; + } + + public List getTestCases() { + return Collections.unmodifiableList(testCases); + } + + public void addTestCases(List testCases){ + if (testCases != null) { + this.testCases.addAll(testCases); + } + } + +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/Manager.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/Manager.java new file mode 100644 index 0000000000..cab812ba2c --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/Manager.java @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.model.clientmodel; + +/** + * The details needed to create a Manager class for the client. + */ +public class Manager { + private String packageName; + /** + * The name of the service client. + */ + private String serviceClientName; + /** + * The name of the service. + */ + private String serviceName; + private ClientMethodParameter azureTokenCredentialsParameter; + private ClientMethodParameter httpPipelineParameter; + + /** + * Create a new Manager with the provided properties. + * @param packageName The package of this manager class. + * @param serviceClientName The name of the service client. + * @param serviceName The name of the service. + * @param azureTokenCredentialsParameter The credentials parameter. + * @param httpPipelineParameter The HttpPipeline parameter. + */ + public Manager(String packageName, String serviceClientName, String serviceName, ClientMethodParameter azureTokenCredentialsParameter, ClientMethodParameter httpPipelineParameter) { + this.packageName = packageName; + this.serviceClientName = serviceClientName; + this.serviceName = serviceName; + this.azureTokenCredentialsParameter = azureTokenCredentialsParameter; + this.httpPipelineParameter = httpPipelineParameter; + } + + public final String getPackage() { + return packageName; + } + + public final String getServiceClientName() { + return serviceClientName; + } + + public final String getServiceName() { + return serviceName; + } + + public final ClientMethodParameter getAzureTokenCredentialsParameter() { + return azureTokenCredentialsParameter; + } + + public final ClientMethodParameter getHttpPipelineParameter() { + return httpPipelineParameter; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/MapType.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/MapType.java new file mode 100644 index 0000000000..2419b5f703 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/MapType.java @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.model.clientmodel; + +import java.util.Set; + +/** + * A map type used by a client. + */ +public class MapType extends GenericType { + + private boolean valueNullable = false; + + /** + * Create a new MapType from the provided properties. + * @param valueType The type of values that are stored in this dictionary. + */ + public MapType(IType valueType) { + this(valueType, false); + } + + public MapType(IType valueType, boolean valueNullable) { + super("java.util", "Map", "JsonToken.START_ARRAY", ClassType.STRING, valueType); + this.valueNullable = valueNullable; + } + + /** + * The type of values that are stored in this map. + */ + public final IType getValueType() { + return getTypeArguments()[1]; + } + + public boolean isValueNullable() { + return valueNullable; + } + + @Override + public void addImportsTo(Set imports, boolean includeImplementationImports) { + super.addImportsTo(imports, includeImplementationImports); + } + + @Override + public String validate(String expression) { + return validate(expression, 0); + } + + @Override + public String validate(String expression, int depth) { + String var = depth == 0 ? "e" : "e" + depth; + String elementValidation = getValueType() instanceof GenericType + ? ((GenericType) getValueType()).validate(var, ++depth) + : getValueType().validate(var); + if (elementValidation != null) { + return String.format("%s.values().forEach(%s -> { if (%s != null) { %s; } })", expression, var, var, elementValidation); + } else { + return null; + } + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/MethodGroupClient.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/MethodGroupClient.java new file mode 100644 index 0000000000..5bf5d72605 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/MethodGroupClient.java @@ -0,0 +1,338 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.model.clientmodel; + +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; +import com.microsoft.typespec.http.client.generator.core.util.ClientModelUtil; +import com.azure.core.util.CoreUtils; + +import java.util.List; +import java.util.Set; + +/** + * The details of a group of methods within a ServiceClient. + */ +public class MethodGroupClient { + /** + * The name of the package. + */ + private String packageName; + /** + * The name of this client's class. + */ + private String className; + /** + * The name of this client's interface. + */ + private String interfaceName; + /** + * The interfaces that the client implements. + */ + private List implementedInterfaces; + /** + * The REST API that this client will send requests to. + */ + private Proxy proxy; + /** + * The name of the ServiceClient that contains this MethodGroupClient. + */ + private String serviceClientName; + /** + * The type of this MethodGroupClient when it is used as a variable. + */ + private String variableType; + /** + * The variable name for any instances of this MethodGroupClient. + */ + private String variableName; + /** + * The client method overloads for this MethodGroupClient. + */ + private List clientMethods; + /** + * The interfaces that the client supports. + */ + private List supportedInterfaces; + + private String classBaseName; + + private List properties; + + /** + * Create a new MethodGroupClient with the provided properties. + * @param className The name of the client's class. + * @param interfaceName The name of the client's interface. + * @param implementedInterfaces The interfaces that the client implements. + * @param supportedInterfaces The interfaces that the client supports. + * @param proxy The REST API that the client will send requests to. + * @param serviceClientName The name of the ServiceClient that contains this MethodGroupClient. + * @param variableType The type of this MethodGroupClient when it is used as a variable. + * @param variableName The variable name for any instances of this MethodGroupClient. + * @param clientMethods The ClientMethods for this MethodGroupClient. + */ + protected MethodGroupClient( + String packageKeyword, String className, String interfaceName, List implementedInterfaces, + Proxy proxy, String serviceClientName, String variableType, String variableName, + List clientMethods, List supportedInterfaces, String classBaseName, + List properties) { + packageName = packageKeyword; + this.className = className; + this.interfaceName = interfaceName; + this.implementedInterfaces = implementedInterfaces; + this.supportedInterfaces = supportedInterfaces; + this.proxy = proxy; + this.serviceClientName = serviceClientName; + this.variableType = variableType; + this.variableName = variableName; + this.clientMethods = clientMethods; + this.classBaseName = classBaseName != null + ? classBaseName + : (className.endsWith("Impl") ? className.substring(0, className.length() - 4) : className); + this.properties = properties; + } + + public final String getPackage() { + return packageName; + } + + public final String getClassName() { + return className; + } + + public final String getInterfaceName() { + return interfaceName; + } + + public final List getImplementedInterfaces() { + return implementedInterfaces; + } + + public List getSupportedInterfaces() { + return supportedInterfaces; + } + + public final Proxy getProxy() { + return proxy; + } + + public final String getServiceClientName() { + return serviceClientName; + } + + public final String getVariableType() { + return variableType; + } + + public final String getVariableName() { + return variableName; + } + + public final List getClientMethods() { + return clientMethods; + } + + public final String getClassBaseName() { + return classBaseName; + } + + public List getProperties() { + return properties; + } + + /** + * Add this property's imports to the provided set of imports. + * @param imports The set of imports to add to. + * @param includeImplementationImports Whether to include imports that are only necessary for method implementations. + */ + public final void addImportsTo(Set imports, boolean includeImplementationImports, JavaSettings settings) { + if (!settings.isFluent() && settings.isGenerateClientInterfaces()) { + imports.add(String.format("%1$s.%2$s", settings.getPackage(), getInterfaceName())); + } + + for (IType type : supportedInterfaces) { + type.addImportsTo(imports, false); + } + + if (includeImplementationImports) { + //ClassType proxyType = settings.isAzureOrFluent() ? ClassType.AzureProxy : ClassType.RestProxy; + ClassType proxyType = getProxyClassType(); + imports.add(proxyType.getFullName()); + + if (settings.isGenerateClientInterfaces()) { + String interfacePackage = ClientModelUtil.getServiceClientInterfacePackageName(); + imports.add(String.format("%1$s.%2$s", interfacePackage, this.getInterfaceName())); + } + } + + Proxy proxy = getProxy(); + if (proxy != null) { + proxy.addImportsTo(imports, includeImplementationImports, settings); + } + + for (ClientMethod clientMethod : getClientMethods()) { + clientMethod.addImportsTo(imports, includeImplementationImports, settings); + } + + if (includeImplementationImports && !CoreUtils.isNullOrEmpty(getProperties())) { + for (ServiceClientProperty property : getProperties()) { + property.addImportsTo(imports, includeImplementationImports); + } + } + } + + protected ClassType getProxyClassType() { + return ClassType.REST_PROXY; + } + + public static class Builder { + protected String packageName; + protected String className; + protected String interfaceName; + protected List implementedInterfaces; + protected Proxy proxy; + protected String serviceClientName; + protected String variableType; + protected String variableName; + protected List clientMethods; + protected List supportedInterfaces; + protected String classBaseName; + private List properties; + + /** + * Sets the name of the package. + * @param packageName the name of the package + * @return the Builder itself + */ + public Builder packageName(String packageName) { + this.packageName = packageName; + return this; + } + + /** + * Sets the name of this client's class. + * @param className the name of this client's class + * @return the Builder itself + */ + public Builder className(String className) { + this.className = className; + return this; + } + + /** + * Sets the name of this client's interface. + * @param interfaceName the name of this client's interface + * @return the Builder itself + */ + public Builder interfaceName(String interfaceName) { + this.interfaceName = interfaceName; + return this; + } + + /** + * Sets the interfaces that the client implements. + * @param implementedInterfaces the interfaces that the client implements + * @return the Builder itself + */ + public Builder implementedInterfaces(List implementedInterfaces) { + this.implementedInterfaces = implementedInterfaces; + return this; + } + + /** + * Sets the REST API that this client will send requests to. + * @param proxy the REST API that this client will send requests to + * @return the Builder itself + */ + public Builder proxy(Proxy proxy) { + this.proxy = proxy; + return this; + } + + /** + * Sets the name of the ServiceClient that contains this MethodGroupClient. + * @param serviceClientName the name of the ServiceClient that contains this MethodGroupClient + * @return the Builder itself + */ + public Builder serviceClientName(String serviceClientName) { + this.serviceClientName = serviceClientName; + return this; + } + + /** + * Sets the type of this MethodGroupClient when it is used as a variable. + * @param variableType the type of this MethodGroupClient when it is used as a variable + * @return the Builder itself + */ + public Builder variableType(String variableType) { + this.variableType = variableType; + return this; + } + + /** + * Sets the variable name for any instances of this MethodGroupClient. + * @param variableName the variable name for any instances of this MethodGroupClient + * @return the Builder itself + */ + public Builder variableName(String variableName) { + this.variableName = variableName; + return this; + } + + /** + * Sets the client method overloads for this MethodGroupClient. + * @param clientMethods the client method overloads for this MethodGroupClient + * @return the Builder itself + */ + public Builder clientMethods(List clientMethods) { + this.clientMethods = clientMethods; + return this; + } + + /** + * Sets the interfaces that the client supports. + * @param supportedInterfaces the interfaces that the client supports + * @return the Builder itself + */ + public Builder supportedInterfaces(List supportedInterfaces) { + this.supportedInterfaces = supportedInterfaces; + return this; + } + + /** + * Sets the class base name. + * @param classBaseName class base name. + * @return the Builder itself + */ + public Builder classBaseName(String classBaseName) { + this.classBaseName = classBaseName; + return this; + } + + /** + * Sets properties. + * + * @param properties the properties from ServiceClient. + * @return the Builder itself + */ + public Builder properties(List properties) { + this.properties = properties; + return this; + } + + public MethodGroupClient build() { + return new MethodGroupClient(packageName, + className, + interfaceName, + implementedInterfaces, + proxy, + serviceClientName, + variableType, + variableName, + clientMethods, + supportedInterfaces, + classBaseName, + properties); + } + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/MethodPageDetails.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/MethodPageDetails.java new file mode 100644 index 0000000000..0aa9a4ac80 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/MethodPageDetails.java @@ -0,0 +1,75 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.model.clientmodel; + +/** + * A page class that contains results that are received from a service request. + */ +public class MethodPageDetails { + /** + * Get whether or not this method is a request to get the next page of a sequence of pages. + */ + private final String nextLinkName; + private final IType nextLinkType; + + private final String itemName; + + /** + * Serialized nextLink name. It is the name in swagger and in response. + */ + private final String serializedNextLinkName; + /** + * Serialized item name. It is the name in swagger and in response. + */ + private final String serializedItemName; + + private final ClientMethod nextMethod; + + // Proxy method return type is Flux. Client method return type is PagedResponse<>. + // This intermediate type is the type of pagination response (the type with values and nextLink). + private final IType lroIntermediateType; + + public MethodPageDetails(String nextLinkName, IType nextLinkType, String itemName, ClientMethod nextMethod, IType lroIntermediateType, + String serializedNextLinkName, String serializedItemName) { + this.nextLinkName = nextLinkName; + this.nextLinkType = nextLinkType; + this.itemName = itemName; + this.nextMethod = nextMethod; + this.lroIntermediateType = lroIntermediateType; + this.serializedNextLinkName = serializedNextLinkName; + this.serializedItemName = serializedItemName; + } + + public String getNextLinkName() { + return nextLinkName; + } + + public IType getNextLinkType() { + return nextLinkType; + } + + public String getSerializedNextLinkName() { + return serializedNextLinkName; + } + + public String getItemName() { + return itemName; + } + + public String getSerializedItemName() { + return serializedItemName; + } + + public ClientMethod getNextMethod() { + return nextMethod; + } + + public IType getLroIntermediateType() { + return lroIntermediateType; + } + + public boolean nonNullNextLink() { + return nextLinkName != null && !nextLinkName.isEmpty(); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/MethodParameter.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/MethodParameter.java new file mode 100644 index 0000000000..1213581901 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/MethodParameter.java @@ -0,0 +1,171 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.model.clientmodel; + +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.RequestParameterLocation; + +/** + * The base type for method parameters. + */ +public abstract class MethodParameter { + /** + * Get the description of this parameter. + */ + private final String description; + /** + * The wire type of this parameter. + */ + private final IType wireType; + /** + * The raw type of this parameter. Result of SchemaMapper. + */ + private final IType rawType; + /** + * The client type of this parameter. + */ + private final IType clientType; + /** + * The name of this parameter. + */ + private final String name; + /** + * Get the location within the REST API method's URL where this parameter will be added. + */ + private final RequestParameterLocation requestParameterLocation; + /** + * Whether this parameter is a constant value. + */ + private final boolean isConstant; + /** + * Whether this parameter is required. + */ + private final boolean isRequired; + /** + * Whether this parameter's value comes from a ServiceClientProperty. + */ + private final boolean fromClient; + /** + * Get the default value of this parameter. + */ + private final String defaultValue; + + /** + * Creates a new instance of {@link MethodParameter}. + * + * @param description The description of the parameter. + * @param wireType The wire type of the parameter. + * @param rawType The raw type of the parameter. + * @param clientType The client type of the parameter + * @param name The name of the parameter. + * @param requestParameterLocation The location within a REST API method's URL where this parameter will be added. + * @param isConstant Whether the parameter is a constant value. + * @param isRequired Whether the parameter is required. + * @param fromClient Whether the parameter;s value comes from a ServiceClientProperty. + * @param defaultValue The default value of the parameter. + */ + protected MethodParameter(String description, IType wireType, IType rawType, IType clientType, String name, + RequestParameterLocation requestParameterLocation, boolean isConstant, boolean isRequired, boolean fromClient, + String defaultValue) { + this.description = description; + this.wireType = wireType; + this.rawType = rawType; + this.clientType = clientType; + this.name = name; + this.requestParameterLocation = requestParameterLocation; + this.isConstant = isConstant; + this.isRequired = isRequired; + this.fromClient = fromClient; + this.defaultValue = defaultValue; + } + + /** + * Gets the description of this parameter. + * + * @return The description of this parameter. + */ + public final String getDescription() { + return description; + } + + /** + * Gets the wire type of this parameter. + * + * @return The wire type of this parameter. + */ + public final IType getWireType() { + return wireType; + } + + /** + * Gets the raw type of this parameter. Result of SchemaMapper. + * + * @return The raw type of this parameter, result of SchemaMapper. + */ + public final IType getRawType() { + return rawType; + } + + /** + * Gets the client type of this parameter. + * + * @return The client type of this parameter. + */ + public final IType getClientType() { + return clientType; + } + + /** + * Gets the name of this parameter. + * + * @return The name of this parameter. + */ + public final String getName() { + return name; + } + + /** + * Gets the location within the REST API method's URL where this parameter will be added. + * + * @return The location within the REST API method's URL where this parameter will be added. + */ + public final RequestParameterLocation getRequestParameterLocation() { + return requestParameterLocation; + } + + /** + * Gets whether this parameter is a constant value. + * + * @return Whether this parameter is a constant value. + */ + public final boolean isConstant() { + return isConstant; + } + + /** + * Gets whether this parameter is required. + * + * @return Whether this parameter is required. + */ + public final boolean isRequired() { + return isRequired; + } + + /** + * Gets whether this parameter's value comes from a ServiceClientProperty. + * + * @return Whether this parameter's value comes from a ServiceClientProperty. + */ + public final boolean isFromClient() { + return fromClient; + } + + /** + * Gets the default value of this parameter. + * + * @return The default value of this parameter. + */ + public final String getDefaultValue() { + return defaultValue; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/MethodPollingDetails.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/MethodPollingDetails.java new file mode 100644 index 0000000000..59b099eca6 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/MethodPollingDetails.java @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.model.clientmodel; + +public class MethodPollingDetails { + private final String pollingStrategy; + private final String syncPollingStrategy; + private final IType intermediateType; + private final IType finalType; + private final int pollIntervalInSeconds; + + public MethodPollingDetails(String pollingStrategy, String syncPollingStrategy, IType intermediateType, + IType finalType, int pollIntervalInSeconds) { + this.pollingStrategy = pollingStrategy; + this.syncPollingStrategy = syncPollingStrategy; + this.intermediateType = intermediateType; + this.finalType = finalType; + this.pollIntervalInSeconds = pollIntervalInSeconds; + } + + public String getPollingStrategy() { + return pollingStrategy; + } + + public String getSyncPollingStrategy() { + return syncPollingStrategy; + } + + public IType getIntermediateType() { + return intermediateType; + } + + public IType getFinalType() { + return finalType; + } + + public int getPollIntervalInSeconds() { + return pollIntervalInSeconds; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/MethodTransformationDetail.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/MethodTransformationDetail.java new file mode 100644 index 0000000000..ed29ee0f66 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/MethodTransformationDetail.java @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.model.clientmodel; + +import java.util.List; + +/** + * A transformation class that contains mappings from input parameters to proxy method parameters. + */ +public class MethodTransformationDetail { + private ClientMethodParameter outParameter; + private List parameterMappings; + + public MethodTransformationDetail(ClientMethodParameter outParameter, List parameterMappings) { + this.outParameter = outParameter; + this.parameterMappings = parameterMappings; + } + + public final ClientMethodParameter getOutParameter() { + return outParameter; + } + + public final List getParameterMappings() { + return parameterMappings; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/ModelProperty.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/ModelProperty.java new file mode 100644 index 0000000000..06a0007515 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/ModelProperty.java @@ -0,0 +1,116 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.model.clientmodel; + +import com.microsoft.typespec.http.client.generator.core.util.ClientModelUtil; + +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +public class ModelProperty { + + private final ClientModelPropertyAccess property; + + private ModelProperty(ClientModelPropertyAccess property) { + this.property = property; + } + + public static ModelProperty ofClientModelProperty(ClientModelPropertyAccess property) { + return new ModelProperty(property); + } + + public String getGetterName() { + return property.getGetterName(); + } + + public String getSetterName() { + return property.getSetterName(); + } + + public void addImportsTo(Set imports) { + property.addImportsTo(imports, false); + } + + public String getName() { + return property.getName(); + } + + public String getDescription() { + return property.getDescription(); + } + + public IType getClientType() { + return property.getClientType(); + } + + public IType getWireType() { + return property.getWireType(); + } + + public boolean isRequired() { + return property.isRequired(); + } + + public boolean isConstant() { + return property.isConstant(); + } + + public boolean isReadOnly() { + return property.isReadOnly(); + } + + public boolean isReadOnlyForCreate() { + return property.isReadOnlyForCreate(); + } + + public boolean isReadOnlyForUpdate() { + return property.isReadOnlyForUpdate(); + } + + public String getSerializedName() { + if (property instanceof ClientModelProperty) { + return ((ClientModelProperty) property).getSerializedName(); + } else if (property instanceof ClientModelPropertyReference) { + return ((ClientModelPropertyReference) property).getAllProperties().stream() + .map(ClientModelProperty::getSerializedName) + .map(s -> s.replace(".", "\\\\.")) + .collect(Collectors.joining(".")); + } else { + throw new IllegalStateException("Unknown subclass of ClientModelPropertyAccess: " + property.getClass().getName()); + } + } + + public List getSerializedNames() { + if (property instanceof ClientModelProperty) { + ClientModelProperty clientModelProperty = (ClientModelProperty) property; + if (!clientModelProperty.getNeedsFlatten()) { + return ClientModelUtil.splitFlattenedSerializedName(clientModelProperty.getSerializedName()); + } else { + return Collections.singletonList(clientModelProperty.getSerializedName()); + } + } else if (property instanceof ClientModelPropertyReference) { + return ((ClientModelPropertyReference) property).getAllProperties().stream() + .map(ClientModelProperty::getSerializedName) + .collect(Collectors.toList()); + } else { + throw new IllegalStateException("Unknown subclass of ClientModelPropertyAccess: " + property.getClass().getName()); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ModelProperty that = (ModelProperty) o; + return Objects.equals(property, that.property); + } + + @Override + public int hashCode() { + return Objects.hash(property); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/ModuleInfo.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/ModuleInfo.java new file mode 100644 index 0000000000..180ed4d987 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/ModuleInfo.java @@ -0,0 +1,145 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.model.clientmodel; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +public class ModuleInfo { + private final String moduleName; + private final List requireModules = new ArrayList<>(); + private final List exportModules = new ArrayList<>(); + private final List openModules = new ArrayList<>(); + + public static class RequireModule { + private final String moduleName; + private final boolean isTransitive; + + public RequireModule(String moduleName) { + this.moduleName = moduleName; + this.isTransitive = false; + } + + public RequireModule(String moduleName, boolean isTransitive) { + this.moduleName = moduleName; + this.isTransitive = isTransitive; + } + + public String getModuleName() { + return moduleName; + } + + public boolean isTransitive() { + return isTransitive; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + RequireModule that = (RequireModule) o; + return isTransitive == that.isTransitive && Objects.equals(moduleName, that.moduleName); + } + + @Override + public int hashCode() { + return Objects.hash(moduleName, isTransitive); + } + } + + public static class ExportModule { + private final String moduleName; + + public ExportModule(String moduleName) { + this.moduleName = moduleName; + } + + public String getModuleName() { + return moduleName; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ExportModule that = (ExportModule) o; + return Objects.equals(moduleName, that.moduleName); + } + + @Override + public int hashCode() { + return Objects.hash(moduleName); + } + } + + public static class OpenModule { + private final String moduleName; + private final List openToModules; + + public OpenModule(String moduleName) { + this.moduleName = moduleName; + this.openToModules = null; + } + + public OpenModule(String moduleName, List openToModules) { + this.moduleName = moduleName; + this.openToModules = openToModules; + } + + public String getModuleName() { + return moduleName; + } + + public List getOpenToModules() { + return openToModules; + } + + public boolean isOpenTo() { + return openToModules != null && !openToModules.isEmpty(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + OpenModule that = (OpenModule) o; + return Objects.equals(moduleName, that.moduleName) && Objects.equals(openToModules, that.openToModules); + } + + @Override + public int hashCode() { + return Objects.hash(moduleName, openToModules); + } + } + + public ModuleInfo(String moduleName) { + this.moduleName = moduleName; + } + + public String getModuleName() { + return moduleName; + } + + public List getRequireModules() { + return requireModules; + } + + public List getExportModules() { + return exportModules; + } + + public List getOpenModules() { + return openModules; + } + + // TODO (weidxu): this method likely will get refactored when we support external model (hence external package) + public void checkForAdditionalDependencies(Set externalPackageNames) { + // currently, only check for azure-core-experimental + if (externalPackageNames.stream().anyMatch(p -> p.startsWith("com.azure.core.experimental"))) { + getRequireModules().add(new ModuleInfo.RequireModule("com.azure.core.experimental", true)); + } + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/PackageInfo.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/PackageInfo.java new file mode 100644 index 0000000000..a5a465d715 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/PackageInfo.java @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.model.clientmodel; + +/** + * The details needed to create a package-info class for the client. + */ +public class PackageInfo { + private String packageName; + private String description; + + public PackageInfo(String packageKeyword, String description) { + packageName = packageKeyword; + this.description = description; + } + + public String getPackage() { + return packageName; + } + + public final String getDescription() { + return description; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/PageDetails.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/PageDetails.java new file mode 100644 index 0000000000..91f7d4e230 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/PageDetails.java @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.model.clientmodel; + +/** + * A page class that contains results that are received from a service request. + */ +public class PageDetails { + public String packageName; + private String nextLinkName; + private String itemName; + private String className; + + public PageDetails(String packageKeyword, String nextLinkName, String itemName, String className) { + packageName = packageKeyword; + this.nextLinkName = nextLinkName; + this.itemName = itemName; + this.className = className; + } + + public final String getNextLinkName() { + return nextLinkName; + } + + public final String getItemName() { + return itemName; + } + + public final String getClassName() { + return className; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/ParameterMapping.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/ParameterMapping.java new file mode 100644 index 0000000000..09c145a849 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/ParameterMapping.java @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.model.clientmodel; + +public class ParameterMapping { + private ClientMethodParameter inputParameter; + + private ClientModelProperty inputParameterProperty; + + private String outputParameterPropertyName; + + private ClientModelProperty outputParameterProperty; + + public ClientMethodParameter getInputParameter() { + return inputParameter; + } + + public ParameterMapping setInputParameter(ClientMethodParameter inputParameter) { + this.inputParameter = inputParameter; + return this; + } + + public ClientModelProperty getInputParameterProperty() { + return inputParameterProperty; + } + + public ParameterMapping setInputParameterProperty(ClientModelProperty inputParameterProperty) { + this.inputParameterProperty = inputParameterProperty; + return this; + } + + public String getOutputParameterPropertyName() { + return outputParameterPropertyName; + } + + public ParameterMapping setOutputParameterPropertyName(String outputParameterPropertyName) { + this.outputParameterPropertyName = outputParameterPropertyName; + return this; + } + + public ClientModelProperty getOutputParameterProperty() { + return outputParameterProperty; + } + + public ParameterMapping setOutputParameterProperty(ClientModelProperty outputParameterProperty) { + this.outputParameterProperty = outputParameterProperty; + return this; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/ParameterSynthesizedOrigin.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/ParameterSynthesizedOrigin.java new file mode 100644 index 0000000000..48dc2f54d0 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/ParameterSynthesizedOrigin.java @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.model.clientmodel; + +public enum ParameterSynthesizedOrigin { + + /** + * host url parameter + */ + HOST("modelerfour:synthesized/host"), + + /** + * accept header + */ + ACCEPT("modelerfour:synthesized/accept"), + + /** + * content-type header + */ + CONTENT_TYPE("modelerfour:synthesized/content-type"), + + /** + * api-version (usually) query parameter + */ + API_VERSION("modelerfour:synthesized/api-version"), + + /** + * Context + */ + CONTEXT("java:synthesized/Context"), + + /** + * RequestOptions + */ + REQUEST_OPTIONS("java:synthesized/RequestOptions"), + + /** + * The parameter is not synthesized. + */ + NONE("none"); // NONE is not defined as m4 output + + + private final String origin; + + ParameterSynthesizedOrigin(String origin) { + this.origin = origin; + } + + public String value() { + return this.origin; + } + + @Override + public String toString() { + return this.value(); + } + + public static ParameterSynthesizedOrigin fromValue(String value) { + if (value == null) { + return NONE; + } + + for (ParameterSynthesizedOrigin v : values()) { + if (v.value().equalsIgnoreCase(value)) { + return v; + } + } + return NONE; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/PipelinePolicyDetails.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/PipelinePolicyDetails.java new file mode 100644 index 0000000000..bd2f6592b4 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/PipelinePolicyDetails.java @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.model.clientmodel; + +public class PipelinePolicyDetails { + + private String requestIdHeaderName; + + public String getRequestIdHeaderName() { + return requestIdHeaderName; + } + + public PipelinePolicyDetails setRequestIdHeaderName(String requestIdHeaderName) { + this.requestIdHeaderName = requestIdHeaderName; + return this; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/Pom.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/Pom.java new file mode 100644 index 0000000000..f1e2e8e697 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/Pom.java @@ -0,0 +1,120 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.model.clientmodel; + +import java.util.List; +import java.util.Map; + +public class Pom { + private String parentIdentifier; + private String parentRelativePath; + private String parentVersion; + private String groupId; + private String artifactId; + private String version; + private String serviceName; + private String serviceDescription; + private List dependencyIdentifiers; + private Map repositories; + + private boolean requireCompilerPlugins = false; + + public List getDependencyIdentifiers() { + return dependencyIdentifiers; + } + + public void setDependencyIdentifiers(List dependencyIdentifiers) { + this.dependencyIdentifiers = dependencyIdentifiers; + } + + public String getParentIdentifier() { + return parentIdentifier; + } + + public Pom setParentIdentifier(String parentIdentifier) { + this.parentIdentifier = parentIdentifier; + return this; + } + + public String getParentRelativePath() { + return parentRelativePath; + } + + public Pom setParentRelativePath(String parentRelativePath) { + this.parentRelativePath = parentRelativePath; + return this; + } + + public String getParentVersion() { + return parentVersion; + } + + public Pom setParentVersion(String parentVersion) { + this.parentVersion = parentVersion; + return this; + } + + public String getGroupId() { + return groupId; + } + + public Pom setGroupId(String groupId) { + this.groupId = groupId; + return this; + } + + public String getArtifactId() { + return artifactId; + } + + public Pom setArtifactId(String artifactId) { + this.artifactId = artifactId; + return this; + } + + public String getVersion() { + return version; + } + + public Pom setVersion(String version) { + this.version = version; + return this; + } + + public String getServiceName() { + return serviceName; + } + + public Pom setServiceName(String serviceName) { + this.serviceName = serviceName; + return this; + } + + public String getServiceDescription() { + return serviceDescription; + } + + public Pom setServiceDescription(String serviceDescription) { + this.serviceDescription = serviceDescription; + return this; + } + + public boolean isRequireCompilerPlugins() { + return requireCompilerPlugins; + } + + public Pom setRequireCompilerPlugins(boolean requireCompilerPlugins) { + this.requireCompilerPlugins = requireCompilerPlugins; + return this; + } + + public Map getRepositories() { + return repositories; + } + + public Pom setRepositories(Map repositories) { + this.repositories = repositories; + return this; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/PrimitiveType.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/PrimitiveType.java new file mode 100644 index 0000000000..e29be80098 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/PrimitiveType.java @@ -0,0 +1,415 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.model.clientmodel; + +import java.time.Instant; +import java.time.ZoneOffset; +import java.util.Set; +import java.util.function.Function; + +/** + * A basic type used by a client. + */ +public class PrimitiveType implements IType { + public static final PrimitiveType VOID = new Builder() + .name("void") + .nullableType(ClassType.VOID) + .build(); + + public static final PrimitiveType BOOLEAN = new Builder() + .name("boolean") + .nullableType(ClassType.BOOLEAN) + .defaultValueExpressionConverter(String::toLowerCase) + .defaultValue("false") + .jsonToken("JsonToken.BOOLEAN") + .serializationMethodBase("writeBoolean") + .jsonDeserializationMethod("getBoolean()") + .xmlAttributeDeserializationTemplate("%s.getBooleanAttribute(%s, %s)") + .xmlElementDeserializationMethod("getBooleanElement()") + .build(); + + public static final PrimitiveType BYTE = new Builder() + .name("byte") + .nullableType(ClassType.BYTE) + .defaultValueExpressionConverter(Function.identity()) + .defaultValue("0") + .jsonToken("JsonToken.NUMBER") + .serializationMethodBase("writeInt") + .jsonDeserializationMethod("getInt()") + .xmlAttributeDeserializationTemplate("%s.getIntAttribute(%s, %s)") + .xmlElementDeserializationMethod("getIntElement()") + .build(); + + public static final PrimitiveType INT = new Builder() + .name("int") + .nullableType(ClassType.INTEGER) + .defaultValueExpressionConverter(Function.identity()) + .defaultValue("0") + .jsonToken("JsonToken.NUMBER") + .serializationMethodBase("writeInt") + .jsonDeserializationMethod("getInt()") + .xmlAttributeDeserializationTemplate("%s.getIntAttribute(%s, %s)") + .xmlElementDeserializationMethod("getIntElement()") + .build(); + + public static final PrimitiveType LONG = new Builder() + .prototypeAsLong() + .build(); + + public static final PrimitiveType FLOAT = new Builder() + .name("float") + .nullableType(ClassType.FLOAT) + .defaultValueExpressionConverter(defaultValueExpression -> defaultValueExpression + "f") + .defaultValue("0.0") + .jsonToken("JsonToken.NUMBER") + .serializationMethodBase("writeFloat") + .jsonDeserializationMethod("getFloat()") + .xmlAttributeDeserializationTemplate("%s.getFloatAttribute(%s, %s)") + .xmlElementDeserializationMethod("getFloatElement()") + .build(); + + public static final PrimitiveType DOUBLE = new Builder() + .prototypeAsDouble() + .build(); + + public static final PrimitiveType CHAR = new Builder() + .name("char") + .nullableType(ClassType.CHARACTER) + .defaultValueExpressionConverter(defaultValueExpression -> Integer.toString(defaultValueExpression.charAt(0))) + .defaultValue("\u0000") + .jsonToken("JsonToken.STRING") + .serializationMethodBase("writeString") + .wrapSerializationWithObjectsToString(true) + .jsonDeserializationMethod("getString().charAt(0)") + .xmlAttributeDeserializationTemplate("%s.getStringAttribute(%s, %s).charAt(0)") + .xmlElementDeserializationMethod("getStringElement().charAt(0)") + .build(); + + public static final PrimitiveType UNIX_TIME_LONG = new Builder() + .prototypeAsLong() + .nullableType(ClassType.UNIX_TIME_LONG) + .build(); + + public static final PrimitiveType DURATION_LONG = new Builder() + .prototypeAsLong() + .nullableType(ClassType.DURATION_LONG) + .build(); + + public static final PrimitiveType DURATION_DOUBLE = new Builder() + .prototypeAsDouble() + .nullableType(ClassType.DURATION_DOUBLE) + .build(); + + /** + * The name of this type. + */ + private final String name; + /** + * The nullable version of this primitive type. + */ + private final ClassType nullableType; + private final Function defaultValueExpressionConverter; + private final String defaultValue; + private final String jsonToken; + private final String serializationMethodBase; + private final boolean wrapSerializationWithObjectsToString; + private final String jsonDeserializationMethod; + private final String xmlAttributeDeserializationTemplate; + private final String xmlElementDeserializationMethod; + + private PrimitiveType(String name, ClassType nullableType, Function defaultValueExpressionConverter, + String defaultValue, String jsonToken, String serializationMethodBase, + boolean wrapSerializationWithObjectsToString, String jsonDeserializationMethod, + String xmlAttributeDeserializationTemplate, String xmlElementDeserializationMethod) { + this.name = name; + this.nullableType = nullableType; + this.defaultValueExpressionConverter = defaultValueExpressionConverter; + this.defaultValue = defaultValue; + this.jsonToken = jsonToken; + this.serializationMethodBase = serializationMethodBase; + this.wrapSerializationWithObjectsToString = wrapSerializationWithObjectsToString; + this.jsonDeserializationMethod = jsonDeserializationMethod; + this.xmlAttributeDeserializationTemplate = xmlAttributeDeserializationTemplate; + this.xmlElementDeserializationMethod = xmlElementDeserializationMethod; + } + +// public static PrimitiveType fromNullableType(ClassType nullableType) { +// if (nullableType == ClassType.Void) { +// return PrimitiveType.Void; +// } else if (nullableType == ClassType.Boolean) { +// return PrimitiveType.Boolean; +// } else if (nullableType == ClassType.Byte) { +// return PrimitiveType.Byte; +// } else if (nullableType == ClassType.Integer) { +// return PrimitiveType.Int; +// } else if (nullableType == ClassType.Long) { +// return PrimitiveType.Long; +// } else if (nullableType == ClassType.Double) { +// return PrimitiveType.Double; +// } else if (nullableType == ClassType.Float) { +// return PrimitiveType.Float; +// } else { +// throw new IllegalArgumentException("Class type " + nullableType + " is not a boxed type"); +// } +// } + + public final String getName() { + return name; + } + + private ClassType getNullableType() { + return nullableType; + } + + @Override + public final void addImportsTo(Set imports, boolean includeImplementationImports) { + if (this == PrimitiveType.UNIX_TIME_LONG) { + imports.add(Instant.class.getName()); + imports.add(ZoneOffset.class.getName()); + } + } + + @Override + public final boolean isNullable() { + return false; + } + + @Override + public final IType asNullable() { + return getNullableType(); + } + + @Override + public final boolean contains(IType type) { + return this == type; + } + + private Function getDefaultValueExpressionConverter() { + return defaultValueExpressionConverter; + } + + @Override + public final String defaultValueExpression(String sourceExpression) { + String result = sourceExpression; + if (result != null && getDefaultValueExpressionConverter() != null) { + result = defaultValueExpressionConverter.apply(sourceExpression); + } + return result; + } + + @Override + public final String defaultValueExpression() { + return defaultValueExpression(defaultValue); + } + + @Override + public final IType getClientType() { + IType clientType = this; + if (this == PrimitiveType.UNIX_TIME_LONG) { + clientType = ClassType.UNIX_TIME_DATE_TIME; + } else if (this == PrimitiveType.DURATION_LONG) { + clientType = ClassType.DURATION; + } else if (this == PrimitiveType.DURATION_DOUBLE) { + clientType = ClassType.DURATION; + } + return clientType; + } + + @Override + public final String convertToClientType(String expression) { + if (getClientType() == this) { + return expression; + } + + if (this == PrimitiveType.UNIX_TIME_LONG) { + expression = String.format("OffsetDateTime.ofInstant(Instant.ofEpochSecond(%1$s), ZoneOffset.UTC)", expression); + } else if (this == PrimitiveType.DURATION_LONG) { + expression = String.format("Duration.ofSeconds(%s)", expression); + } else if (this == PrimitiveType.DURATION_DOUBLE) { + expression = String.format("Duration.ofNanos((long) (%s * 1000_000_000L))", expression); + } + return expression; + } + + @Override + public final String convertFromClientType(String expression) { + if (getClientType() == this) { + return expression; + } + + if (this == PrimitiveType.UNIX_TIME_LONG) { + expression = String.format("%1$s.toEpochSecond()", expression); + } else if (this == PrimitiveType.DURATION_LONG) { + expression = String.format("%s.getSeconds()", expression); + } else if (this == PrimitiveType.DURATION_DOUBLE) { + expression = String.format("(double) %s.toNanos() / 1000_000_000L", expression); + } + return expression; + } + + @Override + public final String validate(String expression) { + return null; + } + + @Override + public String jsonToken() { + return jsonToken; + } + + @Override + public String jsonDeserializationMethod(String jsonReaderName) { + if (jsonDeserializationMethod == null) { + return null; + } + + return jsonReaderName + "." + jsonDeserializationMethod; + } + + @Override + public String jsonSerializationMethodCall(String jsonWriterName, String fieldName, String valueGetter, + boolean jsonMergePatch) { + if (wrapSerializationWithObjectsToString) { + return fieldName == null + ? String.format("%s.%s(Objects.toString(%s, null))", jsonWriterName, serializationMethodBase, valueGetter) + : String.format("%s.%sField(\"%s\", Objects.toString(%s, null))", jsonWriterName, + serializationMethodBase, fieldName, valueGetter); + } + + return fieldName == null + ? String.format("%s.%s(%s)", jsonWriterName, serializationMethodBase, valueGetter) + : String.format("%s.%sField(\"%s\", %s)", jsonWriterName, serializationMethodBase, fieldName, valueGetter); + } + + @Override + public String xmlDeserializationMethod(String xmlReaderName, String attributeName, String attributeNamespace, + boolean namespaceIsConstant) { + if (attributeName == null) { + return xmlReaderName + "." + xmlElementDeserializationMethod; + } else if (attributeNamespace == null) { + return String.format(xmlAttributeDeserializationTemplate, xmlReaderName, "null", + "\"" + attributeName + "\""); + } else { + String namespace = namespaceIsConstant ? attributeNamespace : "\"" + attributeNamespace + "\""; + return String.format(xmlAttributeDeserializationTemplate, xmlReaderName, namespace, + "\"" + attributeName + "\""); + } + } + + @Override + public String xmlSerializationMethodCall(String xmlWriterName, String attributeOrElementName, String namespaceUri, + String valueGetter, boolean isAttribute, boolean nameIsVariable, boolean namespaceIsConstant) { + String value = wrapSerializationWithObjectsToString + ? "Objects.toString(" + valueGetter + ", null)" : valueGetter; + + return ClassType.xmlSerializationCallHelper(xmlWriterName, serializationMethodBase, attributeOrElementName, + namespaceUri, value, isAttribute, nameIsVariable, namespaceIsConstant); + } + + @Override + public boolean isUsedInXml() { + return false; + } + + @Override + public String toString() { + return getName(); + } + + private static class Builder { + + private String name; + private ClassType nullableType; + private Function defaultValueExpressionConverter; + private String defaultValue; + private String jsonToken; + private String serializationMethodBase; + private boolean wrapSerializationWithObjectsToString = false; + private String jsonDeserializationMethod; + private String xmlAttributeDeserializationTemplate; + private String xmlElementDeserializationMethod; + + public Builder name(String name) { + this.name = name; + return this; + } + + public Builder prototypeAsLong() { + return this.name("long") + .nullableType(ClassType.LONG) + .defaultValueExpressionConverter(defaultValueExpression -> defaultValueExpression + 'L') + .defaultValue("0") + .jsonToken("JsonToken.NUMBER") + .serializationMethodBase("writeLong") + .wrapSerializationWithObjectsToString(false) + .jsonDeserializationMethod("getLong()") + .xmlAttributeDeserializationTemplate("%s.getLongAttribute(%s, %s)") + .xmlElementDeserializationMethod("getLongElement()"); + } + + public Builder prototypeAsDouble() { + return this.name("double") + .nullableType(ClassType.DOUBLE) + .defaultValueExpressionConverter(defaultValueExpression -> java.lang.Double.toString(java.lang.Double.parseDouble(defaultValueExpression))) + .defaultValue("0.0") + .jsonToken("JsonToken.NUMBER") + .serializationMethodBase("writeDouble") + .wrapSerializationWithObjectsToString(false) + .jsonDeserializationMethod("getDouble()") + .xmlAttributeDeserializationTemplate("%s.getDoubleAttribute(%s, %s)") + .xmlElementDeserializationMethod("getDoubleElement()"); + } + + public Builder nullableType(ClassType nullableType) { + this.nullableType = nullableType; + return this; + } + + public Builder defaultValue(String defaultValue) { + this.defaultValue = defaultValue; + return this; + } + + public Builder defaultValueExpressionConverter(java.util.function.Function defaultValueExpressionConverter) { + this.defaultValueExpressionConverter = defaultValueExpressionConverter; + return this; + } + + public Builder wrapSerializationWithObjectsToString(boolean wrapSerializationWithObjectsToString) { + this.wrapSerializationWithObjectsToString = wrapSerializationWithObjectsToString; + return this; + } + + public Builder jsonToken(String jsonToken) { + this.jsonToken = jsonToken; + return this; + } + + public Builder jsonDeserializationMethod(String jsonDeserializationMethod) { + this.jsonDeserializationMethod = jsonDeserializationMethod; + return this; + } + + public Builder serializationMethodBase(String serializationMethodBase) { + this.serializationMethodBase = serializationMethodBase; + return this; + } + + public Builder xmlAttributeDeserializationTemplate(String xmlAttributeDeserializationTemplate) { + this.xmlAttributeDeserializationTemplate = xmlAttributeDeserializationTemplate; + return this; + } + + public Builder xmlElementDeserializationMethod(String xmlElementDeserializationMethod) { + this.xmlElementDeserializationMethod = xmlElementDeserializationMethod; + return this; + } + + public PrimitiveType build() { + return new PrimitiveType(name, nullableType, defaultValueExpressionConverter, defaultValue, jsonToken, + serializationMethodBase, wrapSerializationWithObjectsToString, jsonDeserializationMethod, + xmlAttributeDeserializationTemplate, xmlElementDeserializationMethod); + } + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/ProtocolExample.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/ProtocolExample.java new file mode 100644 index 0000000000..7b6ab753c1 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/ProtocolExample.java @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.model.clientmodel; + +public class ProtocolExample { + + private final ClientMethod clientMethod; + + private final AsyncSyncClient syncClient; + + private final ClientBuilder clientBuilder; + + private final String filename; + + private final ProxyMethodExample proxyMethodExample; + + public ProtocolExample( + ClientMethod clientMethod, + AsyncSyncClient client, + ClientBuilder clientBuilder, + String filename, + ProxyMethodExample proxyMethodExample) { + this.clientMethod = clientMethod; + this.syncClient = client; + this.clientBuilder = clientBuilder; + this.filename = filename; + this.proxyMethodExample = proxyMethodExample; + } + + public ClientMethod getClientMethod() { + return clientMethod; + } + + public AsyncSyncClient getSyncClient() { + return syncClient; + } + + public ClientBuilder getClientBuilder() { + return clientBuilder; + } + + public String getFilename() { + return filename; + } + + public ProxyMethodExample getProxyMethodExample() { + return proxyMethodExample; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/Proxy.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/Proxy.java new file mode 100644 index 0000000000..627e2cc448 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/Proxy.java @@ -0,0 +1,129 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.model.clientmodel; + +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; + +import java.util.List; +import java.util.Set; + + +/** + * Details that describe the dynamic proxy. + */ +public class Proxy { + /** + * Get the name of the REST API interface. + */ + private String name; + /** + * Get the name of the method group. + */ + private String clientTypeName; + /** + * Get the base URL that will be used for each REST API method. + */ + private String baseURL; + /** + * Get the methods of this REST API. + */ + private List methods; + + /** + * Create a new Proxy using the provided properties. + * @param name The name of the REST API interface. + * @param clientTypeName The name of the method group. + * @param baseURL The base URL that will be used for each REST API method. + * @param methods The methods of this REST API. + */ + protected Proxy(String name, String clientTypeName, String baseURL, List methods) { + this.name = name; + this.clientTypeName = clientTypeName; + this.baseURL = baseURL; + this.methods = methods; + } + + public final String getName() { + return name; + } + + public final String getClientTypeName() { + return clientTypeName; + } + + public final String getBaseURL() { + return baseURL; + } + + public final List getMethods() { + return methods; + } + + /** + * Add this property's imports to the provided set of imports. + * @param imports The set of imports to add to. + * @param includeImplementationImports Whether to include imports that are only necessary for method implementations. + */ + public void addImportsTo(Set imports, boolean includeImplementationImports, JavaSettings settings) { + if (includeImplementationImports) { + Annotation.HOST.addImportsTo(imports); + Annotation.SERVICE_INTERFACE.addImportsTo(imports); + } + + for (ProxyMethod method : getMethods()) { + method.addImportsTo(imports, includeImplementationImports, settings); + } + } + + public static class Builder { + protected String name; + protected String clientTypeName; + protected String baseURL; + protected List methods; + + /** + * Sets the name of the REST API interface. + * @param name the name of the REST API interface + * @return the Builder itself + */ + public Builder name(String name) { + this.name = name; + return this; + } + /** + * Sets the name of the method group. + * @param clientTypeName the name of the method group + * @return the Builder itself + */ + public Builder clientTypeName(String clientTypeName) { + this.clientTypeName = clientTypeName; + return this; + } + /** + * Sets the base URL that will be used for each REST API method. + * @param baseURL the base URL that will be used for each REST API method + * @return the Builder itself + */ + public Builder baseURL(String baseURL) { + this.baseURL = baseURL; + return this; + } + /** + * Sets the methods of this REST API. + * @param methods the methods of this REST API + * @return the Builder itself + */ + public Builder methods(List methods) { + this.methods = methods; + return this; + } + + public Proxy build() { + return new Proxy(name, + clientTypeName, + baseURL, + methods); + } + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/ProxyMethod.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/ProxyMethod.java new file mode 100644 index 0000000000..e3df3fcb5f --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/ProxyMethod.java @@ -0,0 +1,724 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.model.clientmodel; + +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; +import com.microsoft.typespec.http.client.generator.core.extension.base.util.HttpExceptionType; +import com.microsoft.typespec.http.client.generator.core.util.CodeNamer; +import com.microsoft.typespec.http.client.generator.core.util.MethodNamer; +import com.azure.core.http.ContentType; +import com.azure.core.http.HttpMethod; + +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * A method within a Proxy. + */ +public class ProxyMethod { + /** + * Get the Content-Type of the request. + */ + private final String requestContentType; + /** + * The value that is returned from this method. + */ + protected IType returnType; + /** + * Get the HTTP method that will be used for this method. + */ + private final HttpMethod httpMethod; + /** + * Get the base URL that will be used for each REST API method. + */ + private final String baseUrl; + /** + * Get the path of this method's request URL. + */ + private final String urlPath; + /** + * Get the status codes that are expected in the response. + */ + private final List responseExpectedStatusCodes; + + private final Map> unexpectedResponseExceptionTypes; + /** + * Get the exception type to throw if this method receives and unexpected response status code. + */ + private final ClassType unexpectedResponseExceptionType; + /** + * Get the name of this Rest API method. + */ + private final String name; + + /** + * Get the base name of this Rest API method. + */ + private final String baseName; + + /** + * Get the parameters that are provided to this method. + */ + protected List parameters; + /** + * Get all parameters defined in swagger to this method. + */ + protected List allParameters; + /** + * Get the description of this method. + */ + private final String description; + /** + * The value of the ReturnValueWireType annotation for this method. + */ + protected IType returnValueWireType; + /** + * The response body type. + */ + private final IType responseBodyType; + /** + * The raw response body type. responseBodyType is set to BinaryData in low-level mode. We need raw type. + */ + private final IType rawResponseBodyType; + /** + * Get whether this method resumes polling of an LRO. + */ + private final boolean isResumable; + /** + * The media-types in response. + */ + private final Set responseContentTypes; + + private final Map examples; + + private final List specialHeaders; + + private final String operationId; + + private final boolean isSync; + private ProxyMethod syncProxy; + private final boolean customHeaderIgnored; + + protected ProxyMethod(String requestContentType, IType returnType, HttpMethod httpMethod, String baseUrl, + String urlPath, List responseExpectedStatusCodes, ClassType unexpectedResponseExceptionType, + Map> unexpectedResponseExceptionTypes, String name, + List parameters, List allParameters, String description, + IType returnValueWireType, IType responseBodyType, IType rawResponseBodyType, boolean isResumable, + Set responseContentTypes, String operationId, Map examples, + List specialHeaders) { + this(requestContentType, returnType, httpMethod, baseUrl, urlPath, responseExpectedStatusCodes, + unexpectedResponseExceptionType, unexpectedResponseExceptionTypes, name, parameters, allParameters, + description, returnValueWireType, responseBodyType, rawResponseBodyType, isResumable, responseContentTypes, + operationId, examples, specialHeaders, false, name, false); + } + + /** + * Create a new RestAPIMethod with the provided properties. + * + * @param requestContentType The Content-Type of the request. + * @param returnType The type of value that is returned from this method. + * @param httpMethod The HTTP method that will be used for this method. + * @param baseUrl The base URL that will be used for each REST API method. + * @param urlPath The path of this method's request URL. + * @param responseExpectedStatusCodes The status codes that are expected in the response. + * @param returnValueWireType The return value's type as it is received from the network (across the wire). + * @param unexpectedResponseExceptionType The exception type to throw if this method receives and unexpected + * response status code. + * @param name The name of this REST API method. + * @param parameters The parameters that are provided to this method. + * @param description The description of this method. + * @param isResumable Whether this method is resumable. + * @param responseContentTypes The media-types in response. + * @param operationId the operation ID + * @param examples the examples for the method. + * @param specialHeaders list of special headers + * @param isSync indicates if this proxy method is a synchronous method. + * @param baseName the base name of the REST method. + */ + protected ProxyMethod(String requestContentType, IType returnType, HttpMethod httpMethod, String baseUrl, + String urlPath, List responseExpectedStatusCodes, ClassType unexpectedResponseExceptionType, + Map> unexpectedResponseExceptionTypes, String name, + List parameters, List allParameters, String description, + IType returnValueWireType, IType responseBodyType, IType rawResponseBodyType, boolean isResumable, + Set responseContentTypes, String operationId, Map examples, + List specialHeaders, boolean isSync, String baseName, boolean customHeaderIgnored) { + this.requestContentType = requestContentType; + this.returnType = returnType; + this.httpMethod = httpMethod; + this.baseUrl = baseUrl; + this.urlPath = urlPath; + this.responseExpectedStatusCodes = responseExpectedStatusCodes; + this.unexpectedResponseExceptionType = unexpectedResponseExceptionType; + this.unexpectedResponseExceptionTypes = unexpectedResponseExceptionTypes; + this.name = name; + this.parameters = parameters; + this.allParameters = allParameters; + this.description = description; + this.returnValueWireType = returnValueWireType; + this.responseBodyType = responseBodyType; + this.rawResponseBodyType = rawResponseBodyType; + this.isResumable = isResumable; + this.responseContentTypes = responseContentTypes; + this.operationId = operationId; + this.examples = examples; + this.specialHeaders = specialHeaders; + this.isSync = isSync; + this.baseName = baseName; + this.customHeaderIgnored = customHeaderIgnored; + } + + public final String getRequestContentType() { + return requestContentType; + } + + public final IType getReturnType() { + return returnType; + } + + public final HttpMethod getHttpMethod() { + return httpMethod; + } + + public final String getBaseUrl() { + return baseUrl; + } + + public final String getUrlPath() { + return urlPath; + } + + public final List getResponseExpectedStatusCodes() { + return responseExpectedStatusCodes; + } + + public final ClassType getUnexpectedResponseExceptionType() { + return unexpectedResponseExceptionType; + } + + public final Map> getUnexpectedResponseExceptionTypes() { + return unexpectedResponseExceptionTypes; + } + + public final String getName() { + return name; + } + + public final String getBaseName() { + return baseName == null ? name : baseName; + } + + public final List getParameters() { + return parameters; + } + + public final List getAllParameters() { + return allParameters; + } + + public final String getDescription() { + return description; + } + + public final IType getReturnValueWireType() { + return returnValueWireType; + } + + public IType getResponseBodyType() { + return responseBodyType; + } + + public IType getRawResponseBodyType() { + return rawResponseBodyType; + } + + public final boolean isResumable() { + return isResumable; + } + + public final String getPagingAsyncSinglePageMethodName() { + return MethodNamer.getPagingAsyncSinglePageMethodName(getName()); + } + + public final String getPagingSinglePageMethodName() { + return MethodNamer.getPagingSinglePageMethodName(getBaseName()); + } + + public final String getSimpleAsyncMethodName() { + return MethodNamer.getSimpleAsyncMethodName(getName()); + } + + public final String getSimpleAsyncRestResponseMethodName() { + return MethodNamer.getSimpleAsyncRestResponseMethodName(getName()); + } + + public final String getSimpleRestResponseMethodName() { + return MethodNamer.getSimpleRestResponseMethodName(getBaseName()); + } + + public final Set getResponseContentTypes() { + return responseContentTypes; + } + + public String getOperationId() { + return operationId; + } + + public Map getExamples() { + return examples; + } + + public List getSpecialHeaders() { + return specialHeaders; + } + + public boolean isSync() { + return isSync; + } + + public boolean isCustomHeaderIgnored() { + return customHeaderIgnored; + } + + public ProxyMethod toSync() { + if (isSync) { + return this; + } + + if (this.syncProxy != null) { + return syncProxy; + } + + List syncParams = this.getParameters() + .stream() + .map(this::mapToSyncParam) + .collect(Collectors.toList()); + + List allSyncParams = this.getAllParameters() + .stream() + .map(this::mapToSyncParam) + .collect(Collectors.toList()); + + this.syncProxy = new ProxyMethod.Builder().parameters(syncParams) + .httpMethod(this.getHttpMethod()) + .name(this.getName() + "Sync") + .baseName(this.getName()) + .description(this.getDescription()) + .baseURL(this.getBaseUrl()) + .operationId(this.getOperationId()) + .isResumable(this.isResumable()) + .examples(this.getExamples()) + .rawResponseBodyType(mapToSyncType(this.getRawResponseBodyType())) + .requestContentType(this.getRequestContentType()) + .responseBodyType(mapToSyncType(this.getResponseBodyType())) + .returnType(mapToSyncType(this.getReturnType())) + .returnValueWireType(mapToSyncType(this.getReturnValueWireType())) + .urlPath(this.getUrlPath()) + .specialHeaders(this.getSpecialHeaders()) + .unexpectedResponseExceptionType(this.getUnexpectedResponseExceptionType()) + .unexpectedResponseExceptionTypes(this.getUnexpectedResponseExceptionTypes()) + .allParameters(allSyncParams) + .responseContentTypes(this.getResponseContentTypes()) + .responseExpectedStatusCodes(this.getResponseExpectedStatusCodes()) + .isSync(true) + .customHeaderIgnored(this.customHeaderIgnored) + .build(); + return this.syncProxy; + } + + private ProxyMethodParameter mapToSyncParam(ProxyMethodParameter param) { + return param.newBuilder() + .clientType(mapToSyncType(param.getClientType())) + .rawType(mapToSyncType(param.getRawType())) + .wireType(mapToSyncType(param.getWireType())) + .build(); + } + + private IType mapToSyncType(IType type) { + if (type == GenericType.FLUX_BYTE_BUFFER) { + return ClassType.BINARY_DATA; + } + + if (type instanceof GenericType) { + GenericType genericType = (GenericType) type; + if (genericType.getName().equals("Mono")) { + if (genericType.getTypeArguments()[0] instanceof GenericType) { + GenericType innerGenericType = (GenericType) genericType.getTypeArguments()[0]; + if (innerGenericType.getName().equals("ResponseBase") + && innerGenericType.getTypeArguments()[1] == GenericType.FLUX_BYTE_BUFFER) { + return GenericType.RestResponse(innerGenericType.getTypeArguments()[0], + JavaSettings.getInstance().isInputStreamForBinary() + ? ClassType.INPUT_STREAM + : ClassType.BINARY_DATA); + } + } + + if (genericType.getTypeArguments()[0] == ClassType.STREAM_RESPONSE) { + return JavaSettings.getInstance().isInputStreamForBinary() ? GenericType.Response( + ClassType.INPUT_STREAM) : GenericType.Response(ClassType.BINARY_DATA); + } + return genericType.getTypeArguments()[0]; + } + if (genericType.getName().equals("PagedFlux")) { + IType pageType = genericType.getTypeArguments()[0]; + return GenericType.PagedIterable(pageType); + } + if (genericType.getName().equals("PollerFlux")) { + IType[] typeArguments = genericType.getTypeArguments(); + IType pollType = typeArguments[0]; + IType resultType = typeArguments[1]; + return GenericType.SyncPoller(pollType, resultType); + } + } + return type; + } + + /** + * Add this property's imports to the provided set of imports. + * + * @param imports The set of imports to add to. + * @param includeImplementationImports Whether to include imports that are only necessary for method + * implementations. + */ + public void addImportsTo(Set imports, boolean includeImplementationImports, JavaSettings settings) { + Annotation.HTTP_REQUEST_INFORMATION.addImportsTo(imports); + Annotation.UNEXPECTED_RESPONSE_EXCEPTION_INFORMATION.addImportsTo(imports); + if (includeImplementationImports) { + if (getUnexpectedResponseExceptionType() != null) { + Annotation.UNEXPECTED_RESPONSE_EXCEPTION_TYPE.addImportsTo(imports); + getUnexpectedResponseExceptionType().addImportsTo(imports, includeImplementationImports); + } + if (getUnexpectedResponseExceptionTypes() != null) { + Annotation.UNEXPECTED_RESPONSE_EXCEPTION_TYPE.addImportsTo(imports); + getUnexpectedResponseExceptionTypes().keySet() + .forEach(e -> e.addImportsTo(imports, includeImplementationImports)); + } + if (isResumable()) { + imports.add("com.azure.core.annotation.ResumeOperation"); + } + imports.add(String.format("%1$s.annotation.%2$s", ExternalPackage.CORE.getPackageName(), + CodeNamer.toPascalCase(getHttpMethod().toString().toLowerCase()))); + + if (settings.isFluent()) { + Annotation.HEADERS.addImportsTo(imports); + } + Annotation.EXPECTED_RESPONSE.addImportsTo(imports); + + if (getReturnValueWireType() != null) { + Annotation.RETURN_VALUE_WIRE_TYPE.addImportsTo(imports); + returnValueWireType.addImportsTo(imports, includeImplementationImports); + } + + returnType.addImportsTo(imports, includeImplementationImports); + + if (ContentType.APPLICATION_X_WWW_FORM_URLENCODED.equals(this.requestContentType)) { + Annotation.FORM_PARAM.addImportsTo(imports); + } + + for (ProxyMethodParameter parameter : parameters) { + parameter.addImportsTo(imports, includeImplementationImports, settings); + } + } + } + + public HttpExceptionType getHttpExceptionType(ClassType classType) { + if (classType == null) { + return null; + } + + if (Objects.equals(ClassType.CLIENT_AUTHENTICATION_EXCEPTION, classType)) { + return HttpExceptionType.CLIENT_AUTHENTICATION; + } else if (Objects.equals(ClassType.RESOURCE_EXISTS_EXCEPTION, classType)) { + return HttpExceptionType.RESOURCE_EXISTS; + } else if (Objects.equals(ClassType.RESOURCE_NOT_FOUND_EXCEPTION, classType)) { + return HttpExceptionType.RESOURCE_NOT_FOUND; + } else if (Objects.equals(ClassType.RESOURCE_MODIFIED_EXCEPTION, classType)) { + return HttpExceptionType.RESOURCE_MODIFIED; + } + + return null; + } + + public static class Builder { + protected String requestContentType; + protected IType returnType; + protected HttpMethod httpMethod; + protected String baseUrl; + protected String urlPath; + protected List responseExpectedStatusCodes; + protected ClassType unexpectedResponseExceptionType; + protected Map> unexpectedResponseExceptionTypes; + protected String name; + protected List parameters; + protected List allParameters; + protected String description; + protected IType returnValueWireType; + protected IType responseBodyType; + protected IType rawResponseBodyType; + protected boolean isResumable; + protected Set responseContentTypes; + protected Map examples; + protected String operationId; + protected List specialHeaders; + protected boolean isSync; + protected String baseName; + protected boolean customHeaderIgnored; + + /* + * Sets the Content-Type of the request. + * @param requestContentType the Content-Type of the request + * @return the Builder itself + */ + public Builder requestContentType(String requestContentType) { + this.requestContentType = requestContentType; + return this; + } + + /** + * Sets the value that is returned from this method. + * + * @param returnType the value that is returned from this method + * @return the Builder itself + */ + public Builder returnType(IType returnType) { + this.returnType = returnType; + return this; + } + + /** + * Sets the HTTP method that will be used for this method. + * + * @param httpMethod the HTTP method that will be used for this method + * @return the Builder itself + */ + public Builder httpMethod(HttpMethod httpMethod) { + this.httpMethod = httpMethod; + return this; + } + + /** + * Sets the base URL that will be used for each REST API method. + * + * @param baseUrl the base URL that will be used for each REST API method + * @return the Builder itself + */ + public Builder baseURL(String baseUrl) { + this.baseUrl = baseUrl; + return this; + } + + /** + * Sets the path of this method's request URL. + * + * @param urlPath the path of this method's request URL + * @return the Builder itself + */ + public Builder urlPath(String urlPath) { + this.urlPath = urlPath; + return this; + } + + /** + * Sets the status codes that are expected in the response. + * + * @param responseExpectedStatusCodes the status codes that are expected in the response + * @return the Builder itself + */ + public Builder responseExpectedStatusCodes(List responseExpectedStatusCodes) { + this.responseExpectedStatusCodes = responseExpectedStatusCodes; + return this; + } + + /** + * Sets the exception type to throw if this method receives any unexpected response status code. + * + * @param unexpectedResponseExceptionType the exception type to throw if this method receives any unexpected + * response status code + * @return the Builder itself + */ + public Builder unexpectedResponseExceptionType(ClassType unexpectedResponseExceptionType) { + this.unexpectedResponseExceptionType = unexpectedResponseExceptionType; + return this; + } + + /** + * Sets the exception type to throw if this method receives certain unexpected response status code. + * + * @param unexpectedResponseExceptionTypes the exception type to throw if this method receives certain + * unexpected response status code + * @return the Builder itself + */ + public Builder unexpectedResponseExceptionTypes( + Map> unexpectedResponseExceptionTypes) { + this.unexpectedResponseExceptionTypes = unexpectedResponseExceptionTypes; + return this; + } + + /** + * Sets the name of this Rest API method. + * + * @param name the name of this Rest API method + * @return the Builder itself + */ + public Builder name(String name) { + this.name = name; + return this; + } + + /** + * Sets the base name of this Rest API method. + * + * @param baseName the name of this Rest API method + * @return the Builder itself + */ + public Builder baseName(String baseName) { + this.baseName = baseName; + return this; + } + + /** + * Sets the parameters that are provided to this method. + * + * @param parameters the parameters that are provided to this method + * @return the Builder itself + */ + public Builder parameters(List parameters) { + this.parameters = parameters; + return this; + } + + /** + * Sets all parameters defined in swagger to this method. + * + * @param allParameters the parameters that are provided to this method + * @return the Builder itself + */ + public Builder allParameters(List allParameters) { + this.allParameters = allParameters; + return this; + } + + /** + * Sets the description of this method. + * + * @param description the description of this method + * @return the Builder itself + */ + public Builder description(String description) { + this.description = description; + return this; + } + + /** + * Sets the value of the ReturnValueWireType annotation for this method. + * + * @param returnValueWireType the value of the ReturnValueWireType annotation for this method + * @return the Builder itself + */ + public Builder returnValueWireType(IType returnValueWireType) { + this.returnValueWireType = returnValueWireType; + return this; + } + + /** + * Sets the response body type. + * + * @param responseBodyType the response body type + * @return the Builder itself + */ + public Builder responseBodyType(IType responseBodyType) { + this.responseBodyType = responseBodyType; + return this; + } + + /** + * Sets the raw response body type. + * + * @param rawResponseBodyType the response body type + * @return the Builder itself + */ + public Builder rawResponseBodyType(IType rawResponseBodyType) { + this.rawResponseBodyType = rawResponseBodyType; + return this; + } + + /** + * Sets whether this method resumes polling of an LRO. + * + * @param isResumable whether this method resumes polling of an LRO + * @return the Builder itself + */ + public Builder isResumable(boolean isResumable) { + this.isResumable = isResumable; + return this; + } + + /** + * Sets the media-types in response. + * + * @param responseContentTypes the media-types in response + * @return the Builder itself + */ + public Builder responseContentTypes(Set responseContentTypes) { + this.responseContentTypes = responseContentTypes; + return this; + } + + /** + * Sets the examples for the method. + * + * @param examples the examples + * @return the Builder itself + */ + public Builder examples(Map examples) { + this.examples = examples; + return this; + } + + /** + * Sets the operation ID for reference. + * + * @param operationId the operation ID + * @return the Builder itself + */ + public Builder operationId(String operationId) { + this.operationId = operationId; + return this; + } + + /** + * Sets the special headers + * + * @param specialHeaders the special headers + * @return the Builder + */ + public Builder specialHeaders(List specialHeaders) { + this.specialHeaders = specialHeaders; + return this; + } + + public Builder isSync(boolean isSync) { + this.isSync = isSync; + return this; + } + + public Builder customHeaderIgnored(boolean customHeaderIgnored) { + this.customHeaderIgnored = customHeaderIgnored; + return this; + } + + /** + * @return an immutable ProxyMethod instance with the configurations on this builder. + */ + public ProxyMethod build() { + return new ProxyMethod(requestContentType, returnType, httpMethod, baseUrl, urlPath, + responseExpectedStatusCodes, unexpectedResponseExceptionType, unexpectedResponseExceptionTypes, name, + parameters, allParameters, description, returnValueWireType, responseBodyType, rawResponseBodyType, + isResumable, responseContentTypes, operationId, examples, specialHeaders, isSync, baseName, + customHeaderIgnored); + } + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/ProxyMethodExample.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/ProxyMethodExample.java new file mode 100644 index 0000000000..63cb56b2be --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/ProxyMethodExample.java @@ -0,0 +1,379 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.model.clientmodel; + +import com.microsoft.typespec.http.client.generator.core.Javagen; +import com.microsoft.typespec.http.client.generator.core.extension.plugin.PluginLogger; +import com.azure.core.http.HttpHeaderName; +import com.azure.core.http.HttpHeaders; +import com.azure.core.util.CoreUtils; +import com.azure.json.JsonProviders; +import com.azure.json.JsonWriter; +import org.slf4j.Logger; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class ProxyMethodExample { + + private final Logger logger = new PluginLogger(Javagen.getPluginInstance(), ProxyMethodExample.class); + private static final String SLASH = "/"; + + private static String tspDirectory = null; + + public static void setTspDirectory(String tspDirectory) { + ProxyMethodExample.tspDirectory = tspDirectory; + } + + // https://azure.github.io/autorest/extensions/#x-ms-examples + // https://github.com/Azure/azure-rest-api-specs/blob/main/documentation/x-ms-examples.md + + public static class ParameterValue { + private final Object objectValue; + + public ParameterValue(Object objectValue) { + this.objectValue = objectValue; + } + + /** + * @return the object value of the parameter + */ + public Object getObjectValue() { + return objectValue; + } + + /** + * Gets the un-escaped query value. + *

+ * This is done by heuristic, and not guaranteed to be correct. + * + * @return the un-escaped query value + */ + public Object getUnescapedQueryValue() { + Object unescapedValue = objectValue; + if (objectValue instanceof String) { + unescapedValue = URLDecoder.decode((String) objectValue, StandardCharsets.UTF_8); + } + return unescapedValue; + } + + @Override + public String toString() { + return "ParameterValue{objectValue=" + getJsonString() + '}'; + } + + public String getJsonString() { + try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + JsonWriter jsonWriter = JsonProviders.createWriter(outputStream)) { + jsonWriter.writeUntyped(objectValue).flush(); + return outputStream.toString(StandardCharsets.UTF_8); + } catch (IOException e) { + return objectValue.toString(); + } + } + } + + public static class Response { + + private final int statusCode; + private final HttpHeaders httpHeaders; + private final Object body; + + @SuppressWarnings("unchecked") + public Response(int statusCode, Object response) { + this.statusCode = statusCode; + this.httpHeaders = new HttpHeaders(); + if (response instanceof Map) { + Map responseMap = (Map) response; + if (responseMap.containsKey("headers") && responseMap.get("headers") instanceof Map) { + Map headersMap = (Map) responseMap.get("headers"); + headersMap.forEach( + (header, value) -> httpHeaders.add(HttpHeaderName.fromString(header), String.valueOf(value))); + } + this.body = responseMap.getOrDefault("body", null); + } else { + this.body = null; + } + } + + /** @return the status code */ + public int getStatusCode() { + return statusCode; + } + + /** @return the http headers */ + public HttpHeaders getHttpHeaders() { + return httpHeaders; + } + + /** @return the response body */ + public Object getBody() { + return body; + } + + /** @return the response body as JSON string */ + public String getJsonBody() { + return getJson(body); + } + + /** + * @param obj the object for JSON string + * @return the object as JSON string + */ + public String getJson(Object obj) { + if (obj != null) { + try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + JsonWriter jsonWriter = JsonProviders.createWriter(outputStream)) { + jsonWriter.writeUntyped(obj).flush(); + return outputStream.toString(StandardCharsets.UTF_8); + } catch (IOException e) { + return obj.toString(); + } + } else { + return ""; + } + } + + @Override + public String toString() { + return "Response{statusCode=" + statusCode + ", httpHeaders=" + httpHeaders + ", body=" + getJsonBody() + + '}'; + } + } + + private final Map parameters = new LinkedHashMap<>(); + private final Map responses = new LinkedHashMap<>(); + private final String originalFile; + private String relativeOriginalFileName; + private String codeSnippetIdentifier; + private String name; + + /** + * @return the map of parameter name to parameter object values + */ + public Map getParameters() { + return parameters; + } + + /** + * @return the map of status code to response + */ + public Map getResponses() { + return responses; + } + + /** + * @return the primary response + */ + public Response getPrimaryResponse() { + if (responses.isEmpty()) { + return null; + } + + Response firstResponse = null; + for (Response response : responses.values()) { + if (firstResponse == null) { + firstResponse = response; + } + + if (response.statusCode / 100 == 2) { + return response; + } + } + + return firstResponse; + } + + /** + * @return value of "x-ms-original-file" extension + */ + public String getOriginalFile() { + return originalFile; + } + + /** + * Heuristically find relative path of the original file to the repository. + *

+ * For instance, + * "specification/resources/resource-manager/Microsoft.Authorization/stable/2020-09-01/examples/getDataPolicyManifest.json" + * + * @return the relative path of the original file + */ + public String getRelativeOriginalFileName() { + if (relativeOriginalFileName == null && !CoreUtils.isNullOrEmpty(this.getOriginalFile())) { + String originalFileName = this.getOriginalFile(); + try { + URL url = new URI(originalFileName).toURL(); + switch (url.getProtocol()) { + case "http": + case "https": { + String[] segments = url.getPath().split(SLASH); + if (segments.length > 3) { + // first 3 should be owner, name, branch + originalFileName = Arrays.stream(segments) + .filter(s -> !s.isEmpty()) + .skip(3) + .collect(Collectors.joining(SLASH)); + } + break; + } + + case "file": { + String relativeFileName = tspDirectory != null + ? getRelativeOriginalFileNameForTsp(url) + : getRelativeOriginalFileNameForSwagger(url); + if (relativeFileName != null) { + originalFileName = relativeFileName; + } + break; + } + + default: { + logger.error("Unknown protocol in x-ms-original-file: '{}'", originalFileName); + break; + } + } + } catch (MalformedURLException | URISyntaxException | IllegalArgumentException e) { + logger.error("Failed to parse x-ms-original-file: '{}'", originalFileName); + } + relativeOriginalFileName = originalFileName; + } + return relativeOriginalFileName; + } + + /** + * identifier of the codesnippet label from codesnippet-maven-plugin + * + * @return the identifier of the codesnippet label that wraps around the example code + * @see codesnippet-maven-plugin + */ + public String getCodeSnippetIdentifier() { + return codeSnippetIdentifier; + } + + /** @return example name */ + public String getName() { + return name; + } + + private ProxyMethodExample(String originalFile) { + this.originalFile = originalFile; + } + + static String getRelativeOriginalFileNameForTsp(URL url) { + // TypeSpec + /* + * Example: + * directory "specification/standbypool/StandbyPool.Management" + * originalFileName "file:///C:/github/azure-sdk-for-java/sdk/standbypool/azure-resourcemanager-standbypool/TempTypeSpecFiles/StandbyPool.Management/examples/2023-12-01-preview/StandbyVirtualMachinePools_Update.json" + * + * There is an overlap of "StandbyPool.Management", so that we can combine the 2 to Result: + * "specification/standbypool/StandbyPool.Management/examples/2023-12-01-preview/StandbyVirtualMachinePools_Update.json" + */ + String originalFileName = null; + String[] directorySegments = tspDirectory.split(SLASH); + String directoryLastSegment = directorySegments[directorySegments.length - 1]; + int sharedDirectorySegment = -1; + String[] segments = url.getPath().split(SLASH); + for (int i = segments.length - 1; i >= 0; --i) { + if (Objects.equals(directoryLastSegment, segments[i])) { + sharedDirectorySegment = i; + break; + } + } + if (sharedDirectorySegment >= 0) { + originalFileName = Stream.concat(Arrays.stream(directorySegments), + Arrays.stream(segments).skip(sharedDirectorySegment + 1)).collect(Collectors.joining(SLASH)); + } + return originalFileName; + } + + static String getRelativeOriginalFileNameForSwagger(URL url) { + // Swagger + /* + * The examples should be under "specification//resource-manager" + * or "specification//data-plane" + */ + String originalFileName = null; + String[] segments = url.getPath().split(SLASH); + int resourceManagerOrDataPlaneSegmentIndex = -1; + for (int i = 0; i < segments.length; ++i) { + if ("resource-manager".equals(segments[i]) || "data-plane".equals(segments[i])) { + resourceManagerOrDataPlaneSegmentIndex = i; + break; + } + } + if (resourceManagerOrDataPlaneSegmentIndex > 2) { + originalFileName = Arrays.stream(segments) + .skip(resourceManagerOrDataPlaneSegmentIndex - 2) + .collect(Collectors.joining(SLASH)); + } + return originalFileName; + } + + @Override + public String toString() { + return "ProxyMethodExample{" + "parameters=" + parameters + ", responses=" + responses + '}'; + } + + public static final class Builder { + private final Map parameters = new LinkedHashMap<>(); + private final Map responses = new LinkedHashMap<>(); + private String originalFile; + private String codeSnippetIdentifier; + private String name; + + public Builder() { + } + + public Builder parameter(String parameterName, Object parameterValue) { + if (parameterValue != null) { + this.parameters.put(parameterName, new ParameterValue(parameterValue)); + } + return this; + } + + public Builder response(Integer statusCode, Object response) { + this.responses.put(statusCode, new Response(statusCode, response)); + return this; + } + + public Builder originalFile(String originalFile) { + this.originalFile = originalFile; + return this; + } + + public Builder codeSnippetIdentifier(String identifier) { + this.codeSnippetIdentifier = identifier; + return this; + } + + public Builder name(String name) { + this.name = name; + return this; + } + + public ProxyMethodExample build() { + ProxyMethodExample proxyMethodExample = new ProxyMethodExample(originalFile); + proxyMethodExample.parameters.putAll(this.parameters); + proxyMethodExample.responses.putAll(this.responses); + proxyMethodExample.codeSnippetIdentifier = this.codeSnippetIdentifier; + proxyMethodExample.name = this.name; + return proxyMethodExample; + } + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/ProxyMethodParameter.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/ProxyMethodParameter.java new file mode 100644 index 0000000000..965bab0b89 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/ProxyMethodParameter.java @@ -0,0 +1,482 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.model.clientmodel; + +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.RequestParameterLocation; +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; +import com.microsoft.typespec.http.client.generator.core.util.CodeNamer; +import com.azure.core.util.serializer.CollectionFormat; + +import java.util.Set; + +/** + * A parameter for a ProxyMethod. + */ +public class ProxyMethodParameter extends MethodParameter { + + public static final ProxyMethodParameter REQUEST_OPTIONS_PARAMETER = new ProxyMethodParameter.Builder() + .description("The options to configure the HTTP request before HTTP client sends it.") + .wireType(ClassType.REQUEST_OPTIONS) + .clientType(ClassType.REQUEST_OPTIONS) + .name("requestOptions") + .requestParameterLocation(RequestParameterLocation.NONE) + .requestParameterName("requestOptions") + .alreadyEncoded(true) + .constant(false) + .required(false) + .nullable(false) + .fromClient(false) + .parameterReference("requestOptions") + .origin(ParameterSynthesizedOrigin.REQUEST_OPTIONS) + .build(); + + /** + * Get the name of this parameter when it is serialized. + */ + private final String requestParameterName; + /** + * Whether the value of this parameter will already be encoded (and can therefore be skipped when other + * parameters' values are being encoded. + */ + private final boolean alreadyEncoded; + /** + * Whether this parameter is nullable. + */ + private final boolean isNullable; + /** + * The x-ms-header-collection-prefix extension value. + */ + private final String headerCollectionPrefix; + /** + * The reference to this parameter from a caller. + */ + private final String parameterReference; + /** + * The collection format if the parameter is a list type. + */ + private final CollectionFormat collectionFormat; + /** + * The explode if the parameter is a list type. + */ + private final boolean explode; + + private final ParameterSynthesizedOrigin origin; + + /** + * Create a new RestAPIParameter based on the provided properties. + * + * @param description The description of this parameter. + * @param rawType The raw type of this parameter. Result of SchemaMapper. + * @param wireType The type of this parameter. + * @param clientType The type of this parameter users interact with. + * @param name The name of this parameter when it is used as a variable. + * @param requestParameterLocation The location within the REST API method's HttpRequest where this parameter will + * be added. + * @param requestParameterName The name of the HttpRequest's parameter to substitute with this parameter's value. + * @param alreadyEncoded Whether the value of this parameter will already be encoded (and can therefore be + * skipped when other parameters' values are being encoded. + * @param isConstant Whether this parameter is a constant value. + * @param isRequired Whether this parameter is required. + * @param isNullable Whether this parameter is nullable. + * @param fromClient Whether this parameter's value comes from a ServiceClientProperty. + * @param headerCollectionPrefix The x-ms-header-collection-prefix extension value. + * @param parameterReference The reference to this parameter from a caller. + * @param defaultValue The default value of the parameter. + * @param collectionFormat The collection format if the parameter is a list type. + * @param explode Whether arrays and objects should generate separate parameters for each array item or object + * property. + */ + protected ProxyMethodParameter(String description, IType rawType, IType wireType, IType clientType, String name, + RequestParameterLocation requestParameterLocation, String requestParameterName, boolean alreadyEncoded, + boolean isConstant, boolean isRequired, boolean isNullable, boolean fromClient, String headerCollectionPrefix, + String parameterReference, String defaultValue, CollectionFormat collectionFormat, boolean explode, + ParameterSynthesizedOrigin origin) { + super(description, wireType, rawType, clientType, name, requestParameterLocation, isConstant, isRequired, + fromClient, defaultValue); + this.requestParameterName = requestParameterName; + this.alreadyEncoded = alreadyEncoded; + this.isNullable = isNullable; + this.headerCollectionPrefix = headerCollectionPrefix; + this.parameterReference = parameterReference; + this.collectionFormat = collectionFormat; + this.explode = explode; + this.origin = origin; + } + + public final String getRequestParameterName() { + return requestParameterName; + } + + public final boolean getAlreadyEncoded() { + return alreadyEncoded; + } + + public final boolean isNullable() { + return isNullable; + } + + public final String getHeaderCollectionPrefix() { + return headerCollectionPrefix; + } + + public final String getParameterReference() { + return parameterReference; + } + + public final String getParameterReferenceConverted() { + return String.format("%1$sConverted", CodeNamer.toCamelCase(CodeNamer.removeInvalidCharacters(getParameterReference()))); + } + + public final CollectionFormat getCollectionFormat() { + return collectionFormat; + } + + public final boolean getExplode() { + return explode; + } + + public ParameterSynthesizedOrigin getOrigin() { + return origin; + } + + public final String convertFromClientType(String source, String target, boolean alwaysNull) { + return convertFromClientType(source, target, alwaysNull, false); + } + + public final String convertFromClientType(String source, String target) { + return convertFromClientType(source, target, false, false); + } + + public final String convertFromClientType(String source, String target, boolean alwaysNull, boolean alwaysNonNull) { + if (getClientType() == getWireType()) { + return String.format("%1$s %2$s = %3$s;", getWireType(), target, source); + } + if (alwaysNull) { + return String.format("%1$s %2$s = null;", getWireType(), target); + } + if (isRequired() || alwaysNonNull) { + return String.format("%1$s %2$s = %3$s;", getWireType(), target, getWireType().convertFromClientType(source)); + } else { + return String.format("%1$s %2$s = %3$s == null ? null : %4$s;", getWireType(), target, source, getWireType().convertFromClientType(source)); + } + } + + /** + * Add this property's imports to the provided set of imports. + * + * @param imports The set of imports to add to. + * @param includeImplementationImports Whether to include imports that are only necessary for method + * implementations. + */ + public void addImportsTo(Set imports, boolean includeImplementationImports, JavaSettings settings) { + if (getRequestParameterLocation() != RequestParameterLocation.NONE/* && getRequestParameterLocation() != RequestParameterLocation.FormData*/) { + if (settings.isBranded()) { + imports.add(String.format("%1$s.annotation.%2$sParam", + ExternalPackage.CORE.getPackageName(), + CodeNamer.toPascalCase(getRequestParameterLocation().toString()))); + } else { + imports.add(String.format("%1$s.http.annotation.%2$sParam", + ExternalPackage.CORE.getPackageName(), + CodeNamer.toPascalCase(getRequestParameterLocation().toString()))); + } + } + if (getRequestParameterLocation() != RequestParameterLocation.BODY) { + if (getClientType() == ArrayType.BYTE_ARRAY) { + imports.add("com.azure.core.util.Base64Util"); + } else if (getClientType() instanceof ListType && !getExplode()) { + imports.add("com.azure.core.util.serializer.CollectionFormat"); + imports.add("com.azure.core.util.serializer.JacksonAdapter"); + } else if (getClientType() instanceof ListType && getExplode()) { + imports.add("java.util.stream.Collectors"); + } + } +// if (getRequestParameterLocation() == RequestParameterLocation.FormData) { +// imports.add(String.format("com.azure.core.annotation.FormParam")); +// } + + if (!settings.isBranded()) { + imports.add("io.clientcore.core.http.models.HttpMethod"); + } + + getWireType().addImportsTo(imports, includeImplementationImports); + } + + /** + * Creates a builder that is initialized with all the builder properties set to current values of this instance. + * + * @return A new builder instance initialized with properties values of this instance. + */ + public ProxyMethodParameter.Builder newBuilder() { + return new Builder(this); + } + + public static class Builder { + protected String description; + protected IType rawType; + protected IType wireType; + protected IType clientType; + protected String name; + protected RequestParameterLocation requestParameterLocation = RequestParameterLocation.values()[0]; + protected String requestParameterName; + protected boolean alreadyEncoded = false; + protected boolean isConstant = false; + protected boolean isRequired; + protected boolean isNullable; + protected boolean fromClient; + protected String headerCollectionPrefix; + protected String parameterReference; + protected String defaultValue; + protected CollectionFormat collectionFormat; + protected boolean explode; + protected ParameterSynthesizedOrigin origin; + + /** + * Sets the description of this parameter. + * + * @param description the description of this parameter + * @return the Builder itself + */ + public Builder description(String description) { + this.description = description; + return this; + } + + /** + * Sets the raw type of this parameter. Result of SchemaMapper. + * + * @param rawType the raw type of this parameter + * @return the Builder itself + */ + public Builder rawType(IType rawType) { + this.rawType = rawType; + return this; + } + + /** + * Sets the type of this parameter. + * + * @param wireType the type of this parameter + * @return the Builder itself + */ + public Builder wireType(IType wireType) { + this.wireType = wireType; + return this; + } + + /** + * Sets the type of this parameter. + * + * @param clientType the type of this parameter + * @return the Builder itself + */ + public Builder clientType(IType clientType) { + this.clientType = clientType; + return this; + } + + /** + * Sets the name of this parameter when it is used as a variable. + * + * @param name the name of this parameter when it is used as a variable + * @return the Builder itself + */ + public Builder name(String name) { + this.name = name; + return this; + } + + /** + * Sets the location within the REST API method's URL where this parameter will be added. + * + * @param requestParameterLocation the location within the REST API method's URL where this parameter will be + * added + * @return the Builder itself + */ + public Builder requestParameterLocation(RequestParameterLocation requestParameterLocation) { + this.requestParameterLocation = requestParameterLocation; + return this; + } + + /** + * Sets the name of this parameter when it is serialized. + * + * @param requestParameterName the name of this parameter when it is serialized + * @return the Builder itself + */ + public Builder requestParameterName(String requestParameterName) { + this.requestParameterName = requestParameterName; + return this; + } + + /** + * Sets whether or not the value of this parameter will already be encoded (and can therefore be skipped when + * other parameters' values are being encoded. + * + * @param alreadyEncoded whether or not the value of this parameter will already be encoded + * @return the Builder itself + */ + public Builder alreadyEncoded(boolean alreadyEncoded) { + this.alreadyEncoded = alreadyEncoded; + return this; + } + + /** + * Sets whether or not this parameter is a constant value. + * + * @param isConstant whether or not this parameter is a constant value + * @return the Builder itself + */ + public Builder constant(boolean isConstant) { + this.isConstant = isConstant; + return this; + } + + /** + * Sets whether or not this parameter is required. + * + * @param isRequired whether or not this parameter is required + * @return the Builder itself + */ + public Builder required(boolean isRequired) { + this.isRequired = isRequired; + return this; + } + + /** + * Sets whether or not this parameter is nullable. + * + * @param isNullable whether or not this parameter is nullable + * @return the Builder itself + */ + public Builder nullable(boolean isNullable) { + this.isNullable = isNullable; + return this; + } + + /** + * Sets whether or not this parameter's value comes from a ServiceClientProperty. + * + * @param fromClient whether or not this parameter's value comes from a ServiceClientProperty + * @return the Builder itself + */ + public Builder fromClient(boolean fromClient) { + this.fromClient = fromClient; + return this; + } + + /** + * Sets the x-ms-header-collection-prefix extension value. + * + * @param headerCollectionPrefix the x-ms-header-collection-prefix extension value + * @return the Builder itself + */ + public Builder headerCollectionPrefix(String headerCollectionPrefix) { + this.headerCollectionPrefix = headerCollectionPrefix; + return this; + } + + /** + * Sets the reference to this parameter from a caller. + * + * @param parameterReference the reference to this parameter from a caller + * @return the Builder itself + */ + public Builder parameterReference(String parameterReference) { + this.parameterReference = parameterReference; + return this; + } + + /** + * Sets the description of this parameter. + * + * @param defaultValue the description of this parameter + * @return the Builder itself + */ + public Builder defaultValue(String defaultValue) { + this.defaultValue = defaultValue; + return this; + } + + /** + * Sets the collection format if the parameter is a list type. + * + * @param collectionFormat the collection format if the parameter is a list type + * @return the Builder itself + */ + public Builder collectionFormat(CollectionFormat collectionFormat) { + this.collectionFormat = collectionFormat; + return this; + } + + /** + * Sets the explode if the parameter is a list type. + * + * @param explode the explode if the parameter is a list type + * @return the Builder itself + */ + public Builder explode(boolean explode) { + this.explode = explode; + return this; + } + + /** + * Sets origin of the parameter. + * + * @param origin the origin of the parameter. + * @return the Builder itself + */ + public Builder origin(ParameterSynthesizedOrigin origin) { + this.origin = origin; + return this; + } + + /** + * Creates a new instance of Builder. + */ + public Builder() { + } + + private Builder(ProxyMethodParameter parameter) { + this.description = parameter.getDescription(); + this.rawType = parameter.getRawType(); + this.wireType = parameter.getWireType(); + this.clientType = parameter.getClientType(); + this.name = parameter.getName(); + this.requestParameterLocation = parameter.getRequestParameterLocation(); + this.requestParameterName = parameter.getRequestParameterName(); + this.alreadyEncoded = parameter.getAlreadyEncoded(); + this.isConstant = parameter.isConstant(); + this.isRequired = parameter.isRequired(); + this.isNullable = parameter.isNullable(); + this.fromClient = parameter.isFromClient(); + this.headerCollectionPrefix = parameter.getHeaderCollectionPrefix(); + this.parameterReference = parameter.getParameterReference(); + this.defaultValue = parameter.getDefaultValue(); + this.collectionFormat = parameter.getCollectionFormat(); + this.explode = parameter.getExplode(); + this.origin = parameter.getOrigin(); + } + + public ProxyMethodParameter build() { + return new ProxyMethodParameter(description, + rawType, + wireType, + clientType, + name, + requestParameterLocation, + requestParameterName, + alreadyEncoded, + isConstant, + isRequired, + isNullable, + fromClient, + headerCollectionPrefix, + parameterReference, + defaultValue, + collectionFormat, + explode, + origin); + } + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/ReturnValue.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/ReturnValue.java new file mode 100644 index 0000000000..191be4c2ad --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/ReturnValue.java @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.model.clientmodel; + +import java.util.Set; + +/** + * A return value from a ClientMethod. + */ +public class ReturnValue { + /** + * The description of the return value. + */ + private String description; + /** + * The type of the return value. + */ + private IType type; + + /** + * Create a new ReturnValue object from the provided properties. + * @param description The description of the return value. + * @param type The type of the return value. + */ + public ReturnValue(String description, IType type) { + this.description = description; + this.type = type; + } + + public final String getDescription() { + return description; + } + + public final IType getType() { + return type; + } + + /** + * Add this return value's imports to the provided set of imports. + * @param imports The set of imports to add to. + * @param includeImplementationImports Whether to include imports that are only necessary for method implementations. + */ + public final void addImportsTo(Set imports, boolean includeImplementationImports) { + getType().addImportsTo(imports, includeImplementationImports); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/SecurityInfo.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/SecurityInfo.java new file mode 100644 index 0000000000..07d13b47e4 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/SecurityInfo.java @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.model.clientmodel; + +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Scheme; + +import java.util.HashSet; +import java.util.Set; + +public class SecurityInfo { + + private Set securityTypes = new HashSet<>(); + + private Set scopes = new HashSet<>(); + + private String headerName; + + private String headerValuePrefix; + + public Set getSecurityTypes() { + return securityTypes; + } + + public void setSecurityTypes(Set securityTypes) { + this.securityTypes = securityTypes; + } + + public Set getScopes() { + return scopes; + } + + public void setScopes(Set scopes) { + this.scopes = scopes; + } + + public String getHeaderName() { + return headerName; + } + + public void setHeaderName(String headerName) { + this.headerName = headerName; + } + + public String getHeaderValuePrefix() { + return headerValuePrefix; + } + + public void setHeaderValuePrefix(String headerValuePrefix) { + this.headerValuePrefix = headerValuePrefix; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/ServiceClient.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/ServiceClient.java new file mode 100644 index 0000000000..54cad28519 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/ServiceClient.java @@ -0,0 +1,546 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.model.clientmodel; + +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; +import com.microsoft.typespec.http.client.generator.core.util.ClientModelUtil; + +import java.util.List; +import java.util.Set; + +/** + * The details of a ServiceClient. + */ +public class ServiceClient { + private final String crossLanguageDefinitionId; + /** + * The package that this service client belongs to. + */ + private String packageName; + /** + * Get the name of this client's class. + */ + private String className; + /** + * Get the name of this client's interface. + */ + private String interfaceName; + /** + * Get the REST API that this client will send requests to. + */ + private Proxy proxy; + /** + * The MethodGroupClients that belong to this ServiceClient. + */ + private List methodGroupClients; + /** + * The properties of this ServiceClient. + */ + private List properties; + /** + * The constructors for this ServiceClient. + */ + private List constructors; + /** + * The client method overloads for this ServiceClient. + */ + private List clientMethods; + /** + * The azure environment parameter. + */ + private ClientMethodParameter azureEnvironmentParameter; + /** + * The default poll interval parameter. + */ + private ClientMethodParameter defaultPollIntervalParameter; + /** + * The credentials parameter. + */ + private ClientMethodParameter tokenCredentialParameter; + /** + * The HttpPipeline parameter. + */ + private ClientMethodParameter httpPipelineParameter; + + private ClientMethodParameter serializerAdapterParameter; + + private String clientBaseName; + + private String defaultCredentialScopes; + + private boolean builderDisabled; + private String builderPackageName; + + /** + * The security configuration information. + */ + private SecurityInfo securityInfo; + + private String baseUrl; + + private PipelinePolicyDetails pipelinePolicyDetails; + + /** + * Create a new ServiceClient with the provided properties. + * @param packageName The package that this service client belongs to. + * @param className The name of the client's class. + * @param interfaceName The name of the client's interface. + * @param proxy The REST API that the client will send requests to. + * @param methodGroupClients The MethodGroupClients that belong to this ServiceClient. + * @param properties The properties of this ServiceClient + * @param constructors The constructors for this ServiceClient. + * @param clientMethods The client method overloads for this ServiceClient. + * @param azureEnvironmentParameter The AzureEnvironment parameter. + * @param tokenCredentialParameter The credentials parameter. + * @param httpPipelineParameter The HttpPipeline parameter. + * @param serializerAdapterParameter The SerializerAdapter parameter. + * @param defaultPollIntervalParameter The default poll interval parameter. + */ + protected ServiceClient(String packageName, String className, String interfaceName, Proxy proxy, List methodGroupClients, List properties, List constructors, List clientMethods, + ClientMethodParameter azureEnvironmentParameter, ClientMethodParameter tokenCredentialParameter, ClientMethodParameter httpPipelineParameter, ClientMethodParameter serializerAdapterParameter, ClientMethodParameter defaultPollIntervalParameter, String defaultCredentialScopes, + boolean builderDisabled, String builderPackageName, SecurityInfo securityInfo, String baseUrl, PipelinePolicyDetails pipelinePolicyDetails, String crossLanguageDefinitionId) { + this.packageName = packageName; + this.className = className; + this.interfaceName = interfaceName; + this.proxy = proxy; + this.methodGroupClients = methodGroupClients; + this.properties = properties; + this.constructors = constructors; + this.clientMethods = clientMethods; + this.azureEnvironmentParameter = azureEnvironmentParameter; + this.tokenCredentialParameter = tokenCredentialParameter; + this.httpPipelineParameter = httpPipelineParameter; + this.serializerAdapterParameter = serializerAdapterParameter; + this.defaultPollIntervalParameter = defaultPollIntervalParameter; + this.clientBaseName = className.endsWith("Impl") ? className.substring(0, className.length() - 4) : className; + this.defaultCredentialScopes = defaultCredentialScopes; + this.builderDisabled = builderDisabled; + this.builderPackageName = builderPackageName; + this.securityInfo = securityInfo; + this.baseUrl = baseUrl; + this.pipelinePolicyDetails = pipelinePolicyDetails; + this.crossLanguageDefinitionId = crossLanguageDefinitionId; + } + + public final String getPackage() { + return packageName; + } + + public final String getClassName() { + return className; + } + + public final String getInterfaceName() { + return interfaceName; + } + + public final Proxy getProxy() { + return proxy; + } + + public final List getMethodGroupClients() { + return methodGroupClients; + } + + public final List getProperties() { + return properties; + } + + public final List getConstructors() { + return constructors; + } + + public final List getClientMethods() { + return clientMethods; + } + + public final ClientMethodParameter getAzureEnvironmentParameter() { + return azureEnvironmentParameter; + } + + public final ClientMethodParameter getDefaultPollIntervalParameter() { + return defaultPollIntervalParameter; + } + + public final ClientMethodParameter getTokenCredentialParameter() { + return tokenCredentialParameter; + } + + public final ClientMethodParameter getHttpPipelineParameter() { + return httpPipelineParameter; + } + + public final ClientMethodParameter getSerializerAdapterParameter() { + return serializerAdapterParameter; + } + + public final String getClientBaseName() { + return clientBaseName; + } + + public final String getDefaultCredentialScopes() { + return defaultCredentialScopes; + } + + public final boolean isBuilderDisabled() { + return builderDisabled; + } + + /** + * Gets the package name for builder and wrapper classes. + *

+ * It can be {@code null}, if no specific value provided. + * In such case, usually the package name from "namespace" option is used. + * + * @return the package name for builder and wrapper classes. + */ + public String getBuilderPackageName() { + return builderPackageName; + } + + public SecurityInfo getSecurityInfo() { + return securityInfo; + } + + /** + * @return the base URL, includes scheme, host (and maybe basePath) + */ + public String getBaseUrl() { + return baseUrl; + } + + /** + * @return the configuration for HttpPipelinePolicy + */ + public PipelinePolicyDetails getPipelinePolicyDetails() { + return pipelinePolicyDetails; + } + + public String getCrossLanguageDefinitionId() { + return crossLanguageDefinitionId; + } + + /** + * Add this property's imports to the provided set of imports. + * @param imports The set of imports to add to. + * @param includeImplementationImports Whether to include imports that are only necessary for method implementations. + */ + public final void addImportsTo(Set imports, boolean includeImplementationImports, boolean includeBuilderImports, JavaSettings settings) { + if (!includeBuilderImports) { + for (ClientMethod clientMethod : getClientMethods()) { + clientMethod.addImportsTo(imports, includeImplementationImports, settings); + } + } + + for (ServiceClientProperty serviceClientProperty : getProperties()) { + serviceClientProperty.addImportsTo(imports, includeImplementationImports); + } + + if (includeImplementationImports) { + if (settings.isFluent()) { + ClassType.HTTP_HEADER_NAME.addImportsTo(imports, includeImplementationImports); + } + if (settings.isFluentPremium()) { + imports.add("com.azure.resourcemanager.resources.fluentcore.AzureServiceClient"); + } + if (!getClientMethods().isEmpty()) { + addRestProxyImport(imports); + } + + for (Constructor constructor : getConstructors()) { + constructor.addImportsTo(imports, includeImplementationImports); + } + + if (!settings.isGenerateClientInterfaces()) { + for (MethodGroupClient methodGroupClient : getMethodGroupClients()) { + imports.add(String.format("%1$s.%2$s", methodGroupClient.getPackage(), methodGroupClient.getClassName())); + } + } else { + String interfacePackage = ClientModelUtil.getServiceClientInterfacePackageName(); + imports.add(String.format("%1$s.%2$s", interfacePackage, this.getInterfaceName())); + for (MethodGroupClient methodGroupClient : this.getMethodGroupClients()) { + imports.add(String.format("%1$s.%2$s", interfacePackage, methodGroupClient.getInterfaceName())); + } + } + } + + if (includeBuilderImports || includeImplementationImports) { + if (!settings.isFluent() && settings.isGenerateClientInterfaces()) { + imports.add(String.format("%1$s.%2$s", settings.getPackage(), getInterfaceName())); + for (MethodGroupClient methodGroupClient : getMethodGroupClients()) { + imports.add(String.format("%1$s.%2$s", settings.getPackage(), methodGroupClient.getInterfaceName())); + } + } + + addPipelineBuilderImport(imports); + addHttpPolicyImports(imports); + } + + if (includeBuilderImports) { + imports.add(String.format("%1$s.%2$s", getPackage(), getClassName())); + } + + Proxy proxy = getProxy(); + if (proxy != null) { + proxy.addImportsTo(imports, includeImplementationImports, settings); + } + } + + protected void addRestProxyImport(Set imports) { + ClassType.REST_PROXY.addImportsTo(imports, false); + } + + protected void addHttpPolicyImports(Set imports) { + if (JavaSettings.getInstance().isBranded()) { + imports.add("com.azure.core.http.policy.RetryPolicy"); + imports.add("com.azure.core.http.policy.UserAgentPolicy"); + } + } + + protected void addPipelineBuilderImport(Set imports) { + ClassType.HTTP_PIPELINE_BUILDER.addImportsTo(imports, false); + } + + public static class Builder { + protected String packageName; + protected String className; + protected String interfaceName; + protected Proxy proxy; + protected List methodGroupClients; + protected List properties; + protected List constructors; + protected List clientMethods; + protected ClientMethodParameter azureEnvironmentParameter; + protected ClientMethodParameter tokenCredentialParameter; + protected ClientMethodParameter httpPipelineParameter; + protected ClientMethodParameter serializerAdapterParameter; + protected ClientMethodParameter defaultPollIntervalParameter; + protected String defaultCredentialScopes; + protected boolean builderDisabled; + protected String builderPackageName; + protected SecurityInfo securityInfo; + protected String baseUrl; + protected PipelinePolicyDetails pipelinePolicyDetails; + private String crossLanguageDefinitionId; + + /** + * Sets the package that this service client belongs to. + * @param packageName the package that this service client belongs to + * @return the Builder itself + */ + public Builder packageName(String packageName) { + this.packageName = packageName; + return this; + } + + /** + * Sets the name of this client's class. + * @param className the name of this client's class + * @return the Builder itself + */ + public Builder className(String className) { + this.className = className; + return this; + } + + /** + * Sets the name of this client's interface. + * @param interfaceName the name of this client's interface + * @return the Builder itself + */ + public Builder interfaceName(String interfaceName) { + this.interfaceName = interfaceName; + return this; + } + + /** + * Sets the REST API that this client will send requests to. + * @param proxy the REST API that this client will send requests to + * @return the Builder itself + */ + public Builder proxy(Proxy proxy) { + this.proxy = proxy; + return this; + } + + /** + * Sets the MethodGroupClients that belong to this ServiceClient. + * @param methodGroupClients the MethodGroupClients that belong to this ServiceClient + * @return the Builder itself + */ + public Builder methodGroupClients(List methodGroupClients) { + this.methodGroupClients = methodGroupClients; + return this; + } + + /** + * Sets the properties of this ServiceClient. + * @param properties the properties of this ServiceClient + * @return the Builder itself + */ + public Builder properties(List properties) { + this.properties = properties; + return this; + } + + /** + * Sets the constructors for this ServiceClient. + * @param constructors the constructors for this ServiceClient + * @return the Builder itself + */ + public Builder constructors(List constructors) { + this.constructors = constructors; + return this; + } + + /** + * Sets the client method overloads for this ServiceClient. + * @param clientMethods the client method overloads for this ServiceClient + * @return the Builder itself + */ + public Builder clientMethods(List clientMethods) { + this.clientMethods = clientMethods; + return this; + } + + /** + * Sets the azure environment parameter. + * @param azureEnvironmentParameter the azure environment + * @return the Builder itself + */ + public Builder azureEnvironmentParameter(ClientMethodParameter azureEnvironmentParameter) { + this.azureEnvironmentParameter = azureEnvironmentParameter; + return this; + } + + /** + * Sets the serializer adapter parameter. + * @param serializerAdapterParameter the serializer adapter + * @return the Builder itself + */ + public Builder serializerAdapterParameter(ClientMethodParameter serializerAdapterParameter) { + this.serializerAdapterParameter = serializerAdapterParameter; + return this; + } + + /** + * Sets the default poll interval parameter. + * @param defaultPollIntervalParameter the poll interval + * @return the Builder itself + */ + public Builder defaultPollIntervalParameter(ClientMethodParameter defaultPollIntervalParameter) { + this.defaultPollIntervalParameter = defaultPollIntervalParameter; + return this; + } + + /** + * Sets the credentials parameter. + * @param tokenCredentialParameter the credentials parameter + * @return the Builder itself + */ + public Builder tokenCredentialParameter(ClientMethodParameter tokenCredentialParameter) { + this.tokenCredentialParameter = tokenCredentialParameter; + return this; + } + + /** + * Sets the HttpPipeline parameter. + * @param httpPipelineParameter the HttpPipeline parameter + * @return the Builder itself + */ + public Builder httpPipelineParameter(ClientMethodParameter httpPipelineParameter) { + this.httpPipelineParameter = httpPipelineParameter; + return this; + } + + /** + * Sets the defaultCredentialScopes parameter. + * @param defaultCredentialScopes the default credential scopes + * @return the Builder itself + */ + public Builder defaultCredentialScopes(String defaultCredentialScopes) { + this.defaultCredentialScopes = defaultCredentialScopes; + return this; + } + + /** + * Sets the builderDisabled parameter. + * @param builderDisabled whether to disable ClientBuilder class + * @return the Builder itself + */ + public Builder builderDisabled(boolean builderDisabled) { + this.builderDisabled = builderDisabled; + return this; + } + + /** + * Sets the builderPackageName parameter. + * @param builderPackageName the package name for builder and wrapper classes + * @return the Builder itself + */ + public Builder builderPackageName(String builderPackageName) { + this.builderPackageName = builderPackageName; + return this; + } + + /** + * Sets the security configuration information. + * @param securityInfo the security configuration information + * @return the Builder itself + */ + public Builder securityInfo(SecurityInfo securityInfo) { + this.securityInfo = securityInfo; + return this; + } + + /** + * Sets the base URL. + * @param baseUrl the base URL + * @return the Builder itself + */ + public Builder baseUrl(String baseUrl) { + this.baseUrl = baseUrl; + return this; + } + + /** + * Configures the HttpPipelinePolicy + * + * @param pipelinePolicyDetails the configuration of HttpPipelinePolicy + * @return the Builder itself + */ + public Builder pipelinePolicyDetails(PipelinePolicyDetails pipelinePolicyDetails) { + this.pipelinePolicyDetails = pipelinePolicyDetails; + return this; + } + + public ServiceClient build() { + return new ServiceClient(packageName, + className, + interfaceName, + proxy, + methodGroupClients, + properties, + constructors, + clientMethods, + azureEnvironmentParameter, + tokenCredentialParameter, + httpPipelineParameter, + serializerAdapterParameter, + defaultPollIntervalParameter, + defaultCredentialScopes, + builderDisabled, + builderPackageName, + securityInfo, + baseUrl, + pipelinePolicyDetails, + crossLanguageDefinitionId); + } + + public Builder crossLanguageDefinitionId(String crossLanguageDefinitionId) { + this.crossLanguageDefinitionId = crossLanguageDefinitionId; + return this; + } + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/ServiceClientProperty.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/ServiceClientProperty.java new file mode 100644 index 0000000000..673f06b09f --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/ServiceClientProperty.java @@ -0,0 +1,205 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.model.clientmodel; + +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaVisibility; + +import java.util.Objects; +import java.util.Set; + +/** + * A property that exists within a service's client. + */ +public class ServiceClientProperty { + /** + * The description of this property. + */ + private final String description; + /** + * The type of this property that is exposed via the client. + */ + private final IType type; + /** + * The name of this property. + */ + private final String name; + + /** + * THe accessor method suffix of this property + */ + private final String accessorMethodSuffix; + /** + * Get whether or not this property's value can be changed by the client library. + */ + private final boolean readOnly; + /** + * Get the expression that evaluates to this property's default value. + */ + private final String defaultValueExpression; + + private final JavaVisibility methodVisibility; + + private final boolean required; + + private String requestParameterName; + + /** + * Create a new ServiceClientProperty with the provided properties. + * @param description The description of this property. + * @param type The type of this property that is exposed via the client. + * @param name The name of this property. + * @param readOnly Whether or not this property's value can be changed by the client library. + * @param defaultValueExpression The expression that evaluates to this property's default value. + */ + public ServiceClientProperty(String description, IType type, String name, boolean readOnly, String defaultValueExpression) { + this(description, type, name, readOnly, defaultValueExpression, name, JavaVisibility.Public, false, null); + } + + public ServiceClientProperty(String description, IType type, String name, boolean readOnly, String defaultValueExpression, JavaVisibility methodVisibility) { + this(description, type, name, readOnly, defaultValueExpression, name, methodVisibility, false, null); + } + + private ServiceClientProperty(String description, IType type, String name, boolean readOnly, String defaultValueExpression, + String accessorMethodSuffix, JavaVisibility methodVisibility, boolean required, String requestParameterName) { + this.description = description; + this.type = type; + this.name = name; + this.readOnly = readOnly; + this.defaultValueExpression = defaultValueExpression; + this.accessorMethodSuffix = accessorMethodSuffix; + this.methodVisibility = methodVisibility; + this.required = required; + this.requestParameterName = requestParameterName; + } + + public final String getDescription() { + return description; + } + + public final IType getType() { + return type; + } + + public final String getName() { + return name; + } + + public final String getAccessorMethodSuffix() { + return accessorMethodSuffix; + } + + public final boolean isReadOnly() { + return readOnly; + } + + public final String getDefaultValueExpression() { + return defaultValueExpression; + } + + public JavaVisibility getMethodVisibility() { + return methodVisibility; + } + + public boolean isRequired() { + return required; + } + + /** + * @return the name of this parameter when it is serialized. It could be null, if this parameter is client only. + */ + public String getRequestParameterName() { + return requestParameterName; + } + + /** + * Add this property's imports to the provided set of imports. + * @param imports The set of imports to add to. + * @param includeImplementationImports Whether to include imports that are only necessary for method implementations. + */ + public final void addImportsTo(Set imports, boolean includeImplementationImports) { + getType().addImportsTo(imports, includeImplementationImports); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ServiceClientProperty that = (ServiceClientProperty) o; + return readOnly == that.readOnly && + Objects.equals(description, that.description) && + Objects.equals(type, that.type) && + Objects.equals(name, that.name) && + Objects.equals(defaultValueExpression, that.defaultValueExpression); + } + + @Override + public int hashCode() { + return Objects.hash(description, type, name, readOnly, defaultValueExpression); + } + + public static final class Builder { + private String description; + private IType type; + private String name; + private String accessorMethodSuffix; + private boolean readOnly = false; + private String defaultValueExpression = null; + private JavaVisibility methodVisibility = JavaVisibility.Public; + private boolean required = false; + private String requestParameterName; + + public Builder description(String description) { + this.description = description; + return this; + } + + public Builder type(IType type) { + this.type = type; + return this; + } + + public Builder name(String name) { + this.name = name; + return this; + } + + public Builder accessorMethodSuffix(String accessorMethodSuffix) { + this.accessorMethodSuffix = accessorMethodSuffix; + return this; + } + + public Builder readOnly(boolean readOnly) { + this.readOnly = readOnly; + return this; + } + + public Builder defaultValueExpression(String defaultValueExpression) { + this.defaultValueExpression = defaultValueExpression; + return this; + } + + public Builder methodVisibility(JavaVisibility methodVisibility) { + this.methodVisibility = methodVisibility; + return this; + } + + public Builder required(boolean required) { + this.required = required; + return this; + } + + public Builder requestParameterName(String requestParameterName) { + this.requestParameterName = requestParameterName; + return this; + } + + public ServiceClientProperty build() { + if (accessorMethodSuffix == null) { + accessorMethodSuffix = name; + } + return new ServiceClientProperty(description, type, name, readOnly, defaultValueExpression, + accessorMethodSuffix, methodVisibility, required, requestParameterName); + } + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/ServiceVersion.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/ServiceVersion.java new file mode 100644 index 0000000000..6cb3fe2b3f --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/ServiceVersion.java @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.model.clientmodel; + +import java.util.List; + +public class ServiceVersion { + + private final String className; + private final String serviceName; + private final List serviceVersions; + + public ServiceVersion(String className, String serviceName, List serviceVersions) { + this.className = className; + this.serviceName = serviceName; + this.serviceVersions = serviceVersions; + } + + public String getClassName() { + return className; + } + + public String getServiceName() { + return serviceName; + } + + public List getServiceVersions() { + return serviceVersions; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/TestContext.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/TestContext.java new file mode 100644 index 0000000000..7efd9e0401 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/TestContext.java @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.model.clientmodel; + +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; + +import java.util.List; +import java.util.Objects; + +public class TestContext { + + private final List serviceClients; + private final List syncClients; + + private final TExample testCase; + + public TestContext(List serviceClients, List syncClients) { + this.serviceClients = Objects.requireNonNull(serviceClients); + this.syncClients = Objects.requireNonNull(syncClients); + this.testCase = null; + } + + /** + * Appends an example as test case to the test context. + * + * @param testContext test context + * @param testCase an example as test case + */ + public TestContext(TestContext testContext, TExample testCase) { + this.serviceClients = Objects.requireNonNull(testContext.getServiceClients()); + this.syncClients = Objects.requireNonNull(testContext.getSyncClients()); + this.testCase = testCase; + } + + public List getServiceClients() { + return serviceClients; + } + + public List getSyncClients() { + return syncClients; + } + + public TExample getTestCase() { + return testCase; + } + + public String getPackageName() { + return JavaSettings.getInstance().getPackage("generated"); + } + + public String getTestBaseClassName() { + return serviceClients.iterator().next().getInterfaceName() + "TestBase"; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/UnionModel.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/UnionModel.java new file mode 100644 index 0000000000..638f04ca83 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/UnionModel.java @@ -0,0 +1,183 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.model.clientmodel; + +import java.util.Collections; +import java.util.List; +import java.util.Set; + +public class UnionModel { + + /** + * The package that this model class belongs to. + */ + private final String packageName; + /** + * Get the name of this model. + */ + private final String name; + /** + * Get the imports for this model. + */ + private final List imports; + /** + * Get the description of this model. + */ + private final String description; + /** + * Get the parent model of this model. + */ + private final String parentModelName; + /** + * Get the properties for this model. + */ + private final List properties; + private final ImplementationDetails implementationDetails; + + protected UnionModel( + String packageKeyword, String name, List imports, String description, + String parentModelName, + List properties, + ImplementationDetails implementationDetails) { + this.packageName = packageKeyword; + this.name = name; + this.imports = imports; + this.description = description; + this.parentModelName = parentModelName; + this.properties = properties; + this.implementationDetails = implementationDetails; + } + + public final String getFullName() { + return String.format("%1$s.%2$s", getPackage(), getName()); + } + + public void addImportsTo(Set imports) { + imports.add(this.getFullName()); + + imports.addAll(getImports()); + + for (ClientModelProperty property : getProperties()) { + property.addImportsTo(imports, false); + } + } + + public String getPackage() { + return packageName; + } + + public String getName() { + return name; + } + + public List getImports() { + return imports; + } + + public String getDescription() { + return description; + } + + public String getParentModelName() { + return parentModelName; + } + + public List getProperties() { + return properties; + } + + public ImplementationDetails getImplementationDetails() { + return implementationDetails; + } + + public static class Builder { + private String packageName; + private String name; + private List imports = Collections.emptyList(); + private String description; + private String parentModelName; + private List properties = Collections.emptyList(); + private ImplementationDetails implementationDetails; + + /** + * Sets the package that this model class belongs to. + * @param packageName the package that this model class belongs to + * @return the Builder itself + */ + public Builder packageName(String packageName) { + this.packageName = packageName; + return this; + } + + /** + * Sets the name of this model. + * @param name the name of this model + * @return the Builder itself + */ + public Builder name(String name) { + this.name = name; + return this; + } + + /** + * Sets the imports for this model. + * @param imports the imports for this model + * @return the Builder itself + */ + public Builder imports(List imports) { + this.imports = imports; + return this; + } + + /** + * Sets the description of this model. + * @param description the description of this model + * @return the Builder itself + */ + public Builder description(String description) { + this.description = description; + return this; + } + + /** + * Sets the parent model of this model. + * @param parentModelName the parent model of this model + * @return the Builder itself + */ + public Builder parentModelName(String parentModelName) { + this.parentModelName = parentModelName; + return this; + } + + /** + * Sets the properties for this model. + * @param properties the properties for this model + * @return the Builder itself + */ + public Builder properties(List properties) { + this.properties = properties; + return this; + } + + /** + * Sets the implementation details for the model. + * @param implementationDetails the implementation details. + * @return the Builder itself + */ + public Builder implementationDetails(ImplementationDetails implementationDetails) { + this.implementationDetails = implementationDetails; + return this; + } + + public UnionModel build() { + return new UnionModel(packageName, + name, + imports, + description, + parentModelName, + properties, + implementationDetails); + } + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/UnionModels.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/UnionModels.java new file mode 100644 index 0000000000..d8e45fb3ec --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/UnionModels.java @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.model.clientmodel; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class UnionModels { + + private static final UnionModels INSTANCE = new UnionModels(); + private final Map> nameMap = new HashMap<>(); + + private UnionModels() { + } + + public final void clear() { + nameMap.clear(); + } + + public static UnionModels getInstance() { + return INSTANCE; + } + + /** + * Gets the UnionModel instance from the name of the model. + * + * @param modelName the name of the model. + * @return the UnionModel instance. + */ + public final List getModel(String modelName) { + return nameMap.get(modelName); + } + + public final void addModel(List models) { + nameMap.put(models.iterator().next().getName(), models); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/Versioning.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/Versioning.java new file mode 100644 index 0000000000..782a9b1481 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/Versioning.java @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.model.clientmodel; + +import java.util.Collections; +import java.util.List; + +public class Versioning { + + private final List added; + + private Versioning(List added) { + this.added = added; + } + + public List getAdded() { + return added; + } + + public static class Builder { + private List added = Collections.emptyList(); + + public Builder() { + } + + public Builder added(List added) { + this.added = added; + return this; + } + + public Versioning build() { + return new Versioning(added); + } + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/XmlSequenceWrapper.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/XmlSequenceWrapper.java new file mode 100644 index 0000000000..eb669a3901 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/XmlSequenceWrapper.java @@ -0,0 +1,102 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.model.clientmodel; + +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.ArraySchema; +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; +import com.microsoft.typespec.http.client.generator.core.mapper.Mappers; +import com.microsoft.typespec.http.client.generator.core.util.CodeNamer; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +/** + * The details needed to create an XML sequence wrapper class for the client. + */ +public class XmlSequenceWrapper { + private final String packageName; + private final IType sequenceType; + private final String xmlRootElementName; + private final String xmlRootElementNamespace; + private final String xmlListElementName; + private final String xmlListElementNamespace; + private final String wrapperClassName; + private final Set imports; + + public XmlSequenceWrapper(String modelTypeName, ArraySchema arraySchema, JavaSettings settings) { + boolean wrapperHasXmlSerialization = arraySchema.getSerialization() != null + && arraySchema.getSerialization().getXml() != null; + boolean elementHasXmlSerialization = arraySchema.getElementType().getSerialization() != null + && arraySchema.getElementType().getSerialization().getXml() != null; + + + if (wrapperHasXmlSerialization) { + xmlRootElementName = arraySchema.getSerialization().getXml().getName(); + xmlRootElementNamespace = arraySchema.getSerialization().getXml().getNamespace(); + } else { + xmlRootElementName = arraySchema.getLanguage().getDefault().getSerializedName(); + xmlRootElementNamespace = arraySchema.getLanguage().getDefault().getNamespace(); + } + + if (elementHasXmlSerialization) { + xmlListElementName = arraySchema.getElementType().getSerialization().getXml().getName(); + xmlListElementNamespace = arraySchema.getElementType().getSerialization().getXml().getNamespace(); + } else { + xmlListElementName = arraySchema.getElementType().getLanguage().getDefault().getSerializedName(); + xmlListElementNamespace = arraySchema.getElementType().getLanguage().getDefault().getNamespace(); + } + + sequenceType = Mappers.getSchemaMapper().map(arraySchema); + Set imports = getXmlSequenceWrapperImports(); + sequenceType.addImportsTo(imports, true); + boolean isCustomType = settings.isCustomType(CodeNamer.toPascalCase(modelTypeName + "Wrapper")); + packageName = isCustomType + ? settings.getPackage(settings.getCustomTypesSubpackage()) + : settings.getPackage(settings.getImplementationSubpackage() + ".models"); + + this.wrapperClassName = modelTypeName + "Wrapper"; + this.imports = imports; + } + + private static Set getXmlSequenceWrapperImports() { + return new HashSet<>(Arrays.asList("com.fasterxml.jackson.annotation.JsonCreator", + "com.fasterxml.jackson.annotation.JsonProperty", + "com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty", + "com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement", + "com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlText")); + } + + public final String getPackage() { + return packageName; + } + + public final IType getSequenceType() { + return sequenceType; + } + + public final String getXmlRootElementName() { + return xmlRootElementName; + } + + public final String getXmlRootElementNamespace() { + return xmlRootElementNamespace; + } + + public final String getXmlListElementName() { + return xmlListElementName; + } + + public String getXmlListElementNamespace() { + return xmlListElementNamespace; + } + + public final String getWrapperClassName() { + return wrapperClassName; + } + + public final Set getImports() { + return imports; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/examplemodel/BinaryDataNode.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/examplemodel/BinaryDataNode.java new file mode 100644 index 0000000000..d4c8db28c2 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/examplemodel/BinaryDataNode.java @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.model.clientmodel.examplemodel; + +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.IType; + +/** + * Example node for BinaryData. + */ +public class BinaryDataNode extends ExampleNode { + public BinaryDataNode(IType clientType, Object objectValue) { + super(clientType, objectValue); + } + + /** + * Get example value as string. + * We treat all example values for BinaryData as string and generate BinaryData.fromBytes(exampleValue.getBytes()). + * @return example value as string + */ + public String getExampleValue() { + return getObjectValue() == null ? null : getObjectValue().toString(); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/examplemodel/ClientModelNode.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/examplemodel/ClientModelNode.java new file mode 100644 index 0000000000..10526944e2 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/examplemodel/ClientModelNode.java @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.model.clientmodel.examplemodel; + +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ModelProperty; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientModel; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.IType; + +import java.util.HashMap; +import java.util.Map; + +/** + * Example node for a client model (a generated Java class). + */ +public class ClientModelNode extends ExampleNode { + + private ClientModel model; + + // modelProperties can contain more properties than in the model, as it includes those properties from the superclass of the model + private final Map modelProperties = new HashMap<>(); + + public ClientModelNode(IType clientType, Object objectValue) { + super(clientType, objectValue); + } + + public ClientModel getClientModel() { + return model; + } + + public ClientModelNode setClientModel(ClientModel model) { + this.model = model; + return this; + } + + public Map getClientModelProperties() { + return modelProperties; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/examplemodel/ExampleHelperFeature.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/examplemodel/ExampleHelperFeature.java new file mode 100644 index 0000000000..d3ba9148e8 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/examplemodel/ExampleHelperFeature.java @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.model.clientmodel.examplemodel; + +public enum ExampleHelperFeature { + // 'mapOf(...)' method in class + MapOfMethod, + + // 'throws IOException' in method signature + ThrowsIOException +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/examplemodel/ExampleNode.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/examplemodel/ExampleNode.java new file mode 100644 index 0000000000..c5f9d77173 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/examplemodel/ExampleNode.java @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.model.clientmodel.examplemodel; + +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.IType; + +import java.util.ArrayList; +import java.util.List; + +/** + * Tree of example nodes. + */ +public abstract class ExampleNode { + + // the full Object at and below this node + private final Object objectValue; + + private final IType clientType; + + private final List childNodes = new ArrayList<>(); + + public ExampleNode(IType clientType, Object objectValue) { + this.clientType = clientType; + this.objectValue = objectValue; + } + + public List getChildNodes() { + return childNodes; + } + + public Object getObjectValue() { + return objectValue; + } + + public IType getClientType() { + return clientType; + } + + public boolean isNull() { + return objectValue == null; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/examplemodel/ListNode.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/examplemodel/ListNode.java new file mode 100644 index 0000000000..c88bda4ed6 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/examplemodel/ListNode.java @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.model.clientmodel.examplemodel; + +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.IType; + +/** + * Example node for a List. + */ +public class ListNode extends ExampleNode { + + public ListNode(IType clientType, Object objectValue) { + super(clientType, objectValue); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/examplemodel/LiteralNode.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/examplemodel/LiteralNode.java new file mode 100644 index 0000000000..ff195807f2 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/examplemodel/LiteralNode.java @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.model.clientmodel.examplemodel; + +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.IType; + +/** + * Example node of an external node, which is primitive type, or can be converted from a primitive type. + */ +public class LiteralNode extends ExampleNode { + + private String literalsValue; + + public LiteralNode(IType clientType, Object objectValue) { + super(clientType, objectValue); + } + + public String getLiteralsValue() { + return literalsValue; + } + + public LiteralNode setLiteralsValue(String literalsValue) { + this.literalsValue = literalsValue; + return this; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/examplemodel/MapNode.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/examplemodel/MapNode.java new file mode 100644 index 0000000000..b8087a1aed --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/examplemodel/MapNode.java @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.model.clientmodel.examplemodel; + +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.IType; + +import java.util.ArrayList; +import java.util.List; + +/** + * Example node for a Map. + */ +public class MapNode extends ExampleNode { + + private final List keys = new ArrayList<>(); + + public MapNode(IType clientType, Object objectValue) { + super(clientType, objectValue); + } + + public List getKeys() { + return keys; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/examplemodel/MethodParameter.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/examplemodel/MethodParameter.java new file mode 100644 index 0000000000..651b9c1208 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/examplemodel/MethodParameter.java @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.model.clientmodel.examplemodel; + +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethodParameter; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ProxyMethodParameter; + +/** 1-1 pair of proxy method parameter and client method parameter */ +public class MethodParameter { + + private final ProxyMethodParameter proxyMethodParameter; + private final ClientMethodParameter clientMethodParameter; + + public MethodParameter(ProxyMethodParameter proxyMethodParameter, ClientMethodParameter clientMethodParameter) { + this.proxyMethodParameter = proxyMethodParameter; + this.clientMethodParameter = clientMethodParameter; + } + + public ProxyMethodParameter getProxyMethodParameter() { + return proxyMethodParameter; + } + + public ClientMethodParameter getClientMethodParameter() { + return clientMethodParameter; + } + + public String getSerializedName() { + return this.getProxyMethodParameter().getRequestParameterName(); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/examplemodel/ObjectNode.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/examplemodel/ObjectNode.java new file mode 100644 index 0000000000..e079dbdf53 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/clientmodel/examplemodel/ObjectNode.java @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.model.clientmodel.examplemodel; + +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.IType; + +/** + * Example node for Object class. + */ +public class ObjectNode extends ExampleNode { + + public ObjectNode(IType clientType, Object objectValue) { + super(clientType, objectValue); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/javamodel/JavaBlock.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/javamodel/JavaBlock.java new file mode 100644 index 0000000000..674666813a --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/javamodel/JavaBlock.java @@ -0,0 +1,93 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.model.javamodel; + +import java.util.function.Consumer; + +public class JavaBlock implements JavaContext { + private final JavaFileContents contents; + + public JavaBlock(JavaFileContents contents) { + this.contents = contents; + } + + public final void indent(Runnable indentAction) { + contents.indent(indentAction); + } + + public final void increaseIndent() { + contents.increaseIndent(); + } + + public final void decreaseIndent() { + contents.decreaseIndent(); + } + + public final void text(String text) { + contents.text(text); + } + + public final void line(String text) { + contents.line(text); + } + + public final void line(String text, Object... formattedArguments) { + contents.line(text, formattedArguments); + } + + public final void line() { + contents.line(); + } + + public final void block(String text, Consumer bodyAction) { + contents.block(text, bodyAction); + } + + public final void javadocComment(String text) { + contents.javadocComment(text); + } + + public final void javadocComment(Consumer commentAction) { + contents.javadocComment(commentAction); + } + + public final void methodReturn(String text) { + contents.methodReturn(text); + } + + public final void annotation(String... annotations) { + contents.annotation(annotations); + } + + public final void returnAnonymousClass(String anonymousClassDeclaration, Consumer anonymousClassBlock) { + contents.returnAnonymousClass(anonymousClassDeclaration, anonymousClassBlock); + } + + public final void anonymousClass(String anonymousClassDeclaration, String instanceName, Consumer anonymousClassBlock) { + contents.anonymousClass(anonymousClassDeclaration, instanceName, anonymousClassBlock); + } + + public final JavaIfBlock ifBlock(String condition, Consumer ifAction) { + contents.ifBlock(condition, ifAction); + return new JavaIfBlock(contents); + } + + public final JavaTryBlock tryBlock(Consumer ifAction) { + contents.tryBlock(ifAction); + return new JavaTryBlock(contents); + } + + public final JavaTryBlock tryBlock(String resource, Consumer ifAction) { + contents.tryBlock(resource, ifAction); + return new JavaTryBlock(contents); + } + + public final void lambda(String parameterType, String parameterName, Consumer body) { + contents.lambda(parameterType, parameterName, body); + } + + public final void lambda(String parameterType, String parameterName, String returnExpression) { + contents.lambda(parameterType, parameterName, returnExpression); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/javamodel/JavaCatchBlock.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/javamodel/JavaCatchBlock.java new file mode 100644 index 0000000000..4742c9aab4 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/javamodel/JavaCatchBlock.java @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.model.javamodel; + +import java.util.function.Consumer; + +public class JavaCatchBlock { + private final JavaFileContents contents; + + public JavaCatchBlock(JavaFileContents contents) { + this.contents = contents; + } + + public final JavaCatchBlock catchBlock(String exception, Consumer catchAction) { + contents.catchBlock(exception, catchAction); + return new JavaCatchBlock(contents); + } + + public final void finallyBlock(Consumer finallyAction) { + contents.finallyBlock(finallyAction); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/javamodel/JavaClass.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/javamodel/JavaClass.java new file mode 100644 index 0000000000..de05051692 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/javamodel/JavaClass.java @@ -0,0 +1,217 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.model.javamodel; + +import com.azure.core.util.CoreUtils; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +public class JavaClass implements JavaType { + private final JavaFileContents contents; + private boolean addNewLine; + + public JavaClass(JavaFileContents contents) { + this.contents = contents; + } + + private void addExpectedNewLine() { + if (addNewLine) { + contents.line(); + addNewLine = false; + } + } + + public final void privateMemberVariable(String variableType, String variableName) { + privateMemberVariable(variableType + " " + variableName); + } + + public final void privateMemberVariable(String variableDeclaration) { + addExpectedNewLine(); + contents.line("private " + variableDeclaration + ";"); + addNewLine = true; + } + + public final void privateFinalMemberVariable(String variableDeclaration) { + addExpectedNewLine(); + contents.line("private final " + variableDeclaration + ";"); + addNewLine = true; + } + + public final void privateFinalMemberVariable(String variableType, String variableName) { + addExpectedNewLine(); + contents.line("private final " + variableType + " " + variableName + ";"); + addNewLine = true; + } + + public final void privateFinalMemberVariable(String variableType, String variableName, String finalValue) { + addExpectedNewLine(); + contents.line("private final " + variableType + " " + variableName + " = " + finalValue + ";"); + addNewLine = true; + } + + public final void publicStaticFinalVariable(String variableDeclaration) { + addExpectedNewLine(); + contents.line("public static final " + variableDeclaration + ";"); + addNewLine = true; + } + + public final void privateStaticFinalVariable(String variableDeclaration) { + addExpectedNewLine(); + contents.line("private static final " + variableDeclaration + ";"); + addNewLine = true; + } + + public final void protectedMemberVariable(String variableType, String variableName) { + addExpectedNewLine(); + contents.line("protected " + variableType + " " + variableName + ";"); + addNewLine = true; + } + + /** + * Adds a variable with the given declaration and visibility. + *

+ * This doesn't support modifiers. If you need to add modifiers, use + * {@link #variable(String, JavaVisibility, JavaModifier...)}. This will just be a non-final, non-static variable + * with the given visibility. + * + * @param variableDeclaration The variable declaration. + * @param visibility The visibility of the variable. + */ + public final void variable(String variableDeclaration, JavaVisibility visibility) { + addExpectedNewLine(); + contents.line(visibility + " " + variableDeclaration + ";"); + addNewLine = true; + } + + /** + * Adds a variable with the given declaration, visibility, and modifiers. + *

+ * Adding a private constant variable would be: + * {@code variable(declaration, JavaVisibility.Private, JavaModifier.Static, JavaModifier.Final)} + * + * @param variableDeclaration The variable declaration. + * @param visibility The visibility of the variable. + * @param modifiers The modifiers of the variable. + */ + public final void variable(String variableDeclaration, JavaVisibility visibility, JavaModifier... modifiers) { + addExpectedNewLine(); + String modifier = CoreUtils.isNullOrEmpty(modifiers) ? "" + : Arrays.stream(modifiers).map(JavaModifier::toString).collect(Collectors.joining(" ")); + contents.line(visibility + " " + modifier + " " + variableDeclaration + ";"); + addNewLine = true; + } + + public final void constructor(JavaVisibility visibility, String constructorSignature, Consumer constructor) { + addExpectedNewLine(); + contents.constructor(visibility, constructorSignature, constructor); + addNewLine = true; + } + + public final void privateConstructor(String constructorSignature, Consumer constructor) { + constructor(JavaVisibility.Private, constructorSignature, constructor); + } + + public final void publicConstructor(String constructorSignature, Consumer constructor) { + constructor(JavaVisibility.Public, constructorSignature, constructor); + } + + public final void packagePrivateConstructor(String constructorSignature, Consumer constructor) { + constructor(JavaVisibility.PackagePrivate, constructorSignature, constructor); + } + + public final void method(JavaVisibility visibility, List modifiers, String methodSignature, Consumer method) { + addExpectedNewLine(); + contents.method(visibility, modifiers, methodSignature, method); + addNewLine = true; + } + + public final void publicMethod(String methodSignature, Consumer method) { + method(JavaVisibility.Public, null, methodSignature, method); + } + + public final void packagePrivateMethod(String methodSignature, Consumer method) { + method(JavaVisibility.PackagePrivate, null, methodSignature, method); + } + + public final void privateMethod(String methodSignature, Consumer method) { + method(JavaVisibility.Private, null, methodSignature, method); + } + + public final void publicStaticMethod(String methodSignature, Consumer method) { + staticMethod(JavaVisibility.Public, methodSignature, method); + } + + /** + * Adds a static method with a declared visibility to the class. + * + * @param visibility The visibility of the method. + * @param methodSignature The method signature. + * @param method The logic to generate the method. + */ + public final void staticMethod(JavaVisibility visibility, String methodSignature, Consumer method) { + Objects.requireNonNull(visibility, "'visibility' cannot be null."); + method(visibility, Collections.singletonList(JavaModifier.Static), methodSignature, method); + } + + public final void interfaceBlock(JavaVisibility visibility, String interfaceSignature, Consumer interfaceBlock) { + addExpectedNewLine(); + contents.interfaceBlock(visibility, interfaceSignature, interfaceBlock); + addNewLine = true; + } + + public final void publicInterface(String interfaceSignature, Consumer interfaceBlock) { + interfaceBlock(JavaVisibility.Public, interfaceSignature, interfaceBlock); + } + + public final void privateStaticFinalClass(String classSignature, Consumer classBlock) { + staticFinalClass(JavaVisibility.Private, classSignature, classBlock); + } + + public final void staticFinalClass(JavaVisibility visibility, String classSignature, Consumer classBlock) { + addExpectedNewLine(); + contents.classBlock(visibility, Arrays.asList(JavaModifier.Static, JavaModifier.Final), classSignature, + classBlock); + addNewLine = true; + } + + public final void blockComment(String description) { + addExpectedNewLine(); + contents.blockComment(description); + } + + public final void lineComment(String description) { + addExpectedNewLine(); + contents.lineComment(description); + } + + public final void blockComment(Consumer commentAction) { + addExpectedNewLine(); + contents.blockComment(commentAction); + } + + public final void javadocComment(String description) { + addExpectedNewLine(); + contents.javadocComment(description); + } + + public final void javadocComment(Consumer commentAction) { + addExpectedNewLine(); + contents.javadocComment(commentAction); + } + + public final void annotation(String... annotations) { + addExpectedNewLine(); + contents.annotation(annotations); + } + + public final void staticBlock(Consumer codeBlock) { + addExpectedNewLine(); + contents.block("static", codeBlock); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/javamodel/JavaContext.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/javamodel/JavaContext.java new file mode 100644 index 0000000000..8f07468194 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/javamodel/JavaContext.java @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.model.javamodel; + +import java.util.function.Consumer; + +public interface JavaContext { + void javadocComment(Consumer commentAction); + + void annotation(String... annotations); +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/javamodel/JavaEnum.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/javamodel/JavaEnum.java new file mode 100644 index 0000000000..ecca80d717 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/javamodel/JavaEnum.java @@ -0,0 +1,113 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.model.javamodel; + +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.IType; +import com.azure.core.util.CoreUtils; + +import java.util.Collections; +import java.util.List; +import java.util.function.Consumer; + +public class JavaEnum { + private final JavaFileContents contents; + private boolean previouslyAddedValue; + private boolean addNewLine; + + public JavaEnum(JavaFileContents contents) { + this.contents = contents; + } + + private void addExpectedNewLine() { + if (addNewLine) { + contents.line(); + addNewLine = false; + } + } + + private void addExpectedCommaAndNewLine() { + if (previouslyAddedValue) { + contents.line(","); + previouslyAddedValue = false; + } + + addExpectedNewLine(); + } + + private void addExpectedSemicolonAndNewLine() { + if (previouslyAddedValue) { + contents.line(";"); + previouslyAddedValue = false; + } + + addExpectedNewLine(); + } + + public final void addExpectedNewLineAfterLastValue() { + if (previouslyAddedValue) { + contents.line(); + previouslyAddedValue = false; + addNewLine = false; + } + } + + public final void value(String name, String value) { + addExpectedCommaAndNewLine(); + contents.javadocComment("Enum value " + value + "."); + contents.text(name + "(\"" + value + "\")"); + previouslyAddedValue = true; + addNewLine = true; + } + + public final void value(String name, String value, String description, IType type) { + addExpectedCommaAndNewLine(); + contents.javadocComment(CoreUtils.isNullOrEmpty(description) ? "Enum value " + value + "." : description); + contents.text(name + "(" + type.defaultValueExpression(value) + ")"); + previouslyAddedValue = true; + addNewLine = true; + } + + public final void privateFinalMemberVariable(String variableType, String variableName) { + addExpectedSemicolonAndNewLine(); + contents.line("private final " + variableType + " " + variableName + ";"); + addNewLine = true; + } + + public final void constructor(String constructorSignature, Consumer constructor) { + addExpectedSemicolonAndNewLine(); + contents.block(constructorSignature, constructor); + previouslyAddedValue = false; + addNewLine = true; + } + + public final void method(JavaVisibility visibility, List modifiers, String methodSignature, Consumer method) { + addExpectedSemicolonAndNewLine(); + contents.method(visibility, modifiers, methodSignature, method); + previouslyAddedValue = false; + addNewLine = true; + } + + public final void publicMethod(String methodSignature, Consumer method) { + method(JavaVisibility.Public, null, methodSignature, method); + } + + public final void publicStaticMethod(String methodSignature, Consumer method) { + method(JavaVisibility.Public, Collections.singletonList(JavaModifier.Static), methodSignature, method); + } + + public final void javadocComment(String description) { + addExpectedSemicolonAndNewLine(); + contents.javadocComment(description); + } + + public final void javadocComment(Consumer commentAction) { + addExpectedSemicolonAndNewLine(); + contents.javadocComment(commentAction); + } + + public final void annotation(String... annotations) { + addExpectedSemicolonAndNewLine(); + contents.annotation(annotations); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/javamodel/JavaFile.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/javamodel/JavaFile.java new file mode 100644 index 0000000000..d745d21890 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/javamodel/JavaFile.java @@ -0,0 +1,121 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.model.javamodel; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +public class JavaFile implements JavaContext { + private String packageKeyword; + private int packageWithPeriodLength; + private final String filePath; + private final JavaFileContents contents; + + public JavaFile(String filePath) { + this.filePath = filePath; + this.contents = new JavaFileContents(); + } + + public final String getFilePath() { + return filePath; + } + + public final JavaFileContents getContents() { + return contents; + } + + public final void text(String text) { + getContents().text(text); + } + + public final void line(String text) { + getContents().line(text); + } + + public final void line() { + getContents().line(); + } + + public final void indent(Runnable indentAction) { + getContents().indent(indentAction); + } + + public final void publicFinalClass(String classDeclaration, Consumer classAction) { + publicClass(Collections.singletonList(JavaModifier.Final), classDeclaration, classAction); + } + + public final void publicClass(List modifiers, String classDeclaration, Consumer classAction) { + classBlock(JavaVisibility.Public, modifiers, classDeclaration, classAction); + } + + public final void classBlock(JavaVisibility visibility, List modifiers, String classDeclaration, Consumer classAction) { + getContents().classBlock(visibility, modifiers, classDeclaration, classAction); + } + + public final void declarePackage(String packageKeyword) { + this.packageKeyword = packageKeyword; + if (packageKeyword == null || packageKeyword.isEmpty()) { + packageWithPeriodLength = 0; + } else { + packageWithPeriodLength = packageKeyword.length(); + if (!packageKeyword.endsWith(".")) { + ++packageWithPeriodLength; + } + } + getContents().declarePackage(packageKeyword); + } + + public final void declareImport(String... imports) { + declareImport(Arrays.asList(imports)); + } + + public final void declareImport(Set imports) { + declareImport(new ArrayList<>(imports)); + } + + public final void declareImport(List imports) { + if (packageKeyword != null && !packageKeyword.isEmpty()) { + // Only import paths that don't start with this file's package, or if they do start + // with this file's package, then they must exist within a subpackage. + imports = imports.stream() + .filter(importKeyword -> !importKeyword.startsWith(packageKeyword) + || importKeyword.indexOf('.', packageWithPeriodLength) != -1) + .collect(Collectors.toList()); + } + getContents().declareImport(imports); + } + + public final void javadocComment(Consumer commentAction) { + getContents().javadocComment(commentAction); + } + + public final void lineComment(Consumer commentAction) { + getContents().lineComment(commentAction); + } + + public final void annotation(String... annotations) { + getContents().annotation(annotations); + } + + public final void publicEnum(String enumName, Consumer enumAction) { + enumBlock(JavaVisibility.Public, enumName, enumAction); + } + + public final void enumBlock(JavaVisibility visibility, String enumName, Consumer enumAction) { + getContents().enumBlock(visibility, enumName, enumAction); + } + + public final void publicInterface(String interfaceName, Consumer interfaceAction) { + interfaceBlock(JavaVisibility.Public, interfaceName, interfaceAction); + } + + public final void interfaceBlock(JavaVisibility visibility, String interfaceName, Consumer interfaceAction) { + getContents().interfaceBlock(visibility, interfaceName, interfaceAction); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/javamodel/JavaFileContents.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/javamodel/JavaFileContents.java new file mode 100644 index 0000000000..d55cc60d7d --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/javamodel/JavaFileContents.java @@ -0,0 +1,349 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.model.javamodel; + +import com.azure.core.util.CoreUtils; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Set; +import java.util.TreeSet; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +public class JavaFileContents { + private static final String SINGLE_INDENT = " "; + + private final List contents; + + private String currentLine; + private String linePrefix; + + private CurrentLineType currentLineType = CurrentLineType.Empty; + + public JavaFileContents() { + this.currentLine = ""; + this.linePrefix = ""; + this.contents = new ArrayList<>(); + } + + private static String toString(List modifiers) { + return modifiers == null ? "" : modifiers.stream().map(JavaModifier::toString).collect(Collectors.joining(" ")); + } + + @Override + public String toString() { + return String.join("\n", contents) + currentLine; + } + + public boolean contains(String str) { + return contents.stream().anyMatch(line -> line.contains(str)); + } + + public final void addToPrefix(String toAdd) { + linePrefix += toAdd; + } + + private void removeFromPrefix(String toRemove) { + int toRemoveLength = toRemove.length(); + if (linePrefix.length() <= toRemoveLength) { + linePrefix = ""; + } else { + linePrefix = linePrefix.substring(0, linePrefix.length() - toRemoveLength); + } + } + + public final void indent(Runnable action) { + increaseIndent(); + action.run(); + decreaseIndent(); + } + + public final void increaseIndent() { + addToPrefix(SINGLE_INDENT); + } + + public final void decreaseIndent() { + removeFromPrefix(SINGLE_INDENT); + } + + private void text(String text, boolean addPrefix, boolean completeLastLine) { + String prefix = addPrefix ? linePrefix : null; + + if (text == null || text.isEmpty()) { + handleLine("", prefix, true, completeLastLine); + } else { + int lineStartIndex = 0; + int textLength = text.length(); + while (lineStartIndex < textLength) { + int newLineCharacterIndex = text.indexOf('\n', lineStartIndex); + if (newLineCharacterIndex == -1) { + handleLine(text.substring(lineStartIndex), prefix, true, completeLastLine); + break; + } else { + handleLine(text.substring(lineStartIndex, newLineCharacterIndex), prefix, false, completeLastLine); + lineStartIndex = newLineCharacterIndex + 1; + } + } + } + } + + private void handleLine(String line, String prefix, boolean lastLine, boolean completeLastLine) { + if (prefix != null + && (!prefix.trim().isEmpty() || (!prefix.isEmpty() && line != null && !line.trim().isEmpty()))) { + currentLine += prefix; + } + + currentLine += line; + if (!lastLine || completeLastLine) { + contents.add(currentLine); + currentLine = ""; + } + } + + public final void text(String text) { + if (currentLineType == CurrentLineType.Empty) { + text(text, true, false); + } else if (currentLineType == CurrentLineType.Text) { + text(text, false, false); + } else if (currentLineType == CurrentLineType.AfterIf) { + line("", false); + text(text, true, false); + } + currentLineType = CurrentLineType.Text; + } + + private void line(String text, boolean addPrefix) { + text(text, addPrefix, true); + currentLineType = CurrentLineType.Empty; + } + + public void line(String text) { + if (currentLineType == CurrentLineType.Empty) { + line(text, true); + } else if (currentLineType == CurrentLineType.Text) { + line(text, false); + } else if (currentLineType == CurrentLineType.AfterIf) { + line("", false); + line(text, true); + } + currentLineType = CurrentLineType.Empty; + } + + public void line(String text, Object... formattedArguments) { + if (formattedArguments != null && formattedArguments.length > 0) { + text = String.format(text, formattedArguments); + } + + line(text); + } + + public void line() { + line(""); + } + + public void declarePackage(String pkg) { + line("package " + pkg + ";"); + } + + public void block(String text, Consumer bodyAction) { + line(text + " {"); + indent(() -> bodyAction.accept(new JavaBlock(this))); + line("}"); + } + + public void declareImport(String... imports) { + declareImport(Arrays.asList(imports)); + } + + public void declareImport(List imports) { + if (imports != null && !imports.isEmpty()) { + Set importSet = new TreeSet<>(new JavaImportComparer()); + importSet.addAll(imports); + for (String toImport : importSet) { + if (toImport != null && !toImport.isEmpty()) { + line("import " + toImport + ";"); + } + } + line(); + } + } + + public void lineComment(String text) { + lineComment(comment -> comment.line(text)); + } + + public void lineComment(Consumer commentAction) { + addToPrefix("// "); + commentAction.accept(new JavaLineComment(this)); + removeFromPrefix("// "); + } + + public void blockComment(String text) { + blockComment(comment -> comment.line(text)); + } + + public void blockComment(Consumer commentAction) { + line("/*"); + addToPrefix(" * "); + commentAction.accept(new JavaLineComment(this)); + removeFromPrefix(" * "); + line(" */"); + } + + public void javadocComment(String text) { + javadocComment(comment -> comment.description(text)); + } + + public void javadocComment(Consumer commentAction) { + line("/**"); + addToPrefix(" * "); + commentAction.accept(new JavaJavadocComment(this)); + removeFromPrefix(" * "); + line(" */"); + } + + public void methodReturn(String text) { + line("return " + text + ";"); + } + + public void returnAnonymousClass(String anonymousClassDeclaration, Consumer anonymousClassBlock) { + line("return " + anonymousClassDeclaration + " {"); + indent(() -> { + JavaClass javaClass = new JavaClass(this); + anonymousClassBlock.accept(javaClass); + }); + line("};"); + } + + public void anonymousClass(String anonymousClassDeclaration, String instanceName, Consumer anonymousClassBlock) { + line(anonymousClassDeclaration + " " + instanceName + " = new " + anonymousClassDeclaration + "() {"); + indent(() -> { + JavaClass javaClass = new JavaClass(this); + anonymousClassBlock.accept(javaClass); + }); + line("};"); + } + + public void annotation(String... annotations) { + annotation(Arrays.asList(annotations)); + } + + public void annotation(List annotations) { + if (annotations != null && !annotations.isEmpty()) { + for (String annotation : annotations) { + if (annotation != null && !annotation.isEmpty()) { + line("@" + annotation); + } + } + } + } + + public void classBlock(JavaVisibility visibility, List modifiers, String classDeclaration, + Consumer classAction) { + String text = CoreUtils.isNullOrEmpty(modifiers) + ? visibility + " class " + classDeclaration + : visibility + " " + toString(modifiers) + " class " + classDeclaration; + block(text, blockAction -> { + if (classAction != null) { + JavaClass javaClass = new JavaClass(this); + classAction.accept(javaClass); + } + }); + } + + public void method(JavaVisibility visibility, List modifiers, String methodSignature, + Consumer method) { + String text = CoreUtils.isNullOrEmpty(modifiers) + ? visibility + " " + methodSignature + : visibility + " " + toString(modifiers) + " " + methodSignature; + + block(text, method); + } + + public void constructor(JavaVisibility visibility, String constructorSignature, Consumer constructor) { + block(visibility + " " + constructorSignature, constructor); + } + + public void enumBlock(JavaVisibility visibility, String enumName, Consumer enumAction) { + block(visibility + " enum " + enumName, block -> { + if (enumAction != null) { + JavaEnum javaEnum = new JavaEnum(this); + enumAction.accept(javaEnum); + javaEnum.addExpectedNewLineAfterLastValue(); + } + }); + } + + public void interfaceBlock(JavaVisibility visibility, String interfaceSignature, Consumer interfaceAction) { + line(visibility + " interface " + interfaceSignature + " {"); + indent(() -> interfaceAction.accept(new JavaInterface(this))); + line("}"); + } + + public void ifBlock(String condition, Consumer ifAction) { + line("if (" + condition + ") {"); + indent(() -> ifAction.accept(new JavaBlock(this))); + text("}"); + currentLineType = CurrentLineType.AfterIf; + } + + public void elseIfBlock(String condition, Consumer ifAction) { + line(" else if (" + condition + ") {", false); + indent(() -> ifAction.accept(new JavaBlock(this))); + text("}"); + currentLineType = CurrentLineType.AfterIf; + } + + public void elseBlock(Consumer elseAction) { + line(" else {", false); + indent(() -> elseAction.accept(new JavaBlock(this))); + line("}"); + } + + public void tryBlock(Consumer tryAction) { + line("try {"); + indent(() -> tryAction.accept(new JavaBlock(this))); + text("}"); + currentLineType = CurrentLineType.AfterIf; + } + + public void tryBlock(String resource, Consumer tryAction) { + line("try (" + resource + ") {"); + indent(() -> tryAction.accept(new JavaBlock(this))); + text("}"); + currentLineType = CurrentLineType.AfterIf; + } + + public void catchBlock(String exception, Consumer catchAction) { + line(" catch (" + exception + ") {", false); + indent(() -> catchAction.accept(new JavaBlock(this))); + line("}"); + currentLineType = CurrentLineType.AfterIf; + } + + public void finallyBlock(Consumer finallyAction) { + line(" finally {", false); + indent(() -> finallyAction.accept(new JavaBlock(this))); + line("}"); + } + + public void lambda(String parameterType, String parameterName, Consumer body) { + text("(" + parameterType + " " + parameterName + ") -> "); + try (JavaLambda lambda = new JavaLambda(this)) { + body.accept(lambda); + } + } + + public void lambda(String parameterType, String parameterName, String returnExpression) { + lambda(parameterType, parameterName, lambda -> lambda.lambdaReturn(returnExpression)); + } + + private enum CurrentLineType { + Empty, + AfterIf, + Text + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/javamodel/JavaFileFactory.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/javamodel/JavaFileFactory.java new file mode 100644 index 0000000000..5b4aaa0396 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/javamodel/JavaFileFactory.java @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.model.javamodel; + +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; + +import java.io.File; +import java.nio.file.Paths; + +public class JavaFileFactory { + private final JavaSettings settings; + + public JavaFileFactory(JavaSettings settings) { + this.settings = settings; + } + + public final JavaFile createEmptySourceFile(String packageKeyword, String fileNameWithoutExtension) { + String folderPath = Paths.get("src", "main", "java", packageKeyword.replace('.', File.separatorChar)).toString(); + String filePath = Paths.get(folderPath).resolve(String.format("%1$s.java", fileNameWithoutExtension)).toString().replace('\\', '/').replace("//", "/"); + return new JavaFile(filePath); + } + + public final JavaFile createSourceFile(String packageKeyword, String fileNameWithoutExtension) { + JavaFile javaFile = createEmptySourceFile(packageKeyword, fileNameWithoutExtension); + + addCommentAndPackage(javaFile, packageKeyword); + + return javaFile; + } + + public final JavaFile createSampleFile(String packageKeyword, String fileNameWithoutExtension) { + String folderPath = Paths.get("src", "samples", "java", packageKeyword.replace('.', File.separatorChar)).toString(); + String filePath = Paths.get(folderPath).resolve(String.format("%1$s.java", fileNameWithoutExtension)).toString().replace('\\', '/').replace("//", "/"); + JavaFile javaFile = new JavaFile(filePath); + + addCommentAndPackage(javaFile, packageKeyword); + + return javaFile; + } + + public final JavaFile createTestFile(String packageKeyword, String fileNameWithoutExtension) { + String folderPath = Paths.get("src", "test", "java", packageKeyword.replace('.', File.separatorChar)).toString(); + String filePath = Paths.get(folderPath).resolve(String.format("%1$s.java", fileNameWithoutExtension)).toString().replace('\\', '/').replace("//", "/"); + JavaFile javaFile = new JavaFile(filePath); + + addCommentAndPackage(javaFile, packageKeyword); + + return javaFile; + } + + private void addCommentAndPackage(JavaFile javaFile, String packageName) { + String headerComment = settings.getFileHeaderText(); + if (headerComment != null && !headerComment.isEmpty()) { + javaFile.lineComment(comment -> comment.line(headerComment)); + javaFile.line(); + } + + javaFile.declarePackage(packageName); + javaFile.line(); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/javamodel/JavaIfBlock.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/javamodel/JavaIfBlock.java new file mode 100644 index 0000000000..dda79bfae3 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/javamodel/JavaIfBlock.java @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.model.javamodel; + +import java.util.function.Consumer; + +public class JavaIfBlock { + private final JavaFileContents contents; + + public JavaIfBlock(JavaFileContents contents) { + this.contents = contents; + } + + public final JavaIfBlock elseIfBlock(String condition, Consumer ifAction) { + contents.elseIfBlock(condition, ifAction); + return new JavaIfBlock(contents); + } + + public final void elseBlock(Consumer elseAction) { + contents.elseBlock(elseAction); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/javamodel/JavaImportComparer.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/javamodel/JavaImportComparer.java new file mode 100644 index 0000000000..aac3ccbb02 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/javamodel/JavaImportComparer.java @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.model.javamodel; + +import java.util.Comparator; +import java.util.Objects; + +public class JavaImportComparer implements Comparator { + private static String[] getImportParts(String importKeyword) { + return importKeyword.split("\\.", -1); + } + + private static boolean isLastPart(int importIndex, String[] importParts) { + return importIndex == importParts.length - 1; + } + + public final int compare(String lhsImport, String rhsImport) { + int result; + + if (Objects.equals(lhsImport, rhsImport)) { + result = 0; + } else if (lhsImport == null) { + result = -1; + } else if (rhsImport == null) { + result = 1; + } else { + result = 0; + + String[] lhsImportParts = getImportParts(lhsImport); + String[] rhsImportParts = getImportParts(rhsImport); + + int minimumImportPartCount = Math.min(lhsImportParts.length, rhsImportParts.length); + for (int i = 0; i < minimumImportPartCount; ++i) { + int caseInsensitiveCompareTo = lhsImportParts[i].compareToIgnoreCase(rhsImportParts[i]); + + if (caseInsensitiveCompareTo != 0) { + boolean isLastLhsPart = isLastPart(i, lhsImportParts); + boolean isLastRhsPart = isLastPart(i, rhsImportParts); + if (isLastLhsPart != isLastRhsPart) { + return isLastLhsPart ? -1 : 1; + } else { + return caseInsensitiveCompareTo; + } + } + } + } + + return result; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/javamodel/JavaInterface.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/javamodel/JavaInterface.java new file mode 100644 index 0000000000..e2c474d279 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/javamodel/JavaInterface.java @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.model.javamodel; + +import java.util.function.Consumer; + +public class JavaInterface implements JavaType { + private final JavaFileContents contents; + private boolean addNewLine; + + public JavaInterface(JavaFileContents contents) { + this.contents = contents; + } + + private void addExpectedNewLine() { + if (addNewLine) { + contents.line(); + addNewLine = false; + } + } + + public final void publicMethod(String methodSignature) { + publicMethod(methodSignature, null); + } + + public final void publicMethod(String methodSignature, Consumer functionBlock) { + addExpectedNewLine(); + contents.line(methodSignature + ";"); + + addNewLine = true; + } + + public final void javadocComment(Consumer commentAction) { + addExpectedNewLine(); + contents.javadocComment(commentAction); + } + + public final void lineComment(String comment) { + addExpectedNewLine(); + contents.lineComment(comment); + } + + public final void annotation(String... annotations) { + addExpectedNewLine(); + contents.annotation(annotations); + } + + public final void interfaceBlock(String interfaceName, Consumer interfaceAction) { + contents.interfaceBlock(JavaVisibility.PackagePrivate, interfaceName, interfaceAction); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/javamodel/JavaJavadocComment.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/javamodel/JavaJavadocComment.java new file mode 100644 index 0000000000..09ec10ca1a --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/javamodel/JavaJavadocComment.java @@ -0,0 +1,99 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.model.javamodel; + +import com.microsoft.typespec.http.client.generator.core.util.CodeNamer; + +import java.util.regex.Pattern; + +public class JavaJavadocComment { + private final JavaFileContents contents; + private boolean expectsLineSeparator; + + // escape the "@" in Javadoc description, if it is not used in inline tag like {@link } + private static final Pattern ESCAPE_AT = Pattern.compile("(?The {@literal &, <, >} characters would be encoded. + * The {@literal @} would also be encoded if not used in inline tags.

+ * + * @param description the Javadoc description. + */ + public final void description(String description) { + String processedText = processText(description); + line(processedText); + } + + /** + * Adds a line to Javadoc. + *

The characters in the line is not encoded. + * This API should not be used to write text from external source, e.g. Swagger or TypeSpec.

+ * + * @param text the line to be written to Javadoc. + */ + public final void line(String text) { + if (text != null && !text.isEmpty()) { + contents.line(text); + expectsLineSeparator = true; + } + } + + public final void param(String parameterName, String parameterDescription) { + addExpectedLineSeparator(); + contents.line("@param " + parameterName + " " + processText(parameterDescription)); + } + + public final void methodReturns(String returnValueDescription) { + if (returnValueDescription != null && !returnValueDescription.isEmpty()) { + addExpectedLineSeparator(); + contents.line("@return " + processText(returnValueDescription)); + } + } + + public final void methodThrows(String exceptionTypeName, String description) { + addExpectedLineSeparator(); + contents.line("@throws " + exceptionTypeName + " " + processText(description)); + } + + public final void inheritDoc() { + addExpectedLineSeparator(); + contents.line("{@inheritDoc}"); + } + + public final void deprecated(String description) { + addExpectedLineSeparator(); + contents.line("@deprecated " + description); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/javamodel/JavaLambda.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/javamodel/JavaLambda.java new file mode 100644 index 0000000000..b4fb45f0a7 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/javamodel/JavaLambda.java @@ -0,0 +1,67 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.model.javamodel; + +import java.io.Closeable; +import java.util.function.Consumer; + +public class JavaLambda implements Closeable { + private final JavaFileContents contents; + private boolean isFirstStatement; + private boolean needsClosingCurlyBracket; + + public JavaLambda(JavaFileContents contents) { + this.contents = contents; + isFirstStatement = true; + needsClosingCurlyBracket = false; + } + + private void nonReturnStatement() { + if (isFirstStatement) { + isFirstStatement = false; + + contents.line("{"); + contents.increaseIndent(); + needsClosingCurlyBracket = true; + } + } + + public final void close() { + if (needsClosingCurlyBracket) { + contents.decreaseIndent(); + contents.text("}"); + } + } + + public final void line(String text) { + nonReturnStatement(); + contents.line(text); + } + + public final void line(String format, Object... args) { + line(String.format(format, args)); + } + + public final void increaseIndent() { + contents.increaseIndent(); + } + + public final void decreaseIndent() { + contents.decreaseIndent(); + } + + public final JavaIfBlock ifBlock(String condition, Consumer ifAction) { + nonReturnStatement(); + contents.ifBlock(condition, ifAction); + return new JavaIfBlock(contents); + } + + public final void lambdaReturn(String text) { + if (isFirstStatement) { + contents.text(text); + } else { + contents.methodReturn(text); + } + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/javamodel/JavaLineComment.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/javamodel/JavaLineComment.java new file mode 100644 index 0000000000..d09833ddf9 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/javamodel/JavaLineComment.java @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.model.javamodel; + +import com.microsoft.typespec.http.client.generator.core.util.CodeNamer; + +public class JavaLineComment { + private final JavaFileContents contents; + + public JavaLineComment(JavaFileContents contents) { + this.contents = contents; + } + + public final void line(String text) { + contents.line(CodeNamer.escapeComment(text)); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/javamodel/JavaModifier.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/javamodel/JavaModifier.java new file mode 100644 index 0000000000..dd24668264 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/javamodel/JavaModifier.java @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.model.javamodel; + +/** + * Modifiers that can be applied to Java types or members. + */ +public enum JavaModifier { + Final("final"), + + Static("static"), + Abstract("abstract"); + + private final String keyword; + + JavaModifier(String keyword) { + this.keyword = keyword; + } + + public int getValue() { + return this.ordinal(); + } + + @Override + public String toString() { + return keyword; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/javamodel/JavaPackage.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/javamodel/JavaPackage.java new file mode 100644 index 0000000000..ae0b2c724e --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/javamodel/JavaPackage.java @@ -0,0 +1,353 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.model.javamodel; + +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; +import com.microsoft.typespec.http.client.generator.core.extension.plugin.NewPlugin; +import com.microsoft.typespec.http.client.generator.core.extension.plugin.PluginLogger; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.AsyncSyncClient; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientBuilder; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientException; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethodExample; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientModel; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientResponse; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.EnumType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.GraalVmConfig; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.MethodGroupClient; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ModuleInfo; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.PackageInfo; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.Pom; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ProtocolExample; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ServiceClient; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ServiceVersion; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.TestContext; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.UnionModel; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.XmlSequenceWrapper; +import com.microsoft.typespec.http.client.generator.core.model.projectmodel.Project; +import com.microsoft.typespec.http.client.generator.core.model.projectmodel.TextFile; +import com.microsoft.typespec.http.client.generator.core.model.xmlmodel.XmlFile; +import com.microsoft.typespec.http.client.generator.core.template.ChangelogTemplate; +import com.microsoft.typespec.http.client.generator.core.template.ClientMethodTestTemplate; +import com.microsoft.typespec.http.client.generator.core.template.ModelTestTemplate; +import com.microsoft.typespec.http.client.generator.core.template.ProtocolSampleBlankTemplate; +import com.microsoft.typespec.http.client.generator.core.template.ProtocolTestBaseTemplate; +import com.microsoft.typespec.http.client.generator.core.template.ProtocolTestTemplate; +import com.microsoft.typespec.http.client.generator.core.template.ReadmeTemplate; +import com.microsoft.typespec.http.client.generator.core.template.ServiceSyncClientTemplate; +import com.microsoft.typespec.http.client.generator.core.template.SwaggerReadmeTemplate; +import com.microsoft.typespec.http.client.generator.core.template.Templates; +import com.microsoft.typespec.http.client.generator.core.template.TestProxyAssetsTemplate; +import com.microsoft.typespec.http.client.generator.core.util.ClientModelUtil; +import com.microsoft.typespec.http.client.generator.core.util.PossibleCredentialException; +import org.slf4j.Logger; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; + +public class JavaPackage { + private final Logger logger; + + private final JavaSettings settings; + private final List javaFiles; + private final List xmlFiles; + protected final List textFiles = new ArrayList<>(); + + private final JavaFileFactory javaFileFactory; + + private final Set filePaths = new HashSet<>(); + + public JavaPackage(NewPlugin host) { + this.settings = JavaSettings.getInstance(); + this.javaFiles = new ArrayList<>(); + this.xmlFiles = new ArrayList<>(); + this.javaFileFactory = new JavaFileFactory(settings); + this.logger = new PluginLogger(host, JavaPackage.class); + } + + protected JavaFileFactory getJavaFileFactory() { + return javaFileFactory; + } + + public List getJavaFiles() { + return javaFiles; + } + + public List getXmlFiles() { + return xmlFiles; + } + + public List getTextFiles() { + return textFiles; + } + + public final void addServiceClient(String packageKeyword, String name, ServiceClient model) { + JavaFile javaFile = javaFileFactory.createSourceFile(packageKeyword, name); + Templates.getServiceClientTemplate().write(model, javaFile); + addJavaFile(javaFile); + } + + public final void addAsyncServiceClient(String packageKeyWord, AsyncSyncClient asyncClient) { + JavaFile javaFile = javaFileFactory.createSourceFile(packageKeyWord, asyncClient.getClassName()); + Templates.getServiceAsyncClientTemplate().write(asyncClient, javaFile); + addJavaFile(javaFile); + } + + public final void addSyncServiceClient(String packageKeyWord, AsyncSyncClient syncClient) { + addSyncServiceClient(packageKeyWord, syncClient, false); + } + + public final void addSyncServiceClient(String packageKeyWord, AsyncSyncClient syncClient, boolean syncClientWrapAsync) { + JavaFile javaFile = javaFileFactory.createSourceFile(packageKeyWord, syncClient.getClassName()); + ServiceSyncClientTemplate template = syncClientWrapAsync + ? Templates.getServiceSyncClientWrapAsyncClientTemplate() + : Templates.getServiceSyncClientTemplate(); + template.write(syncClient, javaFile); + addJavaFile(javaFile); + } + + public final void addServiceClientInterface(String name, ServiceClient model) { + JavaFile javaFile = javaFileFactory.createSourceFile(settings.getPackage(), name); + Templates.getServiceClientInterfaceTemplate().write(model, javaFile); + addJavaFile(javaFile); + } + + public final void addServiceClientInterface(String packageKeyword, String name, ServiceClient model) { + JavaFile javaFile = javaFileFactory.createSourceFile(packageKeyword, name); + Templates.getServiceClientInterfaceTemplate().write(model, javaFile); + addJavaFile(javaFile); + } + + public final void addServiceClientBuilder(ClientBuilder model) { + JavaFile javaFile = javaFileFactory.createSourceFile(model.getPackageName(), model.getClassName()); + Templates.getServiceClientBuilderTemplate().write(model, javaFile); + addJavaFile(javaFile); + } + + public final void addServiceVersion(String packageKeyword, ServiceVersion serviceVersion) { + JavaFile javaFile = javaFileFactory.createSourceFile(packageKeyword, serviceVersion.getClassName()); + Templates.getServiceVersionTemplate().write(serviceVersion, javaFile); + addJavaFile(javaFile); + } + + public final void addMethodGroup(String packageKeyword, String name, MethodGroupClient model) { + JavaFile javaFile = javaFileFactory.createSourceFile(packageKeyword, name); + Templates.getMethodGroupTemplate().write(model, javaFile); + addJavaFile(javaFile); + } + + public final void addMethodGroupInterface(String name, MethodGroupClient model) { + JavaFile javaFile = javaFileFactory.createSourceFile(settings.getPackage(), name); + Templates.getMethodGroupInterfaceTemplate().write(model, javaFile); + addJavaFile(javaFile); + } + + public final void addMethodGroupInterface(String packageKeyword, String name, MethodGroupClient model) { + JavaFile javaFile = javaFileFactory.createSourceFile(packageKeyword, name); + Templates.getMethodGroupInterfaceTemplate().write(model, javaFile); + addJavaFile(javaFile); + } + + public final void addModel(String packageKeyword, String name, ClientModel model) { + JavaFile javaFile = javaFileFactory.createSourceFile(packageKeyword, name); + + if (settings.isStreamStyleSerialization()) { + Templates.getStreamStyleModelTemplate().write(model, javaFile); + } else { + Templates.getModelTemplate().write(model, javaFile); + } + + addJavaFile(javaFile); + } + + public final void addException(String packageKeyword, String name, ClientException model) { + JavaFile javaFile = javaFileFactory.createSourceFile(packageKeyword, name); + Templates.getExceptionTemplate().write(model, javaFile); + addJavaFile(javaFile); + } + + public final void addEnum(String packageKeyword, String name, EnumType model) { + JavaFile javaFile = javaFileFactory.createSourceFile(packageKeyword, name); + Templates.getEnumTemplate().write(model, javaFile); + addJavaFile(javaFile); + } + + public final void addClientResponse(String packageKeyword, String name, ClientResponse model) { + JavaFile javaFile = javaFileFactory.createSourceFile(packageKeyword, name); + Templates.getResponseTemplate().write(model, javaFile); + addJavaFile(javaFile); + } + + public final void addXmlSequenceWrapper(String packageKeyword, String name, XmlSequenceWrapper model) { + JavaFile javaFile = javaFileFactory.createSourceFile(packageKeyword, name); + Templates.getXmlSequenceWrapperTemplate().write(model, javaFile); + addJavaFile(javaFile); + } + + public final void addUnionModel(UnionModel model) { + JavaFile javaFile = javaFileFactory.createSourceFile(model.getPackage(), model.getName()); + Templates.getUnionModelTemplate().write(model, javaFile); + addJavaFile(javaFile); + } + + public final void addPackageInfo(String packageKeyword, String name, PackageInfo model) { + JavaFile javaFile = javaFileFactory.createEmptySourceFile(packageKeyword, name); + Templates.getPackageInfoTemplate().write(model, javaFile); + addJavaFile(javaFile); + } + + public final void addModuleInfo(ModuleInfo moduleInfo) { + JavaFile javaFile = javaFileFactory.createEmptySourceFile("", "module-info"); + Templates.getModuleInfoTemplate().write(moduleInfo, javaFile); + addJavaFile(javaFile); + } + + public final void addPom(String name, Pom pom) { + XmlFile xmlFile = new XmlFile(name, new XmlFile.Options().setIndent(2)); + Templates.getPomTemplate().write(pom, xmlFile); + this.checkDuplicateFile(xmlFile.getFilePath()); + xmlFiles.add(xmlFile); + } + + public final void addJavaFromResources(String packageName, String name) { + addJavaFromResources(packageName, name, name); + } + + public final void addJavaFromResources(String packageName, String resourceName, String fileName) { + JavaFile javaFile = javaFileFactory.createSourceFile(packageName, fileName); + try (InputStream inputStream = JavaPackage.class.getClassLoader().getResourceAsStream(resourceName + ".java"); + BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream))) { + Iterator linesIterator = bufferedReader.lines().iterator(); + while (linesIterator.hasNext()) { + javaFile.line(linesIterator.next()); + } + } catch (IOException e) { + throw new RuntimeException("Failed to read " + resourceName + ".java from resources.", e); + } + addJavaFile(javaFile); + } + + protected void addJavaFile(JavaFile javaFile) { + this.checkDuplicateFile(javaFile.getFilePath()); + filePaths.add(javaFile.getFilePath()); + javaFiles.add(javaFile); + } + + public void addProtocolExamples(ProtocolExample protocolExample) { + JavaFile javaFile = javaFileFactory.createSampleFile(settings.getPackage("generated"), protocolExample.getFilename()); + Templates.getProtocolSampleTemplate().write(protocolExample, javaFile); + this.checkDuplicateFile(javaFile.getFilePath()); + javaFiles.add(javaFile); + } + + public void addClientMethodExamples(ClientMethodExample clientMethodExample) { + JavaFile javaFile = javaFileFactory.createSampleFile(settings.getPackage("generated"), clientMethodExample.getFilename()); + Templates.getClientMethodSampleTemplate().write(clientMethodExample, javaFile); + this.checkDuplicateFile(javaFile.getFilePath()); + javaFiles.add(javaFile); + } + + public void addProtocolExamplesBlank() { + JavaFile javaFile = javaFileFactory.createSampleFile(settings.getPackage(), "ReadmeSamples"); + new ProtocolSampleBlankTemplate().write(null, javaFile); + this.checkDuplicateFile(javaFile.getFilePath()); + javaFiles.add(javaFile); + } + + public void addProtocolTestBase(TestContext testContext) { + JavaFile javaFile = javaFileFactory.createTestFile(testContext.getPackageName(), testContext.getTestBaseClassName()); + ProtocolTestBaseTemplate.getInstance().write(testContext, javaFile); + this.checkDuplicateFile(javaFile.getFilePath()); + javaFiles.add(javaFile); + } + + public void addProtocolTest(TestContext testContext) { + String className = testContext.getTestCase().getFilename() + "Tests"; + JavaFile javaFile = javaFileFactory.createTestFile(testContext.getPackageName(), className); + ProtocolTestTemplate.getInstance().write(testContext, javaFile); + this.checkDuplicateFile(javaFile.getFilePath()); + javaFiles.add(javaFile); + } + + public void addClientMethodTest(TestContext testContext) { + String className = testContext.getTestCase().getFilename() + "Tests"; + JavaFile javaFile = javaFileFactory.createTestFile(testContext.getPackageName(), className); + ClientMethodTestTemplate.getInstance().write(testContext, javaFile); + this.checkDuplicateFile(javaFile.getFilePath()); + javaFiles.add(javaFile); + } + + public void addModelUnitTest(ClientModel model) { + try { + String className = model.getName() + "Tests"; + JavaFile javaFile = javaFileFactory.createTestFile(JavaSettings.getInstance().getPackage("generated"), className); + ModelTestTemplate.getInstance().write(model, javaFile); + this.checkDuplicateFile(javaFile.getFilePath()); + javaFiles.add(javaFile); + } catch (PossibleCredentialException e) { + // skip this test file + logger.warn("Skip unit test for model '{}', caused by key '{}'", model.getName(), e.getKeyName()); + } + } + + public void addReadmeMarkdown(Project project) { + TextFile textFile = new TextFile("README.md", new ReadmeTemplate().write(project)); + this.checkDuplicateFile(textFile.getFilePath()); + textFiles.add(textFile); + } + + public void addSwaggerReadmeMarkdown(Project project) { + TextFile textFile = new TextFile("swagger/README.md", new SwaggerReadmeTemplate().write(project)); + this.checkDuplicateFile(textFile.getFilePath()); + textFiles.add(textFile); + } + + public void addChangelogMarkdown(Project project) { + TextFile textFile = new TextFile("CHANGELOG.md", new ChangelogTemplate().write(project)); + this.checkDuplicateFile(textFile.getFilePath()); + textFiles.add(textFile); + } + + public void addTestProxyAssetsJson(Project project) { + TextFile textFile = new TextFile("assets.json", new TestProxyAssetsTemplate().write(project)); + this.checkDuplicateFile(textFile.getFilePath()); + textFiles.add(textFile); + } + + public final void addGraalVmConfig(String groupId, String artifactId, GraalVmConfig graalVmConfig) { + String metaInfPath = Paths.get("src", "main", "resources", "META-INF", "native-image", groupId, artifactId).toString(); + + TextFile proxyConfigFile = new TextFile(Paths.get(metaInfPath, "proxy-config.json").toString(), graalVmConfig.toProxyConfigJson()); + textFiles.add(proxyConfigFile); + + TextFile reflectConfigFile = new TextFile(Paths.get(metaInfPath, "reflect-config.json").toString(), graalVmConfig.toReflectConfigJson()); + textFiles.add(reflectConfigFile); + + if (graalVmConfig.generateResourceConfig()) { + TextFile resourceConfigFile = new TextFile(Paths.get(metaInfPath, "resource-config.json").toString(), graalVmConfig.toResourceConfigJson(artifactId)); + textFiles.add(resourceConfigFile); + } + } + + protected void checkDuplicateFile(String filePath) { + if (filePaths.contains(filePath)) { +// throw new IllegalStateException(String.format("Name conflict for output file '%1$s'.", filePath)); + logger.warn(String.format("Name conflict for output file '%1$s'.", filePath)); + } + } + + public void addJsonMergePatchHelper(List models) { + JavaFile javaFile = javaFileFactory.createSourceFile(settings.getPackage(settings.getImplementationSubpackage()), ClientModelUtil.JSON_MERGE_PATCH_HELPER_CLASS_NAME); + Templates.getJsonMergePatchHelperTemplate().write(models, javaFile); + this.checkDuplicateFile(javaFile.getFilePath()); + javaFiles.add(javaFile); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/javamodel/JavaTryBlock.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/javamodel/JavaTryBlock.java new file mode 100644 index 0000000000..3b2b9a8072 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/javamodel/JavaTryBlock.java @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.model.javamodel; + +import java.util.function.Consumer; + +public class JavaTryBlock { + private final JavaFileContents contents; + + public JavaTryBlock(JavaFileContents contents) { + this.contents = contents; + } + + public final JavaCatchBlock catchBlock(String exception, Consumer catchAction) { + contents.catchBlock(exception, catchAction); + return new JavaCatchBlock(contents); + } + + public final void finallyBlock(Consumer finallyAction) { + contents.finallyBlock(finallyAction); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/javamodel/JavaType.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/javamodel/JavaType.java new file mode 100644 index 0000000000..bfc4d1136f --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/javamodel/JavaType.java @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.model.javamodel; + +import java.util.function.Consumer; + +public interface JavaType extends JavaContext { + void publicMethod(String methodSignature, Consumer functionBlock); +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/javamodel/JavaVisibility.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/javamodel/JavaVisibility.java new file mode 100644 index 0000000000..7b6e9d349c --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/javamodel/JavaVisibility.java @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.model.javamodel; + +/** + * The visibility of a Java type or member. + */ +public enum JavaVisibility { + Public("public"), + + Protected("protected"), + + Private("private"), + + PackagePrivate(""); + + private final String keyword; + + JavaVisibility(String keyword) { + this.keyword = keyword; + } + + + @Override + public String toString() { + return keyword; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/projectmodel/Project.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/projectmodel/Project.java new file mode 100644 index 0000000000..9705c077ec --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/projectmodel/Project.java @@ -0,0 +1,436 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.model.projectmodel; + +import com.microsoft.typespec.http.client.generator.core.Javagen; +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; +import com.microsoft.typespec.http.client.generator.core.extension.plugin.PluginLogger; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.Client; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ExternalPackage; +import com.microsoft.typespec.http.client.generator.core.template.TemplateHelper; +import com.microsoft.typespec.http.client.generator.core.util.ClientModelUtil; +import com.azure.core.util.CoreUtils; +import org.slf4j.Logger; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.w3c.dom.Text; +import org.xml.sax.SAXException; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import java.io.BufferedReader; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +public class Project { + + private static final Logger LOGGER = new PluginLogger(Javagen.getPluginInstance(), Project.class); + + public static final String AZURE_GROUP_ID = ExternalPackage.CORE.getGroupId(); + + protected String serviceName; + protected String serviceDescription; + protected String namespace; + protected String groupId = AZURE_GROUP_ID; + protected String artifactId; + protected String version = "1.0.0-beta.1"; + protected final List pomDependencyIdentifiers = new ArrayList<>(); + protected String sdkRepositoryPath; + + private List apiVersions; + + private boolean integratedWithSdk = false; + + public enum Dependency { + // azure + AZURE_CLIENT_SDK_PARENT("com.azure", "azure-client-sdk-parent", "1.7.0"), + AZURE_JSON("com.azure", "azure-json", "1.2.0"), + AZURE_XML("com.azure", "azure-xml", "1.1.0"), + AZURE_CORE("com.azure", "azure-core", "1.51.0"), + AZURE_CORE_MANAGEMENT("com.azure", "azure-core-management", "1.15.2"), + AZURE_CORE_HTTP_NETTY("com.azure", "azure-core-http-netty", "1.15.3"), + AZURE_CORE_TEST("com.azure", "azure-core-test", "1.26.2"), + AZURE_IDENTITY("com.azure", "azure-identity", "1.13.2"), + AZURE_CORE_EXPERIMENTAL("com.azure", "azure-core-experimental", "1.0.0-beta.52"), + + CLIENTCORE("io.clientcore", "core", "1.0.0-beta.1"), + CLIENTCORE_JSON("io.clientcore", "core-json", "1.0.0-beta.1"), + + // external + JUNIT_JUPITER_API("org.junit.jupiter", "junit-jupiter-api", "5.9.3"), + JUNIT_JUPITER_ENGINE("org.junit.jupiter", "junit-jupiter-engine", "5.9.3"), + MOCKITO_CORE("org.mockito", "mockito-core", "4.11.0"), + BYTE_BUDDY("net.bytebuddy", "byte-buddy", "1.14.12"), + BYTE_BUDDY_AGENT("net.bytebuddy", "byte-buddy-agent", "1.14.12"), + SLF4J_SIMPLE("org.slf4j", "slf4j-simple", "1.7.36"); + + private final String groupId; + private final String artifactId; + private String version; // version could be updated in place, from "version_client.txt" or "external_dependencies.txt", on findPackageVersions method + + Dependency(String groupId, String artifactId, String defaultVersion) { + this.groupId = groupId; + this.artifactId = artifactId; + this.version = defaultVersion; + } + + public String getGroupId() { + return groupId; + } + + public String getArtifactId() { + return artifactId; + } + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + public String getDependencyIdentifier() { + return String.format("%s:%s:%s", groupId, artifactId, version); + } + } + + protected Project() { + } + + public Project(Client client, List apiVersions) { + JavaSettings settings = JavaSettings.getInstance(); + String serviceName = settings.getServiceName(); + if (CoreUtils.isNullOrEmpty(serviceName)) { + serviceName = client.getClientName(); + } + + this.serviceName = serviceName; + this.namespace = JavaSettings.getInstance().getPackage(); + this.artifactId = ClientModelUtil.getArtifactId(); + + this.serviceDescription = TemplateHelper.getPomProjectDescription(serviceName); + + this.apiVersions = apiVersions; + } + + // TODO (weidxu): this method likely will get refactored when we support external model (hence external package) + public void checkForAdditionalDependencies(Set externalPackageNames) { + // currently, only check for azure-core-experimental + if (externalPackageNames.stream().anyMatch(p -> p.startsWith("com.azure.core.experimental"))) { + // add to pomDependencyIdentifiers is not already there + if (this.pomDependencyIdentifiers.stream() + .noneMatch(identifier -> identifier.startsWith(Dependency.AZURE_CORE_EXPERIMENTAL.getGroupId() + ":" + Dependency.AZURE_CORE_EXPERIMENTAL.getArtifactId() + ":"))) { + this.pomDependencyIdentifiers.add(Dependency.AZURE_CORE_EXPERIMENTAL.getDependencyIdentifier()); + } + } + } + + public void integrateWithSdk() { + findPackageVersions(); + + findPomDependencies(); + + findSdkRepositoryUri(); + } + + protected void findSdkRepositoryUri() { + JavaSettings settings = JavaSettings.getInstance(); + String outputFolder = settings.getAutorestSettings().getOutputFolder(); + if (outputFolder != null) { + Path path = Paths.get(outputFolder).normalize(); + List pathSegment = new ArrayList<>(); + while (path != null) { + if (path.getFileName() == null) { + // likely the case of "C:\" + path = null; + break; + } + + Path childPath = path; + path = path.getParent(); + + pathSegment.add(childPath.getFileName().toString()); + + if (isRepoSdkFolder(childPath)) { + // childPath = azure-sdk-for-java/sdk, path = azure-sdk-for-java + break; + } + } + if (path != null) { + Collections.reverse(pathSegment); + sdkRepositoryPath = String.join("/", pathSegment); + LOGGER.info("Repository path '{}' deduced from 'output-folder' parameter", sdkRepositoryPath); + } + } + } + + private String findSdkFolder() { + JavaSettings settings = JavaSettings.getInstance(); + String sdkFolderOpt = settings.getAutorestSettings().getJavaSdksFolder(); + if (sdkFolderOpt == null) { + LOGGER.info("'java-sdks-folder' parameter not available"); + } else { + if (!Paths.get(sdkFolderOpt).isAbsolute()) { + LOGGER.info("'java-sdks-folder' parameter is not an absolute path"); + sdkFolderOpt = null; + } + } + + // try to deduct it from "output-folder" + if (sdkFolderOpt == null) { + String outputFolder = settings.getAutorestSettings().getOutputFolder(); + if (outputFolder != null && Paths.get(outputFolder).isAbsolute()) { + Path path = Paths.get(outputFolder).normalize(); + while (path != null) { + if (path.getFileName() == null) { + // likely the case of "C:\" + path = null; + break; + } + + Path childPath = path; + path = path.getParent(); + + if (isRepoSdkFolder(childPath)) { + // childPath = azure-sdk-for-java/sdk, path = azure-sdk-for-java + break; + } + } + if (path != null) { + LOGGER.info("'azure-sdk-for-java' SDK folder '{}' deduced from 'output-folder' parameter", path.toString()); + sdkFolderOpt = path.toString(); + } + } + } + + if (sdkFolderOpt == null) { + LOGGER.warn("'azure-sdk-for-java' SDK folder not found, fallback to default versions for dependencies"); + } + + return sdkFolderOpt; + } + + private static boolean isRepoSdkFolder(Path path) { + boolean ret = false; + if (path.getFileName() != null && "sdk".equals(path.getFileName().toString())) { + Path parentPomPath = path.resolve("parents/azure-client-sdk-parent/pom.xml"); + if (parentPomPath.toFile().isFile()) { + ret = true; + } + } + return ret; + } + + private static final Map VERSION_UPDATE_TAG_MAP = Map.of( + // see https://github.com/Azure/azure-sdk-for-java/blob/main/eng/versioning/external_dependencies.txt + "net.bytebuddy:byte-buddy", "testdep_net.bytebuddy:byte-buddy", + "net.bytebuddy:byte-buddy-agent", "testdep_net.bytebuddy:byte-buddy-agent" + ); + + /** + * Gets the version update tag (x-version-update) for the groupId and artifactId. + * + * @param groupId the group ID. + * @param artifactId the artifact ID. + * @return the version update tag. + */ + public static String getVersionUpdateTag(String groupId, String artifactId) { + String tag = groupId + ":" + artifactId; + String ret = VERSION_UPDATE_TAG_MAP.get(tag); + return ret == null ? tag : ret; + } + + protected void findPackageVersions() { + String sdkFolderOpt = findSdkFolder(); + this.integratedWithSdk = sdkFolderOpt != null; + if (sdkFolderOpt == null) { + return; + } + + // find dependency version from versioning txt + Path sdkPath = Paths.get(sdkFolderOpt); + Path versionClientPath = sdkPath.resolve(Paths.get("eng", "versioning", "version_client.txt")); + Path versionExternalPath = sdkPath.resolve(Paths.get("eng", "versioning", "external_dependencies.txt")); + if (Files.isReadable(versionClientPath) && Files.isReadable(versionExternalPath)) { + try { + findPackageVersions(versionClientPath); + } catch (IOException e) { + LOGGER.warn("Failed to parse 'version_client.txt'", e); + } + try { + findPackageVersions(versionExternalPath); + } catch (IOException e) { + LOGGER.warn("Failed to parse 'external_dependencies.txt'", e); + } + } else { + LOGGER.warn("'version_client.txt' or 'external_dependencies.txt' not found or not readable"); + } + } + + private void findPackageVersions(Path path) throws IOException { + try (BufferedReader reader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) { + reader.lines().forEach(line -> { + for (Dependency dependency : Dependency.values()) { + String artifact = getVersionUpdateTag(dependency.getGroupId(), dependency.getArtifactId()); + checkArtifact(line, artifact).ifPresent(dependency::setVersion); + } + }); + } + } + + public static Optional checkArtifact(String line, String artifact) { + if (line.startsWith(artifact + ";")) { + String[] segments = line.split(";"); + if (segments.length >= 2) { + String version = segments[1]; + LOGGER.info("Found version '{}' for artifact '{}'", version, artifact); + return Optional.of(version); + } + } + return Optional.empty(); + } + + protected void findPomDependencies() { + JavaSettings settings = JavaSettings.getInstance(); + String outputFolder = settings.getAutorestSettings().getOutputFolder(); + if (outputFolder != null && Paths.get(outputFolder).isAbsolute()) { + Path pomPath = Paths.get(outputFolder, "pom.xml"); + + if (Files.isReadable(pomPath)) { + try { + DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance(); + DocumentBuilder dBuilder = dbFactory.newDocumentBuilder(); + Document doc = dBuilder.parse(pomPath.toFile()); + NodeList nodeList = doc.getDocumentElement().getChildNodes(); + for (int i = 0; i < nodeList.getLength(); ++i) { + Node node = nodeList.item(i); + if (node.getNodeType() == Node.ELEMENT_NODE) { + Element elementNode = (Element) node; + if ("dependencies".equals(elementNode.getTagName())) { + NodeList dependencyNodeList = elementNode.getChildNodes(); + for (int j = 0; j < dependencyNodeList.getLength(); ++j) { + Node dependencyNode = dependencyNodeList.item(j); + if (dependencyNode.getNodeType() == Node.ELEMENT_NODE) { + Element dependencyElementNode = (Element) dependencyNode; + if ("dependency".equals(dependencyElementNode.getTagName())) { + String groupId = null; + String artifactId = null; + String version = null; + String scope = null; + NodeList itemNodeList = dependencyElementNode.getChildNodes(); + for (int k = 0; k < itemNodeList.getLength(); ++k) { + Node itemNode = itemNodeList.item(k); + if (itemNode.getNodeType() == Node.ELEMENT_NODE) { + Element elementItemNode = (Element) itemNode; + switch (elementItemNode.getTagName()) { + case "groupId": + groupId = ((Text) elementItemNode.getChildNodes().item(0)).getWholeText(); + break; + case "artifactId": + artifactId = ((Text) elementItemNode.getChildNodes().item(0)).getWholeText(); + break; + case "version": + version = ((Text) elementItemNode.getChildNodes().item(0)).getWholeText(); + break; + case "scope": + scope = ((Text) elementItemNode.getChildNodes().item(0)).getWholeText(); + break; + } + } + } + + if (groupId != null && artifactId != null && version != null) { + String dependencyIdentifier = String.format("%s:%s:%s", groupId, artifactId, version); + if (scope != null) { + dependencyIdentifier += ":" + scope; + } + this.pomDependencyIdentifiers.add(dependencyIdentifier); + LOGGER.info("Found dependency identifier '{}' from POM", dependencyIdentifier); + } + } + } + } + } + } + } + } catch (IOException | ParserConfigurationException | SAXException e) { + LOGGER.warn("Failed to parse 'pom.xml'", e); + } + } else { + LOGGER.info("'pom.xml' not found or not readable"); + } + } else { + LOGGER.warn("'output-folder' parameter is not an absolute path, fall back to default dependencies"); + } + } + + public String getServiceName() { + return serviceName; + } + + public String getServiceDescription() { + return this.serviceDescription; + } + + public String getServiceDescriptionForPom() { + return this.serviceDescription; + } + + public String getServiceDescriptionForMarkdown() { + return this.serviceDescription; + } + + public String getNamespace() { + return namespace; + } + + public String getGroupId() { + return groupId; + } + + public String getArtifactId() { + return artifactId; + } + + public String getVersion() { + return version; + } + + public List getApiVersions() { + return apiVersions; + } + + public List getPomDependencyIdentifiers() { + return pomDependencyIdentifiers; + } + + public Optional getSdkRepositoryUri() { + return Optional.ofNullable(sdkRepositoryPath == null ? null : ("https://github.com/Azure/azure-sdk-for-java/blob/main/" + sdkRepositoryPath)); + } + + public Optional getSdkRepositoryPath() { + return Optional.ofNullable(sdkRepositoryPath); + } + + public boolean isIntegratedWithSdk() { + return integratedWithSdk; + } + + public boolean isGenerateSamples() { + return JavaSettings.getInstance().isGenerateSamples(); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/projectmodel/TextFile.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/projectmodel/TextFile.java new file mode 100644 index 0000000000..05349e185b --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/projectmodel/TextFile.java @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.model.projectmodel; + +public class TextFile { + private final String filePath; + private String contents; + + public TextFile(String filePath) { + this(filePath, null); + } + + public TextFile(String filePath, String contents) { + this.filePath = filePath; + this.contents = contents; + } + + public final String getFilePath() { + return filePath; + } + + public final String getContents() { + return contents; + } + + public void setContents(String contents) { + this.contents = contents; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/xmlmodel/XmlBlock.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/xmlmodel/XmlBlock.java new file mode 100644 index 0000000000..4fe4131644 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/xmlmodel/XmlBlock.java @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.model.xmlmodel; + +import com.microsoft.typespec.http.client.generator.core.util.CodeNamer; + +import java.util.function.Consumer; + +public class XmlBlock { + private final XmlFileContents contents; + + public XmlBlock(XmlFileContents contents) { + this.contents = contents; + } + + public final void indent(Runnable indentAction) { + contents.indent(indentAction); + } + + public final void increaseIndent() { + contents.increaseIndent(); + } + + public final void decreaseIndent() { + contents.decreaseIndent(); + } + + public final void text(String text) { + contents.text(text); + } + + public final void line(String text, Object... formattedArguments) { + contents.line(text, formattedArguments); + } + + public final void line() { + contents.line(); + } + + public final void tag(String tag, String value) { + contents.tag(tag, CodeNamer.escapeXmlComment(value)); + } + + public final void block(String text, Consumer bodyAction) { + contents.block(text, bodyAction); + } + + public final void tagWithInlineComment(String tag, String value, String comment) { + contents.line("<%1$s>%2$s ", tag, CodeNamer.escapeXmlComment(value), CodeNamer.escapeXmlComment(comment)); + } + + public final void tagCData(String tag, String value) { + contents.line("<%1$s>", tag, value); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/xmlmodel/XmlFile.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/xmlmodel/XmlFile.java new file mode 100644 index 0000000000..f610ace759 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/xmlmodel/XmlFile.java @@ -0,0 +1,86 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.model.xmlmodel; + +import java.util.Map; +import java.util.function.Consumer; + +public class XmlFile { + private final String filePath; + private final XmlFileContents contents; + + public static class Options { + private int indent = 4; + + public int getIndent() { + return indent; + } + + public Options setIndent(int indent) { + this.indent = indent; + return this; + } + } + + public XmlFile(String filePath) { + this(filePath, null, null); + } + + public XmlFile(String filePath, Options options) { + this(filePath, null, options); + } + + public XmlFile(String filePath, String fileContents) { + this(filePath, fileContents, null); + } + + public XmlFile(String filePath, String fileContents, Options options) { + this.filePath = filePath; + contents = new XmlFileContents(fileContents, options); + } + + public final String getFilePath() { + return filePath; + } + + public final XmlFileContents getContents() { + return contents; + } + + public final void text(String text) { + getContents().text(text); + } + + public final void line(String text) { + getContents().line(text); + } + + public final void line() { + getContents().line(); + } + + public final void tag(String tag, String value) { + getContents().tag(tag, value); + } + + public final void indent(Runnable indentAction) { + getContents().indent(indentAction); + } + + public void block(String text, Consumer bodyAction) { + getContents().block(text, bodyAction); + } + + public void block(String text, Map annotations, Consumer bodyAction) { + getContents().block(text, annotations, bodyAction); + } + + public void blockComment(String text) { + getContents().blockComment(text); + } + + public void blockComment(Consumer commentAction) { + getContents().blockComment(commentAction); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/xmlmodel/XmlFileContents.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/xmlmodel/XmlFileContents.java new file mode 100644 index 0000000000..8b55679498 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/xmlmodel/XmlFileContents.java @@ -0,0 +1,195 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.model.xmlmodel; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Map; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +public class XmlFileContents { + private String singleIndent = " "; + + private final StringBuilder contents; + private final StringBuilder linePrefix; + + private CurrentLineType currentLineType = CurrentLineType.values()[0]; + + public XmlFileContents() { + this(null); + } + + public XmlFileContents(String fileContents) { + this(fileContents, null); + } + + public XmlFileContents(String fileContents, XmlFile.Options options) { + if (options != null) { + if (options.getIndent() > 0) { + char[] chars = new char[options.getIndent()]; + Arrays.fill(chars, ' '); + singleIndent = String.valueOf(chars); + } + } + + contents = new StringBuilder(); + linePrefix = new StringBuilder(); + + if (fileContents != null && !fileContents.isEmpty()) { + contents.append(fileContents); + } + } + + @Override + public String toString() { + return contents.toString(); + } + + public final String[] getLines() { + return toString().split("\n", -1); + } + + public final void addToPrefix(String toAdd) { + linePrefix.append(toAdd); + } + + private void removeFromPrefix(String toRemove) { + int toRemoveLength = toRemove.length(); + if (linePrefix.length() <= toRemoveLength) { + linePrefix.setLength(0); + } else { + linePrefix.delete(linePrefix.length() - toRemoveLength, linePrefix.length() - toRemoveLength + toRemoveLength); + } + } + + public final void indent(Runnable action) { + increaseIndent(); + action.run(); + decreaseIndent(); + } + + public final void increaseIndent() { + addToPrefix(singleIndent); + } + + public final void decreaseIndent() { + removeFromPrefix(singleIndent); + } + + private void text(String text, boolean addPrefix) { + ArrayList lines = new ArrayList<>(); + + if (text == null || text.isEmpty()) { + lines.add(""); + } else { + int lineStartIndex = 0; + int textLength = text.length(); + while (lineStartIndex < textLength) { + int newLineCharacterIndex = text.indexOf('\n', lineStartIndex); + if (newLineCharacterIndex == -1) { + String line = text.substring(lineStartIndex); + lines.add(line); + lineStartIndex = textLength; + } else { + int nextLineStartIndex = newLineCharacterIndex + 1; + String line = text.substring(lineStartIndex, nextLineStartIndex); + lines.add(line); + lineStartIndex = nextLineStartIndex; + } + } + } + + String prefix = addPrefix ? linePrefix.toString() : null; + for (String line : lines) { + if (addPrefix && prefix != null && !prefix.trim().isEmpty() || (prefix != null && !prefix.isEmpty() && line != null && !line.trim().isEmpty())) { + contents.append(prefix); + } + + contents.append(line); + } + } + + public final void text(String text) { + if (currentLineType == CurrentLineType.Empty) { + text(text, true); + } else if (currentLineType == CurrentLineType.Text) { + text(text, false); + } else if (currentLineType == CurrentLineType.AfterIf) { + line("", false); + text(text, true); + } + currentLineType = CurrentLineType.Text; + } + + private void line(String text, boolean addPrefix) { + text(text + "\n", addPrefix); + currentLineType = CurrentLineType.Empty; + } + + public void line(String text) { + if (currentLineType == CurrentLineType.Empty) { + line(text, true); + } else if (currentLineType == CurrentLineType.Text) { + line(text, false); + } else if (currentLineType == CurrentLineType.AfterIf) { + line("", false); + line(text, true); + } + currentLineType = CurrentLineType.Empty; + } + + public void line(String text, Object... formattedArguments) { + if (formattedArguments != null && formattedArguments.length > 0) { + text = String.format(text, formattedArguments); + } + + line(text); + } + + public void line() { + line(""); + } + + public void tag(String tag, String value) { + line("<" + tag + ">" + value + ""); + } + + public void block(String text, Consumer bodyAction) { + line("<" + text + ">"); + indent(() -> + bodyAction.accept(new XmlBlock(this))); + line(""); + } + + public void block(String text, Map annotations, Consumer bodyAction) { + if (annotations != null && !annotations.isEmpty()) { + String append = annotations.entrySet().stream() + .map(entry -> entry.getKey() + "=\"" + entry.getValue() + "\"") + .collect(Collectors.joining(" ")); + line("<" + text + " " + append + ">"); + } else { + line("<" + text + ">"); + } + indent(() -> + bodyAction.accept(new XmlBlock(this))); + line(""); + } + + public void blockComment(String text) { + blockComment(comment -> comment.line(text)); + } + + public void blockComment(Consumer commentAction) { + line(""); + } + + private enum CurrentLineType { + Empty, + AfterIf, + Text + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/xmlmodel/XmlLineComment.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/xmlmodel/XmlLineComment.java new file mode 100644 index 0000000000..1d5db0913f --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/model/xmlmodel/XmlLineComment.java @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.model.xmlmodel; + +public class XmlLineComment { + private final XmlFileContents contents; + + public XmlLineComment(XmlFileContents contents) { + this.contents = contents; + } + + public final void line(String text) { + contents.line(text); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/partialupdate/util/PartialUpdateHandler.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/partialupdate/util/PartialUpdateHandler.java new file mode 100644 index 0000000000..fe871bbaa4 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/partialupdate/util/PartialUpdateHandler.java @@ -0,0 +1,745 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.partialupdate.util; + +import com.github.javaparser.JavaToken; +import com.github.javaparser.StaticJavaParser; +import com.github.javaparser.ast.CompilationUnit; +import com.github.javaparser.ast.ImportDeclaration; +import com.github.javaparser.ast.NodeList; +import com.github.javaparser.ast.PackageDeclaration; +import com.github.javaparser.ast.body.BodyDeclaration; +import com.github.javaparser.ast.body.CallableDeclaration; +import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration; +import com.github.javaparser.ast.body.InitializerDeclaration; +import com.github.javaparser.ast.body.TypeDeclaration; +import com.github.javaparser.ast.comments.Comment; +import com.github.javaparser.ast.comments.JavadocComment; +import com.github.javaparser.ast.comments.LineComment; +import com.github.javaparser.ast.expr.SimpleName; +import com.github.javaparser.ast.modules.ModuleDeclaration; +import com.github.javaparser.ast.modules.ModuleDirective; +import com.github.javaparser.ast.nodeTypes.NodeWithSimpleName; +import com.github.javaparser.ast.visitor.GenericVisitor; +import com.github.javaparser.ast.visitor.VoidVisitor; +import com.github.javaparser.printer.DefaultPrettyPrinterVisitor; + +import java.io.BufferedReader; +import java.io.StringReader; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Partial update handler. It can handle partial update for .java class files. + * + *

Below partial update use cases are supported: + * + *

    + *
  • Manually add class member (field / method / constructor) -> keep the added member + *
  • Manually update method signature, e.g. parameter change, method access level change -> keep the manual changed + * signature, and not generate the corresponding method with the same method name + *
  • Manually remove one class member -> if the member's definition is in swagger, this member will be auto + * generated again + *
  • Swagger add a new api -> add the new api to generated file + *
  • Swagger update an existing api -> if the api is auto generated, then the existing generated member will be + * replaced to the new one. If it is manual updated, we will keep the manual updated member. + *
  • Swagger delete an existing api -> if the existing api is auto generated, then it should be removed. If it is + * manual updated, we will keep the manual updated member. + *
+ */ +public class PartialUpdateHandler { + /** + * Javadoc comment that denotes the start of the generated content. + */ + public static final String START_GENERATED_JAVA_DOC = ""; + + /** + * Javadoc comment that denotes the end of the generated content. + */ + public static final String END_GENERATED_JAVA_DOC = ""; + + // The following constant MUST be in the order of off - on to make sure code formatting remains enabled for this + // class. + private static final String JAVADOC_FORMATTER_OFF = ""; + private static final String JAVADOC_FORMATTER_ON = ""; + + private static final String FORMATTER_OFF_COMMENT = " @formatter:off"; + private static final String FORMATTER_ON_COMMENT = " @formatter:on"; + + /** + * Handle partial update by comparing generatedFileContent and existingFileContent. It supports handling partial + * update for class or interface file, package-info.java file, and module-info.java file. + *

+ * When handling partial update for each interface or class file, it will compare existing file which has manual + * update and generated file which is generated by autorest. It keeps the manual update members and replace + * generated members with the newly generated one. + *

+ * When handling partial update for each package-info.java, it will find copy the existing Javadoc comments and + * replace anything between {@code } and {@code } with the + * newly generated Javadoc comments. If there isn't any generated Javadoc comments there will be no changes to the + * package-info.java Javadoc comments. + *

+ * When handling partial update for each module-info.java file, for simplicity, currently it will always use + * existing file if existing file is considered as manually modified. + * + *

Handle partial update steps: + *

    + *
  • Parse existing file content and generated file content using JavaParser + *
  • If the file is package-info.java, then handle it by inspecting the existing Javadoc comments and replacing + * only the previously generated Javadoc comments. + *
  • If the file is module-info.java, then handle it by simply compare the difference between existing file and + * generated file + *
  • If the file is class or interface file, handle partial update for it + *
  • Otherwise, we just return the generatedFileContent directly + *
+ * + * @param generatedFileContent the newly generated file content + * @param existingFileContent the existing file content that contains user's manual update code + * @return the file content after handling partial update + */ + public static String handlePartialUpdateForFile(String generatedFileContent, String existingFileContent) { + // 1. Parse existing file content and generated file content using JavaParser + CompilationUnit compilationUnitForGeneratedFile = StaticJavaParser.parse(generatedFileContent); + CompilationUnit compilationUnitForExistingFile = StaticJavaParser.parse(existingFileContent); + + // 2. If it's module-info.java file, then go to handlePartialUpdateForModuleInfoFile + if (compilationUnitForExistingFile.getModule().isPresent() && compilationUnitForGeneratedFile.getModule() + .isPresent()) { + return handlePartialUpdateForModuleInfoFile(compilationUnitForGeneratedFile, + compilationUnitForExistingFile); + } + + // 3. If it's package-info.java file, then go to handlePartialUpdateForPackageInfoFile + if (isPackageInfoFile(compilationUnitForExistingFile) && isPackageInfoFile(compilationUnitForGeneratedFile)) { + return handlePartialUpdateForPackageInfoFile(compilationUnitForGeneratedFile, + compilationUnitForExistingFile); + } + + // 4. If it's class or interface file, handle partial update for class or interface file + if (isClassOrInterfaceFile(compilationUnitForExistingFile) && isClassOrInterfaceFile( + compilationUnitForGeneratedFile)) { + return handlePartialUpdateForClassOrInterfaceFile(compilationUnitForGeneratedFile, generatedFileContent, + compilationUnitForExistingFile); + } + + return generatedFileContent; + } + + /** + *

Handle partial update for class or interface file steps: + *

    + *
  • Parse existing file content and generated file content using JavaParser + *
  • Get class members for existing file and generated file + *
  • Check if the file is in scope of partial update by iterate the members in generated file to see if there is + * a method has {@code @Generated} annotation. If it has {@code @Generated} annotation, then the file is in scope + * of partial update, otherwise return generated file content directly. + *
  • Iterate existing file members, keep manual updated members, and replace generated members with the + * corresponding newly generated one. Here we will not do the replace on the existing file member list, we just + * create a new member list {@code updatedMembersList} and put in those manually update members and newly generated + * members. + *
  • Add remaining newly generated members to {@code updatedMembersList} + *
  • Update generated file members with {@code updatedMembersList} + *
  • Update generated file imports + *
+ * + * @param compilationUnitForGeneratedFile the newly generated file content + * @param generatedFileContent the newly generated file content + * @param compilationUnitForExistingFile the existing file content that contains user's manual update code + * @return the file content after handling partial update + */ + private static String handlePartialUpdateForClassOrInterfaceFile(CompilationUnit compilationUnitForGeneratedFile, + String generatedFileContent, CompilationUnit compilationUnitForExistingFile) { + // 1. Parse existing file content and generated file content using JavaParser + ClassOrInterfaceDeclaration generatedClazz = getClassOrInterfaceDeclaration(compilationUnitForGeneratedFile); + ClassOrInterfaceDeclaration existingClazz = getClassOrInterfaceDeclaration(compilationUnitForExistingFile); + + // 2. Get class members for existing file and generated file + List> generatedFileMembers = new ArrayList<>(); + if (generatedClazz != null) { + generatedFileMembers = generatedClazz.getMembers(); + } + List> existingFileMembers = new ArrayList<>(); + if (existingClazz != null) { + existingFileMembers = existingClazz.getMembers(); + } + + // 3. Verify Generated File, will throw error if there is invalid part found. + validateGeneratedClassOrInterface(generatedFileMembers); + + // 4. Check if the file is in scope of partial update: + // if there is a method has @Generated annotation, then the file is in scope of partial update, otherwise return + // directly + boolean hasGeneratedAnnotations = generatedFileMembers.stream() + .anyMatch(PartialUpdateHandler::hasGeneratedAnnotation); + + if (!hasGeneratedAnnotations) { + return generatedFileContent; + } + + // TODO (weidxu): for now, formatter:on/off is not added by codegen -- hence the commented out block +// // Remove all orphan comments from the declaration to prevent them being added multiple times when code is +// // regenerated. +// // Create a new List for the orphaned comments as Java Parser returns the same list instance, wrapped by +// // Collections.unmodifiableList, for each call to getOrphanComments. Without the new ArrayList, the orphan +// // comments would be removed from the list returned during iteration, causing a ConcurrentModificationException. +// List orphanComments = new ArrayList<>(existingClazz.getOrphanComments()); +// orphanComments.stream().filter(PartialUpdateHandler::isFormatterComment).forEach(Node::remove); +// existingClazz.accept(new VoidVisitorAdapter<>() { +// @Override +// public void visit(LineComment n, Object arg) { +// if (isFormatterComment(n)) { +// n.remove(); +// } +// } +// }, null); + + NodeList> updatedMembersList = new NodeList<>(); + // 5. Iterate existingFileMembers, keep manual written members, and replace generated members with the + // corresponding newly generated one + for (BodyDeclaration existingMember : existingFileMembers) { + boolean isGeneratedMethod = isMemberGenerated(existingMember); + if (!isGeneratedMethod) { // manual written member + // TODO (weidxu): for now, formatter:on/off is not added by codegen -- hence the commented out block +// updatedMembersList.add(surroundCustomCodeWithFormatterOff(existingMember)); + updatedMembersList.add(existingMember); + } else { + // find the corresponding newly generated member + for (BodyDeclaration generatedMember : generatedFileMembers) { + if (isMembersCorresponding(existingMember, generatedMember)) { + updatedMembersList.add(generatedMember); + break; + } + } + } + } + + // 6. Add remaining members in generated file to the new members list + for (BodyDeclaration generatedMember : generatedFileMembers) { + boolean needToAddToUpdateMembersList = true; + for (BodyDeclaration existingMember : updatedMembersList) { + if (existingMember instanceof NonGeneratedBodyDeclaration) { + existingMember = ((NonGeneratedBodyDeclaration) existingMember).wrapped; + } + + // If the generated member and the existing member is corresponding, + // or if there is an existing member who has the same name as the generated member and is manually written, + // Then we don't put the generated member to the updatedMembersList. + if (isMembersCorresponding(existingMember, generatedMember) || ( + isMembersWithSameName(existingMember, generatedMember) && !isMemberGenerated(existingMember))) { + needToAddToUpdateMembersList = false; + break; + } + } + if (needToAddToUpdateMembersList) { + updatedMembersList.add(generatedMember); + } + } + + // 7. Update members + generatedClazz.setMembers(updatedMembersList); + + // 8. Update imports + compilationUnitForGeneratedFile.getImports().addAll(compilationUnitForExistingFile.getImports()); + + return compilationUnitForGeneratedFile.toString(); + } + + /** + * Surrounds the non-generated BodyDeclaration with Eclipse formatter disable tags. + * + * @param declaration the BodyDeclaration to surround with formatter disable tags + * @return the BodyDeclaration surrounded with formatter disable tags + */ + private static BodyDeclaration surroundCustomCodeWithFormatterOff(BodyDeclaration declaration) { + return new NonGeneratedBodyDeclaration<>(declaration); + } + + private static boolean isFormatterComment(Comment comment) { + // Formatter comments are always line comments + if (!(comment instanceof LineComment)) { + return false; + } + + String lineComment = comment.getContent(); + + // Check for the comment starting with formatter: + if (!FORMATTER_OFF_COMMENT.regionMatches(0, lineComment, 0, 12)) { + return false; + } + + return lineComment.endsWith("off") || lineComment.endsWith("on"); + } + + /** + * Custom implementation of {@link BodyDeclaration} that wraps a non-generated {@link BodyDeclaration}. + *

+ * This is necessary as Java Parser doesn't write trailing orphan comments when printing the AST. So, this class + * overrides the method that is called during printing to explicitly add the code formatter off comments. + * + * @param the type of the wrapped {@link BodyDeclaration} + */ + private static final class NonGeneratedBodyDeclaration> extends BodyDeclaration { + private final BodyDeclaration wrapped; + + NonGeneratedBodyDeclaration(BodyDeclaration wrapped) { + this.wrapped = wrapped; + } + + @Override + public R accept(GenericVisitor v, A arg) { + return wrapped.accept(v, arg); + } + + @Override + public void accept(VoidVisitor v, A arg) { + if (v instanceof DefaultPrettyPrinterVisitor) { + new LineComment(FORMATTER_OFF_COMMENT).accept(v, arg); + wrapped.accept(v, arg); + new LineComment(FORMATTER_ON_COMMENT).accept(v, arg); + } else { + wrapped.accept(v, arg); + } + } + } + + /** + * Verify if the generated class or interface is valid + * + * @param generatedFileMembers, members in the generated file + * @return true if the generated class or interface is valid, otherwise return false + */ + private static void validateGeneratedClassOrInterface(List> generatedFileMembers) { + // 1. Verify there is no duplicate methods (methods with same signature are considered duplicate methods) + Set methodSignatureSet = new HashSet<>(); + for (BodyDeclaration generatedMember : generatedFileMembers) { + if (generatedMember.isCallableDeclaration()) { + if (methodSignatureSet.contains(generatedMember.asCallableDeclaration().getSignature())) { + throw new RuntimeException( + String.format("Found duplicate methods in the generated file. Signature: %s", + generatedMember.asCallableDeclaration().getSignature())); + } + methodSignatureSet.add(generatedMember.asCallableDeclaration().getSignature()); + } + } + + // 2. Verify there is no more than 1 static initializer declaration + List staticInitializerDeclaration = generatedFileMembers.stream() + .filter(m -> m instanceof InitializerDeclaration) + .map(m -> (InitializerDeclaration) m) + .filter(InitializerDeclaration::isStatic) + .collect(Collectors.toList()); + if (staticInitializerDeclaration.size() > 1) { + throw new RuntimeException( + String.format("Found more than 1 static initializer declaration in the generated file. Code:\n%s", + staticInitializerDeclaration.stream() + .map(m -> m.getBody().toString()) + .collect(Collectors.joining("\n\n")))); + } + } + + /** + * Handle partial update for module-info.java file. We will merge module-info.java file contents. + * + * @param compilationUnitForGeneratedFile the newly generated file content + * @param compilationUnitForExistingFile the existing file content that contains user's manual update code + * @return the content after handling partial update + */ + private static String handlePartialUpdateForModuleInfoFile(CompilationUnit compilationUnitForGeneratedFile, + CompilationUnit compilationUnitForExistingFile) { + return mergeModuleFileContent(compilationUnitForGeneratedFile, compilationUnitForExistingFile); + } + + /** + * The basic logic is as below: 1. Parse the directives from the two files 2. Create requires, exports, opens, uses, + * provides directive lists from the generated file and existing file 3. Merge the requires, exports, opens, uses, + * provides directive lists one by one 4. Add the directive lists to ModuleDeclaration in generated file, then use + * generated file as return value + * + * @param compilationUnitForGeneratedFile the newly generated file content + * @param compilationUnitForExistingFile the existing file content that contains user's manual update code + * @return merged module-info.java file content + */ + private static String mergeModuleFileContent(CompilationUnit compilationUnitForGeneratedFile, + CompilationUnit compilationUnitForExistingFile) { + if (!compilationUnitForExistingFile.getModule().isPresent() || !compilationUnitForGeneratedFile.getModule() + .isPresent()) { + throw new RuntimeException("Generated file or existing file is not module-info file"); + } + + NodeList directivesForGeneratedFile = compilationUnitForGeneratedFile.getModule() + .get() + .getDirectives(); + + NodeList directivesForExistingFile = compilationUnitForExistingFile.getModule() + .get() + .getDirectives(); + + // generated file directives + NodeList requiresDirectivesForGeneratedFile = new NodeList<>(); + NodeList exportsDirectivesForGeneratedFile = new NodeList<>(); + NodeList opensDirectivesForGeneratedFile = new NodeList<>(); + NodeList usesDirectivesForGeneratedFile = new NodeList<>(); + NodeList providesDirectivesForGeneratedFile = new NodeList<>(); + addToEachTypeOfDirectiveList(directivesForGeneratedFile, requiresDirectivesForGeneratedFile, + exportsDirectivesForGeneratedFile, opensDirectivesForGeneratedFile, usesDirectivesForGeneratedFile, + providesDirectivesForGeneratedFile); + + // existing file directives + NodeList requiresDirectivesForExistingFile = new NodeList<>(); + NodeList exportsDirectivesForExistingFile = new NodeList<>(); + NodeList opensDirectivesForExistingFile = new NodeList<>(); + NodeList usesDirectivesForExistingFile = new NodeList<>(); + NodeList providesDirectivesForExistingFile = new NodeList<>(); + addToEachTypeOfDirectiveList(directivesForExistingFile, requiresDirectivesForExistingFile, + exportsDirectivesForExistingFile, opensDirectivesForExistingFile, usesDirectivesForExistingFile, + providesDirectivesForExistingFile); + + // generated file directives + NodeList requiresDirectiveNodeList = mergeDirectiveNodeList(requiresDirectivesForGeneratedFile, + requiresDirectivesForExistingFile); + NodeList exportsDirectiveNodeList = mergeDirectiveNodeList(exportsDirectivesForGeneratedFile, + exportsDirectivesForExistingFile); + NodeList opensDirectiveNodeList = mergeDirectiveNodeList(opensDirectivesForGeneratedFile, + opensDirectivesForExistingFile); + NodeList usesDirectiveNodeList = mergeDirectiveNodeList(usesDirectivesForGeneratedFile, + usesDirectivesForExistingFile); + NodeList providesDirectiveNodeList = mergeDirectiveNodeList(providesDirectivesForGeneratedFile, + providesDirectivesForExistingFile); + addToEachTypeOfDirectiveList(directivesForGeneratedFile, requiresDirectivesForExistingFile, + exportsDirectivesForExistingFile, opensDirectivesForExistingFile, usesDirectivesForExistingFile, + providesDirectivesForExistingFile); + + NodeList moduleDirectives = new NodeList<>(); + moduleDirectives.addAll(requiresDirectiveNodeList); + moduleDirectives.addAll(exportsDirectiveNodeList); + moduleDirectives.addAll(opensDirectiveNodeList); + moduleDirectives.addAll(usesDirectiveNodeList); + moduleDirectives.addAll(providesDirectiveNodeList); + + ModuleDeclaration moduleDeclaration = compilationUnitForGeneratedFile.getModule().get(); + moduleDeclaration.setDirectives(moduleDirectives); + + compilationUnitForGeneratedFile.setModule(moduleDeclaration); + + // add comments as compilationUnitForGeneratedFile.toString() does not include comments + StringBuilder comments = new StringBuilder(); + for (Comment comment : compilationUnitForGeneratedFile.getOrphanComments()) { + comments.append(comment.toString()); + } + + return comments + "\n" + compilationUnitForGeneratedFile; + } + + /** + * Handle partial update for package-info.java file. We will merge package-info.java file contents. + * + * @param compilationUnitForGeneratedFile the newly generated file content + * @param compilationUnitForExistingFile the existing file content that contains user's manual update code + * @return the content after handling partial update + */ + private static String handlePartialUpdateForPackageInfoFile(CompilationUnit compilationUnitForGeneratedFile, + CompilationUnit compilationUnitForExistingFile) { + if (!isPackageInfoFile(compilationUnitForExistingFile) || !isPackageInfoFile(compilationUnitForGeneratedFile)) { + throw new RuntimeException("Generated file or existing file is not package-info file"); + } + + JavadocComment generatedJavadoc = compilationUnitForGeneratedFile.getPackageDeclaration() + .flatMap(PackageDeclaration::getComment) + .map(Comment::asJavadocComment) + .orElse(null); + + JavadocComment existingJavadoc = compilationUnitForExistingFile.getPackageDeclaration() + .flatMap(PackageDeclaration::getComment) + .map(Comment::asJavadocComment) + .orElse(null); + + // If the existing file has no Javadocs just return the generated file. + if (existingJavadoc == null) { + return compilationUnitForGeneratedFile.toString(); + } + + // Use JavadocComment.parse and get the description text as this doesn't contain the leading '*' character. + // This makes it easier to find if there is leading and trailing custom Javadoc. + // Downside is that it requires us to add back the leading '*' character when we add the custom Javadoc back. + String existingJavadocDescription = existingJavadoc.parse().getDescription().toText(); + int existingGeneratedDocStartPosition = existingJavadocDescription.indexOf(START_GENERATED_JAVA_DOC); + int existingGeneratedDocEndPosition = existingJavadocDescription.indexOf(END_GENERATED_JAVA_DOC); + + if (existingGeneratedDocStartPosition == -1 && existingGeneratedDocEndPosition == -1) { + // If the existing file has no generated doc, just return the existing file. + compilationUnitForGeneratedFile.getPackageDeclaration().get().setComment(existingJavadoc); + return compilationUnitForExistingFile.toString(); + } + + if (existingGeneratedDocEndPosition == -1) { + throw new RuntimeException("Existing file has a start generated doc ('" + START_GENERATED_JAVA_DOC + "') " + + "but no end generated doc ('" + END_GENERATED_JAVA_DOC + "')."); + } else if (existingGeneratedDocStartPosition == -1) { + throw new RuntimeException("Existing file has an end generated doc ('" + END_GENERATED_JAVA_DOC + "') but " + + "no start generated doc ('" + START_GENERATED_JAVA_DOC + "')."); + } + + String lineEnding = existingJavadoc.getLineEndingStyle().asRawString(); + String generatedJavadocDescription = generatedJavadoc.parse().getDescription().toText(); + + // Existing Javadoc doesn't start with the generated Javadoc, surround the custom Javadoc with Eclipse formatter + // tags to disable formatting. + String leadingCustomJavadoc; + if (existingGeneratedDocStartPosition > 0) { + String existing = existingJavadocDescription.substring(0, existingGeneratedDocStartPosition) + .replace(JAVADOC_FORMATTER_OFF + lineEnding, "") + .replace(JAVADOC_FORMATTER_ON + lineEnding, ""); + + leadingCustomJavadoc = JAVADOC_FORMATTER_OFF + lineEnding; + + if (!existing.endsWith(lineEnding)) { + leadingCustomJavadoc += existing + lineEnding; + } else { + leadingCustomJavadoc += existing; + } + + leadingCustomJavadoc += JAVADOC_FORMATTER_ON + lineEnding; + } else { + leadingCustomJavadoc = ""; + } + + // Existing Javadoc doesn't end with the generated Javadoc, surround the custom Javadoc with Eclipse formatter + // tags to disable formatting. + String trailingCustomJavadoc; + if (existingGeneratedDocEndPosition < existingJavadocDescription.length() - END_GENERATED_JAVA_DOC.length()) { + if (!generatedJavadocDescription.endsWith(lineEnding)) { + trailingCustomJavadoc = lineEnding + JAVADOC_FORMATTER_OFF; + } else { + trailingCustomJavadoc = JAVADOC_FORMATTER_OFF; + } + + String existing = existingJavadocDescription.substring( + existingGeneratedDocEndPosition + END_GENERATED_JAVA_DOC.length()) + .replace(JAVADOC_FORMATTER_OFF + lineEnding, "") + .replace(JAVADOC_FORMATTER_ON + lineEnding, "") + .replace(JAVADOC_FORMATTER_ON, ""); + + if (!existing.startsWith(lineEnding)) { + trailingCustomJavadoc += lineEnding + existing; + } else { + trailingCustomJavadoc += existing; + } + + if (!trailingCustomJavadoc.endsWith(lineEnding)) { + trailingCustomJavadoc += lineEnding + JAVADOC_FORMATTER_ON + lineEnding; + } else { + trailingCustomJavadoc += JAVADOC_FORMATTER_ON + lineEnding; + } + } else { + trailingCustomJavadoc = ""; + } + + String mergedJavadoc = leadingCustomJavadoc + generatedJavadocDescription + trailingCustomJavadoc; + + List lines = new BufferedReader(new StringReader(mergedJavadoc)).lines() + .map(line -> "* " + line) + .collect(Collectors.toList()); + + if (lines.isEmpty()) { + compilationUnitForGeneratedFile.getPackageDeclaration().get().setComment(new JavadocComment()); + } else if (lines.size() == 1) { + compilationUnitForGeneratedFile.getPackageDeclaration().get().setComment(new JavadocComment(lines.get(0))); + } else { + compilationUnitForGeneratedFile.getPackageDeclaration() + .get() + .setComment(new JavadocComment(String.join(lineEnding, lines))); + } + + return compilationUnitForGeneratedFile.toString(); + } + + /** + * Merge two directive list. The logic is as below: + * + * 1. Add all the directives in list1 to the returned list. 2. For each directive in list2, check if it is in list1, + * if it is in list1, then we don't need to add it to returned list, otherwise, we need to add it to the returned + * list + * + * @param list1 first directive list + * @param list2 second directive list + * @return the merged directive list + */ + private static NodeList mergeDirectiveNodeList(NodeList list1, + NodeList list2) { + NodeList res = new NodeList<>(); + res.addAll(list1); + for (ModuleDirective directive2 : list2) { + boolean isInList1 = false; + for (ModuleDirective directive1 : list1) { + if (directive1.getTokenRange().isPresent() && directive2.getTokenRange().isPresent()) { + // 1. build two token string lists from the two directives, only put in non-empty tokens + // 2. compare the two token list + // 3. if the two token lists are the same, then we consider directive2 is in list1, otherwise, we consider directive2 is not in list1 + List tokenList1 = new ArrayList<>(); + List tokenList2 = new ArrayList<>(); + for (JavaToken token1 : directive1.getTokenRange().get()) { + String trimmedToken1 = token1.asString().trim(); + if (!trimmedToken1.isEmpty()) { + tokenList1.add(trimmedToken1); + } + } + for (JavaToken token2 : directive2.getTokenRange().get()) { + String trimmedToken2 = token2.asString().trim(); + if (!trimmedToken2.isEmpty()) { + tokenList2.add(trimmedToken2); + } + } + if (tokenList1.equals(tokenList2)) { + isInList1 = true; + } + } + } + if (!isInList1) { + res.add(directive2); + } + } + return res; + } + + private static void addToEachTypeOfDirectiveList(NodeList allDirectives, + NodeList requiresDirectiveNodeList, NodeList exportsDirectiveNodeList, + NodeList opensDirectiveNodeList, NodeList usesDirectiveNodeList, + NodeList providesDirectiveNodeList) { + for (ModuleDirective directive : allDirectives) { + if (directive.isModuleRequiresDirective()) { + requiresDirectiveNodeList.add(directive); + } + if (directive.isModuleExportsDirective()) { + exportsDirectiveNodeList.add(directive); + } + if (directive.isModuleOpensDirective()) { + opensDirectiveNodeList.add(directive); + } + if (directive.isModuleUsesDirective()) { + usesDirectiveNodeList.add(directive); + } + if (directive.isModuleProvidesDirective()) { + providesDirectiveNodeList.add(directive); + } + } + + } + + /** + * Checks whether the code block has {@code @Generated} annotation. + * + * @param member the code block. + * @return whether the code block has {@code @Generated} annotation. + */ + private static boolean hasGeneratedAnnotation(BodyDeclaration member) { + if (member.getAnnotations() != null && !member.getAnnotations().isEmpty()) { + return member.getAnnotations() + .stream() + .anyMatch(annotationExpr -> annotationExpr.getName().toString().equals("Generated")); + } else { + return false; + } + } + + /** + * Checks whether the code block should be treated as generated. + * + * @param member the code block. + * @return whether the code block should be treated as generated. + */ + private static boolean isMemberGenerated(BodyDeclaration member) { + if (member instanceof InitializerDeclaration && ((InitializerDeclaration) member).isStatic()) { + // the assumption here is that user should not add static initializer declaration as customization + // so any existing one (in a file that is known to be @Generated) is generated member + return true; + } else { + // check @Generated annotation + return hasGeneratedAnnotation(member); + } + } + + /** + * Compare whether two members are corresponding: if two members are callable, which means they are constructor or + * method, we will compare the signature, otherwise, we will compare the name. + * + * In the case of static initializer declaration, since they do not have a name, we would always treat them as + * corresponding. Given the assumption that user should not add such customization, and generated code cannot have + * more than 1 such block. + * + * @param member1 + * @param member2 + * @return true if two members are corresponding, false if two members are not corresponding. + */ + private static boolean isMembersCorresponding(BodyDeclaration member1, BodyDeclaration member2) { + if (member1.isCallableDeclaration() && member2.isCallableDeclaration()) { + // compare signature + return member1.asCallableDeclaration() + .getSignature() + .equals(member2.asCallableDeclaration().getSignature()); + } else if (member1 instanceof InitializerDeclaration && member2 instanceof InitializerDeclaration + && ((InitializerDeclaration) member1).isStatic() && ((InitializerDeclaration) member2).isStatic()) { + // the assumption here is that there is at most 1 static initializer declaration + // and the static initializer declaration is generated member; see "hasGeneratedAnnotation" + return true; + } else { + return isMembersWithSameName(member1, member2); + } + } + + private static boolean isMembersWithSameName(BodyDeclaration member1, BodyDeclaration member2) { + if (member1.isFieldDeclaration() && member2.isFieldDeclaration()) { + return isFieldDeclarationWithSameName(member1, member2); + } else if (member1.getMetaModel().equals(member2.getMetaModel()) && member1 instanceof NodeWithSimpleName + && member2 instanceof NodeWithSimpleName) { + // compare name + return ((NodeWithSimpleName) member2).getName().equals(((NodeWithSimpleName) member1).getName()); + } + return false; + } + + private static boolean isFieldDeclarationWithSameName(BodyDeclaration member1, BodyDeclaration member2) { + if (member1.asFieldDeclaration().getVariables() != null && !member1.asFieldDeclaration() + .getVariables() + .isEmpty() && member2.asFieldDeclaration().getVariables() != null && !member2.asFieldDeclaration() + .getVariables() + .isEmpty()) { + // for FieldDeclaration, currently make it simple, we only compare the first variable, if the first variable + // has the same name, then we consider they are field declarations with same name + return member1.asFieldDeclaration() + .getVariables() + .get(0) + .getName() + .equals(member2.asFieldDeclaration().getVariables().get(0).getName()); + } + return false; + } + + private static ClassOrInterfaceDeclaration getClassOrInterfaceDeclaration(CompilationUnit cu) { + NodeList> types = cu.getTypes(); + if (types.size() == 1 && types.get(0).isClassOrInterfaceDeclaration()) { + SimpleName className = types.get(0).getName(); + if (cu.getClassByName(className.asString()).isPresent()) { + return cu.getClassByName(className.asString()).get(); + } + } + return null; + } + + // A package-info.java file has no types and should only be comprised of comments, imports, and a package + // declaration. + private static boolean isPackageInfoFile(CompilationUnit cu) { + return (cu.getTypes() == null || cu.getTypes().isEmpty()) && cu.getChildNodes() + .stream() + .allMatch(node -> node instanceof Comment || node instanceof ImportDeclaration + || node instanceof PackageDeclaration); + } + + private static boolean isClassOrInterfaceFile(CompilationUnit cu) { + NodeList> types = cu.getTypes(); + return types.size() == 1 && types.get(0).isClassOrInterfaceDeclaration(); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/postprocessor/Postprocessor.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/postprocessor/Postprocessor.java new file mode 100644 index 0000000000..0ff22ae874 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/postprocessor/Postprocessor.java @@ -0,0 +1,250 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.postprocessor; + +import com.microsoft.typespec.http.client.generator.core.customization.Customization; +import com.microsoft.typespec.http.client.generator.core.customization.implementation.Utils; +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; +import com.microsoft.typespec.http.client.generator.core.extension.plugin.NewPlugin; +import com.microsoft.typespec.http.client.generator.core.extension.plugin.PluginLogger; +import com.microsoft.typespec.http.client.generator.core.extension.base.util.FileUtils; +import com.microsoft.typespec.http.client.generator.core.partialupdate.util.PartialUpdateHandler; +import com.microsoft.typespec.http.client.generator.core.postprocessor.implementation.CodeFormatterUtil; +import com.azure.json.JsonReader; +import org.slf4j.Logger; + +import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +public class Postprocessor { + protected final NewPlugin plugin; + private final Logger logger; + + public Postprocessor(NewPlugin plugin) { + this.plugin = plugin; + this.logger = new PluginLogger(plugin, Postprocessor.class); + } + + @SuppressWarnings("unchecked") + public void postProcess(Map fileContents) { + String jarPath = JavaSettings.getInstance().getCustomizationJarPath(); + String className = JavaSettings.getInstance().getCustomizationClass(); + + if (className == null) { + try { + writeToFiles(fileContents, plugin, logger); + } catch (Exception e) { + logger.error("Failed to complete postprocessing.", e); + throw new RuntimeException("Failed to complete postprocessing.", e); + } + return; + } + + if (jarPath == null && !className.endsWith(".java")) { + logger.warn("Must provide a JAR path or a source file path containing the customization class {}", className); + throw new RuntimeException("Must provide a JAR path or a source file path containing the customization class " + className); + } + + try { + //Step 1: post process + Class customizationClass; + if (jarPath != null) { + URL jarUrl = null; + if (!jarPath.startsWith("http")) { + if (Paths.get(jarPath).isAbsolute()) { + jarUrl = new File(jarPath).toURI().toURL(); + } else { + String baseDirectory = getBaseDirectory(plugin); + if (baseDirectory != null) { + jarUrl = Paths.get(baseDirectory, jarPath).toUri().toURL(); + } + } + } else { + jarUrl = new URI(jarPath).toURL(); + } + if (jarUrl == null || Files.notExists(Paths.get(jarUrl.toURI()))) { + new PluginLogger(plugin, Postprocessor.class, "LoadCustomizationJar") + .warn("Customization JAR {} not found. Customization skipped.", jarPath); + return; + } + URLClassLoader loader = URLClassLoader.newInstance(new URL[]{jarUrl}, ClassLoader.getSystemClassLoader()); + try { + customizationClass = (Class) Class.forName(className, true, loader); + } catch (Exception e) { + new PluginLogger(plugin, Postprocessor.class, "LoadCustomizationClass") + .warn("Customization class " + className + + " not found in customization jar. Customization skipped.", e); + return; + } + } else if (className.endsWith(".java")) { + customizationClass = loadCustomizationClassFromJavaCode(className, getBaseDirectory(plugin), logger); + } else { + throw new RuntimeException("Invalid customization class " + className); + } + + try { + Customization customization = customizationClass.getConstructor().newInstance(); + logger.info("Running customization, this may take a while..."); + fileContents = customization.run(fileContents, logger); + } catch (Exception e) { + logger.error("Unable to complete customization", e); + throw new RuntimeException("Unable to complete customization", e); + } + + //Step 2: Print to files + writeToFiles(fileContents, plugin, logger); + } catch (Exception e) { + logger.error("Failed to complete postprocessing.", e); + throw new RuntimeException("Failed to complete postprocessing.", e); + } + } + + public static void writeToFiles(Map javaFiles, NewPlugin plugin, Logger logger) { + JavaSettings settings = JavaSettings.getInstance(); + if (settings.isHandlePartialUpdate()) { + handlePartialUpdate(javaFiles, plugin, logger); + } + + try { + CodeFormatterUtil.formatCode(javaFiles, plugin); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } + + private static String getReadme(NewPlugin plugin) { + List configurationFiles = plugin.getValueWithJsonReader("configurationFiles", + jsonReader -> jsonReader.readArray(JsonReader::getString)); + + return configurationFiles == null || configurationFiles.isEmpty() + ? JavaSettings.getInstance().getAutorestSettings().getOutputFolder() + : configurationFiles.stream().filter(key -> !key.contains(".autorest")).findFirst().orElse(null); + } + + private static String getBaseDirectory(NewPlugin plugin) { + String readme = getReadme(plugin); + if (readme != null) { + return new File(URI.create(readme).getPath()).getParent(); + } + + // TODO: get autorest running directory + return null; + } + + public static Class loadCustomizationClassFromJavaCode(String filePath, + String baseDirectory, Logger logger) { + Path customizationFile = Paths.get(filePath); + if (!customizationFile.isAbsolute()) { + if (baseDirectory != null) { + customizationFile = Paths.get(baseDirectory, filePath); + } + } + + try { + String code = Files.readString(customizationFile); + return loadCustomizationClass(customizationFile.getFileName().toString().replace(".java", ""), code); + } catch (IOException e) { + logger.error("Cannot read customization from base directory {} and file {}", baseDirectory, + customizationFile); + return null; + } + } + + @SuppressWarnings("unchecked") + public static Class loadCustomizationClass(String className, String code) { + Path customizationCompile = null; + try { + customizationCompile = FileUtils.createTempDirectory("customizationCompile" + UUID.randomUUID()); + + Path pomPath = customizationCompile.resolve("compile-pom.xml"); + Files.copy(Postprocessor.class.getClassLoader().getResourceAsStream("readme/pom.xml"), pomPath); + + Path sourcePath = customizationCompile.resolve("src/main/java/" + className + ".java"); + Files.createDirectories(sourcePath.getParent()); + + Files.writeString(sourcePath, code); + + attemptMavenInstall(pomPath); + + URL fileUrl = customizationCompile.resolve("target/classes").toUri().toURL(); + URLClassLoader classLoader = URLClassLoader.newInstance(new URL[]{fileUrl}, + ClassLoader.getSystemClassLoader()); + return (Class) Class.forName(className, true, classLoader); + } catch (Exception ex) { + throw new RuntimeException(ex); + } finally { + if (customizationCompile != null) { + Utils.deleteDirectory(customizationCompile.toFile()); + } + } + } + + private static void handlePartialUpdate(Map fileContents, NewPlugin plugin, Logger logger) { + logger.info("Begin handle partial update..."); + // handle partial update + // currently only support add additional interface or overload a generated method in sync and async client + fileContents.replaceAll((path, generatedFileContent) -> { + if (path.endsWith(".java")) { // only handle for .java file + // get existing file path + // use output-folder from autorest, if exists and is absolute path + String projectBaseDirectoryPath = null; + String outputFolderPath = JavaSettings.getInstance().getAutorestSettings().getOutputFolder(); + if (Paths.get(outputFolderPath).isAbsolute()) { + projectBaseDirectoryPath = outputFolderPath; + } + if (projectBaseDirectoryPath == null || !(new File(projectBaseDirectoryPath).isDirectory())) { + // use parent directory of swagger/readme.md + projectBaseDirectoryPath = new File(getBaseDirectory(plugin)).getParent(); + } + Path existingFilePath = Paths.get(projectBaseDirectoryPath, path); + // check if existingFile exists, if not, no need to handle partial update + if (Files.exists(existingFilePath)) { + try { + String existingFileContent = Files.readString(existingFilePath); + return PartialUpdateHandler.handlePartialUpdateForFile(generatedFileContent, existingFileContent); + } catch (Exception e) { + logger.error("Unable to get content from file path", e); + throw new RuntimeException(e); + } + } + } + return generatedFileContent; + }); + logger.info("Finish handle partial update."); + } + + private static void attemptMavenInstall(Path pomPath) { + String[] command = Utils.isWindows() + ? new String[] { "cmd", "/c", "mvn", "compiler:compile", "-f", pomPath.toString() } + : new String[] { "mvn", "compiler:compile", "-f", pomPath.toString() }; + + try { + File outputFile = Files.createTempFile(pomPath.getParent(), "compile", ".log").toFile(); + Process process = new ProcessBuilder(command) + .redirectErrorStream(true) + .redirectOutput(ProcessBuilder.Redirect.to(outputFile)) + .start(); + process.waitFor(60, TimeUnit.SECONDS); + + if (process.isAlive() || process.exitValue() != 0) { + process.destroyForcibly(); + throw new RuntimeException("Compile failed to complete within 60 seconds or failed with an error code. " + + Files.readString(outputFile.toPath()) + + "If this happens 'mvn compile -f " + pomPath + "' to install dependencies manually."); + } + } catch (IOException | InterruptedException ex) { + throw new RuntimeException("Failed to run compile on generated code.", ex); + } + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/postprocessor/implementation/CodeFormatterUtil.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/postprocessor/implementation/CodeFormatterUtil.java new file mode 100644 index 0000000000..8bf00281f4 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/postprocessor/implementation/CodeFormatterUtil.java @@ -0,0 +1,209 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.postprocessor.implementation; + +import com.microsoft.typespec.http.client.generator.core.extension.plugin.NewPlugin; +import com.microsoft.typespec.http.client.generator.core.extension.plugin.PluginLogger; +import com.github.javaparser.StaticJavaParser; +import com.github.javaparser.ast.CompilationUnit; +import com.github.javaparser.ast.ImportDeclaration; +import com.github.javaparser.ast.PackageDeclaration; +import com.github.javaparser.ast.comments.JavadocComment; +import com.github.javaparser.ast.nodeTypes.NodeWithIdentifier; +import com.github.javaparser.ast.nodeTypes.NodeWithName; +import com.github.javaparser.ast.nodeTypes.NodeWithSimpleName; +import org.eclipse.jdt.core.ToolFactory; +import org.eclipse.jdt.core.formatter.CodeFormatter; +import org.eclipse.jdt.internal.compiler.env.IModule; +import org.eclipse.jface.text.Document; +import org.eclipse.jface.text.IDocument; +import org.eclipse.text.edits.TextEdit; +import org.slf4j.Logger; +import org.w3c.dom.NodeList; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; + +/** + * Utility class that handles code formatting. + */ +public final class CodeFormatterUtil { + /** + * Formats the given files by removing unused imports and applying Eclipse code formatting. + * + * @param files The files to format. + * @param plugin The plugin to use to write the formatted files. + * @throws Exception If code formatting fails. + */ + public static void formatCode(Map files, NewPlugin plugin) throws Exception { + AtomicReference loggerReference = new AtomicReference<>(); + Map eclipseSettings = loadEclipseSettings(); + files.entrySet().parallelStream().forEach(fileEntry -> { + try { + String file = removeUnusedImports(fileEntry.getValue()); + file = formatCode(file, fileEntry.getKey(), ToolFactory.createCodeFormatter(eclipseSettings)); + + plugin.writeFile(fileEntry.getKey(), file, null); + } catch (Exception e) { + // print file content + Logger logger = loggerReference.updateAndGet(logger1 -> + logger1 == null ? new PluginLogger(plugin, CodeFormatterUtil.class) : logger1); + String errorMessage = "Failed to format file: " + fileEntry.getKey() + ". File content: \n" + fileEntry.getValue(); + logger.error(errorMessage); + + throw new RuntimeException("Failed to format: " + fileEntry.getKey(), e); + } + }); + } + + /** + * Formats the given files by removing unused imports and applying Eclipse code formatting. + * + * @param files The files to format. The entry is filename and content. + * @return the files after format. + * @throws Exception If code formatting fails. + */ + public static List formatCode(List> files) throws Exception { + Map eclipseSettings = loadEclipseSettings(); + return files.parallelStream().map(fileEntry -> { + try { + String file = removeUnusedImports(fileEntry.getValue()); + file = formatCode(file, fileEntry.getKey(), ToolFactory.createCodeFormatter(eclipseSettings)); + return file; + } catch (Exception e) { + throw new RuntimeException(e); + } + }).collect(Collectors.toList()); + } + + /** + * Loads the Eclipse formatter settings from the XML file. + * + * @return The Eclipse formatter settings. + * @throws Exception If the formatter settings could not be loaded. + */ + private static Map loadEclipseSettings() throws Exception { + DocumentBuilder documentBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); + org.w3c.dom.Document document = documentBuilder.parse(CodeFormatterUtil.class.getClassLoader() + .getResourceAsStream("readme/eclipse-format-azure-sdk-for-java.xml")); + + NodeList formatterSettingXml = document.getElementsByTagName("setting"); + Map formatterSettings = new HashMap<>(); + for (int i = 0; i < formatterSettingXml.getLength(); i++) { + org.w3c.dom.Node node = formatterSettingXml.item(i); + formatterSettings.put(node.getAttributes().getNamedItem("id").getNodeValue(), + node.getAttributes().getNamedItem("value").getNodeValue()); + } + + return formatterSettings; + } + + /** + * Removes unused imports from the given file. + * + * @param file The file to remove unused imports from. + * @return The file with unused imports removed. + */ + private static String removeUnusedImports(String file) { + CompilationUnit compilationUnit = StaticJavaParser.parse(file); + com.github.javaparser.ast.NodeList imports = compilationUnit.getImports(); + + // Nothing to clean up. + if (imports.isEmpty()) { + return file; + } + + // Package declaration could be null. + PackageDeclaration packageDeclaration = compilationUnit.getPackageDeclaration().orElse(null); + String packageName = packageDeclaration != null ? packageDeclaration.getNameAsString() : null; + + // Collect all names used in the file that aren't associated with the package or imports. + Set types = compilationUnit.stream() + .filter(node -> node instanceof NodeWithIdentifier || node instanceof NodeWithName + || node instanceof NodeWithSimpleName) + .filter(node -> !node.isDescendantOf(packageDeclaration) && !(node instanceof PackageDeclaration)) + .filter(node -> imports.stream().noneMatch(node::isDescendantOf) && !(node instanceof ImportDeclaration)) + .map(node -> { + if (node instanceof NodeWithIdentifier) { + return ((NodeWithIdentifier) node).getIdentifier(); + } else if (node instanceof NodeWithName) { + return ((NodeWithName) node).getNameAsString(); + } else { + return ((NodeWithSimpleName) node).getNameAsString(); + } + }) + .collect(Collectors.toSet()); + + // Collect all the types used in the Javadoc comments. + compilationUnit.getAllComments().stream() + .filter(comment -> comment instanceof JavadocComment) + .map(comment -> (JavadocComment) comment) + .forEach(javadoc -> javadoc.parse().getBlockTags() + .forEach(tag -> tag.getName().ifPresent(types::add))); + + // Get the list of imports that are unused. + Map importsToRemove = imports.stream().filter(importDeclaration -> { + String fullImportName = importDeclaration.getNameAsString(); + if (Objects.equals(fullImportName, packageName)) { + return true; + } + + String importType = importDeclaration.getName().getIdentifier(); + return !types.contains(importType); + }).collect(Collectors.toMap(importDeclaration -> importDeclaration.getRange().get().begin.line, + importDeclaration -> importDeclaration)); + + // Get the list of duplicate imports. + imports.stream().collect(Collectors.groupingBy(ImportDeclaration::getNameAsString)).entrySet().stream() + .filter(entry -> entry.getValue().size() > 1) + .flatMap(entry -> entry.getValue().stream().skip(1)) + .forEach(importDeclaration -> importsToRemove.put(importDeclaration.getRange().get().begin.line, + importDeclaration)); + + // Nothing to clean up. + if (importsToRemove.isEmpty()) { + return file; + } + + // Split the file into lines to remove the unused imports. + List lines = new ArrayList<>(Arrays.asList(file.split("\r?\n"))); + + List sortedImportsToRemove = importsToRemove.entrySet().stream() + .sorted(Map.Entry.comparingByKey(Comparator.reverseOrder())).map(Map.Entry::getValue) + .collect(Collectors.toList()); + + for (ImportDeclaration importDeclaration : sortedImportsToRemove) { + int startLine = importDeclaration.getRange().get().begin.line - 1; + int endLine = importDeclaration.getRange().get().end.line - 1; + + for (int i = startLine; i <= endLine; i++) { + lines.remove(i); + } + } + + return String.join(System.lineSeparator(), lines); + } + + private static String formatCode(String file, String fileName, CodeFormatter codeFormatter) throws Exception { + IDocument doc = new Document(file); + + int kind = IModule.MODULE_INFO_JAVA.equals(fileName) + ? CodeFormatter.K_MODULE_INFO : CodeFormatter.K_COMPILATION_UNIT; + kind |= CodeFormatter.F_INCLUDE_COMMENTS; + TextEdit edit = codeFormatter.format(kind, file, 0, file.length(), 0, null); + edit.apply(doc); + + return doc.get(); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/preprocessor/Preprocessor.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/preprocessor/Preprocessor.java new file mode 100644 index 0000000000..8ecf0a32e7 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/preprocessor/Preprocessor.java @@ -0,0 +1,337 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.preprocessor; + +import com.microsoft.typespec.http.client.generator.core.extension.jsonrpc.Connection; +import com.microsoft.typespec.http.client.generator.core.extension.model.Message; +import com.microsoft.typespec.http.client.generator.core.extension.model.MessageChannel; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.ChoiceSchema; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.ChoiceValue; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.CodeModel; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.ConstantSchema; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.ObjectSchema; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Schema; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.SealedChoiceSchema; +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; +import com.microsoft.typespec.http.client.generator.core.extension.plugin.NewPlugin; +import com.microsoft.typespec.http.client.generator.core.extension.plugin.PluginLogger; +import com.microsoft.typespec.http.client.generator.core.extension.base.util.FileUtils; +import com.microsoft.typespec.http.client.generator.core.preprocessor.tranformer.Transformer; +import com.azure.json.JsonProviders; +import com.azure.json.JsonReader; +import com.azure.json.ReadValueCallback; +import org.slf4j.Logger; +import org.yaml.snakeyaml.DumperOptions; +import org.yaml.snakeyaml.LoaderOptions; +import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.constructor.Constructor; +import org.yaml.snakeyaml.inspector.TrustedTagInspector; +import org.yaml.snakeyaml.introspector.Property; +import org.yaml.snakeyaml.nodes.NodeTuple; +import org.yaml.snakeyaml.nodes.Tag; +import org.yaml.snakeyaml.representer.Representer; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.function.Function; +import java.util.stream.Collectors; + +public class Preprocessor extends NewPlugin { + private final NewPlugin wrappedPlugin; + private final Logger logger; + protected static Preprocessor instance; + + public Preprocessor(NewPlugin wrappedPlugin, Connection connection, String plugin, String sessionId) { + super(connection, plugin, sessionId); + this.wrappedPlugin = wrappedPlugin; + this.logger = new PluginLogger(this, Preprocessor.class); + instance = this; + } + + public static Preprocessor getPluginInstance() { + return instance; + } + + public CodeModel processCodeModel() { + List allFiles = listInputs(); + List files = allFiles.stream().filter(s -> s.contains("no-tags")).collect(Collectors.toList()); + if (files.size() != 1) { + throw new RuntimeException( + String.format("Generator received incorrect number of inputs: %s : %s}", files.size(), + String.join(", ", files))); + } + String file = readFile(files.get(0)); + + Path codeModelFolder; + try { + codeModelFolder = FileUtils.createTempDirectory("code-model" + UUID.randomUUID()); + logger.info("Created temp directory for code model: {}", codeModelFolder); + } catch (IOException ex) { + logger.error("Failed to create temp directory for code model.", ex); + throw new RuntimeException("Failed to create temp directory for code model.", ex); + } + + try { + Files.writeString(codeModelFolder.resolve("code-model.yaml"), file); + } catch (Exception e) { + // + } + + CodeModel codeModel; + try { + if (!file.startsWith("{")) { + // YAML + codeModel = yamlMapper.loadAs(file, CodeModel.class); + } else { + try (JsonReader jsonReader = JsonProviders.createReader(file)) { + codeModel = CodeModel.fromJson(jsonReader); + } + } + } catch (Exception e) { + System.err.println("Got an error " + e.getMessage()); + connection.sendError(1, 500, "Cannot parse input into code model: " + e.getMessage()); + throw new RuntimeException("Cannot parse input into code model.", e); + } + + performPretransformUpdates(codeModel); + codeModel = new Transformer().transform(codeModel); + performPosttransformUpdates(codeModel); + + Representer representer = new Representer(new DumperOptions()) { + @Override + protected NodeTuple representJavaBeanProperty(Object javaBean, Property property, Object propertyValue, + Tag customTag) { + // if value of property is null, ignore it. + if (propertyValue == null) { + return null; + } else { + return super.representJavaBeanProperty(javaBean, property, propertyValue, customTag); + } + } + }; + LoaderOptions loaderOptions = new LoaderOptions(); + loaderOptions.setCodePointLimit(50 * 1024 * 1024); + loaderOptions.setMaxAliasesForCollections(Integer.MAX_VALUE); + loaderOptions.setTagInspector(new TrustedTagInspector()); + Yaml newYaml = new Yaml(new Constructor(loaderOptions), representer, new DumperOptions(), loaderOptions); + String output = newYaml.dump(codeModel); + + try { + Files.writeString(codeModelFolder.resolve("code-model-processed-no-tags.yaml"), output); + } catch (Exception e) { + logger.error("Failed to pre-process the code model.", e); + throw new RuntimeException("Failed to pre-process the code model.", e); + } + + return codeModel; + } + + private CodeModel performPosttransformUpdates(CodeModel codeModel) { + if (JavaSettings.getInstance().isOptionalConstantAsEnum()) { + return convertOptionalConstantsToEnum(codeModel); + } + return codeModel; + } + + private CodeModel performPretransformUpdates(CodeModel codeModel) { + if (JavaSettings.getInstance().isOptionalConstantAsEnum()) { + return convertOptionalConstantsToEnum(codeModel); + } + return codeModel; + } + + public static CodeModel convertOptionalConstantsToEnum(CodeModel codeModel) { + Function schemaIsConstantWithChoice = schema -> schema.getValueType() instanceof ChoiceSchema; + + Set constantSchemas = new HashSet<>(codeModel.getSchemas().getConstants()); + if (!constantSchemas.isEmpty()) { + Map convertedChoiceSchemas = new HashMap<>(); + + codeModel.getOperationGroups().stream().flatMap(og -> og.getOperations().stream()).forEach(o -> { + o.getParameters().stream().filter(p -> !p.isRequired() && p.getSchema() instanceof ConstantSchema) + .forEach(p -> { + ConstantSchema constantSchema = (ConstantSchema) p.getSchema(); + if (schemaIsConstantWithChoice.apply(constantSchema)) { + p.setSchema(constantSchema.getValueType()); + } else { + SealedChoiceSchema sealedChoiceSchema = convertedChoiceSchemas.computeIfAbsent(constantSchema, + Preprocessor::convertToChoiceSchema); + p.setSchema(sealedChoiceSchema); + } + + o.getSignatureParameters().add(p); + }); + + o.getRequests().forEach(r -> { + r.getParameters().stream().filter(p -> !p.isRequired() && p.getSchema() instanceof ConstantSchema) + .forEach(p -> { + ConstantSchema constantSchema = (ConstantSchema) p.getSchema(); + if (schemaIsConstantWithChoice.apply(constantSchema)) { + p.setSchema(constantSchema.getValueType()); + } else { + SealedChoiceSchema sealedChoiceSchema = convertedChoiceSchemas.computeIfAbsent( + constantSchema, Preprocessor::convertToChoiceSchema); + p.setSchema(sealedChoiceSchema); + } + + r.getSignatureParameters().add(p); + }); + }); + }); + + codeModel.getSchemas().getObjects().stream().flatMap(s -> s.getProperties().stream()) + .filter(p -> !p.isRequired() && p.getSchema() instanceof ConstantSchema) + .forEach(p -> { + ConstantSchema constantSchema = (ConstantSchema) p.getSchema(); + if (schemaIsConstantWithChoice.apply(constantSchema)) { + p.setSchema(constantSchema.getValueType()); + } else { + SealedChoiceSchema sealedChoiceSchema = convertedChoiceSchemas.computeIfAbsent(constantSchema, + Preprocessor::convertToChoiceSchema); + p.setSchema(sealedChoiceSchema); + } + }); + + if (JavaSettings.getInstance().getClientFlattenAnnotationTarget() + == JavaSettings.ClientFlattenAnnotationTarget.NONE) { + codeModel.getSchemas().getObjects().stream().flatMap(s -> s.getProperties().stream()) + .filter(p -> !p.isRequired() && p.getExtensions() != null && p.getExtensions().isXmsClientFlatten()) + .filter(p -> p.getSchema() instanceof ObjectSchema).forEach( + p -> ((ObjectSchema) p.getSchema()).getProperties().stream() + .filter(p1 -> p1.getSchema() instanceof ConstantSchema).forEach(p1 -> { + ConstantSchema constantSchema = (ConstantSchema) p1.getSchema(); + SealedChoiceSchema sealedChoiceSchema = convertedChoiceSchemas.computeIfAbsent( + constantSchema, Preprocessor::convertToChoiceSchema); + p1.setSchema(sealedChoiceSchema); + })); + } + + codeModel.getSchemas().getSealedChoices().addAll(convertedChoiceSchemas.values()); + } + return codeModel; + } + + private static SealedChoiceSchema convertToChoiceSchema(ConstantSchema constantSchema) { + SealedChoiceSchema sealedChoiceSchema = new SealedChoiceSchema(); + sealedChoiceSchema.setType(Schema.AllSchemaTypes.SEALED_CHOICE); + sealedChoiceSchema.setChoiceType(constantSchema.getValueType()); + sealedChoiceSchema.setDefaultValue(constantSchema.getDefaultValue()); + sealedChoiceSchema.setLanguage(constantSchema.getLanguage()); + sealedChoiceSchema.setSummary(constantSchema.getSummary()); + sealedChoiceSchema.setUsage(constantSchema.getUsage()); + + ChoiceValue choice = new ChoiceValue(); + choice.setValue(constantSchema.getValue().getValue().toString()); + choice.setLanguage(constantSchema.getValue().getLanguage()); + sealedChoiceSchema.setChoices(Collections.singletonList(choice)); + return sealedChoiceSchema; + } + + @Override + public String readFile(String fileName) { + return wrappedPlugin.readFile(fileName); + } + + @Override + public T getValue(String key, ReadValueCallback converter) { + return wrappedPlugin.getValue(key, converter); + } + +// @Override +// public Map getMapValue(Class keyType, Class valueType, String key) { +// return wrappedPlugin.getMapValue(keyType, valueType, key); +// } +// +// @Override +// public List getListValue(Class valueType, String key) { +// return wrappedPlugin.getListValue(valueType, key); +// } + + @Override + public String getStringValue(String key) { + return wrappedPlugin.getStringValue(key); + } + + @Override + public String getStringValue(String key, String defaultValue) { + return wrappedPlugin.getStringValue(key, defaultValue); + } + + @Override + public Boolean getBooleanValue(String key) { + return wrappedPlugin.getBooleanValue(key); + } + + @Override + public boolean getBooleanValue(String key, boolean defaultValue) { + return wrappedPlugin.getBooleanValue(key, defaultValue); + } + + @Override + public List listInputs() { + return wrappedPlugin.listInputs(); + } + + @Override + public List listInputs(String artifactType) { + return wrappedPlugin.listInputs(artifactType); + } + + @Override + public void message(Message message) { + wrappedPlugin.message(message); + } + + @Override + public void message(MessageChannel channel, String text, Throwable error, List keys) { + wrappedPlugin.message(channel, text, error, keys); + } + + @Override + public void writeFile(String fileName, String content, List sourceMap) { + wrappedPlugin.writeFile(fileName, content, sourceMap); + } + + @Override + public void writeFile(String fileName, String content, List sourceMap, String artifactType) { + wrappedPlugin.writeFile(fileName, content, sourceMap, artifactType); + } + + @Override + public void protectFiles(String path) { + wrappedPlugin.protectFiles(path); + } + + @Override + public String getConfigurationFile(String fileName) { + return wrappedPlugin.getConfigurationFile(fileName); + } + + @Override + public void updateConfigurationFile(String filename, String content) { + wrappedPlugin.updateConfigurationFile(filename, content); + } + + @Override + public boolean process() { + throw new UnsupportedOperationException("Use processCodeModel instead."); + } + + @Override + public boolean processInternal() { + throw new UnsupportedOperationException("Use processCodeModel instead."); + } + + private void clear() { + JavaSettings.clear(); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/preprocessor/namer/CodeNamer.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/preprocessor/namer/CodeNamer.java new file mode 100644 index 0000000000..70dadf55f8 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/preprocessor/namer/CodeNamer.java @@ -0,0 +1,347 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.preprocessor.namer; + +import org.atteo.evo.inflector.English; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; +import java.util.function.Predicate; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +public class CodeNamer { + private static final String[] BASIC_LATIN_CHARACTERS; + + private static final Set RESERVED_WORDS; + private static final Set RESERVED_WORDS_CLASSES; + + private static final Pattern CASE_SPLIT = Pattern.compile("[_\\- ]"); + + static { + BASIC_LATIN_CHARACTERS = new String[128]; + BASIC_LATIN_CHARACTERS[32] = "Space"; + BASIC_LATIN_CHARACTERS[33] = "ExclamationMark"; + BASIC_LATIN_CHARACTERS[34] = "QuotationMark"; + BASIC_LATIN_CHARACTERS[35] = "NumberSign"; + BASIC_LATIN_CHARACTERS[36] = "DollarSign"; + BASIC_LATIN_CHARACTERS[37] = "PercentSign"; + BASIC_LATIN_CHARACTERS[38] = "Ampersand"; + BASIC_LATIN_CHARACTERS[39] = "Apostrophe"; + BASIC_LATIN_CHARACTERS[40] = "LeftParenthesis"; + BASIC_LATIN_CHARACTERS[41] = "RightParenthesis"; + BASIC_LATIN_CHARACTERS[42] = "Asterisk"; + BASIC_LATIN_CHARACTERS[43] = "PlusSign"; + BASIC_LATIN_CHARACTERS[44] = "Comma"; + BASIC_LATIN_CHARACTERS[45] = "HyphenMinus"; + BASIC_LATIN_CHARACTERS[46] = "FullStop"; + BASIC_LATIN_CHARACTERS[47] = "Slash"; + BASIC_LATIN_CHARACTERS[48] = "Zero"; + BASIC_LATIN_CHARACTERS[49] = "One"; + BASIC_LATIN_CHARACTERS[50] = "Two"; + BASIC_LATIN_CHARACTERS[51] = "Three"; + BASIC_LATIN_CHARACTERS[52] = "Four"; + BASIC_LATIN_CHARACTERS[53] = "Five"; + BASIC_LATIN_CHARACTERS[54] = "Six"; + BASIC_LATIN_CHARACTERS[55] = "Seven"; + BASIC_LATIN_CHARACTERS[56] = "Eight"; + BASIC_LATIN_CHARACTERS[57] = "Nine"; + BASIC_LATIN_CHARACTERS[58] = "Colon"; + BASIC_LATIN_CHARACTERS[59] = "Semicolon"; + BASIC_LATIN_CHARACTERS[60] = "LessThanSign"; + BASIC_LATIN_CHARACTERS[61] = "EqualSign"; + BASIC_LATIN_CHARACTERS[62] = "GreaterThanSign"; + BASIC_LATIN_CHARACTERS[63] = "QuestionMark"; + BASIC_LATIN_CHARACTERS[64] = "AtSign"; + BASIC_LATIN_CHARACTERS[91] = "LeftSquareBracket"; + BASIC_LATIN_CHARACTERS[92] = "Backslash"; + BASIC_LATIN_CHARACTERS[93] = "RightSquareBracket"; + BASIC_LATIN_CHARACTERS[94] = "CircumflexAccent"; + BASIC_LATIN_CHARACTERS[96] = "GraveAccent"; + BASIC_LATIN_CHARACTERS[123] = "LeftCurlyBracket"; + BASIC_LATIN_CHARACTERS[124] = "VerticalBar"; + BASIC_LATIN_CHARACTERS[125] = "RightCurlyBracket"; + BASIC_LATIN_CHARACTERS[126] = "Tilde"; + + RESERVED_WORDS = Set.of("abstract", "assert", "boolean", "Boolean", "break", + "byte", "Byte", "case", "catch", "char", "Character", "class", "Class", "const", "continue", "default", + "do", "double", "Double", "else", "enum", "extends", "false", "final", "finally", "float", "Float", "for", + "goto", "if", "implements", "import", "int", "Integer", "long", "Long", "interface", "instanceof", "native", + "new", "null", "package", "private", "protected", "public", "return", "short", "Short", "static", + "strictfp", "super", "switch", "synchronized", "this", "throw", "throws", "transient", "true", "try", + "void", "Void", "volatile", "while", "Date", "Datetime", "OffsetDateTime", "Duration", "Period", "Stream", + "String", "Object", "header", "_"); + + // following are commonly used classes/annotations in service client, from azure-core + RESERVED_WORDS_CLASSES = new HashSet<>(RESERVED_WORDS); + RESERVED_WORDS_CLASSES.addAll(Arrays.asList("Host", "ServiceInterface", "ServiceMethod", "ServiceClient", + "ReturnType", "Get", "Put", "Post", "Patch", "Delete", "Headers", "ExpectedResponses", + "UnexpectedResponseExceptionType", "UnexpectedResponseExceptionTypes", "HostParam", "PathParam", + "QueryParam", "HeaderParam", "FormParam", "BodyParam", "Fluent", "Immutable", "JsonFlatten", "Override")); + } + + private CodeNamer() { + } + + public static String getBasicLatinCharacter(char c) { + if (c >= 128) { + return null; + } + + return BASIC_LATIN_CHARACTERS[c]; + } + + public static String toCamelCase(String name) { + if (name == null || name.trim().isEmpty()) { + return name; + } + + // Remove leading underscores. + if (name.charAt(0) == '_') { + return toCamelCase(name.substring(1)); + } + + String[] splits = CASE_SPLIT.split(name); + if (splits.length == 0) { + return ""; + } + + splits[0] = formatCase(splits[0], true); + for (int i = 1; i != splits.length; i++) { + splits[i] = formatCase(splits[i], false); + } + return String.join("", splits); + } + + public static String toPascalCase(String name) { + if (name == null || name.trim().isEmpty()) { + return name; + } + + // Preserve leading underscores and treat them like + // uppercase characters by calling 'CamelCase()' on the rest. + if (name.charAt(0) == '_') { + return '_' + toCamelCase(name.substring(1)); + } + + return CASE_SPLIT.splitAsStream(name) + .filter(s -> s != null && !s.isEmpty()) + .map(s -> formatCase(s, false)) + .collect(Collectors.joining()); + } + + public static String escapeXmlComment(String comment) { + if (comment == null || comment.isEmpty()) { + return comment; + } + + // Use a linear replacement for the all the characters. + // This has a few benefits: + // 1. It performs a single loop over the comment string. + // 2. It avoids instantiating multiple strings if multiple of the replacement cases are found. + // 3. If no replacements are needed, it returns the original string. + StringBuilder sb = null; + int prevStart = 0; + int commentLength = comment.length(); + + for (int i = 0; i < commentLength; i++) { + String replacement = null; + char c = comment.charAt(i); + if (c == '&') { + replacement = "&"; + } else if (c == '<') { + replacement = "<"; + } else if (c == '>') { + replacement = ">"; + } + + if (replacement != null) { + if (sb == null) { + // Add enough overhead to account for 1/8 of the string to be replaced. + sb = new StringBuilder(commentLength + 3 * (commentLength / 8)); + } + + if (prevStart != i) { + sb.append(comment, prevStart, i); + } + sb.append(replacement); + prevStart = i + 1; + } + } + + if (sb == null) { + return comment; + } + + sb.append(comment, prevStart, commentLength); + return sb.toString(); + } + + private static String formatCase(String name, boolean toLower) { + if (name == null || name.isEmpty()) { + return name; + } + + int length = name.length(); + char c0 = name.charAt(0); + if ((length < 2) + || ((length == 2) && Character.isUpperCase(c0) && Character.isUpperCase(name.charAt(1)))) { + return toLower ? name.toLowerCase() : name.toUpperCase(); + } else { + return (toLower ? Character.toLowerCase(c0) : Character.toUpperCase(c0)) + name.substring(1); + } + } + + public static String removeInvalidCharacters(String name) { + return getValidName(name, c -> c == '_' || c == '-'); + } + + /** + * Gets a valid name for the given name. + * + * @param name The name to get a valid name for. + * @return The valid name. + */ + public static String getValidName(String name) { + return getValidName(name, c -> false); + } + + /** + * Gets a valid name for the given name. + * + * @param name The name to get a valid name for. + * @param allowedCharacterMatcher A predicate that determines if a character is allowed. + * @return The valid name. + */ + public static String getValidName(String name, Predicate allowedCharacterMatcher) { + String correctName = removeInvalidCharacters(name, allowedCharacterMatcher); + + // here we have only letters and digits or an empty String + if (correctName == null || correctName.isEmpty() || getBasicLatinCharacter(correctName.charAt(0)) != null) { + StringBuilder sb = new StringBuilder(); + for (char symbol : name.toCharArray()) { + String basicLatinCharacterReplacement = getBasicLatinCharacter(symbol); + if (basicLatinCharacterReplacement != null) { + sb.append(basicLatinCharacterReplacement); + } else { + sb.append(symbol); + } + } + correctName = removeInvalidCharacters(sb.toString(), allowedCharacterMatcher); + } + + // if it is still empty String, throw + if (correctName == null || correctName.isEmpty()) { + throw new IllegalArgumentException( + "Property name " + name + " cannot be used as an Identifier, as it contains only invalid characters."); + } + + return correctName; + } + + public static String getClientName(String name) { + if (name == null || name.trim().isEmpty()) { + return name; + } + return getEscapedReservedNameAndClasses(toPascalCase(removeInvalidCharacters(name)), "Client"); + } + + public static String getTypeName(String name) { + if (name == null || name.trim().isEmpty()) { + return name; + } + return getEscapedReservedNameAndClasses(toPascalCase(removeInvalidCharacters(name)), "Model"); + } + + public static String getParameterName(String name) { + if (name == null || name.trim().isEmpty()) { + return name; + } + return getEscapedReservedName(toCamelCase(removeInvalidCharacters(name)), "Parameter"); + } + + public static String getPropertyName(String name) { + if (name == null || name.trim().isEmpty()) { + return name; + } + return getEscapedReservedName(toCamelCase(removeInvalidCharacters(name)), "Property"); + } + + public static String getMethodGroupName(String name) { + if (name == null || name.trim().isEmpty()) { + return name; + } + name = toPascalCase(name); + return getEscapedReservedName(name, "Operation"); + } + + public static String getPlural(String name) { + if (name != null && !name.isEmpty() && !name.endsWith("s") && !name.endsWith("S")) { + name = English.plural(name); + } + return name; + } + + public static String getMethodName(String name) { + name = toCamelCase(name); + return getEscapedReservedName(name, "Method"); + } + + public static String getEscapedReservedName(String name, String appendValue) { + Objects.requireNonNull(name); + Objects.requireNonNull(appendValue); + + if (RESERVED_WORDS.contains(name)) { + name += appendValue; + } + + return name; + } + + protected static String getEscapedReservedNameAndClasses(String name, String appendValue) { + Objects.requireNonNull(name); + Objects.requireNonNull(appendValue); + + if (RESERVED_WORDS_CLASSES.contains(name)) { + name += appendValue; + } + + return name; + } + + private static String removeInvalidCharacters(String name, Predicate allowedCharacterMatcher) { + if (name == null || name.isEmpty()) { + return name; + } + + StringBuilder sb = null; + int prevStart = 0; + int nameLength = name.length(); + + for (int i = 0; i < nameLength; i++) { + char c = name.charAt(i); + if (!Character.isLetterOrDigit(c) && !allowedCharacterMatcher.test(c)) { + if (sb == null) { + sb = new StringBuilder(nameLength); + } + + if (prevStart != i) { + sb.append(name, prevStart, i); + } + + sb.append('_'); + prevStart = i + 1; + } + } + + if (sb == null) { + return name; + } + + sb.append(name, prevStart, nameLength); + return sb.toString(); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/preprocessor/tranformer/Transformer.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/preprocessor/tranformer/Transformer.java new file mode 100644 index 0000000000..578af267c6 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/preprocessor/tranformer/Transformer.java @@ -0,0 +1,591 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.preprocessor.tranformer; + +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.AndSchema; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.BinarySchema; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.ChoiceSchema; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Client; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.CodeModel; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.DictionarySchema; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Language; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Languages; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Metadata; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.NumberSchema; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.ObjectSchema; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Operation; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.OperationGroup; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.OrSchema; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Parameter; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Property; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Protocol; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Protocols; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Request; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.RequestParameterLocation; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Schema; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.SchemaContext; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Schemas; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.SealedChoiceSchema; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.StringSchema; +import com.microsoft.typespec.http.client.generator.core.extension.model.extensionmodel.XmsExtensions; +import com.microsoft.typespec.http.client.generator.core.extension.model.extensionmodel.XmsPageable; +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; +import com.microsoft.typespec.http.client.generator.core.preprocessor.namer.CodeNamer; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.ListIterator; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class Transformer { + + public CodeModel transform(CodeModel codeModel) { + renameCodeModel(codeModel); + transformSchemas(codeModel.getSchemas()); + if (JavaSettings.getInstance().getClientFlattenAnnotationTarget() == JavaSettings.ClientFlattenAnnotationTarget.NONE) { + markFlattenedSchemas(codeModel); + } + transformOperationGroups(codeModel.getOperationGroups(), codeModel); + // multi-clients for TypeSpec + if (codeModel.getClients() != null) { + transformClients(codeModel.getClients(), codeModel); + } + return codeModel; + } + + private void transformSchemas(Schemas schemas) { + // merge GroupSchema into ObjectSchema + if (schemas.getGroups() != null) { + schemas.getGroups().forEach(group -> { + if (group.getUsage() == null) { + group.setUsage(new HashSet<>()); + } + group.getUsage().add(SchemaContext.OPTIONS_GROUP); + }); + } + schemas.getObjects().addAll(schemas.getGroups()); + schemas.setGroups(new ArrayList<>()); + + for (ObjectSchema objectSchema : schemas.getObjects()) { + renameType(objectSchema); + for (Property property : objectSchema.getProperties()) { + renameProperty(property); + } + if (objectSchema.getDiscriminator() != null) { + renameProperty(objectSchema.getDiscriminator().getProperty()); + } + } + for (AndSchema andSchema : schemas.getAnds()) { + renameType(andSchema); + } + for (ChoiceSchema choiceSchema : schemas.getChoices()) { + renameType(choiceSchema); + } + for (SealedChoiceSchema sealedChoiceSchema : schemas.getSealedChoices()) { + renameType(sealedChoiceSchema); + } + for (DictionarySchema dictionarySchema : schemas.getDictionaries()) { + renameType(dictionarySchema); + } + for (OrSchema unionSchema : schemas.getOrs()) { + renameType(unionSchema); + + // these ObjectSchema is not added to codeModel.schemas + for (ObjectSchema objectSchema : unionSchema.getAnyOf()) { + renameType(objectSchema); + for (Property property : objectSchema.getProperties()) { + renameProperty(property); + } + } + } + } + + private void transformClients(List clients, CodeModel codeModel) { + for (Client client : clients) { + renameClient(client); + + if (client.getServiceVersion() != null) { + renameClient(client.getServiceVersion()); + } + + if (client.getOperationGroups() != null) { + for (OperationGroup operationGroup : client.getOperationGroups()) { + List pagingOperations = new ArrayList<>(); + + operationGroup.setCodeModel(client); + renameMethodGroup(operationGroup); + for (Operation operation : operationGroup.getOperations()) { + operation.setOperationGroup(operationGroup); + + if (operation.getExtensions() != null && operation.getExtensions().getXmsPageable() != null) { + pagingOperations.add(operation); + } + } + + // paging + for (Operation operation : pagingOperations) { + if (nonNullNextLink(operation)) { + addPagingNextOperation(client, operation.getOperationGroup(), operation); + } + } + } + } + } + } + + private void transformOperationGroups(List operationGroups, CodeModel codeModel) { + List pagingOperations = new ArrayList<>(); + for (OperationGroup operationGroup : operationGroups) { + operationGroup.setCodeModel(codeModel); + renameMethodGroup(operationGroup); + for (Operation operation : operationGroup.getOperations()) { + operation.setOperationGroup(operationGroup); + renameMethod(operation); + if (operation.getConvenienceApi() != null) { + renameMethod(operation.getConvenienceApi()); + if (operation.getConvenienceApi().getRequests() != null) { + for (Request request : operation.getConvenienceApi().getRequests()) { + for (Parameter parameter : request.getParameters()) { + parameter.setOperation(operation); + renameVariable(parameter); + } + } + } + } + for (Request request : operation.getRequests()) { + Stream newParameters = Stream.concat(operation.getParameters().stream(), request.getParameters().stream()); + request.setParameters(newParameters.collect(Collectors.toList())); + Stream newSignatureParameters = Stream.concat(operation.getSignatureParameters().stream(), request.getSignatureParameters().stream()); + newSignatureParameters = + newSignatureParameters.filter(param -> param.getGroupedBy() == null); + request.setSignatureParameters(newSignatureParameters.collect(Collectors.toList())); + for (int i = 0; i < request.getParameters().size(); i++) { + Parameter parameter = request.getParameters().get(i); + parameter.setOperation(operation); + renameVariable(parameter); + // add Content-Length for Flux if not already present + JavaSettings settings = JavaSettings.getInstance(); + if (!settings.isDataPlaneClient()) { + if (parameter.getSchema() instanceof BinarySchema) { + if (request.getParameters().stream().noneMatch(p -> p.getProtocol() != null + && p.getProtocol().getHttp() != null + && p.getProtocol().getHttp().getIn() == RequestParameterLocation.HEADER + && "content-length".equalsIgnoreCase(p.getLanguage().getDefault().getSerializedName()))) { + Parameter contentLength = createContentLengthParameter(operation, parameter); + // put contentLength parameter before input body + request.getParameters().add(++i, contentLength); + request.getSignatureParameters().add(request.getSignatureParameters().indexOf(parameter) + 1, contentLength); + } + } + } + // convert contentType to header param + Optional contentType = request.getParameters().stream() + .filter(p -> (p.getProtocol() == null || p.getProtocol().getHttp() == null) && "contentType".equals(p.getLanguage().getDefault().getName())) + .findFirst(); + if (contentType.isPresent()) { + Protocols protocols = new Protocols(); + protocols.setHttp(new Protocol()); + protocols.getHttp().setIn(RequestParameterLocation.HEADER); + contentType.get().setProtocol(protocols); + contentType.get().getLanguage().getDefault().setSerializedName("Content-Type"); + } + } + renameOdataParameterNames(request); + deduplicateParameterNames(request); + } + + if (operation.getExtensions() != null && operation.getExtensions().getXmsPageable() != null) { + pagingOperations.add(operation); + } + } + } + + // paging + for (Operation operation : pagingOperations) { + if (nonNullNextLink(operation)) { + addPagingNextOperation(codeModel, operation.getOperationGroup(), operation); + } + } + } + + private static void markFlattenedSchemas(CodeModel codeModel) { + for (ObjectSchema objectSchema : codeModel.getSchemas().getObjects()) { + Map flattenedSchemas = null; + for (Property property : objectSchema.getProperties()) { + if (property.getExtensions() != null && property.getExtensions().isXmsClientFlatten() && property.getSchema() instanceof ObjectSchema) { + ObjectSchema flattenedSchema = (ObjectSchema) property.getSchema(); + + boolean isPolymorphic = flattenedSchema.getDiscriminator() != null || flattenedSchema.getDiscriminatorValue() != null; + if (isPolymorphic) { +// LOGGER.warn("x-ms-client-flatten is not allowed on polymorphic model '{}', on property '{}'", flattenedSchema.getLanguage().getJava().getName(), property.getLanguage().getJava().getName()); + property.getExtensions().setXmsClientFlatten(false); + continue; + } + + if (flattenedSchemas == null) { + flattenedSchemas = new HashMap<>(); + } + flattenedSchemas.put(property.getLanguage().getJava().getName(), flattenedSchema); + + // mark as flattened schema + flattenedSchema.setFlattenedSchema(true); + } + } + } + } + + private static boolean nonNullNextLink(Operation operation) { + return operation.getExtensions().getXmsPageable().getNextLinkName() != null && !operation.getExtensions().getXmsPageable().getNextLinkName().isEmpty(); + } + + private static class OperationSignature { + private final String operationGroup; + private final String operationName; + + private OperationSignature(String operationGroup, String operationName) { + this.operationGroup = operationGroup; + this.operationName = operationName; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + OperationSignature that = (OperationSignature) o; + return Objects.equals(operationGroup, that.operationGroup) && Objects.equals(operationName, that.operationName); + } + + @Override + public int hashCode() { + return Objects.hash(operationGroup, operationName); + } + } + + private final Map pagingNextOperationResponseSchemaMap = new HashMap<>(); + + // Operation -> next page operation + private final Map operationNextPageOperationMap = new HashMap<>(); + + /** + * Adds next page operation for the given operation. + * If the same operation instance is provided, same nextOperation will be returned. + * Current operation and generated nextOperation share the same extension instance. next page operation's nextOperation property should + * always point to itself for it to be recognized as the next page operation(see ClientMethodMapper#createPageableClientMethods in javagen module). + * + * @param client code model client object + * @param operationGroup operation group of the operation + * @param operation pageable operation to add next page operation + */ + private void addPagingNextOperation(Client client, OperationGroup operationGroup, Operation operation) { + String operationGroupName; + String operationName; + if (operation.getExtensions().getXmsPageable().getOperationName() != null) { + String operationGroupAndName = operation.getExtensions().getXmsPageable().getOperationName(); + String operationNameTmp; + if (operationGroupAndName.contains("_")) { + String[] parts = operationGroupAndName.split("_", 2); + operationGroupName = CodeNamer.getMethodGroupName(parts[0]); + operationNameTmp = CodeNamer.getMethodName(parts[1]); + } else { + operationGroupName = operationGroup.getLanguage().getJava().getName(); + operationNameTmp = CodeNamer.getMethodName(operationGroupAndName); + } + + if (!operation.getResponses().isEmpty() && operation.getResponses().iterator().next().getSchema() != null) { + Schema responseSchema = operation.getResponses().iterator().next().getSchema(); + OperationSignature signature = new OperationSignature(operationGroupName, operationNameTmp); + if (pagingNextOperationResponseSchemaMap.containsKey(signature) && pagingNextOperationResponseSchemaMap.get(signature) != responseSchema) { + // method signature conflict for different response schema, try a different operation name + operationName = operation.getLanguage().getJava().getName() + "Next"; + signature = new OperationSignature(operationGroupName, operationName); + } else { + operationName = operationNameTmp; + } + pagingNextOperationResponseSchemaMap.put(signature, responseSchema); + } else { + operationName= operationNameTmp; + } + } else { + operationGroupName = operationGroup.getLanguage().getJava().getName(); + operationName = operation.getLanguage().getJava().getName() + "Next"; + } + if (client.getOperationGroups().stream() + .noneMatch(og -> og.getLanguage().getJava().getName().equals(operationGroupName))) { + OperationGroup newOg = new OperationGroup(); + newOg.setCodeModel(client); + newOg.set$key(operationGroupName); + newOg.setOperations(new ArrayList<>()); + newOg.setExtensions(operationGroup.getExtensions()); + newOg.setLanguage(new Languages()); + newOg.getLanguage().setJava(new Language()); + newOg.getLanguage().getJava().setName(operationGroupName); + newOg.getLanguage().getJava().setDescription(operationGroup.getLanguage().getJava().getDescription()); + newOg.setProtocol(operationGroup.getProtocol()); + + client.getOperationGroups().add(newOg); + operationGroup = newOg; + } + + if (operationGroup.getOperations().stream() + .noneMatch(o -> o.getLanguage().getJava().getName().equals(operationName))) { + Operation nextOperation = new Operation(); + OperationSignature operationSignature = new OperationSignature( + operation.getOperationGroup().getLanguage().getJava().getName(), + operation.getLanguage().getJava().getName()); + if (!operationNextPageOperationMap.containsKey(operationSignature)) { + nextOperation.setOperationGroup(operationGroup); + nextOperation.set$key(operationName); + nextOperation.setLanguage(new Languages()); + nextOperation.getLanguage().setJava(new Language()); + nextOperation.getLanguage().getJava().setName(operationName); + nextOperation.getLanguage().getJava().setDescription("Get the next page of items"); + nextOperation.setRequests(new ArrayList<>()); + Request request = new Request(); + nextOperation.getRequests().add(request); + nextOperation.getRequests().get(0).setProtocol(new Protocols()); + nextOperation.getRequests().get(0).getProtocol().setHttp(new Protocol()); + nextOperation.getRequests().get(0).getProtocol().getHttp().setPath("{nextLink}"); + nextOperation.getRequests().get(0).getProtocol().getHttp() + .setUri(operation.getRequests().get(0).getProtocol().getHttp().getUri()); + nextOperation.getRequests().get(0).getProtocol().getHttp().setMethod("get"); + nextOperation.getRequests().get(0).setExtensions(operation.getRequests().get(0).getExtensions()); + nextOperation.getRequests().get(0).setLanguage(operation.getLanguage()); + Parameter nextLink = new Parameter(); + nextLink.setOperation(nextOperation); + nextLink.setImplementation(Parameter.ImplementationLocation.METHOD); + nextLink.set$key("nextLink"); + nextLink.setNullable(false); + nextLink.setSummary("The URL to get the next list of items"); + nextLink.setSchema(new StringSchema()); + nextLink.setRequired(true); + nextLink.setLanguage(new Languages()); + nextLink.getLanguage().setJava(new Language()); + nextLink.getLanguage().getJava().setName("nextLink"); + nextLink.getLanguage().getJava().setSerializedName("nextLink"); + nextLink.getLanguage().setDefault(nextLink.getLanguage().getJava()); + nextLink.setProtocol(new Protocols()); + nextLink.getProtocol().setHttp(new Protocol()); + nextLink.getProtocol().getHttp().setIn(RequestParameterLocation.PATH); + nextLink.setExtensions(new XmsExtensions()); + nextLink.getExtensions().setXmsSkipUrlEncoding(true); + List requestParams = new ArrayList<>(); + requestParams.add(nextLink); + nextOperation.getRequests().get(0).setParameters(requestParams); + List signatureParams = new ArrayList<>(); + signatureParams.add(nextLink); + nextOperation.getRequests().get(0).setSignatureParameters(signatureParams); + nextOperation.setApiVersions(operation.getApiVersions()); + nextOperation.setDeprecated(operation.getDeprecated()); + nextOperation.setDescription(operation.getDescription()); + nextOperation.setExceptions(operation.getExceptions()); + nextOperation.setExtensions(operation.getExtensions()); + nextOperation.setExternalDocs(operation.getExternalDocs()); + nextOperation.setProfile(operation.getProfile()); + nextOperation.setResponses(operation.getResponses()); + nextOperation.setSummary(operation.getSummary()); + nextOperation.setUid(operation.getUid()); + + Operation nextOperationLocal = nextOperation; + + if (operation.getExtensions().getXmsPageable().getOperationName() == null) { + operation.getRequests().stream().flatMap(r -> r.getParameters().stream()) + .filter(parameter -> { + return parameter.getProtocol() == null || parameter.getProtocol().getHttp() == null + || (parameter.getProtocol().getHttp().getIn() != null + && (parameter.getProtocol().getHttp().getIn().equals(RequestParameterLocation.HEADER) + || parameter.getProtocol().getHttp().getIn().equals(RequestParameterLocation.URI))); + }) + .forEach(param -> { + nextOperationLocal.getRequests().get(0).getParameters().add(param); + }); + + operation.getRequests().stream().flatMap(r -> r.getSignatureParameters().stream()) + .filter(parameter -> { + return parameter.getProtocol() == null || parameter.getProtocol().getHttp() == null + || (parameter.getProtocol().getHttp().getIn() != null + && (parameter.getProtocol().getHttp().getIn().equals(RequestParameterLocation.HEADER) + || parameter.getProtocol().getHttp().getIn().equals(RequestParameterLocation.URI))); + }) + .forEach(param -> { + nextOperationLocal.getRequests().get(0).getSignatureParameters().add(param); + }); + } + operation.getExtensions().getXmsPageable().setNextOperation(nextOperation); + nextOperation.getExtensions().getXmsPageable().setNextOperation(nextOperation); + operationNextPageOperationMap.put(operationSignature, nextOperation); + } else { + // In case the same operation instance is processed more than once(both in "transformOperationGroups" and "transformClients"), + // we share the same next-page operation for the same operation instance. + nextOperation = operationNextPageOperationMap.get(operationSignature); + } + operationGroup.getOperations().add(nextOperation); + } else { + Operation nextOperation = operationGroup.getOperations().stream() + .filter(o -> o.getLanguage().getJava().getName().equals(operationName)) + .findFirst().get(); + if (nextOperation.getExtensions() == null) { + nextOperation.setExtensions(new XmsExtensions()); + } + if (nextOperation.getExtensions().getXmsPageable() == null) { + nextOperation.getExtensions().setXmsPageable(new XmsPageable()); + } + operation.getExtensions().getXmsPageable().setNextOperation(nextOperation); + nextOperation.getExtensions().getXmsPageable().setNextOperation(nextOperation); + } + } + + private void renameType(Metadata schema) { + Language language = schema.getLanguage().getDefault(); + Language java = addJavaLanguage(schema); + java.setName(CodeNamer.getTypeName(language.getName())); + java.setSerializedName(language.getSerializedName()); + java.setDescription(language.getDescription()); + schema.getLanguage().setJava(java); + } + + private void renameProperty(Property property) { + Language language = property.getLanguage().getDefault(); + Language java = addJavaLanguage(property); + java.setName(CodeNamer.getPropertyName(language.getName())); + java.setSerializedName(language.getSerializedName()); + java.setDescription(language.getDescription()); + property.getLanguage().setJava(java); + } + + private void renameCodeModel(CodeModel codeModel) { + renameType(codeModel); + if (codeModel.getLanguage().getJava().getName() == null + || codeModel.getLanguage().getJava().getName().isEmpty()) { + codeModel.getLanguage().getJava().setName(CodeNamer.getClientName(codeModel.getInfo().getTitle())); + codeModel.getLanguage().getJava().setDescription(codeModel.getInfo().getDescription()); + } + } + + private void renameClient(Metadata client) { + Language language = client.getLanguage().getDefault(); + Language java = addJavaLanguage(client); + java.setName(CodeNamer.getClientName(language.getName())); + java.setDescription(language.getDescription()); + client.getLanguage().setJava(java); + } + + private void renameVariable(Metadata schema) { + Language language = schema.getLanguage().getDefault(); + Language java = addJavaLanguage(schema); + java.setName(CodeNamer.getParameterName(language.getName())); + java.setSerializedName(language.getSerializedName()); + java.setDescription(language.getDescription()); + schema.getLanguage().setJava(java); + } + + private void renameMethodGroup(Metadata schema) { + Language language = schema.getLanguage().getDefault(); + Language java = addJavaLanguage(schema); + java.setName(CodeNamer.getMethodGroupName(language.getName())); + java.setSerializedName(language.getSerializedName()); + java.setDescription(language.getDescription()); + schema.getLanguage().setJava(java); + } + + private void renameMethod(Metadata schema) { + Language language = schema.getLanguage().getDefault(); + Language java = addJavaLanguage(schema); + java.setName(CodeNamer.getMethodName(language.getName())); + java.setSerializedName(language.getSerializedName()); + java.setDescription(language.getDescription()); + } + + private Language addJavaLanguage(Metadata schema) { + Language java = schema.getLanguage().getJava(); + if (java == null) { + java = new Language(); + schema.getLanguage().setJava(java); + } + return java; + } + + private Parameter createContentLengthParameter(Operation operation, Parameter bodyParam) { + Parameter contentType = new Parameter(); + contentType.setOperation(operation); + contentType.setDescription("The Content-Length header for the request"); + contentType.setRequired(bodyParam.isRequired()); + NumberSchema longSchema = new NumberSchema(); + longSchema.setPrecision(64); + longSchema.setType(Schema.AllSchemaTypes.INTEGER); + contentType.setSchema(longSchema); + contentType.setImplementation(Parameter.ImplementationLocation.METHOD); + contentType.setProtocol(new Protocols()); + contentType.getProtocol().setHttp(new Protocol()); + contentType.getProtocol().getHttp().setIn(RequestParameterLocation.HEADER); + Language language = new Language(); + language.setName("contentLength"); + language.setSerializedName("Content-Length"); + language.setDescription("The Content-Length header for the request"); + contentType.setLanguage(new Languages()); + contentType.getLanguage().setDefault(language); + contentType.getLanguage().setJava(language); + return contentType; + } + + private static void deduplicateParameterNames(Request request) { + if (request == null || request.getParameters() == null || request.getParameters().isEmpty()) { + return; + } + + List parameters = request.getParameters(); + // remove duplicate item + List deduplicatedParameters = parameters.stream() + .distinct() + .collect(Collectors.toList()); + if (deduplicatedParameters.size() < parameters.size()) { + parameters = deduplicatedParameters; + request.setParameters(parameters); + } + + // rename if name conflict + Set parameterNames = new HashSet<>(); + ListIterator iter = parameters.listIterator(); + while (iter.hasNext()) { + Parameter parameter = iter.next(); + if (parameter.getOriginalParameter() == null // skip the parameters resulted from parameter-flattening as they are not in proxy method + && parameterNames.contains(parameter.getLanguage().getJava().getName())) { + parameter.getLanguage().getJava().setName(parameter.getLanguage().getJava().getName() + "Param"); + } + + parameterNames.add(parameter.getLanguage().getJava().getName()); + } + } + + private final static Map ODATA_PARAMETER_NAME_CONVERSION = new HashMap<>(2); + static { + ODATA_PARAMETER_NAME_CONVERSION.put("maxpagesize", "maxPageSize"); + ODATA_PARAMETER_NAME_CONVERSION.put("orderby", "orderBy"); + } + + private static void renameOdataParameterNames(Request request) { + List parameters = request.getParameters(); + ListIterator iter = parameters.listIterator(); + while (iter.hasNext()) { + Parameter parameter = iter.next(); + if (parameter.getProtocol() != null && parameter.getProtocol().getHttp() != null + && (parameter.getProtocol().getHttp().getIn() == RequestParameterLocation.QUERY + || parameter.getProtocol().getHttp().getIn() == RequestParameterLocation.HEADER)) { + String serializedName = parameter.getLanguage().getDefault().getSerializedName(); + String convertedName = ODATA_PARAMETER_NAME_CONVERSION.get(serializedName); + if (convertedName != null + // no x-ms-client-name + && serializedName.equals(parameter.getLanguage().getJava().getName())) { + parameter.getLanguage().getJava().setName(convertedName); + } + } + } + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/ChangelogTemplate.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/ChangelogTemplate.java new file mode 100644 index 0000000000..9939296b6a --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/ChangelogTemplate.java @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.template; + +import com.microsoft.typespec.http.client.generator.core.model.projectmodel.Project; +import com.microsoft.typespec.http.client.generator.core.util.TemplateUtil; + +public class ChangelogTemplate { + + public String write(Project project) { + return TemplateUtil.loadTextFromResource("Changelog_protocol.txt", + TemplateUtil.SERVICE_NAME, project.getServiceName(), + TemplateUtil.SERVICE_DESCRIPTION, project.getServiceDescriptionForMarkdown(), + TemplateUtil.ARTIFACT_VERSION, project.getVersion(), + TemplateUtil.DATE_UTC, "Unreleased" + ); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/ClientMethodSampleTemplate.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/ClientMethodSampleTemplate.java new file mode 100644 index 0000000000..19cce762ce --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/ClientMethodSampleTemplate.java @@ -0,0 +1,94 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.template; + +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.AsyncSyncClient; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethod; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethodExample; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ConvenienceMethod; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ProxyMethodExample; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ServiceClient; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.examplemodel.ExampleHelperFeature; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaFile; +import com.microsoft.typespec.http.client.generator.core.template.example.ClientInitializationExampleWriter; +import com.microsoft.typespec.http.client.generator.core.template.example.ClientMethodExampleWriter; +import com.microsoft.typespec.http.client.generator.core.template.example.ModelExampleWriter; + +import java.util.HashSet; +import java.util.Set; + +public class ClientMethodSampleTemplate implements IJavaTemplate { + private static final ClientMethodSampleTemplate INSTANCE = new ClientMethodSampleTemplate(); + + public static ClientMethodSampleTemplate getInstance() { + return INSTANCE; + } + + @Override + public void write(ClientMethodExample clientMethodExample, JavaFile javaFile) { + String filename = clientMethodExample.getFilename(); + final ClientMethod method = clientMethodExample.getClientMethod(); + final AsyncSyncClient syncClient = clientMethodExample.getSyncClient(); + final ServiceClient serviceClient = clientMethodExample.getClientBuilder().getServiceClient(); + final ProxyMethodExample proxyMethodExample = clientMethodExample.getProxyMethodExample(); + + ClientInitializationExampleWriter clientInitializationExampleWriter = + new ClientInitializationExampleWriter( + syncClient, + method, + proxyMethodExample, + serviceClient); + + ClientMethodExampleWriter clientMethodExampleWriter = + new ClientMethodExampleWriter(method, clientInitializationExampleWriter.getClientVarName(), proxyMethodExample); + + // declare imports + Set imports = new HashSet<>(); + imports.addAll(clientInitializationExampleWriter.getImports()); + imports.addAll(clientMethodExampleWriter.getImports()); + method.getReturnValue().getType().addImportsTo(imports, false); + javaFile.declareImport(imports); + + javaFile.publicClass(null, filename, classBlock -> { + Set helperFeatures = clientMethodExampleWriter.getHelperFeatures(); + String methodSignature = "void main(String[] args)"; + if (helperFeatures.contains(ExampleHelperFeature.ThrowsIOException)) { + methodSignature += " throws IOException"; + } + classBlock.publicStaticMethod(methodSignature, methodBlock -> { + // write client initialization + clientInitializationExampleWriter.write(methodBlock); + + // write method invocation + + // codesnippet begin + if (proxyMethodExample.getCodeSnippetIdentifier() != null) { + methodBlock.line(String.format("// BEGIN:%s", proxyMethodExample.getCodeSnippetIdentifier())); + } + + clientMethodExampleWriter.writeClientMethodInvocation(methodBlock, false); + + // codesnippet end + if (proxyMethodExample.getCodeSnippetIdentifier() != null) { + methodBlock.line(String.format("// END:%s", proxyMethodExample.getCodeSnippetIdentifier())); + } + }); + if (helperFeatures.contains(ExampleHelperFeature.MapOfMethod)) { + ModelExampleWriter.writeMapOfMethod(classBlock); + } + }); + } + + /** + * Returns whether the given convenience example should be included in the generated sample code. + * @param clientMethod the client method to generate samples for + * @param convenienceMethod the convenience method + * @return whether the given convenience example should be included in the generated sample code + */ + public boolean isExampleIncluded(ClientMethod clientMethod, ConvenienceMethod convenienceMethod) { + ConvenienceSyncMethodTemplate syncMethodTemplate = Templates.getConvenienceSyncMethodTemplate(); + return syncMethodTemplate.isMethodIncluded(clientMethod) + && syncMethodTemplate.isMethodIncluded(convenienceMethod); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/ClientMethodTemplate.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/ClientMethodTemplate.java new file mode 100644 index 0000000000..4c3766cead --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/ClientMethodTemplate.java @@ -0,0 +1,1582 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.template; + +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.RequestParameterLocation; +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ArrayType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClassType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethod; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethodParameter; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethodType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.EnumType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.GenericType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.IType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.IterableType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ListType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.MethodTransformationDetail; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ParameterMapping; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ParameterSynthesizedOrigin; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.PrimitiveType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ProxyMethod; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ProxyMethodParameter; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaBlock; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaClass; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaIfBlock; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaInterface; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaJavadocComment; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaType; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaVisibility; +import com.microsoft.typespec.http.client.generator.core.util.CodeNamer; +import com.microsoft.typespec.http.client.generator.core.util.MethodNamer; +import com.microsoft.typespec.http.client.generator.core.util.MethodUtil; +import com.microsoft.typespec.http.client.generator.core.util.TemplateUtil; +import com.azure.core.annotation.ReturnType; +import com.azure.core.http.HttpHeaderName; +import com.azure.core.util.CoreUtils; +import com.azure.core.util.serializer.CollectionFormat; + +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * Writes a ClientMethod to a JavaType block. + */ +public class ClientMethodTemplate extends ClientMethodTemplateBase { + private static final ClientMethodTemplate INSTANCE = new ClientMethodTemplate(); + + protected ClientMethodTemplate() { + } + + public static ClientMethodTemplate getInstance() { + return INSTANCE; + } + + /** + * Adds validations to the client method. + * + * @param function The client method code block. + * @param expressionsToCheck Expressions to validate as non-null. + * @param validateExpressions Expressions to validate with a custom validation (key is the expression, value is the + * validation). + * @param settings AutoRest generation settings, used to determine if validations should be added. + */ + protected static void addValidations(JavaBlock function, List expressionsToCheck, + Map validateExpressions, JavaSettings settings) { + if (!settings.isClientSideValidations()) { + return; + } + + // Iteration of validateExpressions uses expressionsToCheck effectively as a set lookup, may as well turn the + // expressionsToCheck to a set. + Set expressionsToCheckSet = new LinkedHashSet<>(expressionsToCheck); + + for (String expressionToCheck : expressionsToCheckSet) { + // TODO (alzimmer): Need to discuss if this can be changed to the more appropriate NullPointerException. + String exceptionExpression = "new IllegalArgumentException(\"Parameter " + expressionToCheck + + " is required and cannot be null.\")"; + + // TODO (alzimmer): Determine if the assumption being made here are always true. + // 1. Assumes that the expression is nullable. + // 2. Assumes that the client method returns a reactive response. + // 3. Assumes that the reactive response is a Mono. + JavaIfBlock nullCheck = function.ifBlock(expressionToCheck + " == null", ifBlock -> { + if (JavaSettings.getInstance().isSyncStackEnabled()) { + if (settings.isUseClientLogger()) { + ifBlock.line("throw LOGGER.atError().log(" + exceptionExpression + ");"); + } else { + ifBlock.line("throw " + exceptionExpression + ";"); + } + } else { + ifBlock.methodReturn("Mono.error(" + exceptionExpression + ")"); + } + }); + + String potentialValidateExpression = validateExpressions.get(expressionToCheck); + if (potentialValidateExpression != null) { + nullCheck.elseBlock(elseBlock -> elseBlock.line(potentialValidateExpression + ";")); + } + } + + for (Map.Entry validateExpression : validateExpressions.entrySet()) { + if (!expressionsToCheckSet.contains(validateExpression.getKey())) { + function.ifBlock(validateExpression.getKey() + " != null", + ifBlock -> ifBlock.line(validateExpression.getValue() + ";")); + } + } + } + + /** + * Adds optional variable instantiations into the client method. + * + * @param function The client method code block. + * @param clientMethod The client method. + */ + protected static void addOptionalVariables(JavaBlock function, ClientMethod clientMethod) { + if (!clientMethod.getOnlyRequiredParameters()) { + return; + } + + for (ClientMethodParameter parameter : clientMethod.getMethodParameters()) { + // Parameter is required and will be part of the method signature. + if (parameter.isRequired()) { + continue; + } + + IType parameterClientType = parameter.getClientType(); + String defaultValue = parameterClientType.defaultValueExpression(parameter.getDefaultValue()); + function.line("final %s %s = %s;", parameterClientType, parameter.getName(), + defaultValue == null ? "null" : defaultValue); + } + } + + /** + * Adds optional variable instantiations and constant variables into the client method. + * + * @param function The client method code block. + * @param clientMethod The client method. + * @param proxyMethodAndConstantParameters Proxy method constant parameters. + * @param settings AutoRest generation settings. + */ + protected static void addOptionalAndConstantVariables(JavaBlock function, ClientMethod clientMethod, + List proxyMethodAndConstantParameters, JavaSettings settings) { + addOptionalAndConstantVariables(function, clientMethod, proxyMethodAndConstantParameters, settings, true, true, + true); + } + + /** + * Add optional and constant variables. + * + * @param function The client method code block. + * @param clientMethod The client method. + * @param proxyMethodAndConstantParameters Proxy method constant parameters. + * @param settings AutoRest generation settings. + * @param addOptional Whether optional variable instantiations are added, initialized to default or null. + * @param addConstant Whether constant variables are added, initialized to default. + * @param ignoreParameterNeedConvert When adding optional/constant variable, ignore those which need conversion from + * client type to wire type. Let "ConvertClientTypesToWireTypes" handle them. + */ + protected static void addOptionalAndConstantVariables(JavaBlock function, ClientMethod clientMethod, + List proxyMethodAndConstantParameters, JavaSettings settings, boolean addOptional, + boolean addConstant, boolean ignoreParameterNeedConvert) { + for (ProxyMethodParameter parameter : proxyMethodAndConstantParameters) { + IType parameterWireType = parameter.getWireType(); + if (parameter.isNullable()) { + parameterWireType = parameterWireType.asNullable(); + } + IType parameterClientType = parameter.getClientType(); + + // TODO (alzimmer): There are a few similar transforms like this but they all have slight nuances on output. + // This always turns ArrayType and ListType into String, the case further down this file may not. + if (parameterWireType != ClassType.BASE_64_URL + && parameter.getRequestParameterLocation() != RequestParameterLocation.BODY + //&& parameter.getRequestParameterLocation() != RequestParameterLocation.FormData + && (parameterClientType instanceof ArrayType || parameterClientType instanceof ListType)) { + parameterWireType = ClassType.STRING; + } + + // If the parameter isn't required and the client method only uses required parameters optional + // parameters are omitted and will need to instantiated in the method. + boolean optionalOmitted = clientMethod.getOnlyRequiredParameters() && !parameter.isRequired(); + + // Optional variables and constants are always null if their wire type and client type differ and applying + // conversions between the types is ignored. + boolean alwaysNull = ignoreParameterNeedConvert && parameterWireType != parameterClientType + && optionalOmitted; + + // Constants should be included if the parameter is a constant and it's either required or optional + // constants aren't generated as enums. + boolean includeConstant = parameter.isConstant() && + (!settings.isOptionalConstantAsEnum() || parameter.isRequired()); + + // Client methods only add local variable instantiations when the parameter isn't passed by the caller, + // isn't always null, is an optional parameter that was omitted or is a constant that is either required + // or AutoRest isn't generating with optional constant as enums. + if (!parameter.isFromClient() + && !alwaysNull + && ((addOptional && optionalOmitted) || (addConstant && includeConstant))) { + String defaultValue = parameterClientType.defaultValueExpression(parameter.getDefaultValue()); + function.line("final %s %s = %s;", parameterClientType, parameter.getParameterReference(), + defaultValue == null ? "null" : defaultValue); + } + } + } + + /** + * Applies parameter transformations to the client method parameters. + * + * @param function The client method code block. + * @param clientMethod The client method. + * @param settings AutoRest generation settings. + */ + protected static void applyParameterTransformations(JavaBlock function, ClientMethod clientMethod, + JavaSettings settings) { + for (MethodTransformationDetail transformation : clientMethod.getMethodTransformationDetails()) { + if (transformation.getParameterMappings().isEmpty()) { + // the case that this flattened parameter is not original parameter from any other parameters + ClientMethodParameter outParameter = transformation.getOutParameter(); + if (outParameter.isRequired() && outParameter.getClientType() instanceof ClassType) { + function.line("%1$s %2$s = new %1$s();", outParameter.getClientType(), outParameter.getName()); + } else { + function.line("%1$s %2$s = null;", outParameter.getClientType(), outParameter.getName()); + } + + // TODO (alzimmer): Should this break here? What if there are subsequent method transformation details? + break; + } + + String nullCheck = transformation.getParameterMappings().stream() + .filter(m -> !m.getInputParameter().isRequired()) + .map(m -> { + ClientMethodParameter parameter = m.getInputParameter(); + + String parameterName; + if (!parameter.isFromClient()) { + parameterName = parameter.getName(); + } else { + parameterName = m.getInputParameterProperty().getName(); + } + + return parameterName + " != null"; + }).collect(Collectors.joining(" || ")); + + boolean conditionalAssignment = !nullCheck.isEmpty() + && !transformation.getOutParameter().isRequired() + && !clientMethod.getOnlyRequiredParameters(); + + // Use a mutable internal variable, leave the original name for effectively final variable + String outParameterName = conditionalAssignment + ? transformation.getOutParameter().getName() + "Internal" + : transformation.getOutParameter().getName(); + if (conditionalAssignment) { + function.line(transformation.getOutParameter().getClientType() + " " + outParameterName + " = null;"); + function.line("if (" + nullCheck + ") {"); + function.increaseIndent(); + } + + IType transformationOutputParameterModelType = transformation.getOutParameter().getClientType(); + boolean generatedCompositeType = false; + if (transformationOutputParameterModelType instanceof ClassType) { + generatedCompositeType = ((ClassType) transformationOutputParameterModelType).getPackage().startsWith(settings.getPackage()); + } + if (generatedCompositeType && transformation.getParameterMappings().stream().anyMatch(m -> m.getOutputParameterPropertyName() != null && !m.getOutputParameterPropertyName().isEmpty())) { + String transformationOutputParameterModelCompositeTypeName = transformationOutputParameterModelType.toString(); + + function.line("%s%s = new %s();", + !conditionalAssignment ? transformation.getOutParameter().getClientType() + " " : "", + outParameterName, + transformationOutputParameterModelCompositeTypeName); + } + + for (ParameterMapping mapping : transformation.getParameterMappings()) { + String inputPath; + if (mapping.getInputParameterProperty() != null) { + inputPath = mapping.getInputParameter().getName() + "." + + CodeNamer.getModelNamer().modelPropertyGetterName(mapping.getInputParameterProperty()) + "()"; + } else { + inputPath = mapping.getInputParameter().getName(); + } + + if (clientMethod.getOnlyRequiredParameters() && !mapping.getInputParameter().isRequired()) { + inputPath = "null"; + } + + String getMapping; + if (mapping.getOutputParameterPropertyName() != null) { + getMapping = String.format(".%s(%s)", CodeNamer.getModelNamer().modelPropertySetterName(mapping.getOutputParameterPropertyName()), inputPath); + } else { + getMapping = " = " + inputPath; + } + + function.line("%s%s%s;", + !conditionalAssignment && !generatedCompositeType ? transformation.getOutParameter().getClientType() + " " : "", + outParameterName, + getMapping); + } + + if (conditionalAssignment) { + function.decreaseIndent(); + function.line("}"); + + String name = transformation.getOutParameter().getName(); + if (clientMethod.getParameters().stream().anyMatch(param -> param.getName().equals(transformation.getOutParameter().getName()))) { + name = name + "Local"; + } + function.line(transformation.getOutParameter().getClientType() + " " + name + " = " + + outParameterName + ";"); + } + } + } + + /** + * Converts the type represented to the client into the type that is sent over the wire to the service. + * + * @param function The client method code block. + * @param clientMethod The client method. + * @param autoRestMethodRetrofitParameters Rest API method parameters. + */ + protected static void convertClientTypesToWireTypes(JavaBlock function, ClientMethod clientMethod, + List autoRestMethodRetrofitParameters) { + for (ProxyMethodParameter parameter : autoRestMethodRetrofitParameters) { + IType parameterWireType = parameter.getWireType(); + + if (parameter.isNullable()) { + parameterWireType = parameterWireType.asNullable(); + } + + IType parameterClientType = parameter.getClientType(); + + // TODO (alzimmer): Reconcile the logic here with that earlier in the file. + // This check parameter explosion but earlier in the file it doesn't. + if (parameterWireType != ClassType.BASE_64_URL + && parameter.getRequestParameterLocation() != RequestParameterLocation.BODY + //&& parameter.getRequestParameterLocation() != RequestParameterLocation.FormData && + && (parameterClientType instanceof ArrayType || parameterClientType instanceof ListType)) { + parameterWireType = (parameter.getExplode()) ? new ListType(ClassType.STRING) : ClassType.STRING; + } + + // If the wire type and client type are the same there is no conversion needed. + if (parameterWireType == parameterClientType) { + continue; + } + + String parameterName = parameter.getParameterReference(); + String parameterWireName = parameter.getParameterReferenceConverted(); + + boolean addedConversion = false; + boolean alwaysNull = clientMethod.getOnlyRequiredParameters() && !parameter.isRequired(); + + RequestParameterLocation parameterLocation = parameter.getRequestParameterLocation(); + if (parameterLocation != RequestParameterLocation.BODY && + //parameterLocation != RequestParameterLocation.FormData && + (parameterClientType instanceof ArrayType || parameterClientType instanceof IterableType)) { + + if (parameterClientType == ArrayType.BYTE_ARRAY) { + String expression = "null"; + if (!alwaysNull) { + String methodCall = (parameterWireType == ClassType.STRING) + ? "Base64Util.encodeToString" : "Base64Url.encode"; + expression = methodCall + "(" + parameterName + ")"; + } + + function.line(parameterWireType + " " + parameterWireName + " = " + expression + ";"); + addedConversion = true; + } else if (parameterClientType instanceof IterableType) { + boolean alreadyNullChecked = clientMethod.getRequiredNullableParameterExpressions() + .contains(parameterName); + IType elementType = ((IterableType) parameterClientType).getElementType(); + String expression; + if (alwaysNull) { + expression = "null"; + } else if (!parameter.getExplode()) { + CollectionFormat collectionFormat = parameter.getCollectionFormat(); + String delimiter = ClassType.STRING.defaultValueExpression(collectionFormat.getDelimiter()); + if (elementType instanceof EnumType) { + // EnumTypes should provide a toString implementation that represents the wire value. + // Circumvent the use of JacksonAdapter and handle this manually. + + // If the parameter is null, the converted value is null. + // Otherwise, convert the parameter to a string, mapping each element to the toString + // value, finally joining with the collection format. + EnumType enumType = (EnumType) elementType; + // Not enums will be backed by Strings. Get the backing value before converting to string it, this + // will prevent using the enum name rather than the enum value when it isn't a String-based + // enum. Ex, a long-based enum with value 100 called HIGH will return "100" rather than + // "HIGH". + String enumToString = enumType.getElementType() == ClassType.STRING + ? "paramItemValue" + : "paramItemValue == null ? null : paramItemValue." + enumType.getToMethodName() + "()"; + if (alreadyNullChecked) { + expression = + parameterName + ".stream()\n" + + " .map(paramItemValue -> Objects.toString(" + enumToString + ", \"\"))\n" + + " .collect(Collectors.joining(" + delimiter + "))"; + } else { + expression = + "(" + parameterName + " == null) ? null : " + parameterName + ".stream()\n" + + " .map(paramItemValue -> Objects.toString(" + enumToString + ", \"\"))\n" + + " .collect(Collectors.joining(" + delimiter + "))"; + } + } else { + if (elementType == ClassType.STRING + || (elementType instanceof ClassType && ((ClassType) elementType).isBoxedType())) { + if (alreadyNullChecked) { + expression = parameterName + ".stream()\n" + + " .map(paramItemValue -> Objects.toString(paramItemValue, \"\"))\n" + + " .collect(Collectors.joining(" + delimiter + "))"; + } else { + expression = "(" + parameterName + " == null) ? null : " + parameterName + ".stream()\n" + + " .map(paramItemValue -> Objects.toString(paramItemValue, \"\"))\n" + + " .collect(Collectors.joining(" + delimiter + "))"; + } + } else { + // Always use serializeIterable as Iterable supports both Iterable and List. + + // this logic depends on rawType of proxy method parameter be List + // alternative would be check wireType of client method parameter + IType elementWireType = parameter.getRawType() instanceof IterableType + ? ((IterableType) parameter.getRawType()).getElementType() + : elementType; + + String serializeIterableInput = parameterName; + if (elementWireType != elementType) { + // convert List to List, if necessary + serializeIterableInput = String.format( + "%s.stream().map(paramItemValue -> %s).collect(Collectors.toList())", + parameterName, elementWireType.convertFromClientType("paramItemValue")); + } + + // convert List to String + expression = String.format( + "JacksonAdapter.createDefaultSerializerAdapter().serializeIterable(%s, CollectionFormat.%s)", + serializeIterableInput, collectionFormat.toString().toUpperCase(Locale.ROOT)); + } + } + } else { + if (alreadyNullChecked) { + expression = parameterName + ".stream()\n" + + " .map(item -> Objects.toString(item, \"\"))\n" + + " .collect(Collectors.toList())"; + } else { + expression = "(" + parameterName + " == null) ? new ArrayList<>()\n" + + ": " + parameterName + ".stream().map(item -> Objects.toString(item, \"\")).collect(Collectors.toList())"; + } + } + function.line("%s %s = %s;", parameterWireType, parameterWireName, expression); + addedConversion = true; + } + } + + if (parameter.getWireType().isUsedInXml() && parameterClientType instanceof ListType + && (parameterLocation == RequestParameterLocation.BODY /*|| parameterLocation == RequestParameterLocation.FormData*/)) { + function.line("%s %s = new %s(%s);", parameter.getWireType(), parameterWireName, + parameter.getWireType(), alwaysNull ? "null" : parameterName); + addedConversion = true; + } + + if (!addedConversion) { + function.line(parameter.convertFromClientType(parameterName, parameterWireName, + clientMethod.getOnlyRequiredParameters() && !parameter.isRequired(), + parameter.isConstant() || alwaysNull)); + } + } + } + + private static boolean addSpecialHeadersToRequestOptions(JavaBlock function, ClientMethod clientMethod) { + // logic only works for DPG, protocol API, on RequestOptions + + boolean requestOptionsLocal = false; + + final boolean repeatabilityRequestHeaders = MethodUtil.isMethodIncludeRepeatableRequestHeaders(clientMethod.getProxyMethod()); + + // optional parameter is in getAllParameters + boolean bodyParameterOptional = clientMethod.getProxyMethod().getAllParameters().stream() + .anyMatch(p -> p.getRequestParameterLocation() == RequestParameterLocation.BODY + && !p.isConstant() && !p.isFromClient() && !p.isRequired()); + // this logic relies on: codegen requires either source defines "content-type" header parameter, or codegen generates a "content-type" header parameter (ref ProxyMethodMapper class) + boolean singleContentType = clientMethod.getProxyMethod().getAllParameters().stream() + .noneMatch(p -> p.getRequestParameterLocation() == RequestParameterLocation.HEADER + && HttpHeaderName.CONTENT_TYPE.getCaseInsensitiveName().equalsIgnoreCase(p.getRequestParameterName()) + && p.getRawType() instanceof EnumType + && ((EnumType) p.getRawType()).getValues().size() > 1); + final boolean contentTypeRequestHeaders = bodyParameterOptional && singleContentType; + + // need a "final" variable for RequestOptions + if (repeatabilityRequestHeaders || contentTypeRequestHeaders) { + requestOptionsLocal = true; + function.line("RequestOptions requestOptionsLocal = requestOptions == null ? new RequestOptions() : requestOptions;"); + } + + // repeatability headers + if (repeatabilityRequestHeaders) { + requestOptionsSetHeaderIfAbsent(function, MethodUtil.REPEATABILITY_REQUEST_ID_EXPRESSION, MethodUtil.REPEATABILITY_REQUEST_ID_HEADER); + if (clientMethod.getProxyMethod().getSpecialHeaders().contains(MethodUtil.REPEATABILITY_FIRST_SENT_HEADER)) { + requestOptionsSetHeaderIfAbsent(function, MethodUtil.REPEATABILITY_FIRST_SENT_EXPRESSION, MethodUtil.REPEATABILITY_FIRST_SENT_HEADER); + } + } + + // content-type headers for optional body parameter + if (contentTypeRequestHeaders) { + final String contentType = clientMethod.getProxyMethod().getRequestContentType(); + function.line("requestOptionsLocal.addRequestCallback(requestLocal -> {"); + function.indent(() -> function.ifBlock("requestLocal.getBody() != null && requestLocal.getHeaders().get(HttpHeaderName.CONTENT_TYPE) == null", + ifBlock -> function.line("requestLocal.getHeaders().set(HttpHeaderName.CONTENT_TYPE, \"" + contentType + "\");"))); + function.line("});"); + } + + return requestOptionsLocal; + } + + private static void requestOptionsSetHeaderIfAbsent(JavaBlock function, String expression, String headerName) { + function.line("requestOptionsLocal.addRequestCallback(requestLocal -> {"); + function.indent(() -> function.ifBlock("requestLocal.getHeaders().get(HttpHeaderName.fromString(\"" + headerName + "\")) == null", + ifBlock -> function.line("requestLocal.getHeaders().set(HttpHeaderName.fromString(\"" + headerName + "\"), " + expression + ");"))); + function.line("});"); + } + + protected static void writeMethod(JavaType typeBlock, JavaVisibility visibility, String methodSignature, Consumer method) { + if (visibility == JavaVisibility.Public) { + typeBlock.publicMethod(methodSignature, method); + } else if (typeBlock instanceof JavaClass) { + JavaClass classBlock = (JavaClass) typeBlock; + classBlock.method(visibility, null, methodSignature, method); + } + } + + public final void write(ClientMethod clientMethod, JavaType typeBlock) { + final boolean writingInterface = typeBlock instanceof JavaInterface; + if (clientMethod.getMethodVisibility() != JavaVisibility.Public && writingInterface) { + return; + } + + JavaSettings settings = JavaSettings.getInstance(); + + ProxyMethod restAPIMethod = clientMethod.getProxyMethod(); + //IType restAPIMethodReturnBodyClientType = restAPIMethod.getReturnType().getClientType(); + + //MethodPageDetails pageDetails = clientMethod.getMethodPageDetails(); + + generateJavadoc(clientMethod, typeBlock, restAPIMethod, writingInterface); + + switch (clientMethod.getType()) { + case PagingSync: + if (settings.isSyncStackEnabled()) { + if (settings.isDataPlaneClient()) { + generateProtocolPagingPlainSync(clientMethod, typeBlock, settings); + } else { + generatePagingPlainSync(clientMethod, typeBlock, settings); + } + } else { + if (settings.isDataPlaneClient()) { + generateProtocolPagingSync(clientMethod, typeBlock, restAPIMethod, settings); + } else { + generatePagingSync(clientMethod, typeBlock, restAPIMethod, settings); + } + } + break; + case PagingAsync: + if (settings.isDataPlaneClient()) { + generateProtocolPagingAsync(clientMethod, typeBlock, restAPIMethod, settings); + } else { + generatePagingAsync(clientMethod, typeBlock, restAPIMethod, settings); + } + break; + case PagingSyncSinglePage: + if (settings.isDataPlaneClient()) { + generateProtocolPagingSinglePage(clientMethod, typeBlock, restAPIMethod.toSync(), settings); + } else { + generatePagedSinglePage(clientMethod, typeBlock, restAPIMethod.toSync(), settings); + } + break; + case PagingAsyncSinglePage: + if (settings.isDataPlaneClient()) { + generateProtocolPagingAsyncSinglePage(clientMethod, typeBlock, restAPIMethod, settings); + } else { + generatePagedAsyncSinglePage(clientMethod, typeBlock, restAPIMethod, settings); + } + break; + + case LongRunningAsync: + generateLongRunningAsync(clientMethod, typeBlock, restAPIMethod, settings); + break; + + case LongRunningSync: + generateLongRunningSync(clientMethod, typeBlock, restAPIMethod, settings); + break; + + case LongRunningBeginAsync: + if (settings.isDataPlaneClient()) { + generateProtocolLongRunningBeginAsync(clientMethod, typeBlock); + } else { + generateLongRunningBeginAsync(clientMethod, typeBlock, restAPIMethod, settings); + } + break; + + case LongRunningBeginSync: + if (settings.isSyncStackEnabled()) { + if (settings.isDataPlaneClient()) { + generateProtocolLongRunningBeginSync(clientMethod, typeBlock); + } else { + generateLongRunningBeginSync(clientMethod, typeBlock, restAPIMethod, settings); + } + } else { + generateLongRunningBeginSyncOverAsync(clientMethod, typeBlock); + } + break; + + case Resumable: + generateResumable(clientMethod, typeBlock, restAPIMethod, settings); + break; + + case SimpleSync: + if (settings.isSyncStackEnabled()) { + generateSimpleSyncMethod(clientMethod, typeBlock); + } else { + generateSimplePlainSyncMethod(clientMethod, typeBlock); + } + break; + case SimpleSyncRestResponse: + if (settings.isSyncStackEnabled()) { + generatePlainSyncMethod(clientMethod, typeBlock, restAPIMethod, settings); + } else { + generateSyncMethod(clientMethod, typeBlock, restAPIMethod, settings); + } + break; + + case SimpleAsyncRestResponse: + generateSimpleAsyncRestResponse(clientMethod, typeBlock, restAPIMethod, settings); + break; + + case SimpleAsync: + generateSimpleAsync(clientMethod, typeBlock, restAPIMethod, settings); + break; + + case SendRequestAsync: + generateSendRequestAsync(clientMethod, typeBlock); + break; + case SendRequestSync: + generateSendRequestSync(clientMethod, typeBlock); + break; + } + } + + protected void generateProtocolPagingSync(ClientMethod clientMethod, JavaType typeBlock, ProxyMethod restAPIMethod, JavaSettings settings) { + generatePagingSync(clientMethod, typeBlock, restAPIMethod, settings); + } + + protected void generateProtocolPagingPlainSync(ClientMethod clientMethod, JavaType typeBlock, JavaSettings settings) { + addServiceMethodAnnotation(typeBlock, ReturnType.COLLECTION); + if (clientMethod.getMethodPageDetails().nonNullNextLink()) { + writeMethod(typeBlock, clientMethod.getMethodVisibility(), clientMethod.getDeclaration(), function -> { + addOptionalVariables(function, clientMethod); + + function.line("RequestOptions requestOptionsForNextPage = new RequestOptions();"); + function.line("requestOptionsForNextPage.setContext(requestOptions != null && requestOptions.getContext() != null ? requestOptions.getContext() : Context.NONE);"); + + function.line("return new PagedIterable<>("); + function.indent(() -> { + function.line("%s,", this.getPagingSinglePageExpression( + clientMethod, + clientMethod.getProxyMethod().getPagingSinglePageMethodName(), + clientMethod.getArgumentList(), + settings)); + function.line("%s);", this.getPagingNextPageExpression( + clientMethod, + clientMethod.getMethodPageDetails().getNextMethod().getProxyMethod().getPagingSinglePageMethodName(), + clientMethod.getMethodPageDetails().getNextMethod().getArgumentList(), + settings)); + }); + }); + } else { + writeMethod(typeBlock, clientMethod.getMethodVisibility(), clientMethod.getDeclaration(), function -> { + addOptionalVariables(function, clientMethod); + function.line("return new PagedIterable<>("); + function.indent(() -> function.line(this.getPagingSinglePageExpression(clientMethod, + clientMethod.getProxyMethod().getPagingSinglePageMethodName(), clientMethod.getArgumentList(), + settings) + ");")); + }); + } + } + + protected void generateProtocolPagingAsync(ClientMethod clientMethod, JavaType typeBlock, ProxyMethod restAPIMethod, JavaSettings settings) { + generatePagingAsync(clientMethod, typeBlock, restAPIMethod, settings); + } + + protected void generateProtocolPagingAsyncSinglePage(ClientMethod clientMethod, JavaType typeBlock, ProxyMethod restAPIMethod, JavaSettings settings) { + generatePagedAsyncSinglePage(clientMethod, typeBlock, restAPIMethod, settings); + } + + protected void generateProtocolPagingSinglePage(ClientMethod clientMethod, JavaType typeBlock, ProxyMethod restAPIMethod, JavaSettings settings) { + generatePagedSinglePage(clientMethod, typeBlock, restAPIMethod, settings); + } + + private void generatePagedSinglePage(ClientMethod clientMethod, JavaType typeBlock, ProxyMethod restAPIMethod, JavaSettings settings) { + addServiceMethodAnnotation(typeBlock, ReturnType.SINGLE); + + writeMethod(typeBlock, clientMethod.getMethodVisibility(), clientMethod.getDeclaration(), function -> { + if (!settings.isSyncStackEnabled()) { + function.methodReturn(String.format("%s(%s).block()", clientMethod.getProxyMethod().getPagingAsyncSinglePageMethodName(), + clientMethod.getArgumentList())); + return; + } + + addValidations(function, clientMethod.getRequiredNullableParameterExpressions(), clientMethod.getValidateExpressions(), settings); + addOptionalAndConstantVariables(function, clientMethod, restAPIMethod.getParameters(), settings); + applyParameterTransformations(function, clientMethod, settings); + convertClientTypesToWireTypes(function, clientMethod, restAPIMethod.getParameters()); + + boolean requestOptionsLocal = false; + if (settings.isDataPlaneClient()) { + requestOptionsLocal = addSpecialHeadersToRequestOptions(function, clientMethod); + } + + String serviceMethodCall = checkAndReplaceParamNameCollision(clientMethod, restAPIMethod, requestOptionsLocal, settings); + function.line(String.format("%s res = %s;", restAPIMethod.getReturnType(), serviceMethodCall)); + function.line("return new PagedResponseBase<>("); + function.line("res.getRequest(),"); + function.line("res.getStatusCode(),"); + function.line("res.getHeaders(),"); + if (settings.isDataPlaneClient()) { + function.line("getValues(res.getValue(), \"%s\"),", clientMethod.getMethodPageDetails().getSerializedItemName()); + } else { + function.line("res.getValue().%s(),", CodeNamer.getModelNamer().modelPropertyGetterName(clientMethod.getMethodPageDetails().getItemName())); + } + if (clientMethod.getMethodPageDetails().nonNullNextLink()) { + if (settings.isDataPlaneClient()) { + function.line("getNextLink(res.getValue(), \"%s\"),", clientMethod.getMethodPageDetails().getSerializedNextLinkName()); + } else { + function.line(nextLinkLine(clientMethod)); + } + } else { + function.line("null,"); + } + + if (responseTypeHasDeserializedHeaders(clientMethod.getProxyMethod().getReturnType())) { + function.line("res.getDeserializedHeaders());"); + } else { + function.line("null);"); + } + }); + } + + + protected void generatePagingSync(ClientMethod clientMethod, JavaType typeBlock, ProxyMethod restAPIMethod, JavaSettings settings) { + addServiceMethodAnnotation(typeBlock, ReturnType.COLLECTION); + writeMethod(typeBlock, clientMethod.getMethodVisibility(), clientMethod.getDeclaration(), function -> { + addOptionalVariables(function, clientMethod); + function.methodReturn(String.format("new PagedIterable<>(%s(%s))", clientMethod.getProxyMethod().getSimpleAsyncMethodName(), clientMethod.getArgumentList())); + }); + } + + protected void generatePagingPlainSync(ClientMethod clientMethod, JavaType typeBlock, JavaSettings settings) { + addServiceMethodAnnotation(typeBlock, ReturnType.COLLECTION); + if (clientMethod.getMethodPageDetails().nonNullNextLink()) { + writeMethod(typeBlock, clientMethod.getMethodVisibility(), clientMethod.getDeclaration(), function -> { + addOptionalVariables(function, clientMethod); + if (settings.isDataPlaneClient()) { + function.line("RequestOptions requestOptionsForNextPage = new RequestOptions();"); + function.line("requestOptionsForNextPage.setContext(requestOptions != null && requestOptions.getContext() != null ? requestOptions.getContext() : Context.NONE);"); + } + function.line("return new PagedIterable<>("); + + String nextMethodArgs = clientMethod.getMethodPageDetails().getNextMethod().getArgumentList().replace("requestOptions", "requestOptionsForNextPage"); + String firstPageArgs = clientMethod.getArgumentList(); + if (clientMethod.getParameters() + .stream() + .noneMatch(p -> p.getClientType() == ClassType.CONTEXT)) { + nextMethodArgs = nextMethodArgs.replace("context", "Context.NONE"); + if (!CoreUtils.isNullOrEmpty(firstPageArgs)) { + firstPageArgs = firstPageArgs + ", Context.NONE"; + } else { + // If there are no first page arguments don't include a leading comma. + firstPageArgs = "Context.NONE"; + } + } + String effectiveNextMethodArgs = nextMethodArgs; + String effectiveFirstPageArgs = firstPageArgs; + function.indent(() -> { + function.line("%s,", this.getPagingSinglePageExpression( + clientMethod, + clientMethod.getProxyMethod().getPagingSinglePageMethodName(), + effectiveFirstPageArgs, + settings)); + function.line("%s);", this.getPagingNextPageExpression( + clientMethod, + clientMethod.getMethodPageDetails().getNextMethod().getProxyMethod().getPagingSinglePageMethodName(), + effectiveNextMethodArgs, + settings)); + }); + }); + } else { + writeMethod(typeBlock, clientMethod.getMethodVisibility(), clientMethod.getDeclaration(), function -> { + + String firstPageArgs = clientMethod.getArgumentList(); + if (clientMethod.getParameters() + .stream() + .noneMatch(p -> p.getClientType() == ClassType.CONTEXT)) { + if (!CoreUtils.isNullOrEmpty(firstPageArgs)) { + firstPageArgs = firstPageArgs + ", Context.NONE"; + } else { + // If there are no first page arguments don't include a leading comma. + firstPageArgs = "Context.NONE"; + } + } + String effectiveFirstPageArgs = firstPageArgs; + addOptionalVariables(function, clientMethod); + function.line("return new PagedIterable<>("); + function.indent(() -> function.line(this.getPagingSinglePageExpression(clientMethod, + clientMethod.getProxyMethod().getPagingSinglePageMethodName(), effectiveFirstPageArgs, settings) + + ");")); + }); + } + } + + protected void generatePagingAsync(ClientMethod clientMethod, JavaType typeBlock, ProxyMethod restAPIMethod, JavaSettings settings) { + addServiceMethodAnnotation(typeBlock, ReturnType.COLLECTION); + if (clientMethod.getMethodPageDetails().nonNullNextLink()) { + writeMethod(typeBlock, clientMethod.getMethodVisibility(), clientMethod.getDeclaration(), function -> { + addOptionalVariables(function, clientMethod); + if (settings.isDataPlaneClient()) { + function.line("RequestOptions requestOptionsForNextPage = new RequestOptions();"); + function.line("requestOptionsForNextPage.setContext(requestOptions != null && requestOptions.getContext() != null ? requestOptions.getContext() : Context.NONE);"); + } + function.line("return new PagedFlux<>("); + function.indent(() -> { + function.line(this.getPagingSinglePageExpression(clientMethod, + clientMethod.getProxyMethod().getPagingAsyncSinglePageMethodName(), + clientMethod.getArgumentList(), settings) + ","); + function.line(this.getPagingNextPageExpression(clientMethod, + clientMethod.getMethodPageDetails().getNextMethod().getProxyMethod().getPagingAsyncSinglePageMethodName(), + clientMethod.getMethodPageDetails().getNextMethod().getArgumentList(), settings) + ");"); + }); + }); + } else { + writeMethod(typeBlock, clientMethod.getMethodVisibility(), clientMethod.getDeclaration(), function -> { + addOptionalVariables(function, clientMethod); + function.line("return new PagedFlux<>("); + function.indent(() -> function.line(this.getPagingSinglePageExpression(clientMethod, + clientMethod.getProxyMethod().getPagingAsyncSinglePageMethodName(), clientMethod.getArgumentList(), + settings) + ");")); + }); + } + } + + private static void addServiceMethodAnnotation(JavaType typeBlock, ReturnType returnType) { + if (JavaSettings.getInstance().isBranded()) { + typeBlock.annotation("ServiceMethod(returns = ReturnType." + returnType.name() + ")"); + } + } + + protected void generateResumable(ClientMethod clientMethod, JavaType typeBlock, ProxyMethod restAPIMethod, JavaSettings settings) { + addServiceMethodAnnotation(typeBlock, ReturnType.SINGLE); + typeBlock.publicMethod(clientMethod.getDeclaration(), function -> { + ProxyMethodParameter parameter = restAPIMethod.getParameters().get(0); + addValidations(function, clientMethod.getRequiredNullableParameterExpressions(), clientMethod.getValidateExpressions(), settings); + function.methodReturn("service." + restAPIMethod.getName() + "(" + parameter.getName() + ")"); + }); + } + + protected void generateSimpleAsync(ClientMethod clientMethod, JavaType typeBlock, ProxyMethod restAPIMethod, JavaSettings settings) { + addServiceMethodAnnotation(typeBlock, ReturnType.SINGLE); + writeMethod(typeBlock, clientMethod.getMethodVisibility(), clientMethod.getDeclaration(), (function -> { + addOptionalVariables(function, clientMethod); + function.line("return %s(%s)", clientMethod.getProxyMethod().getSimpleAsyncRestResponseMethodName(), clientMethod.getArgumentList()); + function.indent(() -> { + if (GenericType.Flux(ClassType.BYTE_BUFFER).equals(clientMethod.getReturnValue().getType())) { + // Previously this used StreamResponse::getValue, but it isn't guaranteed that the return is + // StreamResponse, instead use Response::getValue as StreamResponse is just a fancier + // Response>. + function.text(".flatMapMany(fluxByteBufferResponse -> fluxByteBufferResponse.getValue());"); + } else if (!GenericType.Mono(ClassType.VOID).equals(clientMethod.getReturnValue().getType()) && + !GenericType.Flux(ClassType.VOID).equals(clientMethod.getReturnValue().getType())) { + function.text(".flatMap(res -> Mono.justOrEmpty(res.getValue()));"); + } else { + function.text(".flatMap(ignored -> Mono.empty());"); + } + }); + })); + } + + private void generateSimpleSyncMethod(ClientMethod clientMethod, JavaType typeBlock) { + addServiceMethodAnnotation(typeBlock, ReturnType.SINGLE); + writeMethod(typeBlock, clientMethod.getMethodVisibility(), clientMethod.getDeclaration(), (function -> { + addOptionalVariables(function, clientMethod); + + String argumentList = clientMethod.getArgumentList(); + if (CoreUtils.isNullOrEmpty(argumentList)) { + // If there are no arguments the argument is Context.NONE + argumentList = "Context.NONE"; + } else if (clientMethod.getParameters().stream().noneMatch(p -> p.getClientType() == ClassType.CONTEXT)) { + // If the arguments don't contain Context append Context.NONE + argumentList += ", Context.NONE"; + } + + if (ClassType.STREAM_RESPONSE.equals(clientMethod.getReturnValue().getType())) { + function.text(".flatMapMany(StreamResponse::getValue);"); + } + if (clientMethod.getReturnValue().getType().equals(PrimitiveType.VOID)) { + function.line("%s(%s);", + clientMethod.getProxyMethod().getSimpleRestResponseMethodName(), + argumentList); + } else { + function.line("return %s(%s).getValue();", + clientMethod.getProxyMethod().getSimpleRestResponseMethodName(), + argumentList); + } + })); + } + + private void generateSimplePlainSyncMethod(ClientMethod clientMethod, JavaType typeBlock) { + addServiceMethodAnnotation(typeBlock, ReturnType.SINGLE); + writeMethod(typeBlock, clientMethod.getMethodVisibility(), clientMethod.getDeclaration(), (function -> { + addOptionalVariables(function, clientMethod); + + String argumentList = clientMethod.getArgumentList(); + if (CoreUtils.isNullOrEmpty(argumentList)) { + // If there are no arguments the argument is Context.NONE + argumentList = "Context.NONE"; + } else if (clientMethod.getParameters().stream().noneMatch(p -> p.getClientType() == ClassType.CONTEXT)) { + // If the arguments don't contain Context append Context.NONE + argumentList += ", Context.NONE"; + } + + if (clientMethod.getReturnValue().getType().equals(PrimitiveType.VOID)) { + function.line("%s(%s);", + clientMethod.getProxyMethod().getSimpleRestResponseMethodName(), + argumentList); + } else { + function.line("return %s(%s).getValue();", + clientMethod.getProxyMethod().getSimpleRestResponseMethodName(), + argumentList); + } + })); + } + + protected void generateSyncMethod(ClientMethod clientMethod, JavaType typeBlock, + ProxyMethod restAPIMethod, JavaSettings settings) { + String asyncMethodName = MethodNamer.getSimpleAsyncMethodName(clientMethod.getName()); + if (clientMethod.getType() == ClientMethodType.SimpleSyncRestResponse) { + asyncMethodName = clientMethod.getProxyMethod().getSimpleAsyncRestResponseMethodName(); + } + String effectiveAsyncMethodName = asyncMethodName; + addServiceMethodAnnotation(typeBlock, ReturnType.SINGLE); + writeMethod(typeBlock, clientMethod.getMethodVisibility(), clientMethod.getDeclaration(), function -> { + addOptionalVariables(function, clientMethod); + if (clientMethod.getReturnValue().getType() == ClassType.INPUT_STREAM) { + function.line("Iterator iterator = %s(%s).map(ByteBufferBackedInputStream::new).toStream().iterator();", + effectiveAsyncMethodName, clientMethod.getArgumentList()); + function.anonymousClass("Enumeration", "enumeration", javaBlock -> { + javaBlock.annotation("Override"); + javaBlock.publicMethod("boolean hasMoreElements()", methodBlock -> methodBlock.methodReturn("iterator.hasNext()")); + javaBlock.annotation("Override"); + javaBlock.publicMethod("InputStream nextElement()", methodBlock -> methodBlock.methodReturn("iterator.next()")); + }); + function.methodReturn("new SequenceInputStream(enumeration)"); + } else if (clientMethod.getReturnValue().getType() != PrimitiveType.VOID) { + IType returnType = clientMethod.getReturnValue().getType(); + if (returnType instanceof PrimitiveType) { + function.line("%s value = %s(%s).block();", returnType.asNullable(), + effectiveAsyncMethodName, clientMethod.getArgumentList()); + function.ifBlock("value != null", ifAction -> ifAction.methodReturn("value")).elseBlock(elseAction -> { + if (settings.isUseClientLogger()) { + elseAction.line("throw LOGGER.atError().log(new NullPointerException());"); + } else { + elseAction.line("throw new NullPointerException();"); + } + }); + } else if (returnType instanceof GenericType && !settings.isDataPlaneClient()) { + GenericType genericType = (GenericType) returnType; + if ("Response".equals(genericType.getName()) && genericType.getTypeArguments()[0].equals(ClassType.INPUT_STREAM)) { + function.line("return %s(%s).map(response -> {", effectiveAsyncMethodName, clientMethod.getArgumentList()); + function.indent(() -> { + function.line("Iterator iterator = response.getValue().map(ByteBufferBackedInputStream::new).toStream().iterator();"); + function.anonymousClass("Enumeration", "enumeration", javaBlock -> { + javaBlock.annotation("Override"); + javaBlock.publicMethod("boolean hasMoreElements()", methodBlock -> methodBlock.methodReturn("iterator.hasNext()")); + javaBlock.annotation("Override"); + javaBlock.publicMethod("InputStream nextElement()", methodBlock -> methodBlock.methodReturn("iterator.next()")); + }); + + function.methodReturn("new SimpleResponse(response.getRequest(), response.getStatusCode(), response.getHeaders(), new SequenceInputStream(enumeration))"); + }); + + function.line("}).block();"); +// } else if ("Response".equals(genericType.getName()) && genericType.getTypeArguments()[0].equals(ClassType.BinaryData)) { +// function.line("return %s(%s).flatMap(response -> new BinaryData(response.getValue())", +// effectiveAsyncMethodName, clientMethod.getArgumentList()); +// function.line(".map(bd -> new SimpleResponse<>(response.getRequest(), response.getStatusCode(), response.getHeaders(), bd)))"); +// function.line(".block();"); + } else { + function.methodReturn(String.format("%s(%s).block()", effectiveAsyncMethodName, clientMethod.getArgumentList())); + } + } else { + function.methodReturn(String.format("%s(%s).block()", effectiveAsyncMethodName, clientMethod.getArgumentList())); + } + } else { + function.line("%s(%s).block();", effectiveAsyncMethodName, clientMethod.getArgumentList()); + } + }); + } + + protected void generatePlainSyncMethod(ClientMethod clientMethod, JavaType typeBlock, + ProxyMethod restAPIMethod, JavaSettings settings) { + String effectiveProxyMethodName = clientMethod.getProxyMethod().getName(); + addServiceMethodAnnotation(typeBlock, ReturnType.SINGLE); + writeMethod(typeBlock, clientMethod.getMethodVisibility(), clientMethod.getDeclaration(), function -> { + + addValidations(function, clientMethod.getRequiredNullableParameterExpressions(), clientMethod.getValidateExpressions(), settings); + addOptionalAndConstantVariables(function, clientMethod, restAPIMethod.getParameters(), settings); + applyParameterTransformations(function, clientMethod, settings); + convertClientTypesToWireTypes(function, clientMethod, restAPIMethod.getParameters()); + + boolean requestOptionsLocal = false; + if (settings.isDataPlaneClient()) { + requestOptionsLocal = addSpecialHeadersToRequestOptions(function, clientMethod); + } + + String serviceMethodCall = checkAndReplaceParamNameCollision(clientMethod, restAPIMethod.toSync(), requestOptionsLocal, + settings); + if (clientMethod.getReturnValue().getType() == ClassType.INPUT_STREAM) { + function.line("Iterator iterator = %s(%s).map(ByteBufferBackedInputStream::new).toStream().iterator();", + effectiveProxyMethodName, clientMethod.getArgumentList()); + function.anonymousClass("Enumeration", "enumeration", javaBlock -> { + javaBlock.annotation("Override"); + javaBlock.publicMethod("boolean hasMoreElements()", methodBlock -> methodBlock.methodReturn("iterator.hasNext()")); + javaBlock.annotation("Override"); + javaBlock.publicMethod("InputStream nextElement()", methodBlock -> methodBlock.methodReturn("iterator.next()")); + }); + function.methodReturn("new SequenceInputStream(enumeration)"); + } else if (clientMethod.getReturnValue().getType() != PrimitiveType.VOID) { + IType returnType = clientMethod.getReturnValue().getType(); + if (returnType instanceof PrimitiveType) { + function.line("%s value = %s(%s);", returnType.asNullable(), + effectiveProxyMethodName, clientMethod.getArgumentList()); + function.ifBlock("value != null", ifAction -> ifAction.methodReturn("value")).elseBlock(elseAction -> { + if (settings.isUseClientLogger()) { + elseAction.line("throw LOGGER.atError().log(new NullPointerException());"); + } else { + elseAction.line("throw new NullPointerException();"); + } + }); + } else { + function.methodReturn(serviceMethodCall); + } + } else { + function.line("%s(%s);", effectiveProxyMethodName, clientMethod.getArgumentList()); + } + }); + } + + /** + * Generate javadoc for client method. + * + * @param clientMethod client method + * @param typeBlock code block + * @param restAPIMethod proxy method + * @param useFullClassName whether to use fully-qualified class name in javadoc + */ + public static void generateJavadoc(ClientMethod clientMethod, JavaType typeBlock, ProxyMethod restAPIMethod, boolean useFullClassName) { + // interface need a fully-qualified exception class name, since exception is usually only included in ProxyMethod + typeBlock.javadocComment(comment -> { + if (JavaSettings.getInstance().isDataPlaneClient()) { + generateProtocolMethodJavadoc(clientMethod, comment); + } else { + generateJavadoc(clientMethod, comment, restAPIMethod, useFullClassName); + } + }); + } + + /** + * Generate javadoc for client method. + * + * @param clientMethod client method + * @param commentBlock comment block + * @param restAPIMethod proxy method + * @param useFullClassName whether to use fully-qualified class name in javadoc + */ + public static void generateJavadoc(ClientMethod clientMethod, JavaJavadocComment commentBlock, ProxyMethod restAPIMethod, boolean useFullClassName) { + commentBlock.description(clientMethod.getDescription()); + List methodParameters = clientMethod.getMethodInputParameters(); + for (ClientMethodParameter parameter : methodParameters) { + commentBlock.param(parameter.getName(), parameterDescriptionOrDefault(parameter)); + } + if (restAPIMethod != null && clientMethod.getParametersDeclaration() != null && !clientMethod.getParametersDeclaration().isEmpty()) { + commentBlock.methodThrows("IllegalArgumentException", "thrown if parameters fail the validation"); + } + generateJavadocExceptions(clientMethod, commentBlock, useFullClassName); + commentBlock.methodThrows("RuntimeException", "all other wrapped checked exceptions if the request fails to be sent"); + commentBlock.methodReturns(clientMethod.getReturnValue().getDescription()); + } + + protected static String parameterDescriptionOrDefault(ClientMethodParameter parameter) { + String paramJavadoc = parameter.getDescription(); + if (CoreUtils.isNullOrEmpty(paramJavadoc)) { + paramJavadoc = "The " + parameter.getName() + " parameter"; + } + return paramJavadoc; + } + + protected void generatePagedAsyncSinglePage(ClientMethod clientMethod, JavaType typeBlock, ProxyMethod restAPIMethod, JavaSettings settings) { + addServiceMethodAnnotation(typeBlock, ReturnType.SINGLE); + + writeMethod(typeBlock, clientMethod.getMethodVisibility(), clientMethod.getDeclaration(), function -> { + if (clientMethod.hasWithContextOverload()) { + String arguments = clientMethod.getArgumentList(); + arguments = CoreUtils.isNullOrEmpty(arguments) ? "context" : arguments + ", context"; + + // If this PagedResponse method doesn't have a Context parameter, call into the overload that does. + // Doing this prevents duplicating validation and setup logic, which in some cases can reduce out + // hundreds of lines of code. + String methodCall = clientMethod.getProxyMethod().getPagingAsyncSinglePageMethodName() + "(" + arguments + ")"; + function.methodReturn("FluxUtil.withContext(context -> " + methodCall +")"); + return; + } + + addValidations(function, clientMethod.getRequiredNullableParameterExpressions(), clientMethod.getValidateExpressions(), settings); + addOptionalAndConstantVariables(function, clientMethod, restAPIMethod.getParameters(), settings); + applyParameterTransformations(function, clientMethod, settings); + convertClientTypesToWireTypes(function, clientMethod, restAPIMethod.getParameters()); + + boolean requestOptionsLocal = false; + if (settings.isDataPlaneClient()) { + requestOptionsLocal = addSpecialHeadersToRequestOptions(function, clientMethod); + } + + String serviceMethodCall = checkAndReplaceParamNameCollision(clientMethod, restAPIMethod, requestOptionsLocal, settings); + if (contextInParameters(clientMethod)) { + function.line("return " + serviceMethodCall); + } else { + function.line("return FluxUtil.withContext(context -> " + serviceMethodCall + ")"); + } + function.indent(() -> { + function.line(".map(res -> new PagedResponseBase<>("); + function.indent(() -> { + function.line("res.getRequest(),"); + function.line("res.getStatusCode(),"); + function.line("res.getHeaders(),"); + if (settings.isDataPlaneClient()) { + function.line("getValues(res.getValue(), \"%s\"),", clientMethod.getMethodPageDetails().getSerializedItemName()); + } else { + function.line("res.getValue().%s(),", CodeNamer.getModelNamer().modelPropertyGetterName(clientMethod.getMethodPageDetails().getItemName())); + } + if (clientMethod.getMethodPageDetails().nonNullNextLink()) { + if (settings.isDataPlaneClient()) { + function.line("getNextLink(res.getValue(), \"%s\"),", clientMethod.getMethodPageDetails().getSerializedNextLinkName()); + } else { + function.line(nextLinkLine(clientMethod)); + } + } else { + function.line("null,"); + } + + if (responseTypeHasDeserializedHeaders(clientMethod.getProxyMethod().getReturnType())) { + function.line("res.getDeserializedHeaders()));"); + } else { + function.line("null));"); + } + }); + }); + }); + } + + protected static String nextLinkLine(ClientMethod clientMethod) { + return nextLinkLine(clientMethod, "getValue()"); + } + + protected static String nextLinkLine(ClientMethod clientMethod, String valueExpression) { + return String.format("res.%3$s.%1$s()%2$s,", + CodeNamer.getModelNamer().modelPropertyGetterName(clientMethod.getMethodPageDetails().getNextLinkName()), + // nextLink could be type URL + (clientMethod.getMethodPageDetails().getNextLinkType() == ClassType.URL ? ".toString()" : ""), + valueExpression); + } + + private static boolean responseTypeHasDeserializedHeaders(IType type) { + if (type instanceof GenericType && "Mono".equals(((GenericType) type).getName())) { + type = ((GenericType) type).getTypeArguments()[0]; + } + + // TODO (alzimmer): ClassTypes should maintain reference to any super class or interface they extend/implement. + // This code is based on the previous implementation that assume if the T type for Mono is a class that + // it has deserialized headers. This won't always be the case, but ClassType also isn't able to maintain + // whether the class is an extension of ResponseBase. + if (type instanceof ClassType) { + return true; + } else + return type instanceof GenericType && "ResponseBase".equals(((GenericType) type).getName()); + } + + private static String checkAndReplaceParamNameCollision(ClientMethod clientMethod, ProxyMethod restAPIMethod, + boolean useLocalRequestOptions, JavaSettings settings) { + // Asynchronous methods will use 'FluxUtils.withContext' to infer 'Context' from the Reactor's context. + // Only replace 'context' with 'Context.NONE' for synchronous methods that don't have a 'Context' parameter. + boolean isSync = clientMethod.getProxyMethod().isSync(); + StringBuilder builder = new StringBuilder("service.").append(restAPIMethod.getName()).append('('); + Map nameToParameter = clientMethod.getParameters().stream() + .collect(Collectors.toMap(ClientMethodParameter::getName, Function.identity())); + Set parametersWithTransformations = clientMethod.getMethodTransformationDetails().stream() + .map(transform -> transform.getOutParameter().getName()) + .collect(Collectors.toSet()); + + boolean firstParameter = true; + for (String proxyMethodArgument : clientMethod.getProxyMethodArguments(settings)) { + String parameterName; + if (useLocalRequestOptions && "requestOptions".equals(proxyMethodArgument)) { + // Simple static mapping for RequestOptions when 'useLocalRequestOptions' is true. + parameterName = "requestOptionsLocal"; + } else { + ClientMethodParameter parameter = nameToParameter.get(proxyMethodArgument); + if (parameter != null && parametersWithTransformations.contains(proxyMethodArgument)) { + // If this ClientMethod contains the ProxyMethod parameter and it has a transformation use the + // '*Local' transformed version in the service call. + parameterName = proxyMethodArgument + "Local"; + } else { + if (!isSync) { + // For asynchronous methods always use the argument name. + parameterName = proxyMethodArgument; + } else { + // For synchronous methods check if this parameter is the 'Context' parameter and map to + // 'Context.NONE' as synchronous methods have no way to infer 'Context'. Without doing this + // mapping generated code will reference a non-existent value which won't compile. + // TODO (alzimmer): If needed in the future use a more complex validation than String matching. + // It could be possible for the interface method to have another parameter called 'context' + // which isn't 'Context'. This can be done by looking for the 'ProxyMethodParameter' with the + // matching name and checking if it's the 'Context' parameter. + parameterName = (parameter == null && "context".equals(proxyMethodArgument)) + ? "Context.NONE" + : proxyMethodArgument; + } + } + } + + if (firstParameter) { + builder.append(parameterName); + firstParameter = false; + } else { + builder.append(", ").append(parameterName); + } + } + + return builder.append(')').toString(); + } + + protected void generateSimpleAsyncRestResponse(ClientMethod clientMethod, JavaType typeBlock, + ProxyMethod restAPIMethod, JavaSettings settings) { + addServiceMethodAnnotation(typeBlock, ReturnType.SINGLE); + writeMethod(typeBlock, clientMethod.getMethodVisibility(), clientMethod.getDeclaration(), function -> { + if (clientMethod.hasWithContextOverload()) { + String arguments = clientMethod.getArgumentList(); + arguments = CoreUtils.isNullOrEmpty(arguments) ? "context" : arguments + ", context"; + + // If this RestResponse method doesn't have a Context parameter, call into the overload that does. + // Doing this prevents duplicating validation and setup logic, which in some cases can reduce out + // hundreds of lines of code. + String methodCall = clientMethod.getProxyMethod().getSimpleAsyncRestResponseMethodName() + "(" + arguments + ")"; + function.methodReturn("FluxUtil.withContext(context -> " + methodCall +")"); + return; + } + + addValidations(function, clientMethod.getRequiredNullableParameterExpressions(), clientMethod.getValidateExpressions(), settings); + addOptionalAndConstantVariables(function, clientMethod, restAPIMethod.getParameters(), settings); + applyParameterTransformations(function, clientMethod, settings); + convertClientTypesToWireTypes(function, clientMethod, restAPIMethod.getParameters()); + + boolean requestOptionsLocal = false; + if (settings.isDataPlaneClient()) { + requestOptionsLocal = addSpecialHeadersToRequestOptions(function, clientMethod); + } + + String serviceMethodCall = checkAndReplaceParamNameCollision(clientMethod, restAPIMethod, requestOptionsLocal, settings); + if (contextInParameters(clientMethod)) { + function.methodReturn(serviceMethodCall); + } else { + function.methodReturn("FluxUtil.withContext(context -> " + serviceMethodCall + ")"); + } + }); + } + + protected boolean contextInParameters(ClientMethod clientMethod) { + return clientMethod.getParameters().stream().anyMatch(param -> getContextType().equals(param.getClientType())); + } + + protected IType getContextType() { + return ClassType.CONTEXT; + } + + /** + * Extension to write LRO async client method. + * + * @param clientMethod client method + * @param typeBlock type block + * @param restAPIMethod proxy method + * @param settings java settings + */ + protected void generateLongRunningAsync(ClientMethod clientMethod, JavaType typeBlock, ProxyMethod restAPIMethod, JavaSettings settings) { + + } + + /** + * Extension to write LRO sync client method. + * + * @param clientMethod client method + * @param typeBlock type block + * @param restAPIMethod proxy method + * @param settings java settings + */ + protected void generateLongRunningSync(ClientMethod clientMethod, JavaType typeBlock, ProxyMethod restAPIMethod, JavaSettings settings) { + + } + + /** + * Extension to write LRO begin async client method. + * + * @param clientMethod client method + * @param typeBlock type block + * @param restAPIMethod proxy method + * @param settings java settings + */ + protected void generateLongRunningBeginAsync(ClientMethod clientMethod, JavaType typeBlock, ProxyMethod restAPIMethod, JavaSettings settings) { + String contextParam; + if (clientMethod.getParameters().stream().anyMatch(p -> p.getClientType().equals(ClassType.CONTEXT))) { + contextParam = "context"; + } else { + contextParam = "Context.NONE"; + } + String pollingStrategy = getPollingStrategy(clientMethod, contextParam); + typeBlock.annotation("ServiceMethod(returns = ReturnType.LONG_RUNNING_OPERATION)"); + writeMethod(typeBlock, clientMethod.getMethodVisibility(), clientMethod.getDeclaration(), function -> { + addOptionalVariables(function, clientMethod); + function.line("return PollerFlux.create(Duration.ofSeconds(%s),", clientMethod.getMethodPollingDetails().getPollIntervalInSeconds()); + function.increaseIndent(); + function.line("() -> this.%s(%s),", clientMethod.getProxyMethod().getSimpleAsyncRestResponseMethodName(), clientMethod.getArgumentList()); + function.line(pollingStrategy + ","); + function.line(TemplateUtil.getLongRunningOperationTypeReferenceExpression(clientMethod.getMethodPollingDetails()) + ");"); + function.decreaseIndent(); + }); + } + + /** + * Extension to write LRO begin sync client method. + * + * @param clientMethod client method + * @param typeBlock type block + */ + protected void generateLongRunningBeginSyncOverAsync(ClientMethod clientMethod, JavaType typeBlock) { + typeBlock.annotation("ServiceMethod(returns = ReturnType.LONG_RUNNING_OPERATION)"); + writeMethod(typeBlock, clientMethod.getMethodVisibility(), clientMethod.getDeclaration(), function -> { + addOptionalVariables(function, clientMethod); + function.methodReturn(String.format("this.%sAsync(%s).getSyncPoller()", + clientMethod.getName(), clientMethod.getArgumentList())); + }); + } + + /** + * Extension to write LRO begin sync client method. + * + * @param clientMethod client method + * @param typeBlock type block + * @param restAPIMethod proxy method + * @param settings java settings + */ + protected void generateLongRunningBeginSync(ClientMethod clientMethod, JavaType typeBlock, ProxyMethod restAPIMethod, JavaSettings settings) { + typeBlock.annotation("ServiceMethod(returns = ReturnType.LONG_RUNNING_OPERATION)"); + String contextParam; + if (clientMethod.getParameters().stream().anyMatch(p -> p.getClientType().equals(ClassType.CONTEXT))) { + contextParam = "context"; + } else { + contextParam = "Context.NONE"; + } + String pollingStrategy = getSyncPollingStrategy(clientMethod, contextParam); + + String argumentList = clientMethod.getArgumentList(); + if (CoreUtils.isNullOrEmpty(argumentList)) { + // If there are no arguments the argument is Context.NONE + argumentList = "Context.NONE"; + } else if (clientMethod.getParameters().stream().noneMatch(p -> p.getClientType() == ClassType.CONTEXT)) { + // If the arguments don't contain Context append Context.NONE + argumentList += ", Context.NONE"; + } + + String effectiveArgumentList = argumentList; + writeMethod(typeBlock, clientMethod.getMethodVisibility(), clientMethod.getDeclaration(), function -> { + addOptionalVariables(function, clientMethod); + function.line("return SyncPoller.createPoller(Duration.ofSeconds(%s),", + clientMethod.getMethodPollingDetails().getPollIntervalInSeconds()); + function.increaseIndent(); + function.line("() -> this.%s(%s),", clientMethod.getProxyMethod().getSimpleRestResponseMethodName(), effectiveArgumentList); + function.line(pollingStrategy + ","); + function.line(TemplateUtil.getLongRunningOperationTypeReferenceExpression(clientMethod.getMethodPollingDetails()) + ");"); + function.decreaseIndent(); + }); + } + + private void generateProtocolLongRunningBeginSync(ClientMethod clientMethod, JavaType typeBlock) { + String contextParam = "requestOptions != null && requestOptions.getContext() != null ? requestOptions.getContext() : Context.NONE"; + String pollingStrategy = getSyncPollingStrategy(clientMethod, contextParam); + typeBlock.annotation("ServiceMethod(returns = ReturnType.LONG_RUNNING_OPERATION)"); + writeMethod(typeBlock, clientMethod.getMethodVisibility(), clientMethod.getDeclaration(), function -> { + addOptionalVariables(function, clientMethod); + function.line("return SyncPoller.createPoller(Duration.ofSeconds(%s),", + clientMethod.getMethodPollingDetails().getPollIntervalInSeconds()); + function.increaseIndent(); + function.line("() -> this.%s(%s),", clientMethod.getProxyMethod().getSimpleRestResponseMethodName(), clientMethod.getArgumentList()); + function.line(pollingStrategy + ","); + function.line(TemplateUtil.getLongRunningOperationTypeReferenceExpression(clientMethod.getMethodPollingDetails()) + ");"); + function.decreaseIndent(); + }); + } + + /** + * Generate long-running begin async method for protocol client + * + * @param clientMethod client method + * @param typeBlock type block + */ + protected void generateProtocolLongRunningBeginAsync(ClientMethod clientMethod, JavaType typeBlock) { + String contextParam = "requestOptions != null && requestOptions.getContext() != null ? requestOptions.getContext() : Context.NONE"; + String pollingStrategy = getPollingStrategy(clientMethod, contextParam); + typeBlock.annotation("ServiceMethod(returns = ReturnType.LONG_RUNNING_OPERATION)"); + writeMethod(typeBlock, clientMethod.getMethodVisibility(), clientMethod.getDeclaration(), function -> { + addOptionalVariables(function, clientMethod); + function.line("return PollerFlux.create(Duration.ofSeconds(%s),", clientMethod.getMethodPollingDetails().getPollIntervalInSeconds()); + function.increaseIndent(); + function.line("() -> this.%s(%s),", clientMethod.getProxyMethod().getSimpleAsyncRestResponseMethodName(), clientMethod.getArgumentList()); + function.line(pollingStrategy + ","); + function.line(TemplateUtil.getLongRunningOperationTypeReferenceExpression(clientMethod.getMethodPollingDetails()) + ");"); + function.decreaseIndent(); + }); + } + + private String getPagingSinglePageExpression(ClientMethod clientMethod, String methodName, String argumentLine, JavaSettings settings) { + if (settings.isDataPlaneClient() && settings.isPageSizeEnabled()) { + Optional serializedName = MethodUtil.serializedNameOfMaxPageSizeParameter(clientMethod.getProxyMethod()); + if (serializedName.isPresent()) { + argumentLine = argumentLine.replace("requestOptions", "requestOptionsLocal"); + StringBuilder expression = new StringBuilder(); + expression.append("(pageSize) -> {"); + expression.append("RequestOptions requestOptionsLocal = requestOptions == null ? new RequestOptions() : requestOptions;") + .append("if (pageSize != null) {") + .append(" requestOptionsLocal.addRequestCallback(requestLocal -> {") + .append(" UrlBuilder urlBuilder = UrlBuilder.parse(requestLocal.getUrl());") + .append(" urlBuilder.setQueryParameter(\"").append(serializedName.get()).append("\", String.valueOf(pageSize));") + .append(" requestLocal.setUrl(urlBuilder.toString());") + .append(" });") + .append("}") + .append(String.format("return %s(%s);", methodName, argumentLine)); + expression.append("}"); + return expression.toString(); + } + } + + return String.format("() -> %s(%s)", methodName, argumentLine); + } + + private String getPagingNextPageExpression(ClientMethod clientMethod, String methodName, String argumentLine, JavaSettings settings) { + if (settings.isDataPlaneClient() && settings.isPageSizeEnabled()) { + Optional serializedName = MethodUtil.serializedNameOfMaxPageSizeParameter(clientMethod.getProxyMethod()); + if (serializedName.isPresent()) { + argumentLine = argumentLine.replace("requestOptions", "requestOptionsLocal"); + StringBuilder expression = new StringBuilder(); + expression.append("(nextLink, pageSize) -> {"); + expression.append("RequestOptions requestOptionsLocal = new RequestOptions();") + .append("requestOptionsLocal.setContext(requestOptionsForNextPage.getContext());") + .append("if (pageSize != null) {") + .append(" requestOptionsLocal.addRequestCallback(requestLocal -> {") + .append(" UrlBuilder urlBuilder = UrlBuilder.parse(requestLocal.getUrl());") + .append(" urlBuilder.setQueryParameter(\"").append(serializedName.get()).append("\", String.valueOf(pageSize));") + .append(" requestLocal.setUrl(urlBuilder.toString());") + .append(" });") + .append("}") + .append(String.format("return %s(%s);", methodName, argumentLine)); + expression.append("}"); + return expression.toString(); + } + } + + if (settings.isDataPlaneClient()) { + argumentLine = argumentLine.replace("requestOptions", "requestOptionsForNextPage"); + } + return String.format("nextLink -> %s(%s)", methodName, argumentLine); + } + + private String getPollingStrategy(ClientMethod clientMethod, String contextParam) { + String endpoint = "null"; + if (clientMethod.getProxyMethod() != null && clientMethod.getProxyMethod().getParameters() != null) { + if (clientMethod.getProxyMethod().getParameters().stream() + .anyMatch(p -> p.isFromClient() && p.getRequestParameterLocation() == RequestParameterLocation.URI && "endpoint".equals(p.getName()))) { + // has EndpointTrait + + final String baseUrl = clientMethod.getProxyMethod().getBaseUrl(); + final String endpointReplacementExpr = clientMethod.getProxyMethod().getParameters().stream() + .filter(p -> p.isFromClient() && p.getRequestParameterLocation() == RequestParameterLocation.URI) + .filter(p -> baseUrl.contains(String.format("{%s}", p.getRequestParameterName()))) + .map(p -> String.format(".replace(%1$s, %2$s)", + ClassType.STRING.defaultValueExpression(String.format("{%s}", p.getRequestParameterName())), + p.getParameterReference() + )).collect(Collectors.joining()); + if (!CoreUtils.isNullOrEmpty(endpointReplacementExpr)) { + endpoint = ClassType.STRING.defaultValueExpression(baseUrl) + endpointReplacementExpr; + } + } + } + return clientMethod.getMethodPollingDetails().getPollingStrategy() + .replace("{httpPipeline}", clientMethod.getClientReference() + ".getHttpPipeline()") + .replace("{endpoint}", endpoint) + .replace("{context}", contextParam) + .replace("{serviceVersion}", getServiceVersionValue(clientMethod)) + .replace("{serializerAdapter}", clientMethod.getClientReference() + ".getSerializerAdapter()") + .replace("{intermediate-type}", clientMethod.getMethodPollingDetails().getIntermediateType().toString()) + .replace("{final-type}", clientMethod.getMethodPollingDetails().getFinalType().toString()) + .replace(".setServiceVersion(null)", "") + .replace(".setEndpoint(null)", ""); + } + + private String getSyncPollingStrategy(ClientMethod clientMethod, String contextParam) { + String endpoint = "null"; + if (clientMethod.getProxyMethod() != null && clientMethod.getProxyMethod().getParameters() != null) { + if (clientMethod.getProxyMethod().getParameters().stream() + .anyMatch(p -> p.isFromClient() && p.getRequestParameterLocation() == RequestParameterLocation.URI && "endpoint".equals(p.getName()))) { + // has EndpointTrait + + final String baseUrl = clientMethod.getProxyMethod().getBaseUrl(); + final String endpointReplacementExpr = clientMethod.getProxyMethod().getParameters().stream() + .filter(p -> p.isFromClient() && p.getRequestParameterLocation() == RequestParameterLocation.URI) + .filter(p -> baseUrl.contains(String.format("{%s}", p.getRequestParameterName()))) + .map(p -> String.format(".replace(%1$s, %2$s)", + ClassType.STRING.defaultValueExpression(String.format("{%s}", p.getRequestParameterName())), + p.getParameterReference() + )).collect(Collectors.joining()); + if (!CoreUtils.isNullOrEmpty(endpointReplacementExpr)) { + endpoint = ClassType.STRING.defaultValueExpression(baseUrl) + endpointReplacementExpr; + } + } + } + return clientMethod.getMethodPollingDetails().getSyncPollingStrategy() + .replace("{httpPipeline}", clientMethod.getClientReference() + ".getHttpPipeline()") + .replace("{endpoint}", endpoint) + .replace("{context}", contextParam) + .replace("{serviceVersion}", getServiceVersionValue(clientMethod)) + .replace("{serializerAdapter}", clientMethod.getClientReference() + ".getSerializerAdapter()") + .replace("{intermediate-type}", clientMethod.getMethodPollingDetails().getIntermediateType().toString()) + .replace("{final-type}", clientMethod.getMethodPollingDetails().getFinalType().toString()) + .replace(".setServiceVersion(null)", "") + .replace(".setEndpoint(null)", ""); + } + + protected void generateSendRequestAsync(ClientMethod clientMethod, JavaType typeBlock) { + addServiceMethodAnnotation(typeBlock, ReturnType.SINGLE); + writeMethod(typeBlock, clientMethod.getMethodVisibility(), clientMethod.getDeclaration(), function -> { + function.line("return FluxUtil.withContext(context -> %1$s.getHttpPipeline().send(%2$s, context)", + clientMethod.getClientReference(), clientMethod.getArgumentList()); + function.indent(() -> { + function.line(".flatMap(response -> BinaryData.fromFlux(response.getBody())"); + function.line(".map(body -> new SimpleResponse<>(response.getRequest(), response.getStatusCode(), response.getHeaders(), body))));"); + }); + }); + } + + protected void generateSendRequestSync(ClientMethod clientMethod, JavaType typeBlock) { + addServiceMethodAnnotation(typeBlock, ReturnType.SINGLE); + writeMethod(typeBlock, clientMethod.getMethodVisibility(), clientMethod.getDeclaration(), function -> + function.methodReturn("this.sendRequestAsync(httpRequest).contextWrite(c -> c.putAll(FluxUtil.toReactorContext(context).readOnly())).block()")); + } + + private static String getServiceVersionValue(ClientMethod clientMethod) { + String serviceVersion = "null"; + if (JavaSettings.getInstance().isDataPlaneClient() && clientMethod.getProxyMethod() != null && clientMethod.getProxyMethod().getParameters() != null) { + if (clientMethod.getProxyMethod().getParameters().stream() + .anyMatch(p -> p.getOrigin() == ParameterSynthesizedOrigin.API_VERSION)) { + serviceVersion = clientMethod.getClientReference() + ".getServiceVersion().getVersion()"; + } + } + return serviceVersion; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/ClientMethodTemplateBase.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/ClientMethodTemplateBase.java new file mode 100644 index 0000000000..7a38b32656 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/ClientMethodTemplateBase.java @@ -0,0 +1,332 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.template; + +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.RequestParameterLocation; +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClassType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientEnumValue; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethod; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethodParameter; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientModel; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientModelProperty; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.EnumType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.GenericType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.IType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ListType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.MapType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ParameterSynthesizedOrigin; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.PrimitiveType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ProxyMethod; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ProxyMethodParameter; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaJavadocComment; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaType; +import com.microsoft.typespec.http.client.generator.core.util.ClientModelUtil; +import com.microsoft.typespec.http.client.generator.core.util.CodeNamer; +import com.azure.core.util.CoreUtils; + +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +public abstract class ClientMethodTemplateBase implements IJavaTemplate { + + protected static void generateProtocolMethodJavadoc(ClientMethod clientMethod, JavaJavadocComment commentBlock) { + commentBlock.description(clientMethod.getDescription()); + + if (clientMethod.getProxyMethod() != null) { + List queryParameters = clientMethod.getProxyMethod().getAllParameters().stream() + .filter(p -> RequestParameterLocation.QUERY.equals(p.getRequestParameterLocation()) + // ignore if synthesized by modelerfour, i.e. api-version + && p.getOrigin() == ParameterSynthesizedOrigin.NONE) + .collect(Collectors.toList()); + if (!queryParameters.isEmpty() && hasParametersToPrintInJavadoc(queryParameters)) { + optionalParametersJavadoc("Query Parameters", queryParameters, commentBlock); + commentBlock.line("You can add these to a request with {@link RequestOptions#addQueryParam}"); + } + + List headerParameters = clientMethod.getProxyMethod().getAllParameters().stream() + .filter(p -> RequestParameterLocation.HEADER.equals(p.getRequestParameterLocation())) + // ignore if synthesized by modelerfour and is constant + // we would want user to provide a correct "content-type" if it is not a constant + .filter(p -> p.getOrigin() == ParameterSynthesizedOrigin.NONE || !p.isConstant()) + .collect(Collectors.toList()); + if (!headerParameters.isEmpty() && hasParametersToPrintInJavadoc(headerParameters)) { + optionalParametersJavadoc("Header Parameters", headerParameters, commentBlock); + commentBlock.line("You can add these to a request with {@link RequestOptions#addHeader}"); + } + + // Request body + Set typesInJavadoc = new HashSet<>(); + + Optional bodyParameter = clientMethod.getProxyMethod().getAllParameters() + .stream().filter(p -> RequestParameterLocation.BODY.equals(p.getRequestParameterLocation())) + .findFirst(); + + if (bodyParameter.isPresent()) { + ClientModel model = ClientModelUtil.getClientModel(bodyParameter.get().getRawType().toString()); + if (model == null || !ClientModelUtil.isMultipartModel(model)) { + // do not generate JSON schema for Multipart request body + boolean isBodyParamRequired = bodyParameter.map(ProxyMethodParameter::isRequired).orElse(false); + bodyParameter.map(ProxyMethodParameter::getRawType).ifPresent(type -> requestBodySchemaJavadoc(type, commentBlock, typesInJavadoc, isBodyParamRequired)); + } + } + + // Response body + IType responseBodyType; + if (JavaSettings.getInstance().isDataPlaneClient()) { + // special handling for paging method + if (clientMethod.getType().isPaging()) { + String itemName = clientMethod.getMethodPageDetails().getItemName(); + // rawResponseType has properties: 'value' and 'nextLink' + IType rawResponseType = clientMethod.getProxyMethod().getRawResponseBodyType(); + if (!(rawResponseType instanceof ClassType)) { + throw new IllegalStateException(String.format("clientMethod.getProxyMethod().getRawResponseBodyType() should be ClassType for paging method. rawResponseType = %s", rawResponseType.toString())); + } + ClientModel model = ClientModelUtil.getClientModel(((ClassType) rawResponseType).getName()); + Map properties = new LinkedHashMap<>(); + traverseProperties(model, properties); + responseBodyType = properties.values().stream() + .filter(property -> property.getName().equals(itemName)) + .map(ClientModelProperty::getClientType) + .map(valueListType -> { + // value type is List, we need to get the typeArguments + if (!(valueListType instanceof ListType)) { + throw new IllegalStateException("value type must be list for paging method. " + + "rawResponseType = " + rawResponseType); + } + IType[] listTypeArgs = ((ListType) valueListType).getTypeArguments(); + if (listTypeArgs.length == 0) { + throw new IllegalStateException(String.format("list type arguments' length should not be 0 for paging method. rawResponseType = %s", + rawResponseType)); + } + return listTypeArgs[0]; + }) + .findFirst().orElse(null); + if (responseBodyType == null) { + throw new IllegalStateException(itemName + " not found in properties of rawResponseType. rawResponseType = " + rawResponseType); + } + } else { + responseBodyType = clientMethod.getProxyMethod().getRawResponseBodyType(); + } + } else { + responseBodyType = clientMethod.getProxyMethod().getResponseBodyType(); + } + if (responseBodyType != null && !responseBodyType.equals(PrimitiveType.VOID)) { + responseBodySchemaJavadoc(responseBodyType, commentBlock, typesInJavadoc); + } + } + + clientMethod.getParameters().forEach(p -> commentBlock.param(p.getName(), methodParameterDescriptionOrDefault(p))); + if (clientMethod.getProxyMethod() != null) { + generateJavadocExceptions(clientMethod, commentBlock, false); + } + commentBlock.methodReturns(clientMethod.getReturnValue().getDescription()); + + + // add external documentation + if (clientMethod.getMethodDocumentation() != null) { + commentBlock.line("@see " + clientMethod.getMethodDocumentation().getDescription() + ""); + } + } + + protected static void generateJavadocExceptions(ClientMethod clientMethod, JavaJavadocComment commentBlock, boolean useFullClassName) { + ProxyMethod restAPIMethod = clientMethod.getProxyMethod(); + if (JavaSettings.getInstance().isBranded()) { + if (restAPIMethod != null && restAPIMethod.getUnexpectedResponseExceptionType() != null) { + commentBlock.methodThrows(useFullClassName + ? restAPIMethod.getUnexpectedResponseExceptionType().getFullName() + : restAPIMethod.getUnexpectedResponseExceptionType().getName(), + "thrown if the request is rejected by server"); + } + if (restAPIMethod != null && restAPIMethod.getUnexpectedResponseExceptionTypes() != null) { + for (Map.Entry> exception : restAPIMethod.getUnexpectedResponseExceptionTypes().entrySet()) { + commentBlock.methodThrows(useFullClassName + ? exception.getKey().getFullName() + : exception.getKey().getName(), + String.format("thrown if the request is rejected by server on status code %s", + exception.getValue().stream().map(String::valueOf).collect(Collectors.joining(", ")))); + } + } + } else { + if (restAPIMethod != null && (restAPIMethod.getUnexpectedResponseExceptionType() != null || restAPIMethod.getUnexpectedResponseExceptionTypes() != null)) { + commentBlock.methodThrows("HttpResponseException", "thrown if the service returns an error"); + } + } + } + + private static void optionalParametersJavadoc(String title, List parameters, + JavaJavadocComment commentBlock) { + commentBlock.line(String.format("

%s

", title)); + commentBlock.line(""); + commentBlock.line(String.format(" ", title)); + commentBlock.line(" "); + for (ProxyMethodParameter parameter : parameters) { + boolean parameterIsConstantOrFromClient = parameter.isConstant() || parameter.isFromClient(); + if (!parameter.isRequired() && !parameterIsConstantOrFromClient) { + commentBlock.line(String.format(" ", + parameter.getRequestParameterName(), + CodeNamer.escapeXmlComment(parameter.getClientType().toString()), + parameterDescriptionOrDefault(parameter))); + } + + } + commentBlock.line("
%s
NameTypeRequiredDescription
%s%sNo%s
"); + } + + private static boolean hasParametersToPrintInJavadoc(List parameters) { + return parameters.stream().anyMatch(parameter -> { + boolean parameterIsConstantOrFromClient = parameter.isConstant() || parameter.isFromClient(); + boolean parameterIsRequired = parameter.isRequired(); + return !parameterIsRequired && !parameterIsConstantOrFromClient; + }); + } + + private static void requestBodySchemaJavadoc(IType requestBodyType, JavaJavadocComment commentBlock, Set typesInJavadoc, boolean isBodyParamRequired) { + typesInJavadoc.clear(); + + if (requestBodyType == null) { + return; + } + commentBlock.line("

Request Body Schema

"); + commentBlock.line("
{@code");
+        bodySchemaJavadoc(requestBodyType, commentBlock, "", null, typesInJavadoc, isBodyParamRequired, isBodyParamRequired, true);
+        commentBlock.line("}
"); + } + + private static void responseBodySchemaJavadoc(IType responseBodyType, JavaJavadocComment commentBlock, Set typesInJavadoc) { + typesInJavadoc.clear(); + + if (responseBodyType == null) { + return; + } + commentBlock.line("

Response Body Schema

"); + commentBlock.line("
{@code");
+        bodySchemaJavadoc(responseBodyType, commentBlock, "", null, typesInJavadoc, true, true, true);
+        commentBlock.line("}
"); + } + + private static void bodySchemaJavadoc(IType type, JavaJavadocComment commentBlock, String indent, String name, Set typesInJavadoc, + boolean isRequired, boolean isRequiredForCreate, boolean isRootSchema) { + String nextIndent = indent + " "; + if ((ClientModelUtil.isClientModel(type) || ClientModelUtil.isExternalModel(type)) && !typesInJavadoc.contains(type)) { + typesInJavadoc.add(type); + ClientModel model = ClientModelUtil.getClientModel(((ClassType) type).getName()); + if (name != null) { + commentBlock.line(indent + name + appendOptionalOrRequiredAttribute(isRequired, isRequiredForCreate, isRootSchema) + ": {"); + } else { + commentBlock.line(indent + appendOptionalOrRequiredAttribute(isRequired, isRequiredForCreate, isRootSchema) + "{"); + } + Map properties = new LinkedHashMap<>(); + traverseProperties(model, properties); + for (ClientModelProperty property : properties.values()) { + bodySchemaJavadoc(property.getWireType(), commentBlock, nextIndent, property.getSerializedName(), typesInJavadoc, property.isRequired() || property.isPolymorphicDiscriminator(), property.isRequiredForCreate(),false); + } + commentBlock.line(indent + "}"); + } else if (typesInJavadoc.contains(type)) { + if (name != null) { + commentBlock.line(indent + name + appendOptionalOrRequiredAttribute(isRequired, isRequiredForCreate, isRootSchema) + ": (recursive schema, see " + name + " above)"); + } else { + commentBlock.line(indent + "(recursive schema, see above)"); + } + } else if (type instanceof ListType) { + if (name != null) { + commentBlock.line(indent + name + appendOptionalOrRequiredAttribute(isRequired, isRequiredForCreate, isRootSchema) + ": ["); + } else { + commentBlock.line(indent + appendOptionalOrRequiredAttribute(isRequired, isRequiredForCreate, isRootSchema) + "["); + } + bodySchemaJavadoc(((ListType) type).getElementType(), commentBlock, nextIndent, null, typesInJavadoc, isRequired, isRequiredForCreate, false); + commentBlock.line(indent + "]"); + } else if (type instanceof EnumType) { + String values = ((EnumType) type).getValues().stream() + .map(ClientEnumValue::getValue) + .collect(Collectors.joining("/")); + if (name != null) { + commentBlock.line(indent + name + ": String(" + values + ")" + appendOptionalOrRequiredAttribute(isRequired, isRequiredForCreate, isRootSchema)); + } else { + commentBlock.line(indent + "String(" + values + ")" + appendOptionalOrRequiredAttribute(isRequired, isRequiredForCreate, isRootSchema)); + } + } else if (type instanceof MapType) { + if (name != null) { + commentBlock.line(indent + name + appendOptionalOrRequiredAttribute(isRequired, isRequiredForCreate, isRootSchema) + ": {"); + } else { + commentBlock.line(indent + appendOptionalOrRequiredAttribute(isRequired, isRequiredForCreate, isRootSchema) + "{"); + } + final boolean valueRequired = !((MapType) type).isValueNullable(); + bodySchemaJavadoc(((MapType) type).getValueType(), commentBlock, nextIndent, "String", typesInJavadoc, valueRequired, valueRequired, false); + commentBlock.line(indent + "}"); + } else { + String javadoc = convertToBodySchemaJavadoc(type); + if (name != null) { + commentBlock.line(indent + name + ": " + javadoc + appendOptionalOrRequiredAttribute(isRequired, isRequiredForCreate, isRootSchema)); + } else { + commentBlock.line(indent + javadoc + appendOptionalOrRequiredAttribute(isRequired, isRequiredForCreate, isRootSchema)); + } + } + } + + /* + * Converts raw type into type to display in javadoc as body schema type. + * 1. converts Flux to BinaryData (applies to request body schema, since DPG response type can't be Flux) + */ + private static String convertToBodySchemaJavadoc(IType type) { + if (GenericType.FLUX_BYTE_BUFFER.equals(type)) { + return ClassType.BINARY_DATA.toString(); + } + return type.toString(); + } + + private static void traverseProperties(ClientModel model, Map properties) { + if (model.getParentModelName() != null) { + traverseProperties(ClientModelUtil.getClientModel(model.getParentModelName()), properties); + } + + model.getProperties().forEach(p -> properties.put(p.getSerializedName(), p)); + } + + private static String parameterDescriptionOrDefault(ProxyMethodParameter parameter) { + String paramJavadoc = parameter.getDescription(); + if (CoreUtils.isNullOrEmpty(paramJavadoc)) { + paramJavadoc = String.format("The %1$s parameter", parameter.getName()); + } + String description = CodeNamer.escapeXmlComment(paramJavadoc); + // query with array, add additional description + if (parameter.getRequestParameterLocation() == RequestParameterLocation.QUERY && parameter.getCollectionFormat() != null) { + description = (CoreUtils.isNullOrEmpty(description) || description.endsWith(".")) ? description : (description + "."); + if (parameter.getExplode()) { + // collectionFormat: multi + description += " Call {@link RequestOptions#addQueryParam} to add string to array."; + } else { + // collectionFormat: csv, ssv, tsv, pipes + description += String.format(" In the form of %s separated string.", + ClassType.STRING.defaultValueExpression(parameter.getCollectionFormat().getDelimiter())); + } + } + return description; + } + + private static String methodParameterDescriptionOrDefault(ClientMethodParameter p) { + String doc = p.getDescription(); + if (CoreUtils.isNullOrEmpty(doc)) { + doc = String.format("The %1$s parameter", p.getName()); + } + return doc; + } + + private static String appendOptionalOrRequiredAttribute(boolean isRequired, boolean isRequiredForCreate, boolean isRootSchema) { + if (isRootSchema) { + return ""; + } else if (isRequired) { + return " (Required)"; + } else if (isRequiredForCreate) { + return " (Optional, Required on create)"; + } else { + return " (Optional)"; + } + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/ClientMethodTestTemplate.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/ClientMethodTestTemplate.java new file mode 100644 index 0000000000..b9ca8de6fe --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/ClientMethodTestTemplate.java @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.template; + +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethod; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethodExample; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.TestContext; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.examplemodel.ExampleHelperFeature; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaFile; +import com.microsoft.typespec.http.client.generator.core.template.example.ClientMethodExampleWriter; +import com.microsoft.typespec.http.client.generator.core.template.example.ModelExampleWriter; +import com.microsoft.typespec.http.client.generator.core.template.example.ProtocolTestWriter; +import com.microsoft.typespec.http.client.generator.core.util.CodeNamer; + +import java.util.Set; + +public class ClientMethodTestTemplate implements IJavaTemplate, JavaFile> { + + private static final ClientMethodTestTemplate INSTANCE = new ClientMethodTestTemplate(); + + protected ClientMethodTestTemplate() { + } + + public static ClientMethodTestTemplate getInstance() { + return INSTANCE; + } + + @Override + public void write(TestContext testContext, JavaFile context) { + + final String className = testContext.getTestCase().getFilename() + "Tests"; + + ProtocolTestWriter writer = new ProtocolTestWriter(testContext); + ClientMethodExample clientMethodExample = testContext.getTestCase(); + ClientMethod clientMethod = clientMethodExample.getClientMethod(); + ClientMethodExampleWriter caseWriter = new ClientMethodExampleWriter( + clientMethod, + CodeNamer.toCamelCase(clientMethodExample.getSyncClient().getClassName()), + clientMethodExample.getProxyMethodExample()); + + Set imports = writer.getImports(); + clientMethod.getReturnValue().getType().addImportsTo(imports, false); + imports.addAll(caseWriter.getImports()); + context.declareImport(imports); + + context.annotation("Disabled"); + context.publicFinalClass(String.format("%1$s extends %2$s", className, testContext.getTestBaseClassName()), classBlock -> { + classBlock.annotation("Test", "Disabled"); // "DoNotRecord(skipInPlayback = true)" not added + Set helperFeatures = caseWriter.getHelperFeatures(); + String methodSignature = String.format("void test%1$s()", className); + if (helperFeatures.contains(ExampleHelperFeature.ThrowsIOException)) { + methodSignature += " throws IOException"; + } + classBlock.publicMethod(methodSignature, methodBlock -> { + methodBlock.line("// method invocation"); + caseWriter.writeClientMethodInvocation(methodBlock, true); + caseWriter.writeAssertion(methodBlock); + }); + if (helperFeatures.contains(ExampleHelperFeature.MapOfMethod)) { + ModelExampleWriter.writeMapOfMethod(classBlock); + } + }); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/ConvenienceAsyncMethodTemplate.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/ConvenienceAsyncMethodTemplate.java new file mode 100644 index 0000000000..6e2f1f080c --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/ConvenienceAsyncMethodTemplate.java @@ -0,0 +1,199 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.template; + +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ArrayType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClassType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethod; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethodType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ConvenienceMethod; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.EnumType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.GenericType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.IType; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaBlock; +import com.microsoft.typespec.http.client.generator.core.util.TemplateUtil; +import com.azure.core.http.rest.PagedResponse; +import com.azure.core.http.rest.PagedResponseBase; +import com.azure.core.util.CoreUtils; +import com.azure.core.util.FluxUtil; +import reactor.core.publisher.Flux; + +import java.util.List; +import java.util.Set; + +public class ConvenienceAsyncMethodTemplate extends ConvenienceMethodTemplateBase { + + private static final ConvenienceAsyncMethodTemplate INSTANCE = new ConvenienceAsyncMethodTemplate(); + + protected ConvenienceAsyncMethodTemplate() { + } + + public static ConvenienceAsyncMethodTemplate getInstance() { + return INSTANCE; + } + + public void addImports(Set imports, List convenienceMethods) { + if (!CoreUtils.isNullOrEmpty(convenienceMethods)) { + super.addImports(imports, convenienceMethods); + + // async e.g. FluxUtil::toMono + imports.add(FluxUtil.class.getName()); + + // async pageable + imports.add(PagedResponse.class.getName()); + imports.add(PagedResponseBase.class.getName()); + imports.add(Flux.class.getName()); + } + } + + @Override + protected boolean isMethodIncluded(ClientMethod method) { + return isMethodAsync(method) && isMethodVisible(method) && !method.isImplementationOnly(); + } + + @Override + protected boolean isMethodIncluded(ConvenienceMethod method) { + return isMethodAsync(method.getProtocolMethod()) && isMethodVisible(method.getProtocolMethod()) + // for LRO, we actually choose the protocol method of "WithModel" + && (method.getProtocolMethod().getType() != ClientMethodType.LongRunningBeginAsync || (method.getProtocolMethod().getImplementationDetails() != null && method.getProtocolMethod().getImplementationDetails().isImplementationOnly())) + && method.getProtocolMethod().getMethodParameters().stream().noneMatch(p -> p.getClientType() == ClassType.CONTEXT); + } + + protected void writeInvocationAndConversion( + ClientMethod convenienceMethod, ClientMethod protocolMethod, + String invocationExpression, + JavaBlock methodBlock, + Set typeReferenceStaticClasses) { + + ClientMethodType methodType = protocolMethod.getType(); + + IType responseBodyType = getResponseBodyType(convenienceMethod); + IType protocolResponseBodyType = getResponseBodyType(protocolMethod); + IType rawResponseBodyType = convenienceMethod.getProxyMethod().getRawResponseBodyType(); + + if (methodType == ClientMethodType.PagingAsync) { + String expressionMapFromBinaryData = expressionMapFromBinaryData( + responseBodyType, rawResponseBodyType, + protocolMethod.getProxyMethod().getResponseContentTypes(), + typeReferenceStaticClasses); + if (expressionMapFromBinaryData == null) { + // no need to do the map + methodBlock.methodReturn(String.format("%1$s(%2$s)", getMethodName(protocolMethod), invocationExpression)); + } else { + methodBlock.line("PagedFlux pagedFluxResponse = %1$s(%2$s);", getMethodName(protocolMethod), invocationExpression); + + methodBlock.methodReturn(String.format( + "PagedFlux.create(() -> (continuationTokenParam, pageSizeParam) -> {\n" + + " Flux> flux = (continuationTokenParam == null)\n" + + " ? pagedFluxResponse.byPage().take(1)\n" + + " : pagedFluxResponse.byPage(continuationTokenParam).take(1);\n" + + " return flux.map(pagedResponse -> new PagedResponseBase(pagedResponse.getRequest(),\n" + + " pagedResponse.getStatusCode(),\n" + + " pagedResponse.getHeaders(),\n" + + " pagedResponse.getValue().stream().map(%2$s).collect(Collectors.toList()),\n" + + " pagedResponse.getContinuationToken(),\n" + + " null));\n" + + "})", + responseBodyType.asNullable(), expressionMapFromBinaryData)); + } + } else if (methodType == ClientMethodType.LongRunningBeginAsync) { + String methodName = protocolMethod.getName(); + methodBlock.methodReturn(String.format("serviceClient.%1$s(%2$s)", methodName, invocationExpression)); + } else { + String returnTypeConversionExpression = ""; + if (protocolResponseBodyType == ClassType.BINARY_DATA) { + returnTypeConversionExpression = expressionConvertFromBinaryData( + responseBodyType, rawResponseBodyType, + protocolMethod.getProxyMethod().getResponseContentTypes(), + typeReferenceStaticClasses); + } + + methodBlock.methodReturn( + String.format("%1$s(%2$s).flatMap(FluxUtil::toMono)%3$s", + getMethodName(protocolMethod), + invocationExpression, + returnTypeConversionExpression)); + } + } + + @Override + protected void writeThrowException(ClientMethodType methodType, String exceptionExpression, JavaBlock methodBlock) { + if (methodType == ClientMethodType.PagingAsync) { + methodBlock.methodReturn(String.format("PagedFlux.create(() -> (ignoredContinuationToken, ignoredPageSize) -> Flux.error(%s))", exceptionExpression)); + } else if (methodType == ClientMethodType.LongRunningBeginAsync) { + methodBlock.methodReturn(String.format("PollerFlux.error(%s)", exceptionExpression)); + } else { + methodBlock.methodReturn(String.format("Mono.error(%s)", exceptionExpression)); + } + } + + private IType getResponseBodyType(ClientMethod method) { + // no need to care about LRO + // Mono / PagedFlux + IType type = ((GenericType) method.getReturnValue().getType()).getTypeArguments()[0]; + if (type instanceof GenericType && ClassType.RESPONSE.getName().equals(((GenericType) type).getName())) { + // Mono> + type = ((GenericType) type).getTypeArguments()[0]; + } + return type; + } + + private String expressionConvertFromBinaryData(IType responseBodyType, IType rawType, + Set mediaTypes, + Set typeReferenceStaticClasses) { + String expressionMapFromBinaryData = expressionMapFromBinaryData( + responseBodyType, rawType, + mediaTypes, + typeReferenceStaticClasses); + if (expressionMapFromBinaryData != null) { + return String.format(".map(%s)", expressionMapFromBinaryData); + } else { + // no need to do the map + return ""; + } + } + + private String expressionMapFromBinaryData(IType responseBodyType, IType rawType, + Set mediaTypes, + Set typeReferenceStaticClasses) { + String mapExpression = null; + SupportedMimeType mimeType = SupportedMimeType.getResponseKnownMimeType(mediaTypes); + // TODO (weidxu): support XML etc. + switch (mimeType) { + case TEXT: + mapExpression = "protocolMethodData -> protocolMethodData.toString()"; + break; + + case BINARY: + mapExpression = null; + break; + + default: + // JSON etc. + if (responseBodyType instanceof EnumType) { + // enum + mapExpression = String.format("protocolMethodData -> %1$s.from%2$s(protocolMethodData.toObject(%2$s.class))", responseBodyType, ((EnumType) responseBodyType).getElementType()); + } else if (responseBodyType instanceof GenericType) { + // generic, e.g. list, map + typeReferenceStaticClasses.add((GenericType) responseBodyType); + mapExpression = String.format("protocolMethodData -> protocolMethodData.toObject(%1$s)", TemplateUtil.getTypeReferenceCreation(responseBodyType)); + } else if (responseBodyType == ClassType.BINARY_DATA) { + // BinaryData, no need to do the map in expressionConvertFromBinaryData + mapExpression = null; + } else if (isModelOrBuiltin(responseBodyType)) { + // class + mapExpression = String.format("protocolMethodData -> protocolMethodData.toObject(%1$s.class)", responseBodyType.asNullable()); + } else if (responseBodyType == ArrayType.BYTE_ARRAY) { + // byte[] + if (rawType == ClassType.BASE_64_URL) { + return "protocolMethodData -> protocolMethodData.toObject(Base64Url.class).decodedBytes()"; + } else { + return "protocolMethodData -> protocolMethodData.toObject(byte[].class)"; + } + } + break; + } + return mapExpression; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/ConvenienceMethodTemplateBase.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/ConvenienceMethodTemplateBase.java new file mode 100644 index 0000000000..790933983e --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/ConvenienceMethodTemplateBase.java @@ -0,0 +1,853 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.template; + +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.RequestParameterLocation; +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.Annotation; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClassType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethod; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethodParameter; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethodType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientModel; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientModelProperty; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ConvenienceMethod; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.EnumType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.GenericType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.IType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.IterableType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ListType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.MapType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.MethodTransformationDetail; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ParameterMapping; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ParameterSynthesizedOrigin; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.PrimitiveType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ProxyMethodParameter; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaBlock; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaClass; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaType; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaVisibility; +import com.microsoft.typespec.http.client.generator.core.template.util.ModelTemplateHeaderHelper; +import com.microsoft.typespec.http.client.generator.core.util.ClientModelUtil; +import com.microsoft.typespec.http.client.generator.core.util.CodeNamer; +import com.microsoft.typespec.http.client.generator.core.util.MethodUtil; +import com.microsoft.typespec.http.client.generator.core.util.TemplateUtil; +import com.azure.core.util.CoreUtils; +import com.azure.core.util.FluxUtil; +import com.azure.core.util.serializer.CollectionFormat; +import com.azure.core.util.serializer.JacksonAdapter; +import com.azure.core.util.serializer.TypeReference; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.TreeMap; +import java.util.function.BiFunction; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.stream.Collectors; + +abstract class ConvenienceMethodTemplateBase { + + protected ConvenienceMethodTemplateBase() { + } + + public void write(ConvenienceMethod convenienceMethodObj, JavaClass classBlock, Set typeReferenceStaticClasses) { + if (!isMethodIncluded(convenienceMethodObj)) { + return; + } + + ClientMethod protocolMethod = convenienceMethodObj.getProtocolMethod(); + convenienceMethodObj.getConvenienceMethods().stream() + .filter(this::isMethodIncluded) + .forEach(convenienceMethod -> { + // javadoc + classBlock.javadocComment(comment -> ClientMethodTemplate.generateJavadoc(convenienceMethod, comment, + convenienceMethod.getProxyMethod(), false)); + + addGeneratedAnnotation(classBlock); + TemplateUtil.writeClientMethodServiceMethodAnnotation(convenienceMethod, classBlock); + + JavaVisibility methodVisibility = convenienceMethod.getMethodVisibilityInWrapperClient(); + + // convenience method + String methodDeclaration = String.format("%1$s %2$s(%3$s)", + convenienceMethod.getReturnValue().getType(), getMethodName(convenienceMethod), + convenienceMethod.getParametersDeclaration()); + classBlock.method(methodVisibility, null, methodDeclaration, methodBlock -> { + methodBlock.line("// Generated convenience method for " + getMethodName(protocolMethod)); + + writeMethodImplementation(protocolMethod, convenienceMethod, methodBlock, typeReferenceStaticClasses); + }); + }); + } + + /** + * Write the implementation of the convenience method. + * + * @param protocolMethod the protocol method. + * @param convenienceMethod the convenience method. + * @param methodBlock the code block. + */ + protected void writeMethodImplementation( + ClientMethod protocolMethod, + ClientMethod convenienceMethod, + JavaBlock methodBlock, + Set typeReferenceStaticClasses) { + + // matched parameters from convenience method to protocol method + Map parametersMap = + findParametersForConvenienceMethod(convenienceMethod, protocolMethod); + + // RequestOptions + methodBlock.line("RequestOptions requestOptions = new RequestOptions();"); + + // parameter transformation + if (!CoreUtils.isNullOrEmpty(convenienceMethod.getMethodTransformationDetails())) { + convenienceMethod.getMethodTransformationDetails() + .forEach(d -> writeParameterTransformation(d, convenienceMethod, protocolMethod, methodBlock, parametersMap)); + } + + writeValidationForVersioning(convenienceMethod, parametersMap.keySet(), methodBlock); + + boolean isJsonMergePatchOperation = protocolMethod != null && protocolMethod.getProxyMethod() != null + && "application/merge-patch+json".equalsIgnoreCase(protocolMethod.getProxyMethod().getRequestContentType()); + Map parameterExpressionsMap = new HashMap<>(); + for (Map.Entry entry : parametersMap.entrySet()) { + MethodParameter parameter = entry.getKey(); + MethodParameter protocolParameter = entry.getValue(); + + if (parameter.getProxyMethodParameter() != null && parameter.getProxyMethodParameter().getOrigin() == ParameterSynthesizedOrigin.CONTEXT) { + // Context + methodBlock.line(String.format("requestOptions.setContext(%s);", parameter.getName())); + } else if (protocolParameter != null) { + // protocol method parameter exists + String expression = expressionConvertToType(parameter.getName(), parameter, protocolMethod.getProxyMethod().getRequestContentType()); + parameterExpressionsMap.put(protocolParameter.getName(), expression); + } else if (parameter.getProxyMethodParameter() != null) { + // protocol method parameter not exist, set the parameter via RequestOptions + switch (parameter.getProxyMethodParameter().getRequestParameterLocation()) { + case HEADER: + writeHeader(parameter, methodBlock); + break; + + case QUERY: + writeQueryParam(parameter, methodBlock); + break; + + case BODY: + Consumer writeLine = javaBlock -> { + IType parameterType = parameter.getClientMethodParameter().getClientType(); + String expression = expressionConvertToBinaryData(parameter.getName(), + parameter.getClientMethodParameter().getWireType(), + protocolMethod.getProxyMethod().getRequestContentType()); + + if (isJsonMergePatchOperation + && ClientModelUtil.isClientModel(parameterType) + && ClientModelUtil.isJsonMergePatchModel(ClientModelUtil.getClientModel(((ClassType) parameterType).getName()), JavaSettings.getInstance())) { + String variableName = writeParameterConversionExpressionWithJsonMergePatchEnabled(javaBlock, parameterType.toString(), parameter.getName(), expression); + javaBlock.line("requestOptions.setBody(" + variableName + ");"); + } else { + javaBlock.line("requestOptions.setBody(" + expression + ");"); + } + }; + if (!parameter.getClientMethodParameter().isRequired()) { + methodBlock.ifBlock(parameter.getName() + " != null", writeLine); + } else { + writeLine.accept(methodBlock); + } + break; + } + } + } + + // invocation with protocol method parameters and RequestOptions + String invocationExpression = protocolMethod.getMethodInputParameters().stream() + .map(p -> { + String parameterName = p.getName(); + String expression = parameterExpressionsMap.get(parameterName); + IType parameterRawType = p.getRawType(); + if (isJsonMergePatchOperation && ClientModelUtil.isClientModel(parameterRawType) + && RequestParameterLocation.BODY.equals(p.getRequestParameterLocation()) + && ClientModelUtil.isJsonMergePatchModel(ClientModelUtil.getClientModel(((ClassType) parameterRawType).getName()), JavaSettings.getInstance())) { + return writeParameterConversionExpressionWithJsonMergePatchEnabled(methodBlock, parameterRawType.toString(), parameterName, expression); + } else { + return expression == null ? parameterName : expression; + } + }) + .collect(Collectors.joining(", ")); + + // write the invocation of protocol method, and related type conversion + writeInvocationAndConversion(convenienceMethod, protocolMethod, invocationExpression, methodBlock, typeReferenceStaticClasses); + } + + + /** + * Write the validation for parameters against current api-version. + * + * @param parameters the parameters + * @param methodBlock the method block + */ + protected void writeValidationForVersioning(ClientMethod convenienceMethod, Set parameters, JavaBlock methodBlock) { + // validate parameter for versioning + for (MethodParameter parameter : parameters) { + if (parameter.getClientMethodParameter().getVersioning() != null && parameter.getClientMethodParameter().getVersioning().getAdded() != null) { + String condition = String.format( + "!Arrays.asList(%s).contains(serviceClient.getServiceVersion().getVersion())", + parameter.getClientMethodParameter().getVersioning().getAdded().stream().map(ClassType.STRING::defaultValueExpression).collect(Collectors.joining(", "))); + methodBlock.ifBlock(condition, ifBlock -> { + String exceptionExpression = String.format( + "new IllegalArgumentException(\"Parameter %1$s is only available in api-version %2$s.\")", + parameter.getName(), + String.join(", ", parameter.getClientMethodParameter().getVersioning().getAdded())); + writeThrowException(convenienceMethod.getType(), exceptionExpression, ifBlock); + }); + } + } + } + + abstract void writeThrowException(ClientMethodType methodType, String exceptionExpression, JavaBlock methodBlock); + + private static boolean isGroupByTransformation(MethodTransformationDetail detail) { + return !CoreUtils.isNullOrEmpty(detail.getParameterMappings()) + && detail.getParameterMappings().iterator().next().getOutputParameterPropertyName() == null; + } + + private static void writeParameterTransformation( + MethodTransformationDetail detail, + ClientMethod convenienceMethod, ClientMethod protocolMethod, + JavaBlock methodBlock, + Map parametersMap) { + + if (isGroupByTransformation(detail)) { + // grouping + + ParameterMapping mapping = detail.getParameterMappings().iterator().next(); + ClientMethodParameter sourceParameter = mapping.getInputParameter(); + + boolean sourceParameterInMethod = false; + for (MethodParameter parameter: parametersMap.keySet()) { + if (parameter.clientMethodParameter != null && parameter.clientMethodParameter.getName() != null + && Objects.equals(parameter.clientMethodParameter.getName(), sourceParameter.getName())) { + sourceParameterInMethod = true; + break; + } + } + + if (sourceParameterInMethod) { + // null check on input parameter + String assignmentExpression = "%1$s %2$s = %3$s.%4$s();"; + if (!sourceParameter.isRequired()) { + assignmentExpression = "%1$s %2$s = %3$s == null ? null : %3$s.%4$s();"; + } + + methodBlock.line(String.format(assignmentExpression, + detail.getOutParameter().getClientType(), + detail.getOutParameter().getName(), + sourceParameter.getName(), + CodeNamer.getModelNamer().modelPropertyGetterName(mapping.getInputParameterProperty()))); + + if (detail.getOutParameter().getRequestParameterLocation() != null) { + ClientMethodParameter clientMethodParameter = detail.getOutParameter(); + ProxyMethodParameter proxyMethodParameter = convenienceMethod.getProxyMethod().getAllParameters().stream() + .filter(p -> clientMethodParameter.getName().equals(CodeNamer.getEscapedReservedClientMethodParameterName(p.getName()))) + .findFirst().orElse(null); + if (proxyMethodParameter != null) { + MethodParameter methodParameter = new MethodParameter(proxyMethodParameter, clientMethodParameter); + parametersMap.put(methodParameter, findProtocolMethodParameterForConvenienceMethod(methodParameter, protocolMethod)); + } + } + } + } else { + // flatten (possible with grouping) + ClientMethodParameter targetParameter = detail.getOutParameter(); + if (targetParameter.getWireType() == ClassType.BINARY_DATA) { + IType targetType = targetParameter.getRawType(); + + StringBuilder ctorExpression = new StringBuilder(); + StringBuilder setterExpression = new StringBuilder(); + String targetParameterName = targetParameter.getName(); + String targetParameterObjectName = targetParameterName + "Obj"; + for (ParameterMapping mapping : detail.getParameterMappings()) { + String parameterName = mapping.getInputParameter().getName(); + + String inputPath = parameterName; + boolean propertyRequired = mapping.getInputParameter().isRequired(); + if (mapping.getInputParameterProperty() != null) { + inputPath = String.format("%s.%s()", mapping.getInputParameter().getName(), + CodeNamer.getModelNamer().modelPropertyGetterName(mapping.getInputParameterProperty())); + propertyRequired = mapping.getInputParameterProperty().isRequired(); + } + if (propertyRequired) { + // required + if (JavaSettings.getInstance().isRequiredFieldsAsConstructorArgs()) { + if (ctorExpression.length() > 0) { + ctorExpression.append(", "); + } + ctorExpression.append(inputPath); + } else { + setterExpression.append(".").append(mapping.getOutputParameterProperty().getSetterName()).append("(").append(inputPath).append(")"); + } + } else if (!convenienceMethod.getOnlyRequiredParameters()) { + // optional + setterExpression.append(".").append(mapping.getOutputParameterProperty().getSetterName()).append("(").append(inputPath).append(")"); + } + } + methodBlock.line(String.format("%1$s %2$s = new %1$s(%3$s)%4$s;", targetType, targetParameterObjectName, ctorExpression, setterExpression)); + + String expression = null; + if (targetParameter.getRawType() instanceof ClassType) { + ClientModel model = ClientModelUtil.getClientModel(targetParameter.getRawType().toString()); + // serialize model for multipart/form-data + if (model != null && ClientModelUtil.isMultipartModel(model)) { + expression = expressionMultipartFormDataToBinaryData(targetParameterObjectName, model); + } + } + if (expression == null) { + expression = expressionConvertToBinaryData(targetParameterObjectName, targetParameter.getRawType(), protocolMethod.getProxyMethod().getRequestContentType()); + } + methodBlock.line(String.format("BinaryData %1$s = %2$s;", targetParameterName, expression)); + } + } + } + + protected void addImports(Set imports, List convenienceMethods) { + // methods + JavaSettings settings = JavaSettings.getInstance(); + convenienceMethods.stream().flatMap(m -> m.getConvenienceMethods().stream()) + .forEach(m -> { + m.addImportsTo(imports, false, settings); + // hack, add wire type of parameters, as they are not added in ClientMethod, even when includeImplementationImports=true + for (ClientMethodParameter p : m.getParameters()) { + p.getWireType().addImportsTo(imports, false); + + // add imports from models, as some convenience API need to process model properties + if (p.getWireType() instanceof ClassType) { + ClientModel model = ClientModelUtil.getClientModel(p.getWireType().toString()); + if (model != null) { + model.addImportsTo(imports, settings); + } + } + } + }); + + ClassType.HTTP_HEADER_NAME.addImportsTo(imports, false); + ClassType.BINARY_DATA.addImportsTo(imports, false); + ClassType.REQUEST_OPTIONS.addImportsTo(imports, false); + imports.add(Collectors.class.getName()); + imports.add(Objects.class.getName()); + imports.add(FluxUtil.class.getName()); + + // collection format + imports.add(JacksonAdapter.class.getName()); + imports.add(CollectionFormat.class.getName()); + imports.add(TypeReference.class.getName()); + if (!JavaSettings.getInstance().isBranded()) { + imports.add(Type.class.getName()); + imports.add(ParameterizedType.class.getName()); + } + + // byte[] + ClassType.BASE_64_URL.addImportsTo(imports, false); + + // flatten payload + imports.add(Map.class.getName()); + imports.add(HashMap.class.getName()); + + // MultipartFormDataHelper class + imports.add(settings.getPackage(settings.getImplementationSubpackage()) + "." + ClientModelUtil.MULTI_PART_FORM_DATA_HELPER_CLASS_NAME); + + // versioning + imports.add(Arrays.class.getName()); + + // JsonMergePatchHelper class + imports.add(settings.getPackage(settings.getImplementationSubpackage()) + "." + ClientModelUtil.JSON_MERGE_PATCH_HELPER_CLASS_NAME); + } + + protected void addGeneratedAnnotation(JavaType typeBlock) { + if (JavaSettings.getInstance().isBranded()) { + typeBlock.annotation(Annotation.GENERATED.getName()); + } else { + typeBlock.annotation(Annotation.METADATA.getName() + "(generated = true)"); + } + } + + /** + * Whether the convenience method should be included. + * + * @param method the convenience method. + * @return Whether include the convenience method. + */ + protected abstract boolean isMethodIncluded(ClientMethod method); + + /** + * Whether the convenience/protocol method should be included. + * + * @param method the convenience/protocol method. + * @return Whether include the convenience/protocol method. + */ + protected abstract boolean isMethodIncluded(ConvenienceMethod method); + + protected boolean isMethodAsync(ClientMethod method) { + return method.getType().name().contains("Async"); + } + + protected boolean isMethodVisible(ClientMethod method) { + return method.getMethodVisibility() == JavaVisibility.Public; + } + + protected String getMethodName(ClientMethod method) { + if (isMethodAsync(method)) { + return method.getName().endsWith("Async") + ? method.getName().substring(0, method.getName().length() - "Async".length()) + : method.getName(); + } else { + return method.getName(); + } + } + + /** + * Write the code of the method invocation of client method, and the conversion of parameters and return value. + * + * @param convenienceMethod the convenience method. + * @param protocolMethod the protocol method. + * @param invocationExpression the prepared expression of invocation on client method. + * @param methodBlock the code block. + */ + protected abstract void writeInvocationAndConversion( + ClientMethod convenienceMethod, ClientMethod protocolMethod, + String invocationExpression, + JavaBlock methodBlock, + Set typeReferenceStaticClasses); + + protected boolean isModelOrBuiltin(IType type) { + // TODO: other built-in types + return type == ClassType.STRING // string + || type == ClassType.OBJECT // unknown + || type == ClassType.BIG_DECIMAL // decimal + || (type instanceof PrimitiveType && type.asNullable() != ClassType.VOID) // boolean, int, float, etc. + || ClientModelUtil.isClientModel(type); // client model + } + + protected enum SupportedMimeType { + TEXT, + XML, + MULTIPART, + BINARY, + JSON; + + // azure-core SerializerEncoding.SUPPORTED_MIME_TYPES + private static final Map SUPPORTED_MIME_TYPES = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + static { + SUPPORTED_MIME_TYPES.put("text/xml", SupportedMimeType.XML); + SUPPORTED_MIME_TYPES.put("application/xml", SupportedMimeType.XML); + SUPPORTED_MIME_TYPES.put("application/json", SupportedMimeType.JSON); + SUPPORTED_MIME_TYPES.put("text/css", SupportedMimeType.TEXT); + SUPPORTED_MIME_TYPES.put("text/csv", SupportedMimeType.TEXT); + SUPPORTED_MIME_TYPES.put("text/html", SupportedMimeType.TEXT); + SUPPORTED_MIME_TYPES.put("text/javascript", SupportedMimeType.TEXT); + SUPPORTED_MIME_TYPES.put("text/plain", SupportedMimeType.TEXT); + // not in azure-core + SUPPORTED_MIME_TYPES.put("application/merge-patch+json", SupportedMimeType.JSON); + } + + public static SupportedMimeType getResponseKnownMimeType(Collection mediaTypes) { + // rfc https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2 + + // Response adds a "application/json;q=0.9" if no "application/json" specified in media types. + // This is mostly for the error response which is in JSON, and is not included in this calculation. + + for (String mediaType : mediaTypes) { + // The declared mime type can be of the form "application/json; charset=utf-8" or "application/json" + // So, we have to check if the mediaType starts with the supported mime type + if (!mediaType.equals(MethodUtil.CONTENT_TYPE_APPLICATION_JSON_ERROR_WEIGHT)) { // skip the error media type + int semicolonIndex = mediaType.indexOf(';'); + String mediaTypeWithoutParameter = semicolonIndex >= 0 ? mediaType.substring(0, semicolonIndex) : mediaType; + + SupportedMimeType type = SUPPORTED_MIME_TYPES.entrySet().stream() + .filter(supportedMimeType -> mediaTypeWithoutParameter.equals(supportedMimeType.getKey())) + .map(Map.Entry::getValue) + .findFirst() + .orElse(null); + + if (mediaTypeWithoutParameter.startsWith("application/") && mediaTypeWithoutParameter.endsWith("+json")) { + // special handling for "+json", rfc https://datatracker.ietf.org/doc/html/rfc6839#section-3.1 + type = SupportedMimeType.JSON; + + // TODO: we may need to handle "+xml" as well + } + + if (type != null) { + return type; + } + } + } + return SupportedMimeType.BINARY; // BINARY if not recognized + } + } + + private static String expressionConvertToBinaryData(String name, IType type, String mediaType) { + SupportedMimeType mimeType = SupportedMimeType.getResponseKnownMimeType(Collections.singleton(mediaType)); + switch (mimeType) { + case TEXT: + return "BinaryData.fromString(" + name + ")"; + + case BINARY: + return name; + + default: + // JSON etc. + if (type == ClassType.BINARY_DATA) { + return name; + } else { + if (type == ClassType.BASE_64_URL) { + return "BinaryData.fromObject(Base64Url.encode(" + name + "))"; + } else if (type instanceof EnumType) { + return "BinaryData.fromObject(" + name + " == null ? null : " + name + "." + ((EnumType) type).getToMethodName() + "())"; + } else { + return "BinaryData.fromObject(" + name + ")"; + } + } + } + } + + private static void writeHeader(MethodParameter parameter, JavaBlock methodBlock) { + Consumer writeLine = javaBlock -> javaBlock.line( + String.format("requestOptions.setHeader(%1$s, %2$s);", + ModelTemplateHeaderHelper.getHttpHeaderNameInstanceExpression(parameter.getSerializedName()), + expressionConvertToString(parameter.getName(), parameter.getClientMethodParameter().getWireType(), parameter.getProxyMethodParameter()))); + if (!parameter.getClientMethodParameter().isRequired()) { + methodBlock.ifBlock(String.format("%s != null", parameter.getName()), writeLine); + } else { + writeLine.accept(methodBlock); + } + } + + private static void writeQueryParam(MethodParameter parameter, JavaBlock methodBlock) { + Consumer writeLine; + if (parameter.proxyMethodParameter.getExplode() && parameter.getClientMethodParameter().getWireType() instanceof IterableType) { + // multi + IType elementType = ((IterableType) parameter.getClientMethodParameter().getWireType()).getElementType(); + String elementTypeExpression = expressionConvertToString("paramItemValue", elementType, parameter.getProxyMethodParameter()); + writeLine = javaBlock -> { + String addQueryParamLine = getAddQueryParamExpression(parameter, elementTypeExpression); + + javaBlock.line(String.format("for (%1$s paramItemValue : %2$s) {", elementType, parameter.getName())); + javaBlock.indent(() -> { + if (elementType instanceof PrimitiveType) { + javaBlock.line(addQueryParamLine); + } else { + javaBlock.ifBlock("paramItemValue != null", ifBlock -> ifBlock.line(addQueryParamLine)); + } + }); + javaBlock.line("}"); + }; + } else { + writeLine = javaBlock -> javaBlock.line( + getAddQueryParamExpression(parameter, + expressionConvertToString(parameter.getName(), parameter.getClientMethodParameter().getWireType(), parameter.getProxyMethodParameter()))); + } + if (!parameter.getClientMethodParameter().isRequired()) { + methodBlock.ifBlock(String.format("%s != null", parameter.getName()), writeLine); + } else { + writeLine.accept(methodBlock); + } + } + + private static String getAddQueryParamExpression(MethodParameter parameter, String variable) { + // TODO: generic not having 3rd parameter "encoded" + if (JavaSettings.getInstance().isBranded()) { + return String.format("requestOptions.addQueryParam(%1$s, %2$s, %3$s);", + ClassType.STRING.defaultValueExpression(parameter.getSerializedName()), + variable, + parameter.getProxyMethodParameter().getAlreadyEncoded()); + } else { + return String.format("requestOptions.addQueryParam(%1$s, %2$s);", + ClassType.STRING.defaultValueExpression(parameter.getSerializedName()), + variable); + } + } + + private static String expressionConvertToString(String name, IType type, ProxyMethodParameter parameter) { + if (type == ClassType.STRING) { + return name; + } else if (type instanceof EnumType) { + // enum + EnumType enumType = (EnumType) type; + if (enumType.getElementType() == ClassType.STRING) { + return name + ".toString()"; + } else { + return String.format("String.valueOf(%1$s.%2$s())", name, enumType.getToMethodName()); + } + } else if (type instanceof IterableType) { + if (parameter.getCollectionFormat() == CollectionFormat.MULTI && parameter.getExplode()) { + // multi, RestProxy will handle the array with "multipleQueryParams = true" + return name; + } else { + String delimiter = ClassType.STRING.defaultValueExpression(parameter.getCollectionFormat().getDelimiter()); + IType elementType = ((IterableType) type).getElementType(); + if (elementType instanceof EnumType) { + // EnumTypes should provide a toString implementation that represents the wire value. + // Circumvent the use of JacksonAdapter and handle this manually. + EnumType enumType = (EnumType) elementType; + // Not enums will be backed by Strings. Get the backing value before converting to string, this + // will prevent using the enum name rather than the enum value when it isn't a String-based + // enum. Ex, a long-based enum with value 100 called HIGH will return "100" rather than + // "HIGH". + String enumToString = enumType.getElementType() == ClassType.STRING + ? "paramItemValue" + : "paramItemValue == null ? null : paramItemValue." + enumType.getToMethodName() + "()"; + return name + ".stream()\n" + + " .map(paramItemValue -> Objects.toString(" + enumToString + ", \"\"))\n" + + " .collect(Collectors.joining(" + delimiter + "))"; + } else if (elementType == ClassType.STRING + || (elementType instanceof ClassType && ((ClassType) elementType).isBoxedType())) { + return name + ".stream()\n" + + " .map(paramItemValue -> Objects.toString(paramItemValue, \"\"))\n" + + " .collect(Collectors.joining(" + delimiter + "))"; + } else { + // Always use serializeIterable as Iterable supports both Iterable and List. + + // this logic depends on rawType of proxy method parameter be List + // alternative would be check wireType of client method parameter + IType elementWireType = parameter.getRawType() instanceof IterableType + ? ((IterableType) parameter.getRawType()).getElementType() + : elementType; + + String serializeIterableInput = name; + if (elementWireType != elementType) { + // convert List to List, if necessary + serializeIterableInput = String.format( + "%s.stream().map(paramItemValue -> %s).collect(Collectors.toList())", + name, elementWireType.convertFromClientType("paramItemValue")); + } + + // convert List to String + return String.format( + "JacksonAdapter.createDefaultSerializerAdapter().serializeIterable(%s, CollectionFormat.%s)", + serializeIterableInput, parameter.getCollectionFormat().toString().toUpperCase(Locale.ROOT)); + } + } + } else { + // primitive or date-time + String conversionExpression = type.convertFromClientType(name); + return String.format("String.valueOf(%s)", conversionExpression); + } + } + + private static String expressionConvertToType(String name, MethodParameter convenienceParameter, String mediaType) { + if (convenienceParameter.getProxyMethodParameter().getRequestParameterLocation() == RequestParameterLocation.BODY) { + IType bodyType = convenienceParameter.getProxyMethodParameter().getRawType(); + if (bodyType instanceof ClassType) { + ClientModel model = ClientModelUtil.getClientModel(bodyType.toString()); + // serialize model for multipart/form-data + if (model != null && ClientModelUtil.isMultipartModel(model)) { + return expressionMultipartFormDataToBinaryData(name, model); + } + } + return expressionConvertToBinaryData(name, convenienceParameter.getClientMethodParameter().getWireType(), mediaType); + } else { + IType type = convenienceParameter.getClientMethodParameter().getWireType(); + if (type instanceof EnumType) { + return expressionConvertToString(name, type, convenienceParameter.getProxyMethodParameter()); + } else if (type instanceof IterableType && ((IterableType) type).getElementType() instanceof EnumType) { + IType enumType = ((IterableType) type).getElementType(); + IType enumValueType = ((EnumType) enumType).getElementType().asNullable(); + if (enumValueType == ClassType.STRING) { + return String.format( + "%1$s.stream().map(paramItemValue -> Objects.toString(paramItemValue, \"\")).collect(Collectors.toList())", + name); + } else { + return String.format( + "%1$s.stream().map(paramItemValue -> paramItemValue == null ? \"\" : String.valueOf(paramItemValue.%2$s())).collect(Collectors.toList())", + name, ((EnumType) enumType).getToMethodName()); + } + } else { + return name; + } + } + } + + private static String expressionMultipartFormDataToBinaryData(String name, ClientModel model) { + BiFunction nullableExpression = (propertyExpr, expr) -> propertyExpr + " == null ? null : " + expr; + + // serialize model for multipart/form-data + StringBuilder builder = new StringBuilder().append("new MultipartFormDataHelper(requestOptions)"); + for (ClientModelProperty property : model.getProperties()) { + String propertyGetExpression = name + "." + property.getGetterName() + "()"; + if (!property.isReadOnly()) { + if (isMultipartModel(property.getWireType())) { + // file, usually application/octet-stream + + String fileExpression = propertyGetExpression + ".getContent()"; + String contentTypeExpression = propertyGetExpression + ".getContentType()"; + String filenameExpression = propertyGetExpression + ".getFilename()"; + if (!property.isRequired()) { + fileExpression = nullableExpression.apply(propertyGetExpression, fileExpression); + contentTypeExpression = nullableExpression.apply(propertyGetExpression, contentTypeExpression); + filenameExpression = nullableExpression.apply(propertyGetExpression, filenameExpression); + } + + builder.append(String.format( + ".serializeFileField(%1$s, %2$s, %3$s, %4$s)", + ClassType.STRING.defaultValueExpression(property.getSerializedName()), + fileExpression, + contentTypeExpression, + filenameExpression + )); + } else if (property.getWireType() instanceof ListType && isMultipartModel(((ListType) property.getWireType()).getElementType())) { + // file array + + // For now, we use 3 List, as we do not wish the Helper class refer to different ##FileDetails model. + // Later, if we switch to a shared class in azure-core, we can change the implementation. + String className = ((ListType) property.getWireType()).getElementType().toString(); + String streamExpressionFormat = "%1$s.stream().map(%2$s::%3$s).collect(Collectors.toList())"; + String fileExpression = String.format(streamExpressionFormat, + propertyGetExpression, className, "getContent"); + String contentTypeExpression = String.format(streamExpressionFormat, + propertyGetExpression, className, "getContentType"); + String filenameExpression = String.format(streamExpressionFormat, + propertyGetExpression, className, "getFilename"); + if (!property.isRequired()) { + fileExpression = nullableExpression.apply(propertyGetExpression, fileExpression); + contentTypeExpression = nullableExpression.apply(propertyGetExpression, contentTypeExpression); + filenameExpression = nullableExpression.apply(propertyGetExpression, filenameExpression); + } + + builder.append(String.format( + ".serializeFileFields(%1$s, %2$s, %3$s, %4$s)", + ClassType.STRING.defaultValueExpression(property.getSerializedName()), + fileExpression, + contentTypeExpression, + filenameExpression + )); + } else if (ClientModelUtil.isClientModel(property.getWireType()) + || property.getWireType() instanceof MapType + || property.getWireType() instanceof IterableType) { + // application/json + builder.append(String.format( + ".serializeJsonField(%1$s, %2$s)", + ClassType.STRING.defaultValueExpression(property.getSerializedName()), + propertyGetExpression + )); + } else { + // text/plain + String stringExpression = propertyGetExpression; + // convert to String + if (property.getWireType() instanceof PrimitiveType) { + stringExpression = String.format("String.valueOf(%s)", stringExpression); + } else if (property.getWireType() != ClassType.STRING) { + stringExpression = String.format("Objects.toString(%s)", stringExpression); + } + builder.append(String.format( + ".serializeTextField(%1$s, %2$s)", + ClassType.STRING.defaultValueExpression(property.getSerializedName()), + stringExpression + )); + } + } + } + builder.append(".end().getRequestBody()"); + return builder.toString(); + } + + private static boolean isMultipartModel(IType type) { + if (ClientModelUtil.isClientModel(type)) { + return ClientModelUtil.isMultipartModel(ClientModelUtil.getClientModel(type.toString())); + } else { + return false; + } + } + + private static Map findParametersForConvenienceMethod( + ClientMethod convenienceMethod, ClientMethod protocolMethod) { + Map parameterMap = new LinkedHashMap<>(); + List convenienceParameters = getParameters(convenienceMethod, true); + Map clientParameters = getParameters(protocolMethod, false).stream() + .collect(Collectors.toMap(MethodParameter::getSerializedName, Function.identity())); + for (MethodParameter convenienceParameter : convenienceParameters) { + String name = convenienceParameter.getSerializedName(); + parameterMap.put(convenienceParameter, clientParameters.get(name)); + } + return parameterMap; + } + + private static MethodParameter findProtocolMethodParameterForConvenienceMethod( + MethodParameter parameter, ClientMethod protocolMethod) { + List protocolParameters = getParameters(protocolMethod, false); + return protocolParameters.stream().filter(p -> Objects.equals(parameter.getSerializedName(), p.getSerializedName())).findFirst().orElse(null); + } + + private static List getParameters(ClientMethod method, boolean useAllParameters) { + List proxyMethodParameters = useAllParameters ? method.getProxyMethod().getAllParameters() : method.getProxyMethod().getParameters(); + Map proxyMethodParameterByClientParameterName = proxyMethodParameters.stream() + .collect(Collectors.toMap(p -> CodeNamer.getEscapedReservedClientMethodParameterName(p.getName()), Function.identity())); + return method.getMethodInputParameters().stream() + .filter(p -> !p.isConstant() && !p.isFromClient()) + .map(p -> new MethodParameter(proxyMethodParameterByClientParameterName.get(p.getName()), p)) + .collect(Collectors.toList()); + } + + /** + * Writes the expression to convert a convenience parameter to a protocol parameter and wrap it in JsonMergePatchHelper. + * @param javaBlock + * @param convenientParameterTypeName + * @param convenientParameterName + * @param expression + * @return the name of the variable that holds the converted parameter + */ + private static String writeParameterConversionExpressionWithJsonMergePatchEnabled(JavaBlock javaBlock, String convenientParameterTypeName, String convenientParameterName, String expression) { + String variableName = convenientParameterName + "InBinaryData"; + javaBlock.line(String.format("JsonMergePatchHelper.get%1$sAccessor().prepareModelForJsonMergePatch(%2$s, true);", convenientParameterTypeName, convenientParameterName)); + javaBlock.line("BinaryData " + variableName + " = " + expression + ";"); + javaBlock.line("// BinaryData.fromObject() will not fire serialization, use getLength() to fire serialization."); + javaBlock.line(variableName + ".getLength();"); + javaBlock.line(String.format("JsonMergePatchHelper.get%1$sAccessor().prepareModelForJsonMergePatch(%2$s, false);", convenientParameterTypeName, convenientParameterName)); + return variableName; + } + + protected static class MethodParameter { + + private final ProxyMethodParameter proxyMethodParameter; + private final ClientMethodParameter clientMethodParameter; + + public MethodParameter(ProxyMethodParameter proxyMethodParameter, ClientMethodParameter clientMethodParameter) { + this.proxyMethodParameter = proxyMethodParameter; + this.clientMethodParameter = clientMethodParameter; + } + + public ProxyMethodParameter getProxyMethodParameter() { + return proxyMethodParameter; + } + + public ClientMethodParameter getClientMethodParameter() { + return clientMethodParameter; + } + + public String getName() { + return this.getClientMethodParameter().getName(); + } + + public String getSerializedName() { + if (this.getProxyMethodParameter() == null) { + return null; + } else { + String name = this.getProxyMethodParameter().getRequestParameterName(); + if (name == null && this.getProxyMethodParameter().getRequestParameterLocation() == RequestParameterLocation.BODY) { + name = "__internal_request_BODY"; + } + return name; + } + } + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/ConvenienceSyncMethodTemplate.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/ConvenienceSyncMethodTemplate.java new file mode 100644 index 0000000000..8f3b3106fc --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/ConvenienceSyncMethodTemplate.java @@ -0,0 +1,253 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.template; + +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ArrayType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClassType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethod; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethodParameter; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethodType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ConvenienceMethod; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.EnumType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.GenericType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.IType; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaBlock; +import com.microsoft.typespec.http.client.generator.core.util.TemplateUtil; +import com.azure.core.http.rest.PagedIterable; +import com.azure.core.http.rest.ResponseBase; +import com.azure.core.util.CoreUtils; + +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +public class ConvenienceSyncMethodTemplate extends ConvenienceMethodTemplateBase { + + private static final ConvenienceSyncMethodTemplate INSTANCE = new ConvenienceSyncMethodTemplate(); + private static final String ASYNC_CLIENT_VAR_NAME = "client"; + + protected ConvenienceSyncMethodTemplate() { + } + + public static ConvenienceSyncMethodTemplate getInstance() { + return INSTANCE; + } + + public void addImports(Set imports, List convenienceMethods) { + if (!CoreUtils.isNullOrEmpty(convenienceMethods)) { + super.addImports(imports, convenienceMethods); + } + + if (JavaSettings.getInstance().isUseClientLogger()) { + ClassType.CLIENT_LOGGER.addImportsTo(imports, false); + } + } + + @Override + protected boolean isMethodIncluded(ClientMethod method) { + return !isMethodAsync(method) && isMethodVisible(method) && !method.isImplementationOnly(); + } + + @Override + protected boolean isMethodIncluded(ConvenienceMethod method) { + return !isMethodAsync(method.getProtocolMethod()) && isMethodVisible(method.getProtocolMethod()) + // for LRO, we actually choose the protocol method of "WithModel" + && (method.getProtocolMethod().getType() != ClientMethodType.LongRunningBeginSync || (method.getProtocolMethod().getImplementationDetails() != null && method.getProtocolMethod().getImplementationDetails().isImplementationOnly())); + } + + @Override + protected void writeMethodImplementation( + ClientMethod protocolMethod, + ClientMethod convenienceMethod, + JavaBlock methodBlock, + Set typeReferenceStaticClasses) { + if (!JavaSettings.getInstance().isSyncStackEnabled()) { + if (protocolMethod.getType() == ClientMethodType.PagingSync) { + // Call the convenience method from async client + // It would need rework, when underlying sync method in Impl is switched to sync protocol method + + String methodInvoke = "new PagedIterable<>(" + getMethodInvokeViaAsyncClient(convenienceMethod) + ")"; + + methodBlock.methodReturn(methodInvoke); + } else if (protocolMethod.getType() == ClientMethodType.LongRunningBeginSync) { + // Call the convenience method from async client + String methodInvoke = getMethodInvokeViaAsyncClient(convenienceMethod) + ".getSyncPoller()"; + + methodBlock.methodReturn(methodInvoke); + } else { + super.writeMethodImplementation(protocolMethod, convenienceMethod, methodBlock, typeReferenceStaticClasses); + } + } else { + super.writeMethodImplementation(protocolMethod, convenienceMethod, methodBlock, typeReferenceStaticClasses); + } + } + + @Override + protected void writeInvocationAndConversion( + ClientMethod convenienceMethod, + ClientMethod protocolMethod, + String invocationExpression, + JavaBlock methodBlock, + Set typeReferenceStaticClasses) { + + IType responseBodyType = getResponseBodyType(convenienceMethod); + IType protocolResponseBodyType = getResponseBodyType(protocolMethod); + IType rawResponseBodyType = convenienceMethod.getProxyMethod().getRawResponseBodyType(); + + String convertFromResponse = convenienceMethod.getType() == ClientMethodType.SimpleSyncRestResponse + ? "" : ".getValue()"; + + if (convenienceMethod.getType() == ClientMethodType.PagingSync) { + methodBlock.methodReturn(String.format( + "serviceClient.%1$s(%2$s).mapPage(bodyItemValue -> %3$s)", + protocolMethod.getName(), + invocationExpression, + expressionConvertFromBinaryData( + responseBodyType, rawResponseBodyType, "bodyItemValue", + protocolMethod.getProxyMethod().getResponseContentTypes(), + typeReferenceStaticClasses))); + } else if (convenienceMethod.getType() == ClientMethodType.LongRunningBeginSync){ + String methodName = protocolMethod.getName(); + methodBlock.methodReturn(String.format("serviceClient.%1$s(%2$s)", methodName, invocationExpression)); + } else if (convenienceMethod.getType() == ClientMethodType.SimpleSyncRestResponse + && !(responseBodyType.asNullable() == ClassType.VOID || responseBodyType == ClassType.BINARY_DATA)) { + + // protocolMethodResponse = ... + methodBlock.line(getProtocolMethodResponseStatement(protocolMethod, invocationExpression)); + + // e.g. protocolMethodResponse.getValue().toObject(...) + String expressConversion = "protocolMethodResponse.getValue()"; + if (protocolResponseBodyType == ClassType.BINARY_DATA) { + expressConversion = expressionConvertFromBinaryData( + responseBodyType, rawResponseBodyType, expressConversion, + protocolMethod.getProxyMethod().getResponseContentTypes(), + typeReferenceStaticClasses); + } + + if (isResponseBase(convenienceMethod.getReturnValue().getType())) { + IType headerType = ((GenericType) convenienceMethod.getReturnValue().getType()).getTypeArguments()[0]; + methodBlock.methodReturn(String.format( + "new ResponseBase<>(protocolMethodResponse.getRequest(), protocolMethodResponse.getStatusCode(), protocolMethodResponse.getHeaders(), %1$s, new %2$s(protocolMethodResponse.getHeaders()))", expressConversion, headerType)); + } else { + methodBlock.methodReturn(String.format( + "new SimpleResponse<>(protocolMethodResponse, %s)", expressConversion)); + } + } else { + String statement = String.format("%1$s(%2$s)%3$s", + getMethodName(protocolMethod), + invocationExpression, + convertFromResponse); + if (protocolResponseBodyType == ClassType.BINARY_DATA) { + statement = expressionConvertFromBinaryData( + responseBodyType, rawResponseBodyType, statement, + protocolMethod.getProxyMethod().getResponseContentTypes(), + typeReferenceStaticClasses); + } + if (convenienceMethod.getType() == ClientMethodType.SimpleSyncRestResponse) { + if (isResponseBase(convenienceMethod.getReturnValue().getType())) { + IType headerType = ((GenericType) convenienceMethod.getReturnValue().getType()).getTypeArguments()[0]; + + methodBlock.line(getProtocolMethodResponseStatement(protocolMethod, invocationExpression)); + + methodBlock.methodReturn(String.format( + "new ResponseBase<>(protocolMethodResponse.getRequest(), protocolMethodResponse.getStatusCode(), protocolMethodResponse.getHeaders(), null, new %1$s(protocolMethodResponse.getHeaders()))", headerType)); + } else { + methodBlock.methodReturn(statement); + } + } else if (responseBodyType.asNullable() == ClassType.VOID) { + methodBlock.line(statement + ";"); + } else { + methodBlock.methodReturn(statement); + } + } + } + + @Override + protected void writeThrowException(ClientMethodType methodType, String exceptionExpression, JavaBlock methodBlock) { + if (JavaSettings.getInstance().isUseClientLogger()) { + methodBlock.line(String.format("throw LOGGER.atError().log(%s);", exceptionExpression)); + } else { + methodBlock.line(String.format("throw %s;", exceptionExpression)); + } + } + + private static String getMethodInvokeViaAsyncClient(ClientMethod convenienceMethod) { + List parameterNames = convenienceMethod.getMethodInputParameters().stream() + .map(ClientMethodParameter::getName).collect(Collectors.toList()); + + return String.format("%1$s.%2$s(%3$s)", + ASYNC_CLIENT_VAR_NAME, convenienceMethod.getName(), String.join(", ", parameterNames)); + } + + private String getProtocolMethodResponseStatement(ClientMethod protocolMethod, String invocationExpression) { + String statement = String.format("%1$s(%2$s)", + getMethodName(protocolMethod), + invocationExpression); + + return String.format( + "%1$s protocolMethodResponse = %2$s;", + protocolMethod.getReturnValue().getType(), statement); + } + + private IType getResponseBodyType(ClientMethod method) { + // no need to care about LRO + IType type = method.getReturnValue().getType(); + if (type instanceof GenericType + && ( + ClassType.RESPONSE.getName().equals(((GenericType) type).getName()) + || (PagedIterable.class.getSimpleName().equals(((GenericType) type).getName())))) { + type = ((GenericType) type).getTypeArguments()[0]; + } else if (isResponseBase(type)) { + // TODO: ResponseBase is not in use, hence it may have bug + type = ((GenericType) type).getTypeArguments()[1]; + } + return type; + } + + private boolean isResponseBase(IType type) { + return type instanceof GenericType && ResponseBase.class.getSimpleName().equals(((GenericType) type).getName()); + } + + private String expressionConvertFromBinaryData(IType responseBodyType, IType rawType, String invocationExpression, + Set mediaTypes, + Set typeReferenceStaticClasses) { + SupportedMimeType mimeType = SupportedMimeType.getResponseKnownMimeType(mediaTypes); + // TODO (weidxu): support XML etc. + switch (mimeType) { + case TEXT: + return String.format("%s.toString()", invocationExpression); + + case BINARY: + return invocationExpression; + + default: + // JSON etc. + if (responseBodyType instanceof EnumType) { + // enum + IType elementType = ((EnumType) responseBodyType).getElementType(); + return String.format("%1$s.from%2$s(%3$s.toObject(%2$s.class))", responseBodyType, elementType, invocationExpression); + } else if (responseBodyType instanceof GenericType) { + // generic, e.g. List, Map + typeReferenceStaticClasses.add((GenericType) responseBodyType); + return String.format("%2$s.toObject(%1$s)", TemplateUtil.getTypeReferenceCreation(responseBodyType), invocationExpression); + } else if (responseBodyType == ClassType.BINARY_DATA) { + // BinaryData + return invocationExpression; + } else if (isModelOrBuiltin(responseBodyType)) { + // class + return String.format("%2$s.toObject(%1$s.class)", responseBodyType.asNullable(), invocationExpression); + } else if (responseBodyType == ArrayType.BYTE_ARRAY) { + // byte[] + if (rawType == ClassType.BASE_64_URL) { + return String.format("%1$s.toObject(Base64Url.class).decodedBytes()", invocationExpression); + } else { + return String.format("%1$s.toObject(byte[].class)", invocationExpression); + } + } else { + return invocationExpression; + } + } + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/DefaultTemplateFactory.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/DefaultTemplateFactory.java new file mode 100644 index 0000000000..6db4a93a9b --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/DefaultTemplateFactory.java @@ -0,0 +1,142 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.template; + +public class DefaultTemplateFactory implements TemplateFactory { + @Override + public ServiceClientInterfaceTemplate getServiceClientInterfaceTemplate() { + return ServiceClientInterfaceTemplate.getInstance(); + } + + @Override + public ServiceClientTemplate getServiceClientTemplate() { + return ServiceClientTemplate.getInstance(); + } + + @Override + public ServiceClientBuilderTemplate getServiceClientBuilderTemplate() { + return ServiceClientBuilderTemplate.getInstance(); + } + + @Override + public ServiceVersionTemplate getServiceVersionTemplate() { + return ServiceVersionTemplate.getInstance(); + } + + @Override + public MethodGroupInterfaceTemplate getMethodGroupInterfaceTemplate() { + return MethodGroupInterfaceTemplate.getInstance(); + } + + @Override + public MethodGroupTemplate getMethodGroupTemplate() { + return MethodGroupTemplate.getInstance(); + } + + @Override + public ProxyTemplate getProxyTemplate() { + return ProxyTemplate.getInstance(); + } + + @Override + public ClientMethodTemplate getClientMethodTemplate() { + return ClientMethodTemplate.getInstance(); + } + + @Override + public ModelTemplate getModelTemplate() { + return ModelTemplate.getInstance(); + } + + @Override + public StreamSerializationModelTemplate getStreamStyleModelTemplate() { + return StreamSerializationModelTemplate.getInstance(); + } + + @Override + public ExceptionTemplate getExceptionTemplate() { + return ExceptionTemplate.getInstance(); + } + + @Override + public EnumTemplate getEnumTemplate() { + return EnumTemplate.getInstance(); + } + + @Override + public ResponseTemplate getResponseTemplate() { + return ResponseTemplate.getInstance(); + } + + @Override + public XmlSequenceWrapperTemplate getXmlSequenceWrapperTemplate() { + return XmlSequenceWrapperTemplate.getInstance(); + } + + @Override + public PackageInfoTemplate getPackageInfoTemplate() { + return PackageInfoTemplate.getInstance(); + } + + @Override + public ServiceAsyncClientTemplate getServiceAsyncClientTemplate() { + return ServiceAsyncClientTemplate.getInstance(); + } + + @Override + public ServiceSyncClientTemplate getServiceSynClientTemplate() { + return ServiceSyncClientTemplate.getInstance(); + } + + @Override + public ServiceSyncClientTemplate getServiceSyncClientWrapAsyncClientTemplate() { + return ServiceSyncClientWrapAsyncClientTemplate.getInstance(); + } + + @Override + public WrapperClientMethodTemplate getWrapperClientMethodTemplate() { + return WrapperClientMethodTemplate.getInstance(); + } + + @Override + public PomTemplate getPomTemplate() { + return PomTemplate.getInstance(); + } + + @Override + public ModuleInfoTemplate getModuleInfoTemplate() { + return ModuleInfoTemplate.getInstance(); + } + + @Override + public ProtocolSampleTemplate getProtocolSampleTemplate() { + return ProtocolSampleTemplate.getInstance(); + } + + @Override + public ConvenienceAsyncMethodTemplate getConvenienceAsyncMethodTemplate() { + return ConvenienceAsyncMethodTemplate.getInstance(); + } + + @Override + public ConvenienceSyncMethodTemplate getConvenienceSyncMethodTemplate() { + return ConvenienceSyncMethodTemplate.getInstance(); + } + + @Override + public UnionModelTemplate getUnionModelTemplate() { + return UnionModelTemplate.getInstance(); + } + + @Override + public ClientMethodSampleTemplate getClientMethodSampleTemplate() { + return ClientMethodSampleTemplate.getInstance(); + } + + @Override + public JsonMergePatchHelperTemplate getJsonMergePatchHelperTemplate() { + return JsonMergePatchHelperTemplate.getInstance(); + } + +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/EnumTemplate.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/EnumTemplate.java new file mode 100644 index 0000000000..39a229a3ab --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/EnumTemplate.java @@ -0,0 +1,328 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.template; + +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.Annotation; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClassType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientEnumValue; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.EnumType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.IType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.PrimitiveType; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaContext; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaEnum; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaFile; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaJavadocComment; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaModifier; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaVisibility; +import com.microsoft.typespec.http.client.generator.core.util.CodeNamer; +import com.azure.core.util.CoreUtils; + +import java.util.HashSet; +import java.util.Set; + +/** + * Writes a EnumType to a JavaFile. + */ +public class EnumTemplate implements IJavaTemplate { + private static final EnumTemplate INSTANCE = new EnumTemplate(); + + protected EnumTemplate() { + } + + public static EnumTemplate getInstance() { + return INSTANCE; + } + + public final void write(EnumType enumType, JavaFile javaFile) { + JavaSettings settings = JavaSettings.getInstance(); + + if (enumType.getExpandable()) { + if(settings.isBranded()) { + writeExpandableStringEnum(enumType, javaFile, settings); + } else { + writeExpandableStringEnumInterface(enumType, javaFile, settings); + } + } else { + writeEnum(enumType, javaFile, settings); + } + } + + private void writeExpandableStringEnumInterface(EnumType enumType, JavaFile javaFile, JavaSettings settings) { + Set imports = new HashSet<>(); + imports.add("java.util.Collection"); + imports.add("java.util.concurrent.ConcurrentHashMap"); + imports.add("java.util.Map"); + imports.add(getStringEnumImport()); + if (!settings.isStreamStyleSerialization()) { + imports.add("com.fasterxml.jackson.annotation.JsonCreator"); + } + + addGeneratedImport(imports); + + javaFile.declareImport(imports); + javaFile.javadocComment(comment -> comment.description(enumType.getDescription())); + + String enumName = enumType.getName(); + IType elementType = enumType.getElementType(); + String typeName = elementType.getClientType().toString(); + String pascalTypeName = CodeNamer.toPascalCase(typeName); + String declaration = enumName + " implements ExpandableEnum<" + pascalTypeName + ">"; + + javaFile.publicFinalClass(declaration, classBlock -> { + classBlock.privateStaticFinalVariable("Map VALUES = new ConcurrentHashMap<>()"); + + for (ClientEnumValue enumValue : enumType.getValues()) { + String value = enumValue.getValue(); + classBlock.javadocComment(CoreUtils.isNullOrEmpty(enumValue.getDescription()) + ? "Static value " + value + " for " + enumName + "." + : enumValue.getDescription()); + addGeneratedAnnotation(classBlock); + classBlock.publicStaticFinalVariable(String.format("%1$s %2$s = from%3$s(%4$s)", enumName, + enumValue.getName(), pascalTypeName, elementType.defaultValueExpression(value))); + } + + classBlock.variable("String name", JavaVisibility.Private, JavaModifier.Final); + classBlock.privateConstructor(enumName + "(String name)", ctor -> { + ctor.line("this.name = name;"); + }); + + // fromString(typeName) + classBlock.javadocComment(comment -> { + comment.description("Creates or finds a " + enumName); + comment.param("name", "a name to look for"); + comment.methodReturns("the corresponding " + enumName); + }); + + addGeneratedAnnotation(classBlock); + if (!settings.isStreamStyleSerialization()) { + classBlock.annotation("JsonCreator"); + } + + classBlock.publicStaticMethod(String.format("%1$s from%2$s(%3$s name)", enumName, pascalTypeName, typeName), + function -> { + function.ifBlock("name == null", ifAction -> ifAction.methodReturn("null")); + function.line(enumName + " value = VALUES.get(name);"); + function.ifBlock("value != null", ifAction -> { + ifAction.line("return value;"); + }); + function.methodReturn("VALUES.computeIfAbsent(name, key -> new " + enumName + "(key))"); + }); + + // getValue + classBlock.javadocComment(comment -> { + comment.description("Gets the value of the " + enumName + " instance."); + comment.methodReturns("the value of the " + enumName + " instance."); + }); + + addGeneratedAnnotation(classBlock); + classBlock.annotation("Override"); + classBlock.publicMethod(pascalTypeName + " getValue()", + function -> function.methodReturn("this.name")); + + addGeneratedAnnotation(classBlock); + classBlock.annotation("Override"); + classBlock.method(JavaVisibility.Public, null, "String toString()", function -> function.methodReturn("name")); + }); + } + + private void writeExpandableStringEnum(EnumType enumType, JavaFile javaFile, JavaSettings settings) { + Set imports = new HashSet<>(); + imports.add("java.util.Collection"); + imports.add(getStringEnumImport()); + if (!settings.isStreamStyleSerialization()) { + imports.add("com.fasterxml.jackson.annotation.JsonCreator"); + } + + addGeneratedImport(imports); + + javaFile.declareImport(imports); + javaFile.javadocComment(comment -> comment.description(enumType.getDescription())); + + String enumName = enumType.getName(); + String declaration = enumName + " extends ExpandableStringEnum<" + enumName + ">"; + + javaFile.publicFinalClass(declaration, classBlock -> { + IType elementType = enumType.getElementType(); + String typeName = elementType.getClientType().toString(); + String pascalTypeName = CodeNamer.toPascalCase(typeName); + for (ClientEnumValue enumValue : enumType.getValues()) { + String value = enumValue.getValue(); + classBlock.javadocComment(CoreUtils.isNullOrEmpty(enumValue.getDescription()) + ? "Static value " + value + " for " + enumName + "." + : enumValue.getDescription()); + addGeneratedAnnotation(classBlock); + classBlock.publicStaticFinalVariable(String.format("%1$s %2$s = from%3$s(%4$s)", enumName, + enumValue.getName(), pascalTypeName, elementType.defaultValueExpression(value))); + } + + // ctor, marked as Deprecated + classBlock.javadocComment(comment -> { + comment.description("Creates a new instance of " + enumName + " value."); + comment.deprecated(String.format("Use the {@link #from%1$s(%2$s)} factory method.", pascalTypeName, typeName)); + }); + + addGeneratedAnnotation(classBlock); + classBlock.annotation("Deprecated"); + classBlock.publicConstructor(enumName + "()", ctor -> { }); + + // fromString(typeName) + classBlock.javadocComment(comment -> { + comment.description("Creates or finds a " + enumName + " from its string representation."); + comment.param("name", "a name to look for"); + comment.methodReturns("the corresponding " + enumName); + }); + + addGeneratedAnnotation(classBlock); + if (!settings.isStreamStyleSerialization()) { + classBlock.annotation("JsonCreator"); + } + + classBlock.publicStaticMethod(String.format("%1$s from%2$s(%3$s name)", enumName, pascalTypeName, typeName), + function -> { + String stringValue = (ClassType.STRING.equals(elementType)) ? "name" : "String.valueOf(name)"; + function.methodReturn("fromString(" + stringValue + ", " + enumName + ".class)"); + }); + + // values() + classBlock.javadocComment(comment -> { + comment.description("Gets known " + enumName + " values."); + comment.methodReturns("known " + enumName + " values"); + }); + + addGeneratedAnnotation(classBlock); + classBlock.publicStaticMethod("Collection<" + enumName + "> values()", + function -> function.methodReturn("values(" + enumName + ".class)")); + }); + } + + private void writeEnum(EnumType enumType, JavaFile javaFile, JavaSettings settings) { + Set imports = new HashSet<>(); + if (!settings.isStreamStyleSerialization()) { + imports.add("com.fasterxml.jackson.annotation.JsonCreator"); + imports.add("com.fasterxml.jackson.annotation.JsonValue"); + } + + addGeneratedImport(imports); + IType elementType = enumType.getElementType(); + elementType.getClientType().addImportsTo(imports, false); + + javaFile.declareImport(imports); + javaFile.javadocComment(comment -> comment.description(enumType.getDescription())); + String declaration = enumType.getName(); + + javaFile.publicEnum(declaration, enumBlock -> { + for (ClientEnumValue value : enumType.getValues()) { + enumBlock.value(value.getName(), value.getValue(), value.getDescription(), elementType); + } + + String enumName = enumType.getName(); + String typeName = elementType.getClientType().toString(); + + // This will be 'from*'. + String converterName = enumType.getFromMethodName(); + + enumBlock.javadocComment("The actual serialized value for a " + enumName + " instance."); + enumBlock.privateFinalMemberVariable(typeName, "value"); + + enumBlock.constructor(enumName + "(" +typeName + " value)", constructor -> constructor.line("this.value = value;")); + + enumBlock.javadocComment((comment) -> { + comment.description("Parses a serialized value to a " + enumName + " instance."); + comment.param("value", "the serialized value to parse."); + comment.methodReturns("the parsed " + enumName + " object, or null if unable to parse."); + }); + + if (!settings.isStreamStyleSerialization()) { + enumBlock.annotation("JsonCreator"); + } + + enumBlock.publicStaticMethod(enumName + " " + converterName + "(" + typeName + " value)", function -> { + if (elementType.isNullable()) { + function.ifBlock("value == null", ifAction -> ifAction.methodReturn("null")); + } + function.line(enumName + "[] items = " + enumName + ".values();"); + function.block("for (" + enumName + " item : items)", foreachBlock -> + foreachBlock.ifBlock(createEnumJsonCreatorIfCheck(enumType), ifBlock -> ifBlock.methodReturn("item"))); + function.methodReturn("null"); + }); + + if (elementType == ClassType.STRING) { + enumBlock.javadocComment(JavaJavadocComment::inheritDoc); + if (!settings.isStreamStyleSerialization()) { + enumBlock.annotation("JsonValue"); + } + enumBlock.annotation("Override"); + } else { + enumBlock.javadocComment(comment -> { + comment.description("De-serializes the instance to " + elementType + " value."); + comment.methodReturns("the " + elementType + " value"); + }); + + if (!settings.isStreamStyleSerialization()) { + enumBlock.annotation("JsonValue"); + } + } + enumBlock.publicMethod(typeName + " " + enumType.getToMethodName() + "()", + function -> function.methodReturn("this.value")); + }); + } + + protected String getStringEnumImport() { + return ClassType.EXPANDABLE_STRING_ENUM.getFullName(); + } + + /** + * Creates the if check used by the JsonCreator method used in the Enum type. + * + * @param enumType The enum type. + * @return The JsonCreator if check. + */ + protected String createEnumJsonCreatorIfCheck(EnumType enumType) { + IType enumElementType = enumType.getElementType(); + String toJsonMethodName = enumType.getToMethodName(); + + if (enumElementType == PrimitiveType.FLOAT) { + return String.format("Float.floatToIntBits(item.%s()) == Float.floatToIntBits(value)", toJsonMethodName); + } else if (enumElementType == PrimitiveType.DOUBLE) { + return String.format("Double.doubleToLongBits(item.%s()) == Double.doubleToLongBits(value)", toJsonMethodName); + } else if (enumElementType instanceof PrimitiveType) { + return String.format("item.%s() == value", toJsonMethodName); + } else if (enumElementType == ClassType.STRING) { + return String.format("item.%s().equalsIgnoreCase(value)", toJsonMethodName); + } else { + return String.format("item.%s().equals(value)", toJsonMethodName); + } + } + + protected void addGeneratedImport(Set imports) { + if (JavaSettings.getInstance().isDataPlaneClient()) { + if (JavaSettings.getInstance().isBranded()) { + Annotation.GENERATED.addImportsTo(imports); + } else { + Annotation.METADATA.addImportsTo(imports); + } + } + } + + protected void addGeneratedAnnotation(JavaContext classBlock) { + if (JavaSettings.getInstance().isDataPlaneClient()) { + if (JavaSettings.getInstance().isBranded()) { + classBlock.annotation(Annotation.GENERATED.getName()); + } else { + classBlock.annotation(Annotation.METADATA.getName() + "(generated = true)"); + } + } + } + + protected void addGeneratedAnnotation(JavaEnum enumBlock) { + if (JavaSettings.getInstance().isDataPlaneClient()) { + if (JavaSettings.getInstance().isBranded()) { + enumBlock.annotation(Annotation.GENERATED.getName()); + } else { + enumBlock.annotation(Annotation.METADATA.getName() + "(generated = true)"); + } + } + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/ExceptionTemplate.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/ExceptionTemplate.java new file mode 100644 index 0000000000..61982e1cfe --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/ExceptionTemplate.java @@ -0,0 +1,73 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.template; + + +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientException; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaFile; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaJavadocComment; + +import java.util.HashSet; +import java.util.Set; + +/** + * Writes a ClientException to a JavaFile. + */ +public class ExceptionTemplate implements IJavaTemplate { + private static final ExceptionTemplate INSTANCE = new ExceptionTemplate(); + + protected ExceptionTemplate() { + } + + public static ExceptionTemplate getInstance() { + return INSTANCE; + } + + public final void write(ClientException exception, JavaFile javaFile) { + Set imports = new HashSet<>(); + imports.add(getHttpResponseImport()); + exception.getParentType().addImportsTo(imports, false); + javaFile.declareImport(imports); + javaFile.javadocComment((comment) -> + { + comment.description(String.format("Exception thrown for an invalid response with %1$s information.", exception.getErrorName())); + }); + javaFile.publicFinalClass(String.format("%1$s extends %2$s", exception.getName(), exception.getParentType().toString()), (classBlock) -> + { + classBlock.javadocComment((comment) -> + { + comment.description(String.format("Initializes a new instance of the %1$s class.", exception.getName())); + comment.param("message", "the exception message or the response content if a message is not available"); + comment.param("response", "the HTTP response"); + }); + classBlock.publicConstructor(String.format("%1$s(String message, HttpResponse response)", exception.getName()), (constructorBlock) -> + { + constructorBlock.line("super(message, response);"); + }); + + classBlock.javadocComment((comment) -> + { + comment.description(String.format("Initializes a new instance of the %1$s class.", exception.getName())); + comment.param("message", "the exception message or the response content if a message is not available"); + comment.param("response", "the HTTP response"); + comment.param("value", "the deserialized response value"); + }); + classBlock.publicConstructor(String.format("%1$s(String message, HttpResponse response, %2$s value)", exception.getName(), exception.getErrorName()), (constructorBlock) -> + { + constructorBlock.line("super(message, response, value);"); + }); + + classBlock.javadocComment(JavaJavadocComment::inheritDoc); + classBlock.annotation("Override"); + classBlock.publicMethod(String.format("%1$s getValue()", exception.getErrorName()), (methodBlock) -> + { + methodBlock.methodReturn(String.format("(%1$s) super.getValue()", exception.getErrorName())); + }); + }); + } + + protected String getHttpResponseImport() { + return "com.azure.core.http.HttpResponse"; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/IJavaTemplate.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/IJavaTemplate.java new file mode 100644 index 0000000000..bbe59236d5 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/IJavaTemplate.java @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.template; + +/** + * Writes a Client Model of type ModelT to a Java syntax context. + */ +public interface IJavaTemplate { + void write(ModelT model, ContextT context); +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/IXmlTemplate.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/IXmlTemplate.java new file mode 100644 index 0000000000..dc2541dd19 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/IXmlTemplate.java @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.template; + +/** + * Writes a Client Model of type ModelT to a Java syntax context. + */ +public interface IXmlTemplate { + void write(ModelT model, ContextT context); +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/JsonMergePatchHelperTemplate.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/JsonMergePatchHelperTemplate.java new file mode 100644 index 0000000000..f5b39268e1 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/JsonMergePatchHelperTemplate.java @@ -0,0 +1,142 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.template; + +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientModel; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientModelProperty; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaClass; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaFile; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaVisibility; +import com.microsoft.typespec.http.client.generator.core.util.ClientModelUtil; +import com.microsoft.typespec.http.client.generator.core.util.CodeNamer; +import com.azure.core.util.CoreUtils; + +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Set; +import java.util.stream.Collectors; + +public class JsonMergePatchHelperTemplate implements IJavaTemplate, JavaFile>{ + + private static final JsonMergePatchHelperTemplate INSTANCE = new JsonMergePatchHelperTemplate(); + + protected JsonMergePatchHelperTemplate() { + } + + public static JsonMergePatchHelperTemplate getInstance() { + return INSTANCE; + } + + @Override + public void write(List models, JavaFile javaFile) { + // imports + JavaSettings settings = JavaSettings.getInstance(); + Set imports = new HashSet<>(); + addImports(imports, models, settings); + javaFile.declareImport(imports); + + // class javadoc + javaFile.javadocComment(comment -> + comment.description("This is the Helper class to enable json merge patch serialization for a model")); + // class code + javaFile.publicClass(null, ClientModelUtil.JSON_MERGE_PATCH_HELPER_CLASS_NAME, + javaClass -> createJsonMergePatchAccessHelpers(models, javaClass)); + } + + /** + * Add imports for JsonMergePatchHelper. + * + * @param imports Set of imports to add to. + * @param models List of models in the service that are used in json-merge-patch. + * @param settings JavaSettings to use. + */ + private static void addImports(Set imports, List models, JavaSettings settings) { + if (models != null && !models.isEmpty()) { + models.forEach(model -> model.addImportsTo(imports, settings)); + } + } + + /** + * Creates the access helpers for the json-merge-patch models in a service. + *

+ * This will create the accessor property, interface, and method for each model that is used in json-merge-patch. + * Instead of following standard patterns where all fields are declared together, the field, interface, and static + * methods for each model are declared together. + * + * @param models List of models in the service that are used in json-merge-patch. + * @param javaClass JavaClass to add accessor properties. + */ + private static void createJsonMergePatchAccessHelpers(List models, JavaClass javaClass) { + if (models == null || models.isEmpty()) { + return; + } + + for (ClientModel model : models) { + if (!model.getImplementationDetails().isInput()) { + // Model is only used as output and doesn't need to support json-merge-patch. + continue; + } + + if (model.isPolymorphic() && CoreUtils.isNullOrEmpty(model.getDerivedModels())) { + // Only polymorphic parent models generate an accessor. + // If it is the super most parent model, it will generate the prepareModelForJsonMergePatch method. + // Other parents need to generate setters for the properties that are used in json-merge-patch, used in + // deserialization to prevent these properties from always being included in serialization. + continue; + } + + List setterProperties = model.getProperties().stream() + .filter(property -> !property.isConstant() && !property.isPolymorphicDiscriminator()) + .collect(Collectors.toList()); + + if (!CoreUtils.isNullOrEmpty(model.getParentModelName()) && setterProperties.isEmpty()) { + // Model isn't the root parent and doesn't have any setter properties, no need to generate an accessor. + continue; + } + + String modelName = model.getName(); + String camelModelName = CodeNamer.toCamelCase(modelName); + + // Accessor field declaration. + javaClass.privateMemberVariable("static " + modelName + "Accessor " + camelModelName + "Accessor"); + + // Accessor interface declaration. + javaClass.interfaceBlock(JavaVisibility.Public, modelName + "Accessor", interfaceBlock -> { + if (CoreUtils.isNullOrEmpty(model.getParentModelName())) { + // Only the super most parent model generates the prepareModelForJsonMergePatch method. + interfaceBlock.publicMethod( + modelName + " prepareModelForJsonMergePatch(" + modelName + " " + camelModelName + + ", boolean jsonMergePatchEnabled)"); + + interfaceBlock.publicMethod("boolean isJsonMergePatch(" + modelName + " " + camelModelName + ")"); + } + + if (model.isPolymorphicParent()) { + String modelNameParameter = model.getName().substring(0, 1).toLowerCase(Locale.ROOT) + + model.getName().substring(1); + for (ClientModelProperty property : model.getProperties()) { + if (property.isConstant() || property.isPolymorphicDiscriminator()) { + // Don't generate setters for constant or discriminator properties. + continue; + } + + interfaceBlock.publicMethod("void " + property.getSetterName() + "(" + + model.getName() + " " + modelNameParameter + ", " + + property.getClientType() + " " + property.getName() + ")"); + } + } + }); + + // Accessor field setter. + javaClass.publicStaticMethod("void set" + modelName + "Accessor(" + modelName + "Accessor accessor)", + methodBlock -> methodBlock.line(camelModelName + "Accessor = accessor;")); + + // Accessor field getter. + javaClass.publicStaticMethod(modelName + "Accessor get" + modelName + "Accessor()", + methodBlock -> methodBlock.methodReturn(camelModelName + "Accessor")); + } + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/MethodGroupInterfaceTemplate.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/MethodGroupInterfaceTemplate.java new file mode 100644 index 0000000000..4ce6d9dcfa --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/MethodGroupInterfaceTemplate.java @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.template; + +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethod; +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.IType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.MethodGroupClient; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaFile; + +import java.util.HashSet; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Writes a MethodGroupClient to a JavaFile as an interface. + */ +public class MethodGroupInterfaceTemplate implements IJavaTemplate { + private static final MethodGroupInterfaceTemplate INSTANCE = new MethodGroupInterfaceTemplate(); + + private MethodGroupInterfaceTemplate() { + } + + public static MethodGroupInterfaceTemplate getInstance() { + return INSTANCE; + } + + public final void write(MethodGroupClient methodGroupClient, JavaFile javaFile) { + JavaSettings settings = JavaSettings.getInstance(); + HashSet imports = new HashSet(); + methodGroupClient.addImportsTo(imports, false, settings); + javaFile.declareImport(imports); + + List interfaces = methodGroupClient.getSupportedInterfaces().stream() + .map(IType::toString).collect(Collectors.toList()); + String parentDeclaration = !interfaces.isEmpty() ? String.format(" extends %1$s", String.join(", ", interfaces)) : ""; + + javaFile.javadocComment((comment) -> { + comment.description(String.format("An instance of this class provides access to all the operations defined in %1$s.", methodGroupClient.getInterfaceName())); + }); + javaFile.publicInterface(String.format("%1$s%2$s", methodGroupClient.getInterfaceName(), parentDeclaration), interfaceBlock -> + { + for (ClientMethod clientMethod : methodGroupClient.getClientMethods()) { + Templates.getClientMethodTemplate().write(clientMethod, interfaceBlock); + } + }); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/MethodGroupTemplate.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/MethodGroupTemplate.java new file mode 100644 index 0000000000..9d1cfbff13 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/MethodGroupTemplate.java @@ -0,0 +1,132 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.template; + +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClassType; +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.IType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.MethodGroupClient; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ServiceClientProperty; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaBlock; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaClass; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaFile; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaVisibility; +import com.microsoft.typespec.http.client.generator.core.util.ClientModelUtil; +import com.microsoft.typespec.http.client.generator.core.util.ModelNamer; +import com.microsoft.typespec.http.client.generator.core.util.TemplateUtil; +import com.azure.core.util.CoreUtils; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Writes a MethodGroupClient to a JavaFile. + */ +public class MethodGroupTemplate implements IJavaTemplate { + private static final MethodGroupTemplate INSTANCE = new MethodGroupTemplate(); + + protected MethodGroupTemplate() { + } + + public static MethodGroupTemplate getInstance() { + return INSTANCE; + } + + public final void write(MethodGroupClient methodGroupClient, JavaFile javaFile) { + JavaSettings settings = JavaSettings.getInstance(); + Set imports = new HashSet<>(); + if (settings.isUseClientLogger()) { + ClassType.CLIENT_LOGGER.addImportsTo(imports, false); + } + + methodGroupClient.addImportsTo(imports, true, settings); + + String serviceClientPackageName = + ClientModelUtil.getServiceClientPackageName(methodGroupClient.getServiceClientName()); + imports.add(String.format("%1$s.%2$s", serviceClientPackageName, methodGroupClient.getServiceClientName())); + + javaFile.declareImport(imports); + + List interfaces = methodGroupClient.getSupportedInterfaces().stream() + .map(IType::toString).collect(Collectors.toList()); + interfaces.addAll(methodGroupClient.getImplementedInterfaces()); + String parentDeclaration = !interfaces.isEmpty() ? String.format(" implements %1$s", String.join(", ", interfaces)) : ""; + + final JavaVisibility visibility = methodGroupClient.getPackage().equals(serviceClientPackageName) + ? JavaVisibility.PackagePrivate + : JavaVisibility.Public; + + javaFile.javadocComment(comment -> { + comment.description(String.format("An instance of this class provides access to all the operations defined in %1$s.", methodGroupClient.getInterfaceName())); + }); + javaFile.publicFinalClass(String.format("%1$s%2$s", methodGroupClient.getClassName(), parentDeclaration), classBlock -> + { + final boolean hasProxy = methodGroupClient.getProxy() != null; + + if (hasProxy) { + classBlock.javadocComment("The proxy service used to perform REST calls."); + classBlock.privateFinalMemberVariable(methodGroupClient.getProxy().getName(), "service"); + } + + classBlock.javadocComment("The service client containing this operation class."); + classBlock.privateFinalMemberVariable(methodGroupClient.getServiceClientName(), "client"); + + classBlock.javadocComment(comment -> + { + comment.description(String.format("Initializes an instance of %1$s.", methodGroupClient.getClassName())); + comment.param("client", "the instance of the service client containing this operation class."); + }); + classBlock.constructor(visibility, String.format("%1$s(%2$s client)", methodGroupClient.getClassName(), methodGroupClient.getServiceClientName()), constructor -> + { + if (methodGroupClient.getProxy() != null) { + writeServiceProxyConstruction(constructor, methodGroupClient); + } + constructor.line("this.client = client;"); + }); + + if (!CoreUtils.isNullOrEmpty(methodGroupClient.getProperties())) { + for (ServiceClientProperty property : methodGroupClient.getProperties()) { + classBlock.javadocComment(comment -> + { + comment.description(String.format("Gets %1$s", property.getDescription())); + comment.methodReturns(String.format("the %1$s value.", property.getName())); + }); + classBlock.method(property.getMethodVisibility(), null, String.format("%1$s %2$s()", + property.getType(), new ModelNamer().modelPropertyGetterName(property)), function -> + { + function.methodReturn(String.format("client.%1$s()", new ModelNamer().modelPropertyGetterName(property))); + }); + } + } + + if (hasProxy) { + Templates.getProxyTemplate().write(methodGroupClient.getProxy(), classBlock); + } + + TemplateUtil.writeClientMethodsAndHelpers(classBlock, methodGroupClient.getClientMethods()); + + writeAdditionalClassBlock(classBlock); + + if (settings.isUseClientLogger()) { + TemplateUtil.addClientLogger(classBlock, methodGroupClient.getClassName(), javaFile.getContents()); + } + }); + } + + protected void writeAdditionalClassBlock(JavaClass classBlock) { + } + + protected void writeServiceProxyConstruction(JavaBlock constructor, MethodGroupClient methodGroupClient) { + ClassType proxyType = ClassType.REST_PROXY; + if (JavaSettings.getInstance().isBranded()) { + constructor.line(String.format("this.service = %1$s.create(%2$s.class, client.getHttpPipeline(), client.getSerializerAdapter());", + proxyType.getName(), methodGroupClient.getProxy().getName())); + } else { + constructor.line(String.format("this.service = %1$s.create(%2$s.class, client.getHttpPipeline());", + proxyType.getName(), methodGroupClient.getProxy().getName())); + } + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/ModelTemplate.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/ModelTemplate.java new file mode 100644 index 0000000000..fad9ed30f1 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/ModelTemplate.java @@ -0,0 +1,1421 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.template; + +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.Annotation; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ArrayType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClassType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientModel; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientModelProperty; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientModelPropertyAccess; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientModelPropertyReference; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.GenericType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.IType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ImplementationDetails; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.IterableType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ListType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.MapType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.PrimitiveType; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaBlock; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaClass; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaContext; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaFile; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaIfBlock; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaJavadocComment; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaModifier; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaVisibility; +import com.microsoft.typespec.http.client.generator.core.template.util.ModelTemplateHeaderHelper; +import com.microsoft.typespec.http.client.generator.core.util.ClientModelUtil; +import com.microsoft.typespec.http.client.generator.core.util.TemplateUtil; +import com.azure.core.http.HttpHeader; +import com.azure.core.util.CoreUtils; +import com.azure.core.util.logging.ClientLogger; +import com.azure.core.util.serializer.JacksonAdapter; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Base64; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.UUID; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Writes a ClientModel to a JavaFile. + */ +public class ModelTemplate implements IJavaTemplate { + + private static final String MISSING_SCHEMA = "MISSING·SCHEMA"; + private static final ModelTemplate INSTANCE = new ModelTemplate(); + + protected ModelTemplate() { + } + + public static ModelTemplate getInstance() { + return INSTANCE; + } + + public final void write(ClientModel model, JavaFile javaFile) { + if (model.getParentModelName() != null && model.getParentModelName().equals(model.getName())) { + throw new IllegalStateException("Parent model name is same as model name: " + model.getName()); + } + + final boolean requireSerialization = modelRequireSerialization(model); + + JavaSettings settings = JavaSettings.getInstance(); + Set imports = settings.isStreamStyleSerialization() ? new StreamStyleImports() : new HashSet<>(); + + addImports(imports, model, settings); + + List propertyReferences = this.getClientModelPropertyReferences(model); + propertyReferences.forEach(p -> p.addImportsTo(imports, false)); + + if (!CoreUtils.isNullOrEmpty(model.getPropertyReferences())) { + if (settings.getClientFlattenAnnotationTarget() == JavaSettings.ClientFlattenAnnotationTarget.NONE) { + model.getPropertyReferences().forEach(p -> p.addImportsTo(imports, false)); + } + propertyReferences.addAll(model.getPropertyReferences()); + } + + javaFile.declareImport(imports); + + javaFile.javadocComment(comment -> comment.description(model.getDescription())); + + final boolean hasDerivedModels = !model.getDerivedModels().isEmpty(); + final boolean immutableModel = isImmutableOutputModel(model, settings); + boolean treatAsXml = model.isUsedInXml(); + + // Handle adding annotations if the model is polymorphic. + handlePolymorphism(model, hasDerivedModels, javaFile); + + // Add class level annotations for serialization formats such as XML. + addClassLevelAnnotations(model, javaFile, settings); + + // Add Fluent or Immutable based on whether the model has any setters. + addFluentOrImmutableAnnotation(model, immutableModel, propertyReferences, javaFile, settings); + + List classModifiers = null; + if (!hasDerivedModels && !model.getNeedsFlatten()) { + classModifiers = Collections.singletonList(JavaModifier.Final); + } + + String classNameWithBaseType = model.getName(); + if (model.getParentModelName() != null) { + classNameWithBaseType += " extends " + model.getParentModelName(); + } else if (requireSerialization) { + classNameWithBaseType = addSerializationImplementations(classNameWithBaseType, model, settings); + } + + javaFile.publicClass(classModifiers, classNameWithBaseType, classBlock -> { + // If the model has any additional properties, needs to be flattened, and isn't being generated with + // stream-style serialization add a constant Pattern that will be used to escape the additional property + // keys. Certain versions of the JVM will compile a Pattern each time '.replace' is called which is very + // expensive. + if (model.getProperties().stream().anyMatch(ClientModelProperty::isAdditionalProperties) + && model.getNeedsFlatten() + && !settings.isStreamStyleSerialization()) { + addGeneratedAnnotation(classBlock); + classBlock.privateStaticFinalVariable("Pattern KEY_ESCAPER = Pattern.compile(\"\\\\.\");"); + } + + // If code is being generated with the behavior to return an empty byte array when the default value + // expression is null and the model has any array types that will need conversion within getter methods + // generate a static byte[] that will be returned instead of creating a new instance each get. + if (isGenerateConstantEmptyByteArray(model, settings)) { + classBlock.privateStaticFinalVariable("byte[] EMPTY_BYTE_ARRAY = new byte[0]"); + } + + // XML namespace constants + addXmlNamespaceConstants(model, classBlock); + + // properties + addProperties(model, classBlock, settings); + + // add jsonMergePatch related properties and accessors + if (ClientModelUtil.isJsonMergePatchModel(model, settings)) { + addJsonMergePatchRelatedPropertyAndAccessors(classBlock, model); + } + + // constructor + JavaVisibility modelConstructorVisibility = immutableModel + ? (hasDerivedModels ? JavaVisibility.Protected : JavaVisibility.Private) + : JavaVisibility.Public; + addModelConstructor(model, modelConstructorVisibility, settings, classBlock); + + for (ClientModelProperty property : getFieldProperties(model, settings)) { + final boolean propertyIsReadOnly = immutableModel || property.isReadOnly(); + + IType propertyWireType = property.getWireType(); + IType propertyClientType = propertyWireType.getClientType(); + + JavaVisibility methodVisibility = property.getClientFlatten() + ? JavaVisibility.Private + : JavaVisibility.Public; + + generateGetterJavadoc(classBlock, property); + addGeneratedAnnotation(classBlock); + if (property.isAdditionalProperties() && !settings.isStreamStyleSerialization()) { + classBlock.annotation("JsonAnyGetter"); + } + if (!propertyIsReadOnly) { + TemplateUtil.addJsonGetter(classBlock, settings, property.getSerializedName()); + } + + boolean overridesParentGetter = overridesParentGetter(model, property, settings, methodVisibility); + if (overridesParentGetter) { + classBlock.annotation("Override"); + } + classBlock.method(methodVisibility, null, + propertyClientType + " " + getGetterName(model, property) + "()", + methodBlock -> addGetterMethod(propertyWireType, propertyClientType, property, treatAsXml, + methodBlock, settings)); + + if (ClientModelUtil.needsPublicSetter(property, settings) && !immutableModel) { + generateSetterJavadoc(classBlock, model, property); + addGeneratedAnnotation(classBlock); + TemplateUtil.addJsonSetter(classBlock, settings, property.getSerializedName()); + classBlock.method(methodVisibility, null, + model.getName() + " " + property.getSetterName() + "(" + propertyClientType + " " + property.getName() + ")", + methodBlock -> addSetterMethod(propertyWireType, propertyClientType, property, treatAsXml, + methodBlock, settings, ClientModelUtil.isJsonMergePatchModel(model, settings))); + } else { + // If stream-style serialization is being generated, some additional setters may need to be added to + // support read-only properties that aren't included in the constructor. + // Jackson handles this by reflectively setting the value in the parent model, but stream-style + // serialization doesn't perform reflective cracking like Jackson Databind does, so it needs a way + // to access the readonly property (aka one without a public setter method). + // + // The package-private setter is added when the property isn't included in the constructor and is + // defined by this model, except for JSON merge patch models as those use the access helper pattern + // to enable subtypes to set the property. + boolean streamStyle = settings.isStreamStyleSerialization(); + boolean hasDerivedTypes = !CoreUtils.isNullOrEmpty(model.getDerivedModels()); + boolean notIncludedInConstructor = !ClientModelUtil.includePropertyInConstructor(property, + settings); + boolean definedByModel = modelDefinesProperty(model, property); + boolean modelIsJsonMergePatch = ClientModelUtil.isJsonMergePatchModel(model, settings); + if (hasDerivedTypes && notIncludedInConstructor && definedByModel + && streamStyle && !property.isPolymorphicDiscriminator() && !modelIsJsonMergePatch) { + generateSetterJavadoc(classBlock, model, property); + addGeneratedAnnotation(classBlock); + classBlock.method(JavaVisibility.PackagePrivate, null, + model.getName() + " " + property.getSetterName() + "(" + propertyClientType + " " + + property.getName() + ")", + methodBlock -> addSetterMethod(propertyWireType, propertyClientType, property, treatAsXml, + methodBlock, settings, + ClientModelUtil.isJsonMergePatchModel(model, settings))); + } + } + + // If the property is additional properties, and stream-style serialization isn't being used, add a + // package-private setter that Jackson can use to set values as it deserializes the key-value pairs. + if (property.isAdditionalProperties() && !settings.isStreamStyleSerialization()) { + addGeneratedAnnotation(classBlock); + classBlock.annotation("JsonAnySetter"); + MapType mapType = (MapType) property.getClientType(); + String methodSignature = "void " + property.getSetterName() + "(String key, " + + mapType.getValueType() + " value)"; + classBlock.packagePrivateMethod(methodSignature, methodBlock -> { + // The additional properties are null by default, so if this is the first time the value is + // being added create the containing map. + methodBlock.ifBlock(property.getName() + " == null", + ifBlock -> ifBlock.line(property.getName() + " = new LinkedHashMap<>();")); + + String key = model.getNeedsFlatten() ? "KEY_ESCAPER.matcher(key).replaceAll(\".\")" : "key"; + methodBlock.line(property.getName() + ".put(" + key + ", value);"); + }); + } + } + + // add setters to override parent setters + if (!immutableModel) { + List settersToOverride = getSuperSetters(model, settings, + propertyReferences); + for (ClientModelPropertyAccess parentProperty : settersToOverride) { + classBlock.javadocComment(JavaJavadocComment::inheritDoc); + addGeneratedAnnotation(classBlock); + classBlock.annotation("Override"); + String methodSignature = model.getName() + " " + parentProperty.getSetterName() + "(" + + parentProperty.getClientType() + " " + parentProperty.getName() + ")"; + + classBlock.publicMethod(methodSignature, methodBlock -> { + methodBlock.line( + "super." + parentProperty.getSetterName() + "(" + parentProperty.getName() + ");"); + if (ClientModelUtil.isJsonMergePatchModel(model, settings)) { + methodBlock.line("this.updatedProperties.add(\"" + parentProperty.getName() + "\");"); + } + methodBlock.methodReturn("this"); + }); + } + } + + if (settings.getClientFlattenAnnotationTarget() == JavaSettings.ClientFlattenAnnotationTarget.NONE) { + // reference to properties from flattened client model + for (ClientModelPropertyReference propertyReference : propertyReferences) { + propertyReference = getLocalFlattenedModelPropertyReference(propertyReference); + if (propertyReference == null) { + continue; + } + + ClientModelPropertyAccess property = propertyReference.getReferenceProperty(); + ClientModelProperty targetProperty = propertyReference.getTargetProperty(); + + IType propertyClientType = property.getClientType(); + final boolean propertyIsReadOnly = immutableModel || property.isReadOnly(); + + if (propertyClientType instanceof PrimitiveType && !targetProperty.isRequired()) { + // since the property to flattened client model is optional, the flattened property should be optional + propertyClientType = propertyClientType.asNullable(); + } + final IType propertyClientTypeFinal = propertyClientType; + + // getter + generateGetterJavadoc(classBlock, property); + addGeneratedAnnotation(classBlock); + classBlock.publicMethod(propertyClientType + " " + propertyReference.getGetterName() + "()", methodBlock -> { + // use ternary operator to avoid directly return null + String ifClause = "this." + targetProperty.getGetterName() + "() == null"; + String nullClause = propertyClientTypeFinal.defaultValueExpression(); + String valueClause = "this." + targetProperty.getGetterName() + "()." + property.getGetterName() + "()"; + + methodBlock.methodReturn(ifClause + " ? " + nullClause + " : " + valueClause); + }); + + // setter + if (!propertyIsReadOnly) { + generateSetterJavadoc(classBlock, model, property); + addGeneratedAnnotation(classBlock); + ClientModelPropertyReference propertyReferenceFinal = propertyReference; + classBlock.publicMethod(String.format("%s %s(%s %s)", model.getName(), propertyReference.getSetterName(), propertyClientType, property.getName()), methodBlock -> { + methodBlock.ifBlock(String.format("this.%s() == null", targetProperty.getGetterName()), ifBlock -> + methodBlock.line(String.format("this.%s = new %s();", targetProperty.getName(), propertyReferenceFinal.getTargetModelType()))); + + methodBlock.line(String.format("this.%s().%s(%s);", targetProperty.getGetterName(), property.getSetterName(), property.getName())); + methodBlock.methodReturn("this"); + }); + } + } + } + + addPropertyValidations(classBlock, model, settings); + + if ((settings.isClientSideValidations() && settings.isUseClientLogger()) || model.isStronglyTypedHeader()) { + TemplateUtil.addClientLogger(classBlock, model.getName(), javaFile.getContents()); + } + + if (requireSerialization) { + writeStreamStyleSerialization(classBlock, model, settings); + } + }); + } + + /** + * Get the property reference referring to the local(field) flattened property. + * + * @param propertyReference propertyReference to check + * @return the property reference referring to the local(field) flattened property, null if it's not + */ + protected ClientModelPropertyReference getLocalFlattenedModelPropertyReference(ClientModelPropertyReference propertyReference) { + if (propertyReference.isFromFlattenedProperty()) { + return propertyReference; + } + // Not a flattening property, return null. + return null; + } + + /** + * Whether the property's getter overrides parent getter. + * + * @param model the client model + * @param property the property to generate getter method + * @param settings {@link JavaSettings} instance + * @param methodVisibility + * @return whether the property's getter overrides parent getter + */ + protected boolean overridesParentGetter(ClientModel model, ClientModelProperty property, JavaSettings settings, JavaVisibility methodVisibility) { + // getter method of discriminator property in subclass is handled differently + return property.isPolymorphicDiscriminator() && !modelDefinesProperty(model, property) && methodVisibility == JavaVisibility.Public; + } + /** + * The model is immutable output if and only if the immutable output model setting is enabled and + * the usage of the model include output and does not include input. + * + * @param model the model to check + * @param settings JavaSettings instance + * @return whether the model is output-only immutable model + */ + static boolean isImmutableOutputModel(ClientModel model, JavaSettings settings) { + return (settings.isOutputModelImmutable() && ClientModelUtil.isOutputOnly(model)); + } + + private void addImports(Set imports, ClientModel model, JavaSettings settings) { + // If there is client side validation and the model will generate a ClientLogger to log the validation + // exceptions add an import of 'com.azure.core.util.logging.ClientLogger' and + // 'com.fasterxml.jackson.annotation.JsonIgnore'. + // + // These are added to support adding the ClientLogger and then to JsonIgnore the ClientLogger so it isn't + // included in serialization. + if (settings.isClientSideValidations() && settings.isUseClientLogger()) { + ClassType.CLIENT_LOGGER.addImportsTo(imports, false); + } + + addSerializationImports(imports, model, settings); + + // Add HttpHeaders as an import when strongly-typed HTTP header objects use that as a constructor parameter. + if (model.isStronglyTypedHeader()) { + ClassType.HTTP_HEADERS.addImportsTo(imports, false); + ClassType.HTTP_HEADER_NAME.addImportsTo(imports, false); + + // Also add any potential imports needed to convert the header to the strong type. + // If the import isn't used it will be removed later on. + imports.add(Base64.class.getName()); + imports.add(LinkedHashMap.class.getName()); + imports.add(HttpHeader.class.getName()); + imports.add(UUID.class.getName()); + imports.add(URL.class.getName()); + imports.add(IOException.class.getName()); + imports.add(UncheckedIOException.class.getName()); + imports.add(ClientLogger.class.getName()); + + // JacksonAdapter will be removed in the future once model types are converted to using stream-style + // serialization. For now, it's needed to handle the rare scenario where the strong type is a non-Java + // base type. + imports.add(JacksonAdapter.class.getName()); + } + + String lastParentName = model.getName(); + ClientModel parentModel = ClientModelUtil.getClientModel(model.getParentModelName()); + while (parentModel != null && !lastParentName.equals(parentModel.getName())) { + imports.addAll(parentModel.getImports()); + lastParentName = parentModel.getName(); + parentModel = ClientModelUtil.getClientModel(parentModel.getParentModelName()); + } + + addGeneratedImport(imports); + + model.addImportsTo(imports, settings); + + // add Json merge patch related imports + if (ClientModelUtil.isJsonMergePatchModel(model, settings)) { + imports.add(settings.getPackage(settings.getImplementationSubpackage()) + "." + ClientModelUtil.JSON_MERGE_PATCH_HELPER_CLASS_NAME); + imports.add(Set.class.getName()); + imports.add(HashSet.class.getName()); + } + } + + protected void addSerializationImports(Set imports, ClientModel model, JavaSettings settings) { + imports.add("com.fasterxml.jackson.annotation.JsonCreator"); + + if (settings.isGettersAndSettersAnnotatedForSerialization()) { + imports.add("com.fasterxml.jackson.annotation.JsonGetter"); + imports.add("com.fasterxml.jackson.annotation.JsonSetter"); + } + + imports.add(Pattern.class.getName()); + } + + /** + * We generate super setters in child class if all of below conditions are met: + * 1. parent property has setter + * 2. child does not contain property that shadow this parent property, otherwise super setters + * will collide with child setters + * + * @see Issue 1320 + */ + protected List getSuperSetters(ClientModel model, JavaSettings settings, + List propertyReferences) { + Set modelPropertyNames = model.getProperties().stream().map(ClientModelProperty::getName) + .collect(Collectors.toSet()); + return propertyReferences.stream() + .filter(ClientModelPropertyReference::isFromParentModel) + .map(ClientModelPropertyReference::getReferenceProperty) + .filter(parentProperty -> { + // parent property doesn't have setter + if (!ClientModelUtil.needsPublicSetter(parentProperty, settings)) { + return false; + } + // child does not contain property that shadow this parent property + return !modelPropertyNames.contains(parentProperty.getName()); + } + ).collect(Collectors.toList()); + } + + /** + * Handles setting up Jackson polymorphism annotations. + * + * @param model The client model. + * @param hasDerivedModels Whether this model has children types. + * @param javaFile The JavaFile being generated. + */ + protected void handlePolymorphism(ClientModel model, boolean hasDerivedModels, JavaFile javaFile) { + // Model isn't polymorphic, no work to do here. + if (!model.isPolymorphic()) { + return; + } + + // After removing the concept of passing discriminator to children models and always doing it, there is no need + // to set the 'include' property of the JsonTypeInfo annotation. We use 'JsonTypeInfo.As.PROPERTY' as the value, + // which is the default value, so it doesn't need to be declared. + // And to support unknown subtypes, we always set a default implementation to the class being generated. + // And the discriminator is passed to child models, so the discriminator property needs to be set to visible. + String jsonTypeInfo = "JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = \"" + + model.getPolymorphicDiscriminatorName() + "\", defaultImpl = " + model.getName() + + ".class, visible = true)"; + + javaFile.annotation(jsonTypeInfo); + javaFile.annotation("JsonTypeName(\"" + model.getSerializedName() + "\")"); + + if (hasDerivedModels) { + javaFile.line("@JsonSubTypes({"); + javaFile.indent(() -> { + Function getDerivedTypeAnnotation = derivedType -> "@JsonSubTypes.Type(name = \"" + + derivedType.getSerializedName() + "\", value = " + derivedType.getName() + ".class)"; + + for (int i = 0; i != model.getDerivedModels().size() - 1; i++) { + ClientModel derivedModel = model.getDerivedModels().get(i); + javaFile.line(getDerivedTypeAnnotation.apply(derivedModel) + ','); + } + javaFile.line(getDerivedTypeAnnotation.apply(model.getDerivedModels() + .get(model.getDerivedModels().size() - 1))); + }); + javaFile.line("})"); + } + } + + /** + * Adds class level annotations such as XML root element, JsonFlatten based on the configurations of the model. + * + * @param model The client model. + * @param javaFile The Java class file. + * @param settings Autorest generation settings. + */ + protected void addClassLevelAnnotations(ClientModel model, JavaFile javaFile, JavaSettings settings) { + if (model.isUsedInXml()) { + if (!CoreUtils.isNullOrEmpty(model.getXmlNamespace())) { + javaFile.annotation("JacksonXmlRootElement(localName = \"" + model.getXmlName() + "\", " + + "namespace = \"" + model.getXmlNamespace() + "\")"); + } else { + javaFile.annotation("JacksonXmlRootElement(localName = \"" + model.getXmlName() + "\")"); + } + } + + if (settings.getClientFlattenAnnotationTarget() == JavaSettings.ClientFlattenAnnotationTarget.TYPE + && model.getNeedsFlatten()) { + javaFile.annotation("JsonFlatten"); + } + } + + /** + * Adds Fluent or Immutable based on whether model has any setters. + * + * @param model The client model. + * @param immutableOutputModel The model is treated as immutable, as it is output only. + * @param propertyReferences The client model property reference. + * @param javaFile The Java class file. + * @param settings Autorest generation settings. + */ + private void addFluentOrImmutableAnnotation(ClientModel model, boolean immutableOutputModel, + List propertyReferences, JavaFile javaFile, JavaSettings settings) { + boolean fluent = !immutableOutputModel && Stream + .concat(model.getProperties().stream(), propertyReferences.stream()) + .anyMatch(p -> ClientModelUtil.needsPublicSetter(p, settings)); + + if (JavaSettings.getInstance().isBranded()) { + if (fluent) { + javaFile.annotation("Fluent"); + } else { + javaFile.annotation("Immutable"); + } + } else { + if (fluent) { + javaFile.annotation("Metadata(conditions = {TypeConditions.FLUENT})"); + } else { + javaFile.annotation("Metadata(conditions = {TypeConditions.IMMUTABLE})"); + } + } + } + + /** + * Adds serialization implementations to the class signature. + * + * @param classSignature The class signature. + * @param model The client model. + * @param settings Autorest generation settings. + * @return The updated class signature with serialization implementations added. + */ + protected String addSerializationImplementations(String classSignature, ClientModel model, JavaSettings settings) { + // no-op as this is an entry point for subclasses of ModelTemplate that provide more specific code generation. + return classSignature; + } + + protected void addXmlNamespaceConstants(ClientModel model, JavaClass classBlock) { + // no-op as this is an entry point for subclasses of ModelTemplate that provide more specific code generation. + } + + /** + * Adds the property fields to a class. + * + * @param model The client model. + * @param classBlock The Java class. + * @param settings AutoRest configuration settings. + */ + private void addProperties(ClientModel model, JavaClass classBlock, JavaSettings settings) { + for (ClientModelProperty property : getFieldProperties(model, settings)) { + addProperty(property, model, classBlock, settings); + } + } + + private void addProperty(ClientModelProperty property, ClientModel model, JavaClass classBlock, + JavaSettings settings) { + String propertyName = property.getName(); + IType propertyType = property.getWireType(); + + String defaultValue; + if (property.isPolymorphicDiscriminator()) { + defaultValue = (property.getDefaultValue() == null) + ? property.getClientType().defaultValueExpression(model.getSerializedName()) + : property.getDefaultValue(); + } else { + defaultValue = property.getDefaultValue(); + } + + String fieldSignature; + if (model.isUsedInXml()) { + if (property.isXmlWrapper()) { + if (!settings.isStreamStyleSerialization()) { + String xmlWrapperClassName = getPropertyXmlWrapperClassName(property); + classBlock.staticFinalClass(JavaVisibility.PackagePrivate, xmlWrapperClassName, + innerClass -> addXmlWrapperClass(innerClass, property, xmlWrapperClassName, settings)); + + fieldSignature = xmlWrapperClassName + " " + propertyName; + } else { + fieldSignature = propertyType + " " + propertyName; + } + } else if (propertyType instanceof ListType) { + fieldSignature = propertyType + " " + propertyName + " = new ArrayList<>()"; + } else { + // handle x-ms-client-default + // Only set the property to a default value if the property isn't included in the constructor. + // There can be cases with polymorphic discriminators where they have both a default value and are + // required, in which case the default value will be set in the constructor. + if (defaultValue != null + && (!ClientModelUtil.includePropertyInConstructor(property, settings) || property.isConstant())) { + fieldSignature = propertyType + " " + propertyName + " = " + defaultValue; + } else { + fieldSignature = propertyType + " " + propertyName; + } + } + } else { + if (property.getClientFlatten() && property.isRequired() && property.getClientType() instanceof ClassType + && !isImmutableOutputModel( + getDefiningModel( + ClientModelUtil.getClientModel(((ClassType) property.getClientType()).getName()), property), + settings) + ) { + // if the property of flattened model is required, and isn't immutable output model(which doesn't have public constructor), + // initialize it + fieldSignature = propertyType + " " + propertyName + " = new " + propertyType + "()"; + } else { + // handle x-ms-client-default + // Only set the property to a default value if the property isn't included in the constructor. + // There can be cases with polymorphic discriminators where they have both a default value and are + // required, in which case the default value will be set in the constructor. + if (defaultValue != null + && (!ClientModelUtil.includePropertyInConstructor(property, settings) || property.isConstant())) { + fieldSignature = propertyType + " " + propertyName + " = " + defaultValue; + } else { + fieldSignature = propertyType + " " + propertyName; + } + } + } + + classBlock.blockComment(comment -> comment.line(property.getDescription())); + + addGeneratedAnnotation(classBlock); + addFieldAnnotations(model, property, classBlock, settings); + + if (ClientModelUtil.includePropertyInConstructor(property, settings)) { + classBlock.privateFinalMemberVariable(fieldSignature); + } else { + classBlock.privateMemberVariable(fieldSignature); + } + } + + /** + * Get properties to generate as fields of the class. + * @param model the model to generate class of + * @param settings JavaSettings + * @return properties to generate as fields of the class + */ + protected List getFieldProperties(ClientModel model, JavaSettings settings) { + return Stream.concat( + model.getParentPolymorphicDiscriminators().stream(), + model.getProperties().stream() + ).collect(Collectors.toList()); + } + + protected void addXmlWrapperClass(JavaClass classBlock, ClientModelProperty property, String wrapperClassName, + JavaSettings settings) { + // While using a wrapping class for XML elements that are wrapped may seem inconvenient it is required. + // There has been previous attempts to remove this by using JacksonXmlElementWrapper, which based on its + // documentation should cover this exact scenario, but it doesn't. Jackson unfortunately doesn't always + // respect the JacksonXmlRootName, or JsonRootName, value when handling types wrapped by an enumeration, + // such as List or Iterable. Instead, it uses the JacksonXmlProperty local name as the + // root XML node name for each element in the enumeration. There are configurations for ObjectMapper, and + // XmlMapper, that always forces Jackson to use the root name but those also add the class name as a root + // XML node name if the class doesn't have a root name annotation which results in an addition XML level + // resulting in invalid service XML. There is also one last work around to use JacksonXmlElementWrapper + // and JacksonXmlProperty together as the wrapper will configure the wrapper name and property will configure + // the element name but this breaks down in cases where the same element name is used in two different + // wrappers, a case being Storage BlockList which uses two block elements for its committed and uncommitted + // block lists. + IType propertyClientType = property.getWireType().getClientType(); + + String listElementName = property.getXmlListElementName(); + String jacksonAnnotation = CoreUtils.isNullOrEmpty(property.getXmlNamespace()) + ? "JacksonXmlProperty(localName = \"" + listElementName + "\")" + : "JacksonXmlProperty(localName = \"" + listElementName + "\", namespace = \"" + property.getXmlNamespace() + "\")"; + + classBlock.annotation(jacksonAnnotation); + classBlock.privateFinalMemberVariable(propertyClientType.toString(), "items"); + + classBlock.annotation("JsonCreator"); + classBlock.privateConstructor( + wrapperClassName + "(@" + jacksonAnnotation + " " + propertyClientType + " items)", + constructor -> constructor.line("this.items = items;")); + } + + /** + * Adds the annotations for a model field. + * + * @param model The model. + * @param property The property that represents the field. + * @param classBlock The Java class. + * @param settings Autorest generation settings. + */ + protected void addFieldAnnotations(ClientModel model, ClientModelProperty property, JavaClass classBlock, JavaSettings settings) { + if (settings.getClientFlattenAnnotationTarget() == JavaSettings.ClientFlattenAnnotationTarget.FIELD && property.getNeedsFlatten()) { + classBlock.annotation("JsonFlatten"); + } + + // If the property is a polymorphic discriminator for the class add the annotation @JsonTypeId. + // This will indicate to Jackson that the discriminator serialization is determined by the property + // instead of the class level @JsonTypeName annotation. This prevents the discriminator property from + // being serialized twice, once for the class level annotation and again for the property annotation. + if (property.isPolymorphicDiscriminator()) { + classBlock.annotation("JsonTypeId"); + } + + if (settings.isDataPlaneClient() + && !property.isAdditionalProperties() + && property.getClientType() instanceof MapType + && ((MapType) (property.getClientType())).isValueNullable()) { + classBlock.annotation("JsonInclude(value = JsonInclude.Include.NON_NULL, content = JsonInclude.Include.ALWAYS)"); + } + + boolean treatAsXml = model.isUsedInXml(); + if (modelRequireSerialization(model)) { + if (!CoreUtils.isNullOrEmpty(property.getHeaderCollectionPrefix())) { + classBlock.annotation("HeaderCollection(\"" + property.getHeaderCollectionPrefix() + "\")"); + } else if (treatAsXml && property.isXmlAttribute()) { + classBlock.annotation("JacksonXmlProperty(localName = \"" + property.getXmlName() + "\", isAttribute = true)"); + } else if (treatAsXml && property.getXmlNamespace() != null && !property.getXmlNamespace().isEmpty()) { + classBlock.annotation("JacksonXmlProperty(localName = \"" + property.getXmlName() + "\", namespace = \"" + property.getXmlNamespace() + "\")"); + } else if (treatAsXml && property.isXmlText()) { + classBlock.annotation("JacksonXmlText"); + } else if (property.isAdditionalProperties()) { + classBlock.annotation("JsonIgnore"); + } else if (treatAsXml && property.getWireType() instanceof ListType && !property.isXmlWrapper()) { + classBlock.annotation("JsonProperty(\"" + property.getXmlListElementName() + "\")"); + } else if (!CoreUtils.isNullOrEmpty(property.getAnnotationArguments())) { + classBlock.annotation("JsonProperty(" + property.getAnnotationArguments() + ")"); + } + } + } + + /** + * Adds the model constructor to the Java class file. + * + * @param model The model. + * @param constructorVisibility The visibility of constructor. + * @param settings AutoRest settings. + * @param classBlock The Java class file. + */ + private void addModelConstructor(ClientModel model, JavaVisibility constructorVisibility, JavaSettings settings, + JavaClass classBlock) { + final boolean requireSerialization = modelRequireSerialization(model); + + // Early out on custom strongly typed headers constructor as this has different handling that doesn't require + // inspecting the required and constant properties. + if (model.isStronglyTypedHeader()) { + ModelTemplateHeaderHelper.addCustomStronglyTypedHeadersConstructor(classBlock, model, settings); + return; + } + + // Get the required properties from the super class structure. + List requiredParentProperties = ClientModelUtil.getParentConstructorProperties(model, settings); + + // Required properties are those that are required but not constant. + List requiredProperties = new ArrayList<>(); + + for (ClientModelProperty property : model.getProperties()) { + // Property isn't required and won't be bucketed into either constant or required properties. + if (!property.isConstant() && !ClientModelUtil.includePropertyInConstructor(property, settings)) { + continue; + } + + // Property matches a parent property, don't need to include it twice. + if (requiredParentProperties.stream().anyMatch(p -> p.getName().equals(property.getName()))) { + continue; + } + + // Only include non-constant properties. + if (!property.isConstant()) { + requiredProperties.add(property); + } + } + + // Jackson requires a constructor with @JsonCreator, with parameters in wire type. Ref https://github.com/Azure/autorest.java/issues/2170 + boolean generatePrivateConstructorForJackson = false; + + // Description for the class is always the same, not matter whether there are required properties. + // If there are required properties, the required properties will extend the consumer to add param Javadocs. + Consumer javadocCommentConsumer = comment -> + comment.description("Creates an instance of " + model.getName() + " class."); + + final int constructorPropertiesStringBuilderCapacity = 128 * (requiredProperties.size() + requiredParentProperties.size()); + + // Use a StringBuilder with an initial capacity of 128 times the total number of required constructor properties. + // If there are no required constructor properties this will simply be zero and result in a no-args constructor + // being generated. + StringBuilder constructorProperties = + new StringBuilder(constructorPropertiesStringBuilderCapacity); + + StringBuilder superProperties = new StringBuilder(64 * requiredParentProperties.size()); + + if (settings.isRequiredFieldsAsConstructorArgs()) { + final boolean constructorParametersContainsMismatchWireType = + requiredProperties.stream().anyMatch(p -> ClientModelUtil.isWireTypeMismatch(p, true)) + || requiredParentProperties.stream().anyMatch(p -> ClientModelUtil.isWireTypeMismatch(p, true)); + + if (constructorParametersContainsMismatchWireType && !settings.isStreamStyleSerialization()) { + generatePrivateConstructorForJackson = requireSerialization; + } + + final boolean addJsonPropertyAnnotation = !(settings.isStreamStyleSerialization() || generatePrivateConstructorForJackson || !requireSerialization); + + // Properties required by the super class structure come first. + for (ClientModelProperty property : requiredParentProperties) { + if (constructorProperties.length() > 0) { + constructorProperties.append(", "); + } + + addModelConstructorParameter(property, constructorProperties, addJsonPropertyAnnotation); + + javadocCommentConsumer = javadocCommentConsumer.andThen(comment -> comment.param(property.getName(), + "the " + property.getName() + " value to set")); + + if (superProperties.length() > 0) { + superProperties.append(", "); + } + + superProperties.append(property.getName()); + } + + // Then properties required by this class come next. + for (ClientModelProperty property : requiredProperties) { + if (constructorProperties.length() > 0) { + constructorProperties.append(", "); + } + + addModelConstructorParameter(property, constructorProperties, addJsonPropertyAnnotation); + + javadocCommentConsumer = javadocCommentConsumer.andThen(comment -> comment.param(property.getName(), + "the " + property.getName() + " value to set")); + } + } + + // Add the Javadocs for the constructor. + classBlock.javadocComment(javadocCommentConsumer); + + addGeneratedAnnotation(classBlock); + // If there are any constructor arguments indicate that this is the JsonCreator. No args constructors are + // implicitly used as the JsonCreator if the class doesn't indicate one. + if (requireSerialization + && constructorProperties.length() > 0 && !settings.isStreamStyleSerialization() + // @JsonCreator will be on the other private constructor + && !generatePrivateConstructorForJackson) { + classBlock.annotation("JsonCreator"); + } + + // If immutableOutputModel, make the constructor private, so that adding required properties, or changing model to input-output will not have breaking changes. + // For user in test, they will need to mock the class. + + // If constructorProperties empty this just becomes an empty constructor. + classBlock.constructor(constructorVisibility, model.getName() + "(" + constructorProperties + ")", constructor -> { + // If there are super class properties, call super() first. + if (superProperties.length() > 0) { + constructor.line("super(" + superProperties + ");"); + } + + // If we're always adding the polymorphic discriminator to updated properties, may as well just make the + // serialization always add them. This will remove the need to track them, further reducing Set updating and + // querying, which can improve performance in high throughput scenarios. + // If there is a polymorphic discriminator , add a line to initialize the discriminator. +// ClientModelProperty polymorphicProperty = model.getPolymorphicDiscriminator(); +// if (polymorphicProperty != null && !polymorphicProperty.isRequired()) { +// if (ClientModelUtil.isJsonMergePatchModel(model, settings)) { +// for (ClientModelProperty property : model.getParentPolymorphicDiscriminators()) { +// constructor.line("this.updatedProperties.add(\"" + property.getName() + "\");"); +// } +// +// constructor.line("this.updatedProperties.add(\"" + polymorphicProperty.getName() + "\");"); +// } +// } + + // constant properties should already be initialized in class variable definition +// // Then, add all constant properties. +// for (ClientModelProperty property : constantProperties) { +// constructor.line(property.getName() + " = " + property.getDefaultValue() + ";"); +// } + + // Finally, add all required properties. + if (settings.isRequiredFieldsAsConstructorArgs()) { + for (ClientModelProperty property : requiredProperties) { + if (property.getClientType() != property.getWireType()) { + // If the property needs to be converted and the passed value is null, set the field to null as the + // converter will likely throw a NullPointerException. + // Otherwise, just convert the value. + constructor.ifBlock(property.getName() + " == null", + ifBlock -> ifBlock.line("this.%s = %s;", property.getName(), property.getWireType().defaultValueExpression())) + .elseBlock(elseBlock -> elseBlock.line("this.%s = %s;", + property.getName(), property.getWireType().convertFromClientType(property.getName()))); + } else { + constructor.line("this." + property.getName() + " = " + property.getWireType().convertFromClientType(property.getName()) + ";"); + } + } + } + }); + + if (generatePrivateConstructorForJackson) { + addGeneratedAnnotation(classBlock); + classBlock.annotation("JsonCreator"); + + StringBuilder constructorPropertiesAsWireType = + new StringBuilder(constructorPropertiesStringBuilderCapacity); + + StringBuilder constructorPropertiesInvokePublicConstructor = + new StringBuilder(constructorPropertiesStringBuilderCapacity); + + final Consumer addParameterInvokePublicConstructor = p -> { + if (constructorPropertiesInvokePublicConstructor.length() > 0) { + constructorPropertiesInvokePublicConstructor.append(", "); + } + + if (p.getWireType() == p.getClientType()) { + constructorPropertiesInvokePublicConstructor.append(p.getName()); + } else { + constructorPropertiesInvokePublicConstructor.append(p.getWireType().convertToClientType(p.getName())); + } + }; + + for (ClientModelProperty property : requiredParentProperties) { + if (constructorPropertiesAsWireType.length() > 0) { + constructorPropertiesAsWireType.append(", "); + } + + addModelConstructorParameterAsWireType(property, constructorPropertiesAsWireType); + + addParameterInvokePublicConstructor.accept(property); + } + for (ClientModelProperty property : requiredProperties) { + if (constructorPropertiesAsWireType.length() > 0) { + constructorPropertiesAsWireType.append(", "); + } + + addModelConstructorParameterAsWireType(property, constructorPropertiesAsWireType); + + addParameterInvokePublicConstructor.accept(property); + } + + classBlock.privateConstructor(model.getName() + "(" + constructorPropertiesAsWireType + ")", constructor -> { + constructor.line("this(" + constructorPropertiesInvokePublicConstructor + ");"); + }); + } + } + + /** + * Adds a constructor parameter to the constructor signature builder. + *

+ * The parameter takes client type of property in constructor. + * + * @param property The client model property as constructor parameter. + * @param constructorSignatureBuilder The constructor signature builder. + * @param addJsonPropertyAnnotation whether to add {@code @JsonProperty} annotation on parameter. + */ + private static void addModelConstructorParameter(ClientModelProperty property, + StringBuilder constructorSignatureBuilder, boolean addJsonPropertyAnnotation) { + + if (addJsonPropertyAnnotation) { + constructorSignatureBuilder.append("@JsonProperty(").append(property.getAnnotationArguments()).append(") "); + } + constructorSignatureBuilder.append(property.getClientType()).append(" ").append(property.getName()); + } + + /** + * Adds a constructor parameter to the constructor signature builder. + *

+ * The parameter takes wire type of property in constructor. + * + * @param property The client model property as constructor parameter. + * @param constructorSignatureBuilder The constructor signature builder. + */ + private static void addModelConstructorParameterAsWireType( + ClientModelProperty property, + StringBuilder constructorSignatureBuilder) { + + constructorSignatureBuilder + .append("@JsonProperty(").append(property.getAnnotationArguments()).append(") ") + .append(property.getWireType()).append(" ").append(property.getName()); + } + + /** + * Adds a getter method. + * + * @param propertyWireType The property wire type. + * @param propertyClientType The client property type. + * @param property The property. + * @param treatAsXml Whether the getter should treat the property as XML. + * @param methodBlock Where the getter method is being added. + * @param settings Java settings. + */ + private static void addGetterMethod(IType propertyWireType, IType propertyClientType, ClientModelProperty property, + boolean treatAsXml, JavaBlock methodBlock, JavaSettings settings) { + String sourceTypeName = propertyWireType.toString(); + String targetTypeName = propertyClientType.toString(); + String expression = "this." + property.getName(); + if (propertyWireType.equals(ArrayType.BYTE_ARRAY)) { + expression = TemplateHelper.getByteCloneExpression(expression); + } + + if (sourceTypeName.equals(targetTypeName)) { + if (treatAsXml && property.isXmlWrapper() && (property.getWireType() instanceof IterableType)) { + String thisGetName = "this." + property.getName(); + if (settings.isStreamStyleSerialization()) { + methodBlock.ifBlock(thisGetName + " == null", ifBlock -> + ifBlock.line(thisGetName + " = new ArrayList<>();")); + methodBlock.methodReturn("this." + property.getName()); + } else { + methodBlock.ifBlock(thisGetName + " == null", ifBlock -> + ifBlock.line("this.%s = new %s(new ArrayList<%s>());", property.getName(), + getPropertyXmlWrapperClassName(property), + ((GenericType) property.getWireType()).getTypeArguments()[0])); + methodBlock.methodReturn(thisGetName + ".items"); + } + } else { + methodBlock.methodReturn(expression); + } + } else { + // If the wire type was null, return null as the returned conversion could, and most likely would, result + // in a NullPointerException. + if (propertyWireType.isNullable()) { + methodBlock.ifBlock(expression + " == null", + ifBlock -> ifBlock.methodReturn(propertyClientType.defaultValueExpression())); + } + + // Return the conversion of the wire type to the client type. An example would be a wire type of + // DateTimeRfc1123 and a client type of OffsetDateTime (type a consumer would use), this makes the return + // "this.value.getDateTime()". + methodBlock.methodReturn(propertyWireType.convertToClientType(expression)); + } + } + + /** + * Adds a setter method. + * + * @param propertyWireType The property wire type. + * @param propertyClientType The client property type. + * @param property The property. + * @param treatAsXml Whether the setter should treat the property as XML. + * @param methodBlock Where the setter method is being added. + * @param isJsonMergePatchModel Whether the client model is a JSON merge patch model. + */ + private static void addSetterMethod(IType propertyWireType, IType propertyClientType, ClientModelProperty property, + boolean treatAsXml, JavaBlock methodBlock, JavaSettings settings, boolean isJsonMergePatchModel) { + String expression = (propertyClientType.equals(ArrayType.BYTE_ARRAY)) + ? TemplateHelper.getByteCloneExpression(property.getName()) + : property.getName(); + + if (propertyClientType != propertyWireType) { + // If the property needs to be converted and the passed value is null, set the field to null as the + // converter will likely throw a NullPointerException. + // Otherwise, just convert the value. + methodBlock.ifBlock(property.getName() + " == null", + ifBlock -> ifBlock.line("this.%s = %s;", property.getName(), property.getWireType().defaultValueExpression())) + .elseBlock(elseBlock -> + elseBlock.line("this.%s = %s;", property.getName(), propertyWireType.convertFromClientType(expression))); + } else { + if (treatAsXml && property.isXmlWrapper()) { + if (settings.isStreamStyleSerialization()) { + methodBlock.line("this." + property.getName() + " = " + expression + ";"); + } else { + methodBlock.line("this.%s = new %s(%s);", property.getName(), + getPropertyXmlWrapperClassName(property), expression); + } + } else { + methodBlock.line("this.%s = %s;", property.getName(), expression); + } + } + + if (isJsonMergePatchModel) { + methodBlock.line("this.updatedProperties.add(\"" + property.getName() + "\");"); + } + + methodBlock.methodReturn("this"); + } + + private void addPropertyValidations(JavaClass classBlock, ClientModel model, JavaSettings settings) { + if (settings.isClientSideValidations()) { + + // javadoc + classBlock.javadocComment((comment) -> { + comment.description("Validates the instance."); + + comment.methodThrows("IllegalArgumentException", "thrown if the instance is not valid"); + }); + + if (this.parentModelHasValidate(model.getParentModelName())) { + classBlock.annotation("Override"); + } + classBlock.publicMethod("void validate()", methodBlock -> { + if (this.callParentValidate(model.getParentModelName())) { + methodBlock.line("super.validate();"); + } + for (ClientModelProperty property : getValidationProperties(model)) { + String validation = property.getClientType().validate(getGetterName(model, property) + "()"); + if (property.isRequired() && !property.isReadOnly() && !property.isConstant() && !(property.getClientType() instanceof PrimitiveType)) { + JavaIfBlock nullCheck = methodBlock.ifBlock(String.format("%s() == null", getGetterName(model, property)), ifBlock -> { + final String errorMessage = String.format("\"Missing required property %s in model %s\"", property.getName(), model.getName()); + if (settings.isUseClientLogger()) { + ifBlock.line( + "throw LOGGER.atError().log(new IllegalArgumentException(" + errorMessage + "));"); + } else { + ifBlock.line("throw new IllegalArgumentException(" + errorMessage + ");"); + } + }); + if (validation != null) { + nullCheck.elseBlock(elseBlock -> elseBlock.line(validation + ";")); + } + } else if (validation != null) { + methodBlock.ifBlock(getGetterName(model, property) + "() != null", + ifBlock -> ifBlock.line(validation + ";")); + } + } + }); + } + } + + /** + * Extension for validation on parent model. + * + * @param parentModelName parent model name + * @return whether to call validate() on parent model + */ + protected boolean callParentValidate(String parentModelName) { + return parentModelHasValidate(parentModelName); + } + + /** + * Gets properties to validate in `validate()` method. + * + * @param model the model to add `validate()` method + * @return properties to validate in `validate()` method + */ + protected List getValidationProperties(ClientModel model) { + return model.getProperties(); + } + + /** + * Gets the property XML wrapper class name. + * + * @param property The property that is getting its XML wrapper class name. + * @return The property XML wrapper class name. + */ + static String getPropertyXmlWrapperClassName(ClientModelProperty property) { + return property.getXmlName() + "Wrapper"; + } + + /** + * Extension for validation on parent model. + * + * @param parentModelName the parent model name + * @return Whether validate() exists in parent model. + */ + protected boolean parentModelHasValidate(String parentModelName) { + return parentModelName != null; + } + + /** + * Extension for property getter method name. + * + * @param model the model + * @param property the property + * @return The property getter method name. + */ + protected String getGetterName(ClientModel model, ClientModelProperty property) { + return property.getGetterName(); + } + + /** + * Extension for Fluent list of client model property reference. + * + * @param model the client model. + * @return the list of client model property reference. + */ + protected List getClientModelPropertyReferences(ClientModel model) { + List propertyReferences = new ArrayList<>(); + String lastParentName = model.getName(); + String parentModelName = model.getParentModelName(); + while (parentModelName != null && !lastParentName.equals(parentModelName)) { + ClientModel parentModel = ClientModelUtil.getClientModel(parentModelName); + if (parentModel != null) { + if (parentModel.getProperties() != null) { + parentModel.getProperties().stream() + .filter(p -> !p.getClientFlatten() && !p.isAdditionalProperties()) + .map(ClientModelPropertyReference::ofParentProperty) + .forEach(propertyReferences::add); + } + + if (parentModel.getPropertyReferences() != null) { + parentModel.getPropertyReferences().stream() + .filter(ClientModelPropertyReference::isFromFlattenedProperty) + .map(ClientModelPropertyReference::ofParentProperty) + .forEach(propertyReferences::add); + } + } + + lastParentName = parentModelName; + parentModelName = parentModel == null ? null : parentModel.getParentModelName(); + } + return propertyReferences; + } + + /** + * Checks whether to generate constant "private final static byte[] EMPTY_BYTE_ARRAY = new byte[0];" + * + * @param model the model + * @param settings Java settings + * @return Whether to generate the constant. + */ + private static boolean isGenerateConstantEmptyByteArray(ClientModel model, JavaSettings settings) { + if (!settings.isNullByteArrayMapsToEmptyArray()) { + return false; + } + + boolean ret = model.getProperties().stream() + .anyMatch(property -> property.getClientType() == ArrayType.BYTE_ARRAY + && property.getWireType() != property.getClientType()); + + if (!ret && !CoreUtils.isNullOrEmpty(model.getParentModelName())) { + ret = ClientModelUtil.getParentProperties(model).stream() + .anyMatch(property -> property.getClientType() == ArrayType.BYTE_ARRAY + && property.getWireType() != property.getClientType()); + } + + // flatten properties + if (!ret && settings.getClientFlattenAnnotationTarget() == JavaSettings.ClientFlattenAnnotationTarget.NONE) { + // "return this.innerProperties() == null ? EMPTY_BYTE_ARRAY : this.innerProperties().property1();" + ret = model.getPropertyReferences().stream() + .filter(ClientModelPropertyReference::isFromFlattenedProperty) + .anyMatch(p -> p.getClientType() == ArrayType.BYTE_ARRAY); + } + + return ret; + } + + /** + * Checks whether the serialization code is required for the model. + * + * @param model the model. + * @return whether the serialization code is required for the model. + */ + private static boolean modelRequireSerialization(ClientModel model) { + // TODO (weidxu): any other case? "binary"? + return !ClientModelUtil.isMultipartModel(model) + // not GroupSchema + && !(model.getImplementationDetails() != null && model.getImplementationDetails().getUsages() != null && model.getImplementationDetails().getUsages().contains(ImplementationDetails.Usage.OPTIONS_GROUP)); + } + + /** + * Writes stream-style serialization logic for serializing to and deserializing from the serialization format that + * the model uses. + * + * @param classBlock The class block where serialization methods will be written. + * @param model The model. + * @param settings Autorest generation settings. + */ + protected void writeStreamStyleSerialization(JavaClass classBlock, ClientModel model, JavaSettings settings) { + // No-op, meant for StreamSerializationModelTemplate. + } + + protected void addGeneratedImport(Set imports) { + if (JavaSettings.getInstance().isDataPlaneClient()) { + if (JavaSettings.getInstance().isBranded()) { + Annotation.GENERATED.addImportsTo(imports); + } else { + Annotation.METADATA.addImportsTo(imports); + } + } + } + + protected void addGeneratedAnnotation(JavaContext classBlock) { + if (JavaSettings.getInstance().isDataPlaneClient()) { + if (JavaSettings.getInstance().isBranded()) { + classBlock.annotation(Annotation.GENERATED.getName()); + } else { + classBlock.annotation(Annotation.METADATA.getName() + "(generated = true)"); + } + } + } + + // Javadoc for getter method + private static void generateGetterJavadoc(JavaClass classBlock, ClientModelPropertyAccess property) { + classBlock.javadocComment(comment -> { + comment.description("Get the " + property.getName() + " property: " + property.getDescription()); + comment.methodReturns("the " + property.getName() + " value"); + }); + } + + // Javadoc for setter method + private static void generateSetterJavadoc(JavaClass classBlock, ClientModel model, + ClientModelPropertyAccess property) { + classBlock.javadocComment((comment) -> { + if (property.getDescription() == null || property.getDescription().contains(MISSING_SCHEMA)) { + comment.description("Set the " + property.getName() + " property"); + } else { + comment.description("Set the " + property.getName() + " property: " + property.getDescription()); + } + if (property.isRequiredForCreate() && !property.isRequired()) { + comment.line("

Required when create the resource.

"); + } + comment.param(property.getName(), "the " + property.getName() + " value to set"); + comment.methodReturns("the " + model.getName() + " object itself."); + }); + } + + private static final class StreamStyleImports extends HashSet { + @Override + public boolean add(String s) { + if (s != null && s.contains("fasterxml")) { + return true; + } + + return super.add(s); + } + } + + /** + * Add json-merge-patch related flag and accessors. + */ + private void addJsonMergePatchRelatedPropertyAndAccessors(JavaClass classBlock, ClientModel model) { + if (!model.getImplementationDetails().isInput()) { + // Model doesn't get used in serialization, no need to add json merge patch related properties and + // accessors. + return; + } + + classBlock.javadocComment(comment -> + comment.description("Stores updated model property, the value is property name, not serialized name")); + addGeneratedAnnotation(classBlock); + classBlock.privateFinalMemberVariable("Set updatedProperties = new HashSet<>()"); + + if (model.isPolymorphic() && CoreUtils.isNullOrEmpty(model.getDerivedModels())) { + // Only polymorphic parent models generate an accessor. + // If it is the super most parent model, it will generate the prepareModelForJsonMergePatch method. + // Other parents need to generate setters for the properties that are used in json-merge-patch, used in + // deserialization to prevent these properties from always being included in serialization. + return; + } + + List setterProperties = !model.isPolymorphic() ? Collections.emptyList() + : model.getProperties().stream() + .filter(property -> !property.isConstant() && !property.isPolymorphicDiscriminator()) + .collect(Collectors.toList()); + + boolean rootParent = CoreUtils.isNullOrEmpty(model.getParentModelName()); + if (!rootParent && setterProperties.isEmpty()) { + // Model isn't the root parent and doesn't have any setter properties, no need to generate an accessor. + return; + } + + if (rootParent) { + // Only the root model needs to have the jsonMergePatch property. + addGeneratedAnnotation(classBlock); + classBlock.privateMemberVariable("boolean jsonMergePatch"); + } + + if (rootParent) { + // setter + addGeneratedAnnotation(classBlock); + classBlock.privateMethod("void serializeAsJsonMergePatch(boolean jsonMergePatch)", + method -> method.line("this.jsonMergePatch = jsonMergePatch;")); + } + + // static code block to access jsonMergePatch setter + classBlock.staticBlock(staticBlock -> { + String accessorName = model.getName() + "Accessor"; + staticBlock.line("JsonMergePatchHelper.set" + accessorName + "(new JsonMergePatchHelper." + accessorName + "() {"); + staticBlock.indent(() -> { + if (rootParent) { + staticBlock.line("@Override"); + staticBlock.block("public " + model.getName() + " prepareModelForJsonMergePatch(" + model.getName() + + " model, boolean jsonMergePatchEnabled)", setJsonMergePatch -> { + staticBlock.line("model.serializeAsJsonMergePatch(jsonMergePatchEnabled);"); + staticBlock.line("return model;"); + }); + + staticBlock.line("@Override"); + staticBlock.block("public boolean isJsonMergePatch(" + model.getName() + " model)", + getJsonMergePatch -> getJsonMergePatch.line("return model.jsonMergePatch;")); + } + + for (ClientModelProperty setter : setterProperties) { + staticBlock.line("@Override"); + staticBlock.block("public void " + setter.getSetterName() + "(" + model.getName() + + " model, " + setter.getWireType() + " " + setter.getName() + ")", + setField -> setField.line("model." + setter.getName() + " = " + setter.getName() + ";")); + } + }); + + staticBlock.line("});"); + }); + } + + static boolean modelDefinesProperty(ClientModel model, ClientModelProperty property) { + return ClientModelUtil.getParentProperties(model).stream().noneMatch(parentProperty -> + Objects.equals(property.getSerializedName(), parentProperty.getSerializedName())); + } + + static ClientModel getDefiningModel(ClientModel model, ClientModelProperty property) { + ClientModel current = model; + while(current != null) { + if (modelDefinesProperty(current, property)) { + return current; + } + current = ClientModelUtil.getClientModel(current.getParentModelName()); + } + throw new IllegalArgumentException("unable to find defining model for property: " + property); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/ModelTestTemplate.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/ModelTestTemplate.java new file mode 100644 index 0000000000..845149e67d --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/ModelTestTemplate.java @@ -0,0 +1,91 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.template; + +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClassType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientModel; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.examplemodel.ExampleHelperFeature; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.examplemodel.ExampleNode; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaFile; +import com.microsoft.typespec.http.client.generator.core.template.example.ModelExampleWriter; +import com.microsoft.typespec.http.client.generator.core.util.ModelExampleUtil; +import com.microsoft.typespec.http.client.generator.core.util.ModelTestCaseUtil; +import com.azure.json.JsonProviders; +import com.azure.json.JsonWriter; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +public class ModelTestTemplate implements IJavaTemplate { + + private static final ModelTestTemplate INSTANCE = new ModelTestTemplate(); + + private ModelTestTemplate() { + } + + public static ModelTestTemplate getInstance() { + return INSTANCE; + } + + @Override + public void write(ClientModel model, JavaFile javaFile) { + + final boolean immutableOutputModel = JavaSettings.getInstance().isOutputModelImmutable() + && model.getImplementationDetails() != null && !model.getImplementationDetails().isInput(); + + Set imports = new HashSet<>(); + model.addImportsTo(imports, JavaSettings.getInstance()); + ClassType.BINARY_DATA.addImportsTo(imports, false); + + String jsonStr; + ExampleNode exampleNode; + try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + JsonWriter jsonWriter = JsonProviders.createWriter(outputStream)) { + Map jsonObject = ModelTestCaseUtil.jsonFromModel(model); + jsonWriter.writeMap(jsonObject, JsonWriter::writeUntyped).flush(); + jsonStr = outputStream.toString(StandardCharsets.UTF_8); + + exampleNode = ModelExampleUtil.parseNode(model.getType(), jsonObject); + } catch (IOException e) { + throw new IllegalStateException("Failed to serialize Map to JSON string", e); + } + + ModelExampleWriter writer = new ModelExampleWriter(exampleNode, "model"); + imports.addAll(writer.getImports()); + + javaFile.declareImport(imports); + + javaFile.publicFinalClass(model.getName() + "Tests", classBlock -> { + // testDeserialize + classBlock.annotation("org.junit.jupiter.api.Test"); + classBlock.publicMethod("void testDeserialize() throws Exception", methodBlock -> { + methodBlock.line(String.format("%1$s model = BinaryData.fromString(%2$s).toObject(%1$s.class);", + model.getName(), ClassType.STRING.defaultValueExpression(jsonStr))); + writer.writeAssertion(methodBlock); + }); + + if (!immutableOutputModel) { + // testSerialize + classBlock.annotation("org.junit.jupiter.api.Test"); + String methodSignature = "void testSerialize() throws Exception"; + classBlock.publicMethod(methodSignature, methodBlock -> { + methodBlock.line(String.format("%1$s model = %2$s;", + model.getName(), writer.getModelInitializationCode())); + methodBlock.line(String.format("model = BinaryData.fromObject(model).toObject(%1$s.class);", + model.getName())); + writer.writeAssertion(methodBlock); + }); + + if (writer.getHelperFeatures().contains(ExampleHelperFeature.MapOfMethod)) { + ModelExampleWriter.writeMapOfMethod(classBlock); + } + } + }); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/ModuleInfoTemplate.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/ModuleInfoTemplate.java new file mode 100644 index 0000000000..e8002e5d28 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/ModuleInfoTemplate.java @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.template; + +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ModuleInfo; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaFile; + +import java.util.stream.Collectors; + +public class ModuleInfoTemplate implements IJavaTemplate { + + private static final ModuleInfoTemplate INSTANCE = new ModuleInfoTemplate(); + + private ModuleInfoTemplate() { + } + + public static ModuleInfoTemplate getInstance() { + return INSTANCE; + } + + @Override + public void write(ModuleInfo model, JavaFile javaFile) { + JavaSettings settings = JavaSettings.getInstance(); + if (settings.getFileHeaderText() != null && !settings.getFileHeaderText().isEmpty()) { + javaFile.lineComment(comment -> { + comment.line(settings.getFileHeaderText()); + }); + javaFile.line(); + } + + javaFile.line(String.format("module %1$s {", model.getModuleName())); + javaFile.indent(() -> { + for (ModuleInfo.RequireModule module : model.getRequireModules().stream().distinct().collect(Collectors.toList())) { + javaFile.line(String.format("requires %1$s%2$s;", + module.isTransitive() ? "transitive " : "", + module.getModuleName())); + } + for (ModuleInfo.ExportModule module : model.getExportModules().stream().distinct().collect(Collectors.toList())) { + javaFile.line(String.format("exports %1$s;", + module.getModuleName())); + } + for (ModuleInfo.OpenModule module : model.getOpenModules().stream().distinct().collect(Collectors.toList())) { + javaFile.line(String.format("opens %1$s%2$s;", + module.getModuleName(), + module.isOpenTo() ? (" to " + String.join(", ", module.getOpenToModules())) : "")); + } + }); + javaFile.line("}"); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/PackageInfoTemplate.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/PackageInfoTemplate.java new file mode 100644 index 0000000000..fa4f1562ea --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/PackageInfoTemplate.java @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.template; + + +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.PackageInfo; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaFile; +import com.microsoft.typespec.http.client.generator.core.partialupdate.util.PartialUpdateHandler; + +import java.util.regex.Pattern; + +/** + * Writes a PackageInfo to a JavaFile. + */ +public class PackageInfoTemplate implements IJavaTemplate { + private static final PackageInfoTemplate INSTANCE = new PackageInfoTemplate(); + + private static final Pattern NEW_LINE = Pattern.compile(Pattern.quote("\r\n")); + + private PackageInfoTemplate() { + } + + public static PackageInfoTemplate getInstance() { + return INSTANCE; + } + + public final void write(PackageInfo packageInfo, JavaFile javaFile) { + JavaSettings settings = JavaSettings.getInstance(); + if (settings.getFileHeaderText() != null && !settings.getFileHeaderText().isEmpty()) { + javaFile.lineComment((comment) -> + comment.line(settings.getFileHeaderText())); + javaFile.line(); + } + + javaFile.javadocComment((comment) -> { + if (settings.isHandlePartialUpdate()) { + comment.line(PartialUpdateHandler.START_GENERATED_JAVA_DOC); + } + + for (String desc : NEW_LINE.split(packageInfo.getDescription(), -1)) { + comment.description(desc); + } + + if (settings.isHandlePartialUpdate()) { + comment.line(PartialUpdateHandler.END_GENERATED_JAVA_DOC); + } + }); + + javaFile.declarePackage(packageInfo.getPackage()); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/PomTemplate.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/PomTemplate.java new file mode 100644 index 0000000000..5ec7fc39b1 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/PomTemplate.java @@ -0,0 +1,254 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.template; + +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.Pom; +import com.microsoft.typespec.http.client.generator.core.model.projectmodel.Project; +import com.microsoft.typespec.http.client.generator.core.model.xmlmodel.XmlBlock; +import com.microsoft.typespec.http.client.generator.core.model.xmlmodel.XmlFile; +import com.azure.core.util.CoreUtils; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * Writes a ServiceClient to a JavaFile. + */ +public class PomTemplate implements IXmlTemplate { + private static final PomTemplate INSTANCE = new PomTemplate(); + + protected PomTemplate() { + } + + public static PomTemplate getInstance() { + return INSTANCE; + } + + public final void write(Pom pom, XmlFile xmlFile) { + JavaSettings settings = JavaSettings.getInstance(); + boolean branded = settings.isBranded(); + + // copyright + xmlFile.blockComment(xmlLineComment -> { + xmlLineComment.line( + Arrays.stream(settings + .getFileHeaderText() + .split(System.lineSeparator())) + .map(line -> " ~ " + line) + .collect(Collectors.joining(System.lineSeparator())) + ); + }); + + Map projectAnnotations = new HashMap<>(); + projectAnnotations.put("xmlns", "http://maven.apache.org/POM/4.0.0"); + projectAnnotations.put("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance"); + projectAnnotations.put("xsi:schemaLocation", "http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"); + + xmlFile.block("project", projectAnnotations, projectBlock -> { + projectBlock.tag("modelVersion", "4.0.0"); + if (pom.getParentIdentifier() != null) { + projectBlock.block("parent", parentBlock -> { + String[] parts = pom.getParentIdentifier().split(":"); + String parentGroupId = parts[0]; + String parentArtifactId = parts[1]; + String parentVersion = parts[2]; + parentBlock.tag("groupId", parentGroupId); + parentBlock.tag("artifactId", parentArtifactId); + parentBlock.tagWithInlineComment("version", parentVersion, + "{x-version-update;com.azure:azure-client-sdk-parent;current}"); + parentBlock.tag("relativePath", pom.getParentRelativePath()); + }); + } + + projectBlock.line(); + + projectBlock.tag("groupId", pom.getGroupId()); + projectBlock.tag("artifactId", pom.getArtifactId()); + projectBlock.tagWithInlineComment("version", pom.getVersion(), + String.format("{x-version-update;%1$s:%2$s;current}", pom.getGroupId(), pom.getArtifactId())); + projectBlock.tag("packaging", "jar"); + + projectBlock.line(); + + projectBlock.tag("name", TemplateHelper.getPomProjectName(pom.getServiceName())); + projectBlock.tag("description", pom.getServiceDescription()); + if (branded) { + projectBlock.tag("url", "https://github.com/Azure/azure-sdk-for-java"); + } + + projectBlock.line(); + + projectBlock.block("licenses", licensesBlock -> { + licensesBlock.block("license", licenseBlock -> { + licenseBlock.tag("name", "The MIT License (MIT)"); + licenseBlock.tag("url", "http://opensource.org/licenses/MIT"); + licenseBlock.tag("distribution", "repo"); + }); + }); + + projectBlock.line(); + + if (branded) { + projectBlock.block("scm", scmBlock -> { + scmBlock.tag("url", "https://github.com/Azure/azure-sdk-for-java"); + scmBlock.tag("connection", "scm:git:git@github.com:Azure/azure-sdk-for-java.git"); + scmBlock.tag("developerConnection", "scm:git:git@github.com:Azure/azure-sdk-for-java.git"); + scmBlock.tag("tag", "HEAD"); + }); + + projectBlock.block("developers", developersBlock -> { + developersBlock.block("developer", developerBlock -> { + developerBlock.tag("id", "microsoft"); + developerBlock.tag("name", "Microsoft"); + }); + }); + } + + if (!branded && pom.getRepositories() != null && !pom.getRepositories().isEmpty()) { + projectBlock.block("repositories", repositoriesBlock -> { + for (Map.Entry repository : pom.getRepositories().entrySet()) { + repositoriesBlock.block("repository", repositoryBlock -> { + repositoryBlock.tag("id", repository.getKey()); + repositoryBlock.tag("url", repository.getValue()); + }); + } + }); + } + + projectBlock.block("properties", propertiesBlock -> { + propertiesBlock.tag("project.build.sourceEncoding", "UTF-8"); + writeJacoco(propertiesBlock); + writeRevapi(propertiesBlock, pom); + }); + + if (!CoreUtils.isNullOrEmpty(pom.getDependencyIdentifiers())) { + projectBlock.block("dependencies", dependenciesBlock -> { + for (String dependency : pom.getDependencyIdentifiers()) { + String[] parts = dependency.split(":"); + String groupId = parts[0]; + String artifactId = parts[1]; + String version; + if (parts.length >= 3) { + version = parts[2]; + } else { + version = null; + } + String scope; + if (parts.length >= 4) { + scope = parts[3]; + } else { + scope = null; + } + dependenciesBlock.block("dependency", dependencyBlock -> { + boolean externalDependency = !groupId.startsWith("com.azure"); // a bit of hack here + dependenciesBlock.tag("groupId", groupId); + dependenciesBlock.tag("artifactId", artifactId); + if (version != null) { + dependencyBlock.tagWithInlineComment("version", version, + String.format("{x-version-update;%1$s;%2$s}", + Project.getVersionUpdateTag(groupId, artifactId), + externalDependency ? "external_dependency" : "dependency")); + } + if (scope != null) { + dependenciesBlock.tag("scope", scope); + } + }); + } + }); + } + + writeBuildBlock(projectBlock, pom); + }); + } + + /** + * Extension for writing jacoco configuration. + * + * @param propertiesBlock the "properties" xml block. + */ + protected void writeJacoco(XmlBlock propertiesBlock) { + // NOOP for data-plane + } + + /** + * Extension for writing revapi configuration. + * + * @param propertiesBlock the "properties" xml block. + */ + protected void writeRevapi(XmlBlock propertiesBlock, Pom pom) { + // NOOP for data-plane + } + + /** + * Extension for writing a "build" block, with array of "plugin" within. + * + * @param projectBlock the "project" xml block. + * @param pom the pom model. + */ + protected void writeBuildBlock(XmlBlock projectBlock, Pom pom) { + if (pom.isRequireCompilerPlugins()) { + projectBlock.block("build", buildBlock -> { + buildBlock.block("plugins", pluginsBlock -> { + writeStandAlonePlugins(projectBlock); + }); + }); + } + } + + /** + * Write a "maven-compiler-plugin" block, for SDK not using com.azure:azure-client-sdk-parent + * + * @param pluginsBlock the "plugins" xml block. + */ + protected void writeStandAlonePlugins(XmlBlock pluginsBlock) { + // maven-compiler-plugin + pluginsBlock.block("plugin", pluginBlock -> { + pluginBlock.tag("groupId", "org.apache.maven.plugins"); + pluginBlock.tag("artifactId", "maven-compiler-plugin"); + pluginBlock.tag("version", "3.10.1"); + pluginBlock.block("configuration", configurationBlock -> { + configurationBlock.tag("release", "11"); + }); + }); + + // maven-source-plugin + pluginsBlock.block("plugin", pluginBlock -> { + pluginBlock.tag("groupId", "org.apache.maven.plugins"); + pluginBlock.tag("artifactId", "maven-source-plugin"); + pluginBlock.tag("version", "3.3.0"); + pluginBlock.block("executions", executionsBlock -> { + executionsBlock.block("execution", executionBlock -> { + executionBlock.tag("id", "attach-sources"); + executionBlock.block("goals", goalsBlock -> { + goalsBlock.tag("goal", "jar"); + }); + }); + }); + }); + + // build-helper-maven-plugin: allow samples to be compiled + pluginsBlock.block("plugin", pluginBlock -> { + pluginBlock.tag("groupId", "org.codehaus.mojo"); + pluginBlock.tag("artifactId", "build-helper-maven-plugin"); + pluginBlock.tag("version", "3.0.0"); + pluginBlock.block("executions", executionsBlock -> { + executionsBlock.block("execution", executionBlock -> { + executionBlock.tag("id", "add-test-source"); + executionBlock.tag("phase", "generate-test-sources"); + executionBlock.block("goals", goalsBlock -> { + goalsBlock.tag("goal", "add-test-source"); + }); + executionBlock.block("configuration", configurationBlock -> { + configurationBlock.block("sources", sourcesBlock -> { + sourcesBlock.tag("source", "${basedir}/src/samples"); + }); + }); + }); + }); + }); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/ProtocolSampleBlankTemplate.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/ProtocolSampleBlankTemplate.java new file mode 100644 index 0000000000..b920f1d452 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/ProtocolSampleBlankTemplate.java @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.template; + +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaFile; + +public class ProtocolSampleBlankTemplate implements IJavaTemplate { + + @Override + public void write(Void model, JavaFile context) { + // the code snippet reference is used in README.md + // see ReadmeTemplate + String snippetReference = JavaSettings.getInstance().getPackage("readme"); + + context.publicFinalClass("ReadmeSamples", classBlock -> { + classBlock.publicMethod("void readmeSamples()", methodBlock -> { + methodBlock.line(String.format("// BEGIN: %s", snippetReference)); + methodBlock.line(String.format("// END: %s", snippetReference)); + }); + }); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/ProtocolSampleTemplate.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/ProtocolSampleTemplate.java new file mode 100644 index 0000000000..36a0f56c30 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/ProtocolSampleTemplate.java @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.template; + +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ProtocolExample; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaFile; +import com.microsoft.typespec.http.client.generator.core.template.example.ProtocolExampleWriter; + +public class ProtocolSampleTemplate implements IJavaTemplate { + + private static final ProtocolSampleTemplate INSTANCE = new ProtocolSampleTemplate(); + + protected ProtocolSampleTemplate() { + } + + public static ProtocolSampleTemplate getInstance() { + return INSTANCE; + } + + public void write(ProtocolExample protocolExample, JavaFile javaFile) { + ProtocolExampleWriter writer = new ProtocolExampleWriter(protocolExample); + + String filename = protocolExample.getFilename(); + + javaFile.declareImport(writer.getImports()); + + javaFile.publicClass(null, filename, classBlock -> { + classBlock.publicStaticMethod("void main(String[] args)", methodBlock -> { + writer.writeClientInitialization(methodBlock); + + // codesnippet begin + if (protocolExample.getProxyMethodExample().getCodeSnippetIdentifier() != null) { + methodBlock.line(String.format("// BEGIN:%s", protocolExample.getProxyMethodExample().getCodeSnippetIdentifier())); + } + + writer.writeClientMethodInvocation(methodBlock, false); + + // codesnippet end + if (protocolExample.getProxyMethodExample().getCodeSnippetIdentifier() != null) { + methodBlock.line(String.format("// END:%s", protocolExample.getProxyMethodExample().getCodeSnippetIdentifier())); + } + }); + }); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/ProtocolTestBaseTemplate.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/ProtocolTestBaseTemplate.java new file mode 100644 index 0000000000..634b5792f4 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/ProtocolTestBaseTemplate.java @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.template; + +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.TestContext; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaFile; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaVisibility; +import com.microsoft.typespec.http.client.generator.core.template.example.ProtocolTestWriter; + +public class ProtocolTestBaseTemplate implements IJavaTemplate { + + private static final ProtocolTestBaseTemplate INSTANCE = new ProtocolTestBaseTemplate(); + + protected ProtocolTestBaseTemplate() { + } + + public static ProtocolTestBaseTemplate getInstance() { + return INSTANCE; + } + + @Override + public void write(TestContext testContext, JavaFile context) { + + ProtocolTestWriter writer = new ProtocolTestWriter(testContext); + + context.lineComment(javaLineComment -> { + javaLineComment.line("The Java test files under 'generated' package are generated for your reference."); + javaLineComment.line("If you wish to modify these files, please copy them out of the 'generated' package, and modify there."); + javaLineComment.line("See https://aka.ms/azsdk/dpg/java/tests for guide on adding a test."); + }); + context.line(); + + context.declareImport(writer.getImports()); + + context.classBlock(JavaVisibility.PackagePrivate, null, String.format("%s extends TestProxyTestBase", testContext.getTestBaseClassName()), classBlock -> { + + writer.writeClientVariables(classBlock); + + classBlock.annotation("Override"); + classBlock.method(JavaVisibility.Protected, null, "void beforeTest()", writer::writeClientInitialization); + }); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/ProtocolTestTemplate.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/ProtocolTestTemplate.java new file mode 100644 index 0000000000..300744f33c --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/ProtocolTestTemplate.java @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.template; + +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ProtocolExample; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.TestContext; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaFile; +import com.microsoft.typespec.http.client.generator.core.template.example.ProtocolExampleWriter; +import com.microsoft.typespec.http.client.generator.core.template.example.ProtocolTestWriter; + +import java.util.Set; + +public class ProtocolTestTemplate implements IJavaTemplate, JavaFile> { + + private static final ProtocolTestTemplate INSTANCE = new ProtocolTestTemplate(); + + protected ProtocolTestTemplate() { + } + + public static ProtocolTestTemplate getInstance() { + return INSTANCE; + } + + @Override + public void write(TestContext testContext, JavaFile context) { + + final String className = testContext.getTestCase().getFilename() + "Tests"; + + ProtocolTestWriter writer = new ProtocolTestWriter(testContext); + ProtocolExampleWriter caseWriter = new ProtocolExampleWriter(testContext.getTestCase()); + + Set imports = writer.getImports(); + imports.addAll(caseWriter.getImports()); + context.declareImport(imports); + + context.publicFinalClass(String.format("%1$s extends %2$s", className, testContext.getTestBaseClassName()), classBlock -> { + classBlock.annotation("Test", "Disabled"); // "DoNotRecord(skipInPlayback = true)" not added + classBlock.publicMethod(String.format("void test%1$s()", className), methodBlock -> { + caseWriter.writeClientMethodInvocation(methodBlock, true); + caseWriter.writeAssertion(methodBlock); + }); + }); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/ProxyTemplate.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/ProxyTemplate.java new file mode 100644 index 0000000000..57f6260133 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/ProxyTemplate.java @@ -0,0 +1,215 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.template; + +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.RequestParameterLocation; +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClassType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.IType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.Proxy; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ProxyMethod; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ProxyMethodParameter; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaClass; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaInterface; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaVisibility; +import com.microsoft.typespec.http.client.generator.core.util.CodeNamer; +import com.azure.core.http.ContentType; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + + +/** + * Writes a Proxy to a JavaClass block. + */ +public class ProxyTemplate implements IJavaTemplate { + private static final ProxyTemplate INSTANCE = new ProxyTemplate(); + + protected ProxyTemplate() { + } + + public static ProxyTemplate getInstance() { + return INSTANCE; + } + + public final void write(Proxy restAPI, JavaClass classBlock) { + JavaSettings settings = JavaSettings.getInstance(); + if (restAPI != null) { + classBlock.javadocComment(comment -> { + comment.description(String.format("The interface defining all the services for %1$s to be used by the proxy service to perform REST calls.", restAPI.getClientTypeName())); + }); + if (settings.isBranded()) { + classBlock.annotation(String.format("Host(\"%1$s\")", restAPI.getBaseURL())); + classBlock.annotation(String.format("ServiceInterface(name = \"%1$s\")", serviceInterfaceWithLengthLimit(restAPI.getClientTypeName()))); + } else { + classBlock.annotation(String.format("ServiceInterface(name = \"%1$s\", host = \"%2$s\")", serviceInterfaceWithLengthLimit(restAPI.getClientTypeName()), restAPI.getBaseURL())); + } + + JavaVisibility visibility = JavaVisibility.Private; + if (settings.isServiceInterfaceAsPublic()) { + visibility = JavaVisibility.Public; + } + + classBlock.interfaceBlock(visibility, restAPI.getName(), interfaceBlock -> + { + for (ProxyMethod restAPIMethod : restAPI.getMethods()) { + if (restAPIMethod.getRequestContentType().equals("multipart/form-data") || restAPIMethod.getRequestContentType().equals("application/x-www-form-urlencoded")) { + interfaceBlock.lineComment(String.format("@Multipart not supported by %1$s", ClassType.REST_PROXY.getName())); + } + + writeProxyMethodHeaders(restAPIMethod, interfaceBlock); + + if (settings.isBranded()) { + interfaceBlock.annotation(String.format("%1$s(\"%2$s\")", CodeNamer.toPascalCase(restAPIMethod.getHttpMethod().toString().toLowerCase()), restAPIMethod.getUrlPath())); + if (!restAPIMethod.getResponseExpectedStatusCodes().isEmpty()) { + interfaceBlock.annotation(String.format("ExpectedResponses({%1$s})", restAPIMethod.getResponseExpectedStatusCodes().stream().map(String::valueOf).collect(Collectors.joining(", ")))); + } + } else { + interfaceBlock.annotation("HttpRequestInformation(method = HttpMethod." + restAPIMethod.getHttpMethod().toString() + + ", path = \"" + restAPIMethod.getUrlPath() + + "\", expectedStatusCodes = { " + restAPIMethod.getResponseExpectedStatusCodes().stream().map(String::valueOf).collect(Collectors.joining(", ")) + " })"); + } + + if (!settings.isDataPlaneClient()) { + if (restAPIMethod.getReturnValueWireType() != null) { + interfaceBlock.annotation(String.format("ReturnValueWireType(%1$s.class)", + restAPIMethod.getReturnValueWireType())); + } + } + + if (!settings.isDataPlaneClient() || isExceptionCustomized()) { + // write @UnexpectedResponseExceptionType + + if (restAPIMethod.getUnexpectedResponseExceptionTypes() != null) { + writeUnexpectedExceptions(restAPIMethod, interfaceBlock); + } + + if (restAPIMethod.getUnexpectedResponseExceptionType() != null) { + writeSingleUnexpectedException(restAPIMethod, interfaceBlock); + } + } + + ArrayList parameterDeclarationList = new ArrayList<>(); + if (restAPIMethod.isResumable()) { + interfaceBlock.annotation("ResumeOperation"); + } + + for (ProxyMethodParameter parameter : restAPIMethod.getParameters()) { + StringBuilder parameterDeclarationBuilder = new StringBuilder(); + RequestParameterLocation location = parameter.getRequestParameterLocation(); + + switch (location) { + case URI: + case PATH: + case QUERY: + case HEADER: + parameterDeclarationBuilder.append(String.format("@%1$sParam(", CodeNamer.toPascalCase(location.toString()))); + if (location == RequestParameterLocation.QUERY && parameter.getAlreadyEncoded() && parameter.getExplode()) { + parameterDeclarationBuilder.append(String.format("value = \"%1$s\", encoded = true, multipleQueryParams = true", parameter.getRequestParameterName())); + } else if (location == RequestParameterLocation.QUERY && parameter.getExplode()) { + parameterDeclarationBuilder.append(String.format("value = \"%1$s\", multipleQueryParams = true", parameter.getRequestParameterName())); + } else if ((location == RequestParameterLocation.PATH || + location == RequestParameterLocation.QUERY) + && parameter.getAlreadyEncoded()) { + parameterDeclarationBuilder.append(String.format("value = \"%1$s\", encoded = true", parameter.getRequestParameterName())); + } else if (location == RequestParameterLocation.HEADER && parameter.getHeaderCollectionPrefix() != null + && !parameter.getHeaderCollectionPrefix().isEmpty()) { + parameterDeclarationBuilder.append(String.format("\"%1$s\"", parameter.getHeaderCollectionPrefix())); + } else { + parameterDeclarationBuilder.append(String.format("\"%1$s\"", parameter.getRequestParameterName())); + } + parameterDeclarationBuilder.append(") "); + + break; + + case BODY: + if (ContentType.APPLICATION_X_WWW_FORM_URLENCODED.equals(restAPIMethod.getRequestContentType())) { + parameterDeclarationBuilder.append(String.format("@FormParam(\"%1$s\") ", + parameter.getRequestParameterName())); + break; + } + parameterDeclarationBuilder.append(String.format("@BodyParam(\"%1$s\") ", restAPIMethod.getRequestContentType())); + break; + + // case FormData: + // parameterDeclarationBuilder.append(String.format("@FormParam(\"%1$s\") ", parameter.getRequestParameterName())); + // break; + + case NONE: + break; + + default: + if (!restAPIMethod.isResumable() && parameter.getWireType() != ClassType.CONTEXT) { + throw new IllegalArgumentException("Unrecognized RequestParameterLocation value: " + location); + } + + break; + } + + parameterDeclarationBuilder.append(parameter.getWireType()).append(" ") + .append(parameter.getName()); + parameterDeclarationList.add(parameterDeclarationBuilder.toString()); + } + + writeProxyMethodSignature(parameterDeclarationList, restAPIMethod, interfaceBlock); + } + }); + } + } + + protected void writeUnexpectedExceptions(ProxyMethod restAPIMethod, JavaInterface interfaceBlock) { + if (JavaSettings.getInstance().isBranded()) { + for (Map.Entry> exception : restAPIMethod.getUnexpectedResponseExceptionTypes().entrySet()) { + interfaceBlock.annotation(String.format("UnexpectedResponseExceptionType(value = %1$s.class, code = {%2$s})", + exception.getKey(), exception.getValue().stream().map(String::valueOf).collect(Collectors.joining(", ")))); + } + } else { + for (Map.Entry> exception : restAPIMethod.getUnexpectedResponseExceptionTypes().entrySet()) { + interfaceBlock.annotation("UnexpectedResponseExceptionDetail(exceptionTypeName = \"" + + restAPIMethod.getHttpExceptionType(exception.getKey()).toString() + + "\", statusCode = {" + exception.getValue().stream().map(String::valueOf).collect(Collectors.joining(",")) + " })"); + } + } + } + + protected void writeSingleUnexpectedException(ProxyMethod restAPIMethod, JavaInterface interfaceBlock) { + if(JavaSettings.getInstance().isBranded()) { + interfaceBlock.annotation(String.format("UnexpectedResponseExceptionType(%1$s.class)", restAPIMethod.getUnexpectedResponseExceptionType())); + } else { + interfaceBlock.annotation("UnexpectedResponseExceptionDetail"); + } + } + + protected void writeProxyMethodSignature(java.util.ArrayList parameterDeclarationList, ProxyMethod restAPIMethod, JavaInterface interfaceBlock) { + String parameterDeclarations = String.join(", ", parameterDeclarationList); + IType restAPIMethodReturnValueClientType = restAPIMethod.getReturnType().getClientType(); + interfaceBlock.publicMethod(String.format("%1$s %2$s(%3$s)", restAPIMethodReturnValueClientType, restAPIMethod.getName(), parameterDeclarations)); + } + + private static String serviceInterfaceWithLengthLimit(String serviceInterfaceName) { + final int lengthLimit = 20; + + return serviceInterfaceName.length() > lengthLimit + ? serviceInterfaceName.substring(0, lengthLimit) + : serviceInterfaceName; + } + + /** + * Extension to write Headers annotation for proxy method. + * + * @param restAPIMethod proxy method + * @param interfaceBlock interface block + */ + protected void writeProxyMethodHeaders(ProxyMethod restAPIMethod, JavaInterface interfaceBlock) { + } + + private static boolean isExceptionCustomized() { + JavaSettings settings = JavaSettings.getInstance(); + return settings.getDefaultHttpExceptionType() != null + || settings.isUseDefaultHttpStatusCodeToExceptionTypeMapping() + || settings.getHttpStatusCodeToExceptionTypeMapping() != null; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/ReadmeTemplate.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/ReadmeTemplate.java new file mode 100644 index 0000000000..fe1d215ece --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/ReadmeTemplate.java @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.template; + +import com.microsoft.typespec.http.client.generator.core.model.projectmodel.Project; +import com.microsoft.typespec.http.client.generator.core.util.TemplateUtil; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; + +public class ReadmeTemplate { + + public String write(Project project) { + return TemplateUtil.loadTextFromResource("Readme_protocol.txt", + TemplateUtil.SERVICE_NAME, project.getServiceName(), + TemplateUtil.SERVICE_DESCRIPTION, project.getServiceDescriptionForMarkdown(), + TemplateUtil.GROUP_ID, project.getGroupId(), + TemplateUtil.ARTIFACT_ID, project.getArtifactId(), + TemplateUtil.ARTIFACT_VERSION, project.getVersion(), + TemplateUtil.PACKAGE_NAME, project.getNamespace(), + TemplateUtil.IMPRESSION_PIXEL, getImpression(project) + ); + } + + protected static String getImpression(Project project) { + String impression = ""; + if (project.getSdkRepositoryPath().isPresent()) { + try { + impression = "![Impressions](https://azure-sdk-impressions.azurewebsites.net/api/impressions/" + + URLEncoder.encode("azure-sdk-for-java/" + project.getSdkRepositoryPath().get() + "/README.png", StandardCharsets.UTF_8.name()) + + ")"; + } catch (UnsupportedEncodingException e) { + // NOOP + } + } + return impression; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/ResponseTemplate.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/ResponseTemplate.java new file mode 100644 index 0000000000..1db7285c3a --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/ResponseTemplate.java @@ -0,0 +1,110 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.template; + +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClassType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientResponse; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.GenericType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.IType; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaFile; + +import java.util.HashSet; +import java.util.Set; + +/** + * Writes a ClientResponse to a JavaFile. + */ +public class ResponseTemplate implements IJavaTemplate { + private static final ResponseTemplate INSTANCE = new ResponseTemplate(); + + protected ResponseTemplate() { + } + + public static ResponseTemplate getInstance() { + return INSTANCE; + } + + public final void write(ClientResponse response, JavaFile javaFile) { + JavaSettings settings = JavaSettings.getInstance(); + + Set imports = new HashSet<>(); + addRequestAndHeaderImports(imports); + IType restResponseType = getRestResponseType(response); + restResponseType.addImportsTo(imports, true); + + boolean isStreamResponse = response.getBodyType().equals(GenericType.FLUX_BYTE_BUFFER); + + // Stream responses implement Closeable to offer a way for the Flux response to be drained + // if it isn't consumed. + if (isStreamResponse) { + imports.add("java.io.Closeable"); + } + + javaFile.declareImport(imports); + + String classSignature; + if (isStreamResponse) { + classSignature = response.getName() + " extends " + restResponseType + " implements Closeable"; + } else if (settings.isGenericResponseTypes()) { + classSignature = restResponseType.toString(); + } else { + classSignature = response.getName() + " extends " + restResponseType; + } + + javaFile.javadocComment(javadoc -> javadoc.description(response.getDescription())); + + javaFile.publicFinalClass(classSignature, classBlock -> { + classBlock.javadocComment(javadoc -> { + javadoc.description("Creates an instance of " + response.getName() + "."); + javadoc.param("request", "the request which resulted in this " + response.getName() + "."); + javadoc.param("statusCode", "the status code of the HTTP response"); + javadoc.param("rawHeaders", "the raw headers of the HTTP response"); + javadoc.param("value", + isStreamResponse ? "the content stream" : "the deserialized value of the HTTP response"); + javadoc.param("headers", "the deserialized headers of the HTTP response"); + }); + + classBlock.publicConstructor( + String.format("%s(HttpRequest request, int statusCode, HttpHeaders rawHeaders, %s value, %s headers)", + response.getName(), response.getBodyType().asNullable(), response.getHeadersType()), + ctorBlock -> ctorBlock.line("super(request, statusCode, rawHeaders, value, headers);")); + + if (!response.getBodyType().asNullable().equals(ClassType.VOID)) { + if (response.getBodyType().equals(GenericType.FLUX_BYTE_BUFFER)) { + classBlock.javadocComment(javadoc -> { + javadoc.description("Gets the response content stream."); + javadoc.methodReturns("the response content stream"); + }); + } else { + classBlock.javadocComment(javadoc -> { + javadoc.description("Gets the deserialized response body."); + javadoc.methodReturns("the deserialized response body"); + }); + } + + classBlock.annotation("Override"); + classBlock.publicMethod(response.getBodyType().asNullable() + " getValue()", + methodBlock -> methodBlock.methodReturn("super.getValue()")); + } + + if (isStreamResponse) { + classBlock.javadocComment( + javadoc -> javadoc.description("Disposes of the connection associated with this stream response.")); + classBlock.annotation("Override"); + classBlock.publicMethod("void close()", + methodBlock -> methodBlock.line("getValue().subscribe(bb -> { }, t -> { }).dispose();")); + } + }); + } + + protected IType getRestResponseType(ClientResponse response) { + return GenericType.RestResponse(response.getHeadersType(), response.getBodyType()); + } + + protected void addRequestAndHeaderImports(java.util.Set imports) { + imports.add("com.azure.core.http.HttpRequest"); + imports.add("com.azure.core.http.HttpHeaders"); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/ServiceAsyncClientTemplate.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/ServiceAsyncClientTemplate.java new file mode 100644 index 0000000000..4c8709503f --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/ServiceAsyncClientTemplate.java @@ -0,0 +1,207 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.template; + +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.RequestParameterLocation; +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.Annotation; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.AsyncSyncClient; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClassType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientBuilder; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethod; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ConvenienceMethod; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.GenericType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.MethodGroupClient; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ServiceClient; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaClass; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaContext; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaFile; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaVisibility; +import com.microsoft.typespec.http.client.generator.core.util.ClientModelUtil; +import com.microsoft.typespec.http.client.generator.core.util.ModelNamer; +import com.microsoft.typespec.http.client.generator.core.util.TemplateUtil; +import com.azure.core.client.traits.EndpointTrait; +import com.azure.core.util.CoreUtils; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Template to create an asynchronous client. + */ +public class ServiceAsyncClientTemplate implements IJavaTemplate { + + private static final ServiceAsyncClientTemplate INSTANCE = new ServiceAsyncClientTemplate(); + + protected ServiceAsyncClientTemplate() { + } + + public static ServiceAsyncClientTemplate getInstance() { + return INSTANCE; + } + + @Override + public final void write(AsyncSyncClient asyncClient, JavaFile javaFile) { + ServiceClient serviceClient = asyncClient.getServiceClient(); + + JavaSettings settings = JavaSettings.getInstance(); + String asyncClassName = asyncClient.getClassName(); + MethodGroupClient methodGroupClient = asyncClient.getMethodGroupClient(); + final boolean wrapServiceClient = methodGroupClient == null; + final String builderPackageName = ClientModelUtil.getServiceClientBuilderPackageName(serviceClient); + final String builderClassName = serviceClient.getInterfaceName() + ClientModelUtil.getBuilderSuffix(); + final boolean samePackageAsBuilder = builderPackageName.equals(asyncClient.getPackageName()); + final JavaVisibility constructorVisibility = samePackageAsBuilder ? JavaVisibility.PackagePrivate : JavaVisibility.Public; + + Set imports = new HashSet<>(); + if (wrapServiceClient) { + serviceClient.addImportsTo(imports, true, false, settings); + imports.add(serviceClient.getPackage() + "." + serviceClient.getClassName()); + } else { + methodGroupClient.addImportsTo(imports, true, settings); + imports.add(methodGroupClient.getPackage() + "." + methodGroupClient.getClassName()); + } + imports.add(builderPackageName + "." + builderClassName); + addServiceClientAnnotationImports(imports); + + Templates.getConvenienceAsyncMethodTemplate().addImports(imports, asyncClient.getConvenienceMethods()); + + javaFile.declareImport(imports); + javaFile.javadocComment(comment -> + comment.description(String.format("Initializes a new instance of the asynchronous %1$s type.", + serviceClient.getInterfaceName()))); + + if (asyncClient.getClientBuilder() != null) { + javaFile.annotation(String.format("ServiceClient(builder = %s.class, isAsync = true)", asyncClient.getClientBuilder().getClassName())); + } + javaFile.publicFinalClass(asyncClassName, classBlock -> + { + // Add service client member variable + addGeneratedAnnotation(classBlock); + if (wrapServiceClient) { + classBlock.privateFinalMemberVariable(serviceClient.getClassName(), "serviceClient"); + } else { + classBlock.privateFinalMemberVariable(methodGroupClient.getClassName(), "serviceClient"); + } + + // Service Client Constructor + classBlock.javadocComment(comment -> { + comment.description(String.format("Initializes an instance of %1$s class.", asyncClient.getClassName())); + comment.param("serviceClient", "the service client implementation."); + }); + addGeneratedAnnotation(classBlock); + if (wrapServiceClient) { + classBlock.constructor(constructorVisibility, String.format("%1$s(%2$s %3$s)", asyncClassName, + serviceClient.getClassName(), "serviceClient"), constructorBlock -> { + constructorBlock.line("this.serviceClient = serviceClient;"); + }); + } else { + classBlock.constructor(constructorVisibility, String.format("%1$s(%2$s %3$s)", asyncClassName, + methodGroupClient.getClassName(), "serviceClient"), constructorBlock -> { + constructorBlock.line("this.serviceClient = serviceClient;"); + }); + } + + if (wrapServiceClient) { + serviceClient.getClientMethods().stream() + .filter(clientMethod -> clientMethod.getMethodVisibility() == JavaVisibility.Public) + .filter(clientMethod -> !clientMethod.isImplementationOnly()) + .filter(clientMethod -> clientMethod.getType().name().contains("Async")) + .filter(clientMethod -> !clientMethod.getMethodParameters() + .stream() + .anyMatch(methodParam -> methodParam.getWireType().contains(ClassType.CONTEXT))) + .forEach(clientMethod -> { + Templates.getWrapperClientMethodTemplate().write(clientMethod, classBlock); + }); + } else { + methodGroupClient.getClientMethods().stream() + .filter(clientMethod -> clientMethod.getMethodVisibility() == JavaVisibility.Public) + .filter(clientMethod -> !clientMethod.isImplementationOnly()) + .filter(clientMethod -> clientMethod.getType().name().contains("Async")) + .filter(clientMethod -> !clientMethod.getMethodParameters() + .stream() + .anyMatch(methodParam -> methodParam.getWireType().contains(ClassType.CONTEXT))) + .forEach(clientMethod -> { + Templates.getWrapperClientMethodTemplate().write(clientMethod, classBlock); + }); + } + + writeConvenienceMethods(asyncClient.getConvenienceMethods(), classBlock); + + ServiceAsyncClientTemplate.addEndpointMethod(classBlock, asyncClient.getClientBuilder(), serviceClient, "this.serviceClient"); + }); + } + + protected void addServiceClientAnnotationImports(Set imports) { + Annotation.SERVICE_CLIENT.addImportsTo(imports); + Annotation.GENERATED.addImportsTo(imports); + } + + protected void addGeneratedAnnotation(JavaContext classBlock) { + if (JavaSettings.getInstance().isBranded()) { + classBlock.annotation(Annotation.GENERATED.getName()); + } else { + classBlock.annotation(Annotation.METADATA.getName() + "(generated = true)"); + } + } + + /** + * Adds "getEndpoint" method, if necessary. + *

+ * This method is companion to "sendRequest" method. Without endpoint, the URL in sendRequest is hard to compose. + * + * @param classBlock the class block for writing the method. + * @param clientBuilder the client builder. + * @param clientReference the code for client reference. E.g. "this.serviceClient" or "this.client". + */ + static void addEndpointMethod(JavaClass classBlock, ClientBuilder clientBuilder, ServiceClient serviceClient, String clientReference) { + // expose "getEndpoint" as public, as companion to "sendRequest" method + if (JavaSettings.getInstance().isGenerateSendRequestMethod()) { + ClientMethod referenceClientMethod = !CoreUtils.isNullOrEmpty(serviceClient.getClientMethods()) + ? serviceClient.getClientMethods().iterator().next() + : serviceClient.getMethodGroupClients().stream().flatMap(mg -> mg.getClientMethods().stream()).findFirst().orElse(null); + + if (referenceClientMethod != null) { + final String baseUrl = serviceClient.getBaseUrl(); + final String endpointReplacementExpr = referenceClientMethod.getProxyMethod().getParameters().stream() + .filter(p -> p.isFromClient() && p.getRequestParameterLocation() == RequestParameterLocation.URI) + .filter(p -> baseUrl.contains(String.format("{%s}", p.getRequestParameterName()))) + .map(p -> String.format(".replace(%1$s, %2$s)", + ClassType.STRING.defaultValueExpression(String.format("{%s}", p.getRequestParameterName())), + p.getParameterReference() + )).collect(Collectors.joining()); + final String endpointExpr = ClassType.STRING.defaultValueExpression(baseUrl) + endpointReplacementExpr; + + clientBuilder.getBuilderTraits().stream() + .filter(t -> EndpointTrait.class.getSimpleName().equals(t.getTraitInterfaceName())) + .map(t -> t.getTraitMethods().iterator().next().getProperty()) + .findAny().ifPresent(serviceClientProperty -> { + classBlock.javadocComment(comment -> { + comment.description("Gets the service endpoint that the client is connected to."); + comment.methodReturns("the service endpoint that the client is connected to."); + }); + String methodName = new ModelNamer().modelPropertyGetterName(serviceClientProperty); + classBlock.method(serviceClientProperty.getMethodVisibility(), null, String.format("%1$s %2$s()", + serviceClientProperty.getType(), methodName), function -> { + function.methodReturn(endpointExpr); + }); + }); + } + } + } + + private void writeConvenienceMethods(List convenienceMethods, JavaClass classBlock) { + Set typeReferenceStaticClasses = new HashSet<>(); + + convenienceMethods.forEach(m -> Templates.getConvenienceAsyncMethodTemplate().write(m, classBlock, typeReferenceStaticClasses)); + + // static variables for TypeReference + for (GenericType typeReferenceStaticClass : typeReferenceStaticClasses) { + addGeneratedAnnotation(classBlock); + TemplateUtil.writeTypeReferenceStaticVariable(classBlock, typeReferenceStaticClass); + } + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/ServiceClientBuilderTemplate.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/ServiceClientBuilderTemplate.java new file mode 100644 index 0000000000..c28b04a398 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/ServiceClientBuilderTemplate.java @@ -0,0 +1,602 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.template; + +import com.microsoft.typespec.http.client.generator.core.Javagen; +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; +import com.microsoft.typespec.http.client.generator.core.extension.plugin.PluginLogger; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.Annotation; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.AsyncSyncClient; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClassType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientBuilder; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientBuilderTrait; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientBuilderTraitMethod; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.PipelinePolicyDetails; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.PrimitiveType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.SecurityInfo; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ServiceClient; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ServiceClientProperty; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaBlock; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaClass; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaContext; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaFile; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaVisibility; +import com.microsoft.typespec.http.client.generator.core.util.ClientModelUtil; +import com.microsoft.typespec.http.client.generator.core.util.CodeNamer; +import com.microsoft.typespec.http.client.generator.core.util.TemplateUtil; +import com.azure.core.http.HttpPipelinePosition; +import com.azure.core.http.policy.AddDatePolicy; +import com.azure.core.http.policy.AddHeadersFromContextPolicy; +import com.azure.core.http.policy.AddHeadersPolicy; +import com.azure.core.http.policy.AzureKeyCredentialPolicy; +import com.azure.core.http.policy.BearerTokenAuthenticationPolicy; +import com.azure.core.http.policy.HttpLoggingPolicy; +import com.azure.core.http.policy.HttpPolicyProviders; +import com.azure.core.http.policy.RequestIdPolicy; +import com.azure.core.util.CoreUtils; +import org.slf4j.Logger; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Writes a ServiceClient to a JavaFile. + */ +public class ServiceClientBuilderTemplate implements IJavaTemplate { + + private final Logger logger = new PluginLogger(Javagen.getPluginInstance(), ServiceClientBuilderTemplate.class); + + private static final String LOCAL_VARIABLE_PREFIX = "local"; + private static final ServiceClientBuilderTemplate INSTANCE = new ServiceClientBuilderTemplate(); + + private static final String JACKSON_SERIALIZER = "JacksonAdapter.createDefaultSerializerAdapter()"; + + protected ServiceClientBuilderTemplate() { + } + + public static ServiceClientBuilderTemplate getInstance() { + return INSTANCE; + } + + public final void write(ClientBuilder clientBuilder, JavaFile javaFile) { + JavaSettings settings = JavaSettings.getInstance(); + ServiceClient serviceClient = clientBuilder.getServiceClient(); + String serviceClientBuilderName = clientBuilder.getClassName(); + + ArrayList commonProperties = addCommonClientProperties(settings, serviceClient.getSecurityInfo()); + + String buildReturnType; + if (!settings.isFluent() && settings.isGenerateClientInterfaces()) { + buildReturnType = serviceClient.getInterfaceName(); + } else { + buildReturnType = serviceClient.getClassName(); + } + + Set imports = new HashSet<>(); + serviceClient.addImportsTo(imports, false, true, settings); + commonProperties.forEach(p -> p.addImportsTo(imports, false)); + imports.add("java.util.List"); + imports.add("java.util.Map"); + imports.add("java.util.HashMap"); + imports.add("java.util.ArrayList"); + ClassType.HTTP_HEADERS.addImportsTo(imports, false); + ClassType.HTTP_HEADER_NAME.addImportsTo(imports, false); + imports.add("java.util.Objects"); + if (settings.isUseClientLogger()) { + ClassType.CLIENT_LOGGER.addImportsTo(imports, false); + } + addServiceClientBuilderAnnotationImport(imports); + addHttpPolicyImports(imports); + addImportForCoreUtils(imports); + addSerializerImport(imports, settings); + addGeneratedImport(imports); + addTraitsImports(clientBuilder, imports); + + List asyncClients = clientBuilder.getAsyncClients(); + List syncClients = clientBuilder.getSyncClients(); + + StringBuilder builderTypes = new StringBuilder(); + builderTypes.append("{"); + if (JavaSettings.getInstance().isGenerateSyncAsyncClients()) { + List clients = new ArrayList<>(syncClients); + if (!settings.isFluentLite()) { + clients.addAll(asyncClients); + } + boolean first = true; + for (AsyncSyncClient client : clients) { + if (first) { + first = false; + } else { + builderTypes.append(", "); + } + builderTypes.append(client.getClassName()).append(".class"); + + client.addImportsTo(imports, false); + } + } else { + builderTypes.append(serviceClient.getClassName()).append(".class"); + } + builderTypes.append("}"); + javaFile.declareImport(imports); + + javaFile.javadocComment(comment -> { + String clientTypeName = settings.isFluent() ? serviceClient.getClassName() : serviceClient.getInterfaceName(); + if (settings.isGenerateBuilderPerClient() && clientBuilder.getSyncClients().size() == 1) { + clientTypeName = clientBuilder.getSyncClients().iterator().next().getClassName(); + } + comment.description(String.format("A builder for creating a new instance of the %1$s type.", clientTypeName)); + }); + + javaFile.annotation(String.format("ServiceClientBuilder(serviceClients = %1$s)", builderTypes)); + String classDefinition = serviceClientBuilderName; + + if (!settings.isAzureOrFluent() && !CoreUtils.isNullOrEmpty(clientBuilder.getBuilderTraits())) { + String serviceClientBuilderGeneric = "<" + serviceClientBuilderName + ">"; + + String interfaces = clientBuilder.getBuilderTraits().stream() + .map(trait -> trait.getTraitInterfaceName() + serviceClientBuilderGeneric) + .collect(Collectors.joining(", ")); + + classDefinition = serviceClientBuilderName + " implements " + interfaces; + } + + javaFile.publicFinalClass(classDefinition, classBlock -> + { + if (!settings.isAzureOrFluent()) { + // sdk name + addGeneratedAnnotation(classBlock); + classBlock.privateStaticFinalVariable("String SDK_NAME = \"name\""); + + // sdk version + addGeneratedAnnotation(classBlock); + classBlock.privateStaticFinalVariable("String SDK_VERSION = \"version\""); + + // default scope + Set scopes = serviceClient.getSecurityInfo() != null ? serviceClient.getSecurityInfo().getScopes() : null; + if (scopes != null && !scopes.isEmpty()) { + addGeneratedAnnotation(classBlock); + classBlock.privateStaticFinalVariable(String.format("String[] DEFAULT_SCOPES = new String[] {%s}", + String.join(", ", scopes))); + } + + if (settings.isBranded()) { + // properties for sdk name and version + String propertiesValue = "new HashMap<>()"; + String artifactId = ClientModelUtil.getArtifactId(); + if (!CoreUtils.isNullOrEmpty(artifactId)) { + propertiesValue = "CoreUtils.getProperties" + "(\"" + artifactId + ".properties\")"; + } + addGeneratedAnnotation(classBlock); + classBlock.privateStaticFinalVariable(String.format("Map PROPERTIES = %s", propertiesValue)); + + addGeneratedAnnotation(classBlock); + classBlock.privateFinalMemberVariable("List", "pipelinePolicies"); + + // constructor + classBlock.javadocComment(String.format("Create an instance of the %s.", serviceClientBuilderName)); + addGeneratedAnnotation(classBlock); + classBlock.publicConstructor(String.format("%1$s()", serviceClientBuilderName), javaBlock -> { + javaBlock.line("this.pipelinePolicies = new ArrayList<>();"); + }); + } else { + addGeneratedAnnotation(classBlock); + classBlock.privateFinalMemberVariable("List", "pipelinePolicies"); + + classBlock.javadocComment(String.format("Create an instance of the %s.", serviceClientBuilderName)); + addGeneratedAnnotation(classBlock); + classBlock.publicConstructor(String.format("%1$s()", serviceClientBuilderName), javaBlock -> { + javaBlock.line("this.pipelinePolicies = new ArrayList<>();"); + }); + } + } + + Stream serviceClientPropertyStream = serviceClient.getProperties().stream() + .filter(p -> !p.isReadOnly()); + if (!settings.isAzureOrFluent()) { + addTraitMethods(clientBuilder, settings, serviceClientBuilderName, classBlock); + serviceClientPropertyStream = serviceClientPropertyStream + .filter(property -> !(clientBuilder.getBuilderTraits().stream() + .flatMap(trait -> trait.getTraitMethods().stream().filter(traitMethod -> traitMethod.getProperty() != null)) + .anyMatch(traitMethod -> property.getName().equals(traitMethod.getProperty().getName())))); + } + + // Add ServiceClient client property variables, getters, and setters + List clientProperties = Stream + .concat(serviceClientPropertyStream, + commonProperties.stream()).collect(Collectors.toList()); + + for (ServiceClientProperty serviceClientProperty : clientProperties) { + classBlock.blockComment(comment -> { + comment.line(serviceClientProperty.getDescription()); + }); + addGeneratedAnnotation(classBlock); + String propertyVariableInit = String.format("%1$s%2$s %3$s", + serviceClientProperty.isReadOnly() ? "final " : "", + serviceClientProperty.getType(), + serviceClientProperty.getName()); + if (serviceClientProperty.getDefaultValueExpression() != null + && serviceClientProperty.getType() instanceof PrimitiveType) { + // init to default value + propertyVariableInit += String.format(" = %1$s", serviceClientProperty.getDefaultValueExpression()); + } + classBlock.privateMemberVariable(propertyVariableInit); + + if (!serviceClientProperty.isReadOnly()) { + classBlock.javadocComment(comment -> + { + comment.description(String.format("Sets %1$s", serviceClientProperty.getDescription())); + comment.param(serviceClientProperty.getName(), String.format("the %1$s value.", serviceClientProperty.getName())); + comment.methodReturns(String.format("the %1$s", serviceClientBuilderName)); + }); + addGeneratedAnnotation(classBlock); + classBlock.publicMethod(String.format("%1$s %2$s(%3$s %4$s)", serviceClientBuilderName, + CodeNamer.toCamelCase(serviceClientProperty.getAccessorMethodSuffix()), serviceClientProperty.getType(), + serviceClientProperty.getName()), function -> + { + function.line(String.format("this.%1$s = %2$s;", serviceClientProperty.getName(), serviceClientProperty.getName())); + function.methodReturn("this"); + }); + } + } + + String buildMethodName = this.primaryBuildMethodName(settings); + + JavaVisibility visibility = settings.isGenerateSyncAsyncClients() ? JavaVisibility.Private : JavaVisibility.Public; + + // build method + classBlock.javadocComment(comment -> { + comment.description(String.format("Builds an instance of %1$s with the provided parameters", buildReturnType)); + comment.methodReturns(String.format("an instance of %1$s", buildReturnType)); + }); + addGeneratedAnnotation(classBlock); + classBlock.method(visibility, null, String.format("%1$s %2$s()", buildReturnType, buildMethodName), function -> { + if (!settings.isAzureOrFluent()) { + function.line("this.validateClient();"); + } + + List allProperties = mergeClientPropertiesWithTraits( + clientProperties, + settings.isAzureOrFluent() ? null : clientBuilder.getBuilderTraits()); + + for (ServiceClientProperty serviceClientProperty : allProperties) { + if (serviceClientProperty.getDefaultValueExpression() != null + && !(serviceClientProperty.getType() instanceof PrimitiveType)) { + function.line(String.format("%1$s %2$s = (%3$s != null) ? %4$s : %5$s;", + serviceClientProperty.getType(), + getLocalBuildVariableName(serviceClientProperty.getName()), + serviceClientProperty.getName(), + serviceClientProperty.getName(), + serviceClientProperty.getDefaultValueExpression())); + } + } + + // additional service client properties in constructor arguments + String constructorArgs = serviceClient.getProperties().stream() + .filter(p -> !p.isReadOnly()) + .map(this::getClientConstructorArgName) + .collect(Collectors.joining(", ")); + if (!constructorArgs.isEmpty()) { + constructorArgs = ", " + constructorArgs; + } + + final String serializerExpression; + if (settings.isDataPlaneClient()) { + serializerExpression = JACKSON_SERIALIZER; + } else { + serializerExpression = getLocalBuildVariableName(getSerializerMemberName()); + } + + if (!settings.isBranded()) { + if (constructorArgs != null && !constructorArgs.isEmpty()) { + function.line(String.format("%1$s client = new %2$s(%3$s%4$s);", + serviceClient.getClassName(), serviceClient.getClassName(), + getLocalBuildVariableName("pipeline"), constructorArgs)); + } else { + function.line(String.format("%1$s client = new %1$s(%2$s);", serviceClient.getClassName(), getLocalBuildVariableName("pipeline"))); + } + } else if (settings.isFluent()) { + function.line(String.format("%1$s client = new %2$s(%3$s, %4$s, %5$s, %6$s%7$s);", + serviceClient.getClassName(), + serviceClient.getClassName(), + getLocalBuildVariableName("pipeline"), + serializerExpression, + getLocalBuildVariableName("defaultPollInterval"), + getLocalBuildVariableName("environment"), + constructorArgs)); + } else { + function.line(String.format("%1$s client = new %2$s(%3$s, %4$s%5$s);", + serviceClient.getClassName(), serviceClient.getClassName(), + getLocalBuildVariableName("pipeline"), serializerExpression, constructorArgs)); + } + function.line("return client;"); + }); + + if (!settings.isAzureOrFluent()) { + List allProperties = mergeClientPropertiesWithTraits(clientProperties, clientBuilder.getBuilderTraits()); + addValidateClientMethod(classBlock, allProperties); + + addCreateHttpPipelineMethod(settings, classBlock, serviceClient.getDefaultCredentialScopes(), serviceClient.getSecurityInfo(), serviceClient.getPipelinePolicyDetails()); + } + + if (JavaSettings.getInstance().isGenerateSyncAsyncClients()) { + if (!settings.isFluentLite()) { + addBuildAsyncClientMethods(clientBuilder, asyncClients, classBlock, buildMethodName); + } + addBuildSyncClientMethods(clientBuilder, asyncClients, syncClients, classBlock, buildMethodName); + } + TemplateUtil.addClientLogger(classBlock, serviceClientBuilderName, javaFile.getContents()); + }); + } + + private static List mergeClientPropertiesWithTraits( + List clientProperties, List builderTraits) { + + List allProperties = new ArrayList<>(); + if (builderTraits != null) { + allProperties.addAll(builderTraits + .stream() + .flatMap(trait -> trait.getTraitMethods().stream()) + .map(ClientBuilderTraitMethod::getProperty) + .filter(Objects::nonNull) + .collect(Collectors.toList())); + } + allProperties.addAll(clientProperties); + return allProperties; + } + + private void addBuildAsyncClientMethods(ClientBuilder clientBuilder, List asyncClients, JavaClass classBlock, String buildMethodName) { + for (AsyncSyncClient asyncClient : asyncClients) { + final boolean wrapServiceClient = asyncClient.getMethodGroupClient() == null; + + classBlock.javadocComment(comment -> + { + comment.description(String + .format("Builds an instance of %1$s class", asyncClient.getClassName())); + comment.methodReturns(String.format("an instance of %1$s", asyncClient.getClassName())); + }); + addGeneratedAnnotation(classBlock); + classBlock.publicMethod(String.format("%1$s %2$s()", asyncClient.getClassName(), clientBuilder.getBuilderMethodNameForAsyncClient(asyncClient)), + function -> { + if (wrapServiceClient) { + function.line("return new %1$s(%2$s());", asyncClient.getClassName(), buildMethodName); + } else { + function.line("return new %1$s(%2$s().get%3$s());", asyncClient.getClassName(), buildMethodName, + CodeNamer.toPascalCase(asyncClient.getMethodGroupClient().getVariableName())); + } + }); + } + } + + private void addBuildSyncClientMethods(ClientBuilder clientBuilder, List asyncClients, List syncClients, JavaClass classBlock, String buildMethodName) { + int syncClientIndex = 0; + for (AsyncSyncClient syncClient : syncClients) { + final boolean wrapServiceClient = syncClient.getMethodGroupClient() == null; + + AsyncSyncClient asyncClient = (asyncClients.size() == syncClients.size()) ? asyncClients.get(syncClientIndex) : null; + + classBlock.javadocComment(comment -> + { + comment.description(String + .format("Builds an instance of %1$s class", syncClient.getClassName())); + comment.methodReturns(String.format("an instance of %1$s", syncClient.getClassName())); + }); + addGeneratedAnnotation(classBlock); + classBlock.publicMethod(String.format("%1$s %2$s()", syncClient.getClassName(), clientBuilder.getBuilderMethodNameForSyncClient(syncClient)), + function -> { + writeSyncClientBuildMethod(syncClient, asyncClient, function, buildMethodName, wrapServiceClient); + }); + + ++syncClientIndex; + } + } + + /** + * Renames the provided variable name to localize it to the method + * @param baseName The base variable name. + * @return The name of the local variable. + */ + private String getLocalBuildVariableName(String baseName) { + return LOCAL_VARIABLE_PREFIX + CodeNamer.toPascalCase(baseName); + } + + private String getClientConstructorArgName(ServiceClientProperty property) { + if (property.getDefaultValueExpression() != null + && !(property.getType() instanceof PrimitiveType)) { + return getLocalBuildVariableName((property.getName())); + } + return "this." + property.getName(); + } + + private void addTraitMethods(ClientBuilder clientBuilder, JavaSettings settings, String serviceClientBuilderName, JavaClass classBlock) { + clientBuilder.getBuilderTraits().stream().flatMap(trait -> trait.getTraitMethods().stream()) + .forEach(traitMethod -> { + ServiceClientProperty serviceClientProperty = traitMethod.getProperty(); + if (serviceClientProperty != null) { + classBlock.blockComment(comment -> { + comment.line(serviceClientProperty.getDescription()); + }); + addGeneratedAnnotation(classBlock); + classBlock.privateMemberVariable(String.format("%1$s%2$s %3$s", + serviceClientProperty.isReadOnly() ? "final " : "", + serviceClientProperty.getType(), + serviceClientProperty.getName())); + } + classBlock.javadocComment(comment -> comment.description(traitMethod.getDocumentation())); + addGeneratedAnnotation(classBlock); + addOverrideAnnotation(classBlock); + classBlock.publicMethod(String.format("%1$s %2$s(%3$s %4$s)", serviceClientBuilderName, + traitMethod.getMethodName(), traitMethod.getMethodParamType(), + traitMethod.getMethodParamName()), traitMethod.getMethodImpl()); + }); + } + + /** + * Extension to write sync client build method invocation + * + * @param syncClient the sync client + * @param asyncClient the async client + * @param function the method block to write method invocation + * @param buildMethodName the name of build method + * @param wrapServiceClient whether the sync client wraps a service client implementation or method group implementation + */ + protected void writeSyncClientBuildMethod(AsyncSyncClient syncClient, AsyncSyncClient asyncClient, JavaBlock function, + String buildMethodName, boolean wrapServiceClient) { + JavaSettings settings = JavaSettings.getInstance(); + boolean syncClientWrapAsync = settings.isSyncClientWrapAsyncClient() + && settings.isDataPlaneClient() + && asyncClient != null; + if (syncClientWrapAsync) { + writeSyncClientBuildMethodFromAsyncClient(syncClient, asyncClient, function, buildMethodName, wrapServiceClient); + } else { + writeSyncClientBuildMethodFromInnerClient(syncClient, function, buildMethodName, wrapServiceClient); + } + } + + protected void writeSyncClientBuildMethodFromInnerClient(AsyncSyncClient syncClient, JavaBlock function, + String buildMethodName, boolean wrapServiceClient) { + if (wrapServiceClient) { + function.line("return new %1$s(%2$s());", syncClient.getClassName(), buildMethodName); + } else { + function.line("return new %1$s(%2$s().get%3$s());", syncClient.getClassName(), buildMethodName, + CodeNamer.toPascalCase(syncClient.getMethodGroupClient().getVariableName())); + } + } + + protected void writeSyncClientBuildMethodFromAsyncClient(AsyncSyncClient syncClient, AsyncSyncClient asyncClient, JavaBlock function, + String buildMethodName, boolean wrapServiceClient) { + if (wrapServiceClient) { + function.line("return new %1$s(new %2$s(%3$s()));", syncClient.getClassName(), asyncClient.getClassName(), + buildMethodName); + } else { + function.line("return new %1$s(new %2$s(%3$s().get%4$s()));", syncClient.getClassName(), asyncClient.getClassName(), + buildMethodName, CodeNamer.toPascalCase(syncClient.getMethodGroupClient().getVariableName())); + } + } + + protected String getSerializerMemberName() { + return "serializerAdapter"; + } + + protected void addSerializerImport(Set imports, JavaSettings settings) { + imports.add(settings.isFluent() ? "com.azure.core.management.serializer.SerializerFactory" : "com.azure.core.util.serializer.JacksonAdapter"); + } + + protected void addImportForCoreUtils(Set imports) { + ClassType.CORE_UTILS.addImportsTo(imports, false); + imports.add("com.azure.core.util.builder.ClientBuilderUtil"); + } + + protected void addHttpPolicyImports(Set imports) { + imports.add(BearerTokenAuthenticationPolicy.class.getName()); + + // one of the key credential policy imports will be removed by the formatter depending + // on which one is used + imports.add(AzureKeyCredentialPolicy.class.getName()); + ClassType.KEY_CREDENTIAL_POLICY.addImportsTo(imports, false); + + imports.add(HttpPolicyProviders.class.getName()); + ClassType.HTTP_PIPELINE_POLICY.addImportsTo(imports, false); + imports.add(HttpLoggingPolicy.class.getName()); + imports.add(AddHeadersPolicy.class.getName()); + imports.add(RequestIdPolicy.class.getName()); + imports.add(AddHeadersFromContextPolicy.class.getName()); + imports.add(AddDatePolicy.class.getName()); + imports.add(HttpPipelinePosition.class.getName()); + imports.add(Collectors.class.getName()); + ClassType.RETRY_POLICY.addImportsTo(imports, false); + ClassType.REDIRECT_POLICY.addImportsTo(imports, false); + } + + protected void addTraitsImports(ClientBuilder clientBuilder, Set imports) { + clientBuilder.getBuilderTraits().stream().forEach(trait -> imports.addAll(trait.getImportPackages())); + } + + protected void addServiceClientBuilderAnnotationImport(Set imports) { + Annotation.SERVICE_CLIENT_BUILDER.addImportsTo(imports); + } + + protected void addCreateHttpPipelineMethod(JavaSettings settings, JavaClass classBlock, + String defaultCredentialScopes, SecurityInfo securityInfo, + PipelinePolicyDetails pipelinePolicyDetails) { + addGeneratedAnnotation(classBlock); + classBlock.privateMethod("HttpPipeline createHttpPipeline()", function -> { + TemplateHelper.createHttpPipelineMethod(settings, defaultCredentialScopes, securityInfo, pipelinePolicyDetails, function); + }); + } + + private void addValidateClientMethod(JavaClass classBlock, List properties) { + addGeneratedAnnotation(classBlock); + classBlock.privateMethod("void validateClient()", methodBlock -> { + methodBlock.line("// This method is invoked from 'buildInnerClient'/'buildClient' method."); + methodBlock.line("// Developer can customize this method, to validate that the necessary conditions are met for the new client."); + for (ServiceClientProperty property : properties) { + // property have a default value would have a "local" for the initialization of client + if (property.isRequired() && property.getDefaultValueExpression() == null) { + methodBlock.line("Objects.requireNonNull(" + property.getName() + ", \"'" + property.getName() + "' cannot be null.\");"); + } + } + }); + } + + protected ArrayList addCommonClientProperties(JavaSettings settings, SecurityInfo securityInfo) { + ArrayList commonProperties = new ArrayList(); + if (settings.isAzureOrFluent()) { + commonProperties.add(new ServiceClientProperty("The environment to connect to", ClassType.AZURE_ENVIRONMENT, "environment", false, "AzureEnvironment.AZURE")); + commonProperties.add(new ServiceClientProperty("The HTTP pipeline to send requests through", ClassType.HTTP_PIPELINE, "pipeline", false, + "new HttpPipelineBuilder().policies(new UserAgentPolicy(), new RetryPolicy()).build()")); + } + if (settings.isFluent()) { + commonProperties.add(new ServiceClientProperty("The default poll interval for long-running operation", ClassType.DURATION, "defaultPollInterval", false, "Duration.ofSeconds(30)")); + } + + // Low-level client does not need serializer. It returns BinaryData. + if (!settings.isDataPlaneClient()) { + commonProperties.add(new ServiceClientProperty("The serializer to serialize an object into a string", + ClassType.SERIALIZER_ADAPTER, getSerializerMemberName(), false, + settings.isFluent() ? "SerializerFactory.createDefaultManagementSerializerAdapter()" : JACKSON_SERIALIZER)); + } + + if (!settings.isAzureOrFluent() && settings.isBranded()) { + commonProperties.add(new ServiceClientProperty("The retry policy that will attempt to retry failed " + + "requests, if applicable.", ClassType.RETRY_POLICY, "retryPolicy", false, null)); + } + return commonProperties; + } + + /** + * Extension for the name of build method. + * + * @return The name of build method. + */ + protected String primaryBuildMethodName(JavaSettings settings) { + return settings.isGenerateSyncAsyncClients() + ? "buildInnerClient" + : "buildClient"; + } + + protected void addGeneratedImport(Set imports) { + if (JavaSettings.getInstance().isBranded()) { + Annotation.GENERATED.addImportsTo(imports); + } else { + Annotation.METADATA.addImportsTo(imports); + } + } + + protected void addGeneratedAnnotation(JavaContext classBlock) { + if (JavaSettings.getInstance().isBranded()) { + classBlock.annotation(Annotation.GENERATED.getName()); + } else { + classBlock.annotation(Annotation.METADATA.getName() + "(generated = true)"); + } + } + + protected void addOverrideAnnotation(JavaContext classBlock) { + classBlock.annotation("Override"); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/ServiceClientInterfaceTemplate.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/ServiceClientInterfaceTemplate.java new file mode 100644 index 0000000000..fd0f05c70e --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/ServiceClientInterfaceTemplate.java @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.template; + + +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethod; +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.MethodGroupClient; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ServiceClient; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ServiceClientProperty; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaFile; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaVisibility; +import com.microsoft.typespec.http.client.generator.core.util.CodeNamer; +import com.microsoft.typespec.http.client.generator.core.util.ModelNamer; + +import java.util.HashSet; + +/** + * Writes a ServiceClient to a JavaFile as an interface. + */ +public class ServiceClientInterfaceTemplate implements IJavaTemplate { + + private static final ServiceClientInterfaceTemplate INSTANCE = new ServiceClientInterfaceTemplate(); + + private ServiceClientInterfaceTemplate() { + } + + public static ServiceClientInterfaceTemplate getInstance() { + return INSTANCE; + } + + public final void write(ServiceClient serviceClient, JavaFile javaFile) { + HashSet imports = new HashSet(); + serviceClient.addImportsTo(imports, false, false, JavaSettings.getInstance()); + javaFile.declareImport(imports); + + javaFile.javadocComment(comment -> + { + comment.description(String.format("The interface for %1$s class.", serviceClient.getInterfaceName())); + }); + javaFile.publicInterface(serviceClient.getInterfaceName(), interfaceBlock -> + { + for (ServiceClientProperty property : serviceClient.getProperties()) { + if (property.getMethodVisibility() == JavaVisibility.Public) { + interfaceBlock.javadocComment(comment -> + { + comment.description(String.format("Gets %1$s", property.getDescription())); + comment.methodReturns(String.format("the %1$s value", property.getName())); + }); + interfaceBlock.publicMethod(String.format("%1$s %2$s()", property.getType(), new ModelNamer().modelPropertyGetterName(property))); + + /* if (!property.isReadOnly()) { + interfaceBlock.javadocComment(comment -> + { + comment.description(String.format("Sets %1$s", property.getDescription())); + comment.param(property.getName(), String.format("the %1$s value", property.getName())); + comment.methodReturns("the service client itself"); + }); + interfaceBlock.publicMethod(String.format("%1$s set%2$s(%3$s %4$s)", serviceClient.getInterfaceName(), CodeNamer.toPascalCase(property.getName()), property.getType(), property.getName())); + } */ + } + } + + for (MethodGroupClient methodGroupClient : serviceClient.getMethodGroupClients()) { + interfaceBlock.javadocComment(comment -> + { + comment.description(String.format("Gets the %1$s object to access its operations.", methodGroupClient.getInterfaceName())); + comment.methodReturns(String.format("the %1$s object.", methodGroupClient.getInterfaceName())); + }); + interfaceBlock.publicMethod(String.format("%1$s get%2$s()", methodGroupClient.getInterfaceName(), CodeNamer.toPascalCase(methodGroupClient.getVariableName()))); + } + + for (ClientMethod clientMethod : serviceClient.getClientMethods()) { + Templates.getClientMethodTemplate().write(clientMethod, interfaceBlock); + } + }); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/ServiceClientTemplate.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/ServiceClientTemplate.java new file mode 100644 index 0000000000..6e3648f749 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/ServiceClientTemplate.java @@ -0,0 +1,332 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.template; + +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.Annotation; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClassType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethodParameter; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.Constructor; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.MethodGroupClient; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ServiceClient; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ServiceClientProperty; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaBlock; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaClass; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaFile; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaVisibility; +import com.microsoft.typespec.http.client.generator.core.template.prototype.MethodTemplate; +import com.microsoft.typespec.http.client.generator.core.util.ClientModelUtil; +import com.microsoft.typespec.http.client.generator.core.util.CodeNamer; +import com.microsoft.typespec.http.client.generator.core.util.ModelNamer; +import com.microsoft.typespec.http.client.generator.core.util.TemplateUtil; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Writes a ServiceClient to a JavaFile. + */ +public class ServiceClientTemplate implements IJavaTemplate { + + private static final ServiceClientTemplate INSTANCE = new ServiceClientTemplate(); + + // Extension for additional class methods + protected List additionalMethods = new ArrayList<>(); + + protected ServiceClientTemplate() { + } + + public static ServiceClientTemplate getInstance() { + return INSTANCE; + } + + public final void write(ServiceClient serviceClient, JavaFile javaFile) { + JavaSettings settings = JavaSettings.getInstance(); + String serviceClientClassDeclaration = String.format("%1$s", serviceClient.getClassName()); + if (settings.isFluentPremium()) { + serviceClientClassDeclaration += String.format(" extends %1$s", "AzureServiceClient"); + } + if (settings.isGenerateClientInterfaces()) { + serviceClientClassDeclaration += String.format(" implements %1$s", serviceClient.getInterfaceName()); + } + + Set imports = new HashSet(); + if (settings.isUseClientLogger()) { + ClassType.CLIENT_LOGGER.addImportsTo(imports, false); + } + + if (settings.isFluent() && !settings.isGenerateSyncAsyncClients()) { + addServiceClientAnnotationImport(imports); + imports.add(String.format("%1$s.%2$s", + ClientModelUtil.getServiceClientBuilderPackageName(serviceClient), + serviceClient.getInterfaceName() + ClientModelUtil.getBuilderSuffix())); + } else { + addSerializerImport(imports); + } + + serviceClient.addImportsTo(imports, true, false, settings); + additionalMethods.forEach(method -> method.addImportsTo(imports)); + javaFile.declareImport(imports); + + final JavaVisibility visibility = !serviceClient.isBuilderDisabled() + && serviceClient.getPackage().equals(ClientModelUtil.getServiceClientBuilderPackageName(serviceClient)) + ? JavaVisibility.PackagePrivate + : JavaVisibility.Public; + + javaFile.javadocComment(comment -> + { + String serviceClientTypeName = settings.isFluent() ? serviceClient.getClassName() : serviceClient.getInterfaceName(); + comment.description(String.format("Initializes a new instance of the %1$s type.", serviceClientTypeName)); + }); + if (settings.isFluent() && !settings.isGenerateSyncAsyncClients() && !settings.clientBuilderDisabled()) { + javaFile.annotation(String.format("ServiceClient(builder = %s.class)", + serviceClient.getInterfaceName() + ClientModelUtil.getBuilderSuffix())); + } + javaFile.publicFinalClass(serviceClientClassDeclaration, classBlock -> + { + // Add proxy service member variable + if (serviceClient.getProxy() != null) { + classBlock.javadocComment("The proxy service used to perform REST calls."); + classBlock.privateFinalMemberVariable(serviceClient.getProxy().getName(), "service"); + } + + // Add ServiceClient client property variables, getters, and setters + for (ServiceClientProperty serviceClientProperty : serviceClient.getProperties()) { + classBlock.javadocComment(comment -> + { + comment.description(serviceClientProperty.getDescription()); + }); + classBlock.privateFinalMemberVariable(serviceClientProperty.getType().toString(), serviceClientProperty.getName()); + + classBlock.javadocComment(comment -> + { + comment.description(String.format("Gets %1$s", serviceClientProperty.getDescription())); + comment.methodReturns(String.format("the %1$s value.", serviceClientProperty.getName())); + }); + classBlock.method(serviceClientProperty.getMethodVisibility(), null, String.format("%1$s %2$s()", + serviceClientProperty.getType(), new ModelNamer().modelPropertyGetterName(serviceClientProperty)), function -> + { + function.methodReturn(String.format("this.%1$s", serviceClientProperty.getName())); + }); + + /* if (!serviceClientProperty.isReadOnly()) { + classBlock.javadocComment(comment -> + { + comment.description(String.format("Sets %1$s", serviceClientProperty.getDescription())); + comment.param(serviceClientProperty.getName(), String.format("the %1$s value.", serviceClientProperty.getName())); + comment.methodReturns("the service client itself"); + }); + + String methodSignature = String.format("%1$s set%2$s(%3$s %4$s)", + serviceClient.getClassName(), CodeNamer.toPascalCase(serviceClientProperty.getName()), + serviceClientProperty.getType(), serviceClientProperty.getName()); + + Consumer methodBody = function -> + { + function.line(String.format("this.%1$s = %2$s;", serviceClientProperty.getName(), + serviceClientProperty.getName())); + function.methodReturn("this"); + }; + classBlock.method(visibility, null, methodSignature, methodBody); + } */ + } + + // AutoRestMethod Group Client declarations and getters + for (MethodGroupClient methodGroupClient : serviceClient.getMethodGroupClients()) { + classBlock.javadocComment(comment -> + { + comment.description(String.format("The %1$s object to access its operations.", methodGroupClient.getVariableType())); + }); + classBlock.privateFinalMemberVariable(methodGroupClient.getVariableType(), methodGroupClient.getVariableName()); + + classBlock.javadocComment(comment -> + { + comment.description(String.format("Gets the %1$s object to access its operations.", methodGroupClient.getVariableType())); + comment.methodReturns(String.format("the %1$s object.", methodGroupClient.getVariableType())); + }); + classBlock.publicMethod(String.format("%1$s get%2$s()", methodGroupClient.getVariableType(), + CodeNamer.toPascalCase(methodGroupClient.getVariableName())), function -> + { + function.methodReturn(String.format("this.%1$s", methodGroupClient.getVariableName())); + }); + } + + // additional service client properties in constructor arguments + String constructorArgs = serviceClient.getProperties().stream() + .filter(p -> !p.isReadOnly()) + .map(ServiceClientProperty::getName) + .collect(Collectors.joining(", ")); + if (!constructorArgs.isEmpty()) { + constructorArgs = ", " + constructorArgs; + } + final String constructorArgsFinal = constructorArgs; + // code lines + Consumer constructorParametersCodes = javaBlock -> { + serviceClient.getProperties().stream() + .filter(p -> !p.isReadOnly()).forEach(p -> javaBlock.line(String.format("this.%1$s = %2$s;", p.getName(), p.getName()))); + }; + + // Service Client Constructors + //boolean serviceClientUsesCredentials = serviceClient.getConstructors().stream().anyMatch(constructor -> constructor.getParameters().contains(serviceClient.getTokenCredentialParameter())); + for (Constructor constructor : serviceClient.getConstructors()) { + classBlock.javadocComment(comment -> + { + comment.description(String.format("Initializes an instance of %1$s client.", serviceClient.getInterfaceName())); + for (ClientMethodParameter parameter : constructor.getParameters()) { + comment.param(parameter.getName(), parameter.getDescription()); + } + for (ServiceClientProperty property : serviceClient.getProperties().stream() + .filter(p -> !p.isReadOnly()) + .collect(Collectors.toList())) { + comment.param(property.getName(), property.getDescription()); + } + }); + + // service client properties in constructor parameters + String constructorParams = Stream.concat(constructor.getParameters().stream().map(ClientMethodParameter::getDeclaration), + serviceClient.getProperties().stream() + .filter(p -> !p.isReadOnly()) + .map(p -> String.format("%1$s %2$s", p.getType(), p.getName()))) + .collect(Collectors.joining(", ")); + + classBlock.constructor(visibility, String.format("%1$s(%2$s)", serviceClient.getClassName(), constructorParams), constructorBlock -> + { + if (!settings.isBranded()) { + if (constructor.getParameters().equals(Arrays.asList(serviceClient.getHttpPipelineParameter()))) { + for (ServiceClientProperty serviceClientProperty : serviceClient.getProperties().stream().collect(Collectors.toList())) { + if (serviceClientProperty.getDefaultValueExpression() != null) { + constructorBlock.line("this.%s = %s;", serviceClientProperty.getName(), serviceClientProperty.getDefaultValueExpression()); + } else { + constructorBlock.line("this.%s = %s;", serviceClientProperty.getName(), serviceClientProperty.getName()); + } + } + + for (MethodGroupClient methodGroupClient : serviceClient.getMethodGroupClients()) { + constructorBlock.line("this.%s = new %s(this);", methodGroupClient.getVariableName(), methodGroupClient.getClassName()); + } + + if (serviceClient.getProxy() != null) { + TemplateHelper.createRestProxyInstance(this, serviceClient, constructorBlock); + } + } + } else if (settings.isFluent()) { + if (constructor.getParameters().equals(Arrays.asList(serviceClient.getHttpPipelineParameter(), serviceClient.getSerializerAdapterParameter(), serviceClient.getDefaultPollIntervalParameter(), serviceClient.getAzureEnvironmentParameter()))) { + if (settings.isFluentPremium()) { + constructorBlock.line(String.format("super(%1$s, %2$s, %3$s);", serviceClient.getHttpPipelineParameter().getName(), + serviceClient.getSerializerAdapterParameter().getName(), + serviceClient.getAzureEnvironmentParameter().getName())); + } + constructorBlock.line("this.httpPipeline = httpPipeline;"); + constructorBlock.line("this.serializerAdapter = serializerAdapter;"); + constructorBlock.line("this.defaultPollInterval = defaultPollInterval;"); + + constructorParametersCodes.accept(constructorBlock); + + for (ServiceClientProperty serviceClientProperty : serviceClient.getProperties().stream().filter(ServiceClientProperty::isReadOnly).collect(Collectors.toList())) { + if (serviceClientProperty.getDefaultValueExpression() != null) { + constructorBlock.line(String.format("this.%1$s = %2$s;", serviceClientProperty.getName(), serviceClientProperty.getDefaultValueExpression())); + } + } + + for (MethodGroupClient methodGroupClient : serviceClient.getMethodGroupClients()) { + constructorBlock.line(String.format("this.%1$s = new %2$s(this);", methodGroupClient.getVariableName(), methodGroupClient.getClassName())); + } + + if (serviceClient.getProxy() != null) { + constructorBlock.line(String.format("this.service = %1$s.create(%2$s.class, this.httpPipeline, %3$s);", ClassType.REST_PROXY.getName(), serviceClient.getProxy().getName(), getSerializerPhrase())); + } + } + } else { + if (constructor.getParameters().isEmpty()) { + final String initializeRetryPolicy = writeRetryPolicyInitialization(); + final String initializeSerializer = writeSerializerInitialization(); + constructorBlock.line("this(new HttpPipelineBuilder().policies(new UserAgentPolicy(), %1$s).build(), %2$s%3$s);", initializeRetryPolicy, initializeSerializer, constructorArgsFinal); + } else if (constructor.getParameters().equals(Arrays.asList(serviceClient.getHttpPipelineParameter()))) { + final String createDefaultSerializerAdapter = writeSerializerInitialization(); + constructorBlock.line("this(httpPipeline, %1$s%2$s);", createDefaultSerializerAdapter, constructorArgsFinal); + } else if (constructor.getParameters().equals(Arrays.asList(serviceClient.getHttpPipelineParameter(), serviceClient.getSerializerAdapterParameter()))) { + constructorBlock.line("this.httpPipeline = httpPipeline;"); + writeSerializerMemberInitialization(constructorBlock); + constructorParametersCodes.accept(constructorBlock); + + for (ServiceClientProperty serviceClientProperty : serviceClient.getProperties().stream().filter(ServiceClientProperty::isReadOnly).collect(Collectors.toList())) { + if (serviceClientProperty.getDefaultValueExpression() != null) { + constructorBlock.line("this.%s = %s;", serviceClientProperty.getName(), serviceClientProperty.getDefaultValueExpression()); + } + } + + for (MethodGroupClient methodGroupClient : serviceClient.getMethodGroupClients()) { + constructorBlock.line("this.%s = new %s(this);", methodGroupClient.getVariableName(), methodGroupClient.getClassName()); + } + + if (serviceClient.getProxy() != null) { + TemplateHelper.createRestProxyInstance(this, serviceClient, constructorBlock); + } + } + } + }); + } + + Templates.getProxyTemplate().write(serviceClient.getProxy(), classBlock); + + TemplateUtil.writeClientMethodsAndHelpers(classBlock, serviceClient.getClientMethods()); + + additionalMethods.forEach(method -> method.writeMethod(classBlock)); + + this.writeAdditionalClassBlock(classBlock); + + if (settings.isUseClientLogger()) { + TemplateUtil.addClientLogger(classBlock, serviceClient.getClassName(), javaFile.getContents()); + } + }); + } + + protected String getSerializerPhrase() { + if (JavaSettings.getInstance().isBranded()) { + return "this.getSerializerAdapter()"; + } + return "RestProxyUtils.createDefaultSerializer()"; + } + + protected void writeSerializerMemberInitialization(JavaBlock constructorBlock) { + if (JavaSettings.getInstance().isBranded()) { + constructorBlock.line("this.serializerAdapter = serializerAdapter;"); + } + } + + protected String writeRetryPolicyInitialization() { + return "new RetryPolicy()"; + } + + protected String writeSerializerInitialization() { + if (JavaSettings.getInstance().isBranded()) { + return "JacksonAdapter.createDefaultSerializerAdapter()"; + } else { + return null; + } + } + + protected void addSerializerImport(Set imports) { + if (JavaSettings.getInstance().isBranded()) { + imports.add("com.azure.core.util.serializer.JacksonAdapter"); + } + } + + protected void addServiceClientAnnotationImport(Set imports) { + Annotation.SERVICE_CLIENT.addImportsTo(imports); + } + + /** + * Extention for additional code in class. + * @param classBlock the class block. + */ + protected void writeAdditionalClassBlock(JavaClass classBlock) { + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/ServiceSyncClientTemplate.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/ServiceSyncClientTemplate.java new file mode 100644 index 0000000000..b4d5436116 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/ServiceSyncClientTemplate.java @@ -0,0 +1,199 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.template; + +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.Annotation; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.AsyncSyncClient; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethod; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ConvenienceMethod; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.GenericType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.MethodGroupClient; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ServiceClient; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaClass; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaContext; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaFile; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaVisibility; +import com.microsoft.typespec.http.client.generator.core.util.ClientModelUtil; +import com.microsoft.typespec.http.client.generator.core.util.TemplateUtil; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Template to create a synchronous client. + */ +public class ServiceSyncClientTemplate implements IJavaTemplate { + + private static final ServiceSyncClientTemplate INSTANCE = new ServiceSyncClientTemplate(); + + protected ServiceSyncClientTemplate() { + } + + public static ServiceSyncClientTemplate getInstance() { + return INSTANCE; + } + + @Override + public final void write(AsyncSyncClient syncClient, JavaFile javaFile) { + final ServiceClient serviceClient = syncClient.getServiceClient(); + + JavaSettings settings = JavaSettings.getInstance(); + final String syncClassName = syncClient.getClassName(); + final MethodGroupClient methodGroupClient = syncClient.getMethodGroupClient(); + final boolean wrapServiceClient = methodGroupClient == null; + final String builderPackageName = ClientModelUtil.getServiceClientBuilderPackageName(serviceClient); + final String builderClassName = serviceClient.getInterfaceName() + ClientModelUtil.getBuilderSuffix(); + final boolean samePackageAsBuilder = builderPackageName.equals(syncClient.getPackageName()); + final JavaVisibility constructorVisibility = samePackageAsBuilder ? JavaVisibility.PackagePrivate : JavaVisibility.Public; + + Set imports = new HashSet<>(); + if (wrapServiceClient) { + serviceClient.addImportsTo(imports, true, false, settings); + imports.add(serviceClient.getPackage() + "." + serviceClient.getClassName()); + } else { + methodGroupClient.addImportsTo(imports, true, settings); + imports.add(methodGroupClient.getPackage() + "." + methodGroupClient.getClassName()); + } + imports.add(builderPackageName + "." + builderClassName); + addServiceClientAnnotationImport(imports); + + Templates.getConvenienceSyncMethodTemplate().addImports(imports, syncClient.getConvenienceMethods()); + + javaFile.declareImport(imports); + javaFile.javadocComment(comment -> + comment.description(String.format("Initializes a new instance of the synchronous %1$s type.", + serviceClient.getInterfaceName()))); + + if (syncClient.getClientBuilder() != null) { + javaFile.annotation(String.format("ServiceClient(builder = %s.class)", syncClient.getClientBuilder().getClassName())); + } + javaFile.publicFinalClass(syncClassName, classBlock -> { + writeClass(syncClient, classBlock, constructorVisibility); + + if (JavaSettings.getInstance().isUseClientLogger()) { + TemplateUtil.addClientLogger(classBlock, syncClassName, javaFile.getContents()); + } + }); + } + + /** + * Extension to write the sync client class. + * + * @param syncClient the sync client + * @param classBlock the class block to write + * @param constructorVisibility the visibility of class constructor + */ + protected void writeClass(AsyncSyncClient syncClient, JavaClass classBlock, JavaVisibility constructorVisibility) { + final ServiceClient serviceClient = syncClient.getServiceClient(); + final MethodGroupClient methodGroupClient = syncClient.getMethodGroupClient(); + final boolean wrapServiceClient = methodGroupClient == null; + + // Add service client member + addGeneratedAnnotation(classBlock); + if (wrapServiceClient) { + classBlock.privateFinalMemberVariable(serviceClient.getClassName(), "serviceClient"); + } else { + classBlock.privateFinalMemberVariable(methodGroupClient.getClassName(), "serviceClient"); + } + + // Service Client Constructor + classBlock.javadocComment(comment -> { + comment.description(String.format("Initializes an instance of %1$s class.", syncClient.getClassName())); + comment.param("serviceClient", "the service client implementation."); + }); + addGeneratedAnnotation(classBlock); + if (wrapServiceClient) { + classBlock.constructor(constructorVisibility, String.format("%1$s(%2$s %3$s)", syncClient.getClassName(), + serviceClient.getClassName(), "serviceClient"), constructorBlock -> { + constructorBlock.line("this.serviceClient = serviceClient;"); + }); + } else { + classBlock.constructor(constructorVisibility, String.format("%1$s(%2$s %3$s)", syncClient.getClassName(), + methodGroupClient.getClassName(), "serviceClient"), constructorBlock -> { + constructorBlock.line("this.serviceClient = serviceClient;"); + }); + } + + writeMethods(syncClient, classBlock); + } + + /** + * Extension to write the sync client methods. + * + * @param syncClient the sync client + * @param classBlock the class block to write + */ + protected void writeMethods(AsyncSyncClient syncClient, JavaClass classBlock) { + final ServiceClient serviceClient = syncClient.getServiceClient(); + final MethodGroupClient methodGroupClient = syncClient.getMethodGroupClient(); + + final boolean useMethodGroupClient = methodGroupClient != null; + List clientMethods = serviceClient.getClientMethods(); + if(useMethodGroupClient) { + clientMethods = methodGroupClient.getClientMethods(); + } + + clientMethods.stream() + .filter(clientMethod -> clientMethod.getMethodVisibility() == JavaVisibility.Public) + .filter(clientMethod -> !clientMethod.isImplementationOnly()) + .filter(clientMethod -> !clientMethod.getType().name().contains("Async")) + .forEach(clientMethod -> { + writeMethod(clientMethod, classBlock); + }); + + writeConvenienceMethods(syncClient.getConvenienceMethods(), classBlock); + + ServiceAsyncClientTemplate.addEndpointMethod(classBlock, syncClient.getClientBuilder(), serviceClient, this.clientReference()); + } + + /** + * Extension for client reference. Usually be either "this.serviceClient" or "this.client". + * + * @return the code for client reference. + */ + protected String clientReference() { + return "this.serviceClient"; + } + + /** + * Extension to write the sync client method. + * + * @param clientMethod the client method in implementation class + * @param classBlock the class block to write + */ + protected void writeMethod(ClientMethod clientMethod, JavaClass classBlock) { + Templates.getWrapperClientMethodTemplate().write(clientMethod, classBlock); + } + + protected void addServiceClientAnnotationImport(Set imports) { + Annotation.SERVICE_CLIENT.addImportsTo(imports); + if (JavaSettings.getInstance().isBranded()) { + Annotation.GENERATED.addImportsTo(imports); + } else { + Annotation.METADATA.addImportsTo(imports); + } + } + + protected void addGeneratedAnnotation(JavaContext classBlock) { + if (JavaSettings.getInstance().isBranded()) { + classBlock.annotation(Annotation.GENERATED.getName()); + } else { + classBlock.annotation(Annotation.METADATA.getName() + "(generated = true)"); + } + } + + private void writeConvenienceMethods(List convenienceMethods, JavaClass classBlock) { + Set typeReferenceStaticClasses = new HashSet<>(); + + convenienceMethods.forEach(m -> Templates.getConvenienceSyncMethodTemplate().write(m, classBlock, typeReferenceStaticClasses)); + + // static variables for TypeReference + for (GenericType typeReferenceStaticClass : typeReferenceStaticClasses) { + addGeneratedAnnotation(classBlock); + TemplateUtil.writeTypeReferenceStaticVariable(classBlock, typeReferenceStaticClass); + } + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/ServiceSyncClientWrapAsyncClientTemplate.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/ServiceSyncClientWrapAsyncClientTemplate.java new file mode 100644 index 0000000000..76854c290c --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/ServiceSyncClientWrapAsyncClientTemplate.java @@ -0,0 +1,98 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.template; + +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.AsyncSyncClient; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethod; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethodParameter; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaBlock; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaClass; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaVisibility; +import com.microsoft.typespec.http.client.generator.core.util.ClientModelUtil; + +import java.util.List; +import java.util.stream.Collectors; + +public class ServiceSyncClientWrapAsyncClientTemplate extends ServiceSyncClientTemplate { + + private static final ServiceSyncClientTemplate INSTANCE = new ServiceSyncClientWrapAsyncClientTemplate(); + + public static ServiceSyncClientTemplate getInstance() { + return INSTANCE; + } + + private static final String ASYNC_CLIENT_VAR_NAME = "client"; + + @Override + protected void writeClass(AsyncSyncClient syncClient, JavaClass classBlock, JavaVisibility constructorVisibility) { + // class variable + String asyncClassName = ClientModelUtil.clientNameToAsyncClientName(syncClient.getClassName()); + + addGeneratedAnnotation(classBlock); + classBlock.privateFinalMemberVariable(asyncClassName, ASYNC_CLIENT_VAR_NAME); + + // constructor + classBlock.javadocComment(comment -> { + comment.description(String.format("Initializes an instance of %1$s class.", syncClient.getClassName())); + comment.param(ASYNC_CLIENT_VAR_NAME, "the async client."); + }); + addGeneratedAnnotation(classBlock); + classBlock.constructor(constructorVisibility, String.format("%1$s(%2$s %3$s)", syncClient.getClassName(), + asyncClassName, ASYNC_CLIENT_VAR_NAME), constructorBlock -> { + constructorBlock.line(String.format("this.%1$s = %1$s;", ASYNC_CLIENT_VAR_NAME)); + }); + + // methods + writeMethods(syncClient, classBlock); + } + + protected String clientReference() { + return "this." + ASYNC_CLIENT_VAR_NAME; + } + + @Override + protected void writeMethod(ClientMethod clientMethod, JavaClass classBlock) { + METHOD_TEMPLATE_INSTANCE.write(clientMethod, classBlock); + } + + private static final WrapperClientMethodTemplate METHOD_TEMPLATE_INSTANCE = new ClientMethodTemplateImpl(); + + private static class ClientMethodTemplateImpl extends WrapperClientMethodTemplate { + + private String clientReference() { + return "this." + ASYNC_CLIENT_VAR_NAME; + } + + @Override + protected void writeMethodInvocation(ClientMethod clientMethod, JavaBlock function, boolean shouldReturn) { + List parameterNames = clientMethod.getMethodInputParameters().stream() + .map(ClientMethodParameter::getName).collect(Collectors.toList()); + + String methodInvoke = String.format("%1$s.%2$s(%3$s)", + this.clientReference(), clientMethod.getName(), String.join(", ", parameterNames)); + switch (clientMethod.getType()) { + case PagingSync: + methodInvoke = "new PagedIterable<>(" + methodInvoke + ")"; + break; + + case LongRunningBeginSync: + methodInvoke = methodInvoke + ".getSyncPoller()"; + break; + + case SendRequestSync: + parameterNames.remove("context"); + methodInvoke = String.format("%1$s.%2$s(%3$s)", + this.clientReference(), clientMethod.getName(), String.join(", ", parameterNames)); + methodInvoke = methodInvoke + ".contextWrite(c -> c.putAll(FluxUtil.toReactorContext(context).readOnly())).block()"; + break; + + default: + methodInvoke = methodInvoke + ".block()"; + break; + } + + function.line((shouldReturn ? "return " : "") + methodInvoke + ";"); + } + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/ServiceVersionTemplate.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/ServiceVersionTemplate.java new file mode 100644 index 0000000000..299dcd28b8 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/ServiceVersionTemplate.java @@ -0,0 +1,75 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.template; + +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ServiceVersion; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaFile; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaJavadocComment; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.regex.Pattern; + +public class ServiceVersionTemplate implements IJavaTemplate { + private static final ServiceVersionTemplate INSTANCE = new ServiceVersionTemplate(); + private static final Pattern VERSION_TO_ENUM = Pattern.compile("[-.]"); + + public static ServiceVersionTemplate getInstance() { + return INSTANCE; + } + + @Override + public void write(ServiceVersion serviceVersion, JavaFile javaFile) { + // imports + Set imports = new HashSet<>(); + imports.add("com.azure.core.util.ServiceVersion"); + javaFile.declareImport(imports); + + javaFile.javadocComment(comment -> { + comment.description("Service version of " + serviceVersion.getServiceName()); + }); + + String className = serviceVersion.getClassName(); + List serviceVersions = serviceVersion.getServiceVersions(); + + javaFile.publicEnum(className + " implements ServiceVersion", classBlock -> { + serviceVersions.forEach(v -> { + classBlock.value(getVersionIdentifier(v), v); + }); + + classBlock.privateFinalMemberVariable("String", "version"); + + classBlock.constructor( + className + "(String version)", + javaBlock -> javaBlock.line("this.version = version;") + ); + + classBlock.javadocComment(JavaJavadocComment::inheritDoc); + classBlock.annotation("Override"); + classBlock.publicMethod( + "String getVersion()", + javaBlock -> javaBlock.line("return this.version;") + ); + + classBlock.javadocComment(comment -> { + comment.description("Gets the latest service version supported by this client library"); + comment.methodReturns(String.format("The latest {@link %s}", className)); + }); + classBlock.publicStaticMethod( + className + " getLatest()", + javaBlock -> javaBlock.methodReturn( + getVersionIdentifier(serviceVersions.get(serviceVersions.size() - 1))) + ); + }); + } + + private String getVersionIdentifier(String version) { + String versionInEnum = VERSION_TO_ENUM.matcher(version).replaceAll("_").toUpperCase(); + if (!versionInEnum.startsWith("V")) { + versionInEnum = "V" + versionInEnum; + } + return versionInEnum; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/StreamSerializationModelTemplate.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/StreamSerializationModelTemplate.java new file mode 100644 index 0000000000..3af737c0ae --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/StreamSerializationModelTemplate.java @@ -0,0 +1,2356 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.template; + +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; +import com.microsoft.typespec.http.client.generator.core.implementation.ClientModelPropertiesManager; +import com.microsoft.typespec.http.client.generator.core.implementation.ClientModelPropertyWithMetadata; +import com.microsoft.typespec.http.client.generator.core.implementation.JsonFlattenedPropertiesTree; +import com.microsoft.typespec.http.client.generator.core.mapper.ModelMapper; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClassType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientModel; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientModelProperty; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientModelPropertyAccess; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientModelPropertyReference; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.IType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.IterableType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.MapType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.PrimitiveType; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaBlock; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaClass; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaFile; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaIfBlock; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaJavadocComment; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaVisibility; +import com.microsoft.typespec.http.client.generator.core.util.ClientModelUtil; +import com.azure.core.util.CoreUtils; +import com.azure.xml.XmlReader; +import com.azure.xml.XmlSerializable; +import com.azure.xml.XmlToken; +import com.azure.xml.XmlWriter; + +import javax.xml.namespace.QName; +import javax.xml.stream.XMLStreamException; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Base64; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static com.microsoft.typespec.http.client.generator.core.util.ClientModelUtil.JSON_MERGE_PATCH_HELPER_CLASS_NAME; +import static com.microsoft.typespec.http.client.generator.core.util.ClientModelUtil.includePropertyInConstructor; + +/** + * Writes a ClientModel to a JavaFile using stream-style serialization. + */ +public class StreamSerializationModelTemplate extends ModelTemplate { + private static final StreamSerializationModelTemplate INSTANCE = new StreamSerializationModelTemplate(); + private static final String READ_MANAGEMENT_ERROR_METHOD_NAME = "readManagementError"; + + // TODO (alzimmer): Future enhancements: + // - Create a utility class in the implementation package containing base serialization for polymorphic types. + // This will enable a central location for shared logic, reducing package size and hopefully JIT optimizations. + // - Convert all logic in this class to an instance type that is created with the ClientModel being generated. + // This will simplify all the APIs to just taking that type rather than passing bits of information from here + // and there everywhere, which require extensive changes each time a new feature is added. + + protected StreamSerializationModelTemplate() { + } + + public static StreamSerializationModelTemplate getInstance() { + return INSTANCE; + } + + @Override + protected void addSerializationImports(Set imports, ClientModel model, JavaSettings settings) { + if (model.getXmlName() != null) { + imports.add(QName.class.getName()); + imports.add(XMLStreamException.class.getName()); + + imports.add(XmlSerializable.class.getName()); + imports.add(XmlWriter.class.getName()); + imports.add(XmlReader.class.getName()); + imports.add(XmlToken.class.getName()); + } else { + imports.add(IOException.class.getName()); + + ClassType.JSON_SERIALIZABLE.addImportsTo(imports, false); + ClassType.JSON_WRITER.addImportsTo(imports, false); + ClassType.JSON_READER.addImportsTo(imports, false); + ClassType.JSON_TOKEN.addImportsTo(imports, false); + } + + ClassType.CORE_UTILS.addImportsTo(imports, false); + + imports.add(ArrayList.class.getName()); + imports.add(Base64.class.getName()); + imports.add(LinkedHashMap.class.getName()); + imports.add(List.class.getName()); + imports.add(Map.class.getName()); + imports.add(Objects.class.getName()); + } + + @Override + protected void handlePolymorphism(ClientModel model, boolean hasDerivedModels, JavaFile javaFile) { + // no-op as stream-style serialization doesn't need to add anything for polymorphic types. + } + + @Override + protected void addClassLevelAnnotations(ClientModel model, JavaFile javaFile, JavaSettings settings) { + // no-op as stream-style serialization doesn't add any class-level annotations. + } + + @Override + protected String addSerializationImplementations(String classSignature, ClientModel model, JavaSettings settings) { + if (!settings.isStreamStyleSerialization() || model.isStronglyTypedHeader()) { + return classSignature; + } + + String interfaceName = (model.getXmlName() != null) + ? XmlSerializable.class.getSimpleName() + : ClassType.JSON_SERIALIZABLE.getName(); + + return classSignature + " implements " + interfaceName + "<" + model.getName() + ">"; + } + + @Override + protected void addXmlNamespaceConstants(ClientModel model, JavaClass classBlock) { + if (model.getXmlName() == null) { + return; + } + + Map constantMap = ClientModelUtil.xmlNamespaceToConstantMapping(model); + for (Map.Entry constant : constantMap.entrySet()) { + classBlock.privateStaticFinalVariable("String " + constant.getValue() + " = \"" + constant.getKey() + "\""); + } + } + + static void xmlWrapperClassXmlSerializableImplementation(JavaClass classBlock, String wrapperClassName, + IType iterableType, String xmlRootElementName, String xmlRootElementNamespace, String xmlListElementName, + String xmlElementNameCamelCase, String xmlListElementNamespace, Consumer addGeneratedAnnotation) { + IType elementType = ((IterableType) iterableType).getElementType(); + + addGeneratedAnnotation.accept(classBlock); + classBlock.annotation("Override"); + classBlock.publicMethod("XmlWriter toXml(XmlWriter xmlWriter) throws XMLStreamException", + methodBlock -> methodBlock.methodReturn("toXml(xmlWriter, null)")); + + addGeneratedAnnotation.accept(classBlock); + classBlock.annotation("Override"); + classBlock.publicMethod("XmlWriter toXml(XmlWriter xmlWriter, String rootElementName) throws XMLStreamException", writerMethod -> { + writerMethod.line("rootElementName = CoreUtils.isNullOrEmpty(rootElementName) ? \"" + xmlRootElementName + "\" : rootElementName;"); + String writeStartElement = (xmlRootElementNamespace != null) + ? "xmlWriter.writeStartElement(\"" + xmlRootElementNamespace + "\", rootElementName);" + : "xmlWriter.writeStartElement(rootElementName);"; + writerMethod.line(writeStartElement); + + writerMethod.ifBlock(xmlElementNameCamelCase + " != null", ifAction -> { + String xmlWrite = elementType.xmlSerializationMethodCall("xmlWriter", xmlListElementName, + xmlListElementNamespace, "element", false, false, false); + ifAction.line("for (%s element : %s) {", elementType, xmlElementNameCamelCase); + ifAction.indent(() -> ifAction.line(xmlWrite + ";")); + ifAction.line("}"); + }); + + writerMethod.methodReturn("xmlWriter.writeEndElement()"); + }); + + addGeneratedAnnotation.accept(classBlock); + classBlock.publicStaticMethod(wrapperClassName + " fromXml(XmlReader xmlReader) throws XMLStreamException", + readerMethod -> readerMethod.methodReturn("fromXml(xmlReader, null)")); + + addGeneratedAnnotation.accept(classBlock); + classBlock.publicStaticMethod(wrapperClassName + " fromXml(XmlReader xmlReader, String rootElementName) throws XMLStreamException", readerMethod -> { + readerMethod.line("rootElementName = CoreUtils.isNullOrEmpty(rootElementName) ? \"" + xmlRootElementName + "\" : rootElementName;"); + String readObject = (xmlRootElementNamespace != null) + ? "return xmlReader.readObject(\"" + xmlRootElementNamespace + "\", rootElementName, reader -> {" + : "return xmlReader.readObject(rootElementName, reader -> {"; + + readerMethod.line(readObject); + readerMethod.indent(() -> { + readerMethod.line(iterableType + " items = null;"); + readerMethod.line(); + readerMethod.line("while (reader.nextElement() != XmlToken.END_ELEMENT) {"); + readerMethod.indent(() -> { + readerMethod.line("QName elementName = reader.getElementName();"); + String condition = getXmlNameConditional(xmlListElementName, xmlListElementNamespace, "elementName", false); + readerMethod.line(); + readerMethod.ifBlock(condition, ifBlock -> { + ifBlock.ifBlock("items == null", ifBlock2 -> ifBlock2.line("items = new ArrayList<>();")); + ifBlock.line(); + + // TODO (alzimmer): Insert XML object reading logic. + ifBlock.line("items.add(" + getSimpleXmlDeserialization(elementType, "reader", null, null, + null, false) + ");"); + }).elseBlock(elseBlock -> elseBlock.line("reader.nextElement();")); + }); + readerMethod.line("}"); + + readerMethod.methodReturn("new " + wrapperClassName + "(items)"); + }); + readerMethod.line("});"); + }); + } + + @Override + protected void addFieldAnnotations(ClientModel model, ClientModelProperty property, JavaClass classBlock, JavaSettings settings) { + // no-op as stream-style serialization doesn't add any field-level annotations. + } + + @Override + protected void writeStreamStyleSerialization(JavaClass classBlock, ClientModel model, JavaSettings settings) { + // Early out as strongly-typed headers do their own thing. + if (model.isStronglyTypedHeader()) { + return; + } + + ClientModelPropertiesManager propertiesManager = new ClientModelPropertiesManager(model, settings); + + if (model.getXmlName() != null) { + writeToXml(classBlock, propertiesManager, Templates.getModelTemplate()::addGeneratedAnnotation); + writeFromXml(classBlock, model, propertiesManager, settings, + Templates.getModelTemplate()::addGeneratedAnnotation); + } else { + if (ClientModelUtil.isJsonMergePatchModel(model, settings)) { + writeToJson(classBlock, propertiesManager, true, Templates.getModelTemplate()::addGeneratedAnnotation); + writeToJsonMergePatch(classBlock, propertiesManager, + Templates.getModelTemplate()::addGeneratedAnnotation); + } else { + writeToJson(classBlock, propertiesManager, false, Templates.getModelTemplate()::addGeneratedAnnotation); + } + writeFromJson(classBlock, model, propertiesManager, settings, + Templates.getModelTemplate()::addGeneratedAnnotation); + if (isManagementErrorSubclass(model, settings)) { + writeManagementErrorDeserializationMethod(classBlock, propertiesManager, settings, + Templates.getModelTemplate()::addGeneratedAnnotation); + } + } + } + + /** + * For stream-style-serialization, we generate shadow properties for read-only properties that's not in constructor. + * @param model the model to generate class of + * @param settings JavaSettings + * @return properties to generate as fields of the class + * @see ModelMapper#passPolymorphicDiscriminatorToChildren + */ + @Override + protected List getFieldProperties(ClientModel model, JavaSettings settings) { + List fieldProperties = super.getFieldProperties(model, settings); + Set propertySerializedNames = fieldProperties.stream().map(ClientModelProperty::getSerializedName).collect(Collectors.toSet()); + for (ClientModelProperty parentProperty : ClientModelUtil.getParentProperties(model, false)) { + if (propertySerializedNames.contains(parentProperty.getSerializedName())) { + continue; + } + propertySerializedNames.add(parentProperty.getSerializedName()); + if (!parentProperty.isPolymorphicDiscriminator() // parent discriminators are already passed to children, see @see in method javadoc + && readOnlyNotInCtor(model, parentProperty, settings) // we shadow parent read-only properties in child class + || parentProperty.getClientFlatten()) { // we shadow parent flattened property in child class + fieldProperties.add(parentProperty); + } + } + return fieldProperties; + } + + /** + * In stream-style-serialization, parent's read-only properties are shadowed in child classes. + * + * @param model the client model + * @param property the property to generate getter + * @param settings {@link JavaSettings} instance + * @param methodVisibility + * @return whether the property's getter overrides parent getter + */ + @Override + protected boolean overridesParentGetter(ClientModel model, ClientModelProperty property, JavaSettings settings, JavaVisibility methodVisibility) { + return !modelDefinesProperty(model, property) && (property.isPolymorphicDiscriminator() || readOnlyNotInCtor(model, property, settings)) + && methodVisibility == JavaVisibility.Public; + } + + /** + * Get the property reference referring to the local(field) flattened property. + * Additionally, in Stream-Style, parent property reference count as well. Since in Stream-Style, the flattened model property + * will be shadowed in child class. + * For example, for the property1FromParent collected by {@link #getClientModelPropertyReferences(ClientModel)} on model2, + * it looks like: + *

{@code
+     *         FlattenedProperties
+     *          - property1                    <--------------
+     *                                                       |
+     *         Model1                                        |
+     *          - innerProperties: FlattenProperties         |
+     *          - property1FromFlatten    ^    <--           |
+     *              - referenceProperty   |      |       ----|
+     *              - targetProperty    ---      |
+     *                                           |
+     *         Model2 extends Model1             |
+     *          (- property1FromParent)          |
+     *              - referenceProperty      ----|
+     *              - targetProperty -> null
+     * }
+     * 
+ * If called on property1FromParent collected from Model2, property1FromFlatten will be returned. + * If this method is called on property1FromFlatten collected from Model1, itself will be returned. + * + * @param propertyReference propertyReference collected by {@link #getClientModelPropertyReferences(ClientModel)} + * @return the property reference referring to the local(field) flattened property, or parent flattening property reference, + * null if neither + */ + @Override + protected ClientModelPropertyReference getLocalFlattenedModelPropertyReference(ClientModelPropertyReference propertyReference) { + if (propertyReference.isFromFlattenedProperty()) { + return propertyReference; + } else if (propertyReference.isFromParentModel()) { + ClientModelPropertyAccess parentProperty = propertyReference.getReferenceProperty(); // parent property + if (parentProperty instanceof ClientModelPropertyReference && ((ClientModelPropertyReference) parentProperty).isFromFlattenedProperty()) { + return (ClientModelPropertyReference) parentProperty; + } + } + // Not a flattening property, return null. + return null; + } + + @Override + protected List getSuperSetters(ClientModel model, JavaSettings settings, List propertyReferences) { + return super.getSuperSetters(model, settings, propertyReferences) + .stream() + // If the propertyReference is flattening property, then in Stream-Style we generate local getter/setter + // for it, thus we don't need to generate super setter. + .filter(propertyReference -> + !((propertyReference instanceof ClientModelPropertyReference) + && ((ClientModelPropertyReference) propertyReference).isFromFlattenedProperty())) + .collect(Collectors.toList()); + } + + private static boolean readOnlyNotInCtor(ClientModel model, ClientModelProperty property, JavaSettings settings) { + return // not required and in constructor + !(property.isRequired() && settings.isRequiredFieldsAsConstructorArgs()) + && ( + // must be read-only and not appear in constructor + (property.isReadOnly() && !settings.isIncludeReadOnlyInConstructorArgs()) + // immutable output model only has package-private setters, making its properties read-only + || isImmutableOutputModel(getDefiningModel(model, property), settings)); + } + + @Override + protected boolean callParentValidate(String parentModelName) { + // in stream-style-serialization, since there are shadowing involved, we validate all properties locally + return false; + } + + @Override + protected List getValidationProperties(ClientModel model) { + // in stream-style-serialization, since there are shadowing involved, we validate all properties locally + return Stream.concat(model.getProperties().stream(), ClientModelUtil.getParentProperties(model).stream()) + .collect(Collectors.toList()); + } + + /** + * write toJson() method. + *

+ * If it is a JsonMergePatch model, toJson() should first check jsonMergePatch flag value and then do serialization + * accordingly. + * + * @param classBlock The class block to write the toJson method to. + * @param propertiesManager The properties manager for the model. + * @param isJsonMergePatch Whether the serialization is for a JSON merge patch. + * @param addGeneratedAnnotation The consumer to add the generated annotation to the class block. + */ + private static void writeToJson(JavaClass classBlock, ClientModelPropertiesManager propertiesManager, + boolean isJsonMergePatch, Consumer addGeneratedAnnotation) { + classBlock.javadocComment(JavaJavadocComment::inheritDoc); + addGeneratedAnnotation.accept(classBlock); + classBlock.annotation("Override"); + classBlock.publicMethod("JsonWriter toJson(JsonWriter jsonWriter) throws IOException", methodBlock -> { + if (isJsonMergePatch) { + // If the model is the root parent use the JSON merge patch serialization tracking property directly, + // otherwise use the access helper to determine whether to use JSON merge patch serialization. + ClientModel rootParent = ClientModelUtil.getRootParent(propertiesManager.getModel()); + String ifStatement = (rootParent == propertiesManager.getModel()) + ? "jsonMergePatch" + : JSON_MERGE_PATCH_HELPER_CLASS_NAME + ".get" + rootParent.getName() + "Accessor().isJsonMergePatch(this)"; + + methodBlock.ifBlock(ifStatement, ifBlock -> ifBlock.methodReturn("toJsonMergePatch(jsonWriter)")) + .elseBlock(elseBlock -> serializeJsonProperties(methodBlock, propertiesManager, false)); + } else { + serializeJsonProperties(methodBlock, propertiesManager, false); + } + }); + } + + /** + * write toJsonMergePatch() method + * + * @param classBlock The class block to write the toJsonMergePatch method to. + * @param propertiesManager The properties manager for the model. + * @param addGeneratedAnnotation The consumer to add the generated annotation to the class block. + */ + private static void writeToJsonMergePatch(JavaClass classBlock, ClientModelPropertiesManager propertiesManager, + Consumer addGeneratedAnnotation) { + addGeneratedAnnotation.accept(classBlock); + classBlock.privateMethod("JsonWriter toJsonMergePatch(JsonWriter jsonWriter) throws IOException", + methodBlock -> serializeJsonProperties(methodBlock, propertiesManager, true)); + } + + /** + * Serializes the properties of a model to JSON. + * @param methodBlock The method block to write the serialization method to. + * @param propertiesManager The properties manager for the model. + * @param isJsonMergePatch Whether the serialization is for a JSON merge patch. + */ + private static void serializeJsonProperties(JavaBlock methodBlock, ClientModelPropertiesManager propertiesManager, + boolean isJsonMergePatch) { + methodBlock.line("jsonWriter.writeStartObject();"); + + BiConsumer serializeJsonProperty = (property, fromSuper) -> serializeJsonProperty( + methodBlock, property, property.getSerializedName(), fromSuper, true, isJsonMergePatch); + + propertiesManager.getModel().getParentPolymorphicDiscriminators() + .forEach(discriminator -> serializeJsonProperty.accept(discriminator, false)); + propertiesManager.forEachSuperRequiredProperty(property -> serializeJsonProperty.accept(property, true)); + propertiesManager.forEachSuperSetterProperty(property -> serializeJsonProperty.accept(property, true)); + propertiesManager.forEachRequiredProperty(property -> serializeJsonProperty.accept(property, false)); + propertiesManager.forEachSetterProperty(property -> serializeJsonProperty.accept(property, false)); + + handleFlattenedPropertiesSerialization(methodBlock, propertiesManager.getJsonFlattenedPropertiesTree(), isJsonMergePatch); + + if (getAdditionalPropertiesPropertyInModelOrFromSuper(propertiesManager) != null) { + String additionalPropertiesAccessExpr = propertiesManager.getAdditionalProperties() != null + ? propertiesManager.getAdditionalProperties().getName() + : propertiesManager.getSuperAdditionalPropertiesProperty().getGetterName() + "()"; + IType wireType = propertiesManager.getAdditionalProperties() != null + ? propertiesManager.getAdditionalProperties().getWireType() + : propertiesManager.getSuperAdditionalPropertiesProperty().getWireType(); + + methodBlock.ifBlock(additionalPropertiesAccessExpr + " != null", ifAction -> { + IType valueType = ((MapType) wireType).getValueType().asNullable(); + ifAction.line("for (Map.Entry additionalProperty : %s.entrySet()) {", valueType, additionalPropertiesAccessExpr); + ifAction.indent(() -> { + if (valueType == ClassType.BINARY_DATA) { + // Special handling for BinaryData + ifAction.line("jsonWriter.writeUntypedField(additionalProperty.getKey(), additionalProperty.getValue() == null ? null : additionalProperty.getValue().toObject(Object.class));"); + } else { + ifAction.line("jsonWriter.writeUntypedField(additionalProperty.getKey(), additionalProperty.getValue());"); + } + }); + ifAction.line("}"); + }); + } + + methodBlock.methodReturn("jsonWriter.writeEndObject()"); + } + + /** + * Serializes a non-flattened, non-additional properties JSON property. + *

+ * If the JSON property needs to be flattened or is additional properties this is a no-op as those require special + * handling that will occur later. + * + * @param methodBlock The method handling serialization. + * @param property The property being serialized. + * @param serializedName The serialized JSON property name. Generally, this is just the {@code property property's} + * serialized name but if a flattened property is being serialized it'll be the last segment of the flattened JSON + * name. + * @param fromSuperType Whether the property is defined by a super type of the model. If the property is declared by + * a super type a getter method will be used to retrieve the value instead of accessing the field directly. + * @param ignoreFlattening Whether flattened properties should be skipped. Will only be false when handling the + * terminal location of a flattened structure. + * @param isJsonMergePatch Whether the serialization is for a JSON Merge Patch model. + */ + private static void serializeJsonProperty(JavaBlock methodBlock, ClientModelProperty property, + String serializedName, boolean fromSuperType, boolean ignoreFlattening, boolean isJsonMergePatch) { + if ((ignoreFlattening && property.getNeedsFlatten()) || property.isAdditionalProperties()) { + // Property will be handled later by flattened or additional properties serialization. + return; + } + + if (property.isReadOnly() && !property.isPolymorphicDiscriminator()) { + // Non-polymorphic discriminator, readonly properties are never serialized. + return; + } + + if (isJsonMergePatch) { + if (!property.isPolymorphicDiscriminator()) { + methodBlock.ifBlock("updatedProperties.contains(\"" + property.getName() + "\")", codeBlock -> { + if (property.getClientType().isNullable()) { + codeBlock.ifBlock(getPropertyGetterStatement(property, fromSuperType) + " == null", + ifBlock -> ifBlock.line("jsonWriter.writeNullField(\"" + property.getSerializedName() + "\");")) + .elseBlock(elseBlock -> serializeJsonProperty(codeBlock, property, serializedName, fromSuperType, true)); + } else { + serializeJsonProperty(codeBlock, property, serializedName, fromSuperType, true, false); + } + }); + } else { + serializeJsonProperty(methodBlock, property, serializedName, fromSuperType, true); + } + } else { + serializeJsonProperty(methodBlock, property, serializedName, fromSuperType, false); + } + } + + /** + * Serializes a non-flattened, non-additional properties JSON property. + *

+ * If the JSON property needs to be flattened or is additional properties this is a no-op as those require special + * handling that will occur later. + * + * @param methodBlock The method handling serialization. + * @param property The property being serialized. + * @param serializedName The serialized JSON property name. Generally, this is just the {@code property property's} + * serialized name but if a flattened property is being serialized it'll be the last segment of the flattened JSON + * name. + * @param fromSuperType Whether the property is defined by a super type of the model. If the property is declared by + * a super type a getter method will be used to retrieve the value instead of accessing the field directly. + * @param isJsonMergePatch Whether the serialization is for a JSON Merge Patch model. + */ + private static void serializeJsonProperty(JavaBlock methodBlock, ClientModelProperty property, + String serializedName, boolean fromSuperType, boolean isJsonMergePatch) { + IType clientType = property.getClientType(); + IType wireType = property.getWireType(); + String propertyValueGetter = getPropertyGetterStatement(property, fromSuperType); + + // Attempt to determine whether the wire type is simple serialization. + // This is primitives, boxed primitives, a small set of string based models, and other ClientModels. + String fieldSerializationMethod = wireType.jsonSerializationMethodCall("jsonWriter", serializedName, + propertyValueGetter, isJsonMergePatch); + if (wireType == ClassType.BINARY_DATA) { + // Special handling for BinaryData (instead of using "serializationMethodBase" and "serializationValueGetterModifier") + // The reason is that some backend would fail the request on "null" value (e.g. OpenAI) + String writeBinaryDataExpr = "jsonWriter.writeUntypedField(\"" + serializedName + "\", " + propertyValueGetter + ".toObject(Object.class));"; + if (!property.isRequired()) { + methodBlock.ifBlock(propertyValueGetter + " != null", ifAction -> ifAction.line(writeBinaryDataExpr)); + } else { + methodBlock.line(writeBinaryDataExpr); + } + } else if (fieldSerializationMethod != null) { + if (isJsonMergePatch && wireType instanceof ClassType && ((ClassType) wireType).isSwaggerType()) { + methodBlock.line("JsonMergePatchHelper.get" + clientType.toString() + "Accessor().prepareModelForJsonMergePatch(" + propertyValueGetter + ", true);"); + } + if (fromSuperType && clientType != wireType && clientType.isNullable()) { + // If the property is from a super type and the client type is different from the wire type then a null + // check is required to prevent a NullPointerException when converting the value. + methodBlock.ifBlock(property.getGetterName() + "() != null", + ifAction -> ifAction.line(fieldSerializationMethod + ";")); + } else { + methodBlock.line(fieldSerializationMethod + ";"); + } + if (isJsonMergePatch && wireType instanceof ClassType && ((ClassType) wireType).isSwaggerType()) { + methodBlock.line("JsonMergePatchHelper.get" + clientType.toString() + "Accessor().prepareModelForJsonMergePatch(" + propertyValueGetter + ", false);"); + } + } else if (wireType == ClassType.OBJECT) { + methodBlock.line("jsonWriter.writeUntypedField(\"" + serializedName + "\", " + propertyValueGetter + ");"); + } else if (wireType instanceof IterableType) { + serializeJsonContainerProperty(methodBlock, "writeArrayField", wireType, ((IterableType) wireType).getElementType(), + serializedName, propertyValueGetter, 0, isJsonMergePatch); + } else if (wireType instanceof MapType) { + // Assumption is that the key type for the Map is a String. This may not always hold true and when that + // becomes reality this will need to be reworked to handle that case. + serializeJsonContainerProperty(methodBlock, "writeMapField", wireType, ((MapType) wireType).getValueType(), + serializedName, propertyValueGetter, 0, isJsonMergePatch); + } else { + // TODO (alzimmer): Resolve this as deserialization logic generation needs to handle all cases. + throw new RuntimeException("Unknown wire type " + wireType + " in serialization. Need to add support for it."); + } + } + + /** + * Helper function to get property getter statement. + *

+ * If the value is from super type, then we will return "getProperty()", otherwise, return "this.property" + * + * @param property The property being serialized. + * @param fromSuperType Whether the property is defined by a super type of the model. + * @return The property getter statement. + */ + private static String getPropertyGetterStatement(ClientModelProperty property, boolean fromSuperType) { + IType clientType = property.getClientType(); + IType wireType = property.getWireType(); + if (fromSuperType) { + return (clientType != wireType) + ? wireType.convertFromClientType(property.getGetterName() + "()") : property.getGetterName() + "()"; + } else { + return "this." + property.getName(); + } + } + + /** + * Helper method to serialize a JSON container property (such as {@link List} and {@link Map}). + * + * @param methodBlock The method handling serialization. + * @param utilityMethod The method aiding in the serialization of the container. + * @param containerType The container type. + * @param elementType The element type for the container, for a {@link List} this is the element type and for a + * {@link Map} this is the value type. + * @param serializedName The serialized property name. + * @param propertyValueGetter The property or property getter for the field being serialized. + * @param depth Depth of recursion for container types, such as {@code Map>} would be 0 when + * {@code Map} is being handled and then 1 when {@code List} is being handled. + * @param isJsonMergePatch Whether the serialization is for a JSON Merge Patch model. + */ + private static void serializeJsonContainerProperty(JavaBlock methodBlock, String utilityMethod, IType containerType, + IType elementType, String serializedName, String propertyValueGetter, int depth, boolean isJsonMergePatch) { + String callingWriterName = depth == 0 ? "jsonWriter" : (depth == 1) ? "writer" : "writer" + (depth - 1); + String lambdaWriterName = depth == 0 ? "writer" : "writer" + depth; + String elementName = depth == 0 ? "element" : "element" + depth; + String valueSerializationMethod = elementType.jsonSerializationMethodCall(lambdaWriterName, null, elementName, + isJsonMergePatch); + String serializeValue = depth == 0 ? propertyValueGetter + : ((depth == 1) ? "element" : "element" + (depth - 1)); + + // First call into serialize container property will need to write the property name. Subsequent calls must + // not write the property name as that would be invalid, ex "myList":["myList":["innerListElement"]]. + if (depth == 0) { + // Container property shouldn't be written if it's null. + methodBlock.line("%s.%s(\"%s\", %s, (%s, %s) -> ", callingWriterName, utilityMethod, serializedName, + serializeValue, lambdaWriterName, elementName); + } else { + // But the inner container should be written if it's null. + methodBlock.line("%s.%s(%s, (%s, %s) -> ", callingWriterName, utilityMethod, serializeValue, + lambdaWriterName, elementName); + } + + methodBlock.indent(() -> { + if (valueSerializationMethod != null) { + if (isJsonMergePatch && containerType instanceof MapType) { + methodBlock.block("", codeBlock -> codeBlock.ifBlock(elementName + "!= null", ifBlock -> { + if (elementType instanceof ClassType && ((ClassType) elementType).isSwaggerType()) { + methodBlock.line("JsonMergePatchHelper.get" + ((ClassType) elementType).getName() + "Accessor().prepareModelForJsonMergePatch(" + elementName + ", true);"); + } + ifBlock.line(valueSerializationMethod + ";"); + if (elementType instanceof ClassType && ((ClassType) elementType).isSwaggerType()) { + methodBlock.line("JsonMergePatchHelper.get" + ((ClassType) elementType).getName() + "Accessor().prepareModelForJsonMergePatch(" + elementName + ", false);"); + } + }).elseBlock(elseBlock -> elseBlock.line(lambdaWriterName + ".writeNull();"))); + } else { + methodBlock.line(valueSerializationMethod); + } + } else if (elementType == ClassType.OBJECT) { + methodBlock.line(lambdaWriterName + ".writeUntyped(" + elementName + ")"); + } else if (elementType instanceof IterableType) { + serializeJsonContainerProperty(methodBlock, "writeArray", elementType, ((IterableType) elementType).getElementType(), + serializedName, propertyValueGetter, depth + 1, isJsonMergePatch); + } else if (elementType instanceof MapType) { + // Assumption is that the key type for the Map is a String. This may not always hold true and when that + // becomes reality this will need to be reworked to handle that case. + serializeJsonContainerProperty(methodBlock, "writeMap", elementType, ((MapType) elementType).getValueType(), + serializedName, propertyValueGetter, depth + 1, isJsonMergePatch); + } else if (elementType == ClassType.BINARY_DATA) { + methodBlock.line(lambdaWriterName + ".writeUntyped(" + elementName + ")"); + } else { + throw new RuntimeException("Unknown value type " + elementType + " in " + containerType + + " serialization. Need to add support for it."); + } + }); + + if (depth > 0) { + methodBlock.line(")"); + } else { + methodBlock.line(");"); + } + } + + /** + * Helper method to serialize flattened properties in a model. + *

+ * Flattened properties are unique as for each level of flattening they'll create a JSON sub-object. But before a + * sub-object is created any field needs to be checked for either being a primitive value or non-null. Primitive + * values are usually serialized no matter their value so those will automatically trigger the JSON sub-object to be + * created, nullable values will be checked for being non-null. + *

+ * In addition to primitive or non-null checking fields, all properties from the same JSON sub-object must be + * written at the same time to prevent an invalid JSON structure. For example if a model has three flattened + * properties with JSON paths "im.flattened", "im.deeper.flattened", and "im.deeper.flattenedtoo" this will create + * the following structure: + * + *

+     * im -> flattened
+     *     | deeper -> flattened
+     *               | flattenedtoo
+     * 
+ * + * So, "im.deeper.flattened" and "im.deeper.flattenedtoo" will need to be serialized at the same time to get the + * correct JSON where there is only one "im: deeper" JSON sub-object. + * + * @param methodBlock The method handling serialization. + * @param flattenedProperties The flattened properties structure. + * @param isJsonMergePatch Whether the serialization is for a JSON merge patch model. + */ + private static void handleFlattenedPropertiesSerialization(JavaBlock methodBlock, + JsonFlattenedPropertiesTree flattenedProperties, boolean isJsonMergePatch) { + // The initial call to handle flattened properties is using the base node which is just a holder. + for (JsonFlattenedPropertiesTree flattened : flattenedProperties.getChildrenNodes().values()) { + handleFlattenedPropertiesSerializationHelper(methodBlock, flattened, isJsonMergePatch); + } + } + + private static void handleFlattenedPropertiesSerializationHelper(JavaBlock methodBlock, + JsonFlattenedPropertiesTree flattenedProperties, boolean isJsonMergePatch) { + ClientModelPropertyWithMetadata flattenedProperty = flattenedProperties.getProperty(); + if (flattenedProperty != null) { + // This is a terminal location, only need to add property serialization. + serializeJsonProperty(methodBlock, flattenedProperty.getProperty(), flattenedProperties.getNodeName(), + flattenedProperty.isFromSuperClass(), false, isJsonMergePatch); + } else { + // Otherwise this is an intermediate location. + // Check for either any of the properties in this subtree being primitives or add an if block checking that + // any of the properties are non-null. + List propertiesInFlattenedGroup = + getClientModelPropertiesInJsonTree(flattenedProperties); + boolean hasPrimitivePropertyInGroup = propertiesInFlattenedGroup.stream() + .map(property -> property.getProperty().getWireType()) + .anyMatch(wireType -> wireType instanceof PrimitiveType); + + if (hasPrimitivePropertyInGroup) { + // Simple case where the flattened group has a primitive type where non-null checking doesn't need + // to be done. + methodBlock.line("jsonWriter.writeStartObject(\"" + flattenedProperties.getNodeName() + "\");"); + for (JsonFlattenedPropertiesTree flattened : flattenedProperties.getChildrenNodes().values()) { + handleFlattenedPropertiesSerializationHelper(methodBlock, flattened, isJsonMergePatch); + } + methodBlock.line("jsonWriter.writeEndObject();"); + } else { + // Complex case where all properties in the flattened group are nullable and a check needs to be made + // if any value is non-null. + String condition = propertiesInFlattenedGroup.stream() + .map(property -> (property.isFromSuperClass()) + ? property.getProperty().getGetterName() + "() != null" + : property.getProperty().getName() + " != null") + .collect(Collectors.joining(" || ")); + + methodBlock.ifBlock(condition, ifAction -> { + ifAction.line("jsonWriter.writeStartObject(\"" + flattenedProperties.getNodeName() + "\");"); + for (JsonFlattenedPropertiesTree flattened : flattenedProperties.getChildrenNodes().values()) { + handleFlattenedPropertiesSerializationHelper(ifAction, flattened, isJsonMergePatch); + } + ifAction.line("jsonWriter.writeEndObject();"); + }); + } + } + } + + /* + * Writes the fromJson(JsonReader) implementation. + */ + private void writeFromJson(JavaClass classBlock, ClientModel model, + ClientModelPropertiesManager propertiesManager, JavaSettings settings, + Consumer addGeneratedAnnotation) { + // All classes will create a public fromJson(JsonReader) method that initiates reading. + // How the implementation looks depends on whether the type is a super type, subtype, both, or is a + // stand-alone type. + // + // Intermediate types, those that are both a super type and subtype, will pass null as the type discriminator + // value. This is done as super types are written to only support themselves or their subtypes, passing a + // discriminator into its own super type would confuse this scenario. For example, one of the test Swaggers + // generates the following hierarchy + // + // Fish + // Salmon Shark + // SmartSalmon Sawshark GoblinShark Cookiecuttershark + // + // If Salmon called into Fish with its discriminator and an error occurred it would mention the Shark subtypes + // as potential legal values for deserialization, confusing the Salmon deserializer. So, calling into Salmon + // will only attempt to handle Salmon and SmartSalmon, and same goes for Shark with Shark, Sawshark, + // GoblinShark, and Cookiecuttershark. It's only Fish that will handle all subtypes, as this is the most generic + // super type. This also creates a well-defined bounds for code generation in regard to type hierarchies. + // + // In a real scenario someone deserializing to Salmon should have an exception about discriminator types + // if the JSON payload is a Shark and not get a ClassCastException as it'd be more confusing on why a Shark + // was trying to be converted to a Salmon. + if (isSuperTypeWithDiscriminator(model)) { + writeSuperTypeFromJson(classBlock, model, propertiesManager, settings, addGeneratedAnnotation); + } else { + writeTerminalTypeFromJson(classBlock, propertiesManager, settings, addGeneratedAnnotation); + } + } + + /* + * Writes the readManagementError(JsonReader) implementation for ManagementError subclass. + */ + private void writeManagementErrorDeserializationMethod(JavaClass classBlock, ClientModelPropertiesManager propertiesManager, + JavaSettings settings, Consumer addGeneratedAnnotation) { + addGeneratedAnnotation.accept(classBlock); + classBlock.staticMethod( + JavaVisibility.Private, + propertiesManager.getModel().getName() + " " + READ_MANAGEMENT_ERROR_METHOD_NAME + "(JsonReader jsonReader) throws IOException", + methodBlock -> readJsonObjectMethodBody(methodBlock, + deserializationBlock -> writeFromJsonDeserialization0(deserializationBlock, propertiesManager, settings))); + } + + /** + * Writes a super type's {@code fromJson(JsonReader)} method. + * + * @param classBlock The class having {@code fromJson(JsonReader)} written to it. + * @param model The Autorest representation of the model. + * @param propertiesManager The properties for the model. + * @param settings The Autorest generation settings. + * @param addGeneratedAnnotation Callback that adds {@code @Generated} annotation to a code block. + */ + private void writeSuperTypeFromJson(JavaClass classBlock, ClientModel model, + ClientModelPropertiesManager propertiesManager, JavaSettings settings, + Consumer addGeneratedAnnotation) { + // Handling polymorphic fields while determining which subclass, or the class itself, to deserialize handles the + // discriminator type always as a String. This is permissible as the found discriminator is never being used in + // a setter or for setting a field, unlike in the actual deserialization method where it needs to be the same + // type as the field. + String fieldNameVariableName = propertiesManager.getJsonReaderFieldNameVariableName(); + ClientModelPropertyWithMetadata discriminatorProperty = propertiesManager.getDiscriminatorProperty(); + readJsonObject(classBlock, propertiesManager, false, methodBlock -> { + // For now, reading polymorphic types will always buffer the current object. + // In the future this can be enhanced to switch if the first property is the discriminator field and to use + // a Map to contain all properties found while searching for the discriminator field. + // TODO (alzimmer): Need to handle non-string wire type discriminator types. + methodBlock.line("String discriminatorValue = null;"); + methodBlock.tryBlock("JsonReader readerToUse = reader.bufferObject()", tryStatement -> { + tryStatement.line("readerToUse.nextToken(); // Prepare for reading"); + tryStatement.line("while (readerToUse.nextToken() != JsonToken.END_OBJECT) {"); + tryStatement.increaseIndent(); + tryStatement.line("String " + fieldNameVariableName + " = readerToUse.getFieldName();"); + tryStatement.line("readerToUse.nextToken();"); + tryStatement.ifBlock( + "\"" + discriminatorProperty.getProperty().getSerializedName() + "\".equals(" + fieldNameVariableName + ")", + ifStatement -> { + ifStatement.line("discriminatorValue = readerToUse.getString();"); + ifStatement.line("break;"); + }).elseBlock(elseBlock -> elseBlock.line("readerToUse.skipChildren();")); + + tryStatement.decreaseIndent(); + tryStatement.line("}"); + + tryStatement.line("// Use the discriminator value to determine which subtype should be deserialized."); + + // Add deserialization for the super type itself. + JavaIfBlock ifBlock = null; + + // Add deserialization for all child types. + List childTypes = getAllChildTypes(model, new ArrayList<>()); + for (ClientModel childType : childTypes) { + // Determine which serialization method to use based on whether the child type is also a polymorphic + // parent and the child shares the same polymorphic discriminator as this model. + // If the child and parent have different discriminator names then the child will need to be + // deserialized checking the multi-level polymorphic discriminator. + // Using the nested discriminator sample, there is + // Fish : kind + // - Salmon : kind + // - Shark : sharktype + // - Sawshark : sharktype + // So, if deserialization enters Fish and the "kind" is "Shark" then it needs to check the + // "sharktype" to determine if it's a Sawshark or another subtype of Shark. + boolean sameDiscriminator = Objects.equals(childType.getPolymorphicDiscriminatorName(), + model.getPolymorphicDiscriminatorName()); + + if (!sameDiscriminator && !Objects.equals(childType.getParentModelName(), model.getName())) { + // Child model and parent model don't share the same discriminator and the child isn't a direct + // child of the parent model, so skip this child model. This is done as the child model should + // be deserialized by the subtype that defines the different polymorphic discriminator. Using + // the sample above, Fish can't use "kind" to deserialize to a Shark subtype, it needs to use + // "sharktype". + continue; + } + + String deserializationMethod = (isSuperTypeWithDiscriminator(childType) && sameDiscriminator) + ? ".fromJsonKnownDiscriminator(readerToUse.reset())" + : ".fromJson(readerToUse.reset())"; + + ifBlock = ifOrElseIf(tryStatement, ifBlock, + "\"" + childType.getSerializedName() + "\".equals(discriminatorValue)", + ifStatement -> ifStatement.methodReturn(childType.getName() + deserializationMethod)); + } + + if (ifBlock == null) { + tryStatement.methodReturn("fromJsonKnownDiscriminator(readerToUse.reset())"); + } else { + ifBlock.elseBlock( + elseBlock -> elseBlock.methodReturn("fromJsonKnownDiscriminator(readerToUse.reset())")); + } + }); + }, addGeneratedAnnotation); + + readJsonObject(classBlock, propertiesManager, true, + methodBlock -> writeFromJsonDeserialization(methodBlock, propertiesManager, settings), + addGeneratedAnnotation); + } + + private static List getAllChildTypes(ClientModel model, List childTypes) { + for (ClientModel childType : model.getDerivedModels()) { + childTypes.add(childType); + if (!CoreUtils.isNullOrEmpty(childType.getDerivedModels())) { + getAllChildTypes(childType, childTypes); + } + } + + return childTypes; + } + + /** + * Gets the additionalProperty model property from this model or its superclass. + * + * @param propertiesManager The properties for the model. + * @return the additionalProperty model property from this model or its superclass. + */ + private static ClientModelProperty getAdditionalPropertiesPropertyInModelOrFromSuper( + ClientModelPropertiesManager propertiesManager) { + return propertiesManager.getAdditionalProperties() != null + ? propertiesManager.getAdditionalProperties() + : propertiesManager.getSuperAdditionalPropertiesProperty(); + } + + /** + * Writes a terminal type's {@code fromJson(JsonReader)} method. + *

+ * A terminal type is either a type without polymorphism or is the terminal type in a polymorphic hierarchy. + * + * @param classBlock The class having {@code fromJson(JsonReader)} written to it. + * @param propertiesManager The properties for the model. + * @param settings The Autorest generation settings. + * @param addGeneratedAnnotation Callback that adds {@code @Generated} annotation to a code block. + */ + private void writeTerminalTypeFromJson(JavaClass classBlock, ClientModelPropertiesManager propertiesManager, + JavaSettings settings, Consumer addGeneratedAnnotation) { + readJsonObject(classBlock, propertiesManager, false, + methodBlock -> writeFromJsonDeserialization(methodBlock, propertiesManager, settings), + addGeneratedAnnotation); + } + + private void writeFromJsonDeserialization(JavaBlock methodBlock, + ClientModelPropertiesManager propertiesManager, JavaSettings settings) { + // Add the deserialization logic. + methodBlock.indent(() -> { + if (isManagementErrorSubclass(propertiesManager.getModel(), settings)) { + writeManagementErrorAdaption(methodBlock, propertiesManager); + } else { + writeFromJsonDeserialization0(methodBlock, propertiesManager, settings); + } + }); + } + + private static void writeManagementErrorAdaption(JavaBlock methodBlock, ClientModelPropertiesManager propertiesManager) { + methodBlock.line("JsonReader bufferedReader = reader.bufferObject();"); + methodBlock.line("bufferedReader.nextToken();"); + + String fieldNameVariableName = propertiesManager.getJsonReaderFieldNameVariableName(); + addReaderWhileLoop("bufferedReader", methodBlock, true, fieldNameVariableName, false, whileBlock -> { + methodBlock + .ifBlock("\"error\".equals(" + fieldNameVariableName + ")", ifAction -> { + ifAction.line("return " + READ_MANAGEMENT_ERROR_METHOD_NAME + "(bufferedReader);"); + }).elseBlock(elseAction -> { + elseAction.line("bufferedReader.skipChildren();"); + }); + }); + + methodBlock.methodReturn(READ_MANAGEMENT_ERROR_METHOD_NAME + "(bufferedReader.reset())"); + } + + private static void writeFromJsonDeserialization0(JavaBlock methodBlock, ClientModelPropertiesManager propertiesManager, JavaSettings settings) { + // Initialize local variables to track what has been deserialized. + initializeLocalVariables(methodBlock, propertiesManager, false, settings); + + boolean polymorphicJsonMergePatchScenario = propertiesManager.getModel().isPolymorphic() + && ClientModelUtil.isJsonMergePatchModel(propertiesManager.getModel(), settings); + + String fieldNameVariableName = propertiesManager.getJsonReaderFieldNameVariableName(); + + // Add the outermost while loop to read the JSON object. + addReaderWhileLoop(methodBlock, true, fieldNameVariableName, false, whileBlock -> { + // Loop over all properties and generate their deserialization handling. + AtomicReference ifBlockReference = new AtomicReference<>(null); + + BiConsumer consumer = (property, fromSuper) -> + handleJsonPropertyDeserialization(propertiesManager.getModel(), property, + propertiesManager.getDeserializedModelName(), whileBlock, ifBlockReference, + fieldNameVariableName, fromSuper, propertiesManager.hasConstructorArguments(), settings, + polymorphicJsonMergePatchScenario); + + Map modelPropertyMap = new HashMap<>(); + for (ClientModelProperty parentProperty : ClientModelUtil.getParentProperties(propertiesManager.getModel())) { + modelPropertyMap.put(parentProperty.getName(), parentProperty); + } + for (ClientModelProperty property : propertiesManager.getModel().getProperties()) { + modelPropertyMap.put(property.getName(), property); + } + + // Child classes may contain properties that shadow parents' ones. + // Thus, we only take the shadowing ones, not the ones shadowed. + Map superRequiredToDeserialized = new LinkedHashMap<>(); + propertiesManager.forEachSuperRequiredProperty(property -> { + if (!property.isConstant() && modelPropertyMap.get(property.getName()) == property) { + superRequiredToDeserialized.put(property.getName(), property); + } + }); + superRequiredToDeserialized.values().forEach(property -> consumer.accept(property, true)); + + // Child classes may contain properties that shadow parents' ones. + // Thus, we only take the shadowing ones, not the ones shadowed. + Map superSettersToDeserialized = new LinkedHashMap<>(); + propertiesManager.forEachSuperSetterProperty(property -> { + if (!property.isConstant() && modelPropertyMap.get(property.getName()) == property) { + superSettersToDeserialized.put(property.getName(), property); + } + }); + superSettersToDeserialized.values().forEach(property -> consumer.accept(property, true)); + + propertiesManager.forEachRequiredProperty(property -> { + if (property.isConstant()) { + return; + } + consumer.accept(property, false); + }); + propertiesManager.forEachSetterProperty(property -> consumer.accept(property, false)); + + JavaIfBlock ifBlock = ifBlockReference.get(); + + handleFlattenedPropertiesDeserialization(propertiesManager.getJsonFlattenedPropertiesTree(), + methodBlock, ifBlock, propertiesManager.getAdditionalProperties(), + propertiesManager.getJsonReaderFieldNameVariableName(), propertiesManager.hasConstructorArguments(), + settings, polymorphicJsonMergePatchScenario); + + // All properties have been checked for, add an else block that will either ignore unknown properties + // or add them into an additional properties bag. + ClientModelProperty additionalProperty = getAdditionalPropertiesPropertyInModelOrFromSuper(propertiesManager); + handleUnknownJsonFieldDeserialization(whileBlock, ifBlock, additionalProperty, + propertiesManager.getJsonReaderFieldNameVariableName()); + }); + + // Add the validation and return logic. + handleReadReturn(methodBlock, propertiesManager.getModel().getName(), propertiesManager, settings); + } + + /** + * Whether the given model is subclass of ManagementError, which needs special deserialization adaption. + * @param model the model to check + * @param settings JavaSettings instance + * @return whether the given model is subclass of ManagementError + */ + protected boolean isManagementErrorSubclass(ClientModel model, JavaSettings settings) { + return false; + } + + /** + * Adds a static method to the class with the signature that handles reading the JSON string into the object type. + *

+ * If {@code superTypeReading} is true the method will be package-private and named + * {@code fromJsonWithKnownDiscriminator} instead of being public and named {@code fromJson}. This is done as super + * types use their {@code fromJson} method to determine the discriminator value and pass the reader to the specific + * type being deserialized. The specific type being deserialized may be the super type itself, so it cannot pass to + * {@code fromJson} as this will be a circular call and if the specific type being deserialized is an intermediate + * type (a type having both super and subclasses) it will attempt to perform discriminator validation which has + * already been done. + * + * @param classBlock The class where the {@code fromJson} method is being written. + * @param propertiesManager Properties information about the object being deserialized. + * @param superTypeReading Whether the object reading is for a super type. + * @param deserializationBlock Logic for deserializing the object. + * @param addGeneratedAnnotation Callback that adds {@code @Generated} annotation to a code block. + */ + private static void readJsonObject(JavaClass classBlock, ClientModelPropertiesManager propertiesManager, + boolean superTypeReading, Consumer deserializationBlock, + Consumer addGeneratedAnnotation) { + JavaVisibility visibility = superTypeReading ? JavaVisibility.PackagePrivate : JavaVisibility.Public; + String methodName = superTypeReading ? "fromJsonKnownDiscriminator" : "fromJson"; + + String modelName = propertiesManager.getModel().getName(); + boolean hasRequiredProperties = propertiesManager.hasRequiredProperties(); + + if (!superTypeReading) { + classBlock.javadocComment(javadocComment -> { + javadocComment.description("Reads an instance of " + modelName + " from the JsonReader."); + javadocComment.param("jsonReader", "The JsonReader being read."); + javadocComment.methodReturns("An instance of " + modelName + " if the JsonReader was pointing to an " + + "instance of it, or null if it was pointing to JSON null."); + + String throwsStatement = null; + if (hasRequiredProperties) { + throwsStatement = "If the deserialized JSON object was missing any required properties."; + } + + if (throwsStatement != null) { + javadocComment.methodThrows("IllegalStateException", throwsStatement); + } + + javadocComment.methodThrows("IOException", "If an error occurs while reading the " + modelName + "."); + }); + } + + addGeneratedAnnotation.accept(classBlock); + classBlock.staticMethod(visibility, modelName + " " + methodName + "(JsonReader jsonReader) throws IOException", methodBlock -> { + readJsonObjectMethodBody(methodBlock, deserializationBlock); + }); + } + + private static void readJsonObjectMethodBody(JavaBlock methodBlock, Consumer deserializationBlock) { + // For now, use the basic readObject which will return null if the JsonReader is pointing to JsonToken.NULL. + // + // Support for a default value if null will need to be supported and for objects that get their value + // from a JSON value instead of JSON object or are an array type. + methodBlock.line("return jsonReader.readObject(reader -> {"); + + deserializationBlock.accept(methodBlock); + + methodBlock.line("});"); + } + + /** + * Initializes the local variables needed to maintain what has been deserialized. + * + * @param methodBlock The method handling deserialization. + * @param propertiesManager The property manager for the model. + */ + private static void initializeLocalVariables(JavaBlock methodBlock, ClientModelPropertiesManager propertiesManager, + boolean isXml, JavaSettings settings) { + if (propertiesManager.hasConstructorArguments()) { + if (isXml) { + // XML only needs to initialize the XML element properties. XML attribute properties are initialized with + // their XML value. + propertiesManager.forEachSuperXmlElement(element -> initializeLocalVariable(methodBlock, element, true, + settings)); + propertiesManager.forEachXmlElement(element -> initializeLocalVariable(methodBlock, element, false, + settings)); + } else { + propertiesManager.forEachSuperRequiredProperty(property -> { + if (property.isConstant()) { + // Constants are never deserialized. + return; + } + initializeLocalVariable(methodBlock, property, true, settings); + }); + propertiesManager.forEachSuperSetterProperty(property -> { + if (readOnlyNotInCtor(propertiesManager.getModel(), property, settings)) { + initializeShadowPropertyLocalVariable(methodBlock, property); + } else { + initializeLocalVariable(methodBlock, property, true, settings); + } + }); + propertiesManager.forEachRequiredProperty(property -> { + if (property.isConstant()) { + // Constants are never deserialized. + return; + } + initializeLocalVariable(methodBlock, property, false, settings); + }); + propertiesManager.forEachSetterProperty(property -> initializeLocalVariable(methodBlock, property, + false, settings)); + } + } else { + String modelName = propertiesManager.getModel().getName(); + methodBlock.line(modelName + " " + propertiesManager.getDeserializedModelName() + " = new " + + modelName + "();"); + } + + ClientModelProperty additionalProperty = getAdditionalPropertiesPropertyInModelOrFromSuper(propertiesManager); + if (additionalProperty != null) { + initializeLocalVariable(methodBlock, additionalProperty, false, settings); + } + } + + /* + * Shadow properties from parent should be initialized as wired type. + */ + private static void initializeShadowPropertyLocalVariable(JavaBlock methodBlock, ClientModelProperty property) { + IType type = property.getWireType(); + String defaultValue = property.isPolymorphicDiscriminator() + ? property.getDefaultValue() : type.defaultValueExpression(); + methodBlock.line(type + " " + property.getName() + " = " + defaultValue + ";"); + } + + private static void initializeLocalVariable(JavaBlock methodBlock, ClientModelProperty property, boolean fromSuper, + JavaSettings settings) { + if (includePropertyInConstructor(property, settings) && !settings.isDisableRequiredJsonAnnotation()) { + // Required properties need an additional boolean variable to indicate they've been found. + methodBlock.line("boolean " + property.getName() + "Found = false;"); + } + + // Always instantiate the local variable. + // If the property is part of the constructor or set by a setter method from the super class, initialize the + // local variable with the client type. Otherwise, initialize as the wire type to prevent multiple conversions + // between wire and client types. + IType type = (includePropertyInConstructor(property, settings) || fromSuper) + ? property.getClientType() : property.getWireType(); + String defaultValue = property.isPolymorphicDiscriminator() + ? property.getDefaultValue() : type.defaultValueExpression(); + methodBlock.line(type + " " + property.getName() + " = " + defaultValue + ";"); + } + + /** + * Adds the while loop that handles reading the JSON object until it is fully consumed. + * + * @param methodBlock The method handling deserialization. + * @param initializeFieldNameVariable Whether the {@code fieldNameVariableName} variable needs to be initialized. If + * this is a nested while loop the variable doesn't need to be initialized. + * @param fieldNameVariableName The name for the variable that tracks the JSON field name. + * @param isXml Whether the reader while loop is for XML reading. + * @param whileBlock The consumer that adds deserialization logic into the while loop. + */ + private static void addReaderWhileLoop(JavaBlock methodBlock, boolean initializeFieldNameVariable, + String fieldNameVariableName, boolean isXml, Consumer whileBlock) { + addReaderWhileLoop("reader", methodBlock, initializeFieldNameVariable, fieldNameVariableName, + isXml, whileBlock); + } + + /** + * Adds the while loop that handles reading the JSON object until it is fully consumed. + * + * @param readerVariableName The name of the local reader variable. + * @param methodBlock The method handling deserialization. + * @param initializeFieldNameVariable Whether the {@code fieldNameVariableName} variable needs to be initialized. If + * this is a nested while loop the variable doesn't need to be initialized. + * @param fieldNameVariableName The name for the variable that tracks the JSON field name. + * @param isXml Whether the reader while loop is for XML reading. + * @param whileBlock The consumer that adds deserialization logic into the while loop. + */ + private static void addReaderWhileLoop(String readerVariableName, JavaBlock methodBlock, boolean initializeFieldNameVariable, + String fieldNameVariableName, boolean isXml, Consumer whileBlock) { + String whileCheck = isXml + ? readerVariableName + ".nextElement() != XmlToken.END_ELEMENT" + : readerVariableName + ".nextToken() != JsonToken.END_OBJECT"; + + methodBlock.block("while (" + whileCheck + ")", whileAction -> { + String fieldNameInitialization = ""; + if (initializeFieldNameVariable) { + fieldNameInitialization = isXml ? "QName" : "String"; + } + + methodBlock.line("%s %s = %s.get%sName();", fieldNameInitialization, fieldNameVariableName, + readerVariableName, isXml ? "Element" : "Field"); + + if (!isXml) { + methodBlock.line(readerVariableName + ".nextToken();"); + } + methodBlock.line(""); + + whileBlock.accept(methodBlock); + }); + } + + private static void handleJsonPropertyDeserialization(ClientModel model, ClientModelProperty property, + String modelVariableName, JavaBlock methodBlock, AtomicReference ifBlockReference, + String fieldNameVariableName, boolean fromSuper, boolean hasConstructorArguments, JavaSettings settings, + boolean polymorphicJsonMergePatchScenario) { + // Property will be handled later by flattened deserialization. + if (property.getNeedsFlatten()) { + return; + } + + JavaIfBlock ifBlock = ifBlockReference.get(); + ifBlock = handleJsonPropertyDeserialization(model, property, modelVariableName, methodBlock, ifBlock, + fieldNameVariableName, fromSuper, hasConstructorArguments, settings, polymorphicJsonMergePatchScenario); + + ifBlockReference.set(ifBlock); + } + + private static JavaIfBlock handleJsonPropertyDeserialization(ClientModel model, ClientModelProperty property, + String modelVariableName, JavaBlock methodBlock, JavaIfBlock ifBlock, String fieldNameVariableName, + boolean fromSuper, boolean hasConstructorArguments, JavaSettings settings, + boolean polymorphicJsonMergePatchScenario) { + String jsonPropertyName = property.getSerializedName(); + if (CoreUtils.isNullOrEmpty(jsonPropertyName)) { + return ifBlock; + } + + return ifOrElseIf(methodBlock, ifBlock, "\"" + jsonPropertyName + "\".equals(" + fieldNameVariableName + ")", + deserializationBlock -> generateJsonDeserializationLogic(deserializationBlock, modelVariableName, model, + property, fromSuper, hasConstructorArguments, settings, polymorphicJsonMergePatchScenario)); + } + + private static void handleFlattenedPropertiesDeserialization( + JsonFlattenedPropertiesTree flattenedProperties, JavaBlock methodBlock, JavaIfBlock ifBlock, + ClientModelProperty additionalProperties, String fieldNameVariableName, boolean hasConstructorArguments, + JavaSettings settings, boolean polymorphicJsonMergePatchScenario) { + // The initial call to handle flattened properties is using the base node which is just a holder. + for (JsonFlattenedPropertiesTree structure : flattenedProperties.getChildrenNodes().values()) { + handleFlattenedPropertiesDeserializationHelper(structure, methodBlock, ifBlock, additionalProperties, + fieldNameVariableName, hasConstructorArguments, settings, polymorphicJsonMergePatchScenario); + } + } + + private static JavaIfBlock handleFlattenedPropertiesDeserializationHelper( + JsonFlattenedPropertiesTree flattenedProperties, JavaBlock methodBlock, JavaIfBlock ifBlock, + ClientModelProperty additionalProperties, String fieldNameVariableName, boolean hasConstructorArguments, + JavaSettings settings, boolean polymorphicJsonMergePatchScenario) { + ClientModelPropertyWithMetadata propertyWithMetadata = flattenedProperties.getProperty(); + if (propertyWithMetadata != null) { + String modelVariableName = "deserialized" + propertyWithMetadata.getModel().getName(); + + // This is a terminal location, so only need to handle checking for the property name. + return ifOrElseIf(methodBlock, ifBlock, + "\"" + flattenedProperties.getNodeName() + "\".equals(" + fieldNameVariableName + ")", + deserializationBlock -> generateJsonDeserializationLogic(deserializationBlock, modelVariableName, + propertyWithMetadata.getModel(), propertyWithMetadata.getProperty(), + propertyWithMetadata.isFromSuperClass(), hasConstructorArguments, settings, + polymorphicJsonMergePatchScenario)); + } else { + // Otherwise this is an intermediate location and a while loop reader needs to be added. + return ifOrElseIf(methodBlock, ifBlock, + "\"" + flattenedProperties.getNodeName() + "\".equals(" + fieldNameVariableName + ") && reader.currentToken() == JsonToken.START_OBJECT", + ifAction -> addReaderWhileLoop(ifAction, false, fieldNameVariableName, false, whileBlock -> { + JavaIfBlock innerIfBlock = null; + for (JsonFlattenedPropertiesTree structure : flattenedProperties.getChildrenNodes().values()) { + innerIfBlock = handleFlattenedPropertiesDeserializationHelper(structure, methodBlock, + innerIfBlock, additionalProperties, fieldNameVariableName, hasConstructorArguments, + settings, polymorphicJsonMergePatchScenario); + } + + handleUnknownJsonFieldDeserialization(whileBlock, innerIfBlock, additionalProperties, + fieldNameVariableName); + })); + } + } + + private static void generateJsonDeserializationLogic(JavaBlock deserializationBlock, String modelVariableName, + ClientModel model, ClientModelProperty property, boolean fromSuper, boolean hasConstructorArguments, JavaSettings settings, + boolean polymorphicJsonMergePatchScenario) { + IType wireType = property.getWireType(); + IType clientType = property.getClientType(); + + // Attempt to determine whether the wire type is simple deserialization. + // This is primitives, boxed primitives, a small set of string based models, and other ClientModels. + String simpleDeserialization = getSimpleJsonDeserialization(wireType, "reader"); + if (simpleDeserialization != null) { + // Need to convert the wire type to the client type for constructors. + // Need to convert the wire type to the client type for public setters. + boolean convertToClientType = (clientType != wireType) + && (includePropertyInConstructor(property, settings) || (fromSuper && !readOnlyNotInCtor(model, property, settings))); + BiConsumer simpleDeserializationConsumer = (logic, block) -> { + if (!hasConstructorArguments) { + handleSettingDeserializedValue(block, modelVariableName, model, property, logic, fromSuper, + polymorphicJsonMergePatchScenario); + } else { + block.line(property.getName() + " = " + logic + ";"); + } + }; + + if (convertToClientType) { + // If the wire type is nullable don't attempt to call the convert to client type until it's known that + // a value was deserialized. This protects against cases such as UnixTimeLong where the wire type is + // Long and the client type of OffsetDateTime. This is converted using Instant.ofEpochMilli(long) which + // would result in a null if the Long is null, which is already guarded using + // reader.readNullable(nonNullReader -> Instant.ofEpochMillis(nonNullReader.readLong())) but this itself + // returns null which would have been passed to OffsetDateTime.ofInstant(Instant, ZoneId) which would + // have thrown a NullPointerException. + if (wireType.isNullable()) { + // Check if the property is required, if so use a holder name as there will be an existing holder + // variable for the value that will be used in the constructor. + String holderName = property.getName() + "Holder"; + deserializationBlock.line(wireType + " " + holderName + " = " + simpleDeserialization + ";"); + deserializationBlock.ifBlock(holderName + " != null", ifBlock -> + simpleDeserializationConsumer.accept(wireType.convertToClientType(holderName), ifBlock)); + } else { + simpleDeserializationConsumer.accept(wireType.convertToClientType(simpleDeserialization), + deserializationBlock); + } + } else { + simpleDeserializationConsumer.accept(simpleDeserialization, deserializationBlock); + } + } else if (wireType == ClassType.OBJECT) { + if (!hasConstructorArguments) { + handleSettingDeserializedValue(deserializationBlock, modelVariableName, model, property, + "reader.readUntyped()", fromSuper, polymorphicJsonMergePatchScenario); + } else { + deserializationBlock.line(property.getName() + " = reader.readUntyped();"); + } + } else if (wireType instanceof IterableType) { + if (!hasConstructorArguments) { + deserializationBlock.text(property.getClientType() + " "); + } + + deserializationBlock.text(property.getName() + " = "); + deserializeJsonContainerProperty(deserializationBlock, "readArray", wireType, + ((IterableType) wireType).getElementType(), ((IterableType) clientType).getElementType(), 0); + + if (!hasConstructorArguments) { + handleSettingDeserializedValue(deserializationBlock, modelVariableName, model, property, + property.getName(), fromSuper, polymorphicJsonMergePatchScenario); + } + } else if (wireType instanceof MapType) { + if (!hasConstructorArguments) { + deserializationBlock.text(property.getClientType() + " "); + } + + // Assumption is that the key type for the Map is a String. This may not always hold true and when that + // becomes reality this will need to be reworked to handle that case. + deserializationBlock.text(property.getName() + " = "); + deserializeJsonContainerProperty(deserializationBlock, "readMap", wireType, + ((MapType) wireType).getValueType(), ((MapType) clientType).getValueType(), 0); + + if (!hasConstructorArguments) { + handleSettingDeserializedValue(deserializationBlock, modelVariableName, model, property, + property.getName(), fromSuper, polymorphicJsonMergePatchScenario); + } + } else { + // TODO (alzimmer): Resolve this as deserialization logic generation needs to handle all cases. + throw new RuntimeException("Unknown wire type " + wireType + ". Need to add support for it."); + } + + // If the property was required, mark it as found. + if (includePropertyInConstructor(property, settings) && !settings.isDisableRequiredJsonAnnotation()) { + deserializationBlock.line(property.getName() + "Found = true;"); + } + } + + /** + * Helper method to deserialize a container property (such as {@link List} and {@link Map}). + * + * @param methodBlock The method handling deserialization. + * @param utilityMethod The method aiding in the deserialization of the container. + * @param containerType The container type. + * @param elementWireType The element type for the container, for a {@link List} this is the element type and for a + * {@link Map} this is the value type. + * @param depth Depth of recursion for container types, such as {@code Map>} would be 0 when + * {@code Map} is being handled and then 1 when {@code List} is being handled. + */ + private static void deserializeJsonContainerProperty(JavaBlock methodBlock, String utilityMethod, IType containerType, + IType elementWireType, IType elementClientType, int depth) { + String callingReaderName = depth == 0 ? "reader" : "reader" + depth; + String lambdaReaderName = "reader" + (depth + 1); + String valueDeserializationMethod = getSimpleJsonDeserialization(elementWireType, lambdaReaderName); + boolean convertToClientType = (elementClientType != elementWireType); + boolean useCodeBlockLambda = valueDeserializationMethod != null && elementWireType.isNullable() + && convertToClientType; + + if (useCodeBlockLambda) { + methodBlock.line(callingReaderName + "." + utilityMethod + "(" + lambdaReaderName + " -> {"); + } else { + methodBlock.line(callingReaderName + "." + utilityMethod + "(" + lambdaReaderName + " ->"); + } + methodBlock.indent(() -> { + if (valueDeserializationMethod != null) { + if (convertToClientType) { + // If the wire type is nullable don't attempt to call the convert to client type until it's known that + // a value was deserialized. This protects against cases such as UnixTimeLong where the wire type is + // Long and the client type of OffsetDateTime. This is converted using Instant.ofEpochMilli(long) which + // would result in a null if the Long is null, which is already guarded using + // reader.readNullable(nonNullReader -> Instant.ofEpochMillis(nonNullReader.readLong())) but this itself + // returns null which would have been passed to OffsetDateTime.ofInstant(Instant, ZoneId) which would + // have thrown a NullPointerException. + if (elementWireType.isNullable()) { + // Check if the property is required, if so use a holder name as there will be an existing holder + // variable for the value that will be used in the constructor. + String holderName = lambdaReaderName + "ValueHolder"; + methodBlock.line(elementWireType + " " + holderName + " = " + valueDeserializationMethod + ";"); + methodBlock.ifBlock(holderName + " != null", + ifBlock -> ifBlock.methodReturn(elementWireType.convertToClientType(holderName))) + .elseBlock(elseBlock -> elseBlock.methodReturn("null")); + } else { + methodBlock.line(elementWireType.convertToClientType(valueDeserializationMethod)); + } + } else { + methodBlock.line(valueDeserializationMethod); + } + } else if (elementWireType == ClassType.OBJECT) { + methodBlock.line(lambdaReaderName + ".readUntyped()"); + } else if (elementWireType instanceof IterableType) { + deserializeJsonContainerProperty(methodBlock, "readArray", elementWireType, + ((IterableType) elementWireType).getElementType(), + ((IterableType) elementClientType).getElementType(), depth + 1); + } else if (elementWireType instanceof MapType) { + // Assumption is that the key type for the Map is a String. This may not always hold true and when that + // becomes reality this will need to be reworked to handle that case. + deserializeJsonContainerProperty(methodBlock, "readMap", elementWireType, + ((MapType) elementWireType).getValueType(), ((MapType) elementClientType).getValueType(), + depth + 1); + } else if (elementWireType == ClassType.BINARY_DATA) { + methodBlock.line(lambdaReaderName + ".readUntyped()"); + } else { + throw new RuntimeException("Unknown value type " + elementWireType + " in " + containerType + + " serialization. Need to add support for it."); + } + }); + + if (useCodeBlockLambda) { + if (depth > 0) { + methodBlock.line("})"); + } else { + methodBlock.line("});"); + } + } else { + if (depth > 0) { + methodBlock.line(")"); + } else { + methodBlock.line(");"); + } + } + } + + private static String getSimpleJsonDeserialization(IType wireType, String readerName) { + return (wireType instanceof ClassType && ((ClassType) wireType).isSwaggerType()) + ? wireType + ".fromJson(" + readerName + ")" + : wireType.jsonDeserializationMethod(readerName); + } + + private static void handleUnknownJsonFieldDeserialization(JavaBlock methodBlock, JavaIfBlock ifBlock, + ClientModelProperty additionalProperties, String fieldNameVariableName) { + Consumer unknownFieldConsumer = javaBlock -> { + if (additionalProperties != null) { + javaBlock.ifBlock(additionalProperties.getName() + " == null", + ifAction -> ifAction.line(additionalProperties.getName() + " = new LinkedHashMap<>();")); + javaBlock.line(); + + // Assumption, additional properties is a Map of String-Object + IType valueType = ((MapType) additionalProperties.getWireType()).getValueType(); + if (valueType == ClassType.OBJECT) { + // String fieldName should be a local variable accessible in this spot of code. + javaBlock.line(additionalProperties.getName() + ".put(" + fieldNameVariableName + ", reader.readUntyped());"); + } else if (valueType instanceof IterableType) { + // The case that element is a List + String varName = additionalProperties.getName() + "ArrayItem"; + javaBlock.text(valueType + " " + varName + " = "); + deserializeJsonContainerProperty(javaBlock, "readArray", valueType, + ((IterableType) valueType).getElementType(), ((IterableType) valueType).getElementType(), 0); + javaBlock.line(additionalProperties.getName() + ".put(" + fieldNameVariableName + ", " + varName + ");"); + } else { + // Another assumption, the additional properties value type is simple. + javaBlock.line(additionalProperties.getName() + ".put(" + fieldNameVariableName + ", " + + getSimpleJsonDeserialization(valueType, "reader") + ");"); + } + } else { + javaBlock.line("reader.skipChildren();"); + } + }; + + if (ifBlock == null) { + unknownFieldConsumer.accept(methodBlock); + } else { + ifBlock.elseBlock(unknownFieldConsumer); + } + } + + /** + * Handles validating that all required properties have been found and creating the return type. + *

+ * Properties are split into two concepts, required and optional properties, and those concepts are split into an + * additional two groups, properties declared by super types and by the model type. + * + * @param methodBlock The method handling deserialization. + * @param modelName The name of the model. + * @param propertiesManager The property manager for the model. + */ + private static void handleReadReturn(JavaBlock methodBlock, String modelName, + ClientModelPropertiesManager propertiesManager, JavaSettings settings) { + StringBuilder constructorArgs = new StringBuilder(); + + propertiesManager.forEachSuperConstructorProperty(arg -> addConstructorParameter(constructorArgs, arg.getName())); + propertiesManager.forEachConstructorProperty(arg -> addConstructorParameter(constructorArgs, arg.getName())); + + // If there are required properties of any type we must check that all required fields were found. + if (propertiesManager.hasRequiredProperties()) { + StringBuilder ifStatementBuilder = new StringBuilder(); + propertiesManager.forEachSuperRequiredProperty(property -> addRequiredCheck(ifStatementBuilder, property, settings)); + propertiesManager.forEachRequiredProperty(property -> addRequiredCheck(ifStatementBuilder, property, settings)); + + if (ifStatementBuilder.length() > 0) { + methodBlock.ifBlock(ifStatementBuilder.toString(), ifAction -> + createObjectAndReturn(methodBlock, modelName, constructorArgs.toString(), propertiesManager, settings)); + + if (propertiesManager.getRequiredPropertiesCount() == 1) { + StringBuilder stringBuilder = new StringBuilder(); + propertiesManager.forEachSuperRequiredProperty(property -> stringBuilder.append(property.getSerializedName())); + propertiesManager.forEachRequiredProperty(property -> stringBuilder.append(property.getSerializedName())); + methodBlock.line("throw new IllegalStateException(\"Missing required property: " + stringBuilder + "\");"); + } else { + methodBlock.line("List missingProperties = new ArrayList<>();"); + propertiesManager.forEachSuperRequiredProperty(property -> addFoundValidationIfCheck(methodBlock, property, settings)); + propertiesManager.forEachRequiredProperty(property -> addFoundValidationIfCheck(methodBlock, property, settings)); + + methodBlock.line(); + methodBlock.line("throw new IllegalStateException(\"Missing required property/properties: \" + String.join(\", \", missingProperties));"); + } + } else { + createObjectAndReturn(methodBlock, modelName, constructorArgs.toString(), propertiesManager, settings); + } + } else { + createObjectAndReturn(methodBlock, modelName, constructorArgs.toString(), propertiesManager, settings); + } + } + + private static void createObjectAndReturn(JavaBlock methodBlock, String modelName, String constructorArgs, + ClientModelPropertiesManager propertiesManager, JavaSettings settings) { + boolean polymorphicJsonMergePatchScenario = propertiesManager.getModel().isPolymorphic() + && ClientModelUtil.isJsonMergePatchModel(propertiesManager.getModel(), settings); + if (propertiesManager.hasConstructorArguments()) { + if (propertiesManager.getSetterPropertiesCount() == 0 + && propertiesManager.getReadOnlyPropertiesCount() == 0 + && propertiesManager.getAdditionalProperties() == null + && propertiesManager.getSuperAdditionalPropertiesProperty() == null) { + methodBlock.methodReturn("new " + modelName + "(" + constructorArgs + ")"); + return; + } + + methodBlock.line(modelName + " " + propertiesManager.getDeserializedModelName() + " = new " + modelName + + "(" + constructorArgs + ");"); + + BiConsumer handleSettingDeserializedValue = (property, fromSuper) -> + handleSettingDeserializedValue(methodBlock, propertiesManager.getDeserializedModelName(), + propertiesManager.getModel(), property, property.getName(), fromSuper, + polymorphicJsonMergePatchScenario); + + propertiesManager.forEachSuperReadOnlyProperty(property -> handleSettingDeserializedValue.accept(property, true)); + propertiesManager.forEachSuperSetterProperty(property -> handleSettingDeserializedValue.accept(property, true)); + propertiesManager.forEachReadOnlyProperty(property -> handleSettingDeserializedValue.accept(property, false)); + propertiesManager.forEachSetterProperty(property -> handleSettingDeserializedValue.accept(property, false)); + } + + if (propertiesManager.getAdditionalProperties() != null) { + handleSettingDeserializedValue(methodBlock, propertiesManager.getDeserializedModelName(), + propertiesManager.getModel(), propertiesManager.getAdditionalProperties(), + propertiesManager.getAdditionalProperties().getName(), false, polymorphicJsonMergePatchScenario); + } else if (propertiesManager.getSuperAdditionalPropertiesProperty() != null) { + handleSettingDeserializedValue(methodBlock, propertiesManager.getDeserializedModelName(), + propertiesManager.getModel(), propertiesManager.getSuperAdditionalPropertiesProperty(), + propertiesManager.getSuperAdditionalPropertiesProperty().getName(), true, + polymorphicJsonMergePatchScenario); + } + + methodBlock.line(); + methodBlock.methodReturn(propertiesManager.getDeserializedModelName()); + } + + private static void addConstructorParameter(StringBuilder constructor, String parameterName) { + if (constructor.length() > 0) { + constructor.append(", "); + } + + constructor.append(parameterName); + } + + private static void addRequiredCheck(StringBuilder ifCheck, ClientModelProperty property, JavaSettings settings) { + // XML attributes and text don't need checks. + if (property.isXmlAttribute() || property.isXmlText() || !includePropertyInConstructor(property, settings)) { + return; + } + + // Constants are ignored during deserialization. + if (property.isConstant()) { + return; + } + + // Required properties aren't being validated for being found. + if (settings.isDisableRequiredJsonAnnotation()) { + return; + } + + if (ifCheck.length() > 0) { + ifCheck.append(" && "); + } + + ifCheck.append(property.getName()).append("Found"); + } + + private static void addFoundValidationIfCheck(JavaBlock methodBlock, ClientModelProperty property, + JavaSettings settings) { + // XML attributes and text don't need checks. + if (property.isXmlAttribute() || property.isXmlText() || !includePropertyInConstructor(property, settings)) { + return; + } + + // Constants are ignored during deserialization. + if (property.isConstant()) { + return; + } + + // Required properties aren't being validated for being found. + if (settings.isDisableRequiredJsonAnnotation()) { + return; + } + + methodBlock.ifBlock("!" + property.getName() + "Found", + ifAction -> ifAction.line("missingProperties.add(\"" + property.getSerializedName() + "\");")); + } + + private static void handleSettingDeserializedValue(JavaBlock methodBlock, String modelVariableName, + ClientModel model, ClientModelProperty property, String value, boolean fromSuper, + boolean polymorphicJsonMergePatchScenario) { + // If the property is defined in a super class use the setter as this will be able to set the value in the + // super class. + if (fromSuper + // If the property is flattened or read-only from parent, it will be shadowed in child class. + && (!readOnlyNotInCtor(model, property, JavaSettings.getInstance()) && !property.getClientFlatten())) { + if (polymorphicJsonMergePatchScenario) { + // Polymorphic JSON merge patch needs special handling as the setter methods are used to track whether + // the property is included in patch serialization. To prevent deserialization from requiring parent + // defined properties to always be included in serialization, access helpers are used to set the value + // without marking the property as included in the patch. + ClientModel definingModel = definingModel(model, property); + methodBlock.line("JsonMergePatchHelper.get" + definingModel.getName() + "Accessor()." + + property.getSetterName() + "(" + modelVariableName + ", " + value + ");"); + } else { + methodBlock.line(modelVariableName + "." + property.getSetterName() + "(" + value + ");"); + } + } else { + methodBlock.line(modelVariableName + "." + property.getName() + " = " + value + ";"); + } + } + + private static boolean isSuperTypeWithDiscriminator(ClientModel child) { + return !CoreUtils.isNullOrEmpty(child.getPolymorphicDiscriminatorName()) + && !CoreUtils.isNullOrEmpty(child.getDerivedModels()); + } + + // TODO (alzimmer): This is a very inefficient design where we're using the ClientModelProperty to find which model + // in the polymorphic hierarchy defines it, but this is a simple bootstrapping method rather than a larger + // re-architecting. + private static ClientModel definingModel(ClientModel model, ClientModelProperty property) { + while (model != null) { + if (model.getProperties().stream().anyMatch(prop -> Objects.equals(prop.getName(), property.getName()))) { + return model; + } + model = ClientModelUtil.getClientModel(model.getParentModelName()); + } + + throw new IllegalStateException("No ClientModel in the polymorphic hierarchy define this property, this should never happen."); + } + + /** + * Helper method for adding a base if condition or an else if condition. + * + * @param baseBlock Base code block where an if condition would be added. + * @param ifBlock If block where an else if condition would be added. + * @param condition The conditional statement. + * @param action The conditional action. + * @return An if block for further chaining. + */ + private static JavaIfBlock ifOrElseIf(JavaBlock baseBlock, JavaIfBlock ifBlock, String condition, + Consumer action) { + return (ifBlock == null) ? baseBlock.ifBlock(condition, action) : ifBlock.elseIfBlock(condition, action); + } + + private static void writeToXml(JavaClass classBlock, ClientModelPropertiesManager propertiesManager, + Consumer addGeneratedAnnotation) { + addGeneratedAnnotation.accept(classBlock); + classBlock.annotation("Override"); + classBlock.publicMethod("XmlWriter toXml(XmlWriter xmlWriter) throws XMLStreamException", + methodBlock -> methodBlock.methodReturn("toXml(xmlWriter, null)")); + + addGeneratedAnnotation.accept(classBlock); + classBlock.annotation("Override"); + classBlock.publicMethod("XmlWriter toXml(XmlWriter xmlWriter, String rootElementName) throws XMLStreamException", methodBlock -> { + String modelXmlName = propertiesManager.getXmlRootElementName(); + methodBlock.line("rootElementName = CoreUtils.isNullOrEmpty(rootElementName) ? \"" + modelXmlName + "\" : rootElementName;"); + methodBlock.line("xmlWriter.writeStartElement(rootElementName);"); + + String modelXmlNamespace = propertiesManager.getXmlRootElementNamespace(); + if (modelXmlNamespace != null) { + methodBlock.line("xmlWriter.writeNamespace(" + propertiesManager.getXmlNamespaceConstant(modelXmlNamespace) + ");"); + } + + propertiesManager.forEachXmlNamespaceWithPrefix((prefix, namespace) -> + methodBlock.line("xmlWriter.writeNamespace(\"" + prefix + "\", " + propertiesManager.getXmlNamespaceConstant(namespace) + ");")); + + // Assumption for XML is polymorphic discriminators are attributes. + if (propertiesManager.getDiscriminatorProperty() != null) { + serializeXml(methodBlock, propertiesManager.getDiscriminatorProperty().getProperty(), false, + propertiesManager); + } + + propertiesManager.forEachSuperXmlAttribute(property -> serializeXml(methodBlock, property, true, propertiesManager)); + propertiesManager.forEachXmlAttribute(property -> serializeXml(methodBlock, property, false, propertiesManager)); + + // Valid XML should only either have elements or text. + if (propertiesManager.hasXmlElements()) { + propertiesManager.forEachSuperXmlElement(property -> serializeXml(methodBlock, property, true, propertiesManager)); + propertiesManager.forEachXmlElement(property -> serializeXml(methodBlock, property, false, propertiesManager)); + } else { + propertiesManager.forEachSuperXmlText(property -> serializeXml(methodBlock, property, true, propertiesManager)); + propertiesManager.forEachXmlText(property -> serializeXml(methodBlock, property, false, propertiesManager)); + } + + methodBlock.methodReturn("xmlWriter.writeEndElement()"); + }); + } + + /** + * Serializes an XML element. + * + * @param methodBlock The method handling serialization. + * @param element The XML element being serialized. + * @param fromSuperType Whether the property is defined in the super type. + */ + private static void serializeXml(JavaBlock methodBlock, ClientModelProperty element, boolean fromSuperType, + ClientModelPropertiesManager propertiesManager) { + IType clientType = element.getClientType(); + IType wireType = element.getWireType(); + String propertyValueGetter; + if (fromSuperType) { + propertyValueGetter = (clientType != wireType) + ? wireType.convertFromClientType(element.getGetterName() + "()") + : element.getGetterName() + "()"; + } else { + propertyValueGetter = "this." + element.getName(); + } + + // Attempt to determine whether the wire type is simple serialization. + // This is primitives, boxed primitives, a small set of string based models, and other ClientModels. + String xmlSerializationMethodCall = wireType.xmlSerializationMethodCall("xmlWriter", element.getXmlName(), + propertiesManager.getXmlNamespaceConstant(element.getXmlNamespace()), propertyValueGetter, + element.isXmlAttribute(), false, true); + if (xmlSerializationMethodCall != null) { + Consumer serializationLogic = javaBlock -> { + // XML text has special handling. + if (element.isXmlText()) { + javaBlock.line("xmlWriter.writeString(" + propertyValueGetter + ");"); + } else { + javaBlock.line(xmlSerializationMethodCall + ";"); + } + }; + + // If the property is from a super type and the client type is different from the wire type then a null + // check is required to prevent a NullPointerException when converting the value. + if (fromSuperType && clientType != wireType && clientType.isNullable()) { + methodBlock.ifBlock(propertyValueGetter + " != null", serializationLogic); + } else { + serializationLogic.accept(methodBlock); + } + } else if (wireType instanceof ClassType && ((ClassType) wireType).isSwaggerType()) { + methodBlock.line("xmlWriter.writeXml(" + propertyValueGetter + ");"); + } else if (wireType instanceof IterableType) { + IType elementType = ((IterableType) wireType).getElementType(); + + methodBlock.ifBlock(propertyValueGetter + " != null", ifAction -> { + if (element.isXmlWrapper()) { + String writeStartElement = element.getXmlNamespace() == null + ? "xmlWriter.writeStartElement(\"" + element.getXmlName() + "\");" + : "xmlWriter.writeStartElement(" + propertiesManager.getXmlNamespaceConstant(element.getXmlNamespace()) + ", \"" + element.getXmlName() + "\");"; + ifAction.line(writeStartElement); + } + + String xmlWrite = elementType.xmlSerializationMethodCall("xmlWriter", element.getXmlListElementName(), + propertiesManager.getXmlNamespaceConstant(element.getXmlListElementNamespace()), "element", false, + false, true); + ifAction.line("for (%s element : %s) {", elementType, propertyValueGetter); + ifAction.indent(() -> ifAction.line(xmlWrite + ";")); + ifAction.line("}"); + + if (element.isXmlWrapper()) { + ifAction.line("xmlWriter.writeEndElement();"); + } + }); + } else if (wireType instanceof MapType) { + // Assumption is that the key type for the Map is a String. This may not always hold true and when that + // becomes reality this will need to be reworked to handle that case. + IType valueType = ((MapType) wireType).getValueType(); + + methodBlock.ifBlock(propertyValueGetter + " != null", ifAction -> { + ifAction.line("xmlWriter.writeStartElement(\"" + element.getXmlName() + "\");"); + + if (valueType instanceof ClassType && ((ClassType) valueType).isSwaggerType()) { + String writeStartElement = (element.getXmlNamespace() != null) + ? "xmlWriter.writeStartElement(" + propertiesManager.getXmlNamespaceConstant(element.getXmlNamespace()) + ", key);" + : "xmlWriter.writeStartElement(key);"; + + ifAction.line("for (Map.Entry entry : %s.entrySet()) {", valueType, propertyValueGetter); + ifAction.indent(() -> { + ifAction.line(writeStartElement); + ifAction.line("xmlWriter.writeXml(entry.getValue());"); + ifAction.line("xmlWriter.writeEndElement();"); + }); + ifAction.line("}"); + } else { + String xmlWrite = valueType.xmlSerializationMethodCall("xmlWriter", "entry.getKey()", + propertiesManager.getXmlNamespaceConstant(element.getXmlNamespace()), "entry.getValue()", false, + true, true); + + ifAction.line("for (Map.Entry entry : %s.entrySet()) {", valueType, propertyValueGetter); + ifAction.indent(() -> ifAction.line(xmlWrite + ";")); + ifAction.line("}"); + } + + ifAction.line("xmlWriter.writeEndElement();"); + }); + } else { + // TODO (alzimmer): Resolve this as serialization logic generation needs to handle all cases. + throw new RuntimeException("Unknown wire type " + wireType + " in XML element serialization. " + + "Need to add support for it."); + } + } + + private static void writeFromXml(JavaClass classBlock, ClientModel model, + ClientModelPropertiesManager propertiesManager, JavaSettings settings, + Consumer addGeneratedAnnotation) { + if (isSuperTypeWithDiscriminator(model)) { + writeSuperTypeFromXml(classBlock, model, propertiesManager, settings, addGeneratedAnnotation); + } else { + writeTerminalTypeFromXml(classBlock, propertiesManager, settings, addGeneratedAnnotation); + } + } + + /** + * Writes a super type's {@code fromXml(XmlReader)} method. + * + * @param classBlock The class having {@code fromXml(XmlReader)} written to it. + * @param model The Autorest representation of the model. + * @param propertiesManager The properties for the model. + * @param settings The Autorest generation settings. + * @param addGeneratedAnnotation Consumer for adding the generated annotation to the class. + */ + private static void writeSuperTypeFromXml(JavaClass classBlock, ClientModel model, + ClientModelPropertiesManager propertiesManager, JavaSettings settings, + Consumer addGeneratedAnnotation) { + // Handling polymorphic fields while determining which subclass, or the class itself, to deserialize handles the + // discriminator type always as a String. This is permissible as the found discriminator is never being used in + // a setter or for setting a field, unlike in the actual deserialization method where it needs to be the same + // type as the field. + ClientModelProperty discriminatorProperty = propertiesManager.getDiscriminatorProperty().getProperty(); + readXmlObject(classBlock, propertiesManager, false, methodBlock -> { + // Assumption for now for XML, only XML properties are used for handling inheritance. + // If this found to be wrong in the future copy the concept of bufferObject and resettable from azure-json + // into azure-xml as bufferElement and resettable. + methodBlock.line("// Get the XML discriminator attribute."); + if (discriminatorProperty.getXmlNamespace() != null) { + methodBlock.line("String discriminatorValue = reader.getStringAttribute(" + + propertiesManager.getXmlNamespaceConstant(discriminatorProperty.getXmlNamespace()) + ", " + + "\"" + discriminatorProperty.getSerializedName() + "\");"); + } else { + methodBlock.line("String discriminatorValue = reader.getStringAttribute(" + + "\"" + discriminatorProperty.getSerializedName() + "\");"); + } + + methodBlock.line("// Use the discriminator value to determine which subtype should be deserialized."); + + // Add deserialization for the super type itself. + JavaIfBlock ifBlock = null; + + // Add deserialization for all child types. + List childTypes = getAllChildTypes(model, new ArrayList<>()); + for (ClientModel childType : childTypes) { + ifBlock = ifOrElseIf(methodBlock, ifBlock, "\"" + childType.getSerializedName() + "\".equals(discriminatorValue)", + ifStatement -> ifStatement.methodReturn(childType.getName() + (isSuperTypeWithDiscriminator(childType) + ? ".fromXmlInternal(reader, finalRootElementName)" + : ".fromXml(reader, finalRootElementName)"))); + } + + if (ifBlock == null) { + methodBlock.methodReturn("fromXmlInternal(reader, finalRootElementName)"); + } else { + ifBlock.elseBlock(elseBlock -> elseBlock.methodReturn("fromXmlInternal(reader, finalRootElementName)")); + } + }, addGeneratedAnnotation); + + readXmlObject(classBlock, propertiesManager, true, + methodBlock -> writeFromXmlDeserialization(methodBlock, propertiesManager, settings), + addGeneratedAnnotation); + } + + /** + * Adds a static method to the class with the signature that handles reading the XML string into the object type. + *

+ * If {@code superTypeReading} is true the method will be package-private and named + * {@code fromXmlWithKnownDiscriminator} instead of being public and named {@code fromXml}. This is done as super + * types use their {@code fromXml} method to determine the discriminator value and pass the reader to the specific + * type being deserialized. The specific type being deserialized may be the super type itself, so it cannot pass to + * {@code fromXml} as this will be a circular call and if the specific type being deserialized is an intermediate + * type (a type having both super and subclasses) it will attempt to perform discriminator validation which has + * already been done. + * + * @param classBlock The class where the {@code fromXml} method is being written. + * @param propertiesManager Properties information about the object being deserialized. + * @param superTypeReading Whether the object reading is for a super type. + * @param deserializationBlock Logic for deserializing the object. + * @param addGeneratedAnnotation Consumer for adding the generated annotation to the class. + */ + private static void readXmlObject(JavaClass classBlock, ClientModelPropertiesManager propertiesManager, + boolean superTypeReading, Consumer deserializationBlock, + Consumer addGeneratedAnnotation) { + JavaVisibility visibility = superTypeReading ? JavaVisibility.PackagePrivate : JavaVisibility.Public; + String methodName = superTypeReading ? "fromXmlInternal" : "fromXml"; + + String modelName = propertiesManager.getModel().getName(); + boolean hasRequiredProperties = propertiesManager.hasConstructorArguments(); + boolean isPolymorphic = propertiesManager.getDiscriminatorProperty() != null + && CoreUtils.isNullOrEmpty(propertiesManager.getModel().getDerivedModels()); + + if (!superTypeReading) { + fromXmlJavadoc(classBlock, modelName, false, hasRequiredProperties, isPolymorphic); + addGeneratedAnnotation.accept(classBlock); + classBlock.publicStaticMethod(modelName + " fromXml(XmlReader xmlReader) throws XMLStreamException", + methodBlock -> methodBlock.methodReturn("fromXml(xmlReader, null)")); + + fromXmlJavadoc(classBlock, modelName, true, hasRequiredProperties, isPolymorphic); + } + + addGeneratedAnnotation.accept(classBlock); + classBlock.staticMethod(visibility, modelName + " " + methodName + "(XmlReader xmlReader, String rootElementName) throws XMLStreamException", methodBlock -> { + // For now, use the basic readObject which will return null if the XmlReader is pointing to JsonToken.NULL. + // + // Support for a default value if null will need to be supported and for objects that get their value + // from a JSON value instead of JSON object or are an array type. + String requiredElementName = propertiesManager.getXmlRootElementName(); + String requiredNamespace = propertiesManager.getXmlRootElementNamespace(); + + methodBlock.line("String finalRootElementName = CoreUtils.isNullOrEmpty(rootElementName) ? " + + "\"" + requiredElementName + "\" : rootElementName;"); + if (requiredNamespace != null) { + methodBlock.line("return xmlReader.readObject(" + propertiesManager.getXmlNamespaceConstant(requiredNamespace) + ", finalRootElementName, reader -> {"); + } else { + methodBlock.line("return xmlReader.readObject(finalRootElementName, reader -> {"); + } + + deserializationBlock.accept(methodBlock); + + methodBlock.line("});"); + }); + } + + private static void fromXmlJavadoc(JavaClass classBlock, String modelName, boolean hasRootElementName, + boolean hasRequiredProperties, boolean isPolymorphic) { + classBlock.javadocComment(javadocComment -> { + javadocComment.description("Reads an instance of " + modelName + " from the XmlReader."); + javadocComment.param("xmlReader", "The XmlReader being read."); + if (hasRootElementName) { + javadocComment.param("rootElementName", "Optional root element name to override the default defined " + + "by the model. Used to support cases where the model can deserialize from different root element " + + "names."); + } + javadocComment.methodReturns("An instance of " + modelName + " if the XmlReader was pointing to an " + + "instance of it, or null if it was pointing to XML null."); + + // TODO (alzimmer): Make the throws statement more descriptive by including the polymorphic + // discriminator property name and the required property names. For now this covers the base functionality. + String throwsStatement = null; + if (hasRequiredProperties && isPolymorphic) { + throwsStatement = "If the deserialized XML object was missing any required properties or the " + + "polymorphic discriminator value is invalid."; + } else if (hasRequiredProperties) { + throwsStatement = "If the deserialized XML object was missing any required properties."; + } else if (isPolymorphic) { + throwsStatement = "If the deserialized XML object has an invalid polymorphic discriminator value."; + } + + if (throwsStatement != null) { + javadocComment.methodThrows("IllegalStateException", throwsStatement); + } + + javadocComment.methodThrows("XMLStreamException", "If an error occurs while reading the " + modelName + "."); + }); + } + + /** + * Writes a terminal type's {@code fromXml(XmlReader)} method. + *

+ * A terminal type is either a type without polymorphism or is the terminal type in a polymorphic hierarchy. + * + * @param classBlock The class having {@code fromXml(XmlReader)} written to it. + * @param propertiesManager The properties for the model. + * @param settings The Autorest generation settings. + * @param addGeneratedAnnotation Consumer for adding the generated annotation to the class. + */ + private static void writeTerminalTypeFromXml(JavaClass classBlock, ClientModelPropertiesManager propertiesManager, + JavaSettings settings, Consumer addGeneratedAnnotation) { + readXmlObject(classBlock, propertiesManager, false, + methodBlock -> writeFromXmlDeserialization(methodBlock, propertiesManager, settings), + addGeneratedAnnotation); + } + + private static void writeFromXmlDeserialization(JavaBlock methodBlock, + ClientModelPropertiesManager propertiesManager, JavaSettings settings) { + + // Add the deserialization logic. + methodBlock.indent(() -> { + // Initialize local variables to track what has been deserialized. + initializeLocalVariables(methodBlock, propertiesManager, true, settings); + + // Assumption for XML is polymorphic discriminators are attributes. + if (propertiesManager.getDiscriminatorProperty() != null) { + deserializeXmlAttribute(methodBlock, propertiesManager.getDiscriminatorProperty().getProperty(), + propertiesManager, false); + } + + // Read the XML attribute properties first. + propertiesManager.forEachSuperXmlAttribute(attribute -> deserializeXmlAttribute(methodBlock, attribute, + propertiesManager, true)); + propertiesManager.forEachXmlAttribute(attribute -> deserializeXmlAttribute(methodBlock, attribute, + propertiesManager, false)); + + // Read the XML text next. + propertiesManager.forEachSuperXmlText(text -> deserializeXmlText(methodBlock, text, propertiesManager, true)); + propertiesManager.forEachXmlText(text -> deserializeXmlText(methodBlock, text, propertiesManager, false)); + + // Model didn't have any XML elements, return early. + String fieldNameVariableName = propertiesManager.getXmlReaderNameVariableName(); + if (!propertiesManager.hasXmlElements()) { + // If the model was attributes only a simplified read loop is needed to ensure the end element token + // is reached. + if (!propertiesManager.hasXmlTexts()) { + methodBlock.block("while (reader.nextElement() != XmlToken.END_ELEMENT)", + whileBlock -> whileBlock.line("reader.skipElement();")); + } + return; + } + + // Add the outermost while loop to read the JSON object. + addReaderWhileLoop(methodBlock, true, fieldNameVariableName, true, whileBlock -> { + JavaIfBlock ifBlock = null; + + if (propertiesManager.getDiscriminatorProperty() != null + && !propertiesManager.getDiscriminatorProperty().getProperty().isXmlAttribute()) { + ClientModelProperty discriminatorProperty = propertiesManager.getDiscriminatorProperty() + .getProperty(); + String ifStatement = String.format("\"%s\".equals(%s)", propertiesManager.getExpectedDiscriminator(), + fieldNameVariableName); + + ifBlock = methodBlock.ifBlock(ifStatement, ifAction -> { + ifAction.line("String %s = reader.getStringElement().getLocalPart();", discriminatorProperty.getName()); + String ifStatement2 = String.format("!%s.equals(%s)", discriminatorProperty.getDefaultValue(), + discriminatorProperty.getName()); + ifAction.ifBlock(ifStatement2, ifAction2 -> ifAction2.line( + "throw new IllegalStateException(\"'%s' was expected to be non-null and equal to '\"%s\"'. " + + "The found '%s' was '\" + %s + \"'.\");", + discriminatorProperty.getSerializedName(), propertiesManager.getExpectedDiscriminator(), + discriminatorProperty.getSerializedName(), discriminatorProperty.getName())); + }); + } + + // Loop over all properties and generate their deserialization handling. + AtomicReference ifBlockReference = new AtomicReference<>(ifBlock); + propertiesManager.forEachSuperXmlElement(element -> handleXmlPropertyDeserialization(element, + whileBlock, ifBlockReference, fieldNameVariableName, propertiesManager, true, settings)); + propertiesManager.forEachXmlElement(element -> handleXmlPropertyDeserialization(element, whileBlock, + ifBlockReference, fieldNameVariableName, propertiesManager, false, settings)); + + ifBlock = ifBlockReference.get(); + + // All properties have been checked for, add an else block that will either ignore unknown properties + // or add them into an additional properties bag. + ClientModelProperty additionalProperty = getAdditionalPropertiesPropertyInModelOrFromSuper(propertiesManager); + handleUnknownXmlFieldDeserialization(whileBlock, ifBlock, additionalProperty, + propertiesManager.getXmlReaderNameVariableName()); + }); + }); + + // Add the validation and return logic. + handleReadReturn(methodBlock, propertiesManager.getModel().getName(), propertiesManager, settings); + } + + private static void deserializeXmlAttribute(JavaBlock methodBlock, ClientModelProperty attribute, + ClientModelPropertiesManager propertiesManager, boolean fromSuper) { + String xmlAttributeDeserialization = getSimpleXmlDeserialization(attribute.getWireType(), "reader", null, + attribute.getXmlName(), propertiesManager.getXmlNamespaceConstant(attribute.getXmlNamespace()), true); + + if (attribute.isPolymorphicDiscriminator() + && CoreUtils.isNullOrEmpty(propertiesManager.getModel().getDerivedModels())) { + // Only validate the discriminator if the model has no derived models. + // Super types will deserialize as themselves if the discriminator doesn't match what's expected. + methodBlock.line("String discriminatorValue = " + xmlAttributeDeserialization + ";"); + String ifStatement = String.format("!%s.equals(discriminatorValue)", attribute.getDefaultValue()); + methodBlock.ifBlock(ifStatement, ifAction2 -> ifAction2.line( + "throw new IllegalStateException(\"'%s' was expected to be non-null and equal to '%s'. " + + "The found '%s' was '\" + discriminatorValue + \"'.\");", + attribute.getSerializedName(), propertiesManager.getExpectedDiscriminator(), + attribute.getSerializedName())); + + xmlAttributeDeserialization = "discriminatorValue"; + } + + if (propertiesManager.hasConstructorArguments()) { + methodBlock.line("%s %s = %s;", attribute.getClientType(), attribute.getName(), xmlAttributeDeserialization); + } else { + handleSettingDeserializedValue(methodBlock, propertiesManager.getDeserializedModelName(), + propertiesManager.getModel(), attribute, xmlAttributeDeserialization, fromSuper, false); + } + } + + private static void deserializeXmlText(JavaBlock methodBlock, ClientModelProperty text, + ClientModelPropertiesManager propertiesManager, boolean fromSuper) { + String xmlTextDeserialization = getSimpleXmlDeserialization(text.getWireType(), "reader", null, null, null, false); + if (propertiesManager.hasConstructorArguments()) { + methodBlock.line(text.getClientType() + " " + text.getName() + " = " + xmlTextDeserialization + ";"); + } else { + handleSettingDeserializedValue(methodBlock, propertiesManager.getDeserializedModelName(), + propertiesManager.getModel(), text, xmlTextDeserialization, fromSuper, false); + } + } + + private static void handleXmlPropertyDeserialization(ClientModelProperty property, JavaBlock methodBlock, + AtomicReference ifBlockReference, String fieldNameVariableName, + ClientModelPropertiesManager propertiesManager, boolean fromSuper, JavaSettings settings) { + // Property will be handled later by flattened deserialization. + // XML should never have flattening. + if (property.getNeedsFlatten()) { + return; + } + + JavaIfBlock ifBlock = ifBlockReference.get(); + ifBlock = handleXmlPropertyDeserialization(property, methodBlock, ifBlock, fieldNameVariableName, + propertiesManager, fromSuper, settings); + + ifBlockReference.set(ifBlock); + } + + private static JavaIfBlock handleXmlPropertyDeserialization(ClientModelProperty property, JavaBlock methodBlock, + JavaIfBlock ifBlock, String fieldNameVariableName, ClientModelPropertiesManager propertiesManager, + boolean fromSuper, JavaSettings settings) { + String xmlElementName = (property.getClientType() instanceof IterableType && !property.isXmlWrapper()) + ? property.getXmlListElementName() : property.getXmlName(); + String xmlNamespace = propertiesManager.getXmlNamespaceConstant(property.getXmlNamespace()); + + if (CoreUtils.isNullOrEmpty(xmlElementName)) { + return ifBlock; + } + + String condition = getXmlNameConditional(xmlElementName, xmlNamespace, fieldNameVariableName, true); + return ifOrElseIf(methodBlock, ifBlock, condition, + deserializationBlock -> generateXmlDeserializationLogic(deserializationBlock, property, propertiesManager, + fromSuper, settings)); + } + + private static void generateXmlDeserializationLogic(JavaBlock deserializationBlock, ClientModelProperty property, + ClientModelPropertiesManager propertiesManager, boolean fromSuper, JavaSettings settings) { + IType wireType = property.getWireType(); + + // Attempt to determine whether the wire type is simple deserialization. + // This is primitives, boxed primitives, a small set of string based models, and other ClientModels. + String simpleDeserialization = getSimpleXmlDeserialization(wireType, "reader", property.getXmlName(), null, + null, false); + if (simpleDeserialization != null) { + if (propertiesManager.hasConstructorArguments()) { + deserializationBlock.line(property.getName() + " = " + simpleDeserialization + ";"); + } else { + handleSettingDeserializedValue(deserializationBlock, propertiesManager.getDeserializedModelName(), + propertiesManager.getModel(), property, simpleDeserialization, fromSuper, false); + } + } else if (wireType instanceof IterableType) { + IType elementType = ((IterableType) wireType).getElementType(); + boolean sameNames = Objects.equals(property.getXmlName(), property.getXmlListElementName()); + String elementDeserialization = getSimpleXmlDeserialization(elementType, "reader", + sameNames ? property.getXmlName() : property.getXmlListElementName(), null, null, false); + String fieldAccess; + if (propertiesManager.hasConstructorArguments()) { + // Cases with constructor arguments will have a local variable based on the name of the property. + fieldAccess = property.getName(); + } else if (fromSuper) { + // Cases where the property is from the super type will need to access the getter. + fieldAccess = propertiesManager.getDeserializedModelName() + "." + property.getGetterName() + "()"; + } else { + // Otherwise access the property directly. + fieldAccess = propertiesManager.getDeserializedModelName() + "." + property.getName(); + } + + if (!property.isXmlWrapper()) { + deserializationBlock.line(fieldAccess + ".add(" + elementDeserialization + ");"); + } else { + deserializationBlock.block("while (reader.nextElement() != XmlToken.END_ELEMENT)", whileBlock -> { + whileBlock.line("elementName = reader.getElementName();"); + String condition = getXmlNameConditional(property.getXmlListElementName(), + propertiesManager.getXmlNamespaceConstant(property.getXmlListElementNamespace()), "elementName", + true); + whileBlock.ifBlock(condition, ifBlock -> { + // TODO (alzimmer): Handle nested container types when needed. + ifBlock.ifBlock(fieldAccess + " == null", ifStatement -> { + if (fromSuper) { + ifStatement.line(propertiesManager.getDeserializedModelName() + "." + property.getSetterName() + + "(new ArrayList<>());"); + } else { + ifStatement.line(fieldAccess + " = new ArrayList<>();"); + } + }); + + ifBlock.line(fieldAccess + ".add(" + elementDeserialization + ");"); + }) + .elseBlock(elseBlock -> elseBlock.line("reader.skipElement();")); + }); + } + } else if (wireType instanceof MapType) { + IType valueType = ((MapType) wireType).getValueType(); + String fieldAccess = propertiesManager.hasConstructorArguments() + ? property.getName() + : propertiesManager.getDeserializedModelName() + "." + property.getName(); + + String valueDeserialization = getSimpleXmlDeserialization(valueType, "reader", property.getXmlName(), null, + null, false); + deserializationBlock.block("while (reader.nextElement() != XmlToken.END_ELEMENT)", whileBlock -> { + // TODO (alzimmer): Handle nested container types when needed. + // Assumption is that the key type for the Map is a String. This may not always hold true and when that + // becomes reality this will need to be reworked to handle that case. + whileBlock.ifBlock(fieldAccess + " == null", + ifStatement -> ifStatement.line(fieldAccess + " = new LinkedHashMap<>();")); + + whileBlock.line(fieldAccess + ".put(reader.getElementName().getLocalPart(), " + valueDeserialization + ");"); + }); + } else { + // TODO (alzimmer): Resolve this as deserialization logic generation needs to handle all cases. + throw new RuntimeException("Unknown wire type " + wireType + ". Need to add support for it."); + } + + // If the property was required, mark it as found. + if (includePropertyInConstructor(property, settings) && !settings.isDisableRequiredJsonAnnotation()) { + deserializationBlock.line(property.getName() + "Found = true;"); + } + } + + private static void handleUnknownXmlFieldDeserialization(JavaBlock methodBlock, JavaIfBlock ifBlock, + ClientModelProperty additionalProperties, String fieldNameVariableName) { + Consumer unknownFieldConsumer = javaBlock -> { + if (additionalProperties != null) { + javaBlock.ifBlock(additionalProperties.getName() + " == null", + ifAction -> ifAction.line(additionalProperties.getName() + " = new LinkedHashMap<>();")); + javaBlock.line(); + + // Assumption, additional properties is a Map of String-Object + IType valueType = ((MapType) additionalProperties.getWireType()).getValueType(); + if (valueType == ClassType.OBJECT) { + // String fieldName should be a local variable accessible in this spot of code. + javaBlock.line(additionalProperties.getName() + ".put(" + fieldNameVariableName + ", reader.readUntyped());"); + } else { + // Another assumption, the additional properties value type is simple. + javaBlock.line(additionalProperties.getName() + ".put(" + fieldNameVariableName + ", " + + getSimpleXmlDeserialization(valueType, "reader", null, null, null, false) + ");"); + } + } else { + javaBlock.line("reader.skipElement();"); + } + }; + + if (ifBlock == null) { + unknownFieldConsumer.accept(methodBlock); + } else { + ifBlock.elseBlock(unknownFieldConsumer); + } + } + + private static String getSimpleXmlDeserialization(IType wireType, String readerName, String elementName, + String attributeName, String attributeNamespace, boolean namespaceIsConstant) { + if (wireType instanceof ClassType && ((ClassType) wireType).isSwaggerType()) { + return CoreUtils.isNullOrEmpty(elementName) + ? wireType + ".fromXml(" + readerName + ")" + : wireType + ".fromXml(" + readerName + ", \"" + elementName + "\")"; + } else { + return wireType.xmlDeserializationMethod(readerName, attributeName, attributeNamespace, namespaceIsConstant); + } + } + + private static List getClientModelPropertiesInJsonTree( + JsonFlattenedPropertiesTree tree) { + if (tree.getProperty() != null) { + // Terminal node only contains a property. + return Collections.singletonList(tree.getProperty()); + } else { + List treeProperties = new ArrayList<>(); + for (JsonFlattenedPropertiesTree childNode : tree.getChildrenNodes().values()) { + treeProperties.addAll(getClientModelPropertiesInJsonTree(childNode)); + } + + return treeProperties; + } + } + + private static String getXmlNameConditional(String localPart, String namespace, String elementName, + boolean namespaceIsConstant) { + String condition = "\"" + localPart + "\".equals(" + elementName + ".getLocalPart())"; + if (!CoreUtils.isNullOrEmpty(namespace)) { + if (namespaceIsConstant) { + condition += " && " + namespace + ".equals(" + elementName + ".getNamespaceURI())"; + } else { + condition += " && \"" + namespace + "\".equals(" + elementName + ".getNamespaceURI())"; + } + } + + return condition; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/SwaggerReadmeTemplate.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/SwaggerReadmeTemplate.java new file mode 100644 index 0000000000..a7e6e529e7 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/SwaggerReadmeTemplate.java @@ -0,0 +1,178 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.template; + +import com.microsoft.typespec.http.client.generator.core.extension.plugin.AutorestSettings; +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; +import com.microsoft.typespec.http.client.generator.core.model.projectmodel.Project; +import com.microsoft.typespec.http.client.generator.core.util.TemplateUtil; +import com.azure.core.util.CoreUtils; +import org.yaml.snakeyaml.DumperOptions; +import org.yaml.snakeyaml.Yaml; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class SwaggerReadmeTemplate { + + private final StringBuilder builder = new StringBuilder(); + + private static final String NEW_LINE = System.lineSeparator(); + + private static final Pattern MARKDOWN_YAML_BLOCK = + Pattern.compile("```\\s?(?:yaml|YAML).*?\\n(.*?)```", Pattern.DOTALL); + + private static final Map OVERRIDE_OPTIONS = new LinkedHashMap<>(); + static { + OVERRIDE_OPTIONS.put("output-folder", "../"); + OVERRIDE_OPTIONS.put("java", true); + OVERRIDE_OPTIONS.put("regenerate-pom", false); +// OVERRIDE_SETTINGS.put("partial-update", true); + OVERRIDE_OPTIONS.put("sdk-integration", null); + } + + private static final Map DEFAULT_OPTIONS = loadDefaultOptions(); + + public String write(Project project) { + JavaSettings settings = JavaSettings.getInstance(); + + // prepare OVERRIDE_SETTINGS + updateOverrideOptions(settings); + + // prepare YAML object + DumperOptions dumperOptions = new DumperOptions(); + dumperOptions.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); + Yaml yaml = new Yaml(dumperOptions); + + Map objectNode = new LinkedHashMap<>(); + addRequireOrInputFile(objectNode, settings.getAutorestSettings()); + // settings from internal + for (Map.Entry entry : OVERRIDE_OPTIONS.entrySet()) { + if (entry.getValue() != null) { + objectNode.put(entry.getKey(), entry.getValue()); + } + } + // settings from external + for (Map.Entry entry : settings.getSimpleJavaSettings().entrySet()) { + if (!OVERRIDE_OPTIONS.containsKey(entry.getKey()) && entry.getValue() != null) { + objectNode.put(entry.getKey(), entry.getValue()); + } + } + // service-versions + objectNode.put("service-versions", project.getApiVersions()); + + objectNode = removeDefaultOptions(objectNode); + + // write README + line("## Generate autorest code"); + newLine(); + line("```yaml"); + builder.append(yaml.dump(objectNode)); + line("```"); + + return builder.toString(); + } + + private static void addRequireOrInputFile(Map objectNode, AutorestSettings autorestSettings) { + // try use "require" + boolean useRequire = false; + List requireList = autorestSettings.getRequire(); + if (!CoreUtils.isNullOrEmpty(requireList)) { + String require = requireList.iterator().next(); + + if (require.contains("data-plane")) { + useRequire = true; + objectNode.put("require", require); + } + } + + if (!useRequire) { + // use "input-file" + objectNode.put("input-file", autorestSettings.getInputFiles()); + } + } + + private static Map removeDefaultOptions(Map objectNode) { + Map filteredNode = new LinkedHashMap<>(); + + objectNode.entrySet().forEach(e -> { + String key = e.getKey(); + if (!(DEFAULT_OPTIONS.containsKey(key) && Objects.equals(e.getValue(), DEFAULT_OPTIONS.get(key)))) { + filteredNode.put(e.getKey(), e.getValue()); + } + }); + + return filteredNode; + } + + @SuppressWarnings("unchecked") + private static Map loadDefaultOptions() { + Map defaultOptions = new LinkedHashMap<>(); + + // the file is copied from javagen/data-plane.md to resources, using maven-resources-plugin + String defaultDpgReadme = TemplateUtil.loadTextFromResource("data-plane.md"); + if (!CoreUtils.isNullOrEmpty(defaultDpgReadme)) { + Matcher matcher = MARKDOWN_YAML_BLOCK.matcher(defaultDpgReadme); + Yaml yaml = new Yaml(); + while (matcher.find()) { + String yamlStr = matcher.group(1); + Object yamlObj = yaml.load(yamlStr); + if (yamlObj instanceof Map) { + Map yamlMap = (Map) yamlObj; + yamlMap.entrySet().forEach(e -> { + if (e.getValue() instanceof String + || e.getValue() instanceof Boolean + || e.getValue() instanceof Integer) { + defaultOptions.put(e.getKey(), e.getValue()); + } + }); + } + } + } + + return defaultOptions; + } + + private static void updateOverrideOptions(JavaSettings settings) { + String title = settings.getAutorestSettings().getTitle(); + if (title != null) { + OVERRIDE_OPTIONS.put("title", title); + } + + if (!settings.getAutorestSettings().getSecurity().isEmpty()) { + OVERRIDE_OPTIONS.putIfAbsent("security", + stringOrArray(settings.getAutorestSettings().getSecurity())); + } + if (!settings.getAutorestSettings().getSecurityScopes().isEmpty()) { + OVERRIDE_OPTIONS.putIfAbsent("security-scopes", + stringOrArray(settings.getAutorestSettings().getSecurityScopes())); + } + + String securityHeaderName = settings.getAutorestSettings().getSecurityHeaderName(); + if (securityHeaderName != null) { + OVERRIDE_OPTIONS.put("security-header-name", securityHeaderName); + } + } + + private void line(String text) { + builder.append(text); + newLine(); + } + + private void newLine() { + builder.append(NEW_LINE); + } + + private static Object stringOrArray(List array) { + if (array.size() == 1) { + return array.iterator().next(); + } else { + return array; + } + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/TemplateFactory.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/TemplateFactory.java new file mode 100644 index 0000000000..9b8932124a --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/TemplateFactory.java @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.template; + +public interface TemplateFactory { + + ServiceClientInterfaceTemplate getServiceClientInterfaceTemplate(); + + ServiceClientTemplate getServiceClientTemplate(); + + ServiceClientBuilderTemplate getServiceClientBuilderTemplate(); + + ServiceVersionTemplate getServiceVersionTemplate(); + + MethodGroupInterfaceTemplate getMethodGroupInterfaceTemplate(); + + MethodGroupTemplate getMethodGroupTemplate(); + + ProxyTemplate getProxyTemplate(); + + ClientMethodTemplate getClientMethodTemplate(); + + ModelTemplate getModelTemplate(); + + StreamSerializationModelTemplate getStreamStyleModelTemplate(); + + ExceptionTemplate getExceptionTemplate(); + + EnumTemplate getEnumTemplate(); + + ResponseTemplate getResponseTemplate(); + + XmlSequenceWrapperTemplate getXmlSequenceWrapperTemplate(); + + PackageInfoTemplate getPackageInfoTemplate(); + + ServiceAsyncClientTemplate getServiceAsyncClientTemplate(); + + ServiceSyncClientTemplate getServiceSynClientTemplate(); + + ServiceSyncClientTemplate getServiceSyncClientWrapAsyncClientTemplate(); + + WrapperClientMethodTemplate getWrapperClientMethodTemplate(); + + PomTemplate getPomTemplate(); + + ModuleInfoTemplate getModuleInfoTemplate(); + + ProtocolSampleTemplate getProtocolSampleTemplate(); + + ConvenienceAsyncMethodTemplate getConvenienceAsyncMethodTemplate(); + + ConvenienceSyncMethodTemplate getConvenienceSyncMethodTemplate(); + + UnionModelTemplate getUnionModelTemplate(); + + ClientMethodSampleTemplate getClientMethodSampleTemplate(); + + JsonMergePatchHelperTemplate getJsonMergePatchHelperTemplate(); +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/TemplateHelper.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/TemplateHelper.java new file mode 100644 index 0000000000..00e4fca2e9 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/TemplateHelper.java @@ -0,0 +1,181 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.template; + +import com.microsoft.typespec.http.client.generator.core.Javagen; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Scheme; +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; +import com.microsoft.typespec.http.client.generator.core.extension.plugin.PluginLogger; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClassType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.PipelinePolicyDetails; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.SecurityInfo; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ServiceClient; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaBlock; +import com.microsoft.typespec.http.client.generator.core.util.CodeNamer; +import com.azure.core.util.CoreUtils; +import org.slf4j.Logger; + +public final class TemplateHelper { + private final static Logger LOGGER = new PluginLogger(Javagen.getPluginInstance(), ServiceClientBuilderTemplate.class); + + public static String getPomProjectName(String serviceName) { + return String.format( + JavaSettings.getInstance().isBranded() ? "Microsoft Azure SDK for %s" : "SDK for %s", + serviceName); + } + + public static String getPomProjectDescription(String serviceName) { + return String.format( + JavaSettings.getInstance().isBranded() ? "This package contains Microsoft Azure %1$s client library." : "This package contains %1$s client library.", + serviceName); + } + + public static String getByteCloneExpression(String propertyName) { + if (JavaSettings.getInstance().isBranded()) { + return String.format("CoreUtils.clone(%s)", propertyName); + } else { + // TODO: generic not having CoreUtils + return propertyName; + } + } + + public static void createHttpPipelineMethod(JavaSettings settings, String defaultCredentialScopes, SecurityInfo securityInfo, PipelinePolicyDetails pipelinePolicyDetails, JavaBlock function) { + if (!settings.isBranded()) { + createGenericHttpPipelineMethod(settings, defaultCredentialScopes, securityInfo, pipelinePolicyDetails, function); + } else { + createAzureHttpPipelineMethod(settings, defaultCredentialScopes, securityInfo, pipelinePolicyDetails, function); + } + } + + private static void createGenericHttpPipelineMethod(JavaSettings settings, String defaultCredentialScopes, SecurityInfo securityInfo, PipelinePolicyDetails pipelinePolicyDetails, JavaBlock function) { + function.line("Configuration buildConfiguration = (configuration == null) ? Configuration" + + ".getGlobalConfiguration() : configuration;"); + String localHttpLogOptionsName = "local" + CodeNamer.toPascalCase("httpLogOptions"); + function.line(String.format("HttpLogOptions %s = this.httpLogOptions == null ? new HttpLogOptions() : this.httpLogOptions;", localHttpLogOptionsName)); + + function.line("HttpPipelineBuilder httpPipelineBuilder = new HttpPipelineBuilder();"); + function.line("List policies = new ArrayList<>();"); + function.line("policies.add(redirectOptions == null ? new HttpRedirectPolicy() : new HttpRedirectPolicy(redirectOptions));"); + function.line("policies.add(retryOptions == null ? new HttpRetryPolicy() : new HttpRetryPolicy(retryOptions));"); + function.line("this.pipelinePolicies.stream().forEach(p -> policies.add(p));"); + if (securityInfo.getSecurityTypes().contains(Scheme.SecuritySchemeType.KEY)) { + function.ifBlock("keyCredential != null", action -> { + final String prefixExpr = CoreUtils.isNullOrEmpty(securityInfo.getHeaderValuePrefix()) + ? "null" + : ClassType.STRING.defaultValueExpression(securityInfo.getHeaderValuePrefix()); + function.line("policies.add(new KeyCredentialPolicy(\"" + + securityInfo.getHeaderName() + + "\", keyCredential, " + + prefixExpr + + "));"); + }); + } + function.line("httpPipelineBuilder.policies(policies.toArray(new HttpPipelinePolicy[0]));"); + function.methodReturn("httpPipelineBuilder.build()"); + } + + private static void createAzureHttpPipelineMethod(JavaSettings settings, String defaultCredentialScopes, SecurityInfo securityInfo, PipelinePolicyDetails pipelinePolicyDetails, JavaBlock function) { + function.line("Configuration buildConfiguration = (configuration == null) ? Configuration" + + ".getGlobalConfiguration() : configuration;"); + + String localHttpLogOptionsName = "local" + CodeNamer.toPascalCase("httpLogOptions"); + String localClientOptionsName = "local" + CodeNamer.toPascalCase("ClientOptions"); + function.line(String.format("HttpLogOptions %s = this.httpLogOptions == null ? new HttpLogOptions() : this.httpLogOptions;", localHttpLogOptionsName)); + function.line(String.format("ClientOptions %s = this.clientOptions == null ? new ClientOptions() : this.clientOptions;", localClientOptionsName)); + + function.line("List policies = new ArrayList<>();"); + + function.line("String clientName = PROPERTIES.getOrDefault(SDK_NAME, \"UnknownName\");"); + function.line("String clientVersion = PROPERTIES.getOrDefault(SDK_VERSION, \"UnknownVersion\");"); + + function.line(String.format("String applicationId = CoreUtils.getApplicationId(%s, %s);", localClientOptionsName, localHttpLogOptionsName)); + function.line("policies.add(new UserAgentPolicy(applicationId, clientName, " + + "clientVersion, buildConfiguration));"); + + if (pipelinePolicyDetails != null && !CoreUtils.isNullOrEmpty(pipelinePolicyDetails.getRequestIdHeaderName())) { + function.line(String.format("policies.add(new RequestIdPolicy(\"%s\"));", pipelinePolicyDetails.getRequestIdHeaderName())); + } else { + function.line("policies.add(new RequestIdPolicy());"); + } + function.line("policies.add(new AddHeadersFromContextPolicy());"); + + // clientOptions header + function.line("HttpHeaders headers = CoreUtils.createHttpHeadersFromClientOptions(" + localClientOptionsName + ");"); + function.ifBlock("headers != null", block -> block.line("policies.add(new AddHeadersPolicy(headers));")); + + function.line("this.pipelinePolicies.stream()" + + ".filter(p -> p.getPipelinePosition() == HttpPipelinePosition.PER_CALL)" + + ".forEach(p -> policies.add(p));"); + function.line("HttpPolicyProviders.addBeforeRetryPolicies(policies);"); + function.line("policies.add(ClientBuilderUtil.validateAndGetRetryPolicy(retryPolicy, retryOptions, new " + + "RetryPolicy()));"); + function.line("policies.add(new AddDatePolicy());"); + + if (securityInfo.getSecurityTypes().contains(Scheme.SecuritySchemeType.KEY)) { + if (CoreUtils.isNullOrEmpty(securityInfo.getHeaderName())) { + LOGGER.error("key-credential-header-name is required for " + + "key-based credential type"); + throw new IllegalStateException("key-credential-header-name is required for " + + "key-based credential type"); + } + + if (settings.isUseKeyCredential()) { + function.ifBlock("keyCredential != null", action -> { + if (CoreUtils.isNullOrEmpty(securityInfo.getHeaderValuePrefix())) { + function.line("policies.add(new KeyCredentialPolicy(\"" + + securityInfo.getHeaderName() + + "\", keyCredential));"); + } else { + function.line("policies.add(new KeyCredentialPolicy(\"" + + securityInfo.getHeaderName() + + "\", keyCredential, \"" + + securityInfo.getHeaderValuePrefix() + + "\"));"); + } + }); + } else { + function.ifBlock("azureKeyCredential != null", action -> { + if (CoreUtils.isNullOrEmpty(securityInfo.getHeaderValuePrefix())) { + function.line("policies.add(new AzureKeyCredentialPolicy(\"" + + securityInfo.getHeaderName() + + "\", azureKeyCredential));"); + } else { + function.line("policies.add(new AzureKeyCredentialPolicy(\"" + + securityInfo.getHeaderName() + + "\", azureKeyCredential, \"" + + securityInfo.getHeaderValuePrefix() + + "\"));"); + } + }); + } + } + if (securityInfo.getSecurityTypes().contains(Scheme.SecuritySchemeType.OAUTH2)) { + function.ifBlock("tokenCredential != null", action -> { + function.line("policies.add(new BearerTokenAuthenticationPolicy(tokenCredential, %s));", defaultCredentialScopes); + }); + } + function.line("this.pipelinePolicies.stream()" + + ".filter(p -> p.getPipelinePosition() == HttpPipelinePosition.PER_RETRY)" + + ".forEach(p -> policies.add(p));"); + function.line("HttpPolicyProviders.addAfterRetryPolicies(policies);"); + + function.line("policies.add(new HttpLoggingPolicy(%s));", localHttpLogOptionsName); + + function.line("HttpPipeline httpPipeline = new HttpPipelineBuilder()" + + ".policies(policies.toArray(new HttpPipelinePolicy[0]))" + + ".httpClient(httpClient)" + + String.format(".clientOptions(%s)", localClientOptionsName) + + ".build();"); + function.methodReturn("httpPipeline"); + } + + + public static void createRestProxyInstance(ServiceClientTemplate template, ServiceClient serviceClient, JavaBlock constructorBlock) { + if (!JavaSettings.getInstance().isBranded()) { + constructorBlock.line("this.service = %s.create(%s.class, this.httpPipeline);", ClassType.REST_PROXY.getName(), serviceClient.getProxy().getName()); + } else { + constructorBlock.line("this.service = %s.create(%s.class, this.httpPipeline, %s);", ClassType.REST_PROXY.getName(), serviceClient.getProxy().getName(), template.getSerializerPhrase()); + } + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/Templates.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/Templates.java new file mode 100644 index 0000000000..9879c6eee4 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/Templates.java @@ -0,0 +1,124 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.template; + +/** + * A collection of templates for writing JV models to Java files and contexts. + */ +public class Templates { + + private static TemplateFactory factory = new DefaultTemplateFactory(); + + public static void setFactory(TemplateFactory templateFactory) { + factory = templateFactory; + } + + public static ServiceClientInterfaceTemplate getServiceClientInterfaceTemplate() { + return factory.getServiceClientInterfaceTemplate(); + } + + public static ServiceClientTemplate getServiceClientTemplate() { + return factory.getServiceClientTemplate(); + } + + public static ServiceClientBuilderTemplate getServiceClientBuilderTemplate() { + return factory.getServiceClientBuilderTemplate(); + } + + public static ServiceVersionTemplate getServiceVersionTemplate() { + return factory.getServiceVersionTemplate(); + } + + public static MethodGroupInterfaceTemplate getMethodGroupInterfaceTemplate() { + return factory.getMethodGroupInterfaceTemplate(); + } + + public static MethodGroupTemplate getMethodGroupTemplate() { + return factory.getMethodGroupTemplate(); + } + + public static ProxyTemplate getProxyTemplate() { + return factory.getProxyTemplate(); + } + + public static ClientMethodTemplate getClientMethodTemplate() { + return factory.getClientMethodTemplate(); + } + + public static ModelTemplate getModelTemplate() { + return factory.getModelTemplate(); + } + + public static StreamSerializationModelTemplate getStreamStyleModelTemplate() { + return factory.getStreamStyleModelTemplate(); + } + + public static ExceptionTemplate getExceptionTemplate() { + return factory.getExceptionTemplate(); + } + + public static EnumTemplate getEnumTemplate() { + return factory.getEnumTemplate(); + } + + public static ResponseTemplate getResponseTemplate() { + return factory.getResponseTemplate(); + } + + public static XmlSequenceWrapperTemplate getXmlSequenceWrapperTemplate() { + return factory.getXmlSequenceWrapperTemplate(); + } + + public static PackageInfoTemplate getPackageInfoTemplate() { + return factory.getPackageInfoTemplate(); + } + + public static ServiceAsyncClientTemplate getServiceAsyncClientTemplate() { + return factory.getServiceAsyncClientTemplate(); + } + + public static WrapperClientMethodTemplate getWrapperClientMethodTemplate() { + return factory.getWrapperClientMethodTemplate(); + } + + public static ServiceSyncClientTemplate getServiceSyncClientTemplate() { + return factory.getServiceSynClientTemplate(); + } + + public static ServiceSyncClientTemplate getServiceSyncClientWrapAsyncClientTemplate() { + return factory.getServiceSyncClientWrapAsyncClientTemplate(); + } + + public static PomTemplate getPomTemplate() { + return factory.getPomTemplate(); + } + + public static ModuleInfoTemplate getModuleInfoTemplate() { + return factory.getModuleInfoTemplate(); + } + + public static ProtocolSampleTemplate getProtocolSampleTemplate() { + return factory.getProtocolSampleTemplate(); + } + + public static ConvenienceAsyncMethodTemplate getConvenienceAsyncMethodTemplate() { + return factory.getConvenienceAsyncMethodTemplate(); + } + + public static ConvenienceSyncMethodTemplate getConvenienceSyncMethodTemplate() { + return factory.getConvenienceSyncMethodTemplate(); + } + + public static UnionModelTemplate getUnionModelTemplate() { + return factory.getUnionModelTemplate(); + } + + public static ClientMethodSampleTemplate getClientMethodSampleTemplate() { + return factory.getClientMethodSampleTemplate(); + } + + public static JsonMergePatchHelperTemplate getJsonMergePatchHelperTemplate() { + return factory.getJsonMergePatchHelperTemplate(); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/TestProxyAssetsTemplate.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/TestProxyAssetsTemplate.java new file mode 100644 index 0000000000..ce1d8f8cf4 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/TestProxyAssetsTemplate.java @@ -0,0 +1,74 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.template; + +import com.microsoft.typespec.http.client.generator.core.extension.base.util.JsonUtils; +import com.microsoft.typespec.http.client.generator.core.model.projectmodel.Project; +import com.microsoft.typespec.http.client.generator.core.util.TemplateUtil; +import com.azure.json.JsonReader; +import com.azure.json.JsonSerializable; +import com.azure.json.JsonWriter; + +import java.io.IOException; + +public class TestProxyAssetsTemplate { + private static class Assets implements JsonSerializable { + private String assetsRepo = "Azure/azure-sdk-assets"; + private String assetsRepoPrefixPath = "java"; + private String tagPrefix; + private String tag = ""; + + public void setTagPrefix(String tagPrefix) { + this.tagPrefix = tagPrefix; + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + return jsonWriter.writeStartObject() + .writeStringField("AssetsRepo", assetsRepo) + .writeStringField("AssetsRepoPrefixPath", assetsRepoPrefixPath) + .writeStringField("TagPrefix", tagPrefix) + .writeStringField("Tag", tag) + .writeEndObject(); + } + + /** + * Deserialize the JSON data into an Assets instance. + * + * @param jsonReader JSON reader + * @return Assets instance + * @throws IOException thrown if the JSON data cannot be deserialized + */ + public static Assets fromJson(JsonReader jsonReader) throws IOException { + return JsonUtils.readObject(jsonReader, Assets::new, (assets, fieldName, reader) -> { + if ("AssetsRepo".equals(fieldName)) { + assets.assetsRepo = reader.getString(); + } else if ("AssetsRepoPrefixPath".equals(fieldName)) { + assets.assetsRepoPrefixPath = reader.getString(); + } else if ("TagPrefix".equals(fieldName)) { + assets.tagPrefix = reader.getString(); + } else if ("Tag".equals(fieldName)) { + assets.tag = reader.getString(); + } else { + reader.skipChildren(); + } + }); + } + } + + public String write(Project project) { + Assets asserts = new Assets(); + String group; + if (project.getSdkRepositoryUri().isPresent()) { + String[] segments = project.getSdkRepositoryUri().get().split("/"); + group = segments[segments.length - 2]; + } else { + // fallback to last segment of artifactId, this could be incorrect + String[] segments = project.getArtifactId().split("-"); + group = segments[segments.length - 1]; + } + asserts.setTagPrefix(String.format("java/%1$s/%2$s", group, project.getArtifactId())); + return TemplateUtil.prettyPrintToJson(asserts); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/UnionModelTemplate.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/UnionModelTemplate.java new file mode 100644 index 0000000000..44a7f1fe9f --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/UnionModelTemplate.java @@ -0,0 +1,109 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.template; + +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientModelProperty; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.IType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.UnionModel; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaFile; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaJavadocComment; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaModifier; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaVisibility; +import com.azure.core.annotation.Immutable; +import com.azure.core.util.CoreUtils; + +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.function.Consumer; + +public class UnionModelTemplate implements IJavaTemplate { + + private static final UnionModelTemplate INSTANCE = new UnionModelTemplate(); + + protected UnionModelTemplate() { + } + + public static UnionModelTemplate getInstance() { + return INSTANCE; + } + + @Override + public void write(UnionModel model, JavaFile javaFile) { + // presently, subclass would only contain one "value" property. + + final boolean isAbstractClass = CoreUtils.isNullOrEmpty(model.getParentModelName()); + final String superClassName = model.getParentModelName(); + + Set imports = new HashSet<>(); + model.addImportsTo(imports); + + imports.add(Immutable.class.getName()); + imports.add("com.fasterxml.jackson.annotation.JsonValue"); + + javaFile.declareImport(imports); + + List modifiers = Collections.singletonList(isAbstractClass ? JavaModifier.Abstract : JavaModifier.Final); + String classDeclaration = isAbstractClass ? model.getName() : (model.getName() + " extends " + superClassName); + javaFile.javadocComment(comment -> comment.description(model.getDescription())); + if (!isAbstractClass) { + javaFile.annotation("Immutable"); + } + javaFile.publicClass(modifiers, classDeclaration, classBlock -> { + // properties as member variables + for (ClientModelProperty property : model.getProperties()) { + classBlock.privateFinalMemberVariable(property.getClientType() + " " + property.getName()); + } + + // constructor + if (isAbstractClass) { + classBlock.javadocComment(comment -> + comment.description("Creates an instance of " + model.getName() + " class.")); + classBlock.constructor(JavaVisibility.Protected, model.getName() + "()", constructor -> { + }); + } else { + StringBuilder constructorProperties = new StringBuilder(); + + Consumer javadocCommentConsumer = comment -> + comment.description("Creates an instance of " + model.getName() + " class."); + + for (ClientModelProperty property : model.getProperties()) { + javadocCommentConsumer = javadocCommentConsumer.andThen(comment -> { + comment.param(property.getName(), "the value"); + }); + + if (constructorProperties.length() > 0) { + constructorProperties.append(", "); + } + constructorProperties.append(property.getClientType()).append(" ").append(property.getName()); + } + + classBlock.javadocComment(javadocCommentConsumer); + classBlock.publicConstructor(String.format("%1$s(%2$s)", model.getName(), constructorProperties), constructor -> { + for (ClientModelProperty property : model.getProperties()) { + constructor.line("this." + property.getName() + " = " + + property.getWireType().convertFromClientType(property.getName()) + ";"); + } + }); + } + + // getter/setters + for (ClientModelProperty property : model.getProperties()) { + String propertyName = property.getName(); + IType clientType = property.getClientType(); + + // getter + classBlock.javadocComment(comment -> { + comment.description("Gets the value"); + comment.methodReturns("the value"); + }); + classBlock.annotation("JsonValue"); + classBlock.publicMethod(clientType + " " + property.getGetterName() + "()", methodBlock -> { + methodBlock.methodReturn("this." + propertyName); + }); + } + }); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/WrapperClientMethodTemplate.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/WrapperClientMethodTemplate.java new file mode 100644 index 0000000000..cb7d12d689 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/WrapperClientMethodTemplate.java @@ -0,0 +1,133 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.template; + +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.Annotation; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClassType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethod; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethodParameter; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethodType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.PrimitiveType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ProxyMethod; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaBlock; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaClass; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaType; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaVisibility; +import com.microsoft.typespec.http.client.generator.core.util.TemplateUtil; +import com.azure.core.util.CoreUtils; + +import java.util.List; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +/** + * Template to generate client methods that are wrappers around the client methods generated by + * {@link ClientMethodTemplate}. + * + */ +public class WrapperClientMethodTemplate extends ClientMethodTemplateBase { + + private static final WrapperClientMethodTemplate INSTANCE = new WrapperClientMethodTemplate(); + + protected WrapperClientMethodTemplate() { + } + + public static WrapperClientMethodTemplate getInstance() { + return INSTANCE; + } + + @Override + public void write(ClientMethod clientMethod, JavaType typeBlock) { + JavaSettings settings = JavaSettings.getInstance(); + + if (clientMethod.getType() == ClientMethodType.PagingAsyncSinglePage || clientMethod.getType() == ClientMethodType.PagingSyncSinglePage) { + return; + } + + ProxyMethod restAPIMethod = clientMethod.getProxyMethod(); + if (settings.isDataPlaneClient()) { + typeBlock.javadocComment(comment -> generateProtocolMethodJavadoc(clientMethod, comment)); + } else { + generateJavadoc(clientMethod, typeBlock, restAPIMethod); + } + + addGeneratedAnnotation(typeBlock); + TemplateUtil.writeClientMethodServiceMethodAnnotation(clientMethod, typeBlock); + + String methodName = clientMethod.getName(); + if (clientMethod.getType().name().contains("Async") && methodName.endsWith("Async")) { + methodName = methodName.substring(0, methodName.length() - "Async".length()); + } + + String declaration = String.format("%1$s %2$s(%3$s)", clientMethod.getReturnValue().getType(), methodName, clientMethod.getParametersDeclaration()); + Consumer method = function -> { + + // API comment + if (clientMethod.getImplementationDetails() != null && !CoreUtils.isNullOrEmpty(clientMethod.getImplementationDetails().getComment())) { + function.line("// " + clientMethod.getImplementationDetails().getComment()); + } + + boolean shouldReturn = true; + if (clientMethod.getReturnValue() != null && clientMethod.getReturnValue().getType() instanceof PrimitiveType) { + PrimitiveType type = (PrimitiveType) clientMethod.getReturnValue().getType(); + if (type.asNullable() == ClassType.VOID) { + shouldReturn = false; + } + } + + writeMethodInvocation(clientMethod, function, shouldReturn); + }; + if (clientMethod.getMethodVisibilityInWrapperClient() == JavaVisibility.Public) { + typeBlock.publicMethod(declaration, method); + } else if (typeBlock instanceof JavaClass) { + JavaClass classBlock = (JavaClass) typeBlock; + classBlock.method(clientMethod.getMethodVisibilityInWrapperClient(), null, declaration, method); + } + + } + + /** + * Extension to write the client method invocation. + * + * @param clientMethod the client method + * @param function the method block to write the method invocation + * @param shouldReturn whether method need return value + */ + protected void writeMethodInvocation(ClientMethod clientMethod, JavaBlock function, boolean shouldReturn) { + List parameters = clientMethod.getMethodInputParameters(); + function.line((shouldReturn ? "return " : "") + "this.serviceClient.%1$s(%2$s);", + clientMethod.getName(), + parameters.stream().map(ClientMethodParameter::getName).collect(Collectors.joining(", "))); + } + + protected void generateJavadoc(ClientMethod clientMethod, JavaType typeBlock, ProxyMethod restAPIMethod) { + typeBlock.javadocComment(comment -> { + comment.description(clientMethod.getDescription()); + List methodParameters = clientMethod.getMethodInputParameters(); + for (ClientMethodParameter parameter : methodParameters) { + comment.param(parameter.getName(), parameter.getDescription()); + } + if (clientMethod.getParametersDeclaration() != null && !clientMethod.getParametersDeclaration().isEmpty()) { + comment.methodThrows("IllegalArgumentException", "thrown if parameters fail the validation"); + } + if (restAPIMethod != null) { + if (restAPIMethod.getUnexpectedResponseExceptionType() != null) { + comment.methodThrows(restAPIMethod.getUnexpectedResponseExceptionType().toString(), + "thrown if the request is rejected by server"); + } + comment.methodThrows("RuntimeException", "all other wrapped checked exceptions if the request fails to be sent"); + } + comment.methodReturns(clientMethod.getReturnValue().getDescription()); + }); + } + + protected void addGeneratedAnnotation(JavaType typeBlock) { + if (JavaSettings.getInstance().isBranded()) { + typeBlock.annotation(Annotation.GENERATED.getName()); + } else { + typeBlock.annotation(Annotation.METADATA.getName() + "(generated = true)"); + } + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/XmlSequenceWrapperTemplate.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/XmlSequenceWrapperTemplate.java new file mode 100644 index 0000000000..fb09138aea --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/XmlSequenceWrapperTemplate.java @@ -0,0 +1,118 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.template; + +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClassType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.IType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.XmlSequenceWrapper; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaClass; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaFile; +import com.microsoft.typespec.http.client.generator.core.util.CodeNamer; +import com.azure.xml.XmlReader; +import com.azure.xml.XmlSerializable; +import com.azure.xml.XmlToken; +import com.azure.xml.XmlWriter; + +import javax.xml.namespace.QName; +import javax.xml.stream.XMLStreamException; +import java.util.ArrayList; + +/** + * Writes an XmlSequenceWrapper to a JavaFile. + */ +public class XmlSequenceWrapperTemplate implements IJavaTemplate { + private static final XmlSequenceWrapperTemplate INSTANCE = new XmlSequenceWrapperTemplate(); + + private XmlSequenceWrapperTemplate() { + } + + public static XmlSequenceWrapperTemplate getInstance() { + return INSTANCE; + } + + public final void write(XmlSequenceWrapper xmlSequenceWrapper, JavaFile javaFile) { + JavaSettings settings = JavaSettings.getInstance(); + + String xmlRootElementName = xmlSequenceWrapper.getXmlRootElementName(); + String xmlListElementName = xmlSequenceWrapper.getXmlListElementName(); + + String xmlElementNameCamelCase = CodeNamer.toCamelCase(xmlRootElementName); + + IType sequenceType = xmlSequenceWrapper.getSequenceType(); + + javaFile.declareImport(xmlSequenceWrapper.getImports()); + + if (settings.isStreamStyleSerialization()) { + javaFile.declareImport(ArrayList.class.getName(), ClassType.CORE_UTILS.getFullName(), QName.class.getName(), + XmlReader.class.getName(), XmlSerializable.class.getName(), XMLStreamException.class.getName(), + XmlToken.class.getName(), XmlWriter.class.getName()); + } + + javaFile.javadocComment(comment -> comment.description( + "A wrapper around " + sequenceType + " which provides top-level metadata for serialization.")); + + String className = xmlSequenceWrapper.getWrapperClassName(); + if (!settings.isStreamStyleSerialization()) { + javaFile.annotation("JacksonXmlRootElement(localName = \"" + xmlRootElementName + "\")"); + } else { + className = className + " implements XmlSerializable<" + className + ">"; + } + javaFile.publicFinalClass(className, classBlock -> { + if (settings.isStreamStyleSerialization()) { + writeStreamStyleXmlWrapper(classBlock, xmlSequenceWrapper, xmlRootElementName, xmlListElementName, + xmlElementNameCamelCase, sequenceType); + } else { + writeJacksonXmlWrapper(classBlock, xmlSequenceWrapper, xmlListElementName, xmlElementNameCamelCase, + sequenceType); + } + }); + } + + private static void writeJacksonXmlWrapper(JavaClass classBlock, XmlSequenceWrapper xmlSequenceWrapper, + String xmlListElementName, String xmlElementNameCamelCase, IType sequenceType) { + classBlock.annotation("JacksonXmlProperty(localName = \"" + xmlListElementName + "\")"); + classBlock.privateFinalMemberVariable(sequenceType.toString(), xmlElementNameCamelCase); + + classBlock.javadocComment(comment -> { + comment.description("Creates an instance of " + xmlSequenceWrapper.getWrapperClassName() + "."); + comment.param(xmlElementNameCamelCase, "the list"); + }); + classBlock.annotation("JsonCreator"); + classBlock.publicConstructor(String.format("%1$s(@JsonProperty(\"%2$s\") %3$s %4$s)", + xmlSequenceWrapper.getWrapperClassName(), xmlListElementName, sequenceType, xmlElementNameCamelCase), + constructor -> constructor.line("this." + xmlElementNameCamelCase + " = " + xmlElementNameCamelCase + ";")); + + addGetter(classBlock, sequenceType, xmlElementNameCamelCase); + } + + private static void addGetter(JavaClass classBlock, IType sequenceType, String xmlElementNameCamelCase) { + classBlock.javadocComment(comment -> { + comment.description("Get the " + sequenceType + " contained in this wrapper."); + comment.methodReturns("the " + sequenceType); + }); + classBlock.publicMethod(sequenceType + " items()", function -> function.methodReturn(xmlElementNameCamelCase)); + } + + private static void writeStreamStyleXmlWrapper(JavaClass classBlock, XmlSequenceWrapper xmlSequenceWrapper, + String xmlRootElementName, String xmlListElementName, String xmlElementNameCamelCase, IType sequenceType) { + classBlock.privateFinalMemberVariable(sequenceType.toString(), xmlElementNameCamelCase); + + classBlock.javadocComment(comment -> { + comment.description("Creates an instance of " + xmlSequenceWrapper.getWrapperClassName() + "."); + comment.param(xmlElementNameCamelCase, "the list"); + }); + classBlock.publicConstructor(String.format("%s(%s %s)", xmlSequenceWrapper.getWrapperClassName(), sequenceType, + xmlElementNameCamelCase), + constructor -> constructor.line("this." + xmlElementNameCamelCase + " = " + xmlElementNameCamelCase + ";")); + + addGetter(classBlock, sequenceType, xmlElementNameCamelCase); + + StreamSerializationModelTemplate.xmlWrapperClassXmlSerializableImplementation(classBlock, + xmlSequenceWrapper.getWrapperClassName(), sequenceType, xmlRootElementName, + xmlSequenceWrapper.getXmlRootElementNamespace(), xmlListElementName, + xmlElementNameCamelCase, xmlSequenceWrapper.getXmlListElementNamespace(), + Templates.getModelTemplate()::addGeneratedAnnotation); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/example/ClientInitializationExampleWriter.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/example/ClientInitializationExampleWriter.java new file mode 100644 index 0000000000..9f6cb94734 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/example/ClientInitializationExampleWriter.java @@ -0,0 +1,132 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.template.example; + +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Scheme; +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.AsyncSyncClient; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClassType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethod; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ProxyMethodExample; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ProxyMethodParameter; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ServiceClient; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ServiceClientProperty; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaBlock; +import com.microsoft.typespec.http.client.generator.core.util.CodeNamer; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.Consumer; + +/** Client initialization example writer for DPG methods. */ +public class ClientInitializationExampleWriter { + private final Set imports = new HashSet<>(); + private final Consumer clientInitializationWriter; + private final String clientVarName; + + public ClientInitializationExampleWriter( + AsyncSyncClient syncClient, + ClientMethod method, + ProxyMethodExample proxyMethodExample, + ServiceClient serviceClient){ + syncClient.addImportsTo(imports, false); + syncClient.getClientBuilder().addImportsTo(imports, false); + clientVarName = CodeNamer.toCamelCase(syncClient.getClassName()); + final String builderName = syncClient.getClientBuilder().getClassName(); + + // credential + imports.add("com.azure.identity.DefaultAzureCredentialBuilder"); + ClassType.AZURE_KEY_CREDENTIAL.addImportsTo(imports, false); + ClassType.KEY_CREDENTIAL.addImportsTo(imports, false); + ClassType.CONFIGURATION.addImportsTo(imports, false); + + // client initialization + List clientParameterLines = new ArrayList<>(); + Set processedServiceClientProperties = new HashSet<>(); + + // proxy method parameters which value comes from client + method.getProxyMethod().getAllParameters() + .stream() + .filter(ProxyMethodParameter::isFromClient) + .forEach(p -> { + for (Map.Entry entry : proxyMethodExample.getParameters().entrySet()) { + String parameterName = entry.getKey(); + ProxyMethodExample.ParameterValue parameterValue = entry.getValue(); + if (parameterName.equalsIgnoreCase(p.getName())) { + String clientValue = p.getClientType() + .defaultValueExpression(parameterValue.getObjectValue().toString()); + serviceClient.getProperties().stream().filter(p1 -> Objects.equals(p.getName(), p1.getName())).findFirst().ifPresent(serviceClientProperty -> { + processedServiceClientProperties.add(serviceClientProperty); + + clientParameterLines.add( + String.format(".%1$s(%2$s)", serviceClientProperty.getAccessorMethodSuffix(), clientValue)); + }); + } + } + }); + + // required service client properties + serviceClient.getProperties().stream().filter(ServiceClientProperty::isRequired).filter(p -> !processedServiceClientProperties.contains(p)).forEach(serviceClientProperty -> { + String defaultValueExpression = serviceClientProperty.getDefaultValueExpression(); + if (defaultValueExpression == null) { + defaultValueExpression = String.format("Configuration.getGlobalConfiguration().get(\"%1$s\")", + serviceClientProperty.getName().toUpperCase(Locale.ROOT)); + } + + clientParameterLines.add( + String.format(".%1$s(%2$s)", serviceClientProperty.getAccessorMethodSuffix(), defaultValueExpression)); + }); + String clientParameterExpr = String.join("", clientParameterLines); + + // credentials + String credentialExpr; + if (serviceClient.getSecurityInfo() != null && serviceClient.getSecurityInfo().getSecurityTypes() != null) { + if (serviceClient.getSecurityInfo().getSecurityTypes().contains(Scheme.SecuritySchemeType.OAUTH2)) { + credentialExpr = ".credential(new DefaultAzureCredentialBuilder().build())"; + } else if (serviceClient.getSecurityInfo().getSecurityTypes().contains(Scheme.SecuritySchemeType.KEY)) { + if (JavaSettings.getInstance().isUseKeyCredential()) { + credentialExpr = ".credential(new KeyCredential(Configuration.getGlobalConfiguration().get(\"API_KEY\")))"; + } else { + credentialExpr = ".credential(new AzureKeyCredential(Configuration.getGlobalConfiguration().get(\"API_KEY\")))"; + } + } else { + credentialExpr = ""; + } + } else { + credentialExpr = ""; + } + + this.clientInitializationWriter = methodBlock -> { + // client + String clientInit = "%1$s %2$s = new %3$s()" + + "%4$s" + // credentials + "%5$s" + // client properties + ".%6$s();"; + methodBlock.line( + String.format(clientInit, + syncClient.getClassName(), clientVarName, + builderName, + credentialExpr, + clientParameterExpr, + syncClient.getClientBuilder().getBuilderMethodNameForSyncClient(syncClient))); + }; + } + + public Set getImports() { + return new HashSet<>(this.imports); + } + + public void write(JavaBlock methodBlock) { + this.clientInitializationWriter.accept(methodBlock); + } + + public String getClientVarName() { + return clientVarName; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/example/ClientMethodExampleWriter.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/example/ClientMethodExampleWriter.java new file mode 100644 index 0000000000..40724019b4 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/example/ClientMethodExampleWriter.java @@ -0,0 +1,406 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.template.example; + +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.RequestParameterLocation; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClassType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethod; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethodParameter; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethodType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientModel; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientModelProperty; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.EnumType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.GenericType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.IType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ListType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.MethodTransformationDetail; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ParameterMapping; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.PrimitiveType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ProxyMethodExample; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.examplemodel.ExampleHelperFeature; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.examplemodel.ExampleNode; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.examplemodel.MethodParameter; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaBlock; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaFileContents; +import com.microsoft.typespec.http.client.generator.core.util.ClientModelUtil; +import com.microsoft.typespec.http.client.generator.core.util.CodeNamer; +import com.microsoft.typespec.http.client.generator.core.util.MethodUtil; +import com.microsoft.typespec.http.client.generator.core.util.ModelExampleUtil; +import com.azure.core.http.ContentType; +import com.azure.core.http.HttpMethod; +import com.azure.core.http.rest.PagedIterable; +import com.azure.core.util.CoreUtils; +import com.azure.core.util.polling.LongRunningOperationStatus; +import com.azure.core.util.polling.SyncPoller; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.stream.Collectors; + +public class ClientMethodExampleWriter { + + private final Set imports = new HashSet<>(); + private final BiConsumer methodBodyWriter; + private final Consumer responseAssertionWriter; + private final ModelExampleWriter.ExampleNodeModelInitializationVisitor nodeVisitor = new ModelExampleWriter.ExampleNodeModelInitializationVisitor(); + + public ClientMethodExampleWriter(ClientMethod method, String clientVarName, ProxyMethodExample proxyMethodExample){ + + List methodParameters = MethodUtil.getParameters(method, true); + List exampleNodes = methodParameters + .stream() + .map(methodParameter -> parseNodeFromParameter(method, proxyMethodExample, methodParameter)) + .collect(Collectors.toList()); + + String parameterInvocations = exampleNodes.stream() + .map(nodeVisitor::accept) + .collect(Collectors.joining(", ")); + + // assertion + this.imports.add("org.junit.jupiter.api.Assertions"); + imports.add(LongRunningOperationStatus.class.getName()); + ClassType.HTTP_HEADER_NAME.addImportsTo(imports, false); + + method.getReturnValue().getType().addImportsTo(imports, false); + + methodBodyWriter = (methodBlock, isTestCode) -> { + StringBuilder methodInvocation = new StringBuilder(); + + if (method.getReturnValue().getType().asNullable() != ClassType.VOID) { + String assignment = String.format("%s %s = ", method.getReturnValue().getType(), "response"); + methodInvocation.append(assignment); + } + + String methodCall = String.format("%s.%s(%s)", + clientVarName, + method.getName(), + parameterInvocations); + if (isTestCode) { + if (method.getType() == ClientMethodType.LongRunningBeginSync) { + methodCall = "setPlaybackSyncPollerPollInterval(" + methodCall + ")"; + } else if (method.getType() == ClientMethodType.LongRunningBeginAsync) { + methodCall = "setPlaybackPollerFluxPollInterval(" + methodCall + ")"; + } + } + methodInvocation.append(methodCall).append(";"); + + methodBlock.line(methodInvocation.toString()); + }; + responseAssertionWriter = methodBlock -> { + ProxyMethodExample.Response response = proxyMethodExample.getPrimaryResponse(); + if (response != null) { + IType returnType = method.getReturnValue().getType(); + if (returnType instanceof GenericType) { + GenericType responseType = (GenericType) returnType; + if (SyncPoller.class.getSimpleName().equals(responseType.getName())) { + // SyncPoller<> + + if (response.getStatusCode() / 100 == 2) { + methodBlock.line(); + methodBlock.line("// response assertion"); + // it should have a 202 leading to SUCCESSFULLY_COMPLETED + // but x-ms-examples usually does not include the final result + methodBlock.line("Assertions.assertEquals(LongRunningOperationStatus.SUCCESSFULLY_COMPLETED, response.waitForCompletion().getStatus());"); + } + } else if (PagedIterable.class.getSimpleName().equals(responseType.getName())) { + // PagedIterable<> + + methodBlock.line(); + methodBlock.line("// response assertion"); + // assert status code + methodBlock.line(String.format("Assertions.assertEquals(%1$s, response.iterableByPage().iterator().next().getStatusCode());", response.getStatusCode())); + // assert headers + response.getHttpHeaders().stream().forEach(header -> { + String expectedValueStr = ClassType.STRING.defaultValueExpression(header.getValue()); + String keyStr = ClassType.STRING.defaultValueExpression(header.getName()); + methodBlock.line(String.format("Assertions.assertEquals(%1$s, response.iterableByPage().iterator().next().getHeaders().get(HttpHeaderName.fromString(%2$s)).getValue());", expectedValueStr, keyStr)); + }); + // assert JSON of first item, or assert count=0 + if (method.getProxyMethod().getResponseContentTypes() != null + && method.getProxyMethod().getResponseContentTypes().contains(ContentType.APPLICATION_JSON) + && responseType.getTypeArguments().length > 0 + && ClientModelUtil.isClientModel(responseType.getTypeArguments()[0]) + && method.getMethodPageDetails() != null + && response.getBody() instanceof Map) { + Map bodyMap = (Map) response.getBody(); + if (bodyMap.containsKey(method.getMethodPageDetails().getSerializedItemName())) { + Object items = bodyMap.get(method.getMethodPageDetails().getSerializedItemName()); + if (items instanceof List) { + List itemArray = (List) items; + if (itemArray.isEmpty()) { + methodBlock.line("Assertions.assertEquals(0, response.stream().count());"); + } else { + Object firstItem = itemArray.iterator().next(); + methodBlock.line("%s firstItem = %s;", responseType.getTypeArguments()[0], "response.iterator().next()"); + writeModelAssertion(methodBlock, nodeVisitor, responseType.getTypeArguments()[0], responseType.getTypeArguments()[0], firstItem, "firstItem", true); + } + } + } + } + } + } else if (ClassType.BOOLEAN.equals(returnType.asNullable()) && HttpMethod.HEAD.equals(method.getProxyMethod().getHttpMethod())) { + methodBlock.line(); + methodBlock.line("// response assertion"); + if (response.getStatusCode() == 200) { + methodBlock.line("Assertions.assertTrue(response);"); + } else if (response.getStatusCode() == 404) { + methodBlock.line("Assertions.assertFalse(response)"); + } + } else if (!ClassType.VOID.equals(returnType.asNullable())){ + methodBlock.line(); + methodBlock.line("// response assertion"); + writeModelAssertion(methodBlock, nodeVisitor, returnType, returnType, response.getBody(), "response", true); + } + } else { + methodBlock.line(); + methodBlock.line("// response assertion"); + methodBlock.line("Assertions.assertNotNull(response);"); + } + }; + + addNecessaryImports(); + } + + private void addNecessaryImports() { + // write dummy, allow nodeVisitor to collect all necessary imports + responseAssertionWriter.accept(new JavaBlock(new JavaFileContents())); + this.imports.addAll(nodeVisitor.getImports()); + } + + /** + * Write assertions for the given model and its example value. + * + * @param methodBlock the method block to write assertions to + * @param nodeVisitor node visitor for example values + * @param modelClientType client type of the model + * @param modelWireType wire type of the model + * @param modelValue example value of the model + * @param modelReference reference of the model that can be used to access the model in generated code + * @param rootModel whether the model is in the root of the response + */ + private void writeModelAssertion(JavaBlock methodBlock, ModelExampleWriter.ExampleNodeModelInitializationVisitor nodeVisitor, + IType modelClientType, IType modelWireType, Object modelValue, String modelReference, boolean rootModel) { + if (modelValue != null) { + modelClientType.addImportsTo(this.imports, false); + if (modelWireType != null) { + modelWireType.addImportsTo(this.imports, false); + } + if (isClientModel(modelClientType, modelValue)) { + methodBlock.line("Assertions.assertNotNull(%s);", modelReference); + // Client Model + ClassType modelClassType = (ClassType) modelClientType; + ClientModel clientModel = ClientModelUtil.getClientModel(modelClassType.getName()); + if (clientModel.getProperties() != null) { + for (ClientModelProperty property : clientModel.getProperties()) { + String serializedName = property.getSerializedName(); + Object propertyValue = ((Map) modelValue).get(serializedName); + if (propertyValue != null) { + if (rootModel) { + methodBlock.line("// verify property \"%s\"", property.getName()); + } + String propertyGetter = String.format("%s.%s()", modelReference, property.getGetterName()); + if (isClientModel(property.getClientType(), propertyValue) || isList(property.getClientType(), propertyValue)) { + String propertyReference = String.format("%s%s", modelReference, CodeNamer.toPascalCase(property.getName())); + methodBlock.line("%s %s = %s;", property.getClientType(), propertyReference, propertyGetter); + writeModelAssertion(methodBlock, nodeVisitor, property.getClientType(), property.getWireType(), propertyValue, propertyReference, false); + } else { + writeModelAssertion(methodBlock, nodeVisitor, property.getClientType(), property.getWireType(), propertyValue, propertyGetter, false); + } + } + } + } + } else if (isList(modelClientType, modelValue)) { + // List + List values = (List) modelValue; + if (values.size() > 0) { + ListType listType = (ListType) modelClientType; + IType elementType = listType.getElementType(); + Object firstItemValue = values.iterator().next(); + if (firstItemValue != null) { + String firstItemGetter = String.format("%s.iterator().next()", modelReference); + if (isClientModel(elementType, firstItemValue) || isList(elementType, firstItemValue)) { + String firstItemReference = String.format("%s%s", modelReference, "FirstItem"); + methodBlock.line("%s %s = %s;", elementType, firstItemReference, firstItemGetter); + writeModelAssertion(methodBlock, nodeVisitor, elementType, elementType, values.iterator().next(), firstItemReference, rootModel); + } else { + writeModelAssertion(methodBlock, nodeVisitor, elementType, elementType, values.iterator().next(), firstItemGetter, rootModel); + } + } + } else { + methodBlock.line("Assertions.assertEquals(0, %s);", String.format("%s.size()", modelReference)); + } + } else if (modelClientType instanceof PrimitiveType || modelClientType instanceof EnumType + || ClassType.STRING.equals(modelClientType) || ClassType.URL.equals(modelClientType) + || (modelClientType instanceof ClassType && ((ClassType) modelClientType).isBoxedType())) { + // simple models that can be compared by "Assertions.assertEquals()" + methodBlock.line(String.format( + "Assertions.assertEquals(%s, %s);", + nodeVisitor.accept(ModelExampleUtil.parseNode(modelClientType, modelWireType, modelValue)), + modelReference + )); + } else { + methodBlock.line("Assertions.assertNotNull(%s);", modelReference); + } + } + } + + private boolean isList(IType modelClientType, Object modelValue) { + return modelClientType instanceof ListType && modelValue instanceof List; + } + + private boolean isClientModel(IType modelClientType, Object modelValue) { + return modelClientType instanceof ClassType + && ClientModelUtil.isClientModel(modelClientType) + && modelValue instanceof Map; + } + + /** + * Parse example node from given parameter, taking into account parameter grouping. + * + * @param convenienceMethod the convenience method to generate example for + * @param proxyMethodExample the proxy method example + * @param methodParameter mapped convenience method parameter to protocol(proxy) method parameter + * @return example node + */ + private ExampleNode parseNodeFromParameter(ClientMethod convenienceMethod, ProxyMethodExample proxyMethodExample, MethodParameter methodParameter) { + if (isGroupingParameter(convenienceMethod, methodParameter)) { + // grouping, possible with flattening first + + // group example values into a map + Map exampleValue = new HashMap<>(); + for (MethodTransformationDetail detail : convenienceMethod.getMethodTransformationDetails()) { + for (ParameterMapping parameterMapping : detail.getParameterMappings()) { + if (parameterMapping.getOutputParameterPropertyName() != null) { + // this is a flattened property, so put flattening(real parameter) value + + // output parameter's name is the "escaped reserved client method parameter name" of the real parameter's serialized name + // since flattened parameter is always in body, we can deal with that explicitly + ClientMethodParameter outputParameter = detail.getOutParameter(); + Map flattenedParameterValue = getFlattenedBodyParameterExampleValue(proxyMethodExample, outputParameter); + if (flattenedParameterValue != null) { + exampleValue.putAll(flattenedParameterValue); + } + // since it's flattened property, all parameterMappings share the same outputParameter(real parameter) + // we only need to put example value once, which is the flattened(real) parameter's value + break; + } else { + // Group property's "serializedName" is the real parameter's "serializedName" on the wire. + // This implicit equivalence is defined in emitter and preserved in mapping client method. + String serializedParameterName = parameterMapping.getInputParameterProperty().getSerializedName(); + ClientMethodParameter parameter = detail.getOutParameter(); + exampleValue.put(serializedParameterName, + ModelExampleUtil.getParameterExampleValue( + proxyMethodExample, serializedParameterName, parameter.getRequestParameterLocation())); + } + } + } + IType type = methodParameter.getClientMethodParameter().getClientType(); + IType wireType = methodParameter.getClientMethodParameter().getWireType(); + return ModelExampleUtil.parseNode(type, wireType, exampleValue); + } else if (isFlattenParameter(convenienceMethod, methodParameter)) { + // flatten, no grouping + ClientMethodParameter outputParameter = convenienceMethod.getMethodTransformationDetails().iterator().next().getOutParameter(); + Map realParameterValue = getFlattenedBodyParameterExampleValue(proxyMethodExample, outputParameter); + + IType type = methodParameter.getClientMethodParameter().getClientType(); + IType wireType = methodParameter.getClientMethodParameter().getWireType(); + + ParameterMapping parameterMapping = convenienceMethod.getMethodTransformationDetails().iterator().next() + .getParameterMappings() + .stream() + .filter(mapping -> Objects.equals(mapping.getInputParameter().getName(), methodParameter.getClientMethodParameter().getName())) + .findFirst().orElse(null); + + Object methodParameterValue = null; + if (realParameterValue != null && parameterMapping != null) { + methodParameterValue = realParameterValue.get(parameterMapping.getOutputParameterProperty().getSerializedName()); + } + return ModelExampleUtil.parseNode(type, wireType, methodParameterValue); + } else { + return ModelExampleUtil.parseNodeFromParameter(proxyMethodExample, methodParameter); + } + } + + @SuppressWarnings("unchecked") + private Map getFlattenedBodyParameterExampleValue(ProxyMethodExample example, ClientMethodParameter clientMethodParameter) { + String clientMethodParameterName = clientMethodParameter.getName(); + Function getParameterValue = (parameterSerializedName) -> example.getParameters().entrySet() + .stream().filter( + p -> CodeNamer.getEscapedReservedClientMethodParameterName(p.getKey()) + .equalsIgnoreCase(parameterSerializedName)) + .map(Map.Entry::getValue) + .findFirst() + .orElse(null); + ProxyMethodExample.ParameterValue parameterValue = getParameterValue.apply(clientMethodParameterName); + + if (parameterValue == null && clientMethodParameter.getRequestParameterLocation() == RequestParameterLocation.BODY && !"body".equalsIgnoreCase(clientMethodParameterName)) { + // fallback, "body" is commonly used in example JSON for request body + clientMethodParameterName = "body"; + parameterValue = getParameterValue.apply(clientMethodParameterName); + } + + return parameterValue == null ? null : (Map) parameterValue.getObjectValue(); + } + + private boolean isGroupingParameter(ClientMethod convenienceMethod, MethodParameter methodParameter) { + List details = convenienceMethod.getMethodTransformationDetails(); + if (CoreUtils.isNullOrEmpty(details) || details.size() <= 1) { + return false; + } + + return details.stream().allMatch( + detail -> + !CoreUtils.isNullOrEmpty(detail.getParameterMappings()) + && detail.getOutParameter() != null + && + // same name + detail.getParameterMappings() + .stream() + .allMatch(mapping -> Objects.equals( + mapping.getInputParameter().getName(), + methodParameter.getClientMethodParameter().getName())) + ); + } + + private boolean isFlattenParameter(ClientMethod convenienceMethod, MethodParameter methodParameter) { + List details = convenienceMethod.getMethodTransformationDetails(); + if (CoreUtils.isNullOrEmpty(details) || details.size() != 1) { + return false; + } + return details.stream().anyMatch( + detail -> + !CoreUtils.isNullOrEmpty(detail.getParameterMappings()) + && detail.getOutParameter() != null + && detail.getParameterMappings().stream() + .allMatch(mapping -> mapping.getOutputParameterPropertyName() != null + && mapping.getInputParameterProperty() == null) + && detail.getParameterMappings() + .stream() + .anyMatch(mapping -> Objects.equals(methodParameter.getClientMethodParameter().getName(), mapping.getInputParameter().getName())) + ); + } + + public Set getImports() { + return new HashSet<>(this.imports); + } + + public void writeClientMethodInvocation(JavaBlock javaBlock, boolean isTestCode) { + methodBodyWriter.accept(javaBlock, isTestCode); + } + + public Set getHelperFeatures() { + return new HashSet<>(nodeVisitor.getHelperFeatures()); + } + + public void writeAssertion(JavaBlock methodBlock) { + responseAssertionWriter.accept(methodBlock); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/example/ModelExampleWriter.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/example/ModelExampleWriter.java new file mode 100644 index 0000000000..238f3f4c91 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/example/ModelExampleWriter.java @@ -0,0 +1,329 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.template.example; + +import com.microsoft.typespec.http.client.generator.core.Javagen; +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; +import com.microsoft.typespec.http.client.generator.core.extension.plugin.PluginLogger; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClassType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientModel; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientModelProperty; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.IType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ModelProperty; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.PrimitiveType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.examplemodel.BinaryDataNode; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.examplemodel.ClientModelNode; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.examplemodel.ExampleHelperFeature; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.examplemodel.ExampleNode; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.examplemodel.ListNode; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.examplemodel.LiteralNode; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.examplemodel.MapNode; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.examplemodel.ObjectNode; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaBlock; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaClass; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaModifier; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaVisibility; +import com.microsoft.typespec.http.client.generator.core.util.ClientModelUtil; +import com.microsoft.typespec.http.client.generator.core.util.TemplateUtil; +import com.azure.json.JsonProviders; +import com.azure.json.JsonWriter; +import org.slf4j.Logger; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class ModelExampleWriter { + + private static final Logger LOGGER = new PluginLogger(Javagen.getPluginInstance(), ModelExampleWriter.class); + + private final Set imports = new HashSet<>(); + + private final Consumer assertionWriter; + private final ExampleNodeModelInitializationVisitor modelInitializationVisitor = + new ExampleNodeModelInitializationVisitor(); + private final String modelInitializationCode; + + public ModelExampleWriter(ExampleNode exampleNode, String modelVariableName) { + this.imports.add("org.junit.jupiter.api.Assertions"); + + ExampleNodeAssertionVisitor assertionVisitor = new ExampleNodeAssertionVisitor(); + assertionVisitor.accept(exampleNode, modelVariableName); + imports.addAll(assertionVisitor.imports); + + this.assertionWriter = methodBlock -> { + assertionVisitor.assertions.forEach(methodBlock::line); + }; + + modelInitializationCode = modelInitializationVisitor.accept(exampleNode); + imports.addAll(modelInitializationVisitor.getImports()); + } + + public Set getImports() { + return imports; + } + + public Set getHelperFeatures() { + return modelInitializationVisitor.getHelperFeatures(); + } + + public void writeAssertion(JavaBlock methodBlock) { + assertionWriter.accept(methodBlock); + } + + public String getModelInitializationCode() { + return modelInitializationCode; + } + + public static void writeMapOfMethod(JavaClass classBlock) { + classBlock.lineComment("Use \"Map.of\" if available"); + classBlock.annotation("SuppressWarnings(\"unchecked\")"); + classBlock.method(JavaVisibility.Private, Collections.singletonList(JavaModifier.Static), " Map mapOf(Object... inputs)", methodBlock -> { + methodBlock.line("Map map = new HashMap<>();"); + methodBlock.line("for (int i = 0; i < inputs.length; i += 2) {"); + methodBlock.indent(() -> { + methodBlock.line("String key = (String) inputs[i];"); + methodBlock.line("T value = (T) inputs[i + 1];"); + methodBlock.line("map.put(key, value);"); + }); + methodBlock.line("}"); + methodBlock.line("return map;"); + }); + } + + public static class ExampleNodeAssertionVisitor { + + private final Set imports = new HashSet<>(); + + private final List assertions = new ArrayList<>(); + + private void addEqualsAssertion(String expected, String code) { + assertions.add(String.format("Assertions.assertEquals(%1$s, %2$s);", expected, code)); + } + + public void accept(ExampleNode node, String getterCode) { + if (node instanceof LiteralNode) { + node.getClientType().addImportsTo(imports, false); + + addEqualsAssertion( + node.getClientType().defaultValueExpression(((LiteralNode) node).getLiteralsValue()), + getterCode); + } else if (node instanceof ObjectNode) { + // additionalProperties + } else if (node instanceof ListNode) { + if (!node.getChildNodes().isEmpty()) { + node = node.getChildNodes().get(0); + getterCode += ".get(0)"; + accept(node, getterCode); + } + } else if (node instanceof MapNode) { + if (!node.getChildNodes().isEmpty()) { + String key = ((MapNode) node).getKeys().get(0); + node = node.getChildNodes().get(0); + getterCode += String.format(".get(%s)", ClassType.STRING.defaultValueExpression(key)); + accept(node, getterCode); + } + } else if (node instanceof ClientModelNode) { + ClientModelNode clientModelNode = ((ClientModelNode) node); + + ClientModel model = clientModelNode.getClientModel(); + + imports.add(model.getFullName()); + + for (ExampleNode childNode : node.getChildNodes()) { + ModelProperty modelProperty = clientModelNode.getClientModelProperties().get(childNode); + String childGetterCode = getterCode + String.format(".%s()", modelProperty.getGetterName()); + accept(childNode, childGetterCode); + } + } + } + + public Set getImports() { + return imports; + } + + public List getAssertions() { + return assertions; + } + } + + public static class ExampleNodeModelInitializationVisitor { + + protected final Set imports = new HashSet<>(); + protected final Set helperFeatures = new HashSet<>(); + + /** + * Extension to write code for deserialize JSON String to Object. + * @param jsonStr the JSON String. + */ + protected String codeDeserializeJsonString(String jsonStr) { + imports.add(com.azure.core.util.serializer.JacksonAdapter.class.getName()); + imports.add(com.azure.core.util.serializer.SerializerEncoding.class.getName()); + + return String.format("JacksonAdapter.createDefaultSerializerAdapter().deserialize(%s, Object.class, SerializerEncoding.JSON)", + ClassType.STRING.defaultValueExpression(jsonStr)); + } + + public Set getImports() { + if (helperFeatures.contains(ExampleHelperFeature.ThrowsIOException)) { + imports.add(java.io.IOException.class.getName()); + } + return imports; + } + + public Set getHelperFeatures() { + return helperFeatures; + } + + public String accept(ExampleNode node) { + if (node instanceof LiteralNode) { + if (node.getClientType() != ClassType.CONTEXT) { + node.getClientType().addImportsTo(imports, false); + } + + if (node.getClientType() == ClassType.URL) { + helperFeatures.add(ExampleHelperFeature.ThrowsIOException); // MalformedURLException from URL ctor + } + + return node.getClientType().defaultValueExpression(((LiteralNode) node).getLiteralsValue()); + } else if (node instanceof ObjectNode) { + IType simpleType = null; + if (node.getObjectValue() instanceof Integer) { + simpleType = PrimitiveType.INT; + } else if (node.getObjectValue() instanceof Long) { + simpleType = PrimitiveType.LONG; + } else if (node.getObjectValue() instanceof Float) { + simpleType = PrimitiveType.FLOAT; + } else if (node.getObjectValue() instanceof Double) { + simpleType = PrimitiveType.DOUBLE; + } else if (node.getObjectValue() instanceof Boolean) { + simpleType = PrimitiveType.BOOLEAN; + } else if (node.getObjectValue() instanceof String) { + simpleType = ClassType.STRING; + } + + if (simpleType != null) { + return simpleType.defaultValueExpression(node.getObjectValue().toString()); + } else { + helperFeatures.add(ExampleHelperFeature.ThrowsIOException); + + try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + JsonWriter jsonWriter = JsonProviders.createWriter(outputStream)) { + jsonWriter.writeUntyped(node.getObjectValue()).flush(); + + return codeDeserializeJsonString(outputStream.toString(StandardCharsets.UTF_8)); + } catch (IOException e) { + LOGGER.error("Failed to write JSON {}", node.getObjectValue()); + throw new IllegalStateException(e); + } + } + } else if (node instanceof ListNode) { + imports.add(java.util.Arrays.class.getName()); + + StringBuilder builder = new StringBuilder(); + // Arrays.asList(...) + builder.append("Arrays.asList(") + .append(node.getChildNodes().stream().map(this::accept).collect(Collectors.joining(", "))) + .append(")"); + + return builder.toString(); + } else if (node instanceof MapNode) { + imports.add(java.util.Map.class.getName()); + imports.add(java.util.HashMap.class.getName()); + + helperFeatures.add(ExampleHelperFeature.MapOfMethod); + + List keys = ((MapNode) node).getKeys(); + + StringBuilder builder = new StringBuilder(); + // mapOf(...) + // similar to Map.of in Java 9 + builder.append("mapOf("); + for (int i = 0; i < keys.size(); ++i) { + if (i != 0) { + builder.append(", "); + } + String key = keys.get(i); + ExampleNode elementNode = node.getChildNodes().get(i); + builder.append(ClassType.STRING.defaultValueExpression(key)) + .append(", ") + .append(this.accept(elementNode)); + } + builder.append(")"); + + return builder.toString(); + } else if (node instanceof ClientModelNode) { + ClientModelNode clientModelNode = ((ClientModelNode) node); + + ClientModel model = clientModelNode.getClientModel(); + + imports.add(model.getFullName()); + + StringBuilder builder = new StringBuilder(); + if (JavaSettings.getInstance().isRequiredFieldsAsConstructorArgs()) { + List requiredParentProperties = ClientModelUtil.getRequiredWritableParentProperties(model); + List requiredProperties = model.getProperties().stream() + .filter(ClientModelProperty::isRequired) + .filter(property -> !property.isConstant() && !property.isReadOnly()) + .collect(Collectors.toList()); + + List properties = Stream.concat( + requiredParentProperties.stream(), requiredProperties.stream()) + .map(ModelProperty::ofClientModelProperty) + .collect(Collectors.toList()); + Map ctorPosition = new HashMap<>(); + for (int i = 0; i < properties.size(); ++i) { + ctorPosition.put(properties.get(i), i); + } + + List initAtCtors = new ArrayList<>(Collections.nCopies(properties.size(), "")); + List initAtSetters = new ArrayList<>(); + for (ExampleNode childNode : node.getChildNodes()) { + ModelProperty modelProperty = clientModelNode.getClientModelProperties().get(childNode); + if (ctorPosition.containsKey(modelProperty)) { + initAtCtors.set(ctorPosition.get(modelProperty), this.accept(childNode)); + } else { + // .setProperty(...) + initAtSetters.add(String.format(".%1$s(%2$s)", modelProperty.getSetterName(), this.accept(childNode))); + } + } + // model constructor + builder.append("new ").append(model.getName()) + .append("(").append(String.join(", ", initAtCtors)).append(")"); + // setters + initAtSetters.forEach(builder::append); + } else { + // model with setters + builder.append("new ").append(model.getName()).append("()"); + for (ExampleNode childNode : node.getChildNodes()) { + ModelProperty modelProperty = clientModelNode.getClientModelProperties().get(childNode); + // .setProperty(...) + builder.append(".").append(modelProperty.getSetterName()) + .append("(").append(this.accept(childNode)).append(")"); + } + } + return builder.toString(); + } else if (node instanceof BinaryDataNode) { + this.imports.add(com.azure.core.util.BinaryData.class.getName()); + this.imports.add(java.nio.charset.StandardCharsets.class.getName()); + return binaryDataNodeExpression((BinaryDataNode) node); + } + return null; + } + } + + private static String binaryDataNodeExpression(BinaryDataNode binaryDataNode) { + return String.format("BinaryData.fromBytes(\"%s\".getBytes(StandardCharsets.UTF_8))", TemplateUtil.escapeString(binaryDataNode.getExampleValue())); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/example/ProtocolExampleWriter.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/example/ProtocolExampleWriter.java new file mode 100644 index 0000000000..9a6884bca2 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/example/ProtocolExampleWriter.java @@ -0,0 +1,386 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.template.example; + +import com.microsoft.typespec.http.client.generator.core.Javagen; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.RequestParameterLocation; +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; +import com.microsoft.typespec.http.client.generator.core.extension.plugin.PluginLogger; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.AsyncSyncClient; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClassType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethod; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethodParameter; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethodType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.GenericType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.IType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.IterableType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ListType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ProtocolExample; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ProxyMethod; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ProxyMethodExample; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ProxyMethodParameter; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ServiceClient; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaBlock; +import com.microsoft.typespec.http.client.generator.core.util.CodeNamer; +import com.microsoft.typespec.http.client.generator.core.util.ModelExampleUtil; +import com.azure.core.http.ContentType; +import com.azure.core.http.rest.PagedIterable; +import com.azure.core.http.rest.Response; +import com.azure.core.util.polling.LongRunningOperationStatus; +import com.azure.core.util.polling.SyncPoller; +import com.azure.core.util.serializer.CollectionFormat; +import org.slf4j.Logger; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.stream.Collectors; + +public class ProtocolExampleWriter { + + private final Logger logger = new PluginLogger(Javagen.getPluginInstance(), ProtocolExampleWriter.class); + + private final Set imports; + private final ClientInitializationExampleWriter clientInitializationExampleWriter; + private final BiConsumer clientMethodInvocationWriter; + private final Consumer assertionWriter; + + @SuppressWarnings("unchecked") + public ProtocolExampleWriter(ProtocolExample protocolExample) { + JavaSettings settings = JavaSettings.getInstance(); + + final ClientMethod method = protocolExample.getClientMethod(); + final AsyncSyncClient syncClient = protocolExample.getSyncClient(); + final ProxyMethodExample proxyMethodExample = protocolExample.getProxyMethodExample(); + final String clientVarName = CodeNamer.toCamelCase(syncClient.getClassName()); + final ServiceClient serviceClient = protocolExample.getClientBuilder().getServiceClient(); + + this.clientInitializationExampleWriter = + new ClientInitializationExampleWriter( + syncClient, + method, + proxyMethodExample, + serviceClient); + + // import + this.imports = new HashSet<>(); + + imports.addAll(this.clientInitializationExampleWriter.getImports()); + + ClassType.BINARY_DATA.addImportsTo(imports, false); + imports.add(java.util.Arrays.class.getName()); + method.addImportsTo(imports, false, settings); + + // assertion + imports.add("org.junit.jupiter.api.Assertions"); + imports.add(LongRunningOperationStatus.class.getName()); + ClassType.HTTP_HEADER_NAME.addImportsTo(imports, false); + + // method invocation + // parameter values and required invocation on RequestOptions + List params = new ArrayList<>(); + for (ClientMethodParameter parameter : method.getParameters()) { + params.add(parameter.getClientType().defaultValueExpression()); + } + + StringBuilder binaryDataStmt = new StringBuilder(); + + List requestOptionsStmts = new ArrayList<>(); + + List proxyMethodParameters = getProxyMethodParameters(method.getProxyMethod(), method.getParameters()); + final int numParam = method.getParameters().size(); + proxyMethodExample.getParameters().forEach((parameterName, parameterValue) -> { + boolean matchRequiredParameter = false; + for (int parameterIndex = 0; parameterIndex < numParam; parameterIndex++) { + ProxyMethodParameter proxyMethodParameter = proxyMethodParameters.get(parameterIndex); + if (proxyMethodParameter != null) { + if (getSerializedName(proxyMethodParameter).equalsIgnoreCase(parameterName)) { + // parameter in example found in method signature + + if (proxyMethodParameter.getCollectionFormat() != null + && (proxyMethodParameter.getClientType() instanceof ListType || proxyMethodParameter.getClientType() instanceof IterableType)) { + // query with array + + List elements = getParameterValueAsList( + proxyMethodParameter.getRequestParameterLocation() == RequestParameterLocation.QUERY + ? parameterValue.getUnescapedQueryValue() + : parameterValue.getObjectValue(), + proxyMethodParameter.getCollectionFormat()); + if (elements != null) { + IType elementType = ((GenericType) proxyMethodParameter.getClientType()).getTypeArguments()[0]; + String exampleValue = String.format( + "Arrays.asList(%s)", + elements.stream().map(value -> elementType.defaultValueExpression(value.toString())).collect(Collectors.joining(", "))); + params.set(parameterIndex, exampleValue); + } + } else if (proxyMethodParameter.getClientType() != ClassType.BINARY_DATA) { + // type like String, int, boolean, date-time + + String exampleValue = proxyMethodParameter.getRequestParameterLocation() == RequestParameterLocation.QUERY + ? parameterValue.getUnescapedQueryValue().toString() + : parameterValue.getObjectValue().toString(); + exampleValue = ModelExampleUtil.convertLiteralToClientValue(proxyMethodParameter.getWireType(), exampleValue); + params.set(parameterIndex, proxyMethodParameter.getClientType().defaultValueExpression(exampleValue)); + } else { + // BinaryData + String binaryDataValue = ClassType.STRING.defaultValueExpression(parameterValue.getJsonString()); + binaryDataStmt.append( + String.format("BinaryData %s = BinaryData.fromString(%s);", + parameterName, binaryDataValue)); + params.set(parameterIndex, parameterName); + } + matchRequiredParameter = true; + break; + } + } + } + if (!matchRequiredParameter) { + // parameter in example not found in method signature, check those parameters defined in spec but was left out of method signature + + method.getProxyMethod().getAllParameters().stream().filter(p -> !p.isFromClient()).filter(p -> getSerializedName(p).equalsIgnoreCase(parameterName)).findFirst().ifPresent(p -> { + switch (p.getRequestParameterLocation()) { + case QUERY: + if (p.getCollectionFormat() != null) { + List elements = getParameterValueAsList( + parameterValue.getUnescapedQueryValue(), + p.getCollectionFormat()); + if (elements != null) { + if (p.getExplode()) { + // collectionFormat: multi + for (Object element : elements) { + requestOptionsStmts.add( + String.format(".addQueryParam(\"%s\", %s)", + parameterName, + ClassType.STRING.defaultValueExpression(element.toString()))); + } + } else { + // collectionFormat: csv, ssv, tsv, pipes + String delimiter = p.getCollectionFormat().getDelimiter(); + String exampleValue = elements.stream() + .map(Object::toString) + .collect(Collectors.joining(delimiter)); + requestOptionsStmts.add( + String.format(".addQueryParam(\"%s\", %s)", + parameterName, + ClassType.STRING.defaultValueExpression(exampleValue))); + } + } + } else { + requestOptionsStmts.add( + String.format(".addQueryParam(\"%s\", %s)", + parameterName, + ClassType.STRING.defaultValueExpression(parameterValue.getUnescapedQueryValue().toString()))); + } + break; + + case HEADER: + // TODO (weidxu): header could have csv etc. + + requestOptionsStmts.add( + String.format(".addHeader(\"%s\", %s)", + parameterName, + ClassType.STRING.defaultValueExpression(parameterValue.getObjectValue().toString()))); + break; + + case BODY: + requestOptionsStmts.add( + String.format(".setBody(BinaryData.fromString(%s))", + ClassType.STRING.defaultValueExpression(parameterValue.getJsonString()))); + break; + + // Path cannot be optional + } + }); + } + }); + + this.clientMethodInvocationWriter = (methodBlock, isTestCode) -> { + // binaryData + if (binaryDataStmt.length() > 0) { + methodBlock.line(binaryDataStmt.toString()); + } + // requestOptions and context + if (requestOptionsStmts.isEmpty()) { + methodBlock.line("RequestOptions requestOptions = new RequestOptions();"); + } else { + StringBuilder sb = new StringBuilder("RequestOptions requestOptions = new RequestOptions()"); + requestOptionsStmts.forEach(sb::append); + sb.append(";"); + methodBlock.line(sb.toString()); + } + for (int i = 0; i < numParam; i++) { + ClientMethodParameter parameter = method.getParameters().get(i); + if (parameter.getClientType() == ClassType.REQUEST_OPTIONS) { + params.set(i, "requestOptions"); + } else if (parameter.getClientType() == ClassType.CONTEXT) { + params.set(i, "Context.NONE"); + } + } + String methodCall = String.format("%s.%s(%s)", + clientVarName, + method.getName(), + String.join(", ", params)); + if (isTestCode) { + if (method.getType() == ClientMethodType.LongRunningBeginSync) { + methodCall = "setPlaybackSyncPollerPollInterval(" + methodCall + ")"; + } else if (method.getType() == ClientMethodType.LongRunningBeginAsync) { + methodCall = "setPlaybackPollerFluxPollInterval(" + methodCall + ")"; + } + } + methodBlock.line(method.getReturnValue().getType() + " response = " + methodCall + ";"); + }; + + this.assertionWriter = methodBlock -> { + ProxyMethodExample.Response response = proxyMethodExample.getPrimaryResponse(); + if (response != null) { + IType returnType = method.getReturnValue().getType(); + if (returnType instanceof GenericType) { + GenericType responseType = (GenericType) returnType; + if (Response.class.getSimpleName().equals(responseType.getName())) { + // Response<> + + // assert status code + methodBlock.line(String.format("Assertions.assertEquals(%1$s, response.getStatusCode());", response.getStatusCode())); + // assert headers + response.getHttpHeaders().stream().forEach(header -> { + String expectedValueStr = ClassType.STRING.defaultValueExpression(header.getValue()); + String keyStr = ClassType.STRING.defaultValueExpression(header.getName()); + methodBlock.line(String.format("Assertions.assertEquals(%1$s, response.getHeaders().get(HttpHeaderName.fromString(%2$s)).getValue());", expectedValueStr, keyStr)); + }); + // assert JSON body + if (method.getProxyMethod().getResponseContentTypes() != null + && method.getProxyMethod().getResponseContentTypes().contains(ContentType.APPLICATION_JSON) + && responseType.getTypeArguments().length > 0 + && responseType.getTypeArguments()[0] == ClassType.BINARY_DATA) { + String expectedJsonStr = ClassType.STRING.defaultValueExpression(response.getJsonBody()); + methodBlock.line(String.format("Assertions.assertEquals(BinaryData.fromString(%1$s).toObject(Object.class), response.getValue().toObject(Object.class));", expectedJsonStr)); + } + } else if (SyncPoller.class.getSimpleName().equals(responseType.getName())) { + // SyncPoller<> + + if (response.getStatusCode() / 100 == 2) { + // it should have a 202 leading to SUCCESSFULLY_COMPLETED + // but x-ms-examples usually does not include the final result + methodBlock.line("Assertions.assertEquals(LongRunningOperationStatus.SUCCESSFULLY_COMPLETED, response.waitForCompletion().getStatus());"); + } + } else if (PagedIterable.class.getSimpleName().equals(responseType.getName())) { + // PagedIterable<> + + // assert status code + methodBlock.line(String.format("Assertions.assertEquals(%1$s, response.iterableByPage().iterator().next().getStatusCode());", response.getStatusCode())); + // assert headers + response.getHttpHeaders().stream().forEach(header -> { + String expectedValueStr = ClassType.STRING.defaultValueExpression(header.getValue()); + String keyStr = ClassType.STRING.defaultValueExpression(header.getName()); + methodBlock.line(String.format("Assertions.assertEquals(%1$s, response.iterableByPage().iterator().next().getHeaders().get(HttpHeaderName.fromString(%2$s)).getValue());", expectedValueStr, keyStr)); + }); + // assert JSON of first item, or assert count=0 + if (method.getProxyMethod().getResponseContentTypes() != null + && method.getProxyMethod().getResponseContentTypes().contains(ContentType.APPLICATION_JSON) + && responseType.getTypeArguments().length > 0 + && responseType.getTypeArguments()[0] == ClassType.BINARY_DATA + && method.getMethodPageDetails() != null + && response.getBody() instanceof Map) { + Map bodyMap = (Map) response.getBody(); + if (bodyMap.containsKey(method.getMethodPageDetails().getSerializedItemName())) { + Object items = bodyMap.get(method.getMethodPageDetails().getSerializedItemName()); + if (items instanceof List) { + List itemArray = (List) items; + if (itemArray.isEmpty()) { + methodBlock.line("Assertions.assertEquals(0, response.stream().count());"); + } else { + Object firstItem = itemArray.iterator().next(); + String expectedJsonStr = ClassType.STRING.defaultValueExpression(response.getJson(firstItem)); + methodBlock.line(String.format("Assertions.assertEquals(BinaryData.fromString(%1$s).toObject(Object.class), response.iterator().next().toObject(Object.class));", expectedJsonStr)); + } + } + } + } + } + } + } else { + methodBlock.line("Assertions.assertNotNull(response);"); + } + }; + } + + public Set getImports() { + return imports; + } + + public void writeClientInitialization(JavaBlock methodBlock) { + clientInitializationExampleWriter.write(methodBlock); + } + + public void writeClientMethodInvocation(JavaBlock methodBlock, boolean isTestCode) { + clientMethodInvocationWriter.accept(methodBlock, isTestCode); + } + + public void writeAssertion(JavaBlock methodBlock) { + assertionWriter.accept(methodBlock); + } + + private static String getSerializedName(ProxyMethodParameter parameter) { + String serializedName = parameter.getRequestParameterName(); + if (serializedName == null && parameter.getRequestParameterLocation() == RequestParameterLocation.BODY) { + serializedName = parameter.getName(); + } + return serializedName; + } + + private List getProxyMethodParameters( + ProxyMethod proxyMethod, + List clientMethodParameters) { + // the list of proxy method parameters will be 1-1 with list of client method parameters + + Map proxyMethodParameterByClientParameterName = proxyMethod.getParameters().stream() + .collect(Collectors.toMap(p -> CodeNamer.getEscapedReservedClientMethodParameterName(p.getName()), Function.identity())); + List proxyMethodParameters = new ArrayList<>(); + for (ClientMethodParameter clientMethodParameter : clientMethodParameters) { + ProxyMethodParameter proxyMethodParameter = proxyMethodParameterByClientParameterName.get(clientMethodParameter.getName()); + proxyMethodParameters.add(proxyMethodParameter); + + if (proxyMethodParameter == null) { + // this should not happen unless we changed the naming of client method parameter from proxy method parameter + logger.warn("Failed to find proxy method parameter for client method parameter with name '{}'", clientMethodParameter.getName()); + } + } + return proxyMethodParameters; + } + + @SuppressWarnings("unchecked") + private static List getParameterValueAsList(Object parameterValue, CollectionFormat collectionFormat) { + List elements = null; + if (parameterValue instanceof String) { + String value = (String) parameterValue; + switch (collectionFormat) { + case CSV: + elements = Arrays.stream(value.split(",", -1)).collect(Collectors.toList()); + break; + case SSV: + elements = Arrays.stream(value.split(" ", -1)).collect(Collectors.toList()); + break; + case PIPES: + elements = Arrays.stream(value.split("\\|", -1)).collect(Collectors.toList()); + break; + case TSV: + elements = Arrays.stream(value.split("\t", -1)).collect(Collectors.toList()); + break; + default: + // TODO (weidxu): CollectionFormat.MULTI + elements = Arrays.stream(value.split(",", -1)).collect(Collectors.toList()); + break; + } + } else if (parameterValue instanceof List) { + elements = (List) parameterValue; + } + return elements; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/example/ProtocolTestWriter.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/example/ProtocolTestWriter.java new file mode 100644 index 0000000000..ec7add66e9 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/example/ProtocolTestWriter.java @@ -0,0 +1,140 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.template.example; + +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Scheme; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.AsyncSyncClient; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClassType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ServiceClient; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ServiceClientProperty; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.TestContext; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaBlock; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaClass; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaIfBlock; +import com.microsoft.typespec.http.client.generator.core.util.CodeNamer; +import com.azure.core.http.HttpClient; +import com.azure.core.http.policy.HttpLogDetailLevel; +import com.azure.core.http.policy.HttpLogOptions; +import com.azure.core.util.Configuration; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Set; +import java.util.function.Consumer; + +public class ProtocolTestWriter { + + private final Set imports; + private final Consumer clientVariableWriter; + private final Consumer clientInitializationWriter; + + public ProtocolTestWriter(TestContext testContext) { + final List serviceClients = testContext.getServiceClients(); + final ServiceClient serviceClient = serviceClients.iterator().next(); + final List syncClients = testContext.getSyncClients(); + final boolean isTokenCredential = serviceClient.getSecurityInfo() != null && serviceClient.getSecurityInfo().getSecurityTypes() != null + && serviceClient.getSecurityInfo().getSecurityTypes().contains(Scheme.SecuritySchemeType.OAUTH2); + + this.imports = new HashSet<>(Arrays.asList( + HttpClient.class.getName(), + HttpLogDetailLevel.class.getName(), + HttpLogOptions.class.getName(), + Configuration.class.getName(), + "com.azure.core.test.utils.MockTokenCredential", + "com.azure.identity.DefaultAzureCredentialBuilder", + "com.azure.core.test.TestProxyTestBase", + "com.azure.core.test.TestMode", +// "com.azure.core.test.annotation.DoNotRecord", + "org.junit.jupiter.api.Disabled", + "org.junit.jupiter.api.Test" + )); + // client and builder + syncClients.forEach(c -> { + c.addImportsTo(imports, false); + c.getClientBuilder().addImportsTo(imports, false); + }); + // base test class + imports.add(String.format("%s.%s", testContext.getPackageName(), testContext.getTestBaseClassName())); + + this.clientVariableWriter = classBlock -> { + syncClients.forEach(c -> { + classBlock.protectedMemberVariable(c.getClassName(), CodeNamer.toCamelCase(c.getClassName())); + }); + }; + + this.clientInitializationWriter = methodBlock -> { + Iterator serviceClientIterator = serviceClients.iterator(); + ServiceClient currentServiceClient = null; + for (AsyncSyncClient syncClient : syncClients) { + if (serviceClientIterator.hasNext()) { + // either a single serviceClient for all syncClients, or 1 serviceClient to 1 syncClient + currentServiceClient = serviceClientIterator.next(); + } + + String clientVarName = CodeNamer.toCamelCase(syncClient.getClassName()); + String builderClassName = syncClient.getClientBuilder().getClassName(); + String builderVarName = CodeNamer.toCamelCase(syncClient.getClassName()) + "builder"; + + methodBlock.line(String.format("%1$s %2$s = new %3$s()", builderClassName, builderVarName, builderClassName)); + methodBlock.increaseIndent(); + // required service client properties + currentServiceClient.getProperties().stream().filter(ServiceClientProperty::isRequired).forEach(serviceClientProperty -> { + String defaultValueExpression = serviceClientProperty.getDefaultValueExpression(); + String expr; + if (defaultValueExpression == null) { + expr = String.format("Configuration.getGlobalConfiguration().get(\"%1$s\", %2$s)", + serviceClientProperty.getName().toUpperCase(Locale.ROOT), ClassType.STRING.defaultValueExpression(serviceClientProperty.getName().toLowerCase(Locale.ROOT))); + } else { + expr = String.format("Configuration.getGlobalConfiguration().get(\"%1$s\", %2$s)", + serviceClientProperty.getName().toUpperCase(Locale.ROOT), defaultValueExpression); + } + methodBlock.line(".%1$s(%2$s)", serviceClientProperty.getAccessorMethodSuffix(), expr); + }); + methodBlock.line(".httpClient(HttpClient.createDefault())"); + methodBlock.line(".httpLogOptions(new HttpLogOptions().setLogLevel(HttpLogDetailLevel.BASIC));"); + methodBlock.decreaseIndent(); + + JavaIfBlock codeBlock = methodBlock.ifBlock("getTestMode() == TestMode.PLAYBACK", ifBlock -> { + if (isTokenCredential) { + ifBlock.line(String.format("%1$s.httpClient(interceptorManager.getPlaybackClient())", builderVarName)); + ifBlock.line(".credential(new MockTokenCredential());"); + } else { + ifBlock.line(String.format("%1$s.httpClient(interceptorManager.getPlaybackClient());", builderVarName)); + } + }).elseIfBlock("getTestMode() == TestMode.RECORD", ifBlock -> { + if (isTokenCredential) { + ifBlock.line(String.format("%1$s.addPolicy(interceptorManager.getRecordPolicy())", builderVarName)); + ifBlock.line(".credential(new DefaultAzureCredentialBuilder().build());"); + } else { + ifBlock.line(String.format("%1$s.addPolicy(interceptorManager.getRecordPolicy());", builderVarName)); + } + }); + + if (isTokenCredential) { + codeBlock.elseIfBlock("getTestMode() == TestMode.LIVE", ifBlock -> { + ifBlock.line(String.format("%1$s.credential(new DefaultAzureCredentialBuilder().build());", builderVarName)); + }); + } + + methodBlock.line(String.format("%1$s = %2$s.%3$s();", clientVarName, builderVarName, syncClient.getClientBuilder().getBuilderMethodNameForSyncClient(syncClient))); + methodBlock.line(); + }; + }; + } + + public Set getImports() { + return imports; + } + + public void writeClientVariables(JavaClass classBlock) { + clientVariableWriter.accept(classBlock); + } + + public void writeClientInitialization(JavaBlock methodBlock) { + clientInitializationWriter.accept(methodBlock);; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/prototype/MethodTemplate.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/prototype/MethodTemplate.java new file mode 100644 index 0000000000..a697857c5f --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/prototype/MethodTemplate.java @@ -0,0 +1,121 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.template.prototype; + +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaBlock; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaClass; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaInterface; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaJavadocComment; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaModifier; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaVisibility; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.function.Consumer; + +public class MethodTemplate { + + private final Set imports; + + private final JavaVisibility visibility; + private final List modifiers; + private final String methodSignature; + + private final Consumer comment; + private final Consumer method; + + private MethodTemplate(Set imports, + JavaVisibility visibility, List modifiers, String methodSignature, + Consumer comment, Consumer method) { + this.imports = imports; + this.visibility = visibility; + this.modifiers = modifiers; + this.methodSignature = methodSignature; + this.comment = comment; + this.method = method; + } + + public final void addImportsTo(Set imports) { + imports.addAll(this.imports); + } + + public final void writeMethod(JavaClass javaClass) { + if (comment != null) { + javaClass.javadocComment(comment); + } + writeMethodWithoutJavadoc(javaClass); + } + + public final void writeMethodWithoutJavadoc(JavaClass javaClass) { + javaClass.method(visibility, modifiers, methodSignature, method); + } + + public final void writeMethodInterface(JavaInterface javaInterface) { + if (visibility == JavaVisibility.Public) { + if (comment != null) { + javaInterface.javadocComment(comment); + } + javaInterface.publicMethod(methodSignature); + } + } + + public final void writeMethodContent(JavaBlock block) { + method.accept(block); + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private final Set imports = new HashSet<>(); + private JavaVisibility visibility = JavaVisibility.Public; + private final List modifiers = new ArrayList<>(); + private String methodSignature; + private Consumer comment = null; + private Consumer method = m -> {}; + + private Builder() { + } + + public Builder imports(Collection imports) { + this.imports.addAll(imports); + return this; + } + + public Builder visibility(JavaVisibility visibility) { + this.visibility = visibility; + return this; + } + + public Builder modifiers(Collection modifiers) { + this.modifiers.addAll(modifiers); + return this; + } + + public Builder methodSignature(String methodSignature) { + this.methodSignature = methodSignature; + return this; + } + + public Builder comment(Consumer comment) { + this.comment = comment; + return this; + } + + public Builder method(Consumer method) { + this.method = method; + return this; + } + + public MethodTemplate build() { + Objects.requireNonNull(methodSignature); + return new MethodTemplate(imports, visibility, modifiers, methodSignature, comment, method); + } + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/util/ModelTemplateHeaderHelper.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/util/ModelTemplateHeaderHelper.java new file mode 100644 index 0000000000..3d26fd16d9 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/util/ModelTemplateHeaderHelper.java @@ -0,0 +1,276 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.template.util; + +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ArrayType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClassType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientModel; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientModelProperty; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.EnumType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.GenericType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.IType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.MapType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.PrimitiveType; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaBlock; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaClass; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaModifier; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaVisibility; +import com.microsoft.typespec.http.client.generator.core.template.ModelTemplate; +import com.microsoft.typespec.http.client.generator.core.util.CodeNamer; +import com.azure.core.http.HttpHeaderName; +import com.azure.core.http.HttpHeaders; +import com.azure.core.util.CoreUtils; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +/** + * Utility class for {@link ModelTemplate} that handles generating {@link HttpHeaders} deserialization to POJOs. + */ +public final class ModelTemplateHeaderHelper { + private static final Map HEADER_TO_KNOWN_HTTPHEADERNAME; + + static { + Map headerToKnownHttpHeaderName = new TreeMap<>(String::compareToIgnoreCase); + for (Field httpHeaderNameConstant : HttpHeaderName.class.getDeclaredFields()) { + if (httpHeaderNameConstant.getType() != HttpHeaderName.class + || !isPublicConstant(httpHeaderNameConstant.getModifiers())) { + continue; + } + + try { + HttpHeaderName httpHeaderName = (HttpHeaderName) httpHeaderNameConstant.get(null); + String constantName = httpHeaderNameConstant.getName(); + headerToKnownHttpHeaderName.put(httpHeaderName.getCaseInsensitiveName(), constantName); + } catch (IllegalAccessException ignored) { + // Do nothing. + } + } + + HEADER_TO_KNOWN_HTTPHEADERNAME = Collections.unmodifiableMap(headerToKnownHttpHeaderName); + } + + /** + * Adds an {@link HttpHeaders}-based constructor to a model. + * + * @param classBlock The class block for the model. + * @param model The model itself. + * @param settings Autorest generation settings. + */ + public static void addCustomStronglyTypedHeadersConstructor(JavaClass classBlock, ClientModel model, + JavaSettings settings) { + addHttpHeaderNameConstants(classBlock, model); + + classBlock.lineComment("HttpHeaders containing the raw property values."); + classBlock.javadocComment(comment -> { + comment.description(String.format("Creates an instance of %1$s class.", model.getName())); + comment.param("rawHeaders", "The raw HttpHeaders that will be used to create the property values."); + }); + classBlock.publicConstructor(String.format("%s(HttpHeaders rawHeaders)", model.getName()), constructor -> { + // HeaderCollections need special handling as they may have multiple values that need to be retrieved from + // the raw headers. + List collectionProperties = new ArrayList<>(); + for (ClientModelProperty property : model.getProperties()) { + if (CoreUtils.isNullOrEmpty(property.getHeaderCollectionPrefix())) { + generateHeaderDeserializationFunction(property, constructor); + } else { + collectionProperties.add(property); + } + } + + if (!CoreUtils.isNullOrEmpty(collectionProperties)) { + // Bundle all collection properties into one iteration over the HttpHeaders. + generateHeaderCollectionDeserialization(collectionProperties, constructor); + } + }); + } + + /** + * Gets an expression of HttpHeaderName instance. + *

+ * For known name, it would be {@code HttpHeaderName.IF_MATCH}. For unknown name, it would be {@code HttpHeaderName.fromString("foo")}. + * + * @param headerName the header name + * @return the expression of HttpHeaderName instance. + */ + public static String getHttpHeaderNameInstanceExpression(String headerName) { + // match the init logic of HEADER_TO_KNOWN_HTTPHEADERNAME + String caseInsensitiveName = HttpHeaderName.fromString(headerName).getCaseInsensitiveName(); + + if (HEADER_TO_KNOWN_HTTPHEADERNAME.containsKey(caseInsensitiveName)) { + // known name + return String.format("HttpHeaderName.%s", HEADER_TO_KNOWN_HTTPHEADERNAME.get(caseInsensitiveName)); + } else { + return String.format("HttpHeaderName.fromString(%s)", ClassType.STRING.defaultValueExpression(headerName)); + } + } + + /** + * Adds {@code private static final} {@link HttpHeaderName} constants representing the headers that are used by the + * {@link ClientModel}. + * + * @param classBlock The class block for the model. + * @param model The model itself. + */ + private static void addHttpHeaderNameConstants(JavaClass classBlock, ClientModel model) { + for (ClientModelProperty property : model.getProperties()) { + if (!CoreUtils.isNullOrEmpty(property.getHeaderCollectionPrefix())) { + // Header collections aren't able to use HttpHeaderName. + continue; + } + + if (HEADER_TO_KNOWN_HTTPHEADERNAME.containsKey(property.getSerializedName())) { + // Header is a well-known HttpHeaderName, don't need to create a private constant. + continue; + } + + String headerName = property.getSerializedName(); + String constantName = CodeNamer.getEnumMemberName(headerName); + classBlock.variable( + String.format("HttpHeaderName %s = HttpHeaderName.fromString(\"%s\")", constantName, headerName), + JavaVisibility.Private, JavaModifier.Static, JavaModifier.Final); + } + } + + private static void generateHeaderDeserializationFunction(ClientModelProperty property, JavaBlock javaBlock) { + IType wireType = property.getWireType(); + boolean needsNullGuarding = wireType != ClassType.STRING && + (wireType instanceof ArrayType || wireType instanceof ClassType + || wireType instanceof EnumType || wireType instanceof GenericType); + + // No matter the wire type the rawHeaders will need to be accessed. + String knownHttpHeaderNameConstant = HEADER_TO_KNOWN_HTTPHEADERNAME.get(property.getSerializedName()); + String httpHeaderName = knownHttpHeaderNameConstant != null ? "HttpHeaderName." + knownHttpHeaderNameConstant + : CodeNamer.getEnumMemberName(property.getSerializedName()); + + String rawHeaderAccess = String.format("rawHeaders.getValue(%s)", httpHeaderName); + if (needsNullGuarding) { + javaBlock.line("String %s = %s;", property.getName(), rawHeaderAccess); + rawHeaderAccess = property.getName(); + } + + boolean needsTryCatch = false; + String setter; + if (wireType == PrimitiveType.BOOLEAN || wireType == ClassType.BOOLEAN) { + setter = String.format("Boolean.parseBoolean(%s)", rawHeaderAccess); + } else if (wireType == PrimitiveType.DOUBLE || wireType == ClassType.DOUBLE) { + setter = String.format("Double.parseDouble(%s)", rawHeaderAccess); + } else if (wireType == PrimitiveType.FLOAT || wireType == ClassType.FLOAT) { + setter = String.format("Float.parseFloat(%s)", rawHeaderAccess); + } else if (wireType == PrimitiveType.INT || wireType == ClassType.INTEGER) { + setter = String.format("Integer.parseInt(%s)", rawHeaderAccess); + } else if (wireType == PrimitiveType.LONG || wireType == ClassType.LONG) { + setter = String.format("Long.parseLong(%s)", rawHeaderAccess); + } else if (wireType == ArrayType.BYTE_ARRAY) { + setter = String.format("Base64.getDecoder().decode(%s)", rawHeaderAccess); + } else if (wireType == ClassType.STRING) { + setter = rawHeaderAccess; + } else if (wireType == ClassType.DATE_TIME_RFC_1123) { + setter = String.format("new DateTimeRfc1123(%s)", rawHeaderAccess); + } else if (wireType == ClassType.DATE_TIME) { + setter = String.format("OffsetDateTime.parse(%s)", rawHeaderAccess); + } else if (wireType == ClassType.LOCAL_DATE) { + setter = String.format("LocalDate.parse(%s)", rawHeaderAccess); + } else if (wireType == ClassType.DURATION) { + setter = String.format("Duration.parse(%s)", rawHeaderAccess); + } else if (wireType == ClassType.UUID) { + setter = "UUID.fromString(" + rawHeaderAccess + ")"; + } else if (wireType == ClassType.URL) { + needsTryCatch = true; + setter = "new URL(" + rawHeaderAccess + ")"; + } else if (wireType instanceof EnumType) { + EnumType enumType = (EnumType) wireType; + setter = String.format("%s.%s(%s)", enumType.getName(), enumType.getFromMethodName(), rawHeaderAccess); + } else { + // TODO (alzimmer): Check if the wire type is a Swagger type that could use stream-style serialization. + needsTryCatch = true; + setter = String.format( + "JacksonAdapter.createDefaultSerializerAdapter().deserializeHeader(rawHeaders.get(\"%s\"), %s)", + property.getSerializedName(), getWireTypeJavaType(wireType)); + } + + if (needsTryCatch) { + javaBlock.line("try {"); + javaBlock.increaseIndent(); + } + + // String is special as the setter is null safe for it, unlike other nullable types. + if (needsNullGuarding) { + javaBlock.ifBlock(String.format("%s != null", property.getName()), + ifBlock -> ifBlock.line("this.%s = %s;", property.getName(), setter)); + } else { + javaBlock.line("this.%s = %s;", property.getName(), setter); + } + + if (needsTryCatch) { + // At this time all try-catching is for IOExceptions. + javaBlock.decreaseIndent(); + javaBlock.line("} catch (IOException ex) {"); + javaBlock.indent(() -> javaBlock.line("throw LOGGER.atError().log(new UncheckedIOException(ex));")); + javaBlock.line("}"); + } + } + + private static String getWireTypeJavaType(IType iType) { + if (iType instanceof ArrayType || iType instanceof ClassType) { + // Both ArrayType and ClassType have toString methods that return the text representation of the type, + // for example "int[]" or "HttpHeaders". These support adding ".class" to get the Java runtime Class. + return iType + ".class"; + } else { + // All other types are GenericTypes. GenericType's toString returns the Java code generic representation, + // such as "List" or "Map". + // + // Use a new TypeReference to get the representing Type for the wire type. + return "new TypeReference<" + iType + ">() {}.getJavaType()"; + } + } + + private static void generateHeaderCollectionDeserialization(List properties, JavaBlock block) { + for (ClientModelProperty property : properties) { + // HeaderCollections are always Maps that use String as the key. + MapType wireType = (MapType) property.getWireType(); + + // Prefix the map with the property name for the cases where multiple header collections exist. + block.line("%s %sHeaderCollection = new HashMap<>();", wireType, property.getName()); + } + + block.line(); + + block.block("for (HttpHeader header : rawHeaders)", body -> { + body.line("String headerName = header.getName();"); + int propertiesSize = properties.size(); + for (int i = 0; i < propertiesSize; i++) { + ClientModelProperty property = properties.get(i); + boolean needsContinue = i < propertiesSize - 1; + body.ifBlock(String.format("headerName.startsWith(\"%s\")", property.getHeaderCollectionPrefix()), + ifBlock -> { + ifBlock.line("%sHeaderCollection.put(headerName.substring(%d), header.getValue());", + property.getName(), property.getHeaderCollectionPrefix().length()); + if (needsContinue) { + ifBlock.line("continue;"); + } + }); + } + }); + + block.line(); + + for (ClientModelProperty property : properties) { + block.line("this.%s = %sHeaderCollection;", property.getName(), property.getName()); + } + } + + private static boolean isPublicConstant(int modifiers) { + return Modifier.isPublic(modifiers) && Modifier.isStatic(modifiers) && Modifier.isFinal(modifiers); + } + + private ModelTemplateHeaderHelper() { + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/util/ClassNameUtil.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/util/ClassNameUtil.java new file mode 100644 index 0000000000..22ddf15745 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/util/ClassNameUtil.java @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.util; + +public final class ClassNameUtil { + + /** + * Truncate class name to avoid path too long. + * + * It contains some heuristic logic, and the result may not be exactly correct. + * + * @param namespace the namespace of the package, used to deduce the artifact id and group id + * @param directory the part of directory from maven project + * @param packageName the namespace/package of the class + * @param className the name of the class + * @return the truncated class name + */ + public static String truncateClassName( + String namespace, String directory, + String packageName, String className) { + // see https://github.com/Azure/azure-sdk-for-java/blob/main/eng/common/pipelines/templates/steps/verify-path-length.yml + final int maxPathLength = 260; + final int basePathLength = 38; + + // usual directory layout is: + // /sdk/////.java + + // heuristic + final int groupLength = namespace.length() - namespace.lastIndexOf("."); + final int artifactLength = namespace.length() - namespace.indexOf("."); + + final int directoryLength = directory.length(); + final int packageLength = packageName.length(); + final int extraLength = 14; + + final int minRemainLength = 5; + + final int remainLength = maxPathLength - basePathLength - groupLength - artifactLength - directoryLength - packageLength - extraLength; + + if (remainLength < className.length() && remainLength >= minRemainLength) { + className = className.substring(0, remainLength - 1); + } + return className; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/util/ClientModelUtil.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/util/ClientModelUtil.java new file mode 100644 index 0000000000..37f884d914 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/util/ClientModelUtil.java @@ -0,0 +1,771 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.util; + +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.ApiVersion; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Client; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.CodeModel; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.ConstantSchema; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.KnownMediaType; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Operation; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.OperationGroup; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Parameter; +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; +import com.microsoft.typespec.http.client.generator.core.mapper.Mappers; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.AsyncSyncClient; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClassType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethod; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientModel; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientModelProperty; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientModelPropertyAccess; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientModels; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ConvenienceMethod; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.GenericType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.IType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ImplementationDetails; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.MethodGroupClient; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ServiceClient; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaVisibility; +import com.azure.core.util.CoreUtils; + +import java.net.URI; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Utilities for client model. + */ +public class ClientModelUtil { + + public static final String MULTI_PART_FORM_DATA_HELPER_CLASS_NAME = "MultipartFormDataHelper"; + public static final String GENERIC_MULTI_PART_FORM_DATA_HELPER_CLASS_NAME = "GenericMultipartFormDataHelper"; + + private static final Pattern SPLIT_FLATTEN_PROPERTY_PATTERN = Pattern.compile("((? asyncClients, List syncClients) { + String packageName = getAsyncSyncClientPackageName(serviceClient); + boolean generateAsyncMethods = JavaSettings.getInstance().isGenerateAsyncMethods(); + boolean generateSyncMethods = JavaSettings.getInstance().isGenerateSyncMethods(); + + if (serviceClient.getProxy() != null) { + AsyncSyncClient.Builder builder = new AsyncSyncClient.Builder() + .packageName(packageName) + .serviceClient(serviceClient) + .crossLanguageDefinitionId(client.getCrossLanguageDefinitionId()); + + final List convenienceMethods = client.getOperationGroups().stream() + .filter(og -> CoreUtils.isNullOrEmpty(og.getLanguage().getJava().getName())) // no resource group + .findAny() + .map(og -> getConvenienceMethods(serviceClient::getClientMethods, og)) + .orElse(Collections.emptyList()); + builder.convenienceMethods(convenienceMethods); + + if (generateAsyncMethods) { + String asyncClassName = clientNameToAsyncClientName(serviceClient.getClientBaseName()); + asyncClients.add(builder.className(asyncClassName).build()); + } + + if (generateSyncMethods) { + String syncClassName = + serviceClient.getClientBaseName().endsWith("Client") + ? serviceClient.getClientBaseName() + : serviceClient.getClientBaseName() + "Client"; + syncClients.add(builder.className(syncClassName).build()); + } + } + + final int count = serviceClient.getMethodGroupClients().size() + asyncClients.size(); + for (MethodGroupClient methodGroupClient : serviceClient.getMethodGroupClients()) { + AsyncSyncClient.Builder builder = new AsyncSyncClient.Builder() + .packageName(packageName) + .serviceClient(serviceClient) + .methodGroupClient(methodGroupClient); + + final List convenienceMethods = client.getOperationGroups().stream() + .filter(og -> methodGroupClient.getClassBaseName().equals(og.getLanguage().getJava().getName())) + .findAny() + .map(og -> getConvenienceMethods(methodGroupClient::getClientMethods, og)) + .orElse(Collections.emptyList()); + builder.convenienceMethods(convenienceMethods); + + if (count == 1) { + // if it is the only method group, use service client name as base. + + if (generateAsyncMethods) { + String asyncClassName = clientNameToAsyncClientName(serviceClient.getClientBaseName()); + asyncClients.add(builder.className(asyncClassName).build()); + } + + if (generateSyncMethods) { + String syncClassName = + serviceClient.getClientBaseName().endsWith("Client") + ? serviceClient.getClientBaseName() + : serviceClient.getClientBaseName() + "Client"; + syncClients.add(builder.className(syncClassName).build()); + } + } else { + if (generateAsyncMethods) { + String asyncClassName = clientNameToAsyncClientName(methodGroupClient.getClassBaseName()); + asyncClients.add(builder.className(asyncClassName).build()); + } + + if (generateSyncMethods) { + String syncClassName = + methodGroupClient.getClassBaseName().endsWith("Client") + ? methodGroupClient.getClassBaseName() + : methodGroupClient.getClassBaseName() + "Client"; + syncClients.add(builder.className(syncClassName).build()); + } + } + } + } + + private static List getConvenienceMethods(Supplier> clientMethods, OperationGroup og) { + return og.getOperations().stream() + .filter(o -> o.getConvenienceApi() != null) + .flatMap(o -> { + List cMethods = Mappers.getClientMethodMapper().map(o, false) + .stream() + .filter(m -> m.getMethodVisibility() == JavaVisibility.Public) + .collect(Collectors.toList()); + if (!cMethods.isEmpty()) { + // sync stack generates additional proxy methods with name suffix "Sync" + String proxyMethodBaseName = cMethods.iterator().next().getProxyMethod().getBaseName(); + return clientMethods.get().stream() + .filter(m -> + proxyMethodBaseName.equals(m.getProxyMethod().getBaseName()) + && m.getMethodVisibility() == JavaVisibility.Public) + .map(m -> new ConvenienceMethod(m, cMethods)); + } else { + return Stream.empty(); + } + }).collect(Collectors.toList()); + } + + /** + * @param codeModel the code model + * @return the interface name of service client. + */ + public static String getClientInterfaceName(Client codeModel) { + JavaSettings settings = JavaSettings.getInstance(); + String serviceClientInterfaceName = (settings.getClientTypePrefix() == null ? "" : settings.getClientTypePrefix()) + + codeModel.getLanguage().getJava().getName(); + if (settings.isDataPlaneClient()) { + // mandate ending Client for LLC + if (!serviceClientInterfaceName.endsWith("Client")) { + String serviceName = settings.getServiceName(); + if (serviceName != null && codeModel instanceof CodeModel) { + serviceName = CodeNamer.removeSpaceCharacters(serviceName); + serviceClientInterfaceName = serviceName.endsWith("Client") ? serviceName : (serviceName + "Client"); + } else { + serviceClientInterfaceName += "Client"; + } + } + } + return serviceClientInterfaceName; + } + + /** + * @param codeModel the code model + * @return the class name of service client implementation. + */ + public static String getClientImplementClassName(Client codeModel) { + String serviceClientInterfaceName = getClientInterfaceName(codeModel); + return getClientImplementClassName(serviceClientInterfaceName); + } + + /** + * @param serviceClientInterfaceName the interface name of service client + * @return the class name of service client implementation. + */ + public static String getClientImplementClassName(String serviceClientInterfaceName) { + JavaSettings settings = JavaSettings.getInstance(); + String serviceClientClassName = serviceClientInterfaceName; + if (settings.isGenerateClientAsImpl()) { + serviceClientClassName += "Impl"; + } + return serviceClientClassName; + } + + /** + * @param serviceClientInterfaceName the interface name of service client + * @return the class name of service version. + */ + public static String getServiceVersionClassName(String serviceClientInterfaceName) { + JavaSettings settings = JavaSettings.getInstance(); + String serviceName; + if (settings.getServiceName() == null) { + if (serviceClientInterfaceName.endsWith("Client")) { + // remove ending Client + serviceName = serviceClientInterfaceName.substring(0, serviceClientInterfaceName.length() - "Client".length()); + } else { + serviceName = serviceClientInterfaceName; + } + } else { + serviceName = CodeNamer.removeSpaceCharacters(settings.getServiceName()); + } + return serviceName + (serviceName.endsWith("Service") ? "Version" : "ServiceVersion"); + } + + /** + * Gets the suffix of the builder class. + *

+ * The class name of the Builder is usually the service client interface name + builder suffix. + * + * @return the suffix of the builder class. + */ + public static String getBuilderSuffix() { + JavaSettings settings = JavaSettings.getInstance(); + StringBuilder builderSuffix = new StringBuilder(); + if (!settings.isFluent() + && settings.isGenerateClientAsImpl() + && !settings.isGenerateSyncAsyncClients() + && !settings.isDataPlaneClient()) { + builderSuffix.append("Impl"); + } + builderSuffix.append("Builder"); + return builderSuffix.toString(); + } + + public static String getServiceClientBuilderPackageName(ServiceClient serviceClient) { + JavaSettings settings = JavaSettings.getInstance(); + String builderPackage = serviceClient.getPackage(); + if (settings.isFluent()) { + builderPackage = settings.getPackage(settings.getImplementationSubpackage()); + } else if (settings.isGenerateSyncAsyncClients() || settings.isDataPlaneClient()) { + if (serviceClient.getBuilderPackageName() != null) { + builderPackage = serviceClient.getBuilderPackageName(); + } else { + builderPackage = settings.getPackage(); + } + } + return builderPackage; + } + + public static String getServiceClientPackageName(String serviceClientClassName) { + JavaSettings settings = JavaSettings.getInstance(); + String subpackage = settings.isGenerateClientAsImpl() ? settings.getImplementationSubpackage() : null; + if (settings.isFluent()) { + if (settings.isGenerateSyncAsyncClients() || settings.isGenerateClientInterfaces()) { + subpackage = settings.getImplementationSubpackage(); + } else { + subpackage = settings.getFluentSubpackage(); + } + } + if (settings.isCustomType(serviceClientClassName)) { + subpackage = settings.getCustomTypesSubpackage(); + } + return settings.getPackage(subpackage); + } + + public static String getAsyncSyncClientPackageName(ServiceClient serviceClient) { + JavaSettings settings = JavaSettings.getInstance(); + if (settings.isFluent()) { + return settings.getPackage(settings.getFluentSubpackage()); + } else { + return getServiceClientBuilderPackageName(serviceClient); + } + } + + public static String getServiceClientInterfacePackageName() { + JavaSettings settings = JavaSettings.getInstance(); + if (settings.isFluent()) { + return settings.getPackage(settings.getFluentSubpackage()); + } else { + return settings.getPackage(); + } + } + + public static String getClientDefaultValueOrConstantValue(Parameter parameter) { + String clientDefaultValueOrConstantValue = parameter.getClientDefaultValue(); + if (clientDefaultValueOrConstantValue == null) { + if (parameter.getSchema() != null && parameter.getSchema() instanceof ConstantSchema) { + ConstantSchema constantSchema = (ConstantSchema) parameter.getSchema(); + if (constantSchema.getValue() != null) { + clientDefaultValueOrConstantValue = constantSchema.getValue().getValue().toString(); + } + } + } + return clientDefaultValueOrConstantValue; + } + + private static String getFirstApiVersionFromOperation(CodeModel codeModel) { + return codeModel.getOperationGroups().stream() + .flatMap(og -> og.getOperations().stream()) + .filter(o -> o.getApiVersions() != null) + .flatMap(o -> o.getApiVersions().stream()) + .filter(Objects::nonNull) + .map(ApiVersion::getVersion) + .filter(Objects::nonNull) + .findFirst() + .orElse(null); + } + + public static List getApiVersions(CodeModel codeModel) { + List versions = codeModel.getClients().stream() + .filter(c -> !CoreUtils.isNullOrEmpty(c.getApiVersions())) + .map(c -> c.getApiVersions().stream().map(ApiVersion::getVersion).collect(Collectors.toList())) + .findFirst().orElse(null); + if (versions == null) { + String version = getFirstApiVersionFromOperation(codeModel); + if (version != null) { + versions = Collections.singletonList(version); + } else { + versions = Collections.emptyList(); + } + } + return versions; + } + + public static String getArtifactId() { + JavaSettings settings = JavaSettings.getInstance(); + String artifactId = settings.getArtifactId(); + if (settings.isDataPlaneClient() && CoreUtils.isNullOrEmpty(artifactId)) { + // convert package/namespace to artifact + artifactId = settings.getPackage().toLowerCase(Locale.ROOT) + .replace("com.", "") + .replace(".", "-"); + } + return artifactId; + } + + public static String clientNameToAsyncClientName(String clientName) { + if (clientName.endsWith("Client")) { + clientName = clientName.substring(0, clientName.length() - "Client".length()) + "AsyncClient"; + } else { + clientName += "AsyncClient"; + } + return clientName; + } + + /** + * Split and unescape the possible flattened serialized property name to its components. + * + * @param serializedName the serialized property name belongs to either model or property that has {@code @JsonFlatten} annotation. + * @return the components of the serialized property names + */ + public static List splitFlattenedSerializedName(String serializedName) { + if (serializedName == null) { + return Collections.emptyList(); + } + + String[] values = SPLIT_FLATTEN_PROPERTY_PATTERN.split(serializedName); + for (int i = 0; i < values.length; ++i) { + values[i] = values[i].replace("\\\\.", "."); + } + return Arrays.asList(values); + } + + private static Function getClientModelFunction = name -> ClientModels.getInstance().getModel(name); + + /** + * Replace the default function of getting ClientModel by name. + *

+ * Used in Fluent for providing additional ClientModel that exists in azure-core-management, + * e.g. Resource, ManagementError + * + * @param function the function of getting ClientModel by name + */ + public static void setGetClientModelFunction(Function function) { + getClientModelFunction = function; + } + + /** + * Get ClientModel by name. + * + * @param name the name of the ClientModel (without package) + * @return the ClientModel instance. null if not found. + */ + public static ClientModel getClientModel(String name) { + return getClientModelFunction.apply(name); + } + + /** + * Check if the type is a ClientModel. + * + * @param type the type + * @return whether the type is a ClientModel. + */ + public static boolean isClientModel(IType type) { + if (type instanceof ClassType) { + ClassType classType = (ClassType) type; + return classType.getPackage().startsWith(JavaSettings.getInstance().getPackage()) + && getClientModel(classType.getName()) != null; + } else { + return false; + } + } + + /** + * Check if the type is an external model. + * + * @param type the type + * @return whether the type is an external model. + */ + public static boolean isExternalModel(IType type) { + if (type instanceof ClassType) { + ClassType classType = (ClassType) type; + ClientModel model = getClientModel(classType.getName()); + return model != null && model.getImplementationDetails() != null && model.getImplementationDetails().getUsages() != null + && model.getImplementationDetails().getUsages().contains(ImplementationDetails.Usage.EXTERNAL); + } else { + return false; + } + } + + /** + * Check if the type is an output only model. + *

+ * A model is considered output only if and only if the model's usages contain either + * {@link ImplementationDetails#isOutput()} or {@link ImplementationDetails#isException()} and do not contain + * {@link ImplementationDetails#isInput()}. + * + * @param model the client model. + * @return whether the type is an output only model. + */ + public static boolean isOutputOnly(ClientModel model) { + if (hasNoUsage(model)) { + return false; + } + ImplementationDetails details = model.getImplementationDetails(); + + return (details.isOutput() || details.isException()) && !details.isInput(); + } + + /** + * Check if the model is used in json-merge-patch operation + */ + public static boolean isJsonMergePatchModel(ClientModel model, JavaSettings settings) { + // JSON merge patch is only supported for stream style serialization. + return settings.isStreamStyleSerialization() + && model.getImplementationDetails() != null && model.getImplementationDetails().getUsages() != null + && model.getImplementationDetails().getUsages().contains(ImplementationDetails.Usage.JSON_MERGE_PATCH); + } + + /** + * Gets all parent properties. + * + * @param model The client model. + * @return Returns all properties that are defined by super types of the client model. + */ + public static List getParentProperties(ClientModel model) { + return getParentProperties(model, true); + } + + /** + * Gets all parent properties. + * + * @param model The client model. + * @param parentPropertiesFirst whether parent properties are in the front of the return list + * @return Returns all properties that are defined by super types of the client model. + */ + public static List getParentProperties(ClientModel model, boolean parentPropertiesFirst) { + String lastParentName = model.getName(); + ClientModel parentModel = getClientModel(model.getParentModelName()); + List parentProperties = new ArrayList<>(); + while (parentModel != null && !lastParentName.equals(parentModel.getName())) { + // Add the properties in inverse order as they be reverse at the end. + List parentProps = new ArrayList<>(parentModel.getProperties()); + for (int i = parentProps.size() - 1; i >= 0; i--) { + parentProperties.add(parentProps.get(i)); + } + + lastParentName = parentModel.getName(); + parentModel = getClientModel(parentModel.getParentModelName()); + } + if (parentPropertiesFirst) { + Collections.reverse(parentProperties); + } + return parentProperties; + } + + public static List getRequiredWritableParentProperties(ClientModel model) { + String lastParentName = model.getName(); + ClientModel parentModel = getClientModel(model.getParentModelName()); + List requiredParentProperties = new ArrayList<>(); + while (parentModel != null && !lastParentName.equals(parentModel.getName())) { + // Add the properties in inverse order as they be reverse at the end. + List ctorArgs = parentModel.getProperties().stream() + .filter(property -> property.isRequired() && !property.isConstant() && !property.isReadOnly()) + .collect(Collectors.toList()); + + for (int i = ctorArgs.size() - 1; i >= 0; i--) { + requiredParentProperties.add(ctorArgs.get(i)); + } + + lastParentName = parentModel.getName(); + parentModel = getClientModel(parentModel.getParentModelName()); + } + Collections.reverse(requiredParentProperties); + return requiredParentProperties; + } + + /** + * Gets all the properties that parent models define that are part of the constructor. + *

+ * This uses {@link ClientModelUtil#includePropertyInConstructor(ClientModelProperty, JavaSettings)} to determine + * which properties should be included in the constructor. + * + * @param model The client model. + * @param settings Autorest generation settings. + * @return All properties that are defined by super types of the client model that should be included in the + * constructor. + */ + public static List getParentConstructorProperties(ClientModel model, JavaSettings settings) { + String lastParentName = model.getName(); + ClientModel parentModel = getClientModel(model.getParentModelName()); + Set constructorProperties = new LinkedHashSet<>(); + while (parentModel != null && !lastParentName.equals(parentModel.getName())) { + // Add the properties in inverse order as they be reverse at the end. + List parentProperties = parentModel.getProperties(); + for (int i = parentProperties.size() - 1; i >= 0; i--) { + ClientModelProperty property = parentProperties.get(i); + if (includePropertyInConstructor(property, settings)) { + constructorProperties.add(property); + } + } + + lastParentName = parentModel.getName(); + parentModel = getClientModel(parentModel.getParentModelName()); + } + + List propertyList = new ArrayList<>(constructorProperties); + Collections.reverse(propertyList); + return propertyList; + } + + /** + * Whether the property needs public setter. + * + * @param property The client model property, or a reference. + * @param settings Autorest generation settings. + * @return whether the property will have a setter method. + */ + public static boolean needsPublicSetter(ClientModelPropertyAccess property, JavaSettings settings) { + return !isReadOnlyOrInConstructor(property, settings) && !isFlattenedProperty(property); + } + + private static boolean isReadOnlyOrInConstructor(ClientModelPropertyAccess property, JavaSettings settings) { + return property.isReadOnly() || (settings.isRequiredFieldsAsConstructorArgs() && property.isRequired()); + } + + private static boolean isFlattenedProperty(ClientModelPropertyAccess property) { + return (property instanceof ClientModelProperty) && ((ClientModelProperty) property).getClientFlatten(); + } + + /** + * Determines whether the {@link ClientModelProperty} should be included in the model's constructor. + *

+ * The {@code property} is included in the constructor if it is {@link ClientModelProperty#isRequired()}, + * {@link JavaSettings#isRequiredFieldsAsConstructorArgs()} is true, and either the property is not + * {@link ClientModelProperty#isReadOnly()}, is {@link ClientModelProperty#isPolymorphicDiscriminator()}, or + * {@link JavaSettings#isIncludeReadOnlyInConstructorArgs()} is true. + * + * @param property The {@link ClientModelProperty} + * @param settings The Autorest generation settings. + * @return Whether the {@code property} should be included in the model's constructor. + */ + public static boolean includePropertyInConstructor(ClientModelProperty property, JavaSettings settings) { + // First, the property must be required and the setting to include required fields as constructor args must be + // enabled. + boolean requiredAndIncluded = property.isRequired() && settings.isRequiredFieldsAsConstructorArgs(); + + // Then, one of the property not being read-only or the setting to include read-only properties in constructor + // args is enabled must be true. + boolean notReadOnlyOrIncludeReadOnly = !property.isReadOnly() || settings.isIncludeReadOnlyInConstructorArgs(); + + // Polymorphic discriminators are a special case as they are their own concept within codegen but there can be + // cases where the same property is defined as the polymorphic discriminator and a property in the class. Only + // when the property defined in the class is required will the polymorphic discriminator be considered needed in + // the constructor. + boolean polymorphicDiscriminatorIsRequired = property.isPolymorphicDiscriminator() && property.isRequired(); + + return requiredAndIncluded && (notReadOnlyOrIncludeReadOnly || polymorphicDiscriminatorIsRequired); + } + + /** + * Checks whether wire type and client type mismatch on this client model property. + * + * @param clientModelProperty the client model property. + * @param ignoreGenericType whether to ignore the mismatch, if both wire type and client type is generic type. + * For example, ignore the case of {@code List} vs {@code List}. + * @return whether wire type and client type mismatch. + */ + public static boolean isWireTypeMismatch(ClientModelProperty clientModelProperty, boolean ignoreGenericType) { + if (clientModelProperty.getClientType() == clientModelProperty.getWireType()) { + // same type + return false; + } else { + // type mismatch + if (ignoreGenericType + && clientModelProperty.getClientType() instanceof GenericType + && clientModelProperty.getWireType() instanceof GenericType) { + // at present, ignore generic type, as type erasure causes conflict of 2 constructors + return false; + } else { + return true; + } + } + } + + /** + * Gets a mapping of XML namespace to constant name for XML namespaces used in the model. + * + * @param model The model to get the XML namespaces from. + * @return A mapping of XML namespace to constant name. + */ + public static Map xmlNamespaceToConstantMapping(ClientModel model) { + Map xmlNamespaceConstantMap = new LinkedHashMap<>(); + ClientModel rootModel = getRootParent(model); + if (rootModel.getXmlNamespace() != null) { + xmlNamespaceConstantMap.put(rootModel.getXmlNamespace(), xmlNamespaceToConstant(rootModel.getXmlNamespace())); + } + + for (ClientModelProperty property : ClientModelUtil.getParentProperties(model)) { + if (property.getXmlNamespace() != null) { + xmlNamespaceConstantMap.put(property.getXmlNamespace(), + xmlNamespaceToConstant(property.getXmlNamespace())); + } + } + + for (ClientModelProperty property : model.getProperties()) { + if (property.getXmlNamespace() != null) { + xmlNamespaceConstantMap.put(property.getXmlNamespace(), + xmlNamespaceToConstant(property.getXmlNamespace())); + } + } + + return xmlNamespaceConstantMap; + } + + private static String xmlNamespaceToConstant(String xmlNamespace) { + URI uri = URI.create(xmlNamespace); + String host = CodeNamer.getEnumMemberName(uri.getHost()); + String path = uri.getPath(); + String[] segments = path.split("/"); + + // XML namespaces are URIs, use the host and the last two segments of the path as the constant. + if (segments.length >= 2) { + // More than two path segments, use HOST_SEGMENT[COUNT - 2]_SEGMENT[COUNT - 1] + return host + "_" + CodeNamer.getEnumMemberName(segments[segments.length - 2]) + "_" + + CodeNamer.getEnumMemberName(segments[segments.length - 1]); + } else if (segments.length == 1) { + // Only one path segment, use HOST_SEGMENT[0] + return host + "_" + CodeNamer.getEnumMemberName(segments[0]); + } else { + // No path segments, just use HOST + return host; + } + } + + /** + * Gets the root parent of the given model. + *

+ * If the model isn't polymorphic or is the root parent the passed model will be returned. + * + * @param model The model to get the root parent of. + * @return The root parent of the given model, or the model itself if it's either not polymorphic or the root + * parent. + */ + public static ClientModel getRootParent(ClientModel model) { + if (!model.isPolymorphic()) { + return model; + } + + while (model.getParentModelName() != null) { + model = getClientModel(model.getParentModelName()); + } + + return model; + } + + public static Set getExternalPackageNamesUsedInClient(List models, CodeModel codeModel) { + // models + Set externalPackageNames = models == null ? new HashSet<>() : models.stream() + .filter(m -> m.getImplementationDetails() != null && m.getImplementationDetails().getUsages() != null + && m.getImplementationDetails().getUsages().contains(ImplementationDetails.Usage.EXTERNAL)) + .map(ClientModel::getPackage) + .collect(Collectors.toSet()); + + // LongRunningMetadata in methods + if (!CoreUtils.isNullOrEmpty(codeModel.getClients())) { + for (Client client : codeModel.getClients()) { + if (!CoreUtils.isNullOrEmpty(client.getOperationGroups())) { + for (OperationGroup og : client.getOperationGroups()) { + if (!CoreUtils.isNullOrEmpty(og.getOperations())) { + externalPackageNames.addAll(og.getOperations().stream() + .filter(o -> o.getLroMetadata() != null && o.getLroMetadata().getPollingStrategy() != null && o.getLroMetadata().getPollingStrategy().getLanguage() != null && o.getLroMetadata().getPollingStrategy().getLanguage().getJava() != null) + .map(o -> o.getLroMetadata().getPollingStrategy().getLanguage().getJava().getNamespace()) + .filter(Objects::nonNull) + .collect(Collectors.toSet())); + } + } + } + } + } + + return externalPackageNames; + } + + public static boolean requireOperationLocationPollingStrategy(CodeModel codeModel) { + if (!CoreUtils.isNullOrEmpty(codeModel.getClients())) { + for (Client client : codeModel.getClients()) { + if (!CoreUtils.isNullOrEmpty(client.getOperationGroups())) { + for (OperationGroup og : client.getOperationGroups()) { + if (!CoreUtils.isNullOrEmpty(og.getOperations())) { + for (Operation operation : og.getOperations()) { + if (operation.getLroMetadata() != null && operation.getLroMetadata().getPollingStrategy() != null) { + if (OPERATION_LOCATION_POLLING_STRATEGY.equals(operation.getLroMetadata().getPollingStrategy().getLanguage().getJava().getName())) { + return true; + } + } + } + } + } + } + } + } + return false; + } + + public static boolean isMultipartModel(ClientModel model) { + return model.getSerializationFormats().contains(KnownMediaType.MULTIPART.value()); + } + + private static boolean hasNoUsage(ClientModel model) { + ImplementationDetails details = model.getImplementationDetails(); + return details == null || CoreUtils.isNullOrEmpty(details.getUsages()); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/util/CodeNamer.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/util/CodeNamer.java new file mode 100644 index 0000000000..9f80a1378a --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/util/CodeNamer.java @@ -0,0 +1,192 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.util; + +import org.atteo.evo.inflector.English; + +import java.util.Set; +import java.util.function.Function; +import java.util.regex.Pattern; + +import static com.microsoft.typespec.http.client.generator.core.preprocessor.namer.CodeNamer.getBasicLatinCharacter; + +public class CodeNamer { + + private static NamerFactory factory = new DefaultNamerFactory(); + + private static final Pattern MERGE_UNDERSCORES = Pattern.compile("_{2,}"); + private static final Pattern CHARACTERS_TO_REPLACE_WITH_UNDERSCORE = Pattern.compile("[\\\\/.+ -]+"); + + public static void setFactory(NamerFactory templateFactory) { + factory = templateFactory; + } + + public static ModelNamer getModelNamer() { + return factory.getModelNamer(); + } + + private CodeNamer() { + } + + public static String toCamelCase(String name) { + return com.microsoft.typespec.http.client.generator.core.preprocessor.namer.CodeNamer.toCamelCase(name); + } + + public static String toPascalCase(String name) { + return com.microsoft.typespec.http.client.generator.core.preprocessor.namer.CodeNamer.toPascalCase(name); + } + + public static String escapeXmlComment(String comment) { + return com.microsoft.typespec.http.client.generator.core.preprocessor.namer.CodeNamer.escapeXmlComment(comment); + } + + public static String escapeComment(String comment) { + if (comment == null || comment.isEmpty()) { + return comment; + } + + StringBuilder sb = null; + int prevStart = 0; + int commentLength = comment.length(); + int replacementIndex; + + while ((replacementIndex = comment.indexOf("*/", prevStart)) != -1) { + if (sb == null) { + // Add enough overhead to account for 1/8 of the string to be replaced. + sb = new StringBuilder(commentLength + 3 * (commentLength / 8)); + } + + sb.append(comment, prevStart, replacementIndex); + sb.append("*/"); + prevStart = replacementIndex + 2; + } + + if (sb == null) { + return comment; + } + + sb.append(comment, prevStart, commentLength); + return sb.toString(); + } + + public static String removeInvalidCharacters(String name) { + return com.microsoft.typespec.http.client.generator.core.preprocessor.namer.CodeNamer.getValidName(name, c -> c == '_' || c == '-'); + } + + public static String getPropertyName(String name) { + if (name == null || name.trim().isEmpty()) { + return name; + } + return com.microsoft.typespec.http.client.generator.core.preprocessor.namer.CodeNamer.getEscapedReservedName( + toCamelCase(removeInvalidCharacters(name)), "Property"); + } + + public static String getPlural(String name) { + if (name != null && !name.isEmpty() && !name.endsWith("s") && !name.endsWith("S")) { + name = English.plural(name); + } + return name; + } + + public static String getEnumMemberName(String name) { + if (name == null || name.trim().isEmpty()) { + return name; + } + + // trim leading and trailing '_' + if ((name.startsWith("_") || name.endsWith("_")) && !name.chars().allMatch(c -> c == '_')) { + StringBuilder sb = new StringBuilder(name); + while (sb.length() > 0 && sb.charAt(0) == '_') { + sb.deleteCharAt(0); + } + while (sb.length() > 0 && sb.charAt(sb.length() - 1) == '_') { + sb.setLength(sb.length() - 1); + } + name = sb.toString(); + } + + String result = removeInvalidCharacters(CHARACTERS_TO_REPLACE_WITH_UNDERSCORE.matcher(name).replaceAll("_")); + result = MERGE_UNDERSCORES.matcher(result).replaceAll("_"); // merge multiple underlines + Function isUpper = c -> c >= 'A' && c <= 'Z'; + Function isLower = c -> c >= 'a' && c <= 'z'; + for (int i = 1; i < result.length() - 1; i++) { + if (isUpper.apply(result.charAt(i))) { + if (result.charAt(i - 1) != '_' && isLower.apply(result.charAt(i - 1))) { + result = result.substring(0, i) + "_" + result.substring(i); + } + } + } + + if (result.startsWith("_") || result.endsWith("_")) { + if (!result.chars().allMatch(c -> c == (int) '_')) { + // some char is not '_', trim it + + StringBuilder sb = new StringBuilder(result); + while (sb.length() > 0 && sb.charAt(0) == '_') { + sb.deleteCharAt(0); + } + while (sb.length() > 0 && sb.charAt(sb.length() - 1) == '_') { + sb.setLength(sb.length() - 1); + } + result = sb.toString(); + } else { + // all char is '_', then transform some '_' to + String basicLatinCharacterReplacement = getBasicLatinCharacter(name.charAt(0)); + if (result.startsWith("_") && basicLatinCharacterReplacement != null) { + result = basicLatinCharacterReplacement + result.substring(1); + + basicLatinCharacterReplacement = getBasicLatinCharacter(name.charAt(name.length() - 1)); + if (result.endsWith("_") && basicLatinCharacterReplacement != null) { + result = result.substring(0, result.length() - 1) + basicLatinCharacterReplacement; + } + } + } + } + + return result.toUpperCase(); + } + + private static final Set RESERVED_CLIENT_METHOD_PARAMETER_NAME = Set.of( + "service", // the ServiceInterface local variable + "client" // the ManagementClient local variable + ); + + public static String getEscapedReservedClientMethodParameterName(String name) { + if (RESERVED_CLIENT_METHOD_PARAMETER_NAME.contains(name)) { + name += "Param"; + } + return name; + } + + public static String removeSpaceCharacters(String str) { + if (str == null || str.isEmpty()) { + return str; + } + + StringBuilder sb = null; + int prevStart = 0; + int strLength = str.length(); + + for (int i = 0; i < strLength; i++) { + if (Character.isWhitespace(str.charAt(i))) { + if (sb == null) { + sb = new StringBuilder(strLength); + } + + if (prevStart != i) { + sb.append(str, prevStart, i); + } + + prevStart = i + 1; + } + } + + if (sb == null) { + return str; + } + + sb.append(str, prevStart, strLength); + return sb.toString(); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/util/DefaultNamerFactory.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/util/DefaultNamerFactory.java new file mode 100644 index 0000000000..987df748f9 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/util/DefaultNamerFactory.java @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.util; + +public class DefaultNamerFactory implements NamerFactory { + + private static final ModelNamer MODEL_NAMER = new ModelNamer(); + + @Override + public ModelNamer getModelNamer() { + return MODEL_NAMER; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/util/MethodNamer.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/util/MethodNamer.java new file mode 100644 index 0000000000..e46ca8665c --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/util/MethodNamer.java @@ -0,0 +1,91 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.util; + +public class MethodNamer { + + public static String getPagingAsyncSinglePageMethodName(String baseName) { + return baseName + "SinglePageAsync"; + } + + public static String getPagingSinglePageMethodName(String baseName) { + return baseName + "SinglePage"; + } + + public static String getSimpleAsyncMethodName(String baseName) { + return baseName + "Async"; + } + + public static String getSimpleAsyncRestResponseMethodName(String baseName) { + return baseName + "WithResponseAsync"; + } + + public static String getSimpleRestResponseMethodName(String baseName) { + return baseName + "WithResponse"; + } + + public static String getLroBeginAsyncMethodName(String baseName) { + return getLroBeginAsyncMethodNameInternal(CodeNamer.toPascalCase(baseName)); + } + + private static String getLroBeginAsyncMethodNameInternal(String formattedName) { + return "begin" + formattedName + "Async"; + } + + public static String getLroBeginMethodName(String baseName) { + return getLroBeginMethodNameInternal(CodeNamer.toPascalCase(baseName)); + } + + private static String getLroBeginMethodNameInternal(String formattedName) { + return "begin" + formattedName; + } + + private final String baseName; + private final String pascalName; + + public MethodNamer(String baseName) { + this.baseName = baseName; + this.pascalName = CodeNamer.toPascalCase(baseName); + } + + public String getMethodName() { + return baseName; + } + + public String getPagingAsyncSinglePageMethodName() { + return getPagingAsyncSinglePageMethodName(this.getMethodName()); + } + + public String getPagingSinglePageMethodName() { + return getPagingSinglePageMethodName(this.getMethodName()); + } + + public String getSimpleAsyncMethodName() { + return getSimpleAsyncMethodName(this.getMethodName()); + } + + public String getSimpleAsyncRestResponseMethodName() { + return getSimpleAsyncRestResponseMethodName(this.getMethodName()); + } + + public String getSimpleRestResponseMethodName() { + return getSimpleRestResponseMethodName(this.getMethodName()); + } + + public String getLroBeginAsyncMethodName() { + return getLroBeginAsyncMethodNameInternal(pascalName); + } + + public String getLroBeginMethodName() { + return getLroBeginMethodNameInternal(pascalName); + } + + public String getLroModelBeginMethodName() { + return "begin" + pascalName + "WithModel"; + } + + public String getLroModelBeginAsyncMethodName() { + return "begin" + pascalName + "WithModelAsync"; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/util/MethodUtil.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/util/MethodUtil.java new file mode 100644 index 0000000000..575995722e --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/util/MethodUtil.java @@ -0,0 +1,380 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.util; + +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.BinarySchema; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.ChoiceValue; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.KnownMediaType; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Language; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Languages; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Operation; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Parameter; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Protocol; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Protocols; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Request; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.RequestParameterLocation; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Schema; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.SealedChoiceSchema; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.StringSchema; +import com.microsoft.typespec.http.client.generator.core.mapper.Mappers; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClassType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientEnumValue; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethod; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.EnumType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.IType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ProxyMethod; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ProxyMethodParameter; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.examplemodel.MethodParameter; +import com.azure.core.http.HttpMethod; +import com.azure.core.util.CoreUtils; + +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; + +public class MethodUtil { + + public static final String REPEATABILITY_REQUEST_ID_HEADER = "repeatability-request-id"; + public static final String REPEATABILITY_FIRST_SENT_HEADER = "repeatability-first-sent"; + public static final String REPEATABILITY_REQUEST_ID_VARIABLE_NAME = CodeNamer.toCamelCase(REPEATABILITY_REQUEST_ID_HEADER); + public static final String REPEATABILITY_FIRST_SENT_VARIABLE_NAME = CodeNamer.toCamelCase(REPEATABILITY_FIRST_SENT_HEADER); + public static final String REPEATABILITY_REQUEST_ID_EXPRESSION = "CoreUtils.randomUuid().toString()"; + public static final String REPEATABILITY_FIRST_SENT_EXPRESSION = "DateTimeRfc1123.toRfc1123String(OffsetDateTime.now())"; + + public static final String CONTENT_TYPE_APPLICATION_JSON_ERROR_WEIGHT = "application/json;q=0.9"; + + private static final Set REPEATABILITY_REQUEST_HTTP_METHODS + = EnumSet.of(HttpMethod.PUT, HttpMethod.PATCH, HttpMethod.DELETE, HttpMethod.POST); + + /** + * Checks that method include special headers for Repeatable Requests Version 1.0 ("repeatability-request-id") + * @param proxyMethod the proxy method + * @return whether method include special headers for Repeatable Requests Version 1.0 + */ + public static boolean isMethodIncludeRepeatableRequestHeaders(ProxyMethod proxyMethod) { + // Repeatable Requests Version 1.0 + // https://docs.oasis-open.org/odata/repeatable-requests/v1.0/cs01/repeatable-requests-v1.0-cs01.html + if (proxyMethod == null || CoreUtils.isNullOrEmpty(proxyMethod.getSpecialHeaders())) { + return false; + } + + // check supported HTTP method + if (!isHttpMethodSupportRepeatableRequestHeaders(proxyMethod.getHttpMethod())) { + return false; + } + + // check "repeatability-request-id" exists + return proxyMethod.getSpecialHeaders().contains(REPEATABILITY_REQUEST_ID_HEADER); + } + + public static boolean isHttpMethodSupportRepeatableRequestHeaders(HttpMethod httpMethod) { + return REPEATABILITY_REQUEST_HTTP_METHODS.contains(httpMethod); + } + + /** + * Gets Javadoc description for method parameter. + * + * @param parameter the method parameter. + * @param name the name of method parameter, used when no description found. + * @param isProtocolMethod whether the description is used for simplified method. + * @return the Javadoc description for method parameter. + */ + public static String getMethodParameterDescription(Parameter parameter, String name, boolean isProtocolMethod) { + String summary = parameter.getSummary(); + String description = null; + // parameter description + if (parameter.getLanguage() != null) { + description = parameter.getLanguage().getDefault().getDescription(); + } + + String javadocDescription = SchemaUtil.mergeSummaryWithDescription(summary, description); + if (CoreUtils.isNullOrEmpty(javadocDescription)) { // fallback to dummy description only when both summary and description are empty + javadocDescription = "The " + name + " parameter"; + } + + // add allowed enum values + if (isProtocolMethod && parameter.getProtocol().getHttp().getIn() != RequestParameterLocation.BODY) { + javadocDescription = MethodUtil.appendAllowedEnumValuesForEnumType(parameter, javadocDescription); + } + + return javadocDescription; + } + + /** + * Gets the HttpMethod from operation. Returns null if not recognized. + * + * @param operation the operation. + * @return the HttpMethod from operation. null if not recognized. + */ + public static HttpMethod getHttpMethod(Operation operation) { + if (!CoreUtils.isNullOrEmpty(operation.getRequests()) + && operation.getRequests().get(0).getProtocol() != null + && operation.getRequests().get(0).getProtocol().getHttp() != null) { + try { + return HttpMethod.valueOf(operation.getRequests().get(0).getProtocol().getHttp().getMethod().toUpperCase(Locale.ROOT)); + } catch (IllegalArgumentException | NullPointerException e) { + return null; + } + } else { + return null; + } + } + + /** + * Find the first request consumes binary type, if no binary request, return the first request in requests. + * If the selected binary request does not have content-type parameter, we will add one for it. + * Update operation with the result requests list. + * + * @param requests a list of requests + * @return the first request consumes binary type, if no binary request, return the first request in requests + */ + public static Request tryMergeBinaryRequestsAndUpdateOperation(List requests, Operation operation) { + Request selectedRequest = requests.get(0); + for (Request request : requests) { + if (request.getProtocol().getHttp().getKnownMediaType() != null + && request.getProtocol().getHttp().getKnownMediaType().equals(KnownMediaType.BINARY)) { + // add contentType parameter + if (getContentTypeCount(requests) > 1 && !hasContentTypeParameter(request)) { + Parameter contentTypeParameter = createContentTypeParameter(request, operation); + request.getParameters().add(findIndexForContentTypeParam(request.getParameters()), contentTypeParameter); + if (contentTypeParameter.isRequired()) { + request.getSignatureParameters().add(findIndexForContentTypeParam(request.getSignatureParameters()), contentTypeParameter); + } + } + selectedRequest = request; + + operation.setRequests(new ArrayList<>()); + operation.getRequests().add(selectedRequest); + break; + } + } + return selectedRequest; + } + + /** + * @param request the request to put contentType on + * @param operation the operation + * @return the created content type parameter + */ + public static Parameter createContentTypeParameter(Request request, Operation operation) { + List requests = operation.getRequests(); + Parameter contentType = new Parameter(); + contentType.setOperation(operation); + contentType.setDescription("The content type"); + for (Parameter parameter : request.getParameters()) { + if (parameter.getSchema() instanceof BinarySchema) { + contentType.setRequired(parameter.isRequired()); + break; + } + } + contentType.setImplementation(Parameter.ImplementationLocation.METHOD); + + Language language = new Language(); + language.setName("ContentType"); + language.setDescription("The content type"); + SealedChoiceSchema sealedChoiceSchema = new SealedChoiceSchema(); + StringSchema stringSchema = new StringSchema(); + stringSchema.setType(Schema.AllSchemaTypes.STRING); + sealedChoiceSchema.setChoiceType(stringSchema); + sealedChoiceSchema.setChoices(getContentTypeChoiceValues(requests)); + sealedChoiceSchema.setType(Schema.AllSchemaTypes.fromValue("sealed-choice")); + sealedChoiceSchema.setLanguage(new Languages()); + sealedChoiceSchema.getLanguage().setDefault(language); + sealedChoiceSchema.getLanguage().setJava(language); + sealedChoiceSchema.setProtocol(new Protocols()); + + language = new Language(); + language.setName("contentType"); + language.setSerializedName("Content-Type"); + language.setDescription("The content type"); + contentType.setSchema(sealedChoiceSchema); + contentType.setImplementation(Parameter.ImplementationLocation.METHOD); + contentType.setProtocol(new Protocols()); + contentType.getProtocol().setHttp(new Protocol()); + contentType.getProtocol().getHttp().setIn(RequestParameterLocation.HEADER); + contentType.setLanguage(new Languages()); + contentType.getLanguage().setDefault(language); + contentType.getLanguage().setJava(language); + return contentType; + } + + /** + * @param requests a list of requests + * @return true if the requests have different content types, otherwise return false + */ + public static int getContentTypeCount(List requests) { + Set mediaTypes = new HashSet<>(); + for (Request request : requests) { + if (!CoreUtils.isNullOrEmpty(request.getProtocol().getHttp().getMediaTypes())) { + mediaTypes.addAll(request.getProtocol().getHttp().getMediaTypes()); + } + } + return mediaTypes.size(); + } + + /** + * If the parameter is not enum type, return the description directly, otherwise append the string of allowed values to the description + * @param parameter a parameter + * @param description parameter description + * @return the description that appends the string of allowed values for enum type parameter + */ + public static String appendAllowedEnumValuesForEnumType(Parameter parameter, String description) { + IType type = Mappers.getSchemaMapper().map(parameter.getSchema()); + if (parameter.getSchema() == null || !(type instanceof EnumType)) { + return description; + } + String res = description; + if (description.endsWith(".")) { + res += " Allowed values: "; + } else { + res += ". Allowed values: "; + } + EnumType enumType = (EnumType) type; + List choices = enumType.getValues(); + if (choices != null && !choices.isEmpty()) { + res += choices.stream().map(choice -> { + if (enumType.getElementType() == ClassType.STRING) { + return "\"" + choice.getValue() + "\""; + } else { + return choice.getValue(); + } + }).collect(Collectors.joining(", ")); + } + res += "."; + return res; + } + + /** + * Get a list of 1-1 pairs of proxy method parameter and client method parameter. + * + * @param clientMethod the client method + * @return the list of 1-1 pair of proxy method parameter and client method parameter + */ + public static List getParameters(ClientMethod clientMethod) { + return getParameters(clientMethod, false); + } + + /** + * Get a list of 1-1 pairs of proxy method parameter and client method parameter. + * + * @param allParameter whether to match non-required proxy method parameter + * @param clientMethod the client method + * @return the list of 1-1 pair of proxy method parameter and client method parameter + */ + public static List getParameters(ClientMethod clientMethod, boolean allParameter) { + List parameters = allParameter ? clientMethod.getProxyMethod().getAllParameters() + : clientMethod.getProxyMethod().getParameters(); + Map proxyMethodParameterByClientParameterName = parameters.stream() + .collect(Collectors.toMap(p -> CodeNamer.getEscapedReservedClientMethodParameterName(p.getName()), Function.identity())); + return clientMethod.getMethodInputParameters().stream() + .filter(p -> !p.isConstant() && !p.isFromClient()) + .map(p -> new MethodParameter(proxyMethodParameterByClientParameterName.get(p.getName()), p)) + .collect(Collectors.toList()); + } + + /** + * Checks if the parameter is "maxpagesize". + *

+ * It checks if the serialized name is "maxpagesize", or client name is "maxPageSize". + * + * @param parameter the parameter + * @return whether the parameter is "maxpagesize". + */ + public static boolean isMaxPageSizeParameter(Parameter parameter) { + return parameter.getProtocol() != null && parameter.getProtocol().getHttp() != null + // query parameter + && parameter.getProtocol().getHttp().getIn() == RequestParameterLocation.QUERY + // serialized name == maxpagesize, or relax a bit, client name == maxPageSize + && (Objects.equals(parameter.getLanguage().getDefault().getSerializedName(), "maxpagesize") || Objects.equals(SchemaUtil.getJavaName(parameter), "maxPageSize")); + } + + /** + * Finds the serialized name of "maxpagesize" parameter, if exists. + * + * @param proxyMethod the proxy method + * @return the serialized name of "maxpagesize" parameter, if exists. + */ + public static Optional serializedNameOfMaxPageSizeParameter(ProxyMethod proxyMethod) { + return proxyMethod.getAllParameters().stream() + .filter(p -> p.getRequestParameterLocation() == RequestParameterLocation.QUERY + && (Objects.equals(p.getRequestParameterName(), "maxpagesize") || Objects.equals(p.getName(), "maxPageSize"))) + .map(ProxyMethodParameter::getRequestParameterName) + .findFirst(); + } + + /** + * + * @param request the input request + * @return true if there is parameter in the request named "contentType", otherwise, return false + */ + private static boolean hasContentTypeParameter(Request request) { + for (Parameter parameter : request.getParameters()) { + if (parameter.getProtocol() != null && parameter.getProtocol().getHttp() != null + && RequestParameterLocation.HEADER == parameter.getProtocol().getHttp().getIn() + && parameter.getLanguage() != null && parameter.getLanguage().getJava() != null + && "Content-Type".equalsIgnoreCase(parameter.getLanguage().getJava().getSerializedName())) { + return true; + } + } + return false; + } + + /** + * @param parameters a list of parameters + * @return return the index of the BinarySchema parameter, if not found, return -1 + */ + private static int findIndexForContentTypeParam(List parameters) { + int binarySchemaBodyIndex = -1; + for (int i = 0; i < parameters.size(); ++i) { + if (parameters.get(i).getProtocol() != null && parameters.get(i).getProtocol().getHttp() != null + && RequestParameterLocation.HEADER == parameters.get(i).getProtocol().getHttp().getIn() + && parameters.get(i).getLanguage() != null && parameters.get(i).getLanguage().getJava() != null + && "Content-Length".equalsIgnoreCase(parameters.get(i).getLanguage().getJava().getSerializedName())) { + return i; + } + if (parameters.get(i).getSchema() instanceof BinarySchema) { + binarySchemaBodyIndex = i; + } + } + return binarySchemaBodyIndex; + } + + /** + * + * @param requests a list of requests + * @return the content type choices of the requests + */ + private static List getContentTypeChoiceValues(List requests) { + List choiceValues = new ArrayList<>(); + for (Request request : requests) { + if (!CoreUtils.isNullOrEmpty(request.getProtocol().getHttp().getMediaTypes())) { + for (String mediaType : request.getProtocol().getHttp().getMediaTypes()) { + ChoiceValue choiceValue = new ChoiceValue(); + choiceValue.setValue(mediaType); + + // TODO: language is never used, is this a mistake or should this be removed? + Language language = new Language(); + language.setName(mediaType.toUpperCase(Locale.ROOT)); + language.setDescription("Content Type " + mediaType); + choiceValues.add(choiceValue); + } + } + } + return choiceValues; + } + + public static boolean isContentTypeInRequest(Request request, String contentType) { + return request.getProtocol() != null && request.getProtocol().getHttp() != null + && request.getProtocol().getHttp().getMediaTypes() != null + && request.getProtocol().getHttp().getMediaTypes().contains(contentType); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/util/ModelExampleUtil.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/util/ModelExampleUtil.java new file mode 100644 index 0000000000..e9c45ff82c --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/util/ModelExampleUtil.java @@ -0,0 +1,460 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.util; + +import com.microsoft.typespec.http.client.generator.core.Javagen; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.RequestParameterLocation; +import com.microsoft.typespec.http.client.generator.core.extension.plugin.PluginLogger; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClassType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientModel; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientModelProperty; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.IType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ListType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.MapType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ModelProperty; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.PrimitiveType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ProxyMethodExample; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.examplemodel.BinaryDataNode; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.examplemodel.ClientModelNode; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.examplemodel.ExampleNode; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.examplemodel.ListNode; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.examplemodel.LiteralNode; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.examplemodel.MapNode; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.examplemodel.MethodParameter; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.examplemodel.ObjectNode; +import com.azure.core.util.Base64Url; +import com.azure.core.util.CoreUtils; +import com.azure.core.util.DateTimeRfc1123; +import com.azure.core.util.serializer.CollectionFormat; +import org.slf4j.Logger; + +import java.time.Instant; +import java.time.OffsetDateTime; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +public class ModelExampleUtil { + + private static final Logger LOGGER = new PluginLogger(Javagen.getPluginInstance(), ModelExampleUtil.class); + + /** + * Parse the type (client model or others) with JSON object to tree of ExampleNode. + * + * @param type the client type, wire type is assumed to be same + * @param objectValue the JSON object + * @return the tree of ExampleNode + */ + public static ExampleNode parseNode(IType type, Object objectValue) { + return parseNode(type, type, objectValue); + } + + /** + * Parse the type (client model or others) with JSON object to tree of ExampleNode. + * + * @param type the client type + * @param wireType the wire type (the related but different type used in JSON, e.g. DateTimeRfc1123 for OffsetDateTime) + * @param objectValue the JSON object + * @return the tree of ExampleNode + */ + @SuppressWarnings("unchecked") + public static ExampleNode parseNode(IType type, IType wireType, Object objectValue) { + ExampleNode node; + if (type instanceof ListType) { + IType elementType = ((ListType) type).getElementType(); + if (objectValue instanceof List) { + ListNode listNode = new ListNode(elementType, objectValue); + node = listNode; + + List elements = (List) objectValue; + for (Object childObjectValue : elements) { + ExampleNode childNode = parseNode(elementType, childObjectValue); + node.getChildNodes().add(childNode); + } + } else { + LOGGER.error("Example value is not List type: {}", objectValue); + node = new ListNode(elementType, null); + } + } else if (type instanceof MapType) { + IType elementType = ((MapType) type).getValueType(); + if (objectValue instanceof Map) { + MapNode mapNode = new MapNode(elementType, objectValue); + node = mapNode; + + Map dict = (Map) objectValue; + for (Map.Entry entry : dict.entrySet()) { + Object value = entry.getValue(); + + // redact possible credential + if (elementType == ClassType.STRING && entry.getValue() instanceof String) { + value = ModelTestCaseUtil.redactStringValue(Collections.singletonList(entry.getKey()), (String) value); + } + + ExampleNode childNode = parseNode(elementType, value); + node.getChildNodes().add(childNode); + mapNode.getKeys().add(entry.getKey()); + } + } else { + LOGGER.error("Example value is not Map type: {}", objectValue); + node = new MapNode(elementType, null); + } + } else if (type == ClassType.OBJECT) { + node = new ObjectNode(type, objectValue); + } else if (type == ClassType.BINARY_DATA && objectValue != null) { + node = new BinaryDataNode(type, objectValue); + } else if (type instanceof ClassType && objectValue instanceof Map) { + ClientModel model = ClientModelUtil.getClientModel(((ClassType) type).getName()); + if (model != null) { + if (model.isPolymorphic()) { + // polymorphic, need to get the correct subclass from discriminator + String serializedName = model.getPolymorphicDiscriminatorName(); + List jsonPropertyNames = Collections.singletonList(serializedName); + if (model.getNeedsFlatten()) { + jsonPropertyNames = ClientModelUtil.splitFlattenedSerializedName(serializedName); + } + + Object childObjectValue = getChildObjectValue(jsonPropertyNames, objectValue); + if (childObjectValue instanceof String) { + String discriminatorValue = (String) childObjectValue; + ClientModel derivedModel = getDerivedModel(model, discriminatorValue); + if (derivedModel != null) { + // use the subclass + type = derivedModel.getType(); + model = derivedModel; + } else { + LOGGER.warn("Failed to find the subclass with discriminator value '{}'", discriminatorValue); + } + } else { + LOGGER.warn("Failed to find the sample value for discriminator property '{}'", serializedName); + } + } + + ClientModelNode clientModelNode = new ClientModelNode(type, objectValue).setClientModel(model); + node = clientModelNode; + + List modelProperties = getWritablePropertiesIncludeSuperclass(model); + for (ModelProperty modelProperty : modelProperties) { + List jsonPropertyNames = modelProperty.getSerializedNames(); + + Object childObjectValue = getChildObjectValue(jsonPropertyNames, objectValue); + if (childObjectValue != null) { + ExampleNode childNode = parseNode(modelProperty.getClientType(), modelProperty.getWireType(), childObjectValue); + node.getChildNodes().add(childNode); + clientModelNode.getClientModelProperties().put(childNode, modelProperty); + + // redact possible credential + if (childNode instanceof LiteralNode && childObjectValue instanceof String) { + LiteralNode literalChildNode = (LiteralNode) childNode; + if (literalChildNode.getClientType() == ClassType.STRING + && literalChildNode.getLiteralsValue() != null) { + literalChildNode.setLiteralsValue(ModelTestCaseUtil.redactStringValue(jsonPropertyNames, literalChildNode.getLiteralsValue())); + } + } + } + } + + // additional properties + ModelProperty additionalPropertiesProperty = getAdditionalPropertiesProperty(model); + if (additionalPropertiesProperty != null) { + // properties already defined in model + Set propertySerializedNames = modelProperties.stream() + .map(p -> p.getSerializedNames().iterator().next()) + .collect(Collectors.toSet()); + // the remaining properties in json + Map remainingValues = ((Map) objectValue).entrySet().stream() + .filter(e -> !propertySerializedNames.contains(e.getKey())) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + + ExampleNode childNode = parseNode(additionalPropertiesProperty.getClientType(), additionalPropertiesProperty.getWireType(), remainingValues); + node.getChildNodes().add(childNode); + clientModelNode.getClientModelProperties().put(childNode, additionalPropertiesProperty); + } + } else { + // e.g. do not throw exception, use defaultValueExpression + node = defaultNode(type, wireType, objectValue); + } + } else if (objectValue == null) { + node = null; + } else { + node = defaultNode(type, wireType, objectValue); + } + return node; + } + + /** + * Default String literal node. + * Generated example will be the type's defaultValueExpression. + * + * @param clientType the client type + * @param wireType the wire type + * @param exampleValue the example value + * @return string literal node + */ + private static ExampleNode defaultNode(IType clientType, IType wireType, Object exampleValue) { + ExampleNode node; + LiteralNode literalNode = new LiteralNode(clientType, exampleValue); + node = literalNode; + + if (exampleValue != null) { + String literalValue = convertLiteralToClientValue(wireType, exampleValue.toString()); + literalNode.setLiteralsValue(literalValue); + } + return node; + } + + /** + * Convert literal value in wire type, to literal value in client type + *

+ * date-time in RFC1123 to RFC3339 + * date-time in Unix epoch to RFC3339 + * bytes in base64URL to bytes in string + * + * @param wireType the wire type + * @param literalInWireType the literal value in wire type + * @return the literal value in client type + */ + public static String convertLiteralToClientValue(IType wireType, String literalInWireType) { + // see ClassType.convertToClientType and PrimitiveType.convertToClientType + String literalValue = literalInWireType; + if (wireType == ClassType.DATE_TIME_RFC_1123) { + literalValue = new DateTimeRfc1123(literalValue).getDateTime().toString(); + } else if (wireType == ClassType.BASE_64_URL) { + literalValue = new Base64Url(literalValue).toString(); + } else if (wireType == PrimitiveType.UNIX_TIME_LONG) { + literalValue = OffsetDateTime.from(Instant.ofEpochSecond(Long.parseLong(literalValue))).toString(); + } + return literalValue; + } + + @SuppressWarnings("unchecked") + public static Object getChildObjectValue(List jsonPropertyNames, Object objectValue) { + boolean found = true; + Object childObjectValue = objectValue; + // walk the sequence of serialized names + for (String name : jsonPropertyNames) { + if (name.isEmpty()) { + found = false; + break; + } + + if (childObjectValue instanceof Map) { + childObjectValue = ((Map) childObjectValue).get(name); + if (childObjectValue == null) { + found = false; + break; + } + } else { + found = false; + break; + } + } + return found ? childObjectValue : null; + } + + /** + * Parse method parameter (client model or others) to example node. + * @param example proxy method example + * @param methodParameter method parameter + * @return example node + */ + public static ExampleNode parseNodeFromParameter(ProxyMethodExample example, MethodParameter methodParameter) { + String serializedName = methodParameter.getSerializedName(); + if (serializedName == null && methodParameter.getProxyMethodParameter().getRequestParameterLocation() == RequestParameterLocation.BODY) { + serializedName = methodParameter.getProxyMethodParameter().getName(); + } + + Object exampleValue = getParameterExampleValue(example, serializedName, methodParameter.getProxyMethodParameter().getRequestParameterLocation()); + + ExampleNode node; + if (exampleValue == null) { + if (ClassType.CONTEXT.equals(methodParameter.getClientMethodParameter().getClientType())) { + node = new LiteralNode(ClassType.CONTEXT, "").setLiteralsValue(""); + } else { + node = new LiteralNode(methodParameter.getClientMethodParameter().getClientType(), null); + } + } else { + node = parseNodeFromMethodParameter(methodParameter, exampleValue); + } + return node; + } + + /** + * Get the example value for the parameter. + * + * @param example proxy method example + * @param serializedName parameter serialized name + * @param requestParameterLocation parameter location + * @return the example value for the parameter, null if not found + */ + public static Object getParameterExampleValue(ProxyMethodExample example, String serializedName, RequestParameterLocation requestParameterLocation) { + + ProxyMethodExample.ParameterValue parameterValue = findParameter(example, serializedName); + + if (parameterValue == null && requestParameterLocation == RequestParameterLocation.BODY) { + // special handling for body, as it does not have serializedName + String paramSuffix = "Param"; + if (serializedName.endsWith(paramSuffix)) { + // hack, remove Param, as it likely added by codegen to avoid naming conflict + serializedName = serializedName.substring(0, serializedName.length() - paramSuffix.length()); + if (!serializedName.isEmpty()) { + parameterValue = findParameter(example, serializedName); + } + } + + // fallback, "body" is commonly used in example JSON for request body + if (parameterValue == null && !"body".equalsIgnoreCase(serializedName)) { + serializedName = "body"; + parameterValue = findParameter(example, serializedName); + } + } + + Object exampleValue = parameterValue; + + if (parameterValue != null) { + exampleValue = requestParameterLocation == RequestParameterLocation.QUERY + ? parameterValue.getUnescapedQueryValue() + : parameterValue.getObjectValue(); + } + + return exampleValue; + } + + /** + * Find parameter example value from proxy method example by serialized parameter name. + * @param example proxy method example + * @param serializedName parameter serialized name + * @return example value for this parameter + */ + public static ProxyMethodExample.ParameterValue findParameter(ProxyMethodExample example, String serializedName) { + return example.getParameters().entrySet() + .stream().filter(p -> p.getKey().equalsIgnoreCase(serializedName)) + .map(Map.Entry::getValue) + .findFirst().orElse(null); + } + + private static ExampleNode parseNodeFromMethodParameter(MethodParameter methodParameter, Object objectValue) { + IType type = methodParameter.getClientMethodParameter().getClientType(); + IType wireType = methodParameter.getClientMethodParameter().getWireType(); + if (methodParameter.getProxyMethodParameter().getCollectionFormat() != null && type instanceof ListType && objectValue instanceof String) { + // handle parameter style + + IType elementType = ((ListType) type).getElementType(); + ListNode listNode = new ListNode(elementType, objectValue); + String value = (String) objectValue; + + CollectionFormat collectionFormat = methodParameter.getProxyMethodParameter().getCollectionFormat(); + String[] elements; + switch (collectionFormat) { + case CSV: + elements = value.split(",", -1); + break; + case SSV: + elements = value.split(" ", -1); + break; + case PIPES: + elements = value.split("\\|", -1); + break; + case TSV: + elements = value.split("\t", -1); + break; + default: + // TODO (weidxu): CollectionFormat.MULTI + elements = value.split(",", -1); + LOGGER.error("Parameter style '{}' is not supported, fallback to CSV", collectionFormat); + break; + } + for (String childObjectValue : elements) { + ExampleNode childNode = ModelExampleUtil.parseNode(elementType, childObjectValue); + listNode.getChildNodes().add(childNode); + } + return listNode; + } else { + return ModelExampleUtil.parseNode(type, wireType, objectValue); + } + } + + private static ModelProperty getAdditionalPropertiesProperty(ClientModel model) { + ModelProperty modelProperty = null; + ClientModelProperty property = model.getProperties().stream() + .filter(ClientModelProperty::isAdditionalProperties) + .findFirst().orElse(null); + if (property != null && property.getClientType() instanceof MapType) { + modelProperty = ModelProperty.ofClientModelProperty(property); + } + return modelProperty; + } + + private static List getWritablePropertiesIncludeSuperclass(ClientModel model) { + Map propertiesMap = new LinkedHashMap<>(); + List properties = new ArrayList<>(); + + List parentModels = new ArrayList<>(); + String parentModelName = model.getParentModelName(); + while (!CoreUtils.isNullOrEmpty(parentModelName)) { + ClientModel parentModel = ClientModelUtil.getClientModel(parentModelName); + if (parentModel != null) { + parentModels.add(parentModel); + } + parentModelName = parentModel == null ? null : parentModel.getParentModelName(); + } + + List> propertiesFromTypeAndParents = new ArrayList<>(); + propertiesFromTypeAndParents.add(new ArrayList<>()); + model.getAccessibleProperties().forEach(p -> { + ModelProperty modelProperty = ModelProperty.ofClientModelProperty(p); + if (propertiesMap.putIfAbsent(modelProperty.getName(), modelProperty) == null) { + propertiesFromTypeAndParents.get(propertiesFromTypeAndParents.size() - 1).add(modelProperty); + } + }); + + for (ClientModel parent : parentModels) { + propertiesFromTypeAndParents.add(new ArrayList<>()); + + parent.getAccessibleProperties().forEach(p -> { + ModelProperty modelProperty = ModelProperty.ofClientModelProperty(p); + if (propertiesMap.putIfAbsent(modelProperty.getName(), modelProperty) == null) { + propertiesFromTypeAndParents.get(propertiesFromTypeAndParents.size() - 1).add(modelProperty); + } + }); + } + + Collections.reverse(propertiesFromTypeAndParents); + for (List properties1 : propertiesFromTypeAndParents) { + properties.addAll(properties1); + } + + return properties.stream() + .filter(p -> !p.isReadOnly() && !p.isConstant()) + .collect(Collectors.toList()); + } + + private static ClientModel getDerivedModel(ClientModel model, String discriminatorValue) { + if (discriminatorValue.equals(model.getSerializedName())) { + return model; + } + + // depth first search + if (model.getDerivedModels() != null) { + for (ClientModel childModel : model.getDerivedModels()) { + if (discriminatorValue.equalsIgnoreCase(childModel.getSerializedName())) { + // found + return childModel; + } else if (childModel.getDerivedModels() != null) { + // recursive + ClientModel childModel2 = getDerivedModel(childModel, discriminatorValue); + if (childModel2 != null) { + return childModel2; + } + } + } + } + + // not found + return null; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/util/ModelNamer.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/util/ModelNamer.java new file mode 100644 index 0000000000..1255faa9f3 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/util/ModelNamer.java @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.util; + +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClassType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientModelProperty; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.IType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.PrimitiveType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ServiceClientProperty; + +public class ModelNamer { + + public String modelPropertyGetterName(ServiceClientProperty property) { + return modelPropertyGetterName(property.getType(), property.getName()); + } + + public String modelPropertyGetterName(ClientModelProperty property) { + return modelPropertyGetterName(property.getClientType(), property.getName()); + } + + public String modelPropertyGetterName(IType clientType, String propertyName) { + String prefix = "get"; + if (clientType == PrimitiveType.BOOLEAN || clientType == ClassType.BOOLEAN) { + prefix = "is"; + if (CodeNamer.toCamelCase(propertyName).startsWith(prefix)) { + return CodeNamer.toCamelCase(propertyName); + } + } + return prefix + CodeNamer.toPascalCase(propertyName); + } + + public String modelPropertyGetterName(String propertyName) { + return "get" + CodeNamer.toPascalCase(propertyName); + } + + public String modelPropertySetterName(ClientModelProperty property) { + return "set" + CodeNamer.toPascalCase(property.getName()); + } + + public String modelPropertySetterName(String propertyName) { + return "set" + CodeNamer.toPascalCase(propertyName); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/util/ModelTestCaseUtil.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/util/ModelTestCaseUtil.java new file mode 100644 index 0000000000..95988fcc52 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/util/ModelTestCaseUtil.java @@ -0,0 +1,301 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.util; + +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClassType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientEnumValue; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientModel; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientModelProperty; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.EnumType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.IType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ListType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.MapType; +import com.azure.core.util.CoreUtils; +import com.azure.core.util.DateTimeRfc1123; + +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.time.OffsetDateTime; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Random; +import java.util.UUID; +import java.util.stream.Collectors; + +public class ModelTestCaseUtil { + + private static final class Configuration { + private final float nullableProbability = 0.0f; + + private final int maxDepth = 5; + + private final int maxStringLen = 16 + 1; + private final int maxList = 4 + 1; + private final int maxDict = 4 + 1; + } + + private static final Random RANDOM = new Random(3); + private static final Configuration CONFIGURATION = new Configuration(); + + /** + * Compose a random JSON object according to the structure of client model. + * + * @param model the client model + * @return the JSON object as Map + */ + public static Map jsonFromModel(ClientModel model) { + return jsonFromModel(0, model); + } + + private static Map jsonFromModel(int depth, ClientModel model) { + Map jsonObject = new LinkedHashMap<>(); + + // polymorphism + if (model.isPolymorphic()) { + addForProperty(jsonObject, + model.getPolymorphicDiscriminatorName(), model.getNeedsFlatten(), + model.getSerializedName()); + } + + // class + for (ClientModelProperty property : model.getProperties()) { + if (!property.isPolymorphicDiscriminator()) { + addForProperty(depth, jsonObject, property, model.getNeedsFlatten()); + } + } + + // superclasses + String parentModelName = model.getParentModelName(); + while (!CoreUtils.isNullOrEmpty(parentModelName)) { + ClientModel parentModel = ClientModelUtil.getClientModel(parentModelName); + if (parentModel != null) { + for (ClientModelProperty property : parentModel.getProperties()) { + if (!property.isPolymorphicDiscriminator()) { + addForProperty(depth, jsonObject, property, parentModel.getNeedsFlatten()); + } + } + } + parentModelName = parentModel == null ? null : parentModel.getParentModelName(); + } + + return jsonObject; + } + + /** + * Compose a random JSON object according to the structure of client model. + * + * @param depth the current depth of the object from its root + * @param type the type + * @return the JSON object as Map + */ + public static Object jsonFromType(int depth, IType type) { + if (type.asNullable() == ClassType.INTEGER) { + return RANDOM.nextInt() & Integer.MAX_VALUE; + } else if (type.asNullable() == ClassType.LONG) { + return RANDOM.nextLong() & Long.MAX_VALUE; + } else if (type.asNullable() == ClassType.FLOAT) { + return RANDOM.nextFloat() * 100; + } else if (type.asNullable() == ClassType.DOUBLE) { + return RANDOM.nextDouble() * 100; + } else if (type.asNullable() == ClassType.BOOLEAN) { + return RANDOM.nextBoolean(); + } else if (type == ClassType.STRING) { + return randomString(); + } else if (type.asNullable() == ClassType.UNIX_TIME_LONG) { + return RANDOM.nextLong() & Long.MAX_VALUE; + } else if (type == ClassType.DATE_TIME) { + return randomDateTime().toString(); + } else if (type == ClassType.DATE_TIME_RFC_1123) { + return DateTimeRfc1123.toRfc1123String(randomDateTime()); + } else if (type == ClassType.DURATION) { + Duration duration = Duration.ZERO; + duration = duration.plusSeconds(RANDOM.nextInt(10 * 24 * 60 * 60)); + return duration.toString(); + } else if (type.asNullable() == ClassType.DURATION_LONG) { + return RANDOM.nextLong() & Long.MAX_VALUE; + } else if (type.asNullable() == ClassType.DURATION_DOUBLE) { + return Math.abs(RANDOM.nextDouble() * 10); + } else if (type == ClassType.UUID) { + return UUID.randomUUID().toString(); + } else if (type == ClassType.URL) { + return "http://example.org/" + URLEncoder.encode(randomString(), StandardCharsets.UTF_8); + } else if (type == ClassType.OBJECT) { + // unknown type, use a simple string + return "data" + randomString(); + } else if (type instanceof EnumType) { + IType elementType = ((EnumType) type).getElementType(); + List values = ((EnumType) type).getValues().stream().map(ClientEnumValue::getValue).collect(Collectors.toList()); + if (values.isEmpty()) { + // empty enum + return null; + } + int index = RANDOM.nextInt(values.size()); + String value = values.get(index); + if (elementType.asNullable() == ClassType.INTEGER) { + return Integer.valueOf(value); + } else if (elementType.asNullable() == ClassType.LONG) { + return Long.valueOf(value); + } else if (elementType.asNullable() == ClassType.FLOAT) { + return Float.valueOf(value); + } else if (elementType.asNullable() == ClassType.DOUBLE) { + return Double.valueOf(value); + } else if (elementType.asNullable() == ClassType.BOOLEAN) { + return Boolean.valueOf(value); + } else if (elementType == ClassType.STRING) { + return value; + } + } else if (type instanceof ListType) { + List list = new ArrayList<>(); + if (depth <= CONFIGURATION.maxDepth) { + IType elementType = ((ListType) type).getElementType(); + int count = RANDOM.nextInt(CONFIGURATION.maxList - 1) + 1; + for (int i = 0; i < count; ++i) { + Object element = jsonFromType(depth + 1, elementType); + if (element != null) { + list.add(element); + } + } + } // else abort + return list; + } else if (type instanceof MapType) { + Map map = new LinkedHashMap<>(); + if (depth <= CONFIGURATION.maxDepth) { + IType elementType = ((MapType) type).getValueType(); + int count = RANDOM.nextInt(CONFIGURATION.maxDict - 1) + 1; + for (int i = 0; i < count; ++i) { + Object element = jsonFromType(depth + 1, elementType); + if (element != null) { + map.put(randomString(), element); + } + } + } // else abort + return map; + } else if (type instanceof ClassType && type != ClassType.CONTEXT) { + ClientModel model = ClientModelUtil.getClientModel(((ClassType) type).getName()); + if (model != null) { + return jsonFromModel(depth + 1, model); + } + } + return null; + } + + + public static String redactStringValue(List serializedNames, String value) { + for (String keyName : serializedNames) { + String keyNameLower = keyName.toLowerCase(Locale.ROOT); + for (String key : POSSIBLE_CREDENTIAL_KEY) { + if (keyNameLower.contains(key)) { + value = "fakeTokenPlaceholder"; + break; + } + } + } + return value; + } + + private static void addForProperty(int depth, Map jsonObject, + ClientModelProperty property, boolean modelNeedsFlatten) { + final boolean maxDepthReached = depth > CONFIGURATION.maxDepth; + + Object value = null; + if (property.isConstant()) { + // TODO (weidxu): skip for now, as the property.getDefaultValue() is the code, not the raw data + //value = property.getDefaultValue(); + return; + } else { + if (property.isRequired() + // required property must be generated + // optional property only be generated when still have depth remains + // we assume here that there is no infinitely nested required properties + || (!maxDepthReached && RANDOM.nextFloat() > CONFIGURATION.nullableProbability)) { + value = jsonFromType(depth, property.getWireType()); + } + } + + addForProperty(jsonObject, + property.getSerializedName(), modelNeedsFlatten || property.getNeedsFlatten(), + value); + } + + private static void addForProperty(Map jsonObject, + String serializedName, boolean modelNeedsFlatten, + Object value) { + if (value != null) { + List serializedNames; + if (modelNeedsFlatten) { + serializedNames = ClientModelUtil.splitFlattenedSerializedName(serializedName); + } else { + serializedNames = Collections.singletonList(serializedName); + } + addToJsonObject(jsonObject, serializedNames, value); + } + } + + @SuppressWarnings("unchecked") + private static void addToJsonObject(Map jsonObject, List serializedNames, Object value) { + checkCredential(serializedNames); + + if (serializedNames.size() == 1) { + jsonObject.put(serializedNames.iterator().next(), value); + } else { + serializedNames = new ArrayList<>(serializedNames); + String serializedName = serializedNames.iterator().next(); + serializedNames.remove(0); + if (jsonObject.containsKey(serializedName)) { + Object nextJsonObject = jsonObject.get(serializedName); + if (nextJsonObject instanceof Map) { + addToJsonObject((Map) nextJsonObject, serializedNames, value); + } + } else { + Map nextJsonObject = new LinkedHashMap<>(); + jsonObject.put(serializedName, nextJsonObject); + addToJsonObject(nextJsonObject, serializedNames, value); + } + } + } + + private static final List POSSIBLE_CREDENTIAL_KEY = Arrays.asList( + "key", + "code", + "credential", + "password", + "token", + "secret", + "authorization" + ); + + private static void checkCredential(List serializedNames) { + for (String keyName : serializedNames) { + String keyNameLower = keyName.toLowerCase(Locale.ROOT); + for (String key : POSSIBLE_CREDENTIAL_KEY) { + if (keyNameLower.contains(key)) { + throw new PossibleCredentialException(keyName); + } + } + } + } + + private static String randomString() { + int leftLimit = 97; // letter 'a' + int rightLimit = 122; // letter 'z' + int targetStringLength = RANDOM.nextInt(CONFIGURATION.maxStringLen - 1) + 1; + + return RANDOM.ints(leftLimit, rightLimit + 1) + .limit(targetStringLength) + .collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append) + .toString(); + } + + + private static final OffsetDateTime TIME = OffsetDateTime.parse("2020-12-20T00:00:00.000Z"); + private static OffsetDateTime randomDateTime() { + return TIME.plusSeconds(RANDOM.nextInt(356 * 24 * 60 * 60)); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/util/NamerFactory.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/util/NamerFactory.java new file mode 100644 index 0000000000..65466ea1d7 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/util/NamerFactory.java @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.util; + +public interface NamerFactory { + + ModelNamer getModelNamer(); +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/util/PossibleCredentialException.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/util/PossibleCredentialException.java new file mode 100644 index 0000000000..02b2e6b376 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/util/PossibleCredentialException.java @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.util; + +public class PossibleCredentialException extends RuntimeException { + + private final String keyName; + + public PossibleCredentialException(String keyName) { + super("Possible credential in value of key: " + keyName); + this.keyName = keyName; + } + + public String getKeyName() { + return keyName; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/util/ReturnTypeDescriptionAssembler.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/util/ReturnTypeDescriptionAssembler.java new file mode 100644 index 0000000000..e1b5ba7555 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/util/ReturnTypeDescriptionAssembler.java @@ -0,0 +1,108 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.util; + +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClassType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.GenericType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.IType; +import com.azure.core.http.rest.PagedFlux; +import com.azure.core.http.rest.PagedIterable; +import com.azure.core.http.rest.Response; +import com.azure.core.util.polling.PollerFlux; +import com.azure.core.util.polling.SyncPoller; +import reactor.core.publisher.Mono; + +public class ReturnTypeDescriptionAssembler { + + /** + * Assemble description for return types. + * @param description parsed swagger description of the returnType, either from operation description, or schema itself + * @param returnType actual returnType that needs documentation + * @param baseType baseType of the returnType + * @return assembled description + */ + public static String assemble(String description, IType returnType, IType baseType) { + return (returnType instanceof GenericType) + ? assembleForGeneric(description, (GenericType) returnType, baseType) + : description; + } + + private static String assembleForGeneric(String description, GenericType returnType, IType baseType) { + if (TypeUtil.isGenericTypeClassSubclassOf(returnType, Mono.class)) { + return assembleForMono(description, returnType, baseType); + } else if (TypeUtil.isGenericTypeClassSubclassOf(returnType, Response.class)) { + return assembleForResponse(description, returnType, baseType); + } else if (TypeUtil.isGenericTypeClassSubclassOf(returnType, PagedIterable.class, PagedFlux.class)) { + return assembleForPagination(description, returnType); + } else if (TypeUtil.isGenericTypeClassSubclassOf(returnType, SyncPoller.class, PollerFlux.class)) { + return assembleForPoller(description, returnType); + } + + return description; + } + + /* + Mono - A {@link Mono} that completes when a successful response is received + Mono> - "Response return type description" on successful completion of {@link Mono} + Mono - "something" on successful completion of {@link Mono} (something here is the description in the operation) + Mono - the response body on successful completion of {@link Mono} + */ + private static String assembleForMono(String description, GenericType returnType, IType baseType) { + if (TypeUtil.isGenericTypeClassSubclassOf(returnType.getTypeArguments()[0], Response.class)) { // Mono> + return assembleForResponse(description, (GenericType) returnType.getTypeArguments()[0], baseType) + + " on successful completion of {@link Mono}"; + } else { + if (description == null) { + if (ClassType.VOID == baseType.asNullable()) { // Mono + return "A {@link " + returnType.getName() + "} that completes when a successful response is received"; + } else { // Mono + return "the response body on successful completion of {@link " + returnType.getName() + "}"; + } + } else { // Mono + return description + " on successful completion of {@link " + returnType.getName() + "}"; + } + } + } + + /* + Response - the {@link Response} + Response - "something" along with {@link Response} + Response - the response body along with {@link Response} + */ + private static String assembleForResponse(String description, GenericType returnType, IType baseType) { + if (description == null) { + if (ClassType.VOID == baseType.asNullable()) { // Response + return "the {@link " + returnType.getName() + "}"; + } else { // Response + return "the response body along with {@link " + returnType.getName() + "}"; + } + } else { // Response + return description + " along with {@link " + returnType.getName() + "}"; + } + } + + /* + PagedIterable - "something" as paginated response with {@link PagedIterable} + PagedIterable - the paginated response with {@link PagedIterable} + */ + private static String assembleForPagination(String description, GenericType returnType) { + if (description == null) { + return "the paginated response with {@link " + returnType.getName() + "}"; + } else { // Response + return description + " as paginated response with {@link " + returnType.getName() + "}"; + } + } + + /* + SyncPoller - the {@link SyncPoller} for polling of "something" + SyncPoller - the {@link SyncPoller} for polling of long-running operation + */ + private static String assembleForPoller(String description, GenericType returnType) { + if (description == null) { + return "the {@link " + returnType.getName() + "} for polling of long-running operation"; + } else { // Response + return "the {@link " + returnType.getName() + "} for polling of " + description; + } + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/util/SchemaUtil.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/util/SchemaUtil.java new file mode 100644 index 0000000000..9719bfd99e --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/util/SchemaUtil.java @@ -0,0 +1,341 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.util; + +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.AnySchema; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Header; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.KnownMediaType; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Metadata; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.ObjectSchema; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Operation; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Property; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.RequestParameterLocation; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Response; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Schema; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.SchemaContext; +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; +import com.microsoft.typespec.http.client.generator.core.mapper.Mappers; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClassType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.EnumType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.IType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ImplementationDetails; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.IterableType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ListType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.PrimitiveType; +import com.azure.core.http.HttpMethod; +import com.azure.core.util.CoreUtils; + +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.Stack; +import java.util.stream.Collectors; + +public class SchemaUtil { + + private SchemaUtil() { + } + + public static Schema getLowestCommonParent(List schemas) { + return getLowestCommonParent(schemas.iterator()); + } + + public static Schema getLowestCommonParent(Iterator schemas) { + if (schemas == null || !schemas.hasNext()) { + return null; + } + + LinkedList chain = null; + while (schemas.hasNext()) { + Schema schema = schemas.next(); + if (chain == null) { + chain = new LinkedList<>(); + chain.addFirst(schema); + while (schema instanceof ObjectSchema + && ((ObjectSchema) schema).getParents() != null + && ((ObjectSchema) schema).getParents().getImmediate() != null + && !((ObjectSchema) schema).getParents().getImmediate().isEmpty()) { + // Assume always inheriting from an ObjectSchema and no multiple inheritance + schema = ((ObjectSchema) schema).getParents().getImmediate().get(0); + chain.addFirst(schema); + } + } else { + Stack newChain = new Stack<>(); + newChain.push(schema); + while (schema instanceof ObjectSchema + && ((ObjectSchema) schema).getParents() != null + && ((ObjectSchema) schema).getParents().getImmediate() != null + && !((ObjectSchema) schema).getParents().getImmediate().isEmpty()) { + // Assume always inheriting from an ObjectSchema and no multiple inheritance + schema = ((ObjectSchema) schema).getParents().getImmediate().get(0); + newChain.push(schema); + } + int i = 0; + while (!newChain.empty() && i < chain.size()) { + Schema top = chain.get(i); + Schema compare = newChain.pop(); + if (top == compare) { + i++; + } else { + for (; i < chain.size(); i++) { + chain.remove(i); + } + } + } + } + } + + return chain.isEmpty() ? new AnySchema() : chain.getLast(); + } + + /* + * Returns raw response type. + * In case of binary response: + * For DPG, returns BinaryData + * For vanilla/mgmt, returns InputStream + */ + public static IType getOperationResponseType(Operation operation, JavaSettings settings) { + Schema responseBodySchema = SchemaUtil.getLowestCommonParent( + operation.getResponses().stream().map(Response::getSchema).filter(Objects::nonNull).iterator()); + + return getOperationResponseType(responseBodySchema, operation, settings); + } + + /* + * Returns raw response type. + * In case of binary response: + * For DPG, returns BinaryData + * For vanilla/mgmt, returns InputStream + */ + public static IType getOperationResponseType(Schema responseBodySchema, Operation operation, + JavaSettings settings) { + IType responseBodyType = Mappers.getSchemaMapper().map(responseBodySchema); + + if (responseBodyType == null) { + if (operationIsHeadAsBoolean(operation)) { + // Azure core would internally convert the response status code to boolean. + responseBodyType = PrimitiveType.BOOLEAN; + } else if (containsBinaryResponse(operation)) { + if (settings.isDataPlaneClient() || !settings.isInputStreamForBinary()) { + responseBodyType = ClassType.BINARY_DATA; + } else { + responseBodyType = ClassType.INPUT_STREAM; + } + } else { + responseBodyType = PrimitiveType.VOID; + } + } + + return responseBodyType; + } + + public static Property getDiscriminatorProperty(ObjectSchema compositeType) { + Property discriminatorProperty = null; + if (compositeType.getDiscriminator() != null) { + discriminatorProperty = compositeType.getDiscriminator().getProperty(); + } else { + for (Schema parent : compositeType.getParents().getAll()) { + if (parent instanceof ObjectSchema && ((ObjectSchema) parent).getDiscriminator() != null) { + discriminatorProperty = ((ObjectSchema) parent).getDiscriminator().getProperty(); + break; + } + } + } + if (discriminatorProperty == null) { + throw new IllegalArgumentException(String.format("discriminator not found in type %s and its parents", + compositeType.getLanguage().getJava().getName())); + } + + return discriminatorProperty; + } + + public static String getDiscriminatorSerializedName(ObjectSchema compositeType) { + String discriminator = null; + if (compositeType.getDiscriminator() != null) { + discriminator = compositeType.getDiscriminator().getProperty().getSerializedName(); + } else if (compositeType.getDiscriminatorValue() != null) { + for (Schema parent : compositeType.getParents().getAll()) { + if (parent instanceof ObjectSchema && ((ObjectSchema) parent).getDiscriminator() != null) { + discriminator = ((ObjectSchema) parent).getDiscriminator().getProperty().getSerializedName(); + break; + } + } + } + if (discriminator == null) { + throw new IllegalArgumentException(String.format("discriminator not found in type %s and its parents", + compositeType.getLanguage().getJava().getName())); + } + return discriminator; + } + + /** + * Whether response contains header schemas. + *

+ * Response headers will be omitted, as polling method has return type as SyncPoller or PollerFlux, not Response. + * + * @param operation the operation + * @param settings the JavaSetting object + * @return whether response of the operation contains headers + */ + public static boolean responseContainsHeaderSchemas(Operation operation, JavaSettings settings) { + return operation.getResponses().stream() + .filter(r -> r.getProtocol() != null && r.getProtocol().getHttp() != null && r.getProtocol().getHttp().getHeaders() != null) + .flatMap(r -> r.getProtocol().getHttp().getHeaders().stream().map(Header::getSchema)) + .anyMatch(Objects::nonNull) + && operationIsNotFluentLRO(operation, settings) && operationIsNotDataPlaneLRO(operation, settings); + } + + /** + * Merge summary and description. + *

+ * If summary exists, it will take 1st line, and description will be moved to 2nd line in Javadoc. + * + * @param summary the summary text. + * @param description the description text. + * @return the merged text for Javadoc. + */ + public static String mergeSummaryWithDescription(String summary, String description) { + if (Objects.equals(summary, description)) { + summary = null; + } + + if (!CoreUtils.isNullOrEmpty(summary) && !CoreUtils.isNullOrEmpty(description)) { + return summary + "\n\n" + description; + } else if (!CoreUtils.isNullOrEmpty(summary)) { + return summary; + } else if (!CoreUtils.isNullOrEmpty(description)) { + return description; + } else { + return ""; + } + } + + public static IType removeModelFromParameter(RequestParameterLocation parameterRequestLocation, IType type) { + IType returnType = type; + if (parameterRequestLocation == RequestParameterLocation.BODY) { + returnType = ClassType.BINARY_DATA; + } else if (!(returnType instanceof PrimitiveType)) { + if (type instanceof EnumType) { + returnType = ClassType.STRING; + } + if (type instanceof IterableType && ((IterableType) type).getElementType() instanceof EnumType) { + returnType = new IterableType(ClassType.STRING); + } + if (type instanceof ListType && ((ListType) type).getElementType() instanceof EnumType) { + returnType = new ListType(ClassType.STRING); + } + } + return returnType; + } + + public static IType removeModelFromResponse(IType type, Operation operation) { + if (type.asNullable() != ClassType.VOID) { + if (!operationIsHeadAsBoolean(operation)) { + type = ClassType.BINARY_DATA; + } + } + return type; + } + + private static boolean operationIsHeadAsBoolean(Operation operation) { + return operation.getRequests().stream().anyMatch(req -> HttpMethod.HEAD.name().equalsIgnoreCase(req.getProtocol().getHttp().getMethod())) + && operation.getResponses().stream().anyMatch(r -> r.getProtocol().getHttp().getStatusCodes().contains("404")); + } + + /** + * Maps CADL model to model from external packages. + * + * @param compositeType the CADL model. + * @return the model from external packages, if available. + */ + public static ClassType mapExternalModel(ObjectSchema compositeType) { + // For now, the external packages is the azure-core + + ClassType classType = null; + if (compositeType.getLanguage() != null && compositeType.getLanguage().getDefault() != null) { + String namespace = compositeType.getLanguage().getDefault().getNamespace(); + String name = compositeType.getLanguage().getDefault().getName(); + + if (!CoreUtils.isNullOrEmpty(namespace) && !CoreUtils.isNullOrEmpty(name)) { + if (Objects.equals(namespace, "Azure.Core.Foundations")) { + // https://github.com/Azure/azure-sdk-for-java/blob/main/sdk/core/azure-core/src/main/java/com/azure/core/models/ResponseError.java + if (Objects.equals(name, "Error")) { + classType = ClassType.RESPONSE_ERROR; + } else if (Objects.equals(name, "InnerError")) { + // InnerError is not public, but usually it is only referenced from Error + classType = ClassType.RESPONSE_INNER_ERROR; + } + // ErrorResponse is not available, but that should only be used in Exception + } + + if (compositeType.getLanguage().getJava() != null + && compositeType.getLanguage().getJava().getNamespace() != null) { + + // https://github.com/Azure/azure-sdk-for-java/blob/main/sdk/core/azure-core-experimental/src/main/java/com/azure/core/experimental/models/PollResult.java + if (Objects.equals(name, ClassType.POLL_OPERATION_DETAILS.getName()) + && Objects.equals(compositeType.getLanguage().getJava().getNamespace(), ClassType.POLL_OPERATION_DETAILS.getPackage())) { + classType = ClassType.POLL_OPERATION_DETAILS; + } else if (Objects.equals(name, ClassType.REQUEST_CONDITIONS.getName()) + && Objects.equals(compositeType.getLanguage().getJava().getNamespace(), ClassType.REQUEST_CONDITIONS.getPackage())) { + classType = ClassType.REQUEST_CONDITIONS; + } else if (Objects.equals(name, ClassType.MATCH_CONDITIONS.getName()) + && Objects.equals(compositeType.getLanguage().getJava().getNamespace(), ClassType.REQUEST_CONDITIONS.getPackage())) { + classType = ClassType.MATCH_CONDITIONS; + } + } + } + } + return classType; + } + + /** + * Maps set of SchemaContext to set of ImplementationDetails.Usage. + * + * @param schemaContexts the set of SchemaContext. + * @return the set of ImplementationDetails.Usage. + */ + public static Set mapSchemaContext(Set schemaContexts) { + if (schemaContexts == null) { + return Collections.emptySet(); + } + return schemaContexts.stream() + .map(ImplementationDetails.Usage::fromSchemaContext) + .collect(Collectors.toSet()); + } + + private static boolean containsBinaryResponse(Operation operation) { + return operation.getResponses().stream().anyMatch(r -> Boolean.TRUE.equals(r.getBinary())); + } + + // SyncPoller or PollerFlux does not contain full Response and hence does not have headers + private static boolean operationIsNotFluentLRO(Operation operation, JavaSettings settings) { + return !(settings.isFluent() && operation.getExtensions() != null && operation.getExtensions().isXmsLongRunningOperation()); + } + + private static boolean operationIsNotDataPlaneLRO(Operation operation, JavaSettings settings) { + return !(settings.isDataPlaneClient() && operation.getExtensions() != null && operation.getExtensions().isXmsLongRunningOperation()); + } + + public static String getDefaultName(Metadata m) { + if (m.getLanguage() == null || m.getLanguage().getDefault() == null) { + return null; + } + return m.getLanguage().getDefault().getName(); + } + + public static String getJavaName(Metadata m) { + if (m.getLanguage() == null || m.getLanguage().getJava() == null) { + return null; + } + return m.getLanguage().getJava().getName(); + } + + public static boolean treatAsXml(Schema schema) { + return (schema.getSerializationFormats() != null && schema.getSerializationFormats().contains(KnownMediaType.XML.value())) + || (schema.getSerialization() != null && schema.getSerialization().getXml() != null); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/util/TemplateUtil.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/util/TemplateUtil.java new file mode 100644 index 0000000000..2f0ef18f88 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/util/TemplateUtil.java @@ -0,0 +1,361 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.util; + +import com.microsoft.typespec.http.client.generator.core.Javagen; +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; +import com.microsoft.typespec.http.client.generator.core.extension.plugin.PluginLogger; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ArrayType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClassType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethod; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethodType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.EnumType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.GenericType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.IType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.MethodPollingDetails; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.PrimitiveType; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaClass; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaFileContents; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaType; +import com.microsoft.typespec.http.client.generator.core.template.Templates; +import com.azure.core.util.CoreUtils; +import com.azure.json.JsonProviders; +import com.azure.json.JsonWriter; +import org.slf4j.Logger; + +import java.io.BufferedReader; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +public class TemplateUtil { + + private static final Logger LOGGER = new PluginLogger(Javagen.getPluginInstance(), TemplateUtil.class); + + // begin of constant for template replacement, used in ResourceUtil.loadTextFromResource + public static final String SERVICE_NAME = "service-name"; + public static final String SERVICE_DESCRIPTION = "service-description"; + + public static final String GROUP_ID = "group-id"; + public static final String ARTIFACT_ID = "artifact-id"; + public static final String ARTIFACT_VERSION = "artifact-version"; + public static final String PACKAGE_NAME = "package-name"; + public static final String IMPRESSION_PIXEL = "impression-pixel"; + + public static final String MANAGER_CLASS = "manager-class"; + + public static final String SAMPLE_CODES = "sample-codes"; + + public static final String DATE_UTC = "date-utc"; + + private static final String[] ESCAPE_REPLACEMENT; + + static { + ESCAPE_REPLACEMENT = new String[128]; + ESCAPE_REPLACEMENT['\\'] = "\\\\"; + ESCAPE_REPLACEMENT['\t'] = "\\t"; + ESCAPE_REPLACEMENT['\b'] = "\\b"; + ESCAPE_REPLACEMENT['\n'] = "\\n"; + ESCAPE_REPLACEMENT['\r'] = "\\r"; + ESCAPE_REPLACEMENT['\f'] = "\\f"; + ESCAPE_REPLACEMENT['\"'] = "\\\""; + } + + // end of constant for template replacement + + /** + * Print object to JSON string with indent. + * + * @param jsonObject the Java object + * @return the JSON string + */ + public static String prettyPrintToJson(Object jsonObject) { + try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + JsonWriter jsonWriter = JsonProviders.createWriter(outputStream)) { + jsonWriter.writeUntyped(jsonObject).flush(); + + return outputStream.toString(StandardCharsets.UTF_8); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + /** + * Load text from resources, with string replacement. + * + * @param filename the filename of the text template. + * @param replacements the string replacement to apply to the text template. + * @return the text, with string replacement applied. + */ + public static String loadTextFromResource(String filename, String... replacements) { + String text = ""; + try (InputStream inputStream = TemplateUtil.class.getClassLoader().getResourceAsStream(filename)) { + if (inputStream != null) { + text = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8)) + .lines() + .collect(Collectors.joining(System.lineSeparator())); + if (!text.isEmpty()) { + text += System.lineSeparator(); + } + + if (replacements.length > 0) { + if (replacements.length % 2 == 0) { + // replacement in template + for (int i = 0; i < replacements.length; i += 2) { + String key = replacements[i]; + String value = replacements[i+1]; + text = text.replace("{{" + key + "}}", value); + } + } else { + LOGGER.warn("Replacements skipped due to incorrect length: {}", Arrays.asList(replacements)); + } + } + } + return text; + } catch (IOException e) { + LOGGER.error("Failed to read file '{}'", filename); + throw new IllegalStateException(e); + } + } + + /** + * Helper function to write client methods to service client and method group + * + * @param classBlock Java class block + * @param clientMethods collection of client methods + */ + public static void writeClientMethodsAndHelpers(JavaClass classBlock, List clientMethods) { + JavaSettings settings = JavaSettings.getInstance(); + + // collect types of TypeReference + Set typeReferenceStaticClasses = new HashSet<>(); + + for (ClientMethod clientMethod : clientMethods) { + Templates.getClientMethodTemplate().write(clientMethod, classBlock); + + // this is coupled with ClientMethodTemplate.generateLongRunningBeginAsync, see getLongRunningOperationTypeReferenceExpression + if (clientMethod.getType() == ClientMethodType.LongRunningBeginAsync && clientMethod.getMethodPollingDetails() != null) { + if (clientMethod.getMethodPollingDetails().getIntermediateType() instanceof GenericType) { + typeReferenceStaticClasses.add((GenericType) clientMethod.getMethodPollingDetails().getIntermediateType()); + } + + if (clientMethod.getMethodPollingDetails().getFinalType() instanceof GenericType) { + typeReferenceStaticClasses.add((GenericType) clientMethod.getMethodPollingDetails().getFinalType()); + } + } + } + + // static classes for LRO + for (GenericType typeReferenceStaticClass : typeReferenceStaticClasses) { + writeTypeReferenceStaticVariable(classBlock, typeReferenceStaticClass); + } + + // helper methods for LLC + if (settings.isDataPlaneClient() && + clientMethods.stream().anyMatch(m -> m.getMethodPageDetails() != null)) { + writePagingHelperMethods(classBlock); + } + } + + /** + * Gets the expression of the intermediate and final type in LRO operation, used for "PollerFlux.create". + * + * @param details the MethodPollingDetails of LRO operation. + * @return the expression + */ + public static String getLongRunningOperationTypeReferenceExpression(MethodPollingDetails details) { + // see writeTypeReferenceStaticClass + return getTypeReferenceCreation(details.getIntermediateType()) + ", " + + getTypeReferenceCreation(details.getFinalType()); + } + + /** + * Gets the expression of the creation of TypeReference for different types. + * It uses a static variable for Generic type. See {@link #writeTypeReferenceStaticVariable(JavaClass, GenericType)} + * + * @param type the type. + * @return the expression + */ + public static String getTypeReferenceCreation(IType type) { + // see writeTypeReferenceStaticClass + // Array, class, enum, and primitive types are all able to use TypeReference.createInstance which will create + // or use a singleton instance. + // Generic types must use a custom instance that supports complex generic parameters. + if (!JavaSettings.getInstance().isBranded()) { + return (type instanceof ArrayType || type instanceof ClassType || type instanceof EnumType || type instanceof PrimitiveType) + ? type.asNullable() + ".class" + : CodeNamer.getEnumMemberName("TypeReference" + ((GenericType) type).toJavaPropertyString()); + } else { + return (type instanceof ArrayType || type instanceof ClassType || type instanceof EnumType || type instanceof PrimitiveType) + ? "TypeReference.createInstance(" + type.asNullable() + ".class)" + : CodeNamer.getEnumMemberName("TypeReference" + ((GenericType) type).toJavaPropertyString()); + } + } + + /** + * Writes a static final variable for TypeReference. + * See {@link #getTypeReferenceCreation(IType)} + * + * @param classBlock the class block to write the code. + * @param type the generic type. + */ + public static void writeTypeReferenceStaticVariable(JavaClass classBlock, GenericType type) { + // see getLongRunningOperationTypeReferenceExpression + + if (!JavaSettings.getInstance().isBranded()) { + StringBuilder sb = new StringBuilder(); + for (IType typeArgument : type.getTypeArguments()) { + if (sb.length() > 0) { + sb.append(", "); + } + sb.append(typeArgument.getClientType().toString()).append(".class"); + } + classBlock.privateStaticFinalVariable(String.format("Type %1$s = new ParameterizedType() {" + + "@Override public Type getRawType() { return " + type.getName() + ".class; }" + + "@Override public Type[] getActualTypeArguments() { return new Type[] { " + sb + " }; }" + + "@Override public Type getOwnerType() { return null; } }", + CodeNamer.getEnumMemberName("TypeReference" + type.toJavaPropertyString()))); + } else { + classBlock.privateStaticFinalVariable(String.format("TypeReference<%1$s> %2$s = new TypeReference<%1$s>() {}", + type, CodeNamer.getEnumMemberName("TypeReference" + type.toJavaPropertyString()))); + } + } + + /** + * Helper function to write helper methods for LLC paging + * + * @param classBlock Java class block + */ + private static void writePagingHelperMethods(JavaClass classBlock) { + classBlock.privateMethod("List getValues(BinaryData binaryData, String path)", block -> { + block.line("try {"); + block.line("Map obj = binaryData.toObject(Map.class);"); + block.line("List values = (List) obj.get(path);"); + block.line("return values.stream().map(BinaryData::fromObject).collect(Collectors.toList());"); + block.line("} catch (RuntimeException e) { return null; }"); + }); + classBlock.privateMethod("String getNextLink(BinaryData binaryData, String path)", block -> { + block.line("try {"); + block.line("Map obj = binaryData.toObject(Map.class);"); + block.line("return (String) obj.get(path);"); + block.line("} catch (RuntimeException e) { return null; }"); + }); + } + + /** + * Writes corresponding "ServiceMethod" annotation for client method. + * + * @param clientMethod the client method. + * @param typeBlock the code block. + */ + public static void writeClientMethodServiceMethodAnnotation(ClientMethod clientMethod, JavaType typeBlock) { + switch (clientMethod.getType()) { + case PagingSync: + case PagingAsync: + typeBlock.annotation("ServiceMethod(returns = ReturnType.COLLECTION)"); + break; + case LongRunningBeginSync: + case LongRunningBeginAsync: + typeBlock.annotation("ServiceMethod(returns = ReturnType.LONG_RUNNING_OPERATION)"); + break; + default: + if (JavaSettings.getInstance().isBranded()) { + typeBlock.annotation("ServiceMethod(returns = ReturnType.SINGLE)"); + } + break; + } + } + + /** + * Helper function to add a JsonGetter to a class block. + * + * @param classBlock The class block being annotated. + * @param settings The AutoRest settings to determine if JsonGetter should be added. + * @param propertyName The JSON property name for the JsonGetter. + */ + public static void addJsonGetter(JavaClass classBlock, JavaSettings settings, String propertyName) { + if (!settings.isStreamStyleSerialization()) { + addJsonGetterOrJsonSetter(classBlock, settings, () -> "JsonGetter(\"" + propertyName + "\")"); + } + } + + /** + * Helper function to add a JsonSetter to a class block. + * + * @param classBlock The class block being annotated. + * @param settings The AutoRest settings to determine if JsonSetter should be added. + * @param propertyName The JSON property name for the JsonSetter. + */ + public static void addJsonSetter(JavaClass classBlock, JavaSettings settings, String propertyName) { + if (!settings.isStreamStyleSerialization()) { + addJsonGetterOrJsonSetter(classBlock, settings, () -> "JsonSetter(\"" + propertyName + "\")"); + } + } + + private static void addJsonGetterOrJsonSetter(JavaClass classBlock, JavaSettings settings, + Supplier annotation) { + if (settings.isGettersAndSettersAnnotatedForSerialization()) { + classBlock.annotation(annotation.get()); + } + } + + public static void addClientLogger(JavaClass classBlock, String className, JavaFileContents javaFileContents) { + // Only need to check for usage of LOGGER as code will generate usages of ClientLogger with LOGGER. + if (javaFileContents.contains("LOGGER")) { + // hack to add LOGGER class variable only if LOGGER is used in code + classBlock.privateStaticFinalVariable( + ClassType.CLIENT_LOGGER + " LOGGER = new ClientLogger(" + className + ".class)"); + } + } + + /** + * Escape String for Java files. + * + * @param str string to escape + * @return escaped string + */ + public static String escapeString(String str) { + if (CoreUtils.isNullOrEmpty(str)) { + return str; + } + + StringBuilder builder = null; + + int last = 0; + for (int i = 0; i < str.length(); i++) { + char c = str.charAt(i); + String replacement = c < 128 ? ESCAPE_REPLACEMENT[c] : null; + + if (replacement == null) { + continue; + } + + if (builder == null) { + builder = new StringBuilder(str.length() * 2); + } + + if (last != i) { + builder.append(str, last, i); + } + + builder.append(replacement); + last = i + 1; + } + + if (builder == null) { + return str; + } + + builder.append(str, last, str.length()); + return builder.toString(); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/util/TypeUtil.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/util/TypeUtil.java new file mode 100644 index 0000000000..901dd7fac5 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/util/TypeUtil.java @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.util; + +import com.microsoft.typespec.http.client.generator.core.Javagen; +import com.microsoft.typespec.http.client.generator.core.extension.plugin.PluginLogger; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.GenericType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.IType; + +import java.util.Arrays; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +public class TypeUtil { + + private static final PluginLogger LOGGER = new PluginLogger(Javagen.getPluginInstance(), TypeUtil.class); + + private static final ConcurrentMap>> TYPE_CLASS_MAP = new ConcurrentHashMap<>(); + + private TypeUtil() { + } + + /** + * Whether the given type is GenericType and is subclass of either of the given classes. + * @param type the type to check + * @param parentClasses classes to match either one + * @return whether the given type is GenericType and is subclass of either of the given classes + */ + public static boolean isGenericTypeClassSubclassOf(IType type, Class... parentClasses) { + if (!(type instanceof GenericType) || parentClasses == null || parentClasses.length == 0) return false; + Class genericClass = getGenericClass((GenericType) type); + return genericClass != null && Arrays.stream(parentClasses).anyMatch(p -> p.isAssignableFrom(genericClass)); + } + + private static Class getGenericClass(GenericType type) { + String className = type.getPackage() + "." + type.getName(); + return TYPE_CLASS_MAP.computeIfAbsent(className, key -> { + try { + return Optional.of(Class.forName(key)); + } catch (ClassNotFoundException e) { + LOGGER.warn("class " + key + " not found!", e); + return Optional.empty(); + } + }).orElse(null); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/util/XmsExampleWrapper.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/util/XmsExampleWrapper.java new file mode 100644 index 0000000000..dfaaaf7180 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/util/XmsExampleWrapper.java @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.core.util; + +public class XmsExampleWrapper { + + private final Object xmsExample; + private final String operationId; + private final String exampleName; + + public XmsExampleWrapper(Object xmsExample, String operationId, String exampleName) { + this.xmsExample = xmsExample; + this.operationId = operationId; + this.exampleName = exampleName; + } + + public String getExampleName() { + return exampleName; + } + + public Object getXmsExample() { + return xmsExample; + } + + public String getOperationId() { + return operationId; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/pom.xml b/packages/http-client-java/generator/http-client-generator-mgmt/pom.xml new file mode 100644 index 0000000000..f1217955cb --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/pom.xml @@ -0,0 +1,34 @@ + + 4.0.0 + + com.microsoft.typespec + typespec-java-generator + 1.0.0-beta.1 + + + com.microsoft.typespec + http-client-generator-mgmt + jar + + http-client-generator-fluent + http://maven.apache.org + + + UTF-8 + + + + + com.microsoft.typespec + http-client-generator-core + 1.0.0-beta.1 + + + + com.azure + azure-core-management + 1.15.2 + + + diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/FluentGen.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/FluentGen.java new file mode 100644 index 0000000000..779dca7055 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/FluentGen.java @@ -0,0 +1,421 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt; + +import com.microsoft.typespec.http.client.generator.core.Javagen; +import com.microsoft.typespec.http.client.generator.core.extension.jsonrpc.Connection; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.CodeModel; +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; +import com.microsoft.typespec.http.client.generator.core.extension.plugin.PluginLogger; +import com.microsoft.typespec.http.client.generator.mgmt.mapper.ExampleParser; +import com.microsoft.typespec.http.client.generator.mgmt.mapper.FluentMapper; +import com.microsoft.typespec.http.client.generator.mgmt.mapper.FluentMapperFactory; +import com.microsoft.typespec.http.client.generator.mgmt.mapper.FluentPomMapper; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentClient; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentExample; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentLiveTests; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentResourceCollection; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentResourceModel; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentStatic; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.examplemodel.FluentMethodMockUnitTest; +import com.microsoft.typespec.http.client.generator.mgmt.model.javamodel.FluentJavaPackage; +import com.microsoft.typespec.http.client.generator.mgmt.model.projectmodel.FluentProject; +import com.microsoft.typespec.http.client.generator.mgmt.namer.FluentNamerFactory; +import com.microsoft.typespec.http.client.generator.mgmt.template.FluentTemplateFactory; +import com.microsoft.typespec.http.client.generator.mgmt.util.FluentJavaSettings; +import com.microsoft.typespec.http.client.generator.mgmt.util.FluentUtils; +import com.microsoft.typespec.http.client.generator.core.mapper.Mappers; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.AsyncSyncClient; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.Client; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientBuilder; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientException; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientModel; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientModels; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientResponse; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.EnumType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.MethodGroupClient; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.PackageInfo; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.Pom; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ServiceClient; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.UnionModels; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.XmlSequenceWrapper; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaFile; +import com.microsoft.typespec.http.client.generator.core.model.projectmodel.TextFile; +import com.microsoft.typespec.http.client.generator.core.model.xmlmodel.XmlFile; +import com.microsoft.typespec.http.client.generator.core.postprocessor.Postprocessor; +import com.microsoft.typespec.http.client.generator.core.template.Templates; +import com.microsoft.typespec.http.client.generator.core.util.ClientModelUtil; +import com.microsoft.typespec.http.client.generator.core.util.CodeNamer; +import com.azure.core.util.CoreUtils; +import org.slf4j.Logger; +import org.yaml.snakeyaml.DumperOptions; +import org.yaml.snakeyaml.LoaderOptions; +import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.constructor.Constructor; +import org.yaml.snakeyaml.inspector.TrustedTagInspector; +import org.yaml.snakeyaml.introspector.Property; +import org.yaml.snakeyaml.nodes.NodeTuple; +import org.yaml.snakeyaml.nodes.Tag; +import org.yaml.snakeyaml.representer.Representer; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +public class FluentGen extends Javagen { + + private final Logger logger = new PluginLogger(this, FluentGen.class); + static FluentGen instance; + + private FluentJavaSettings fluentJavaSettings; + private FluentMapper fluentMapper; + + private List fluentPremiumExamples; + + public FluentGen(Connection connection, String plugin, String sessionId) { + super(connection, plugin, sessionId); + instance = this; + Javagen.instance = this; + ClientModelUtil.setGetClientModelFunction(FluentUtils::getClientModel); + } + + public static FluentGen getPluginInstance() { + return instance; + } + + @Override + public boolean processInternal() { + this.clear(); + + try { + JavaSettings settings = JavaSettings.getInstance(); + + logger.info("Read YAML"); + // Parse yaml to code model + CodeModel codeModel = new FluentNamer(this, connection, pluginName, sessionId) + .processCodeModel(); + + // Map code model to client model + Client client = this.handleMap(codeModel); + + // Write to templates + FluentJavaPackage javaPackage = this.handleTemplate(client); + + // Fluent Lite + this.handleFluentLite(codeModel, client, javaPackage); + + // Print to files + logger.info("Write Java"); + Postprocessor.writeToFiles(javaPackage.getJavaFiles().stream() + .collect(Collectors.toMap(JavaFile::getFilePath, file -> file.getContents().toString())), this, logger); + + logger.info("Write Xml"); + for (XmlFile xmlFile : javaPackage.getXmlFiles()) { + writeFile(xmlFile.getFilePath(), xmlFile.getContents().toString(), null); + } + logger.info("Write Text"); + for (TextFile textFile : javaPackage.getTextFiles()) { + writeFile(textFile.getFilePath(), textFile.getContents(), null); + } + return true; + } catch (Exception e) { + logger.error("Failed to successfully run fluentgen plugin " + e, e); + //connection.sendError(1, 500, "Error occurred while running fluentgen plugin: " + e.getMessage()); + return false; + } + } + + CodeModel handleYaml(String yamlContent) { + Representer representer = new Representer(new DumperOptions()) { + @Override + protected NodeTuple representJavaBeanProperty(Object javaBean, Property property, Object propertyValue, Tag customTag) { + // if value of property is null, ignore it. + if (propertyValue == null) { + return null; + } + else { + return super.representJavaBeanProperty(javaBean, property, propertyValue, customTag); + } + } + }; + LoaderOptions loaderOptions = new LoaderOptions(); + loaderOptions.setCodePointLimit(50 * 1024 * 1024); + loaderOptions.setMaxAliasesForCollections(Integer.MAX_VALUE); + loaderOptions.setNestingDepthLimit(Integer.MAX_VALUE); + loaderOptions.setTagInspector(new TrustedTagInspector()); + Yaml newYaml = new Yaml(new Constructor(loaderOptions), representer, new DumperOptions(), loaderOptions); + return newYaml.loadAs(yamlContent, CodeModel.class); + } + + protected Client handleMap(CodeModel codeModel) { + JavaSettings settings = JavaSettings.getInstance(); + FluentStatic.setFluentJavaSettings(getFluentJavaSettings()); + + FluentMapper fluentMapper = this.getFluentMapper(); + + logger.info("Map code model to client model"); + fluentMapper.preModelMap(codeModel); + + Client client = Mappers.getClientMapper().map(codeModel); + + // samples for Fluent Premium + if (fluentJavaSettings.isGenerateSamples() && settings.isFluentPremium()) { + FluentStatic.setClient(client); + ExampleParser exampleParser = new ExampleParser(); + fluentPremiumExamples = client.getServiceClient().getMethodGroupClients().stream() + .flatMap(mg -> exampleParser.parseMethodGroup(mg).stream()) + .collect(Collectors.toList()); + } + + return client; + } + + protected FluentJavaPackage handleTemplate(Client client) { + JavaSettings javaSettings = JavaSettings.getInstance(); + + logger.info("Java template for client model"); + FluentJavaPackage javaPackage = new FluentJavaPackage(this); + + // Service client + String interfacePackage = ClientModelUtil.getServiceClientInterfacePackageName(); + if (CoreUtils.isNullOrEmpty(client.getServiceClients())) { + ServiceClient serviceClient = client.getServiceClient(); + addServiceClient(javaSettings, javaPackage, interfacePackage, serviceClient); + } else { + addServiceClient(javaSettings, javaPackage, interfacePackage, client.getServiceClients().iterator().next()); + } + + // Async/sync service clients + if (!javaSettings.isFluentLite()) { + // fluent lite only expose sync client + for (AsyncSyncClient asyncClient : client.getAsyncClients()) { + javaPackage.addAsyncServiceClient(asyncClient.getPackageName(), asyncClient); + } + } + for (AsyncSyncClient syncClient : client.getSyncClients()) { + javaPackage.addSyncServiceClient(syncClient.getPackageName(), syncClient); + } + + // Service client builder + for (ClientBuilder clientBuilder : client.getClientBuilders()) { + javaPackage.addServiceClientBuilder(clientBuilder); + } + + // Method group + for (MethodGroupClient methodGroupClient : client.getServiceClient().getMethodGroupClients()) { + javaPackage.addMethodGroup(methodGroupClient.getPackage(), methodGroupClient.getClassName(), methodGroupClient); + if (javaSettings.isGenerateClientInterfaces()) { + javaPackage.addMethodGroupInterface(interfacePackage, methodGroupClient.getInterfaceName(), methodGroupClient); + } + } + + // Response + for (ClientResponse response : client.getResponseModels()) { + javaPackage.addClientResponse(response.getPackage(), response.getName(), response); + } + + // Client model + for (ClientModel model : client.getModels()) { + javaPackage.addModel(model.getPackage(), model.getName(), model); + } + + // Enum + for (EnumType enumType : client.getEnums()) { + javaPackage.addEnum(enumType.getPackage(), enumType.getName(), enumType); + } + + // XML sequence wrapper + for (XmlSequenceWrapper xmlSequenceWrapper : client.getXmlSequenceWrappers()) { + javaPackage.addXmlSequenceWrapper(xmlSequenceWrapper.getPackage(), + xmlSequenceWrapper.getWrapperClassName(), xmlSequenceWrapper); + } + + // Exception + for (ClientException exception : client.getExceptions()) { + javaPackage.addException(exception.getPackage(), exception.getName(), exception); + } + + // Package-info + for (PackageInfo packageInfo : client.getPackageInfos()) { + javaPackage.addPackageInfo(packageInfo.getPackage(), "package-info", packageInfo); + } + + // GraalVM config + if (javaSettings.isGenerateGraalVmConfig()) { + String artifactId = FluentUtils.getArtifactId(); + if (fluentJavaSettings.getGraalVmConfigSuffix().isPresent()) { + artifactId = artifactId + "_" + fluentJavaSettings.getGraalVmConfigSuffix().get(); + } + javaPackage.addGraalVmConfig("com.azure.resourcemanager", artifactId, client.getGraalVmConfig()); + } + + // Samples + if (fluentPremiumExamples != null) { + for (FluentExample example : fluentPremiumExamples) { + javaPackage.addSample(example); + } + } + + if (javaSettings.isGenerateTests()) { + // Unit tests for models + for (ClientModel model : client.getModels()) { + if (!model.isStronglyTypedHeader()) { + javaPackage.addModelUnitTest(model); + } + } + } + + return javaPackage; + } + + private void addServiceClient(JavaSettings javaSettings, FluentJavaPackage javaPackage, String interfacePackage, ServiceClient serviceClient) { + javaPackage + .addServiceClient(serviceClient.getPackage(), serviceClient.getClassName(), + serviceClient); + if (javaSettings.isGenerateClientInterfaces()) { + javaPackage + .addServiceClientInterface(interfacePackage, serviceClient.getInterfaceName(), serviceClient); + } + } + + protected FluentClient handleFluentLite(CodeModel codeModel, Client client, FluentJavaPackage javaPackage) { + FluentJavaSettings fluentJavaSettings = this.getFluentJavaSettings(); + JavaSettings javaSettings = JavaSettings.getInstance(); + + FluentClient fluentClient = null; + + // Fluent Lite + if (javaSettings.isFluentLite()) { + final boolean isSdkIntegration = fluentJavaSettings.isSdkIntegration(); + FluentStatic.setFluentJavaSettings(fluentJavaSettings); + FluentStatic.setClient(client); + + logger.info("Process for Fluent Lite, SDK integration {}", (isSdkIntegration ? "enabled" : "disabled")); + + fluentClient = this.getFluentMapper().map(codeModel, client); + + // project + FluentProject project = new FluentProject(fluentClient); + if (isSdkIntegration) { + project.integrateWithSdk(); + } + + // Fluent manager + javaPackage.addFluentManager(fluentClient.getManager(), project); + + // Fluent resource models + for (FluentResourceModel model : fluentClient.getResourceModels()) { + javaPackage.addFluentResourceModel(model); + } + + // Fluent resource collections + for (FluentResourceCollection collection : fluentClient.getResourceCollections()) { + javaPackage.addFluentResourceCollection(collection); + } + + // Utils + javaPackage.addResourceManagerUtils(); + + // module-info + javaPackage.addModuleInfo(fluentClient.getModuleInfo()); + + // package-info + ensureModelsPackageInfos(javaPackage, fluentClient); + + // POM + if (javaSettings.isRegeneratePom()) { + Pom pom = new FluentPomMapper().map(project); + javaPackage.addPom(fluentJavaSettings.getPomFilename(), pom); + } + + // Samples + List sampleJavaFiles = new ArrayList<>(); + for (FluentExample example : fluentClient.getExamples()) { + sampleJavaFiles.add(javaPackage.addSample(example)); + } + + // Readme and Changelog + if (isSdkIntegration) { + javaPackage.addReadmeMarkdown(project); + javaPackage.addChangelogMarkdown(project.getChangelog()); + if (fluentJavaSettings.isGenerateSamples() && project.getSdkRepositoryUri().isPresent()) { + javaPackage.addSampleMarkdown(fluentClient.getExamples(), sampleJavaFiles); + } + } + + // Tests + if (javaSettings.isGenerateTests()) { + // Live tests + for (FluentLiveTests liveTests : fluentClient.getLiveTests()) { + javaPackage.addLiveTests(liveTests); + } + + // Unit tests for APIs + for (FluentMethodMockUnitTest unitTest : fluentClient.getMockUnitTests()) { + javaPackage.addOperationUnitTest(unitTest); + } + } + } + + return fluentClient; + } + + // Fix the case where there are no models but only resource collections. + private void ensureModelsPackageInfos(FluentJavaPackage javaPackage, FluentClient fluentClient) { + Set packageInfos = fluentClient + .getInnerClient().getPackageInfos() + .stream() + .map(PackageInfo::getPackage) + .collect(Collectors.toSet()); + + for (FluentResourceCollection resourceCollection : fluentClient.getResourceCollections()) { + String packageName = resourceCollection.getInterfaceType().getPackage(); + if (!packageInfos.contains(packageName)) { + javaPackage.addPackageInfo( + packageName, + "package-info", + new PackageInfo( + packageName, + String.format("Package containing the data models for %s.\n%s", fluentClient.getInnerClient().getClientName(), + fluentClient.getInnerClient().getClientDescription()))); + packageInfos.add(packageName); + } + } + } + + void clear() { + FluentStatic.setClient(null); + FluentStatic.setFluentClient(null); + FluentStatic.setFluentJavaSettings(null); + + JavaSettings.clear(); + ClientModels.getInstance().clear(); + UnionModels.getInstance().clear(); + fluentJavaSettings = null; + fluentMapper = null; + fluentPremiumExamples = null; + } + + protected FluentJavaSettings getFluentJavaSettings() { + if (fluentJavaSettings == null) { + fluentJavaSettings = new FluentJavaSettings(this); + } + return fluentJavaSettings; + } + + protected FluentMapper getFluentMapper() { + if (fluentMapper == null) { + // use fluent mapper and template + Mappers.setFactory(new FluentMapperFactory()); + Templates.setFactory(new FluentTemplateFactory()); + + FluentJavaSettings fluentJavaSettings = getFluentJavaSettings(); + CodeNamer.setFactory(new FluentNamerFactory(fluentJavaSettings)); + + fluentMapper = new FluentMapper(fluentJavaSettings); + } + return fluentMapper; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/FluentNamer.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/FluentNamer.java new file mode 100644 index 0000000000..058ae2f6f9 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/FluentNamer.java @@ -0,0 +1,149 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt; + +import com.microsoft.typespec.http.client.generator.core.extension.jsonrpc.Connection; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.CodeModel; +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; +import com.microsoft.typespec.http.client.generator.core.extension.plugin.NewPlugin; +import com.microsoft.typespec.http.client.generator.core.extension.plugin.PluginLogger; +import com.microsoft.typespec.http.client.generator.core.extension.base.util.FileUtils; +import com.microsoft.typespec.http.client.generator.mgmt.namer.FluentNamerFactory; +import com.microsoft.typespec.http.client.generator.mgmt.transformer.FluentTransformer; +import com.microsoft.typespec.http.client.generator.mgmt.util.FluentJavaSettings; +import com.microsoft.typespec.http.client.generator.core.preprocessor.Preprocessor; +import com.microsoft.typespec.http.client.generator.core.preprocessor.tranformer.Transformer; +import com.microsoft.typespec.http.client.generator.core.util.CodeNamer; +import com.azure.json.JsonProviders; +import com.azure.json.JsonReader; +import org.slf4j.Logger; +import org.yaml.snakeyaml.DumperOptions; +import org.yaml.snakeyaml.LoaderOptions; +import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.constructor.Constructor; +import org.yaml.snakeyaml.inspector.TrustedTagInspector; +import org.yaml.snakeyaml.introspector.Property; +import org.yaml.snakeyaml.nodes.NodeTuple; +import org.yaml.snakeyaml.nodes.Tag; +import org.yaml.snakeyaml.representer.Representer; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; + +public class FluentNamer extends Preprocessor { + + private final Logger logger; + private static FluentNamer instance; + + public FluentNamer(NewPlugin plugin, Connection connection, String pluginName, String sessionId) { + super(plugin, connection, pluginName, sessionId); + this.logger = new PluginLogger(this, FluentNamer.class); + FluentNamer.instance = this; + } + + public static FluentNamer getPluginInstance() { + return instance; + } + + public CodeModel processCodeModel() { + this.clear(); + + try { + + Path codeModelFolder; + try { + codeModelFolder = FileUtils.createTempDirectory("code-model" + UUID.randomUUID()); + logger.info("Created temp directory for code model: {}", codeModelFolder); + } catch (IOException ex) { + logger.error("Failed to create temp directory for code model.", ex); + throw new RuntimeException("Failed to create temp directory for code model.", ex); + } + + CodeModel codeModel = getCodeModelAndWriteToTargetFolder(codeModelFolder); + // Do necessary transformation + codeModel = transform(codeModel); + // Write to local file (for debugging) + Yaml newYaml = createYaml(); + String output = newYaml.dump(codeModel); + + // Output updated code model + Files.writeString(codeModelFolder.resolve("code-model-fluentnamer-no-tags.yaml"), output); + + return codeModel; + } catch (Exception e) { + logger.error("Failed to successfully run fluentnamer plugin.", e); + throw new RuntimeException("Failed to successfully run fluentnamer plugin.", e); + } + } + + protected CodeModel getCodeModelAndWriteToTargetFolder(Path codeModelFolder) throws IOException { + List files = listInputs().stream().filter(s -> s.contains("no-tags")).collect(Collectors.toList()); + if (files.size() != 1) { + throw new RuntimeException(String + .format("Generator received incorrect number of inputs: %s : %s}", files.size(), String.join(", ", files))); + } + // Read input file + String file = readFile(files.get(0)); + // Write the input code model file to a local code model file to help debugging + Files.writeString(codeModelFolder.resolve("code-model.yaml"), file); + // Deserialize the input code model string to CodeModel object + return loadCodeModel(file); + } + + private CodeModel loadCodeModel(String file) throws IOException { + if (!file.startsWith("{")) { + return yamlMapper.loadAs(file, CodeModel.class); + } else { + try (JsonReader jsonReader = JsonProviders.createReader(file)) { + return CodeModel.fromJson(jsonReader); + } + } + } + + private Yaml createYaml() { + Representer representer = new Representer(new DumperOptions()) { + @Override + protected NodeTuple representJavaBeanProperty(Object javaBean, Property property, Object propertyValue, + Tag customTag) { + // if value of property is null, ignore it. + if (propertyValue == null) { + return null; + } else { + return super.representJavaBeanProperty(javaBean, property, propertyValue, customTag); + } + } + }; + LoaderOptions loaderOptions = new LoaderOptions(); + loaderOptions.setCodePointLimit(50 * 1024 * 1024); + loaderOptions.setMaxAliasesForCollections(Integer.MAX_VALUE); + loaderOptions.setNestingDepthLimit(Integer.MAX_VALUE); + loaderOptions.setTagInspector(new TrustedTagInspector()); + return new Yaml(new Constructor(loaderOptions), representer, new DumperOptions(), loaderOptions); + } + + public CodeModel transform(CodeModel codeModel) { + logger.info("Load fluent settings"); + FluentJavaSettings fluentJavaSettings = new FluentJavaSettings(this); + CodeNamer.setFactory(new FluentNamerFactory(fluentJavaSettings)); + + // Step 2: Transform + logger.info("Transform code model"); + FluentTransformer transformer = new FluentTransformer(fluentJavaSettings); + codeModel = transformer.preTransform(codeModel); + + codeModel = new Transformer().transform(codeModel); + + codeModel = transformer.postTransform(codeModel); + + return codeModel; + } + + private void clear() { + JavaSettings.clear(); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/Main.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/Main.java new file mode 100644 index 0000000000..01b900d2bb --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/Main.java @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt; + +import com.microsoft.typespec.http.client.generator.core.extension.jsonrpc.Connection; + +public class Main { + + public static void main(String[] args) { + Connection connection = new Connection(System.out, System.in); + connection.dispatch("GetPluginNames", () -> "[\"fluentgen\"]"); + connection.dispatch("Process", (plugin, sessionId) -> new FluentGen(connection, plugin, sessionId).process()); + connection.dispatchNotification("Shutdown", connection::stop); + + // wait for something to do. + connection.waitForAll(); + System.exit(0); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/mapper/ExampleParser.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/mapper/ExampleParser.java new file mode 100644 index 0000000000..fc14ca3985 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/mapper/ExampleParser.java @@ -0,0 +1,498 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.mapper; + +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.RequestParameterLocation; +import com.microsoft.typespec.http.client.generator.core.extension.plugin.PluginLogger; +import com.microsoft.typespec.http.client.generator.mgmt.FluentGen; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentCollectionMethod; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentExample; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentResourceCollection; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentResourceModel; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentStatic; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.examplemodel.FluentClientMethodExample; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.examplemodel.FluentCollectionMethodExample; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.examplemodel.FluentMethodExample; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.examplemodel.FluentResourceCreateExample; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.examplemodel.FluentResourceUpdateExample; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.examplemodel.ParameterExample; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.create.DefinitionStage; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.create.DefinitionStageBlank; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.create.DefinitionStageCreate; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.create.DefinitionStageMisc; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.create.DefinitionStageParent; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.create.ResourceCreate; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.method.FluentDefineMethod; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.method.FluentMethod; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.update.ResourceUpdate; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.update.UpdateStage; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.update.UpdateStageApply; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.update.UpdateStageMisc; +import com.microsoft.typespec.http.client.generator.mgmt.util.FluentUtils; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClassType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethod; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethodParameter; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientModel; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.MethodGroupClient; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ModelProperty; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ProxyMethodExample; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.examplemodel.ExampleNode; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.examplemodel.LiteralNode; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.examplemodel.MethodParameter; +import com.microsoft.typespec.http.client.generator.core.util.CodeNamer; +import com.microsoft.typespec.http.client.generator.core.util.MethodUtil; +import com.microsoft.typespec.http.client.generator.core.util.ModelExampleUtil; +import org.slf4j.Logger; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +public class ExampleParser { + + private static final Logger LOGGER = new PluginLogger(FluentGen.getPluginInstance(), ExampleParser.class); + + private final boolean aggregateExamples; + + public ExampleParser() { + this(true); + } + + public ExampleParser(boolean aggregateExamples) { + this.aggregateExamples = aggregateExamples; + } + + public List parseMethodGroup(MethodGroupClient methodGroup) { + List methodExamples = new ArrayList<>(); + + methodGroup.getClientMethods().forEach(m -> { + List examples = ExampleParser.parseMethod(methodGroup, m); + if (examples != null) { + methodExamples.addAll(examples); + } + }); + + Map examples = new HashMap<>(); + methodExamples.forEach(e -> { + FluentExample example = getExample(examples, e.getMethodGroup(), e.getClientMethod(), e.getName()); + example.getClientMethodExamples().add(e); + }); + + return new ArrayList<>(examples.values()); + } + + public List parseResourceCollection(FluentResourceCollection resourceCollection) { + List methodExamples = new ArrayList<>(); + List resourceCreateExamples = new ArrayList<>(); + List resourceUpdateExamples = new ArrayList<>(); + + resourceCollection.getMethodsForTemplate().forEach(m -> { + List examples = ExampleParser.parseMethod(resourceCollection, m); + if (examples != null) { + methodExamples.addAll(examples); + } + }); + resourceCollection.getResourceCreates().forEach(rc -> { + List examples = ExampleParser.parseResourceCreate(resourceCollection, rc); + if (examples != null) { + resourceCreateExamples.addAll(examples); + } + }); + resourceCollection.getResourceUpdates().forEach(ru -> { + List examples = ExampleParser.parseResourceUpdate(resourceCollection, ru); + if (examples != null) { + resourceUpdateExamples.addAll(examples); + } + }); + + Map examples = new HashMap<>(); + methodExamples.forEach(e -> { + FluentExample example = getExample(examples, e.getResourceCollection(), e.getCollectionMethod(), e.getName()); + example.getCollectionMethodExamples().add(e); + }); + resourceCreateExamples.forEach(e -> { + FluentExample example = getExample(examples, e.getResourceCollection(), e.getResourceCreate().getMethodReferences().iterator().next(), e.getName()); + example.getResourceCreateExamples().add(e); + }); + resourceUpdateExamples.forEach(e -> { + FluentExample example = getExample(examples, e.getResourceCollection(), e.getResourceUpdate().getMethodReferences().iterator().next(), e.getName()); + example.getResourceUpdateExamples().add(e); + }); + + return new ArrayList<>(examples.values()); + } + + private FluentExample getExample(Map examples, + FluentResourceCollection resourceCollection, FluentCollectionMethod collectionMethod, + String exampleName) { + return getExample(examples, resourceCollection.getInnerGroupClient(), collectionMethod.getInnerClientMethod(), exampleName); + } + + private FluentExample getExample(Map examples, + MethodGroupClient methodGroup, ClientMethod clientMethod, + String exampleName) { + String groupName = methodGroup.getClassBaseName(); + String methodName = clientMethod.getProxyMethod().getName(); + String name = CodeNamer.toPascalCase(groupName) + CodeNamer.toPascalCase(methodName); + if (!this.aggregateExamples) { + name += com.microsoft.typespec.http.client.generator.core.preprocessor.namer.CodeNamer.getTypeName(exampleName); + } + FluentExample example = examples.get(name); + if (example == null) { + example = new FluentExample(CodeNamer.toPascalCase(groupName), CodeNamer.toPascalCase(methodName), + this.aggregateExamples ? null : exampleName); + examples.put(name, example); + } + return example; + } + + private static List parseMethod(FluentResourceCollection collection, FluentCollectionMethod collectionMethod) { + List ret = null; + + ClientMethod clientMethod = collectionMethod.getInnerClientMethod(); + if (FluentUtils.validRequestContentTypeToGenerateExample(clientMethod)) { + ret = new ArrayList<>(); + + List methodParameters = MethodUtil.getParameters(clientMethod); + for (Map.Entry entry : collectionMethod.getInnerClientMethod().getProxyMethod().getExamples().entrySet()) { + LOGGER.info("Parse collection method example '{}'", entry.getKey()); + + FluentCollectionMethodExample collectionMethodExample = + parseMethodForExample(collection, collectionMethod, methodParameters, entry.getKey(), entry.getValue()); + ret.add(collectionMethodExample); + } + } + return ret; + } + + private static List parseMethod(MethodGroupClient methodGroup, ClientMethod clientMethod) { + List ret = null; + + if (FluentUtils.validRequestContentTypeToGenerateExample(clientMethod)) { + ret = new ArrayList<>(); + + List methodParameters = MethodUtil.getParameters(clientMethod); + for (Map.Entry entry : clientMethod.getProxyMethod().getExamples().entrySet()) { + LOGGER.info("Parse client method example '{}'", entry.getKey()); + + FluentClientMethodExample clientMethodExample = + parseMethodForExample(methodGroup, clientMethod, methodParameters, entry.getKey(), entry.getValue()); + ret.add(clientMethodExample); + } + } + return ret; + } + + protected static FluentCollectionMethodExample parseMethodForExample(FluentResourceCollection collection, FluentCollectionMethod collectionMethod, + List methodParameters, + String exampleName, ProxyMethodExample proxyMethodExample) { + FluentCollectionMethodExample collectionMethodExample = new FluentCollectionMethodExample( + exampleName, proxyMethodExample.getRelativeOriginalFileName(), + FluentStatic.getFluentManager(), collection, collectionMethod); + + addMethodParametersToMethodExample(methodParameters, proxyMethodExample, collectionMethodExample); + return collectionMethodExample; + } + + public static FluentCollectionMethodExample parseMethodExample(FluentResourceCollection resourceCollection, Collection collectionMethods, ProxyMethodExample example) { + FluentCollectionMethod collectionMethod = collectionMethods.stream().filter(method -> FluentUtils.requiresExample(method.getInnerClientMethod())).findFirst().get(); + return parseMethodForExample(resourceCollection, collectionMethod, MethodUtil.getParameters(collectionMethod.getInnerClientMethod()), example.getName(), example); + } + + private static FluentClientMethodExample parseMethodForExample( + MethodGroupClient methodGroup, ClientMethod clientMethod, List methodParameters, + String exampleName, ProxyMethodExample proxyMethodExample) { + + FluentClientMethodExample clientMethodExample = new FluentClientMethodExample( + exampleName, proxyMethodExample.getRelativeOriginalFileName(), methodGroup, clientMethod); + + addMethodParametersToMethodExample(methodParameters, proxyMethodExample, clientMethodExample); + return clientMethodExample; + } + + private static void addMethodParametersToMethodExample(List methodParameters, ProxyMethodExample proxyMethodExample, FluentMethodExample methodExample) { + for (MethodParameter methodParameter : methodParameters) { + ExampleNode node = ModelExampleUtil.parseNodeFromParameter(proxyMethodExample, methodParameter); + + if (node.getObjectValue() == null) { + if (methodParameter.getClientMethodParameter().isRequired()) { + LOGGER.warn("Failed to assign sample value to required parameter '{}'", methodParameter.getClientMethodParameter().getName()); + } + } + + ParameterExample parameterExample = new ParameterExample(node); + methodExample.getParameters().add(parameterExample); + } + } + + private static List parseResourceCreate(FluentResourceCollection collection, ResourceCreate resourceCreate) { + List ret = null; + + final boolean methodIsCreateOrUpdate = methodIsCreateOrUpdate(resourceCreate.getResourceModel()); + + List collectionMethods = resourceCreate.getMethodReferences(); + for (FluentCollectionMethod collectionMethod : collectionMethods) { + ClientMethod clientMethod = collectionMethod.getInnerClientMethod(); + if (FluentUtils.validRequestContentTypeToGenerateExample(clientMethod)) { + if (ret == null) { + ret = new ArrayList<>(); + } + + List methodParameters = MethodUtil.getParameters(clientMethod); + MethodParameter requestBodyParameter = findRequestBodyParameter(methodParameters); + + for (Map.Entry entry : collectionMethod.getInnerClientMethod().getProxyMethod().getExamples().entrySet()) { + if (methodIsCreateOrUpdate && FluentUtils.exampleIsUpdate(entry.getKey())) { + // likely a resource update example + LOGGER.info("Skip possible resource update example '{}' in create", entry.getKey()); + continue; + } + + LOGGER.info("Parse resource create example '{}'", entry.getKey()); + + FluentResourceCreateExample resourceCreateExample = parseResourceCreate(collection, resourceCreate, entry.getValue(), methodParameters, requestBodyParameter); + + ret.add(resourceCreateExample); + } + } + } + return ret; + } + + protected static FluentResourceCreateExample parseResourceCreate(FluentResourceCollection collection, ResourceCreate resourceCreate, ProxyMethodExample example, List methodParameters, MethodParameter requestBodyParameter) { + FluentResourceCreateExample resourceCreateExample = new FluentResourceCreateExample( + example.getName(), example.getRelativeOriginalFileName(), + FluentStatic.getFluentManager(), collection, resourceCreate); + + FluentDefineMethod defineMethod = resourceCreate.getDefineMethod(); + ExampleNode defineNode = null; + if (defineMethod.getMethodParameter() != null) { + MethodParameter methodParameter = findMethodParameter(methodParameters, defineMethod.getMethodParameter()); + defineNode = ModelExampleUtil.parseNodeFromParameter(example, methodParameter); + + if (defineNode.getObjectValue() == null) { + LOGGER.warn("Failed to assign sample value to define method '{}'", defineMethod.getName()); + } + } + resourceCreateExample.getParameters().add(new ParameterExample(defineMethod, defineNode)); + + for (DefinitionStage stage : resourceCreate.getDefinitionStages()) { + List fluentMethods = stage.getMethods(); + if (!fluentMethods.isEmpty()) { + FluentMethod fluentMethod = fluentMethods.iterator().next(); + List exampleNodes = new ArrayList<>(); + + if (stage instanceof DefinitionStageBlank || stage instanceof DefinitionStageCreate) { + // blank and create stage does not have parameter + } else if (stage instanceof DefinitionStageParent) { + List parameters = fluentMethod.getParameters().stream() + .map(p -> findMethodParameter(methodParameters, p)) + .collect(Collectors.toList()); + exampleNodes.addAll(parameters.stream() + .map(p -> ModelExampleUtil.parseNodeFromParameter(example, p)) + .collect(Collectors.toList())); + } else if (stage instanceof DefinitionStageMisc) { + DefinitionStageMisc miscStage = (DefinitionStageMisc) stage; + MethodParameter methodParameter = findMethodParameter(methodParameters, miscStage.getMethodParameter()); + ExampleNode node = ModelExampleUtil.parseNodeFromParameter(example, methodParameter); + + if (stage.isMandatoryStage() || !node.isNull()) { + exampleNodes.add(node); + } + } else { + ModelProperty modelProperty = stage.getModelProperty(); + if (modelProperty != null) { + ExampleNode node = parseNodeFromModelProperty(example, requestBodyParameter, resourceCreate.getRequestBodyParameterModel(), modelProperty); + + if (stage.isMandatoryStage() || !node.isNull()) { + exampleNodes.add(node); + } + } + } + + if (exampleNodes.stream().anyMatch(ExampleNode::isNull)) { + if (stage.isMandatoryStage()) { + LOGGER.warn("Failed to assign sample value to required stage '{}'", stage.getName()); + } + } + + if (!exampleNodes.isEmpty()) { + resourceCreateExample.getParameters().add(new ParameterExample(fluentMethod, exampleNodes)); + } + } + } + return resourceCreateExample; + } + + public static FluentResourceCreateExample parseResourceCreate(FluentResourceCollection resourceCollection, ResourceCreate create, ProxyMethodExample example) { + List methodParameters = MethodUtil.getParameters( + create.getMethodReferences() + .stream() + .filter(collectionMethod-> FluentUtils.requiresExample(collectionMethod.getInnerClientMethod())) + .findFirst().get() + .getInnerClientMethod()); + MethodParameter requestBodyParameter = findRequestBodyParameter(methodParameters); + return parseResourceCreate(resourceCollection, create, example, methodParameters, requestBodyParameter); + } + + private static List parseResourceUpdate(FluentResourceCollection collection, ResourceUpdate resourceUpdate) { + List ret = null; + + final boolean methodIsCreateOrUpdate = methodIsCreateOrUpdate(resourceUpdate.getResourceModel()); + FluentCollectionMethod resourceGetMethod = findResourceGetMethod(resourceUpdate); + if (resourceGetMethod == null) { + // 'get' method not found + return null; + } + List resourceGetMethodParameters = MethodUtil.getParameters(resourceGetMethod.getInnerClientMethod()); + + List collectionMethods = resourceUpdate.getMethodReferences(); + for (FluentCollectionMethod collectionMethod : collectionMethods) { + ClientMethod clientMethod = collectionMethod.getInnerClientMethod(); + if (FluentUtils.validRequestContentTypeToGenerateExample(clientMethod)) { + if (ret == null) { + ret = new ArrayList<>(); + } + + List methodParameters = MethodUtil.getParameters(clientMethod); + MethodParameter requestBodyParameter = findRequestBodyParameter(methodParameters); + + for (Map.Entry entry : collectionMethod.getInnerClientMethod().getProxyMethod().getExamples().entrySet()) { + if (methodIsCreateOrUpdate && !FluentUtils.exampleIsUpdate(entry.getKey())) { + // likely not a resource update example + LOGGER.info("Skip possible resource create example '{}' in update", entry.getKey()); + continue; + } + + LOGGER.info("Parse resource update example '{}'", entry.getKey()); + + ProxyMethodExample example = entry.getValue(); + FluentResourceUpdateExample resourceUpdateExample = parseResourceUpdate(collection, resourceUpdate, example, resourceGetMethod, resourceGetMethodParameters, methodParameters, requestBodyParameter); + + ret.add(resourceUpdateExample); + } + } + } + return ret; + } + + private static FluentCollectionMethod findResourceGetMethod(ResourceUpdate resourceUpdate) { + FluentCollectionMethod resourceGetMethod = null; + if (resourceUpdate.getResourceModel().getResourceRefresh() != null) { + resourceGetMethod = resourceUpdate.getResourceModel().getResourceRefresh().getMethodReferences().stream() + .filter(m -> m.getInnerClientMethod().getParameters().stream().anyMatch(p -> ClassType.CONTEXT.equals(p.getClientType()))) + .findFirst().orElse(null); + } + return resourceGetMethod; + } + + private static FluentResourceUpdateExample parseResourceUpdate(FluentResourceCollection collection, ResourceUpdate resourceUpdate, ProxyMethodExample example, FluentCollectionMethod resourceGetMethod, List resourceGetMethodParameters, List methodParameters, MethodParameter requestBodyParameter) { + FluentCollectionMethodExample resourceGetExample = + parseMethodForExample(collection, resourceGetMethod, resourceGetMethodParameters, example.getName(), example); + FluentResourceUpdateExample resourceUpdateExample = new FluentResourceUpdateExample( + example.getName(), example.getRelativeOriginalFileName(), + FluentStatic.getFluentManager(), collection, resourceUpdate, resourceGetExample); + + for (UpdateStage stage : resourceUpdate.getUpdateStages()) { + List fluentMethods = stage.getMethods(); + if (!fluentMethods.isEmpty()) { + FluentMethod fluentMethod = fluentMethods.iterator().next(); + List exampleNodes = new ArrayList<>(); + + if (stage instanceof UpdateStageApply) { + // apply stage does not have parameter + } else if (stage instanceof UpdateStageMisc) { + UpdateStageMisc miscStage = (UpdateStageMisc) stage; + MethodParameter methodParameter = findMethodParameter(methodParameters, miscStage.getMethodParameter()); + ExampleNode node = ModelExampleUtil.parseNodeFromParameter(example, methodParameter); + + if (!node.isNull()) { + exampleNodes.add(node); + } + } else { + ModelProperty modelProperty = stage.getModelProperty(); + if (modelProperty != null) { + ExampleNode node = parseNodeFromModelProperty(example, requestBodyParameter, resourceUpdate.getRequestBodyParameterModel(), modelProperty); + + if (!node.isNull()) { + exampleNodes.add(node); + } + } + } + + if (!exampleNodes.isEmpty()) { + resourceUpdateExample.getParameters().add(new ParameterExample(fluentMethod, exampleNodes)); + } + } + } + return resourceUpdateExample; + } + + public static FluentResourceUpdateExample parseResourceUpdate(FluentResourceCollection resourceCollection, ResourceUpdate resourceUpdate, ProxyMethodExample example) { + FluentCollectionMethod resourceGetMethod = findResourceGetMethod(resourceUpdate); + if (resourceGetMethod == null) { + // 'get' method not found + return null; + } + List resourceGetMethodParameters = MethodUtil.getParameters(resourceGetMethod.getInnerClientMethod()); + List methodParameters = MethodUtil.getParameters( + resourceUpdate.getMethodReferences() + .stream() + .filter(collectionMethod-> FluentUtils.requiresExample(collectionMethod.getInnerClientMethod())) + .findFirst().get() + .getInnerClientMethod() + ); + MethodParameter requestBodyParameter = findRequestBodyParameter(methodParameters); + return parseResourceUpdate(resourceCollection, resourceUpdate, example, resourceGetMethod, resourceGetMethodParameters, methodParameters, requestBodyParameter); + + } + + protected static MethodParameter findRequestBodyParameter(List methodParameters) { + return methodParameters.stream() + .filter(p -> p.getProxyMethodParameter().getRequestParameterLocation() == RequestParameterLocation.BODY) + .findFirst().orElse(null); + } + + private static MethodParameter findMethodParameter(List methodParameters, ClientMethodParameter clientMethodParameter) { + MethodParameter parameter = methodParameters.stream() + .filter(p -> p.getClientMethodParameter() == clientMethodParameter) + .findFirst().orElse(null); + if (parameter == null) { + parameter = methodParameters.stream() + .filter(p -> p.getClientMethodParameter().getName().equals(clientMethodParameter.getName())) + .findFirst().orElse(null); + } + return parameter; + } + + private static ExampleNode parseNodeFromModelProperty(ProxyMethodExample example, MethodParameter methodParameter, + ClientModel clientModel, ModelProperty modelProperty) { + String serializedName = methodParameter.getProxyMethodParameter().getName(); + + ProxyMethodExample.ParameterValue parameterValue = ModelExampleUtil.findParameter(example, serializedName); + ExampleNode node; + if (parameterValue == null) { + node = new LiteralNode(modelProperty.getClientType(), null); + } else { + List jsonPropertyNames = modelProperty.getSerializedNames(); + + Object childObjectValue = ModelExampleUtil.getChildObjectValue(jsonPropertyNames, parameterValue.getObjectValue()); + if (childObjectValue != null) { + node = ModelExampleUtil.parseNode(modelProperty.getClientType(), modelProperty.getWireType(), childObjectValue); + } else { + node = new LiteralNode(modelProperty.getClientType(), null); + } + } + return node; + } + + private static boolean methodIsCreateOrUpdate(FluentResourceModel resourceModel) { + return resourceModel.getResourceCreate() != null && resourceModel.getResourceUpdate() != null + && Objects.equals(resourceModel.getResourceCreate().getMethodReferences().iterator().next().getMethodName(), resourceModel.getResourceUpdate().getMethodReferences().iterator().next().getMethodName()); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/mapper/FluentClientMethodMapper.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/mapper/FluentClientMethodMapper.java new file mode 100644 index 0000000000..3990139e64 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/mapper/FluentClientMethodMapper.java @@ -0,0 +1,126 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.mapper; + +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Operation; +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentStatic; +import com.microsoft.typespec.http.client.generator.core.mapper.ClientMethodMapper; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethod; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethodParameter; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethodType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.IType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ProxyMethod; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaVisibility; + +import java.util.List; + +public class FluentClientMethodMapper extends ClientMethodMapper { + + private static final FluentClientMethodMapper INSTANCE = new FluentClientMethodMapper(); + + public static FluentClientMethodMapper getInstance() { + return INSTANCE; + } + + @Override + protected void createAdditionalLroMethods( + Operation operation, ClientMethod.Builder builder, List methods, boolean isProtocolMethod, + IType asyncReturnType, IType syncReturnType, ProxyMethod proxyMethod, List parameters, + boolean generateClientMethodWithOnlyRequiredParameters, MethodOverloadType defaultOverloadType) { + + // fluent provides the simple wrapper API for LRO + // the difference is that it does not have a RestResponse overload, as Response data is not included in an LRO API + + // async + methods.add(builder + .returnValue(createLongRunningAsyncReturnValue(operation, asyncReturnType, syncReturnType)) + .name(proxyMethod.getSimpleAsyncMethodName()) + .onlyRequiredParameters(false) + .type(ClientMethodType.LongRunningAsync) + .groupedParameterRequired(false) + .methodVisibility(methodVisibility(ClientMethodType.LongRunningAsync, defaultOverloadType, false, isProtocolMethod)) + .build()); + + if (generateClientMethodWithOnlyRequiredParameters) { + methods.add(builder + .onlyRequiredParameters(true) + .methodVisibility(methodVisibility(ClientMethodType.LongRunningAsync, MethodOverloadType.OVERLOAD_MINIMUM, false, isProtocolMethod)) + .build()); + } + + addClientMethodWithContext(methods, + builder.methodVisibility(methodVisibility(ClientMethodType.LongRunningAsync, defaultOverloadType, true, isProtocolMethod)), + parameters, getContextParameter(isProtocolMethod)); + + // sync + methods.add(builder + .returnValue(createLongRunningSyncReturnValue(operation, syncReturnType)) + .name(proxyMethod.getName()) + .onlyRequiredParameters(false) + .type(ClientMethodType.LongRunningSync) + .groupedParameterRequired(false) + .onlyRequiredParameters(true) + .methodVisibility(methodVisibility(ClientMethodType.LongRunningSync, defaultOverloadType, false, isProtocolMethod)) + .build()); + + if (generateClientMethodWithOnlyRequiredParameters) { + methods.add(builder + .onlyRequiredParameters(true) + .methodVisibility(methodVisibility(ClientMethodType.LongRunningSync, MethodOverloadType.OVERLOAD_MINIMUM, false, isProtocolMethod)) + .build()); + } + + addClientMethodWithContext(methods, + builder.methodVisibility(methodVisibility(ClientMethodType.LongRunningSync, defaultOverloadType, true, isProtocolMethod)), + parameters, getContextParameter(isProtocolMethod)); + } + + @Override + protected JavaVisibility methodVisibility( + ClientMethodType methodType, + MethodOverloadType methodOverloadType, + boolean hasContextParameter, + boolean isProtocolMethod) { + + JavaVisibility visibility; + if (methodType == ClientMethodType.PagingAsyncSinglePage) { + // utility methods + // single page method is not visible, but the method is required for other client methods + visibility = NOT_VISIBLE; + } else if (methodType == ClientMethodType.PagingSyncSinglePage) { + // wait for sync-stack to decide + visibility = NOT_GENERATE; + } else if (hasContextParameter && (methodType == ClientMethodType.SimpleAsyncRestResponse || methodType == ClientMethodType.PagingAsync || methodType == ClientMethodType.LongRunningBeginAsync || methodType == ClientMethodType.LongRunningAsync)) { + // utility methods + // async + Context method is not visible, but the method is required for other client methods + visibility = NOT_VISIBLE; + } else { + if (methodType.name().contains("Async") && hasContextParameter) { + // async method has both minimum overload and maximum overload, but no overload with Context parameter + visibility = NOT_GENERATE; + } else if (methodType == ClientMethodType.SimpleSync && hasContextParameter) { + // SimpleSync with Context is covered by SimpleSyncRestResponse with Context + visibility = NOT_GENERATE; + } else if (methodType == ClientMethodType.SimpleAsync && methodOverloadType == MethodOverloadType.OVERLOAD_MAXIMUM) { + // SimpleAsync with maximum overload is covered by SimpleAsyncRestResponse + visibility = NOT_GENERATE; + } else if (((methodType.name().contains("Sync") && !hasContextParameter)) + && ((methodOverloadType.value() & MethodOverloadType.OVERLOAD_MINIMUM.value()) != MethodOverloadType.OVERLOAD_MINIMUM.value())) { + // sync method has both minimum overload and maximum overload + Context parameter, but not maximum overload without Context parameter + visibility = NOT_GENERATE; + } else { + visibility = super.methodVisibility(methodType, methodOverloadType, hasContextParameter, isProtocolMethod); + } + } + + if (JavaSettings.getInstance().isFluentLite() && !FluentStatic.getFluentJavaSettings().isGenerateAsyncMethods()) { + // by default, Fluent lite disable all async method + if (visibility == JavaVisibility.Public && methodType.name().contains("Async")) { + visibility = JavaVisibility.Private; + } + } + return visibility; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/mapper/FluentExceptionMapper.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/mapper/FluentExceptionMapper.java new file mode 100644 index 0000000000..0ae18fefd6 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/mapper/FluentExceptionMapper.java @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.mapper; + +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.ObjectSchema; +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; +import com.microsoft.typespec.http.client.generator.mgmt.model.FluentType; +import com.microsoft.typespec.http.client.generator.mgmt.util.Utils; +import com.microsoft.typespec.http.client.generator.core.mapper.ExceptionMapper; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClassType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientException; + +public class FluentExceptionMapper extends ExceptionMapper { + + private static final FluentExceptionMapper INSTANCE = new FluentExceptionMapper(); + + protected FluentExceptionMapper() { + } + + public static FluentExceptionMapper getInstance() { + return INSTANCE; + } + + protected ClientException buildException(ObjectSchema compositeType, JavaSettings settings) { + if (!FluentType.nonManagementError(Utils.getJavaName(compositeType))) { + // Use ManagementException directly, no need to build new Exception class. + return null; + } + + String errorName = compositeType.getLanguage().getJava().getName(); + String methodOperationExceptionTypeName = errorName + "Exception"; + + boolean isManagementException = compositeType.getParents() != null + && !FluentType.nonManagementError(Utils.getJavaName(compositeType.getParents().getImmediate().get(0))); + + ClientException exception = new ClientException.Builder() + .packageName(settings.getPackage(settings.getModelsSubpackage())) + .name(methodOperationExceptionTypeName) + .errorName(errorName) + .parentType(isManagementException ? FluentType.MANAGEMENT_EXCEPTION : ClassType.HTTP_RESPONSE_EXCEPTION) + .build(); + return exception; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/mapper/FluentLiveTestsMapper.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/mapper/FluentLiveTestsMapper.java new file mode 100644 index 0000000000..ae4a852a9f --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/mapper/FluentLiveTestsMapper.java @@ -0,0 +1,171 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.mapper; + +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.CodeModel; +import com.microsoft.typespec.http.client.generator.core.extension.plugin.PluginLogger; +import com.microsoft.typespec.http.client.generator.mgmt.FluentGen; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentClient; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentCollectionMethod; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentExampleLiveTestStep; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentLiveTestCase; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentLiveTestStep; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentLiveTests; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentResourceCollection; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.examplemodel.FluentCollectionMethodExample; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.examplemodel.FluentExample; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.examplemodel.FluentResourceCreateExample; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.examplemodel.FluentResourceUpdateExample; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.create.ResourceCreate; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.update.ResourceUpdate; +import com.microsoft.typespec.http.client.generator.mgmt.template.FluentExampleTemplate; +import com.microsoft.typespec.http.client.generator.mgmt.util.FluentJavaSettings; +import com.microsoft.typespec.http.client.generator.mgmt.util.FluentUtils; +import com.microsoft.typespec.http.client.generator.mgmt.util.Utils; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ExampleLiveTestStep; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.LiveTestStep; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.LiveTests; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ProxyMethodExample; +import com.microsoft.typespec.http.client.generator.core.util.CodeNamer; + +import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * A mapper to map vanilla live tests to fluent live tests. + */ +public class FluentLiveTestsMapper { + private final PluginLogger logger = new PluginLogger(FluentGen.getPluginInstance(), FluentLiveTestsMapper.class); + + private final FluentExampleTemplate fluentExampleTemplate = FluentExampleTemplate.getInstance(); + + private static final FluentLiveTestsMapper INSTANCE = new FluentLiveTestsMapper(); + + public static FluentLiveTestsMapper getInstance(){ + return INSTANCE; + } + + public FluentLiveTests map(LiveTests liveTests, FluentClient fluentClient, CodeModel codeModel, FluentJavaSettings fluentJavaSettings) { + + FluentLiveTests.Builder resultBuilder = FluentLiveTests.newBuilder(); + + resultBuilder.className(liveTests.getFilename() + "Tests"); + + resultBuilder.addTestCases(liveTests.getTestCases().stream().map(liveTestCase -> { + FluentLiveTestCase.Builder testCaseBuilder = FluentLiveTestCase.newBuilder().methodName(CodeNamer.toCamelCase(liveTestCase.getName())); + testCaseBuilder.addSteps( + liveTestCase.getTestSteps() + .stream() + // future work: support other step types + .filter(testStep -> testStep instanceof ExampleLiveTestStep) + .map((Function>) step -> { + ExampleLiveTestStep exampleStep = (ExampleLiveTestStep) step; + String operationId = exampleStep.getOperationId(); + // operationId is from testModel, if it's null, ignore the step. + if (operationId == null) { + logger.warn(String.format("null operationId found for example file step : %s", exampleStep.getExample().getName())); + return Optional.empty(); + } + OperationGroupPair operationGroupPair = getOperationGroupPair(operationId, codeModel, fluentJavaSettings); + String operationGroup = operationGroupPair.operationGroup; + String operation = operationGroupPair.operation; + ProxyMethodExample example = exampleStep.getExample(); + FluentResourceCollection resourceCollection = findResourceCollection(fluentClient, operationGroup); + + FluentExampleTemplate.ExampleMethod exampleMethod = null; + // find collectionMethod + Optional collectionMethodOptional = findCollectionMethod(resourceCollection, operation); + if (collectionMethodOptional.isPresent()) { + FluentCollectionMethodExample collectionMethodExample = ExampleParser.parseMethodExample( + resourceCollection + , resourceCollection.getMethodsForTemplate() + .stream() + .filter(m -> m.getMethodName().contains(CodeNamer.toCamelCase(operation))) // getXxWithResponse + .collect(Collectors.toList()) + , example + ); + exampleMethod = fluentExampleTemplate.generateExampleMethod(collectionMethodExample); + setExampleStepFeatures(resultBuilder, testCaseBuilder, collectionMethodExample, exampleMethod); + } else { + // find resourceCreate + Optional createMethod = findResourceCreate(resourceCollection, operation); + if (createMethod.isPresent()) { + ResourceCreate create = createMethod.get(); + FluentResourceCreateExample createExample = ExampleParser.parseResourceCreate(resourceCollection, create, example); + exampleMethod = fluentExampleTemplate.generateExampleMethod(createExample); + setExampleStepFeatures(resultBuilder, testCaseBuilder, createExample, exampleMethod); + } else { + // find resourceUpdate + Optional updateMethod = resourceCollection.getResourceUpdates().stream().filter(rc -> FluentUtils.exampleIsUpdate(rc.getMethodName()) && rc.getMethodName().equalsIgnoreCase(operation)).findFirst(); + if (updateMethod.isPresent()) { + ResourceUpdate update = updateMethod.get(); + FluentResourceUpdateExample updateExample = ExampleParser.parseResourceUpdate(resourceCollection, update, example); + if (updateExample == null) { + return Optional.empty(); + } + exampleMethod = fluentExampleTemplate.generateExampleMethod(updateExample); + setExampleStepFeatures(resultBuilder, testCaseBuilder, updateExample, exampleMethod); + } + } + } + if (exampleMethod != null) { + resultBuilder.addHelperFeatures(testCaseBuilder.getHelperFeatures()); + return Optional.of(FluentExampleLiveTestStep.newBuilder().description(step.getDescription()).exampleMethod(exampleMethod).build()); + } else { + // can't find method, ignore the whole test case altogether + logger.warn(String.format("Operation : %s not found, ignore this test case.", operationId)); + return Optional.empty(); + } + }) + .filter(Optional::isPresent) + .map(Optional::get) + .collect(Collectors.toList())); + return testCaseBuilder.build(); + }).collect(Collectors.toList())); + + return resultBuilder.build(); + } + + private OperationGroupPair getOperationGroupPair(String operationId, CodeModel codeModel, FluentJavaSettings fluentJavaSettings) { + if (!operationId.contains("_")){ + return new OperationGroupPair(Utils.getNameForUngroupedOperations(codeModel, fluentJavaSettings), operationId); + } + String[] pair = operationId.split("_"); + return new OperationGroupPair(pair[0], pair[1]); + } + + private void setExampleStepFeatures(FluentLiveTests.Builder resultBuilder, FluentLiveTestCase.Builder testCaseBuilder, FluentExample fluentExample, FluentExampleTemplate.ExampleMethod exampleMethod) { + testCaseBuilder.addHelperFeatures(exampleMethod.getHelperFeatures()); + resultBuilder.addImports(exampleMethod.getImports()) + .managerName(fluentExample.getEntryName()) + .managerType(fluentExample.getEntryType()); + } + + private FluentResourceCollection findResourceCollection(FluentClient fluentClient, String operationGroup) { + return fluentClient.getResourceCollections().stream().filter(collection -> collection.getInterfaceType().getName().equalsIgnoreCase(CodeNamer.getPlural(operationGroup))).findFirst().get(); + } + + private Optional findCollectionMethod(FluentResourceCollection resourceCollection, String operation) { + return resourceCollection.getMethodsForTemplate().stream().filter(m -> m.getMethodName().contains(CodeNamer.toCamelCase(operation))).findFirst(); + } + + private Optional findResourceCreate(FluentResourceCollection resourceCollection, String operation) { + return resourceCollection.getResourceCreates().stream().filter(rc -> + !FluentUtils.exampleIsUpdate(rc.getMethodName()) && + rc.getMethodName().equalsIgnoreCase(operation)).findFirst(); + } + + private static class OperationGroupPair { + private final String operationGroup; + private final String operation; + + public OperationGroupPair(String operationGroup, String operation) { + this.operationGroup = operationGroup; + this.operation = operation; + } + } + + +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/mapper/FluentMapper.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/mapper/FluentMapper.java new file mode 100644 index 0000000000..398ba4f4fc --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/mapper/FluentMapper.java @@ -0,0 +1,309 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.mapper; + +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.ArraySchema; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.CodeModel; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.DictionarySchema; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.ObjectSchema; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Operation; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Response; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Value; +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; +import com.microsoft.typespec.http.client.generator.core.extension.plugin.PluginLogger; +import com.microsoft.typespec.http.client.generator.mgmt.FluentGen; +import com.microsoft.typespec.http.client.generator.mgmt.model.FluentType; +import com.microsoft.typespec.http.client.generator.mgmt.model.ResourceCollectionAssociation; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentClient; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentExample; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentManager; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentManagerProperty; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentResourceCollection; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentResourceModel; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentStatic; +import com.microsoft.typespec.http.client.generator.mgmt.util.FluentJavaSettings; +import com.microsoft.typespec.http.client.generator.mgmt.util.Utils; +import com.microsoft.typespec.http.client.generator.core.mapper.Mappers; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.Client; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientModels; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ModuleInfo; +import org.slf4j.Logger; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class FluentMapper { + + private static final Logger LOGGER = new PluginLogger(FluentGen.getPluginInstance(), FluentMapper.class); + + private final FluentJavaSettings fluentJavaSettings; + + public FluentMapper(FluentJavaSettings fluentJavaSettings) { + this.fluentJavaSettings = fluentJavaSettings; + } + + public void preModelMap(CodeModel codeModel) { + processInnerModel(codeModel); + FluentModelMapper.getInstance().addRemovedModels(fluentJavaSettings.getJavaNamesForRemoveModel()); + } + + public FluentClient map(CodeModel codeModel, Client client) { + FluentClient fluentClient = basicMap(codeModel, client); + + // parse resource collections to identify create/update/refresh flow on resource instance + for (ResourceCollectionAssociation overrideAssociation : fluentJavaSettings.getResourceCollectionAssociations()) { + String modelName = overrideAssociation.getResource(); + String collectionName = overrideAssociation.getCollection(); + Optional modelOpt = fluentClient.getResourceModels().stream().filter(m -> Objects.equals(m.getName(), modelName)).findFirst(); + if (modelOpt.isPresent()) { + FluentResourceModel model = modelOpt.get(); + if (collectionName == null) { +// // this resource model does not associate with any collection +// // use a dummy ResourceCreate to prevent future parseResourcesCategory invocation from process the model +// model.setResourceCreate(ResourceCreate.NO_ASSOCIATION); + } else { + Optional collectionOpt = fluentClient.getResourceCollections().stream().filter(c -> Objects.equals(c.getInterfaceType().getName(), collectionName)).findFirst(); + if (collectionOpt.isPresent()) { + FluentResourceCollection collection = collectionOpt.get(); + ResourceParser.parseResourcesCategory(collection, Collections.singletonList(model), FluentStatic.getClient().getModels()); + } else { + LOGGER.warn("Resource collection '{}' not found in association override '{}' to '{}'.", collectionName, modelName, collectionName); + } + } + } else { + LOGGER.warn("Resource model '{}' not found in association override '{}' to '{}'.", modelName, modelName, collectionName); + } + } + fluentClient.getResourceCollections() + .forEach(c -> ResourceParser.parseResourcesCategory(c, fluentClient.getResourceModels(), FluentStatic.getClient().getModels())); +// // clean up NO_ASSOCIATION +// for (FluentResourceModel model : fluentClient.getResourceModels()) { +// if (model.getResourceCreate() == ResourceCreate.NO_ASSOCIATION) { +// model.setResourceCreate(null); +// } +// } + ResourceParser.processAdditionalMethods(fluentClient); + + // samples + if (fluentJavaSettings.isGenerateSamples()) { + ExampleParser exampleParser = new ExampleParser(); + List examples = fluentClient.getResourceCollections().stream() + .flatMap(rc -> exampleParser.parseResourceCollection(rc).stream()) + .sorted() + .collect(Collectors.toList()); + fluentClient.getExamples().addAll(examples); + } + + if (JavaSettings.getInstance().isGenerateTests()) { + // live tests + fluentClient.getLiveTests().addAll( + client.getLiveTests() + .stream() + .map(liveTests -> FluentLiveTestsMapper.getInstance().map(liveTests, fluentClient, codeModel, fluentJavaSettings)) + .collect(Collectors.toList())); + + // mock API tests + MockTestParser mockUnitTestParser = new MockTestParser(); + fluentClient.getMockUnitTests().addAll( + fluentClient.getResourceCollections().stream() + .flatMap(rc -> mockUnitTestParser.parseResourceCollectionForUnitTest(rc).stream()) + .collect(Collectors.toList())); + } + + return fluentClient; + } + + FluentClient basicMap(CodeModel codeModel, Client client) { + FluentClient fluentClient = new FluentClient(client); + + final String implementationModelsPackage = JavaSettings.getInstance().getImplementationSubpackage() + "." + JavaSettings.getInstance().getModelsSubpackage(); + final boolean hasImplementationModels = ClientModels.getInstance().getModels().stream() + .anyMatch(m -> m.getPackage().endsWith(implementationModelsPackage)); + fluentClient.setModuleInfo(getModuleInfo(hasImplementationModels)); + + FluentStatic.setFluentClient(fluentClient); + + // manager, service API + fluentClient.setManager(new FluentManager(client, Utils.getJavaName(codeModel))); + + // wrapper for response objects, potentially as resource instance + fluentClient.getResourceModels().addAll( + codeModel.getSchemas().getObjects().stream() + .map(o -> FluentResourceModelMapper.getInstance().map(o)) + .filter(Objects::nonNull) + .collect(Collectors.toList())); + + // resource collection APIs + fluentClient.getResourceCollections().addAll( + codeModel.getOperationGroups().stream() + .map(og -> FluentResourceCollectionMapper.getInstance().map(og)) + .filter(Objects::nonNull) + .collect(Collectors.toList())); + + // set resource collection APIs to service API + fluentClient.getManager().getProperties().addAll( + fluentClient.getResourceCollections().stream() + .map(FluentManagerProperty::new) + .collect(Collectors.toList())); + + return fluentClient; + } + + private static ModuleInfo getModuleInfo(boolean hasImplementationModels) { + JavaSettings settings = JavaSettings.getInstance(); + ModuleInfo moduleInfo = new ModuleInfo(settings.getPackage()); + + List requireModules = moduleInfo.getRequireModules(); + requireModules.add(new ModuleInfo.RequireModule("com.azure.core.management", true)); + + List exportModules = moduleInfo.getExportModules(); + exportModules.add(new ModuleInfo.ExportModule(settings.getPackage())); + exportModules.add(new ModuleInfo.ExportModule(settings.getPackage(settings.getFluentSubpackage()))); + exportModules.add(new ModuleInfo.ExportModule(settings.getPackage(settings.getFluentModelsSubpackage()))); + exportModules.add(new ModuleInfo.ExportModule(settings.getPackage(settings.getModelsSubpackage()))); + + List openToModules = new ArrayList<>(); + openToModules.add("com.azure.core"); + if (!settings.isStreamStyleSerialization()) { + openToModules.add("com.fasterxml.jackson.databind"); + } + List openModules = moduleInfo.getOpenModules(); + openModules.add(new ModuleInfo.OpenModule(settings.getPackage(settings.getFluentModelsSubpackage()), openToModules)); + openModules.add(new ModuleInfo.OpenModule(settings.getPackage(settings.getModelsSubpackage()), openToModules)); + if (hasImplementationModels) { + openModules.add(new ModuleInfo.OpenModule(settings.getPackage(settings.getImplementationSubpackage(), settings.getModelsSubpackage()), openToModules)); + } + + return moduleInfo; + } + + private void processInnerModel(CodeModel codeModel) { + // Add "Inner" to all method response types, and recursively to types having it as property. + + final FluentObjectMapper objectMapper = (FluentObjectMapper) Mappers.getObjectMapper(); + + Set compositeTypes = Stream.concat(Stream.concat(Stream.concat( + // ObjectSchema + codeModel.getOperationGroups().stream() + .flatMap(og -> og.getOperations().stream()) + .filter(o -> !isPossiblePagedList(o)) + .flatMap(o -> o.getResponses().stream()) + .map(Response::getSchema), + // Paged list + codeModel.getOperationGroups().stream() + .flatMap(og -> og.getOperations().stream()) + .filter(FluentMapper::isPossiblePagedList) + .flatMap(o -> o.getResponses().stream()) + .filter(r -> r.getSchema() instanceof ObjectSchema) + .map(r -> (ObjectSchema) r.getSchema()) + .flatMap(s -> s.getProperties().stream()) + .filter(p -> p.getSerializedName().equals("value") && p.getSchema() instanceof ArraySchema) + .map(p -> ((ArraySchema) p.getSchema()).getElementType())), + // ArraySchema + codeModel.getOperationGroups().stream() + .flatMap(og -> og.getOperations().stream()) + .flatMap(o -> o.getResponses().stream()) + .map(Response::getSchema) + .filter(s -> s instanceof ArraySchema) + .map(s -> ((ArraySchema) s).getElementType())), + // DictionarySchema + codeModel.getOperationGroups().stream() + .flatMap(og -> og.getOperations().stream()) + .flatMap(o -> o.getResponses().stream()) + .map(Response::getSchema) + .filter(s -> s instanceof DictionarySchema) + .map(s -> ((DictionarySchema) s).getElementType())) + .filter(s -> s instanceof ObjectSchema) + .map(s -> (ObjectSchema) s) + .filter(FluentType::nonResourceType) + .collect(Collectors.toSet()); + + Set errorTypes = codeModel.getOperationGroups().stream() + .flatMap(og -> og.getOperations().stream()) + .flatMap(o -> o.getExceptions().stream()) + .map(Response::getSchema) + .filter(s -> s instanceof ObjectSchema) + .map(s -> (ObjectSchema) s) + .filter(o -> FluentType.nonManagementError(Utils.getJavaName(o))) + .collect(Collectors.toSet()); + + compositeTypes.removeAll(errorTypes); + + compositeTypes = objectMapper.addInnerModels(compositeTypes); + if (LOGGER.isInfoEnabled()) { + LOGGER.info("Add Inner to response types: {}", + compositeTypes.stream().map(Utils::getJavaName).collect(Collectors.toList())); + } + recursiveAddInnerModel(objectMapper, codeModel, compositeTypes); + + final Set javaNamesForAddInner = fluentJavaSettings.getJavaNamesForAddInner(); + if (!javaNamesForAddInner.isEmpty()) { + compositeTypes = codeModel.getSchemas().getObjects().stream() + .filter(s -> javaNamesForAddInner.contains(Utils.getJavaName(s))) + .collect(Collectors.toSet()); + + compositeTypes = objectMapper.addInnerModels(compositeTypes); + if (LOGGER.isInfoEnabled()) { + LOGGER.info("Add Inner as requested: {}", + compositeTypes.stream().map(Utils::getJavaName).collect(Collectors.toList())); + } + recursiveAddInnerModel(objectMapper, codeModel, compositeTypes); + } + + final Set javaNamesForRemoveInner = fluentJavaSettings.getJavaNamesForRemoveInner(); + if (!javaNamesForRemoveInner.isEmpty()) { + objectMapper.removeInnerModels(javaNamesForRemoveInner); + } + } + + private static boolean isPossiblePagedList(Operation operation) { + return (operation.getExtensions() != null && operation.getExtensions().getXmsPageable() != null); +// || (Utils.getJavaName(operation).equals(WellKnownMethodName.LIST) || Utils.getJavaName(operation).equals(WellKnownMethodName.LIST_BY_RESOURCE_GROUP)); + } + + private static void recursiveAddInnerModel(FluentObjectMapper objectMapper, CodeModel codeModel, Collection compositeTypes) { + compositeTypes.forEach(s -> recursiveAddInnerModel(objectMapper, codeModel, Utils.getJavaName(s))); + } + + private static void recursiveAddInnerModel(FluentObjectMapper objectMapper, CodeModel codeModel, String typeName) { + if (typeName == null) return; + + Set compositeTypesInProperties = Stream.concat(Stream.concat( + // ObjectSchema + codeModel.getSchemas().getObjects().stream(), + // ArraySchema + codeModel.getSchemas().getArrays().stream() + .map(ArraySchema::getElementType) + .filter(s -> s instanceof ObjectSchema) + .map(s -> (ObjectSchema) s)), + // DictionarySchema + codeModel.getSchemas().getDictionaries().stream() + .map(DictionarySchema::getElementType) + .filter(s -> s instanceof ObjectSchema) + .map(s -> (ObjectSchema) s)) + .filter(s -> { + if (s.getProperties() == null) return false; + return s.getProperties().stream() + .map(Value::getSchema) + .anyMatch(t -> t instanceof ObjectSchema && typeName.equals(Utils.getJavaName(t))); + }) + .collect(Collectors.toSet()); + + if (!compositeTypesInProperties.isEmpty()) { + compositeTypesInProperties = objectMapper.addInnerModels(compositeTypesInProperties); + if (LOGGER.isInfoEnabled()) { + LOGGER.info("Add Inner for type '{}': {}", typeName, + compositeTypesInProperties.stream().map(Utils::getJavaName).collect(Collectors.toList())); + } + recursiveAddInnerModel(objectMapper, codeModel, compositeTypesInProperties); + } + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/mapper/FluentMapperFactory.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/mapper/FluentMapperFactory.java new file mode 100644 index 0000000000..5d7ca385fb --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/mapper/FluentMapperFactory.java @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.mapper; + +import com.microsoft.typespec.http.client.generator.core.mapper.ClientMethodMapper; +import com.microsoft.typespec.http.client.generator.core.mapper.DefaultMapperFactory; +import com.microsoft.typespec.http.client.generator.core.mapper.ExceptionMapper; +import com.microsoft.typespec.http.client.generator.core.mapper.MethodGroupMapper; +import com.microsoft.typespec.http.client.generator.core.mapper.ModelMapper; +import com.microsoft.typespec.http.client.generator.core.mapper.ObjectMapper; +import com.microsoft.typespec.http.client.generator.core.mapper.PrimitiveMapper; +import com.microsoft.typespec.http.client.generator.core.mapper.ProxyMethodMapper; + +public class FluentMapperFactory extends DefaultMapperFactory { + + @Override + public ObjectMapper getObjectMapper() { + return FluentObjectMapper.getInstance(); + } + + @Override + public MethodGroupMapper getMethodGroupMapper() { + return FluentMethodGroupMapper.getInstance(); + } + + @Override + public ProxyMethodMapper getProxyMethodMapper() { + return FluentProxyMethodMapper.getInstance(); + } + + @Override + public ExceptionMapper getExceptionMapper() { + return FluentExceptionMapper.getInstance(); + } + + @Override + public ClientMethodMapper getClientMethodMapper() { + return FluentClientMethodMapper.getInstance(); + } + + @Override + public PrimitiveMapper getPrimitiveMapper() { + return FluentPrimitiveMapper.getInstance(); + } + + @Override + public ModelMapper getModelMapper() { + return FluentModelMapper.getInstance(); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/mapper/FluentMethodGroupMapper.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/mapper/FluentMethodGroupMapper.java new file mode 100644 index 0000000000..52d26630eb --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/mapper/FluentMethodGroupMapper.java @@ -0,0 +1,107 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.mapper; + +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.OperationGroup; +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; +import com.microsoft.typespec.http.client.generator.core.extension.plugin.PluginLogger; +import com.microsoft.typespec.http.client.generator.mgmt.FluentGen; +import com.microsoft.typespec.http.client.generator.mgmt.model.FluentType; +import com.microsoft.typespec.http.client.generator.mgmt.model.WellKnownMethodName; +import com.microsoft.typespec.http.client.generator.mgmt.util.TypeConversionUtils; +import com.microsoft.typespec.http.client.generator.mgmt.util.Utils; +import com.microsoft.typespec.http.client.generator.core.mapper.MethodGroupMapper; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethod; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.GenericType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.IType; +import org.slf4j.Logger; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; + +public class FluentMethodGroupMapper extends MethodGroupMapper { + + private static final Logger LOGGER = new PluginLogger(FluentGen.getPluginInstance(), FluentMethodGroupMapper.class); + + private static final FluentMethodGroupMapper INSTANCE = new FluentMethodGroupMapper(); + + public static FluentMethodGroupMapper getInstance() { + return INSTANCE; + } + + @Override + protected List supportedInterfaces(OperationGroup operationGroup, List clientMethods) { + if (!JavaSettings.getInstance().isFluentLite()) { + return findSupportedInterfaces(operationGroup, clientMethods); + } else { + return Collections.emptyList(); + } + } + + List findSupportedInterfaces(OperationGroup operationGroup, List clientMethods) { + List interfaces = new ArrayList<>(); + Optional classTypeForGet = supportGetMethod(clientMethods); + Optional classTypeForList = supportListMethod(clientMethods); + Optional classTypeForDelete = supportDeleteMethod(clientMethods); + + classTypeForGet.ifPresent(iType -> interfaces.add(FluentType.InnerSupportsGet(iType))); + classTypeForList.ifPresent(iType -> interfaces.add(FluentType.InnerSupportsList(iType))); + classTypeForDelete.ifPresent(iType -> interfaces.add(FluentType.InnerSupportsDelete(iType))); + + if (!interfaces.isEmpty()) { + LOGGER.info("Method group '{}' support interfaces {}", + Utils.getJavaName(operationGroup), + interfaces.stream().map(IType::toString).collect(Collectors.toList())); + } + return interfaces; + } + + private Optional supportGetMethod(List clientMethods) { + return clientMethods.stream() + .filter(m -> WellKnownMethodName.GET_BY_RESOURCE_GROUP.getMethodName().equals(m.getName()) + && checkNonClientRequiredParameters(m, 2)) + .map(m -> m.getReturnValue().getType()) + .findFirst(); + } + + private Optional supportDeleteMethod(List clientMethods) { + return clientMethods.stream() + .filter(m -> WellKnownMethodName.DELETE.getMethodName().equals(m.getName()) + && checkNonClientRequiredParameters(m, 2)) + .map(m -> m.getReturnValue().getType()) + .findFirst(); + } + + private Optional supportListMethod(List clientMethods) { + Optional listType = clientMethods.stream() + .filter(m -> WellKnownMethodName.LIST.getMethodName().equals(m.getName()) + && checkNonClientRequiredParameters(m, 0)) + .map(m -> m.getReturnValue().getType()) + .findFirst(); + + Optional listByResourceGroupType =clientMethods.stream() + .filter(m -> WellKnownMethodName.LIST_BY_RESOURCE_GROUP.getMethodName().equals(m.getName()) + && checkNonClientRequiredParameters(m, 1)) + .map(m -> m.getReturnValue().getType()) + .findFirst(); + + Optional commonListType = (listType.isPresent() && listByResourceGroupType.isPresent() && Objects.equals(listType.get().toString(), listByResourceGroupType.get().toString())) + ? listType + : Optional.empty(); + + return commonListType.filter(TypeConversionUtils::isPagedIterable) + .map(t -> ((GenericType) t).getTypeArguments()[0]); + } + + private boolean checkNonClientRequiredParameters(ClientMethod clientMethod, int requiredCount) { + final boolean countRequiredParametersOnly = JavaSettings.getInstance().isRequiredParameterClientMethods(); + return requiredCount == clientMethod.getParameters().stream() + .filter(p -> (!countRequiredParametersOnly || p.isRequired()) && !p.isConstant() && !p.isFromClient()) + .count(); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/mapper/FluentModelMapper.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/mapper/FluentModelMapper.java new file mode 100644 index 0000000000..b42a6b4ccc --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/mapper/FluentModelMapper.java @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.mapper; + +import com.microsoft.typespec.http.client.generator.mgmt.model.FluentType; +import com.microsoft.typespec.http.client.generator.core.mapper.ModelMapper; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClassType; + +import java.util.HashSet; +import java.util.Set; + +public class FluentModelMapper extends ModelMapper { + + private static final FluentModelMapper INSTANCE = new FluentModelMapper(); + + private final Set removedModels = new HashSet<>(); + + public static FluentModelMapper getInstance() { + return INSTANCE; + } + + @Override + protected boolean isPredefinedModel(ClassType modelType) { + return !FluentType.nonResourceType(modelType) + || !FluentType.nonManagementError(modelType) + || !FluentType.nonSystemData(modelType) + || removedModels.contains(modelType.getName()); + } + + public void addRemovedModels(Set models) { + removedModels.addAll(models); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/mapper/FluentObjectMapper.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/mapper/FluentObjectMapper.java new file mode 100644 index 0000000000..7207c2019b --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/mapper/FluentObjectMapper.java @@ -0,0 +1,76 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.mapper; + +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.ObjectSchema; +import com.microsoft.typespec.http.client.generator.mgmt.model.FluentType; +import com.microsoft.typespec.http.client.generator.mgmt.util.Utils; +import com.microsoft.typespec.http.client.generator.core.mapper.ObjectMapper; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClassType; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +public class FluentObjectMapper extends ObjectMapper { + + private static final FluentObjectMapper INSTANCE = new FluentObjectMapper(); + + public static FluentObjectMapper getInstance() { + return INSTANCE; + } + + private final Set innerModels = ConcurrentHashMap.newKeySet(); + + @Override + protected boolean isInnerModel(ObjectSchema compositeType) { + return innerModels.contains(compositeType); + } + + @Override + protected ClassType mapPredefinedModel(ObjectSchema compositeType) { + ClassType result = null; + if (compositeType.getLanguage().getJava().getName().equals(FluentType.RESOURCE.getName())) { + result = FluentType.RESOURCE; + } else if (compositeType.getLanguage().getJava().getName().equals(FluentType.PROXY_RESOURCE.getName())) { + result = FluentType.PROXY_RESOURCE; + } else if (compositeType.getLanguage().getJava().getName().equals(FluentType.SUB_RESOURCE.getName())) { + result = FluentType.SUB_RESOURCE; + } else if (compositeType.getLanguage().getJava().getName().equals(FluentType.MANAGEMENT_ERROR.getName())) { + result = FluentType.MANAGEMENT_ERROR; + } else if (compositeType.getLanguage().getJava().getName().equals(FluentType.SYSTEM_DATA.getName())) { + result = FluentType.SYSTEM_DATA; + } else if (compositeType.getLanguage().getJava().getName().equals(FluentType.ADDITIONAL_INFO.getName())) { + result = FluentType.ADDITIONAL_INFO; + } + return result; + } + + /** + * Add types as Inner. + * + * @param compositeTypes The types to add as Inner. + * @return The types from compositeTypes that need to be added. + */ + public Set addInnerModels(Collection compositeTypes) { + Set compositeTypesToAdd = new HashSet<>(compositeTypes); + compositeTypesToAdd.removeAll(innerModels); + innerModels.addAll(compositeTypesToAdd); + return compositeTypesToAdd; + } + + /** + * Remove types as Inner. + * + * @param javaNames The Java class names to remove as Inner. + */ + public void removeInnerModels(Set javaNames) { + Set compositeTypesToRemove = innerModels.stream() + .filter(type -> javaNames.contains(Utils.getJavaName(type))) + .collect(Collectors.toSet()); + innerModels.removeAll(compositeTypesToRemove); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/mapper/FluentPomMapper.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/mapper/FluentPomMapper.java new file mode 100644 index 0000000000..f922c27bc7 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/mapper/FluentPomMapper.java @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.mapper; + +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; +import com.microsoft.typespec.http.client.generator.mgmt.model.projectmodel.FluentProject; +import com.microsoft.typespec.http.client.generator.core.mapper.PomMapper; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.Pom; +import com.microsoft.typespec.http.client.generator.core.model.projectmodel.Project; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +public class FluentPomMapper extends PomMapper { + + public Pom map(FluentProject project) { + Pom pom = new Pom(); + pom.setGroupId(project.getGroupId()); + pom.setArtifactId(project.getArtifactId()); + pom.setVersion(project.getVersion()); + + pom.setServiceName(project.getServiceName() + " Management"); + pom.setServiceDescription(project.getServiceDescriptionForPom()); + + Set addedDependencyPrefixes = new HashSet<>(); + List dependencyIdentifiers = new ArrayList<>(); + if (JavaSettings.getInstance().isStreamStyleSerialization()) { + addDependencyIdentifier(dependencyIdentifiers, addedDependencyPrefixes, + Project.Dependency.AZURE_JSON, false); + } + addDependencyIdentifier(dependencyIdentifiers, addedDependencyPrefixes, + Project.Dependency.AZURE_CORE, false); + addDependencyIdentifier(dependencyIdentifiers, addedDependencyPrefixes, + Project.Dependency.AZURE_CORE_MANAGEMENT, false); + if (JavaSettings.getInstance().isGenerateTests()) { + addDependencyIdentifier(dependencyIdentifiers, addedDependencyPrefixes, + Project.Dependency.AZURE_CORE_TEST, true); + addDependencyIdentifier(dependencyIdentifiers, addedDependencyPrefixes, + Project.Dependency.AZURE_IDENTITY, true); + addDependencyIdentifier(dependencyIdentifiers, addedDependencyPrefixes, + Project.Dependency.JUNIT_JUPITER_API, true); + addDependencyIdentifier(dependencyIdentifiers, addedDependencyPrefixes, + Project.Dependency.JUNIT_JUPITER_ENGINE, true); + + addDependencyIdentifier(dependencyIdentifiers, addedDependencyPrefixes, + Project.Dependency.SLF4J_SIMPLE, true); + } + + // merge dependencies in POM and dependencies added above + dependencyIdentifiers.addAll(project.getPomDependencyIdentifiers().stream() + .filter(dependencyIdentifier -> addedDependencyPrefixes.stream().noneMatch(dependencyIdentifier::startsWith)) + .collect(Collectors.toList())); + + pom.setDependencyIdentifiers(dependencyIdentifiers); + + if (project.isIntegratedWithSdk()) { + pom.setParentIdentifier(Project.Dependency.AZURE_CLIENT_SDK_PARENT.getDependencyIdentifier()); + pom.setParentRelativePath("../../parents/azure-client-sdk-parent"); + } + + pom.setRequireCompilerPlugins(!project.isIntegratedWithSdk()); + + return pom; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/mapper/FluentPrimitiveMapper.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/mapper/FluentPrimitiveMapper.java new file mode 100644 index 0000000000..666f3b1d5c --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/mapper/FluentPrimitiveMapper.java @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.mapper; + +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.PrimitiveSchema; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Schema; +import com.microsoft.typespec.http.client.generator.core.mapper.PrimitiveMapper; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClassType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.IType; + +public class FluentPrimitiveMapper extends PrimitiveMapper { + + private static final FluentPrimitiveMapper INSTANCE = new FluentPrimitiveMapper(); + + public static FluentPrimitiveMapper getInstance() { + return INSTANCE; + } + + @Override + public IType map(PrimitiveSchema primaryType) { + if (primaryType == null) { + return null; + } + if (parsed.containsKey(primaryType)) { + return parsed.get(primaryType); + } + if (primaryType.getType() == Schema.AllSchemaTypes.CREDENTIAL) { + // swagger is "format": "password", which mostly serve as a hint + IType type = ClassType.STRING; + parsed.put(primaryType, type); + return type; + } else { + return super.map(primaryType); + } + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/mapper/FluentProxyMethodMapper.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/mapper/FluentProxyMethodMapper.java new file mode 100644 index 0000000000..0ca475f5d9 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/mapper/FluentProxyMethodMapper.java @@ -0,0 +1,92 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.mapper; + +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Operation; +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; +import com.microsoft.typespec.http.client.generator.mgmt.model.FluentType; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentStatic; +import com.microsoft.typespec.http.client.generator.mgmt.util.Utils; +import com.microsoft.typespec.http.client.generator.core.mapper.ProxyMethodMapper; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClassType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ProxyMethod; +import com.azure.core.util.CoreUtils; + +import java.util.List; +import java.util.Objects; + +public class FluentProxyMethodMapper extends ProxyMethodMapper { + + private static final FluentProxyMethodMapper INSTANCE = new FluentProxyMethodMapper(); + + public static FluentProxyMethodMapper getInstance() { + return INSTANCE; + } + + @Override + protected void buildUnexpectedResponseExceptionTypes(ProxyMethod.Builder builder, + Operation operation, List expectedStatusCodes, + JavaSettings settings) { + if (CoreUtils.isNullOrEmpty(operation.getExceptions())) { + // use ManagementException + builder.unexpectedResponseExceptionType(FluentType.MANAGEMENT_EXCEPTION); + } else { + super.buildUnexpectedResponseExceptionTypes(builder, operation, expectedStatusCodes, settings); + } + + /* + final HttpMethod httpMethod = HttpMethod.valueOf(operation.getRequests().get(0).getProtocol().getHttp().getMethod().toUpperCase()); + final boolean isResourceModify = httpMethod == HttpMethod.PUT || httpMethod == HttpMethod.POST || httpMethod == HttpMethod.PATCH || httpMethod == HttpMethod.DELETE; + final boolean hasETagHeader = operation.getRequests().stream().flatMap(r -> r.getParameters().stream()) + .filter(p -> p.getImplementation() == Parameter.ImplementationLocation.METHOD) + .filter(p -> p.getProtocol() != null && p.getProtocol().getHttp() != null && p.getProtocol().getHttp().getIn() == RequestParameterLocation.Header) + .map(p -> p.getLanguage().getDefault().getSerializedName()) + .filter(Objects::nonNull) + .anyMatch(sn -> sn.equalsIgnoreCase("If-Match") || sn.equalsIgnoreCase("If-None-Match")); + + builder.unexpectedResponseExceptionType(ClassType.HttpResponseException); + Map> unexpectedResponseExceptionTypes = new HashMap<>(); + if (!expectedStatusCodes.contains(HttpResponseStatus.UNAUTHORIZED)) { + unexpectedResponseExceptionTypes.put(ClassType.ClientAuthenticationException, Collections.singletonList(HttpResponseStatus.UNAUTHORIZED)); + } + if (!expectedStatusCodes.contains(HttpResponseStatus.NOT_FOUND)) { + unexpectedResponseExceptionTypes.put(ClassType.ResourceNotFoundException, Collections.singletonList(HttpResponseStatus.NOT_FOUND)); + } + if (isResourceModify && !expectedStatusCodes.contains(HttpResponseStatus.CONFLICT)) { + unexpectedResponseExceptionTypes.put(ClassType.ResourceModifiedException, Collections.singletonList(HttpResponseStatus.CONFLICT)); + } + if (!expectedStatusCodes.contains(HttpResponseStatus.TOO_MANY_REQUESTS)) { + unexpectedResponseExceptionTypes.put(ClassType.TooManyRedirectsException, Collections.singletonList(HttpResponseStatus.TOO_MANY_REQUESTS)); + } + if (hasETagHeader && !expectedStatusCodes.contains(HttpResponseStatus.PRECONDITION_FAILED)) { + unexpectedResponseExceptionTypes.put(ClassType.ResourceExistsException, Collections.singletonList(HttpResponseStatus.PRECONDITION_FAILED)); + } + builder.unexpectedResponseExceptionTypes(unexpectedResponseExceptionTypes); + */ + } + + @Override + protected ClassType processExceptionClassType(ClassType errorType, JavaSettings settings) { + if (!FluentType.nonManagementError(errorType)) { + return FluentType.MANAGEMENT_EXCEPTION; + } else { + return super.processExceptionClassType(errorType, settings); + } + } + + @Override + protected ClassType getHttpResponseExceptionType() { + return FluentType.MANAGEMENT_EXCEPTION; + } + + @Override + protected boolean operationGroupNotNull(Operation operation, JavaSettings settings) { + return super.operationGroupNotNull(operation, settings) + // hack for Fluent, as Lite use "ResourceProvider" if operation group is unnamed + && !( + settings.isFluent() + && Objects.equals(Utils.getNameForUngroupedOperations(operation.getOperationGroup().getCodeModel(), FluentStatic.getFluentJavaSettings()), operation.getOperationGroup().getLanguage().getDefault().getName()) + ); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/mapper/FluentResourceCollectionMapper.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/mapper/FluentResourceCollectionMapper.java new file mode 100644 index 0000000000..9dc1fffb74 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/mapper/FluentResourceCollectionMapper.java @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.mapper; + +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.OperationGroup; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentResourceCollection; +import com.microsoft.typespec.http.client.generator.core.mapper.IMapper; +import com.microsoft.typespec.http.client.generator.core.mapper.Mappers; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.MethodGroupClient; + +public class FluentResourceCollectionMapper implements IMapper { + + private static final FluentResourceCollectionMapper INSTANCE = new FluentResourceCollectionMapper(); + + public static FluentResourceCollectionMapper getInstance() { + return INSTANCE; + } + + @Override + public FluentResourceCollection map(OperationGroup operationGroup) { + FluentResourceCollection fluentResourceCollection = null; + + MethodGroupClient groupClient = Mappers.getMethodGroupMapper().map(operationGroup); + if (groupClient != null && !groupClient.getClassBaseName().isEmpty()) { + fluentResourceCollection = new FluentResourceCollection(groupClient); + } + + return fluentResourceCollection; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/mapper/FluentResourceModelMapper.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/mapper/FluentResourceModelMapper.java new file mode 100644 index 0000000000..940762118d --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/mapper/FluentResourceModelMapper.java @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.mapper; + +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.ObjectSchema; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentResourceModel; +import com.microsoft.typespec.http.client.generator.mgmt.util.FluentUtils; +import com.microsoft.typespec.http.client.generator.core.mapper.IMapper; +import com.microsoft.typespec.http.client.generator.core.mapper.Mappers; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientModel; +import com.azure.core.util.CoreUtils; + +import java.util.ArrayList; +import java.util.List; + +public class FluentResourceModelMapper implements IMapper { + + private static final FluentResourceModelMapper INSTANCE = new FluentResourceModelMapper(); + + public static FluentResourceModelMapper getInstance() { + return INSTANCE; + } + + @Override + public FluentResourceModel map(ObjectSchema objectSchema) { + FluentResourceModel fluentResourceModel = null; + + ClientModel clientModel = Mappers.getModelMapper().map(objectSchema); + if (clientModel != null && FluentUtils.isInnerClassType(clientModel.getPackage(), clientModel.getName())) { + List parentModels = new ArrayList<>(); + String parentModelName = clientModel.getParentModelName(); + while (!CoreUtils.isNullOrEmpty(parentModelName)) { + ClientModel parentModel = FluentUtils.getClientModel(parentModelName); + if (parentModel != null) { + parentModels.add(parentModel); + } + parentModelName = parentModel == null ? null : parentModel.getParentModelName(); + } + + fluentResourceModel = new FluentResourceModel(clientModel, parentModels); + } + + return fluentResourceModel; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/mapper/MockTestParser.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/mapper/MockTestParser.java new file mode 100644 index 0000000000..c39d38ab28 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/mapper/MockTestParser.java @@ -0,0 +1,234 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.mapper; + +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.RequestParameterLocation; +import com.microsoft.typespec.http.client.generator.core.extension.plugin.PluginLogger; +import com.microsoft.typespec.http.client.generator.mgmt.FluentGen; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentCollectionMethod; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentResourceCollection; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ListType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.examplemodel.MethodParameter; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.examplemodel.FluentCollectionMethodExample; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.examplemodel.FluentMethodMockUnitTest; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.examplemodel.FluentResourceCreateExample; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.create.ResourceCreate; +import com.microsoft.typespec.http.client.generator.mgmt.util.FluentUtils; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClassType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethod; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethodType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.GenericType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.IType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ProxyMethodExample; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.examplemodel.ExampleNode; +import com.microsoft.typespec.http.client.generator.core.util.MethodUtil; +import com.microsoft.typespec.http.client.generator.core.util.ModelExampleUtil; +import com.microsoft.typespec.http.client.generator.core.util.ModelTestCaseUtil; +import com.microsoft.typespec.http.client.generator.core.util.PossibleCredentialException; +import com.azure.core.http.HttpMethod; +import org.slf4j.Logger; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class MockTestParser extends ExampleParser { + + private static final Logger LOGGER = new PluginLogger(FluentGen.getPluginInstance(), MockTestParser.class); + + public List parseResourceCollectionForUnitTest(FluentResourceCollection resourceCollection) { + List fluentMethodMockUnitTests = new ArrayList<>(); + + resourceCollection.getMethodsForTemplate().forEach(m -> { + FluentMethodMockUnitTest example = parseMethod(resourceCollection, m); + if (example != null) { + fluentMethodMockUnitTests.add(example); + } + }); + resourceCollection.getResourceCreates().forEach(rc -> { + FluentMethodMockUnitTest example = parseResourceCreate(resourceCollection, rc); + if (example != null) { + fluentMethodMockUnitTests.add(example); + } + }); + return fluentMethodMockUnitTests; + } + + private static FluentMethodMockUnitTest parseResourceCreate(FluentResourceCollection collection, ResourceCreate resourceCreate) { + FluentMethodMockUnitTest unitTest = null; + + try { + List collectionMethods = resourceCreate.getMethodReferences(); + for (FluentCollectionMethod collectionMethod : collectionMethods) { + ClientMethod clientMethod = collectionMethod.getInnerClientMethod(); + if (FluentUtils.validRequestContentTypeToGenerateExample(clientMethod) && FluentUtils.validResponseContentTypeToGenerateExample(clientMethod) && requiresExample(clientMethod)) { + List methodParameters = MethodUtil.getParameters(clientMethod); + MethodParameter requestBodyParameter = findRequestBodyParameter(methodParameters); + ProxyMethodExample proxyMethodExample = createProxyMethodExample(clientMethod, methodParameters); + FluentResourceCreateExample resourceCreateExample = + parseResourceCreate(collection, resourceCreate, proxyMethodExample, methodParameters, requestBodyParameter); + + ResponseInfo responseInfo = createProxyMethodExampleResponse(clientMethod); + unitTest = new FluentMethodMockUnitTest(resourceCreateExample, collection, collectionMethod, + FluentUtils.isResponseType(collectionMethod.getFluentReturnType()) ? FluentUtils.getValueTypeFromResponseType(collectionMethod.getFluentReturnType()) : collectionMethod.getFluentReturnType(), + responseInfo.responseExample, responseInfo.verificationObjectName, responseInfo.verificationNode); + + break; + } + } + } catch (PossibleCredentialException e) { + LOGGER.warn("Skip unit test for resource '{}', caused by key '{}'", resourceCreate.getResourceModel().getInnerModel().getName(), e.getKeyName()); + } + return unitTest; + } + + private static FluentMethodMockUnitTest parseMethod(FluentResourceCollection collection, FluentCollectionMethod collectionMethod) { + FluentMethodMockUnitTest unitTest = null; + + try { + ClientMethod clientMethod = collectionMethod.getInnerClientMethod(); + if (FluentUtils.validRequestContentTypeToGenerateExample(clientMethod) && FluentUtils.validResponseContentTypeToGenerateExample(clientMethod) && requiresExample(clientMethod)) { + List methodParameters = MethodUtil.getParameters(clientMethod); + ProxyMethodExample proxyMethodExample = createProxyMethodExample(clientMethod, methodParameters); + FluentCollectionMethodExample collectionMethodExample = + parseMethodForExample(collection, collectionMethod, methodParameters, proxyMethodExample.getName(), proxyMethodExample); + + ResponseInfo responseInfo = createProxyMethodExampleResponse(clientMethod); + unitTest = new FluentMethodMockUnitTest(collectionMethodExample, collection, collectionMethod, collectionMethod.getFluentReturnType(), + responseInfo.responseExample, responseInfo.verificationObjectName, responseInfo.verificationNode); + } + } catch (PossibleCredentialException e) { + LOGGER.warn("Skip unit test for method '{}', caused by key '{}'", collectionMethod.getMethodName(), e.getKeyName()); + } + return unitTest; + } + + private static ProxyMethodExample createProxyMethodExample(ClientMethod clientMethod, List methodParameters) { + ProxyMethodExample.Builder example = new ProxyMethodExample.Builder() + .name(clientMethod.getName()); + + for (MethodParameter methodParameter : methodParameters) { + // create mock data for each parameter + + String serializedName = methodParameter.getSerializedName(); + if (serializedName == null && methodParameter.getProxyMethodParameter().getRequestParameterLocation() == RequestParameterLocation.BODY) { + serializedName = methodParameter.getProxyMethodParameter().getName(); + } + + Object jsonParam; + if (methodParameter.getProxyMethodParameter().getCollectionFormat() != null + && methodParameter.getProxyMethodParameter().getWireType() == ClassType.STRING + && methodParameter.getProxyMethodParameter().getClientType() instanceof ListType) { + // use element type without delimiter + jsonParam = ModelTestCaseUtil.jsonFromType(0, ((ListType) methodParameter.getProxyMethodParameter().getClientType()).getElementType()).toString(); + } else { + jsonParam = ModelTestCaseUtil.jsonFromType(0, methodParameter.getProxyMethodParameter().getWireType()); + } + + example.parameter(serializedName, jsonParam); + } + + return example.build(); + } + + private static class ResponseInfo { + private final ProxyMethodExample.Response responseExample; + private final ExampleNode verificationNode; + private final String verificationObjectName; + + private ResponseInfo(ProxyMethodExample.Response responseExample, + ExampleNode verificationNode, String verificationObjectName) { + this.responseExample = responseExample; + this.verificationNode = verificationNode; + this.verificationObjectName = verificationObjectName; + } + } + + private static ResponseInfo createProxyMethodExampleResponse(ClientMethod clientMethod) { + // create a mock response + + int statusCode = clientMethod.getProxyMethod().getResponseExpectedStatusCodes().iterator().next(); + Object jsonObject; + ExampleNode verificationNode; + String verificationObjectName; + + IType clientReturnType = clientMethod.getReturnValue().getType(); + final boolean isResponseType = FluentUtils.isResponseType(clientReturnType); + if (isResponseType) { + clientReturnType = FluentUtils.getValueTypeFromResponseType(clientReturnType); + } + + if (clientMethod.getType() == ClientMethodType.PagingSync) { + // pageable + if (clientReturnType instanceof GenericType) { + IType elementType = ((GenericType) clientReturnType).getTypeArguments()[0]; + + Object firstJsonObjectInPageable = ModelTestCaseUtil.jsonFromType(0, elementType); + // put to first element in array + Map jsonMap = new HashMap<>(); + jsonMap.put(clientMethod.getMethodPageDetails().getSerializedItemName(), Collections.singletonList(firstJsonObjectInPageable)); + + jsonObject = jsonMap; + + // pageable will verify the first element + verificationObjectName = "response.iterator().next()"; + verificationNode = ModelExampleUtil.parseNode(elementType, firstJsonObjectInPageable); + } else { + throw new IllegalStateException("Response of pageable operation must be PagedIterable<>"); + } + } else { + // simple or LRO + jsonObject = ModelTestCaseUtil.jsonFromType(0, clientReturnType); + + if (jsonObject == null) { + jsonObject = new Object(); + } + if (clientMethod.getType() == ClientMethodType.LongRunningSync) { + // LRO, hack to set properties.provisioningState == Succeeded, so that LRO can stop at activation operation + setProvisioningState(jsonObject); + } + + verificationObjectName = "response"; + verificationNode = ModelExampleUtil.parseNode(clientReturnType, jsonObject); + } + Map responseObject = new HashMap<>(); + responseObject.put("body", jsonObject); + return new ResponseInfo(new ProxyMethodExample.Response(statusCode, responseObject), verificationNode, verificationObjectName); + } + + private static boolean requiresExample(ClientMethod clientMethod) { + if (clientMethod.getType() == ClientMethodType.SimpleSync + || clientMethod.getType() == ClientMethodType.SimpleSyncRestResponse + // pageable + || (clientMethod.getType() == ClientMethodType.PagingSync + // not pageable + LRO + && clientMethod.getMethodPageDetails().getLroIntermediateType() == null) + // LRO + || (clientMethod.getType() == ClientMethodType.LongRunningSync + // limit the scope of LRO to status code of 200 + && clientMethod.getProxyMethod().getResponseExpectedStatusCodes().contains(200) + // also azure-core-management does not support LRO from GET + && clientMethod.getProxyMethod().getHttpMethod() != HttpMethod.GET)) { + // generate example for the method with full parameters + return clientMethod.getParameters().stream().anyMatch(p -> ClassType.CONTEXT.equals(p.getClientType())); + } + return false; + } + + @SuppressWarnings("unchecked") + private static void setProvisioningState(Object jsonObject) { + // properties.provisioningState = Succeeded + if ((jsonObject instanceof Map) && ((Map) jsonObject).containsKey("properties")) { + Object propertiesObject = ((Map) jsonObject).get("properties"); + if ((propertiesObject instanceof Map) && ((Map) propertiesObject).containsKey("provisioningState")) { + Object provisioningStateObject = ((Map) propertiesObject).get("provisioningState"); + if (provisioningStateObject instanceof String) { + ((Map) propertiesObject).put("provisioningState", "Succeeded"); + } + } + } + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/mapper/ResourceParser.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/mapper/ResourceParser.java new file mode 100644 index 0000000000..c877ff687a --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/mapper/ResourceParser.java @@ -0,0 +1,551 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.mapper; + +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.RequestParameterLocation; +import com.microsoft.typespec.http.client.generator.core.extension.plugin.PluginLogger; +import com.microsoft.typespec.http.client.generator.mgmt.FluentGen; +import com.microsoft.typespec.http.client.generator.mgmt.model.ResourceTypeName; +import com.microsoft.typespec.http.client.generator.mgmt.model.arm.ModelCategory; +import com.microsoft.typespec.http.client.generator.mgmt.model.arm.UrlPathSegments; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentClient; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentCollectionMethod; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentResourceCollection; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentResourceModel; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.examplemodel.MethodParameter; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.ResourceLocalVariables; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.action.ResourceActions; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.create.ResourceCreate; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.delete.ResourceDelete; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.get.ResourceRefresh; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.update.ResourceUpdate; +import com.microsoft.typespec.http.client.generator.mgmt.util.FluentUtils; +import com.microsoft.typespec.http.client.generator.mgmt.util.Utils; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClassType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethod; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethodType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientModel; +import com.microsoft.typespec.http.client.generator.core.template.prototype.MethodTemplate; +import com.microsoft.typespec.http.client.generator.core.util.ClientModelUtil; +import com.azure.core.http.HttpMethod; +import com.azure.core.management.Region; +import com.azure.core.util.CoreUtils; +import org.slf4j.Logger; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +public class ResourceParser { + + private static final Logger LOGGER = new PluginLogger(FluentGen.getPluginInstance(), ResourceParser.class); + + public static void parseResourcesCategory(FluentResourceCollection collection, + List availableFluentModels, + List availableModels) { + // resource create + List resourceCreates = ResourceParser.resolveResourceCreate(collection, availableFluentModels, availableModels); + + // resource update + resourceCreates.forEach(rc -> ResourceParser.resolveResourceUpdate(collection, rc, availableModels)); + + // resource refresh (and get in collection) + resourceCreates.forEach(rc -> ResourceParser.resolveResourceRefresh(collection, rc)); + + // delete in collection + resourceCreates.forEach(rc -> ResourceParser.resolveResourceDelete(collection, rc)); + + // resource actions + resourceCreates.forEach(rc -> ResourceParser.resourceResourceActions(collection, rc)); + } + + static void processAdditionalMethods(FluentClient fluentClient) { + fluentClient.getResourceModels().forEach(ResourceParser::processAdditionalProperties); + + fluentClient.getResourceCollections().forEach(ResourceParser::processAdditionalCollectionMethods); + } + + private static void processAdditionalProperties(FluentResourceModel model) { + List methods = model.getAdditionalMethods(); + + // region() from location() + if (model.getCategory() != ModelCategory.IMMUTABLE) { + if (FluentUtils.modelHasLocationProperty(model) && !model.hasProperty("region")) { + // if resource instance has location property, add region() method + methods.add(MethodTemplate.builder() + .imports(Collections.singletonList(Region.class.getName())) + .comment(commentBlock -> { + commentBlock.description("Gets the region of the resource."); + commentBlock.methodReturns("the region of the resource."); + }) + .methodSignature("Region region()") + .method(methodBlock -> { + methodBlock.methodReturn("Region.fromName(this.regionName())"); + }) + .build()); + methods.add(MethodTemplate.builder() + .comment(commentBlock -> { + commentBlock.description("Gets the name of the resource region."); + commentBlock.methodReturns("the name of the resource region."); + }) + .methodSignature("String regionName()") + .method(methodBlock -> { + methodBlock.methodReturn("this.location()"); + }) + .build()); + } + } + + // resourceGroupName() from class variable + if ((model.getCategory() == ModelCategory.RESOURCE_GROUP_AS_PARENT || model.getCategory() == ModelCategory.NESTED_CHILD) + && model.getResourceCreate() != null + // here we use class variable "resourceGroupName", and hence we need the FluentConstructorByInner that parses the resource ID to resourceGroupName + // for a create-only resource, "resourceGroupName" variable would be null, if the resource is retrieved via Get or List from collection + // alternatively, we can parse resourceGroupName from resource ID in method implementation + && model.getResourceUpdate() != null + && !model.hasProperty("resourceGroupName")) { + UrlPathSegments urlPathSegments = model.getResourceCreate().getUrlPathSegments(); + urlPathSegments.getReverseParameterSegments().stream() + .filter(s -> s.getType() == UrlPathSegments.ParameterSegmentType.RESOURCE_GROUP) + .findFirst().ifPresent(segment -> { + ResourceLocalVariables localVariables = model.getResourceCreate().getResourceLocalVariables(); + + Map pathParametersMap = model.getResourceCreate().getPathParameters().stream() + .collect(Collectors.toMap(p -> p.getClientMethodParameter().getName(), Function.identity())); + localVariables.getLocalVariablesMap().entrySet().stream() + .filter(e -> e.getKey().getClientType() == ClassType.STRING) + // match url path segment to method parameter to local variable + .filter(e -> { + MethodParameter pathParameter = pathParametersMap.get(e.getKey().getName()); + return pathParameter != null && pathParameter.getProxyMethodParameter() != null + && segment.getParameterName().equalsIgnoreCase(pathParameter.getSerializedName()); + }) + .map(Map.Entry::getValue) + .findFirst().ifPresent(var -> { + methods.add(MethodTemplate.builder() + .comment(commentBlock -> { + commentBlock.description("Gets the name of the resource group."); + commentBlock.methodReturns("the name of the resource group."); + }) + .methodSignature("String resourceGroupName()") + .method(methodBlock -> methodBlock.methodReturn(var.getName())) + .build()); + }); + }); + } + } + + private static void processAdditionalCollectionMethods(FluentResourceCollection collection) { + // getById method + collection.getAdditionalMethods().addAll( + collection.getResourceGets().stream() + .flatMap(rg -> rg.getGetByIdCollectionMethods().stream()) + .collect(Collectors.toList())); + + // deleteById method + collection.getAdditionalMethods().addAll( + collection.getResourceDeletes().stream() + .flatMap(rg -> rg.getDeleteByIdCollectionMethods().stream()) + .collect(Collectors.toList())); + } + + static List resolveResourceCreate( + FluentResourceCollection collection, + List availableFluentModels, + List availableModels) { + + List categories = Arrays.asList( + ModelCategory.RESOURCE_GROUP_AS_PARENT, + ModelCategory.SUBSCRIPTION_AS_PARENT, + ModelCategory.NESTED_CHILD, + ModelCategory.SCOPE_AS_PARENT, + ModelCategory.SCOPE_NESTED_CHILD); + + return resolveResourceCreate(collection, availableFluentModels, availableModels, categories); + } + + // for unit test purpose + static List resolveResourceCreate( + FluentResourceCollection collection, + List availableFluentModels, + List availableModels, + List categories) { + + // reference https://github.com/Azure/azure-resource-manager-rpc/blob/master/v1.0/resource-api-reference.md + + Map fluentModelMapByName = availableFluentModels.stream() + .collect(Collectors.toMap(m -> m.getInterfaceType().toString(), Function.identity())); + + List supportsCreateList = new ArrayList<>(); + Set foundModels = new HashSet<>(); + + for (ModelCategory category : categories) { + Map modelResourceCreateMap = + findResourceCreateForCategory(collection, fluentModelMapByName, availableModels, foundModels, category); + + foundModels.addAll(modelResourceCreateMap.keySet()); + + for (Map.Entry entry : modelResourceCreateMap.entrySet()) { + FluentResourceModel fluentModel = entry.getKey(); + ResourceCreate resourceCreate = entry.getValue(); + + fluentModel.setCategory(category); + fluentModel.setResourceCreate(resourceCreate); + collection.getResourceCreates().add(resourceCreate); + + supportsCreateList.add(resourceCreate); + + LOGGER.info("Fluent model '{}' as category {}", fluentModel.getName(), category); + } + } + + supportsCreateList.forEach(rc -> { + rc.getMethodReferences().addAll(collectMethodReferences(collection, rc.getMethodName())); + }); + + return supportsCreateList; + } + + static Optional resolveResourceUpdate( + FluentResourceCollection collection, + ResourceCreate resourceCreate, + List availableModels) { + + ResourceUpdate resourceUpdate = null; + + Predicate nameMatcher = name -> !(name.contains("create") && !name.contains("update")); + // PATCH takes priority + FluentCollectionMethod method = findCollectionMethod(collection, resourceCreate, HttpMethod.PATCH, nameMatcher); + if (method == null) { + // fallback to PUT + method = findCollectionMethod(collection, resourceCreate, HttpMethod.PUT, nameMatcher); + } + if (method != null) { + ClientModel bodyClientModel = getBodyClientModel(method, availableModels); + if (bodyClientModel == null) { + LOGGER.warn("client model not found for collection '{}', method '{}'", collection.getInterfaceType().getName(), method.getInnerClientMethod().getName()); + } else { + resourceUpdate = new ResourceUpdate(resourceCreate.getResourceModel(), collection, + resourceCreate.getUrlPathSegments(), method.getInnerClientMethod().getName(), + bodyClientModel); + + resourceCreate.getResourceModel().setResourceUpdate(resourceUpdate); + collection.getResourceUpdates().add(resourceUpdate); + + resourceUpdate.getMethodReferences().addAll(collectMethodReferences(collection, resourceUpdate.getMethodName())); + } + } + + return Optional.ofNullable(resourceUpdate); + } + + static Optional resolveResourceRefresh( + FluentResourceCollection collection, + ResourceCreate resourceCreate) { + + ResourceRefresh resourceRefresh = null; + + FluentCollectionMethod method = findCollectionMethod(collection, resourceCreate, HttpMethod.GET, name -> name.contains("get")); + if (method != null) { + resourceRefresh = new ResourceRefresh(resourceCreate.getResourceModel(), collection, + resourceCreate.getUrlPathSegments(), method.getInnerClientMethod().getName()); + + resourceCreate.getResourceModel().setResourceRefresh(resourceRefresh); + collection.getResourceGets().add(resourceRefresh); + + resourceRefresh.getMethodReferences().addAll(collectMethodReferences(collection, resourceRefresh.getMethodName())); + } + + return Optional.ofNullable(resourceRefresh); + } + + static Optional resolveResourceDelete( + FluentResourceCollection collection, + ResourceCreate resourceCreate) { + + ResourceDelete resourceDelete = null; + + FluentCollectionMethod method = findCollectionMethod(collection, resourceCreate, HttpMethod.DELETE, name -> name.contains("delete")); + if (method != null) { + resourceDelete = new ResourceDelete(resourceCreate.getResourceModel(), collection, + resourceCreate.getUrlPathSegments(), method.getInnerClientMethod().getName()); + + collection.getResourceDeletes().add(resourceDelete); + + resourceDelete.getMethodReferences().addAll(collectMethodReferences(collection, resourceDelete.getMethodName())); + } + + return Optional.ofNullable(resourceDelete); + } + + static Optional resourceResourceActions( + FluentResourceCollection collection, + ResourceCreate resourceCreate) { + + // reference https://github.com/Azure/azure-resource-manager-rpc/blob/master/v1.0/proxy-api-reference.md#resource-action-requests + + ResourceActions resourceActions = null; + List actionMethods = new ArrayList<>(); + + for (FluentCollectionMethod method : collection.getMethods()) { + HttpMethod httpMethod = method.getInnerProxyMethod().getHttpMethod(); + // POST + if (httpMethod == HttpMethod.POST) { + String url = method.getInnerProxyMethod().getUrlPath(); + // except last literal segment, same url as create + if (url.startsWith(resourceCreate.getUrlPathSegments().getPath()) + && url.substring(0, url.lastIndexOf("/")).equals(resourceCreate.getUrlPathSegments().getPath()) + && !new UrlPathSegments(url).getReverseSegments().iterator().next().isParameterSegment()) { + // parameter from request body + if (method.getInnerProxyMethod().getParameters().stream() + .allMatch(p -> p.isFromClient() + || !p.isRequired() + || (p.getRequestParameterLocation() == RequestParameterLocation.QUERY && p.isConstant()) // usually 'api-version' query parameter + || (p.getRequestParameterLocation() == RequestParameterLocation.HEADER && p.isConstant()) // usually 'accept' header + || p.getRequestParameterLocation() == RequestParameterLocation.PATH + || p.getRequestParameterLocation() == RequestParameterLocation.BODY)) { + actionMethods.add(method); + } + } + } + } + + if (!actionMethods.isEmpty()) { + resourceActions = new ResourceActions(resourceCreate.getResourceModel(), collection, actionMethods); + + resourceCreate.getResourceModel().setResourceActions(resourceActions); + } + + return Optional.ofNullable(resourceActions); + } + + static Map findResourceCreateForCategory( + FluentResourceCollection collection, + Map fluentModelMapByName, + List availableModels, + Set excludeModels, + ModelCategory category) { + + Map foundModels = new LinkedHashMap<>(); + + collection.getMethods().forEach(m -> { + HttpMethod method = m.getInnerProxyMethod().getHttpMethod(); + + // PUT + if (method == HttpMethod.PUT) { + // not only "update", usually "createOrUpdate" or "create", sometimes "put" + String methodNameLowerCase = m.getInnerClientMethod().getName().toLowerCase(Locale.ROOT); + if (!(methodNameLowerCase.contains("update") && !methodNameLowerCase.contains("create"))) { + // body in request + if (m.getInnerProxyMethod().getParameters().stream().anyMatch(p -> p.getRequestParameterLocation() == RequestParameterLocation.BODY)) { + String returnTypeName = m.getFluentReturnType().toString(); + FluentResourceModel fluentModel = fluentModelMapByName.get(returnTypeName); + // at present, cannot handle derived models + if (fluentModel != null && fluentModel.getInnerModel().getDerivedModels().isEmpty()) { + // "id", "name", "type" in resource instance + if (fluentModel != null && fluentModel.getResourceCreate() == null + && !foundModels.containsKey(fluentModel) && !excludeModels.contains(fluentModel) + && fluentModel.hasProperty(ResourceTypeName.FIELD_ID) + && fluentModel.hasProperty(ResourceTypeName.FIELD_NAME) + && fluentModel.hasProperty(ResourceTypeName.FIELD_TYPE)) { + String url = m.getInnerProxyMethod().getUrlPath(); + UrlPathSegments urlPathSegments = new UrlPathSegments(url); + + //logger.info("Candidate fluent model '{}', hasSubscription '{}', hasResourceGroup '{}', isNested '{}', method name '{}'", fluentModel.getName(), urlPathSegments.hasSubscription(), urlPathSegments.hasResourceGroup(), urlPathSegments.isNested(), m.getInnerClientMethod().getName()); + + // has "subscriptions" segment, and last segment should be resource name + if (!urlPathSegments.getReverseSegments().isEmpty() && urlPathSegments.getReverseSegments().iterator().next().isParameterSegment()) { + + // requires named parameters in URL + boolean urlParameterSegmentsNamed = urlPathSegments.getReverseParameterSegments().stream() + .noneMatch(s -> CoreUtils.isNullOrEmpty(s.getSegmentName())); + + boolean categoryMatch = false; + if (urlParameterSegmentsNamed && urlPathSegments.hasSubscription()) { + switch (category) { + case RESOURCE_GROUP_AS_PARENT: + if (urlPathSegments.hasResourceGroup() && !urlPathSegments.isNested()) { + categoryMatch = true; + } + break; + + case SUBSCRIPTION_AS_PARENT: + if (!urlPathSegments.hasResourceGroup() && !urlPathSegments.isNested()) { + categoryMatch = true; + } + break; + + case NESTED_CHILD: + if (urlPathSegments.isNested()) { + categoryMatch = true; + } + break; + } + } + if (!categoryMatch && (category == ModelCategory.SCOPE_AS_PARENT || category == ModelCategory.SCOPE_NESTED_CHILD)) { + // check for scope, required named parameters except scope + boolean urlParameterSegmentsNamedExceptScope = urlPathSegments.getReverseParameterSegments().stream() + .noneMatch(s -> s.getType() != UrlPathSegments.ParameterSegmentType.SCOPE && CoreUtils.isNullOrEmpty(s.getSegmentName())); + + if (urlParameterSegmentsNamedExceptScope && urlPathSegments.hasScope() + && !urlPathSegments.hasSubscription() && !urlPathSegments.hasResourceGroup()) { + switch (category) { + case SCOPE_AS_PARENT: + if (!urlPathSegments.isNested()) { + categoryMatch = true; + } + break; + + case SCOPE_NESTED_CHILD: + if (urlPathSegments.isNested()) { + categoryMatch = true; + } + break; + } + } + } + + if (categoryMatch) { + ClientModel bodyClientModel = getBodyClientModel(m, availableModels); + if (bodyClientModel == null) { + LOGGER.warn("client model not found for collection '{}', method '{}'", collection.getInterfaceType().getName(), m.getInnerClientMethod().getName()); + } else { + ResourceCreate resourceCreate = new ResourceCreate(fluentModel, collection, urlPathSegments, + m.getInnerClientMethod().getName(), bodyClientModel); + + foundModels.put(fluentModel, resourceCreate); + } + } + } + } + } + } + } + } + }); + + return foundModels; + } + + private static ClientModel getBodyClientModel(FluentCollectionMethod method, List availableModels) { + Optional bodyTypeNameOpt = method.getInnerClientMethod().getProxyMethod().getParameters() + .stream() + .filter(p -> p.getRequestParameterLocation() == RequestParameterLocation.BODY) + .map(p -> p.getClientType().toString()) + .findAny(); + + if (!bodyTypeNameOpt.isPresent()) { + throw new IllegalStateException("Body type not found for method " + method.getInnerClientMethod().getName()); + } + + Optional clientModelOpt = availableModels.stream() + .filter(model -> model.getName().equals(bodyTypeNameOpt.get())) + .findAny(); + + if (!clientModelOpt.isPresent()) { + LOGGER.warn("Client model not found for type name '{}', method '{}'", bodyTypeNameOpt.get(), method.getInnerClientMethod().getName()); + } + return clientModelOpt.orElse(null); + } + + private static FluentCollectionMethod findCollectionMethod(FluentResourceCollection collection, + ResourceCreate resourceCreate, + HttpMethod matchingMethod, Predicate nameMatcher) { + boolean isGetOrDelete = matchingMethod == HttpMethod.GET || matchingMethod == HttpMethod.DELETE; + boolean isDelete = matchingMethod == HttpMethod.DELETE; + + for (FluentCollectionMethod method : collection.getMethods()) { + HttpMethod httpMethod = method.getInnerProxyMethod().getHttpMethod(); + // match http method + if (httpMethod == matchingMethod) { + String methodNameLowerCase = method.getInnerClientMethod().getName().toLowerCase(Locale.ROOT); + // match name + if (nameMatcher.test(methodNameLowerCase)) { + String returnTypeName = method.getFluentReturnType().toString(); + // same model as create + if (isDelete || returnTypeName.equals(resourceCreate.getResourceModel().getInterfaceType().getName())) { + String url = method.getInnerProxyMethod().getUrlPath(); + // same url as create + if (url.equals(resourceCreate.getUrlPathSegments().getPath())) { + boolean hasBodyParam = methodHasBodyParameter(method); + boolean hasRequiredQueryParam = method.getInnerProxyMethod().getParameters().stream() + .anyMatch(p -> p.getRequestParameterLocation() == RequestParameterLocation.QUERY + && p.isRequired() + && !p.isFromClient() && !p.isConstant()); + boolean hasNewNonConstantPathParam = method.getInnerProxyMethod().getParameters().stream() + .anyMatch(p -> p.getRequestParameterLocation() == RequestParameterLocation.PATH + && !p.isConstant() && !p.isFromClient() + && resourceCreate.getMethodReferences().stream().allMatch( + m -> m.getInnerProxyMethod().getParameters().stream().anyMatch( + p1 -> p1.getRequestParameterLocation() == RequestParameterLocation.PATH + && p1.getRequestParameterName().equals(p.getRequestParameterName()) + && p1.isConstant() && !p1.isFromClient()))); + // if for update, need a body parameter + // if for get or delete, do not allow required query parameter (that not from client, and not constant), since it cannot be deduced from resource id + if ((isGetOrDelete && !hasRequiredQueryParam && !hasNewNonConstantPathParam) + || (!isGetOrDelete && hasBodyParam)) { + return method; + } + } + } + } + } + } + return null; + } + + private static List collectMethodReferences(FluentResourceCollection collection, String methodName) { + // The matching method could already contain the postfix, so we need to create both the WithResponse and + // non-WithResponse matches. + String nonWithResponseMatch; + String withResponseMatch; + if (methodName.endsWith(Utils.METHOD_POSTFIX_WITH_RESPONSE)) { + withResponseMatch = methodName; + nonWithResponseMatch = methodName.substring(0, methodName.length() - Utils.METHOD_POSTFIX_WITH_RESPONSE.length()); + } else { + nonWithResponseMatch = methodName; + withResponseMatch = methodName + Utils.METHOD_POSTFIX_WITH_RESPONSE; + } + + List collectionMethods = new ArrayList<>(); + for (FluentCollectionMethod fluentMethod : collection.getMethods()) { + ClientMethod innerMethod = fluentMethod.getInnerClientMethod(); + String innerName = innerMethod.getName(); + HttpMethod httpMethod = fluentMethod.getInnerProxyMethod().getHttpMethod(); + + // Check for the method name matching the non-WithResponse match or the method being a WithResponse method + // and matching the WithResponse match. + if (!innerName.equals(nonWithResponseMatch) + && !(innerMethod.getType() == ClientMethodType.SimpleSyncRestResponse && innerName.equals(withResponseMatch))) { + continue; + } + + // Check for the HTTP method being either GET or DELETE and the method having a body parameter. + if (httpMethod != HttpMethod.GET && httpMethod != HttpMethod.DELETE && !methodHasBodyParameter(fluentMethod)) { + continue; + } + + collectionMethods.add(fluentMethod); + } + + return collectionMethods; + } + + private static boolean methodHasBodyParameter(FluentCollectionMethod method) { + // Previous it filtered on isClientModel but filter on parameter location as that's the cheaper check. + return method.getInnerProxyMethod().getParameters().stream() + .filter(p -> p.getRequestParameterLocation() == RequestParameterLocation.BODY) + .anyMatch(p -> ClientModelUtil.isClientModel(p.getClientType())); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/FluentType.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/FluentType.java new file mode 100644 index 0000000000..3e0e165007 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/FluentType.java @@ -0,0 +1,91 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.model; + +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.ObjectSchema; +import com.microsoft.typespec.http.client.generator.mgmt.util.Utils; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClassType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.GenericType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.IType; +import com.azure.core.management.ProxyResource; +import com.azure.core.management.Region; +import com.azure.core.management.Resource; +import com.azure.core.management.SubResource; +import com.azure.core.management.SystemData; +import com.azure.core.management.exception.AdditionalInfo; +import com.azure.core.management.exception.ManagementError; +import com.azure.core.management.exception.ManagementException; +import com.azure.core.management.profile.AzureProfile; + +public class FluentType { + + public static final ClassType RESOURCE = new ClassType.Builder().knownClass(Resource.class).build(); + public static final ClassType PROXY_RESOURCE = new ClassType.Builder().knownClass(ProxyResource.class).build(); + public static final ClassType SUB_RESOURCE = new ClassType.Builder().knownClass(SubResource.class).build(); + + public static final ClassType MANAGEMENT_EXCEPTION = new ClassType.Builder().knownClass(ManagementException.class) + .build(); + public static final ClassType MANAGEMENT_ERROR = new ClassType.Builder().knownClass(ManagementError.class).build(); + + public static final ClassType AZURE_PROFILE = new ClassType.Builder().knownClass(AzureProfile.class).build(); + + public static final ClassType REGION = new ClassType.Builder().knownClass(Region.class).build(); + + public static final ClassType SYSTEM_DATA = new ClassType.Builder().knownClass(SystemData.class).build(); + + public static final ClassType AZURE_RESOURCE_MANAGER = new ClassType.Builder() + .packageName("com.azure.resourcemanager").name("AzureResourceManager").build(); + + public static final ClassType ADDITIONAL_INFO = new ClassType.Builder().knownClass(AdditionalInfo.class).build(); + + private FluentType() { + } + + public static GenericType InnerSupportsGet(IType typeArgument) { + return new GenericType("com.azure.resourcemanager.resources.fluentcore.collection", "InnerSupportsGet", + typeArgument); + } + + public static GenericType InnerSupportsList(IType typeArgument) { + return new GenericType("com.azure.resourcemanager.resources.fluentcore.collection", "InnerSupportsListing", + typeArgument); + } + + public static GenericType InnerSupportsDelete(IType typeArgument) { + return new GenericType("com.azure.resourcemanager.resources.fluentcore.collection", "InnerSupportsDelete", + typeArgument); + } + + public static boolean nonResourceType(ObjectSchema compositeType) { + return nonResourceType(Utils.getJavaName(compositeType)); + } + + public static boolean nonResourceType(ClassType modelType) { + return !(RESOURCE.equals(modelType) + || PROXY_RESOURCE.equals(modelType) + || SUB_RESOURCE.equals(modelType)); + } + + public static boolean nonResourceType(String modelName) { + return !(RESOURCE.getName().equals(modelName) + || PROXY_RESOURCE.getName().equals(modelName) + || SUB_RESOURCE.getName().equals(modelName)); + } + + public static boolean nonSystemData(ClassType modelType) { + return nonSystemData(modelType.getName()); + } + + public static boolean nonSystemData(String modelName) { + return !SYSTEM_DATA.getName().equals(modelName); + } + + public static boolean nonManagementError(ClassType modelType) { + return nonManagementError(modelType.getName()); + } + + public static boolean nonManagementError(String modelName) { + return !MANAGEMENT_ERROR.getName().equals(modelName); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/ResourceCollectionAssociation.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/ResourceCollectionAssociation.java new file mode 100644 index 0000000000..719ae665db --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/ResourceCollectionAssociation.java @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.model; + +import com.microsoft.typespec.http.client.generator.core.extension.base.util.JsonUtils; +import com.azure.json.JsonReader; +import com.azure.json.JsonSerializable; +import com.azure.json.JsonWriter; + +import java.io.IOException; + +public class ResourceCollectionAssociation implements JsonSerializable { + + private String resource; + private String collection; + + public String getResource() { + return resource; + } + + public void setResource(String resource) { + this.resource = resource; + } + + public String getCollection() { + return collection; + } + + public void setCollection(String collection) { + this.collection = collection; + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + return jsonWriter.writeStartObject() + .writeStringField("resource", resource) + .writeStringField("collection", collection) + .writeEndObject(); + } + + public static ResourceCollectionAssociation fromJson(JsonReader jsonReader) throws IOException { + return JsonUtils.readObject(jsonReader, ResourceCollectionAssociation::new, + (association, fieldName, reader) -> { + if (fieldName.equals("resource")) { + association.resource = reader.getString(); + } else if (fieldName.equals("collection")) { + association.collection = reader.getString(); + } else { + reader.skipChildren(); + } + }); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/ResourceType.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/ResourceType.java new file mode 100644 index 0000000000..6c992bba34 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/ResourceType.java @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.model; + +public enum ResourceType { + + RESOURCE(ResourceTypeName.RESOURCE), + PROXY_RESOURCE(ResourceTypeName.PROXY_RESOURCE), + SUB_RESOURCE(ResourceTypeName.SUB_RESOURCE); + + private String className; + + ResourceType(String className) { + this.className = className; + } + + public String getClassName() { + return className; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/ResourceTypeName.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/ResourceTypeName.java new file mode 100644 index 0000000000..97f647539a --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/ResourceTypeName.java @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.model; + +public class ResourceTypeName { + + // need a compile-time constant for switch clause + public static final String RESOURCE = "Resource"; + public static final String AZURE_RESOURCE = "AzureResource"; + public static final String SUB_RESOURCE = "SubResource"; + public static final String PROXY_RESOURCE = "ProxyResource"; + public static final String TRACKED_RESOURCE = "TrackedResource"; + public static final String EXTENSION_RESOURCE = "ExtensionResource"; + + private static final String AUTO_GENERATED = "AutoGenerated"; + // these are duplications for above. modelerfour would add "AutoGenerated" to type name, if 2 types having same name but different definition. + public static final String RESOURCE_AUTO_GENERATED = RESOURCE + AUTO_GENERATED; + public static final String AZURE_RESOURCE_AUTO_GENERATED = AZURE_RESOURCE + AUTO_GENERATED; + public static final String SUB_RESOURCE_AUTO_GENERATED = SUB_RESOURCE + AUTO_GENERATED; + public static final String PROXY_RESOURCE_AUTO_GENERATED = PROXY_RESOURCE + AUTO_GENERATED; + public static final String TRACKED_RESOURCE_AUTO_GENERATED = TRACKED_RESOURCE + AUTO_GENERATED; + + public static final String FIELD_ID = "id"; + public static final String FIELD_NAME = "name"; + public static final String FIELD_TYPE = "type"; + public static final String FIELD_LOCATION = "location"; + public static final String FIELD_TAGS = "tags"; +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/WellKnownMethodName.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/WellKnownMethodName.java new file mode 100644 index 0000000000..a3f25b2afa --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/WellKnownMethodName.java @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.model; + +public enum WellKnownMethodName { + // client + LIST("list"), + LIST_BY_RESOURCE_GROUP("listByResourceGroup"), + GET_BY_RESOURCE_GROUP("getByResourceGroup"), + DELETE("delete"), + + // fluent + DELETE_BY_RESOURCE_GROUP("deleteByResourceGroup"); + + private final String methodName; + + WellKnownMethodName(String methodName) { + this.methodName = methodName; + } + + public String getMethodName() { + return methodName; + } + + public static WellKnownMethodName fromMethodName(String methodName) { + for (WellKnownMethodName name : WellKnownMethodName.values()) { + if (name.getMethodName().equals(methodName)) { + return name; + } + } + return null; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/arm/ErrorClientModel.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/arm/ErrorClientModel.java new file mode 100644 index 0000000000..f31d673fbb --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/arm/ErrorClientModel.java @@ -0,0 +1,99 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.model.arm; + +import com.microsoft.typespec.http.client.generator.mgmt.model.FluentType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClassType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientModel; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientModelProperty; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ListType; + +import java.util.Arrays; +import java.util.Objects; +import java.util.Optional; + +public class ErrorClientModel { + + private ErrorClientModel(){ + + } + + public static final ClientModel MANAGEMENT_ERROR = new ClientModel.Builder() + .name(FluentType.MANAGEMENT_ERROR.getName()) + .packageName("com.azure.core.management.exception") + .properties(Arrays.asList( + new ClientModelProperty.Builder() + .name("code") + .serializedName("code") + .description("The error code parsed from the body of the http error response.") + .readOnly(true) + .wireType(ClassType.STRING) + .clientType(ClassType.STRING) + .build(), + new ClientModelProperty.Builder() + .name("message") + .serializedName("message") + .description("The error message parsed from the body of the http error response.") + .readOnly(true) + .wireType(ClassType.STRING) + .clientType(ClassType.STRING) + .build(), + new ClientModelProperty.Builder() + .name("target") + .serializedName("target") + .description("The target of the error.") + .readOnly(true) + .wireType(ClassType.STRING) + .clientType(ClassType.STRING) + .build(), + new ClientModelProperty.Builder() + .name("details") + .serializedName("details") + .description("Details for the error.") + .readOnly(true) + .wireType(new ListType(FluentType.MANAGEMENT_ERROR)) + .clientType(new ListType(FluentType.MANAGEMENT_ERROR)) + .build(), + new ClientModelProperty.Builder() + .name("additionalInfo") + .serializedName("additionalInfo") + .description("Additional info for the error.") + .readOnly(true) + .wireType(new ListType(FluentType.ADDITIONAL_INFO)) + .clientType(new ListType(FluentType.ADDITIONAL_INFO)) + .build() + )).build(); + + private static final ClientModel ADDITIONAL_INFO = new ClientModel.Builder() + .name(FluentType.ADDITIONAL_INFO.getName()) + .packageName("com.azure.core.management.exception") + .properties(Arrays.asList( + new ClientModelProperty.Builder() + .name("type") + .serializedName("type") + .description("The type of additional info.") + .readOnly(true) + .wireType(ClassType.STRING) + .clientType(ClassType.STRING) + .build(), + new ClientModelProperty.Builder() + .name("info") + .serializedName("info") + .description("The additional info.") + .readOnly(true) + .wireType(ClassType.OBJECT) + .clientType(ClassType.OBJECT) + .build() + )).build(); + + public static Optional getErrorClientModel(String modelName) { + Optional result = Optional.empty(); + if (Objects.equals(modelName, FluentType.MANAGEMENT_ERROR.getName())) { + result = Optional.of(MANAGEMENT_ERROR); + } else if (Objects.equals(modelName, FluentType.ADDITIONAL_INFO.getName())) { + result = Optional.of(ADDITIONAL_INFO); + } + return result; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/arm/ModelCategory.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/arm/ModelCategory.java new file mode 100644 index 0000000000..49aa012ed1 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/arm/ModelCategory.java @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.model.arm; + +public enum ModelCategory { + + // e.g. /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Storage/storageAccounts/{accountName} + RESOURCE_GROUP_AS_PARENT, + + SUBSCRIPTION_AS_PARENT, + + // e.g. /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Storage/storageAccounts/{accountName}/blobServices/default/containers/{containerName} + NESTED_CHILD, + + // e.g. /{scope}/providers/Microsoft.Authorization/roleAssignments/{roleAssignmentName} + SCOPE_AS_PARENT, + + // e.g. /{resourceUri}/providers/Microsoft.Advisor/recommendations/{recommendationId}/suppressions/{name} + SCOPE_NESTED_CHILD, + + // not an Azure resource, merely an immutable + IMMUTABLE +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/arm/ResourceClientModel.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/arm/ResourceClientModel.java new file mode 100644 index 0000000000..1a14894b8d --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/arm/ResourceClientModel.java @@ -0,0 +1,115 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.model.arm; + +import com.microsoft.typespec.http.client.generator.mgmt.model.ResourceTypeName; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClassType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientModel; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientModelProperty; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.MapType; + +import java.util.Arrays; +import java.util.Collections; +import java.util.Optional; + +public class ResourceClientModel { + + private ResourceClientModel() { + + } + + private static final ClientModel MODEL_SUB_RESOURCE = new ClientModel.Builder() + .name(ResourceTypeName.SUB_RESOURCE) + .packageName("com.azure.core.management") + .properties(Collections.singletonList( + new ClientModelProperty.Builder() + .name(ResourceTypeName.FIELD_ID) + .serializedName(ResourceTypeName.FIELD_ID) + .description("Fully qualified resource Id for the resource.") + .wireType(ClassType.STRING) + .clientType(ClassType.STRING) + .build() + )) + .build(); + private static final ClientModel MODEL_PROXY_RESOURCE = new ClientModel.Builder() + .name(ResourceTypeName.PROXY_RESOURCE) + .packageName("com.azure.core.management") + .properties(Arrays.asList( + new ClientModelProperty.Builder() + .name(ResourceTypeName.FIELD_ID) + .serializedName(ResourceTypeName.FIELD_ID) + .description("Fully qualified resource Id for the resource.") + .required(true) + .readOnly(true) + .wireType(ClassType.STRING) + .clientType(ClassType.STRING) + .build(), + new ClientModelProperty.Builder() + .name(ResourceTypeName.FIELD_NAME) + .serializedName(ResourceTypeName.FIELD_NAME) + .description("The name of the resource.") + .required(true) + .readOnly(true) + .wireType(ClassType.STRING) + .clientType(ClassType.STRING) + .build(), + new ClientModelProperty.Builder() + .name(ResourceTypeName.FIELD_TYPE) + .serializedName(ResourceTypeName.FIELD_TYPE) + .description("The type of the resource.") + .required(true) + .readOnly(true) + .wireType(ClassType.STRING) + .clientType(ClassType.STRING) + .build() + )) + .build(); + + private static final ClientModel MODEL_RESOURCE = new ClientModel.Builder() + .name(ResourceTypeName.RESOURCE) + .packageName("com.azure.core.management") + .parentModelName(ResourceTypeName.PROXY_RESOURCE) + .properties(Arrays.asList( + new ClientModelProperty.Builder() + .name(ResourceTypeName.FIELD_LOCATION) + .serializedName(ResourceTypeName.FIELD_LOCATION) + .description("The geo-location where the resource lives.") + .required(true) + .readOnly(false) + .wireType(ClassType.STRING) + .clientType(ClassType.STRING) + .mutabilities(Arrays.asList(ClientModelProperty.Mutability.CREATE, ClientModelProperty.Mutability.READ)) + .build(), + new ClientModelProperty.Builder() + .name(ResourceTypeName.FIELD_TAGS) + .serializedName(ResourceTypeName.FIELD_TAGS) + .description("Resource tags.") + .required(false) + .readOnly(false) + .wireType(new MapType(ClassType.STRING)) + .clientType(new MapType(ClassType.STRING)) + .build() + )) + .build(); + + public static Optional getResourceClientModel(String modelName) { + ClientModel model = null; + + switch (modelName) { + case ResourceTypeName.RESOURCE: + model = MODEL_RESOURCE; + break; + + case ResourceTypeName.PROXY_RESOURCE: + model = MODEL_PROXY_RESOURCE; + break; + + case ResourceTypeName.SUB_RESOURCE: + model = MODEL_SUB_RESOURCE; + break; + } + + return Optional.ofNullable(model); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/arm/UrlPathSegments.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/arm/UrlPathSegments.java new file mode 100644 index 0000000000..bc99b1f239 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/arm/UrlPathSegments.java @@ -0,0 +1,229 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.model.arm; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Objects; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +public class UrlPathSegments { + + public static final String SEGMENT_NAME_EMPTY = ""; + + public enum ParameterSegmentType { + RESOURCE_GROUP, + SUBSCRIPTION, + SCOPE, + OTHER + } + + public interface Segment { + String getSegmentName(); + + boolean isParameterSegment(); + } + + public static class ParameterSegment implements Segment { + private final String segmentName; + private final String parameterName; + private final ParameterSegmentType type; + + public ParameterSegment(String segmentName, String parameterName) { + this(segmentName, parameterName, false); + } + + public ParameterSegment(String segmentName, String parameterName, boolean scopeSegment) { + this.segmentName = segmentName; + this.parameterName = parameterName; + + if (scopeSegment) { + this.type = ParameterSegmentType.SCOPE; + } else { + switch (segmentName.toLowerCase(Locale.ROOT)) { + case "resourcegroups": + this.type = ParameterSegmentType.RESOURCE_GROUP; + break; + case "subscriptions": + this.type = ParameterSegmentType.SUBSCRIPTION; + break; + default: + this.type = ParameterSegmentType.OTHER; + break; + } + } + } + + @Override + public String getSegmentName() { + return segmentName; + } + + @Override + public boolean isParameterSegment() { + return true; + } + + public String getParameterName() { + return parameterName; + } + + public ParameterSegmentType getType() { + return type; + } + + @Override + public String toString() { + return new StringBuilder() + .append("Parameter: segName=").append(segmentName) + .append(", parameterName=").append(parameterName) + .append(", type=").append(type) + .toString(); + } + } + + public static class LiteralSegment implements Segment { + private final String segmentName; + + public LiteralSegment(String segmentName) { + this.segmentName = segmentName; + } + + @Override + public String getSegmentName() { + return segmentName; + } + + @Override + public boolean isParameterSegment() { + return false; + } + + @Override + public String toString() { + return new StringBuilder() + .append("Literal: segName=").append(segmentName) + .toString(); + } + } + + private final String path; + + private final List reverseSegments = new ArrayList<>(); + + public UrlPathSegments(String path) { + this.path = Objects.requireNonNull(path); + + String[] segmentArray = path.split(Pattern.quote("/")); + + String currentParameterName = null; + for (int i = segmentArray.length - 1; i >= 0; --i) { + String segmentStr = segmentArray[i].trim(); + + if (!segmentStr.isEmpty()) { + if (segmentStr.startsWith("{") && segmentStr.endsWith("}")) { + String parameterName = segmentStr.substring(1, segmentStr.length() - 1); + + if (currentParameterName != null) { + reverseSegments.add(new ParameterSegment(SEGMENT_NAME_EMPTY, currentParameterName)); + } + currentParameterName = parameterName; + } else { + String segmentName = segmentStr; + + if (currentParameterName != null) { + reverseSegments.add(new ParameterSegment(segmentName, currentParameterName)); + currentParameterName = null; + } else { + reverseSegments.add(new LiteralSegment(segmentName)); + } + } + } + } + if (currentParameterName != null) { + reverseSegments.add(new ParameterSegment(SEGMENT_NAME_EMPTY, currentParameterName, true)); + } + } + + public String getPath() { + return path; + } + + public List getReverseSegments() { + return reverseSegments; + } + + public List getReverseParameterSegments() { + return reverseSegments.stream() + .filter(Segment::isParameterSegment) + .map(s -> (ParameterSegment) s) + .collect(Collectors.toList()); + } + + public boolean hasResourceGroup() { + return getNestLevel() >= 1 && reverseSegments.stream() + .filter(Segment::isParameterSegment) + .map(s -> (ParameterSegment) s) + .anyMatch(s -> s.getType() == ParameterSegmentType.RESOURCE_GROUP); + } + + public boolean hasSubscription() { + return (getNestLevel() >= 1 + || (getNestLevel() == 0 && !getReverseParameterSegments().isEmpty() && getReverseParameterSegments().iterator().next().getType() == ParameterSegmentType.RESOURCE_GROUP)) // the special case for ResourceGroup + && reverseSegments.stream() + .filter(Segment::isParameterSegment) + .map(s -> (ParameterSegment) s) + .anyMatch(s -> s.getType() == ParameterSegmentType.SUBSCRIPTION); + } + + public boolean hasScope() { + return getNestLevel() >= 1 && reverseSegments.stream() + .filter(Segment::isParameterSegment) + .map(s -> (ParameterSegment) s) + .anyMatch(s -> s.getType() == ParameterSegmentType.SCOPE); + } + + public boolean isNested() { + return getNestLevel() > 1; + } + + private int getNestLevel() { + int level = 0; + for (Segment segment : reverseSegments) { + if (segment.isParameterSegment()) { + ParameterSegmentType type = ((ParameterSegment) segment).getType(); + if (type == ParameterSegmentType.OTHER) { + level++; + } else { + break; + } + } + } + return level; + } + + @Override + public String toString() { + return reverseSegments.toString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + UrlPathSegments that = (UrlPathSegments) o; + return path.equals(that.path); + } + + @Override + public int hashCode() { + return Objects.hash(path); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/FluentClient.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/FluentClient.java new file mode 100644 index 0000000000..5ace25f15e --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/FluentClient.java @@ -0,0 +1,76 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel; + +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.examplemodel.FluentMethodMockUnitTest; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.Client; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ModuleInfo; + +import java.util.ArrayList; +import java.util.List; + +/** + * Model for all Fluent lite related models. + */ +public class FluentClient { + + private final Client client; + + private FluentManager manager; + + private ModuleInfo moduleInfo; + + private final List resourceModels = new ArrayList<>(); + + private final List resourceCollections = new ArrayList<>(); + + private final List examples = new ArrayList<>(); + private final List mockUnitTests = new ArrayList<>(); + + private final List liveTests = new ArrayList<>(); + + public FluentClient(Client client) { + this.client = client; + } + + public List getResourceModels() { + return resourceModels; + } + + public List getResourceCollections() { + return resourceCollections; + } + + public FluentManager getManager() { + return manager; + } + + public void setManager(FluentManager manager) { + this.manager = manager; + } + + public Client getInnerClient() { + return this.client; + } + + public ModuleInfo getModuleInfo() { + return moduleInfo; + } + + public void setModuleInfo(ModuleInfo moduleInfo) { + this.moduleInfo = moduleInfo; + } + + public List getExamples() { + return examples; + } + + public List getMockUnitTests() { + return mockUnitTests; + } + + public List getLiveTests() { + return liveTests; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/FluentCollectionMethod.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/FluentCollectionMethod.java new file mode 100644 index 0000000000..138ba58b1e --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/FluentCollectionMethod.java @@ -0,0 +1,88 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel; + +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.immutablemodel.CollectionMethodTemplate; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.immutablemodel.CollectionMethodTypeConversionTemplate; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.immutablemodel.ImmutableMethod; +import com.microsoft.typespec.http.client.generator.mgmt.util.FluentUtils; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethod; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethodParameter; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.IType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ProxyMethod; +import com.microsoft.typespec.http.client.generator.core.template.prototype.MethodTemplate; + +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +public class FluentCollectionMethod { + + private final ClientMethod method; + private final String methodName; + + private final IType fluentReturnType; + + private final ImmutableMethod immutableMethod; + + public FluentCollectionMethod(ClientMethod method) { + this(method, method.getName()); + } + + public FluentCollectionMethod(ClientMethod method, String methodName) { + this.method = method; + this.methodName = methodName; + this.fluentReturnType = FluentUtils.getFluentWrapperType(method.getReturnValue().getType()); + + this.immutableMethod = this.fluentReturnType == method.getReturnValue().getType() + ? new CollectionMethodTemplate(this, method.getReturnValue().getType()) + : new CollectionMethodTypeConversionTemplate(this, method.getReturnValue().getType()); + } + + public IType getFluentReturnType() { + return fluentReturnType; + } + + // method signature + public String getMethodSignature() { + return String.format("%1$s %2$s(%3$s)", this.getFluentReturnType(), getMethodName(), method.getParametersDeclaration()); + } + + public String getMethodName() { + return methodName; + } + + // method invocation + public String getMethodInvocation() { + List methodParameters = method.getMethodInputParameters(); + String argumentsLine = methodParameters.stream().map(ClientMethodParameter::getName).collect(Collectors.joining(", ")); + return String.format("%1$s(%2$s)", method.getName(), argumentsLine); + } + + public String getDescription() { + return method.getDescription(); + } + + public ClientMethod getInnerClientMethod() { + return method; + } + + public ProxyMethod getInnerProxyMethod() { + return method.getProxyMethod(); + } + + public MethodTemplate getImplementationMethodTemplate() { + return immutableMethod.getMethodTemplate(); + } + + public void addImportsTo(Set imports, boolean includeImplementationImports) { + this.getFluentReturnType().addImportsTo(imports, false); + method.addImportsTo(imports, includeImplementationImports, JavaSettings.getInstance()); + + if (includeImplementationImports) { + immutableMethod.getMethodTemplate().addImportsTo(imports); + } + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/FluentExample.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/FluentExample.java new file mode 100644 index 0000000000..010183e5a7 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/FluentExample.java @@ -0,0 +1,81 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel; + +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.examplemodel.FluentClientMethodExample; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.examplemodel.FluentCollectionMethodExample; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.examplemodel.FluentResourceCreateExample; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.examplemodel.FluentResourceUpdateExample; +import com.microsoft.typespec.http.client.generator.core.util.ClassNameUtil; + +import java.util.ArrayList; +import java.util.List; + +public class FluentExample implements Comparable { + + private final String groupName; + private final String methodName; + private final String exampleName; + + private final List collectionMethodExamples = new ArrayList<>(); + private final List resourceCreateExamples = new ArrayList<>(); + private final List resourceUpdateExamples = new ArrayList<>(); + + private final List clientMethodExamples = new ArrayList<>(); + + public FluentExample(String groupName, String methodName, String exampleName) { + this.groupName = groupName; + this.methodName = methodName; + this.exampleName = exampleName; + } + + public List getCollectionMethodExamples() { + return collectionMethodExamples; + } + + public List getResourceCreateExamples() { + return resourceCreateExamples; + } + + public List getResourceUpdateExamples() { + return resourceUpdateExamples; + } + + public List getClientMethodExamples() { + return clientMethodExamples; + } + + public String getGroupName() { + return groupName; + } + + public String getMethodName() { + return methodName; + } + + public String getPackageName() { + JavaSettings settings = JavaSettings.getInstance(); + return settings.getPackage("generated"); + } + + public String getClassName() { + String className = groupName + methodName + "Samples"; + return ClassNameUtil.truncateClassName( + JavaSettings.getInstance().getPackage(), "src/samples/java", + this.getPackageName(), className); + } + + @Override + public int compareTo(FluentExample o) { + int ret = this.groupName.compareTo(o.groupName); + if (ret == 0) { + ret = this.methodName.compareTo(o.methodName); + } + if (ret == 0 && this.exampleName != null && o.exampleName != null) { + ret = this.exampleName.compareTo(o.exampleName); + } + return ret; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/FluentExampleLiveTestStep.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/FluentExampleLiveTestStep.java new file mode 100644 index 0000000000..d25b06a166 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/FluentExampleLiveTestStep.java @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel; + +import com.microsoft.typespec.http.client.generator.mgmt.template.FluentExampleTemplate; + +public class FluentExampleLiveTestStep extends FluentLiveTestStep { + + private FluentExampleTemplate.ExampleMethod exampleMethod; + + public static Builder newBuilder() { + return new Builder(); + } + + public static class Builder extends FluentLiveTestStep.Builder { + + private Builder(){ + super(new FluentExampleLiveTestStep()); + } + + public Builder exampleMethod(FluentExampleTemplate.ExampleMethod exampleMethod) { + getStep().exampleMethod = exampleMethod; + return this; + } + + @Override + protected Builder getThis() { + return this; + } + + } + + public FluentExampleTemplate.ExampleMethod getExampleMethod() { + return exampleMethod; + } + +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/FluentLiveTestCase.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/FluentLiveTestCase.java new file mode 100644 index 0000000000..c5281a7116 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/FluentLiveTestCase.java @@ -0,0 +1,87 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel; + +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.examplemodel.ExampleHelperFeature; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class FluentLiveTestCase { + + private final Set helperFeatures = new HashSet<>(); + private final List steps = new ArrayList<>(); + private String methodName; + private String description; + + public static Builder newBuilder() { + return new Builder(); + } + + public String getDescription() { + return description; + } + + public String getMethodName() { + return methodName; + } + + public Set getHelperFeatures() { + return helperFeatures; + } + + public List getSteps() { + return steps; + } + + public static final class Builder { + private Set helperFeatures = new HashSet<>(); + private List steps = new ArrayList<>(); + private String methodName; + private String description; + + private Builder() { + } + + public Set getHelperFeatures(){ + return Collections.unmodifiableSet(this.helperFeatures); + } + + public Builder addHelperFeatures(Set helperFeatures) { + if (helperFeatures != null) { + this.helperFeatures.addAll(helperFeatures); + } + return this; + } + + public Builder addSteps(List steps) { + if (steps != null) { + this.steps.addAll(steps); + } + return this; + } + + public Builder methodName(String methodName) { + this.methodName = methodName; + return this; + } + + public Builder description(String description) { + this.description = description; + return this; + } + + public FluentLiveTestCase build() { + FluentLiveTestCase fluentLiveTestCase = new FluentLiveTestCase(); + fluentLiveTestCase.steps.addAll(this.steps); + fluentLiveTestCase.helperFeatures.addAll(this.helperFeatures); + fluentLiveTestCase.description = this.description; + fluentLiveTestCase.methodName = this.methodName; + return fluentLiveTestCase; + } + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/FluentLiveTestStep.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/FluentLiveTestStep.java new file mode 100644 index 0000000000..d5a312909e --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/FluentLiveTestStep.java @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel; + +public abstract class FluentLiveTestStep { + + private String description; + + public String getDescription() { + return description; + } + + void setDescription(String description) { + this.description = description; + } + + public static abstract class Builder { + private final S step; + + abstract protected T getThis(); + + protected Builder(S step) { + this.step = step; + } + + protected S getStep() { + return step; + } + + public T description(String description) { + step.setDescription(description); + return getThis(); + } + + public S build(){ + return step; + } + + } + +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/FluentLiveTests.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/FluentLiveTests.java new file mode 100644 index 0000000000..496d1c2bc1 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/FluentLiveTests.java @@ -0,0 +1,117 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel; + +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClassType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.examplemodel.ExampleHelperFeature; +import com.azure.core.util.CoreUtils; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class FluentLiveTests { + + private String className; + private final Set imports = new HashSet<>(); + private final Set helperFeatures = new HashSet<>(); + private final List testCases = new ArrayList<>(); + private ClassType managerType; + private String managerName; + + public ClassType getManagerType() { + return managerType; + } + + + public String getManagerName() { + return managerName; + } + + public String getClassName() { + return className; + } + + public Set getImports() { + return imports; + } + + public Set getHelperFeatures() { + return helperFeatures; + } + + public List getTestCases() { + return testCases; + } + + public String getPackageName() { + return JavaSettings.getInstance().getPackage("livetests"); + } + + public static Builder newBuilder() { + return new Builder(); + } + + public static final class Builder { + private String className; + private final Set imports = new HashSet<>(); + private final Set helperFeatures = new HashSet<>(); + private final List testCases = new ArrayList<>(); + private ClassType managerType; + private String managerName; + + private Builder() { + } + + public Builder className(String className) { + this.className = className; + return this; + } + + public Builder addImports(Collection imports) { + if (!CoreUtils.isNullOrEmpty(imports)) { + this.imports.addAll(imports); + } + return this; + } + + public Builder addHelperFeatures(Collection helperFeatures) { + if (!CoreUtils.isNullOrEmpty(helperFeatures)) { + this.helperFeatures.addAll(helperFeatures); + } + return this; + } + + public Builder addTestCases(Collection testCase) { + if (testCase != null) { + this.testCases.addAll(testCase); + } + return this; + } + + public Builder managerType(ClassType managerType) { + this.managerType = managerType; + return this; + } + + public Builder managerName(String managerName) { + this.managerName = managerName; + return this; + } + + public FluentLiveTests build() { + FluentLiveTests fluentLiveTests = new FluentLiveTests(); + fluentLiveTests.className = className; + fluentLiveTests.managerType = managerType; + fluentLiveTests.managerName = managerName; + fluentLiveTests.testCases.addAll(this.testCases); + fluentLiveTests.helperFeatures.addAll(this.helperFeatures); + fluentLiveTests.imports.addAll(this.imports); + return fluentLiveTests; + } + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/FluentManager.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/FluentManager.java new file mode 100644 index 0000000000..f16c8f7f08 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/FluentManager.java @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel; + +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; +import com.microsoft.typespec.http.client.generator.mgmt.util.FluentUtils; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClassType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.Client; +import com.microsoft.typespec.http.client.generator.core.util.CodeNamer; +import com.azure.core.util.CoreUtils; + +import java.util.ArrayList; +import java.util.List; + +/** + * Model for Manager. + */ +public class FluentManager { + + private final Client client; + + private final ClassType type; + + private final String serviceName; + + private final List properties = new ArrayList<>(); + + public FluentManager(Client client, String clientName) { + JavaSettings settings = JavaSettings.getInstance(); + + this.client = client; + + this.serviceName = FluentUtils.getServiceName(clientName); + + this.type = new ClassType.Builder() + .packageName(settings.getPackage()) + .name(CodeNamer.toPascalCase(this.serviceName) + "Manager") + .build(); + } + + public Client getClient() { + return client; + } + + public ClassType getType() { + return type; + } + + public String getDescription() { + String description = String.format("Entry point to %1$s.", this.getType().getName()); + if (!CoreUtils.isNullOrEmpty(client.getClientDescription())) { + description += "\n" + client.getClientDescription(); + } + return description; + } + + public String getServiceName() { + return serviceName; + } + + public List getProperties() { + return properties; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/FluentManagerProperty.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/FluentManagerProperty.java new file mode 100644 index 0000000000..9ed083f1c1 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/FluentManagerProperty.java @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel; + +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClassType; +import com.microsoft.typespec.http.client.generator.core.util.CodeNamer; + +import java.util.List; +import java.util.stream.Collectors; + +public class FluentManagerProperty { + + private final String name; + + private final ClassType fluentType; + private final ClassType fluentImplementType; + private final String innerClientGetMethod; + + private final FluentResourceCollection resourceCollection; + + public FluentManagerProperty(FluentResourceCollection collection) { + this.resourceCollection = collection; + + this.fluentType = collection.getInterfaceType(); + this.fluentImplementType = collection.getImplementationType(); + + String interfaceName = fluentType.getName(); + this.name = CodeNamer.toCamelCase(interfaceName); + + this.innerClientGetMethod = "get" + CodeNamer.toPascalCase(collection.getInnerGroupClient().getVariableName()); + } + + public String getName() { + return name; + } + + public ClassType getFluentType() { + return fluentType; + } + + public ClassType getFluentImplementType() { + return fluentImplementType; + } + + public String getMethodName() { + return CodeNamer.getModelNamer().modelPropertyGetterName(name); + } + + public String getInnerClientGetMethod() { + return innerClientGetMethod; + } + + public List getResourceModelTypes() { + return this.resourceCollection.getResourceCreates().stream() + .map(rc -> rc.getResourceModel().getInterfaceType()) + .collect(Collectors.toList()); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/FluentModelProperty.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/FluentModelProperty.java new file mode 100644 index 0000000000..10e777758f --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/FluentModelProperty.java @@ -0,0 +1,95 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel; + +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.immutablemodel.ImmutableMethod; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.immutablemodel.PropertyTemplate; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.immutablemodel.PropertyTypeConversionTemplate; +import com.microsoft.typespec.http.client.generator.mgmt.util.FluentUtils; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClassType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientModelPropertyAccess; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.IType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ListType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.MapType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ModelProperty; +import com.microsoft.typespec.http.client.generator.core.template.prototype.MethodTemplate; + +import java.util.Set; + +public class FluentModelProperty { + + private final ModelProperty modelProperty; + + private final IType fluentType; + + private final ImmutableMethod immutableMethod; + + public FluentModelProperty(ClientModelPropertyAccess property) { + this.modelProperty = ModelProperty.ofClientModelProperty(property); + this.fluentType = getWrapperType(property.getClientType()); + this.immutableMethod = this.fluentType == property.getClientType() + ? new PropertyTemplate(this, this.modelProperty) + : new PropertyTypeConversionTemplate(this, this.modelProperty); + } + + public String getName() { + return modelProperty.getName(); + } + + public String getDescription() { + return modelProperty.getDescription(); + } + + public IType getFluentType() { + return fluentType; + } + + // method signature for model property + public String getMethodSignature() { + return String.format("%1$s %2$s()", this.getFluentType(), this.getGetterName()); + } + + public String getMethodName() { + return this.getGetterName(); + } + + public void addImportsTo(Set imports, boolean includeImplementationImports) { + this.fluentType.addImportsTo(imports, false); + + if (includeImplementationImports) { + this.immutableMethod.getMethodTemplate().addImportsTo(imports); + } + } + + public MethodTemplate getImplementationMethodTemplate() { + return immutableMethod.getMethodTemplate(); + } + + private String getGetterName() { + return modelProperty.getGetterName(); + } + + private static IType getWrapperType(IType clientType) { + IType wrapperType = clientType; + if (clientType instanceof ClassType) { + ClassType type = (ClassType) clientType; + if (FluentUtils.isInnerClassType(type)) { + wrapperType = FluentUtils.resourceModelInterfaceClassType(type); + } + } else if (clientType instanceof ListType) { + ListType type = (ListType) clientType; + IType wrapperElementType = getWrapperType(type.getElementType()); + wrapperType = wrapperElementType == type.getElementType() ? type : new ListType(wrapperElementType); + } else if (clientType instanceof MapType) { + MapType type = (MapType) clientType; + IType wrapperElementType = getWrapperType(type.getValueType()); + wrapperType = wrapperElementType == type.getValueType() ? type : new MapType(wrapperElementType); + } + return wrapperType; + } + + public ModelProperty getModelProperty() { + return modelProperty; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/FluentResourceCollection.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/FluentResourceCollection.java new file mode 100644 index 0000000000..7637a7d1c0 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/FluentResourceCollection.java @@ -0,0 +1,202 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel; + +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; +import com.microsoft.typespec.http.client.generator.mgmt.model.WellKnownMethodName; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.create.ResourceCreate; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.delete.ResourceDelete; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.get.ResourceRefresh; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.update.ResourceUpdate; +import com.microsoft.typespec.http.client.generator.mgmt.util.FluentUtils; +import com.microsoft.typespec.http.client.generator.mgmt.util.Utils; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClassType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethod; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethodParameter; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethodType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.MethodGroupClient; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaVisibility; +import com.microsoft.typespec.http.client.generator.core.template.prototype.MethodTemplate; +import com.microsoft.typespec.http.client.generator.core.util.CodeNamer; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Model for Azure resource collection. + */ +// Fluent resource collection API. E.g. StorageAccounts. +public class FluentResourceCollection { + + // implementation client. E.g. StorageAccountsClientImpl. + private final MethodGroupClient groupClient; + + // class type for inner client. E.g. StorageAccountsClient (which is a layer over StorageAccountsClientImpl). + private final ClassType innerClientType; + + // class type for interface and implementation + private final ClassType interfaceType; + private final ClassType implementationType; + + // API methods + private final List methods = new ArrayList<>(); + + // resource models + private final List resourceCreates = new ArrayList<>(); + private final List resourceUpdates = new ArrayList<>(); + private final List resourceRefreshes = new ArrayList<>(); + private final List resourceDeletes = new ArrayList<>(); + private final List additionalMethods = new ArrayList<>(); + + public FluentResourceCollection(MethodGroupClient groupClient) { + JavaSettings settings = JavaSettings.getInstance(); + + this.groupClient = groupClient; + + String baseClassName = CodeNamer.getPlural(groupClient.getClassBaseName()); + + this.interfaceType = new ClassType.Builder() + .packageName(settings.getPackage(settings.getModelsSubpackage())) + .name(baseClassName) + .build(); + this.implementationType = new ClassType.Builder() + .packageName(settings.getPackage(settings.getImplementationSubpackage())) + .name(baseClassName + ModelNaming.COLLECTION_IMPL_SUFFIX) + .build(); + + this.innerClientType = new ClassType.Builder() + .packageName(settings.getPackage(settings.getFluentSubpackage())) + .name(groupClient.getInterfaceName()) + .build(); + + Set existingMethodNames = this.groupClient.getClientMethods().stream() + .filter(m -> !m.isImplementationOnly() && m.getMethodVisibility() == JavaVisibility.Public) + .map(ClientMethod::getName) + .collect(Collectors.toSet()); + + for (ClientMethod clientMethod : this.groupClient.getClientMethods()) { + if (clientMethod.isImplementationOnly() || clientMethod.getMethodVisibility() != JavaVisibility.Public) { + continue; + } + + ClientMethodType methodType = clientMethod.getType(); + boolean isSyncMethod = (methodType == ClientMethodType.SimpleSync + || methodType == ClientMethodType.PagingSync + || methodType == ClientMethodType.LongRunningSync + || methodType == ClientMethodType.SimpleSyncRestResponse); +// boolean isAsyncMethod = (methodType == ClientMethodType.SimpleAsync +// || methodType == ClientMethodType.PagingAsync +// || methodType == ClientMethodType.LongRunningAsync +// || methodType == ClientMethodType.SimpleAsyncRestResponse); + + if (!isSyncMethod /*&& (!isAsyncMethod && FluentStatic.getFluentJavaSettings().isGenerateAsyncMethods())*/) { + continue; + } + + // map "delete" in client to "deleteByResourceGroup" in collection + String methodName = clientMethod.getName(); + List methodParameters = clientMethod.getMethodParameters(); + + FluentCollectionMethod fluentMethod; + if (WellKnownMethodName.DELETE.getMethodName().equals(methodName) + && (methodType == ClientMethodType.SimpleSync || methodType == ClientMethodType.LongRunningSync) + && !existingMethodNames.contains(WellKnownMethodName.DELETE_BY_RESOURCE_GROUP.getMethodName()) + && methodParameters.size() == 2 + && methodParameters.get(0).getClientType() == ClassType.STRING + && methodParameters.get(1).getClientType() == ClassType.STRING) { + // Transform "delete(String, String)" into "deleteByResourceGroup(String, String)" + fluentMethod = new FluentCollectionMethod(clientMethod, WellKnownMethodName.DELETE_BY_RESOURCE_GROUP.getMethodName()); + existingMethodNames.add(fluentMethod.getMethodName()); + } else if ((WellKnownMethodName.DELETE.getMethodName() + Utils.METHOD_POSTFIX_WITH_RESPONSE).equals(methodName) + && methodType == ClientMethodType.SimpleSyncRestResponse + && !existingMethodNames.contains(WellKnownMethodName.DELETE_BY_RESOURCE_GROUP.getMethodName() + Utils.METHOD_POSTFIX_WITH_RESPONSE) + && methodParameters.size() == 3 + && methodParameters.get(0).getClientType() == ClassType.STRING + && methodParameters.get(1).getClientType() == ClassType.STRING) { + // Transform "deleteWithResponse(String, String, ?)" into "deleteByResourceGroupWithResponse(String, String, ?)" + fluentMethod = new FluentCollectionMethod(clientMethod, WellKnownMethodName.DELETE_BY_RESOURCE_GROUP.getMethodName() + Utils.METHOD_POSTFIX_WITH_RESPONSE); + existingMethodNames.add(fluentMethod.getMethodName()); + } else { + fluentMethod = new FluentCollectionMethod(clientMethod); + } + + this.methods.add(fluentMethod); + } + } + + public MethodGroupClient getInnerGroupClient() { + return groupClient; + } + + public ClassType getInterfaceType() { + return interfaceType; + } + + public ClassType getImplementationType() { + return implementationType; + } + + public List getMethodsForTemplate() { + List fluentMethods = new ArrayList<>(methods); + + Set excludeMethods = new HashSet<>(); + excludeMethods.addAll(this.getResourceCreates().stream().flatMap(rc -> rc.getMethodReferences().stream()).collect(Collectors.toSet())); + excludeMethods.addAll(this.getResourceUpdates().stream().flatMap(ru -> ru.getMethodReferences().stream()).collect(Collectors.toSet())); + fluentMethods.removeAll(excludeMethods); + + return fluentMethods; + } + + public List getMethods() { + return this.methods; + } + + public String getDescription() { + return String.format("Resource collection API of %s.", interfaceType.getName()); + } + + public ClassType getInnerClientType() { + return innerClientType; + } + + // method signature for inner client + public String getInnerMethodSignature() { + return String.format("%1$s %2$s()", this.getInnerClientType().getName(), FluentUtils.getGetterName(ModelNaming.METHOD_SERVICE_CLIENT)); + } + + public List getResourceCreates() { + return resourceCreates; + } + + public List getResourceUpdates() { + return resourceUpdates; + } + + public List getResourceGets() { + return resourceRefreshes; + } + + public List getResourceDeletes() { + return resourceDeletes; + } + + public List getAdditionalMethods() { + return additionalMethods; + } + + public void addImportsTo(Set imports, boolean includeImplementationImports) { + innerClientType.addImportsTo(imports, false); + + this.getMethods().forEach(m -> m.addImportsTo(imports, includeImplementationImports)); + + if (includeImplementationImports) { + interfaceType.addImportsTo(imports, false); + } + + additionalMethods.forEach(m -> m.addImportsTo(imports)); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/FluentResourceModel.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/FluentResourceModel.java new file mode 100644 index 0000000000..41de3dc32c --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/FluentResourceModel.java @@ -0,0 +1,201 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel; + +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; +import com.microsoft.typespec.http.client.generator.mgmt.model.arm.ModelCategory; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.action.ResourceActions; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.create.ResourceCreate; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.ResourceImplementation; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.get.ResourceRefresh; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.update.ResourceUpdate; +import com.microsoft.typespec.http.client.generator.mgmt.util.FluentUtils; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClassType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientModel; +import com.microsoft.typespec.http.client.generator.core.template.prototype.MethodTemplate; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Model for Azure resource instance. + */ +// Fluent resource instance. E.g. StorageAccount. +// Also include some simple wrapper class. +public class FluentResourceModel { + + // inner model. E.g. StorageAccountInner. + private final ClientModel innerModel; + // all parent models of the inner model (property of which need to be put to resource class as well) + private final List parentModels; + + // class type for interface and implementation + private final ClassType interfaceType; + private final ClassType implementationType; + + // resource properties + private final Map propertiesMap = new LinkedHashMap<>(); + private final List properties = new ArrayList<>(); + + // category of the resource + private ModelCategory category = ModelCategory.IMMUTABLE; + private ResourceCreate resourceCreate; + private ResourceUpdate resourceUpdate; + private ResourceRefresh resourceRefresh; + private ResourceActions resourceActions; + private final List additionalMethods = new ArrayList<>(); + + public FluentResourceModel(ClientModel innerModel, List parentModels) { + JavaSettings settings = JavaSettings.getInstance(); + + this.innerModel = innerModel; + this.parentModels = parentModels; + + interfaceType = FluentUtils.resourceModelInterfaceClassType(innerModel.getName()); + implementationType = new ClassType.Builder() + .packageName(settings.getPackage(settings.getImplementationSubpackage())) + .name(interfaceType.getName() + ModelNaming.MODEL_IMPL_SUFFIX) + .build(); + + List> propertiesFromTypeAndParents = new ArrayList<>(); + propertiesFromTypeAndParents.add(new ArrayList<>()); + this.innerModel.getAccessibleProperties().stream() + .map(FluentModelProperty::new) + .forEach(p -> { + propertiesMap.putIfAbsent(p.getName(), p); + propertiesFromTypeAndParents.get(propertiesFromTypeAndParents.size() - 1).add(p); + }); + + for (ClientModel parent : parentModels) { + propertiesFromTypeAndParents.add(new ArrayList<>()); + + parent.getAccessibleProperties().stream() + .map(FluentModelProperty::new) + .forEach(p -> { + if (propertiesMap.putIfAbsent(p.getName(), p) == null) { + propertiesFromTypeAndParents.get(propertiesFromTypeAndParents.size() - 1).add(p); + } + }); + } + + Collections.reverse(propertiesFromTypeAndParents); + for (List properties1 : propertiesFromTypeAndParents) { + properties.addAll(properties1); + } + } + + public String getName() { + return interfaceType.getName(); + } + + public ClientModel getInnerModel() { + return innerModel; + } + + public ClassType getInterfaceType() { + return interfaceType; + } + + public ClassType getImplementationType() { + return implementationType; + } + + public boolean hasProperty(String name) { + return propertiesMap.containsKey(name); + } + + public FluentModelProperty getProperty(String name) { + return propertiesMap.get(name); + } + + public Collection getProperties() { + return properties; + } + + public String getDescription() { + return String.format("An immutable client-side representation of %s.", interfaceType.getName()); + } + + // method signature for inner model + public String getInnerMethodSignature() { + return String.format("%1$s %2$s()", this.getInnerModel().getName(), FluentUtils.getGetterName(ModelNaming.METHOD_INNER_MODEL)); + } + + public ModelCategory getCategory() { + return category; + } + + public void setCategory(ModelCategory category) { + this.category = category; + } + + public ResourceImplementation getResourceImplementation() { + return new ResourceImplementation(this); + } + + public ResourceCreate getResourceCreate() { + return resourceCreate; + } + + public void setResourceCreate(ResourceCreate resourceCreate) { + this.resourceCreate = resourceCreate; + } + + public ResourceUpdate getResourceUpdate() { + return resourceUpdate; + } + + public void setResourceUpdate(ResourceUpdate resourceUpdate) { + this.resourceUpdate = resourceUpdate; + } + + public ResourceRefresh getResourceRefresh() { + return resourceRefresh; + } + + public void setResourceRefresh(ResourceRefresh resourceRefresh) { + this.resourceRefresh = resourceRefresh; + } + + public ResourceActions getResourceActions() { + return resourceActions; + } + + public void setResourceActions(ResourceActions resourceActions) { + this.resourceActions = resourceActions; + } + + public List getAdditionalMethods() { + return additionalMethods; + } + + public void addImportsTo(Set imports, boolean includeImplementationImports) { + imports.add(this.getInnerModel().getFullName()); + + this.getProperties().forEach(p -> p.addImportsTo(imports, includeImplementationImports)); + + if (includeImplementationImports) { + interfaceType.addImportsTo(imports, false); + } + + if (resourceCreate != null) { + resourceCreate.addImportsTo(imports, includeImplementationImports); + } + if (resourceUpdate != null) { + resourceUpdate.addImportsTo(imports, includeImplementationImports); + } + if (resourceRefresh != null) { + resourceRefresh.addImportsTo(imports, includeImplementationImports); + } + if (resourceActions != null) { + resourceActions.addImportsTo(imports, includeImplementationImports); + } + additionalMethods.forEach(m -> m.addImportsTo(imports)); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/FluentStatic.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/FluentStatic.java new file mode 100644 index 0000000000..3dbfb370d3 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/FluentStatic.java @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel; + +import com.microsoft.typespec.http.client.generator.mgmt.util.FluentJavaSettings; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.Client; + +/** + * Convenient class for global variables. + * + * Avoid using it unless no better solution. + */ +public class FluentStatic { + + private static Client client; + + private static FluentClient fluentClient; + + private static FluentJavaSettings fluentJavaSettings; + + private FluentStatic() { + } + + /** + * @return the client on service client and method groups. + */ + public static Client getClient() { + return client; + } + + public static void setClient(Client client) { + FluentStatic.client = client; + } + + /** + * @return the client on Fluent manager, resource collections and instances (models) + */ + public static FluentManager getFluentManager() { + return fluentClient.getManager(); + } + + public static void setFluentClient(FluentClient fluentClient) { + FluentStatic.fluentClient = fluentClient; + } + + /** + * @return settings for Fluent. + */ + public static FluentJavaSettings getFluentJavaSettings() { + return fluentJavaSettings; + } + + public static void setFluentJavaSettings(FluentJavaSettings fluentJavaSettings) { + FluentStatic.fluentJavaSettings = fluentJavaSettings; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/ModelNaming.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/ModelNaming.java new file mode 100644 index 0000000000..0d888c3f86 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/ModelNaming.java @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel; + +public class ModelNaming { + + public static final String METHOD_SERVICE_CLIENT = "serviceClient"; + public static final String METHOD_INNER_MODEL = "innerModel"; + public static final String METHOD_MANAGER = "manager"; + + public static final String MODEL_IMPL_SUFFIX = "Impl"; + + public static final String MODEL_PROPERTY_INNER = "innerObject"; + public static final String MODEL_PROPERTY_MANAGER = "serviceManager"; + + public static final String COLLECTION_IMPL_SUFFIX = "Impl"; + + public static final String COLLECTION_PROPERTY_INNER = "innerClient"; + public static final String COLLECTION_PROPERTY_MANAGER = "serviceManager"; + + public static final String MANAGER_PROPERTY_CLIENT = "clientObject"; + + public static final String MODEL_FLUENT_INTERFACE_DEFINITION = "Definition"; + public static final String MODEL_FLUENT_INTERFACE_DEFINITION_STAGES = "DefinitionStages"; + + public static final String MODEL_FLUENT_INTERFACE_UPDATE = "Update"; + public static final String MODEL_FLUENT_INTERFACE_UPDATE_STAGES = "UpdateStages"; + + public static final String METHOD_PARAMETER_NAME_ID = "id"; + + public static final String CLASS_RESOURCE_MANAGER_UTILS = "ResourceManagerUtils"; + + private ModelNaming() { + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/examplemodel/FluentBaseExample.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/examplemodel/FluentBaseExample.java new file mode 100644 index 0000000000..3661784576 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/examplemodel/FluentBaseExample.java @@ -0,0 +1,67 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.examplemodel; + +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentManager; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentResourceCollection; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClassType; + +import java.util.ArrayList; +import java.util.List; + +public abstract class FluentBaseExample implements FluentExample { + + private final String name; + private final String originalFileName; + private final FluentManager manager; + private final FluentResourceCollection collection; + private final List parameters = new ArrayList<>(); + + public FluentBaseExample(String name, String originalFileName, + FluentManager manager, FluentResourceCollection collection) { + this.name = name; + this.originalFileName = originalFileName; + this.manager = manager; + this.collection = collection; + } + + @Override + public String getName() { + return name; + } + + @Override + public String getOriginalFileName() { + return originalFileName; + } + + public FluentManager getManager() { + return manager; + } + + + @Override + public ClassType getEntryType() { + return manager.getType(); + } + + @Override + public String getEntryName() { + return "manager"; + } + + @Override + public String getEntryDescription() { + return String.format("Entry point to %1$s.", manager.getType().getName()); + } + + public FluentResourceCollection getResourceCollection() { + return collection; + } + + @Override + public List getParameters() { + return parameters; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/examplemodel/FluentClientMethodExample.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/examplemodel/FluentClientMethodExample.java new file mode 100644 index 0000000000..fc57c20cec --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/examplemodel/FluentClientMethodExample.java @@ -0,0 +1,145 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.examplemodel; + +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; +import com.microsoft.typespec.http.client.generator.mgmt.model.FluentType; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.ModelNaming; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClassType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethod; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.MethodGroupClient; +import com.microsoft.typespec.http.client.generator.core.util.CodeNamer; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; + +/** + * Model of example for service client method (usually for Fluent Premium). + */ +public class FluentClientMethodExample implements FluentMethodExample { + + private final String name; + private final String originalFileName; + private final MethodGroupClient methodGroup; + private final ClientMethod clientMethod; + private final List parameters = new ArrayList<>(); + + public FluentClientMethodExample(String name, String originalFileName, + MethodGroupClient methodGroup, ClientMethod clientMethod) { + this.name = name; + this.originalFileName = originalFileName; + this.methodGroup = methodGroup; + this.clientMethod = clientMethod; + } + + public MethodGroupClient getMethodGroup() { + return methodGroup; + } + + public ClientMethod getClientMethod() { + return clientMethod; + } + + @Override + public String getName() { + return name; + } + + @Override + public String getOriginalFileName() { + return originalFileName; + } + + @Override + public ClassType getEntryType() { + return FluentType.AZURE_RESOURCE_MANAGER; + } + + @Override + public String getEntryName() { + return "azure"; + } + + @Override + public String getEntryDescription() { + return "The entry point for accessing resource management APIs in Azure."; + } + + @Override + public List getParameters() { + return parameters; + } + + @Override + public String getMethodReference() { + JavaSettings settings = JavaSettings.getInstance(); + + String namespace = settings.getPackage(); + String[] identifiers = namespace.split(Pattern.quote(".")); + String lastIdentifier = identifiers[identifiers.length - 1]; + + if (!MANAGER_REFERENCE.containsKey(lastIdentifier)) { + throw new IllegalStateException("Package '" + namespace + "' is not supported by Fluent Premium"); + } + + String managerReference = MANAGER_REFERENCE.get(lastIdentifier) + "." + ModelNaming.METHOD_MANAGER + "()"; + String serviceClientReference = ModelNaming.METHOD_SERVICE_CLIENT + "()"; + if ("authorization".equals(lastIdentifier)) { + serviceClientReference = "roleServiceClient()"; + } else if ("resources".equals(lastIdentifier)) { + String tag = settings.getAutorestSettings().getTag(); + if (tag.contains("feature")) { + serviceClientReference = "featureClient()"; + } else if (tag.contains("policy")) { + serviceClientReference = "policyClient()"; + } else if (tag.contains("subscriptions")) { + serviceClientReference = "subscriptionClient()"; + } else if (tag.contains("locks")) { + serviceClientReference = "managementLockClient()"; + } else if (tag.contains("changes")) { + serviceClientReference = "resourceChangeClient()"; + } else if (tag.contains("deploymentstacks")) { + serviceClientReference = "deploymentStackClient()"; + } + } + + String methodGroupReference = "get" + CodeNamer.toPascalCase(methodGroup.getVariableName()) + "()"; + return managerReference + "." + serviceClientReference + "." + methodGroupReference; + } + + @Override + public String getMethodName() { + return clientMethod.getName(); + } + + private final static Map MANAGER_REFERENCE = new HashMap<>(); + static { + MANAGER_REFERENCE.put("appplatform", "springServices()"); + MANAGER_REFERENCE.put("appservice", "webApps()"); + MANAGER_REFERENCE.put("authorization", "accessManagement().roleAssignments()"); + MANAGER_REFERENCE.put("cdn", "cdnProfiles()"); + MANAGER_REFERENCE.put("compute", "virtualMachines()"); + MANAGER_REFERENCE.put("containerinstance", "containerGroups()"); + MANAGER_REFERENCE.put("containerregistry", "containerRegistries()"); + MANAGER_REFERENCE.put("containerservice", "kubernetesClusters()"); + MANAGER_REFERENCE.put("cosmos", "cosmosDBAccounts()"); + MANAGER_REFERENCE.put("dns", "dnsZones()"); + MANAGER_REFERENCE.put("eventhubs", "eventHubs()"); + MANAGER_REFERENCE.put("keyvault", "vaults()"); + MANAGER_REFERENCE.put("monitor", "diagnosticSettings()"); + MANAGER_REFERENCE.put("msi", "identities()"); + MANAGER_REFERENCE.put("network", "networks()"); + MANAGER_REFERENCE.put("privatedns", "privateDnsZones()"); + MANAGER_REFERENCE.put("redis", "redisCaches()"); + MANAGER_REFERENCE.put("resources", "genericResources()"); + MANAGER_REFERENCE.put("search", "searchServices()"); + MANAGER_REFERENCE.put("servicebus", "serviceBusNamespaces()"); + MANAGER_REFERENCE.put("sql", "sqlServers()"); + MANAGER_REFERENCE.put("storage", "storageAccounts()"); + MANAGER_REFERENCE.put("trafficmanager", "trafficManagerProfiles()"); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/examplemodel/FluentCollectionMethodExample.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/examplemodel/FluentCollectionMethodExample.java new file mode 100644 index 0000000000..a450b8e8e3 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/examplemodel/FluentCollectionMethodExample.java @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.examplemodel; + +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentCollectionMethod; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentManager; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentResourceCollection; +import com.microsoft.typespec.http.client.generator.core.util.CodeNamer; + +/** + * Model of example for FluentCollectionMethod. + */ +public class FluentCollectionMethodExample extends FluentBaseExample implements FluentMethodExample { + + private final FluentCollectionMethod collectionMethod; + + public FluentCollectionMethodExample(String name, String originalFileName, + FluentManager manager, FluentResourceCollection collection, + FluentCollectionMethod collectionMethod) { + super(name, originalFileName, manager, collection); + this.collectionMethod = collectionMethod; + } + + public FluentCollectionMethod getCollectionMethod() { + return collectionMethod; + } + + @Override + public String getMethodReference() { + return CodeNamer.toCamelCase(this.getResourceCollection().getInterfaceType().getName()) + "()"; + } + + @Override + public String getMethodName() { + return collectionMethod.getMethodName(); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/examplemodel/FluentExample.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/examplemodel/FluentExample.java new file mode 100644 index 0000000000..e31e8c5a2e --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/examplemodel/FluentExample.java @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.examplemodel; + +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClassType; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentManager; + +import java.util.List; + +/** + * Basic info for Fluent samples. + */ +public interface FluentExample { + + /** + * @return the name of the sample. + */ + String getName(); + + /** + * @return the file name of the original example in JSON. + */ + String getOriginalFileName(); + + /** + * @return the type of the entry (usually a {@link FluentManager}). + */ + ClassType getEntryType(); + + /** + * @return the name of the entry. + */ + String getEntryName(); + + /** + * @return the description of the entry. + */ + String getEntryDescription(); + + /** + * @return the list of parameters used in the sample. + */ + List getParameters(); +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/examplemodel/FluentMethodExample.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/examplemodel/FluentMethodExample.java new file mode 100644 index 0000000000..ab54e4a880 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/examplemodel/FluentMethodExample.java @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.examplemodel; + +/** + * Basic info for Fluent samples with method call. + */ +public interface FluentMethodExample extends FluentExample { + + /** + * Gets the reference from entry class to the method. + * + * E.g. "deployments()" for collection method from Fluent Lite, + * or "serviceClient().getDeployments()" for service client method from Fluent Premium. + * + * This method is used together with {@link FluentExample#getEntryType()}. + * + * @return the reference from entry class to the method. + */ + String getMethodReference(); + + /** + * @return the method name (of the resource collection method, or the client method). + */ + String getMethodName(); +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/examplemodel/FluentMethodMockUnitTest.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/examplemodel/FluentMethodMockUnitTest.java new file mode 100644 index 0000000000..ebf862cd9a --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/examplemodel/FluentMethodMockUnitTest.java @@ -0,0 +1,109 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.examplemodel; + +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentCollectionMethod; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentResourceCollection; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.IType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ProxyMethodExample; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.examplemodel.ExampleNode; + +public class FluentMethodMockUnitTest { + + // method with mock data + private final FluentMethodExample fluentMethodExample; + private final IType fluentReturnType; + private final FluentResourceCreateExample fluentResourceCreateExample; + + private final FluentResourceCollection resourceCollection; + private final FluentCollectionMethod collectionMethod; + + private final ProxyMethodExample.Response response; + + // for pageable, following variable and data is the first element + private final String responseVerificationVariableName; + private final ExampleNode responseVerificationNode; + + public FluentMethodMockUnitTest( + FluentMethodExample fluentMethodExample, + FluentResourceCollection resourceCollection, FluentCollectionMethod fluentCollectionMethod, + IType fluentReturnType, ProxyMethodExample.Response response, + String responseVerificationVariableName, ExampleNode responseVerificationNode) { + + this.fluentMethodExample = fluentMethodExample; + this.fluentResourceCreateExample = null; + this.resourceCollection = resourceCollection; + this.collectionMethod = fluentCollectionMethod; + this.fluentReturnType = fluentReturnType; + this.response = response; + this.responseVerificationVariableName = responseVerificationVariableName; + this.responseVerificationNode = responseVerificationNode; + } + + public FluentMethodMockUnitTest( + FluentResourceCreateExample fluentResourceCreateExample, + FluentResourceCollection resourceCollection, FluentCollectionMethod collectionMethod, + IType fluentReturnType, ProxyMethodExample.Response response, + String responseVerificationVariableName, ExampleNode responseVerificationNode) { + + this.fluentMethodExample = null; + this.fluentResourceCreateExample = fluentResourceCreateExample; + this.resourceCollection = resourceCollection; + this.collectionMethod = collectionMethod; + this.fluentReturnType = fluentReturnType; + this.response = response; + this.responseVerificationVariableName = responseVerificationVariableName; + this.responseVerificationNode = responseVerificationNode; + } + + /** + * @return example of method in collection, mutually exclusive with {@link #getFluentResourceCreateExample()}. + */ + public FluentMethodExample getFluentMethodExample() { + return fluentMethodExample; + } + + /** + * @return return type. It could be the inner client model type of Fluent resource create/update/get method. + */ + public IType getFluentReturnType() { + return fluentReturnType; + } + + /** + * @return example of resource creation, mutually exclusive with {@link #getFluentMethodExample()}. + */ + public FluentResourceCreateExample getFluentResourceCreateExample() { + return fluentResourceCreateExample; + } + + public FluentResourceCollection getResourceCollection() { + return resourceCollection; + } + + public FluentCollectionMethod getCollectionMethod() { + return collectionMethod; + } + + /** + * @return the mock data response with status code and "body" and "headers". + */ + public ProxyMethodExample.Response getResponse() { + return response; + } + + /** + * @return variable name for verification, e.g. "response", "response.iterator().next()" for pageable. + */ + public String getResponseVerificationVariableName() { + return responseVerificationVariableName; + } + + /** + * @return example node as data of the variable for verification. + */ + public ExampleNode getResponseVerificationNode() { + return responseVerificationNode; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/examplemodel/FluentResourceCreateExample.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/examplemodel/FluentResourceCreateExample.java new file mode 100644 index 0000000000..2af5ee6b3e --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/examplemodel/FluentResourceCreateExample.java @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.examplemodel; + +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentManager; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentResourceCollection; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.create.ResourceCreate; + +/** + * Model of example for ResourceCreate. + */ +public class FluentResourceCreateExample extends FluentBaseExample { + + private final ResourceCreate resourceCreate; + + public FluentResourceCreateExample(String name, String originalFileName, + FluentManager manager, FluentResourceCollection collection, + ResourceCreate resourceCreate) { + super(name, originalFileName, manager, collection); + this.resourceCreate = resourceCreate; + } + + public ResourceCreate getResourceCreate() { + return resourceCreate; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/examplemodel/FluentResourceUpdateExample.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/examplemodel/FluentResourceUpdateExample.java new file mode 100644 index 0000000000..015d8e6ea2 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/examplemodel/FluentResourceUpdateExample.java @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.examplemodel; + +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentManager; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentResourceCollection; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.update.ResourceUpdate; + +/** + * Model of example for ResourceUpdate. + */ +public class FluentResourceUpdateExample extends FluentBaseExample { + + private final ResourceUpdate resourceUpdate; + private final FluentCollectionMethodExample resourceGetExample; + + public FluentResourceUpdateExample(String name, String originalFileName, + FluentManager manager, FluentResourceCollection collection, + ResourceUpdate resourceUpdate, + FluentCollectionMethodExample resourceGetExample) { + super(name, originalFileName, manager, collection); + this.resourceUpdate = resourceUpdate; + this.resourceGetExample = resourceGetExample; + } + + public ResourceUpdate getResourceUpdate() { + return resourceUpdate; + } + + public FluentCollectionMethodExample getResourceGetExample() { + return resourceGetExample; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/examplemodel/ParameterExample.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/examplemodel/ParameterExample.java new file mode 100644 index 0000000000..e2f7c46670 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/examplemodel/ParameterExample.java @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.examplemodel; + +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.method.FluentMethod; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.examplemodel.ExampleNode; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +public class ParameterExample { + private final FluentMethod fluentMethod; + private final List exampleNodes = new ArrayList<>(); + + public ParameterExample(FluentMethod fluentMethod, Collection exampleNodeIterator) { + this.fluentMethod = fluentMethod; + exampleNodes.addAll(exampleNodeIterator); + } + + public ParameterExample(FluentMethod fluentMethod, ExampleNode exampleNode) { + this.fluentMethod = fluentMethod; + if (exampleNode != null) { + this.exampleNodes.add(exampleNode); + } + } + + public ParameterExample(ExampleNode exampleNode) { + this.fluentMethod = null; + if (exampleNode != null) { + this.exampleNodes.add(exampleNode); + } + } + + public FluentMethod getFluentMethod() { + return fluentMethod; + } + + public List getExampleNodes() { + return exampleNodes; + } + + public ExampleNode getExampleNode() { + return exampleNodes.isEmpty() ? null : exampleNodes.iterator().next(); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/fluentmodel/FluentInterfaceStage.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/fluentmodel/FluentInterfaceStage.java new file mode 100644 index 0000000000..0c491e196c --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/fluentmodel/FluentInterfaceStage.java @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel; + +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ModelProperty; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.method.FluentMethod; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethodParameter; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +public class FluentInterfaceStage { + + protected final String name; + protected FluentInterfaceStage nextStage; + protected String extendStages; + protected ModelProperty property; + protected ClientMethodParameter parameter; + + protected final List methods = new ArrayList<>(); + + protected FluentInterfaceStage(String name) { + this.name = name; + } + + protected FluentInterfaceStage(String name, ModelProperty property) { + this.name = name; + this.property = property; + } + + protected FluentInterfaceStage(String name, ClientMethodParameter parameter) { + this.name = name; + this.parameter = parameter; + } + + public String getName() { + return name; + } + + public boolean isMandatoryStage() { + return (parameter == null) && (property == null || property.isRequired()); + } + + public FluentInterfaceStage getNextStage() { + return nextStage; + } + + public void setNextStage(FluentInterfaceStage nextStage) { + this.nextStage = nextStage; + } + + public String getExtendStages() { + return extendStages; + } + + public void setExtendStages(String extendStages) { + this.extendStages = extendStages; + } + + public List getMethods() { + return methods; + } + + public void addImportsTo(Set imports, boolean includeImplementationImports) { + this.getMethods().forEach(m -> m.addImportsTo(imports, includeImplementationImports)); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/fluentmodel/LocalVariable.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/fluentmodel/LocalVariable.java new file mode 100644 index 0000000000..ff2945f125 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/fluentmodel/LocalVariable.java @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel; + +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.RequestParameterLocation; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethodParameter; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.IType; + +/** + * Class variable or local variable. + */ +public class LocalVariable { + private final String name; + private final IType variableType; + private final ClientMethodParameter methodParameterReference; + private final RequestParameterLocation parameterLocation; + private boolean initializeRequired = false; + private String initializeExpression; + + public LocalVariable(String name, IType variableType, RequestParameterLocation parameterLocation, ClientMethodParameter methodParameterReference) { + this.name = name; + this.variableType = variableType; + this.parameterLocation = parameterLocation; + this.methodParameterReference = methodParameterReference; + } + + public String getName() { + return name; + } + + public IType getVariableType() { + return variableType; + } + + public RequestParameterLocation getParameterLocation() { + return parameterLocation; + } + + public ClientMethodParameter getMethodParameterReference() { + return methodParameterReference; + } + + public boolean isInitializeRequired() { + return initializeRequired; + } + + public String getInitializeExpression() { + return initializeExpression; + } + + public void setInitializeExpression(String initializeExpression) { + this.initializeRequired = true; + this.initializeExpression = initializeExpression; + } + + public LocalVariable getRenameLocalVariable(String newName) { + return new LocalVariable(newName, this.getVariableType(), this.getParameterLocation(), this.getMethodParameterReference()); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/fluentmodel/ResourceImplementation.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/fluentmodel/ResourceImplementation.java new file mode 100644 index 0000000000..95b06bb536 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/fluentmodel/ResourceImplementation.java @@ -0,0 +1,160 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel; + +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentResourceModel; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.ModelNaming; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.method.FluentMethod; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.method.FluentMethodType; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.immutablemodel.ImmutableMethod; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaVisibility; +import com.microsoft.typespec.http.client.generator.core.template.prototype.MethodTemplate; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class ResourceImplementation { + + private final List methods = new ArrayList<>(); + private final List localVariables = new ArrayList<>(); + + public ResourceImplementation(FluentResourceModel fluentModel) { + List fluentMethods = new ArrayList<>(); + List localVariables = new ArrayList<>(); + if (fluentModel.getResourceCreate() != null) { + fluentMethods.addAll(fluentModel.getResourceCreate().getFluentMethods()); + localVariables.addAll(fluentModel.getResourceCreate().getLocalVariables()); + } + if (fluentModel.getResourceUpdate() != null) { + fluentMethods.addAll(fluentModel.getResourceUpdate().getFluentMethods()); + localVariables.addAll(fluentModel.getResourceUpdate().getLocalVariables()); + } + if (fluentModel.getResourceRefresh() != null) { + fluentMethods.addAll(fluentModel.getResourceRefresh().getFluentMethods()); + //localVariables.addAll(fluentModel.getResourceRefresh().getLocalVariables()); + } + if (fluentModel.getResourceActions() != null) { + fluentMethods.addAll(fluentModel.getResourceActions().getFluentMethods()); + } + this.groupMethods(fluentMethods); + this.groupLocalVariables(localVariables); + } + + private void groupLocalVariables(Collection localVariables) { + Map localVariablesMap = new LinkedHashMap<>(); + localVariables.forEach(var -> localVariablesMap.putIfAbsent(var.getName(), var)); + this.localVariables.addAll(localVariablesMap.values()); + } + + private void groupMethods(Collection fluentMethods) { + Map groupedMethodsMap = new LinkedHashMap<>(); + for (FluentMethod method : fluentMethods) { + if (method.getType() == FluentMethodType.CREATE_WITH || method.getType() == FluentMethodType.UPDATE_WITH) { + GroupedMethod groupedMethod = groupedMethodsMap.computeIfAbsent(method.getImplementationMethodSignature(), key -> new GroupedMethod()); + if (method.getType() == FluentMethodType.CREATE_WITH) { + groupedMethod.methodCreateWith = method; + } else { + groupedMethod.methodUpdateWith = method; + } + } else { + this.methods.add(method); + } + } + + boolean branchMethodNeeded = false; + + for (GroupedMethod groupedMethod : groupedMethodsMap.values()) { + if (groupedMethod.size() == 1) { + this.methods.add(groupedMethod.single()); + } else { + MergedFluentMethod method = new MergedFluentMethod(groupedMethod); + this.methods.add(method); + + branchMethodNeeded = branchMethodNeeded || method.isBranchMethodNeeded(); + } + } + + if (branchMethodNeeded) { + this.methods.add(new FluentMethodCreateMode()); + } + } + + public List getMethods() { + return this.methods; + } + + public List getLocalVariables() { + return this.localVariables; + } + + private static class MergedFluentMethod implements ImmutableMethod { + + private final MethodTemplate implementationMethodTemplate; + private final boolean branchMethodNeeded; + + public MergedFluentMethod(GroupedMethod groupedMethod) { + if (groupedMethod.methodCreateWith.equals(groupedMethod.methodUpdateWith)) { + this.implementationMethodTemplate = groupedMethod.methodCreateWith.getMethodTemplate(); + branchMethodNeeded = false; + } else { + this.implementationMethodTemplate = MethodTemplate.builder() + .methodSignature(groupedMethod.methodCreateWith.getImplementationMethodSignature()) + .method(block -> { + block.ifBlock("isInCreateMode()", ifBlock -> { + groupedMethod.methodCreateWith.getMethodTemplate().writeMethodContent(ifBlock); + }).elseBlock(elseBlock -> { + groupedMethod.methodUpdateWith.getMethodTemplate().writeMethodContent(elseBlock); + }); + }) + .build(); + branchMethodNeeded = true; + } + } + + public boolean isBranchMethodNeeded() { + return branchMethodNeeded; + } + + @Override + public MethodTemplate getMethodTemplate() { + return implementationMethodTemplate; + } + } + + private static class FluentMethodCreateMode implements ImmutableMethod { + + private final MethodTemplate implementationMethodTemplate; + + public FluentMethodCreateMode() { + this.implementationMethodTemplate = MethodTemplate.builder() + .visibility(JavaVisibility.Private) + .methodSignature("boolean isInCreateMode()") + .method(block -> { + block.methodReturn(String.format("this.%1$s().id() == null", ModelNaming.METHOD_INNER_MODEL)); + }) + .build(); + } + + @Override + public MethodTemplate getMethodTemplate() { + return implementationMethodTemplate; + } + } + + private static class GroupedMethod { + private FluentMethod methodCreateWith; + private FluentMethod methodUpdateWith; + + private int size() { + return (methodCreateWith == null ? 0 : 1) + (methodUpdateWith == null ? 0 : 1); + } + + private FluentMethod single() { + return methodUpdateWith == null ? methodCreateWith : methodUpdateWith; + } + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/fluentmodel/ResourceLocalVariables.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/fluentmodel/ResourceLocalVariables.java new file mode 100644 index 0000000000..e3d41b1169 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/fluentmodel/ResourceLocalVariables.java @@ -0,0 +1,82 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel; + +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.RequestParameterLocation; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.examplemodel.MethodParameter; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethod; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethodParameter; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ProxyMethodParameter; +import com.microsoft.typespec.http.client.generator.core.util.CodeNamer; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * Collection of parameters that need to be provided during create/update flow, and hence need to be provided as class variable or local variable. + * + * E.g. resourceGroupName, resourceName, createParameters from ResourceCreate; resourceGroupName, resourceName, updateParameters from ResourceUpdate. + */ +public class ResourceLocalVariables { + + private final Map localVariablesMap = new LinkedHashMap<>(); + + public ResourceLocalVariables(ResourceOperation resourceOperation) { + String prefix = resourceOperation.getLocalVariablePrefix(); + + List pathParameters = resourceOperation.getPathParameters().stream().map(MethodParameter::getClientMethodParameter).collect(Collectors.toList()); + pathParameters.forEach(p -> localVariablesMap.put(p, new LocalVariable(p.getName(), p.getClientType(), RequestParameterLocation.PATH, p))); + + List miscParameters = resourceOperation.getMiscParameters(); + miscParameters.forEach(p -> { + LocalVariable var = new LocalVariable(prefix + CodeNamer.toPascalCase(p.getName()), p.getClientType(), RequestParameterLocation.QUERY, p); + var.setInitializeExpression("null"); + localVariablesMap.put(p, var); + }); + + ClientMethodParameter bodyParameter = resourceOperation.getBodyParameter(); + if (bodyParameter != null && !bodyParameter.getClientType().toString().equals(resourceOperation.getResourceModel().getInnerModel().getName())) { + LocalVariable var = new LocalVariable(prefix + CodeNamer.toPascalCase(bodyParameter.getName()), bodyParameter.getClientType(), RequestParameterLocation.BODY, bodyParameter); + var.setInitializeExpression(String.format("new %1$s()", bodyParameter.getClientType().toString())); + localVariablesMap.put(bodyParameter, var); + } + } + + public ResourceLocalVariables(ClientMethod clientMethod) { + Map proxyMethodParameterByClientParameterName = clientMethod.getProxyMethod().getParameters().stream() + .filter(p -> p.getRequestParameterLocation() == RequestParameterLocation.PATH) + .collect(Collectors.toMap(p -> CodeNamer.getEscapedReservedClientMethodParameterName(p.getName()), Function.identity())); + List pathParameters = clientMethod.getParameters().stream() + .filter(p -> proxyMethodParameterByClientParameterName.containsKey(p.getName())) + .collect(Collectors.toList()); + pathParameters.forEach(p -> localVariablesMap.put(p, new LocalVariable(p.getName(), p.getClientType(), RequestParameterLocation.PATH, p))); + } + + private ResourceLocalVariables() { + } + + public Map getLocalVariablesMap() { + return this.localVariablesMap; + } + + public LocalVariable getLocalVariableByMethodParameter(ClientMethodParameter methodParameter) { + return this.localVariablesMap.get(methodParameter); + } + + public ResourceLocalVariables getDeduplicatedLocalVariables(Set occupiedVariableNames) { + ResourceLocalVariables newLocalVariables = new ResourceLocalVariables(); + this.localVariablesMap.forEach((parameter, variable) -> { + if (occupiedVariableNames.contains(variable.getName())) { + newLocalVariables.localVariablesMap.put(parameter, variable.getRenameLocalVariable("var" + CodeNamer.toPascalCase(variable.getName()))); + } else { + newLocalVariables.localVariablesMap.put(parameter, variable); + } + }); + return newLocalVariables; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/fluentmodel/ResourceOperation.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/fluentmodel/ResourceOperation.java new file mode 100644 index 0000000000..a41d565e45 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/fluentmodel/ResourceOperation.java @@ -0,0 +1,274 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel; + +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.RequestParameterLocation; +import com.microsoft.typespec.http.client.generator.core.extension.plugin.PluginLogger; +import com.microsoft.typespec.http.client.generator.mgmt.FluentGen; +import com.microsoft.typespec.http.client.generator.mgmt.model.ResourceTypeName; +import com.microsoft.typespec.http.client.generator.mgmt.model.arm.UrlPathSegments; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentCollectionMethod; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentModelProperty; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentResourceCollection; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentResourceModel; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.examplemodel.MethodParameter; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ModelProperty; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.method.FluentMethod; +import com.microsoft.typespec.http.client.generator.mgmt.util.FluentUtils; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethod; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethodParameter; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientModel; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ProxyMethodParameter; +import com.microsoft.typespec.http.client.generator.core.util.CodeNamer; +import com.azure.core.util.CoreUtils; +import org.slf4j.Logger; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; + +public abstract class ResourceOperation { + + private static final Logger LOGGER = new PluginLogger(FluentGen.getPluginInstance(), ResourceOperation.class); + + protected final FluentResourceModel resourceModel; + protected final FluentResourceCollection resourceCollection; + + protected final UrlPathSegments urlPathSegments; + + protected final String methodName; + + protected final ClientModel requestBodyParameterModel; + + protected final List methodReferences = new ArrayList<>(); + + public ResourceOperation(FluentResourceModel resourceModel, FluentResourceCollection resourceCollection, + UrlPathSegments urlPathSegments, String methodName, ClientModel requestBodyParameterModel) { + this.resourceModel = resourceModel; + this.resourceCollection = resourceCollection; + this.urlPathSegments = urlPathSegments; + this.methodName = methodName; + this.requestBodyParameterModel = requestBodyParameterModel; + } + + public FluentResourceModel getResourceModel() { + return resourceModel; + } + + public FluentResourceCollection getResourceCollection() { + return resourceCollection; + } + + public UrlPathSegments getUrlPathSegments() { + return urlPathSegments; + } + + public String getMethodName() { + return methodName; + } + + public List getMethodReferences() { + return methodReferences; + } + + public ClientModel getRequestBodyParameterModel() { + return requestBodyParameterModel; + } + + abstract public List getFluentMethods(); + + abstract public String getLocalVariablePrefix(); + + // properties on model inner object, or request body model + protected List getProperties() { + List properties = new ArrayList<>(); + + List commonPropertyNames = Arrays.asList(ResourceTypeName.FIELD_LOCATION, ResourceTypeName.FIELD_TAGS); + + if (this.isBodyParameterSameAsFluentModel()) { + for (String commonPropertyName : commonPropertyNames) { + if (resourceModel.hasProperty(commonPropertyName)) { + FluentModelProperty property = resourceModel.getProperty(commonPropertyName); + properties.add(property.getModelProperty()); + } + } + for (FluentModelProperty property : resourceModel.getProperties()) { + if (!commonPropertyNames.contains(property.getName())) { + properties.add(property.getModelProperty()); + } + } + } else { + Map propertyMap = this.getRequestBodyModelPropertiesMap(); + for (String commonPropertyName : commonPropertyNames) { + if (propertyMap.containsKey(commonPropertyName)) { + ModelProperty property = propertyMap.get(commonPropertyName); + properties.add(property); + } + } + for (ModelProperty property : this.getRequestBodyModelProperties()) { + if (!commonPropertyNames.contains(property.getName())) { + properties.add(property); + } + } + } + + return properties.stream() + .filter(p -> !p.isReadOnly() && !p.isConstant()) + .collect(Collectors.toList()); + } + + // method parameters + private List getParametersByLocation(RequestParameterLocation parameterLocation) { + return getParametersByLocation(new HashSet<>(Collections.singletonList(parameterLocation))); + } + + private List getParametersByLocation(Set parameterLocations) { + ClientMethod clientMethod = getMethodReferencesOfFullParameters().iterator().next().getInnerClientMethod(); + Map proxyMethodParameterByClientParameterName = clientMethod.getProxyMethod().getParameters().stream() + .filter(p -> parameterLocations.contains(p.getRequestParameterLocation())) + .collect(Collectors.toMap(p -> CodeNamer.getEscapedReservedClientMethodParameterName(p.getName()), Function.identity())); + return clientMethod.getMethodParameters().stream() + .filter(p -> proxyMethodParameterByClientParameterName.containsKey(p.getName())) + .map(p -> new MethodParameter(proxyMethodParameterByClientParameterName.get(p.getName()), p)) + .collect(Collectors.toList()); + } + + public ClientMethodParameter getBodyParameter() { + List parameters = getParametersByLocation(RequestParameterLocation.BODY); + return parameters.isEmpty() ? null : parameters.iterator().next().getClientMethodParameter(); + } + + public List getPathParameters() { + return getParametersByLocation(RequestParameterLocation.PATH); + } + + public List getMiscParameters() { + // header or query + return getParametersByLocation(new HashSet<>(Arrays.asList(RequestParameterLocation.HEADER, RequestParameterLocation.QUERY))) + .stream().map(MethodParameter::getClientMethodParameter).collect(Collectors.toList()); + } + + public Collection getLocalVariables() { + return this.getResourceLocalVariables().getLocalVariablesMap().values(); + } + + protected List getMethodReferencesOfFullParameters() { + // method references of full parameters (include optional parameters) + return this.getMethodReferences().stream() + .filter(m -> !m.getInnerClientMethod().getOnlyRequiredParameters()) + .collect(Collectors.toList()); + } + + protected Optional findMethod(boolean hasContextParameter, List parameters) { + Optional methodOpt = this.getMethodReferencesOfFullParameters().stream() + .filter(m -> hasContextParameter + ? m.getInnerClientMethod().getParameters().stream().anyMatch(FluentUtils::isContextParameter) + : m.getInnerClientMethod().getParameters().stream().noneMatch(FluentUtils::isContextParameter)) + .findFirst(); + if (methodOpt.isPresent() && hasContextParameter) { + ClientMethodParameter contextParameter = methodOpt.get() + .getInnerClientMethod().getParameters().stream() + .filter(FluentUtils::isContextParameter) + .findFirst().get(); + parameters.add(contextParameter); + } + return methodOpt; + } + + // local variables + private ResourceLocalVariables resourceLocalVariables; + + public ResourceLocalVariables getResourceLocalVariables() { + if (resourceLocalVariables == null) { + resourceLocalVariables = new ResourceLocalVariables(this); + } + return resourceLocalVariables; + } + + protected LocalVariable getLocalVariableByMethodParameter(ClientMethodParameter methodParameter) { + return this.getResourceLocalVariables().getLocalVariablesMap().get(methodParameter); + } + + // request body model and properties, used when request body is not fluent model inner object + private Map requestBodyModelPropertiesMap; + private List requestBodyModelProperties; + + protected boolean isBodyParameterSameAsFluentModel() { + return requestBodyParameterModel == resourceModel.getInnerModel(); + } + + private void initRequestBodyClientModel() { + if (requestBodyModelPropertiesMap == null) { + requestBodyModelPropertiesMap = new LinkedHashMap<>(); + requestBodyModelProperties = new ArrayList<>(); + + List parentModels = new ArrayList<>(); + String parentModelName = requestBodyParameterModel.getParentModelName(); + while (!CoreUtils.isNullOrEmpty(parentModelName)) { + ClientModel parentModel = FluentUtils.getClientModel(parentModelName); + if (parentModel != null) { + parentModels.add(parentModel); + } + parentModelName = parentModel == null ? null :parentModel.getParentModelName(); + } + + List> propertiesFromTypeAndParents = new ArrayList<>(); + propertiesFromTypeAndParents.add(new ArrayList<>()); + requestBodyParameterModel.getAccessibleProperties().forEach(p -> { + ModelProperty property = ModelProperty.ofClientModelProperty(p); + if (requestBodyModelPropertiesMap.putIfAbsent(property.getName(), property) == null) { + propertiesFromTypeAndParents.get(propertiesFromTypeAndParents.size() - 1).add(property); + } + }); + + for (ClientModel parent : parentModels) { + propertiesFromTypeAndParents.add(new ArrayList<>()); + + parent.getAccessibleProperties().forEach(p -> { + ModelProperty property = ModelProperty.ofClientModelProperty(p); + if (requestBodyModelPropertiesMap.putIfAbsent(property.getName(), property) == null) { + propertiesFromTypeAndParents.get(propertiesFromTypeAndParents.size() - 1).add(property); + } + }); + } + + Collections.reverse(propertiesFromTypeAndParents); + for (List properties1 : propertiesFromTypeAndParents) { + requestBodyModelProperties.addAll(properties1); + } + } + } + + private List getRequestBodyModelProperties() { + initRequestBodyClientModel(); + return this.requestBodyModelProperties; + } + + private Map getRequestBodyModelPropertiesMap() { + initRequestBodyClientModel(); + return this.requestBodyModelPropertiesMap; + } + + protected boolean isIdProperty(ModelProperty property) { + return property.getName().equals(ResourceTypeName.FIELD_ID); + } + + protected boolean isLocationProperty(ModelProperty property) { + return FluentUtils.modelHasLocationProperty(resourceModel) && property.getName().equals(ResourceTypeName.FIELD_LOCATION); + } + + protected boolean hasConflictingMethod(String name) { + return resourceCollection.getMethods().stream() + .anyMatch(m -> name.equals(m.getInnerClientMethod().getName())); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/fluentmodel/action/ResourceActions.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/fluentmodel/action/ResourceActions.java new file mode 100644 index 0000000000..c47bef7c52 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/fluentmodel/action/ResourceActions.java @@ -0,0 +1,83 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.action; + +import com.microsoft.typespec.http.client.generator.core.extension.plugin.PluginLogger; +import com.microsoft.typespec.http.client.generator.mgmt.FluentGen; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentCollectionMethod; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentModelProperty; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentResourceCollection; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentResourceModel; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.method.FluentActionMethod; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.method.FluentMethod; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.method.FluentMethodType; +import org.slf4j.Logger; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Collection of resource actions. + */ +public class ResourceActions { + + private static final Logger LOGGER = new PluginLogger(FluentGen.getPluginInstance(), ResourceActions.class); + + private final FluentResourceModel resourceModel; + private final FluentResourceCollection resourceCollection; + private final List actionMethods; + + private List resourceActionMethods; + + public ResourceActions(FluentResourceModel resourceModel, FluentResourceCollection resourceCollection, + List actionMethods) { + + this.resourceModel = resourceModel; + this.resourceCollection = resourceCollection; + this.actionMethods = actionMethods; + + if (LOGGER.isInfoEnabled()) { + Set methodNames = actionMethods.stream().map(m -> m.getInnerProxyMethod().getName()).collect(Collectors.toSet()); + LOGGER.info("ResourceActions: Fluent model '{}', action methods: {}", resourceModel.getName(), methodNames); + } + } + + public List getFluentMethods() { + Set unavailableMethodNames = this.getUnavailableMethodNames(); + if (resourceActionMethods == null) { + resourceActionMethods = new ArrayList<>(); + for (FluentCollectionMethod method : actionMethods) { + if (!unavailableMethodNames.contains(method.getMethodName())) { + resourceActionMethods.add(new FluentActionMethod(resourceModel, FluentMethodType.OTHER, + resourceCollection, method, + resourceModel.getResourceCreate().getResourceLocalVariables())); + } + } + } + return resourceActionMethods; + } + + public void addImportsTo(Set imports, boolean includeImplementationImports) { + this.getFluentMethods().forEach(m -> m.addImportsTo(imports, includeImplementationImports)); + } + + private Set getUnavailableMethodNames() { + Set unavailableMethodNames = resourceModel.getProperties().stream() + .map(FluentModelProperty::getMethodName) + .collect(Collectors.toSet()); + if (resourceModel.getResourceCreate() != null) { + unavailableMethodNames.add("create"); + } + if (resourceModel.getResourceUpdate() != null) { + unavailableMethodNames.add("update"); + unavailableMethodNames.add("apply"); + } + if (resourceModel.getResourceUpdate() != null) { + unavailableMethodNames.add("refresh"); + } + return unavailableMethodNames; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/fluentmodel/create/DefinitionStage.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/fluentmodel/create/DefinitionStage.java new file mode 100644 index 0000000000..3fd6863992 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/fluentmodel/create/DefinitionStage.java @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.create; + +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ModelProperty; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.FluentInterfaceStage; + +public class DefinitionStage extends FluentInterfaceStage { + + public DefinitionStage(String name) { + super(name); + } + + public DefinitionStage(String name, ModelProperty property) { + super(name, property); + } + + public String getDescription(String modelName) { + return property == null + ? String.format("The stage of the %1$s definition.", modelName) + : String.format("The stage of the %1$s definition allowing to specify %2$s.", modelName, property.getName()); + } + + public ModelProperty getModelProperty() { + return this.property; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/fluentmodel/create/DefinitionStageBlank.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/fluentmodel/create/DefinitionStageBlank.java new file mode 100644 index 0000000000..1547ff3454 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/fluentmodel/create/DefinitionStageBlank.java @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.create; + +public class DefinitionStageBlank extends DefinitionStage { + + public DefinitionStageBlank() { + super("Blank"); + } + + @Override + public String getDescription(String modelName) { + return String.format("The first stage of the %1$s definition.", modelName); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/fluentmodel/create/DefinitionStageCreate.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/fluentmodel/create/DefinitionStageCreate.java new file mode 100644 index 0000000000..a8cb54b4d1 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/fluentmodel/create/DefinitionStageCreate.java @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.create; + +public class DefinitionStageCreate extends DefinitionStage { + + public DefinitionStageCreate() { + super("WithCreate"); + } + + @Override + public String getDescription(String modelName) { + return String.format("The stage of the %1$s definition which contains all the minimum required properties for the resource to be created, but also allows for any other optional properties to be specified.", modelName); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/fluentmodel/create/DefinitionStageMisc.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/fluentmodel/create/DefinitionStageMisc.java new file mode 100644 index 0000000000..cf66f7b2d1 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/fluentmodel/create/DefinitionStageMisc.java @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.create; + +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethodParameter; + +public class DefinitionStageMisc extends DefinitionStage { + + public DefinitionStageMisc(String name, ClientMethodParameter parameter) { + super(name); + this.parameter = parameter; + } + + public String getDescription(String modelName) { + return String.format("The stage of the %1$s definition allowing to specify %2$s.", modelName, parameter.getName()); + } + + public ClientMethodParameter getMethodParameter() { + return parameter; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/fluentmodel/create/DefinitionStageParent.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/fluentmodel/create/DefinitionStageParent.java new file mode 100644 index 0000000000..1bc46cf88c --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/fluentmodel/create/DefinitionStageParent.java @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.create; + +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.method.FluentMethod; + +import java.util.Arrays; +import java.util.List; + +public class DefinitionStageParent extends DefinitionStage { + + private FluentMethod existingParentMethod; + + public DefinitionStageParent(String name) { + super(name); + } + + @Override + public String getDescription(String modelName) { + return String.format("The stage of the %1$s definition allowing to specify parent resource.", modelName); + } + + @Override + public List getMethods() { + return Arrays.asList(existingParentMethod); + } + + public void setExistingParentMethod(FluentMethod existingParentMethod) { + this.existingParentMethod = existingParentMethod; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/fluentmodel/create/ResourceCreate.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/fluentmodel/create/ResourceCreate.java new file mode 100644 index 0000000000..9e5108a021 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/fluentmodel/create/ResourceCreate.java @@ -0,0 +1,432 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.create; + +import com.microsoft.typespec.http.client.generator.core.extension.plugin.PluginLogger; +import com.microsoft.typespec.http.client.generator.mgmt.FluentGen; +import com.microsoft.typespec.http.client.generator.mgmt.model.ResourceTypeName; +import com.microsoft.typespec.http.client.generator.mgmt.model.arm.ModelCategory; +import com.microsoft.typespec.http.client.generator.mgmt.model.arm.UrlPathSegments; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentCollectionMethod; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentResourceCollection; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentResourceModel; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentStatic; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.examplemodel.MethodParameter; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.ModelNaming; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ModelProperty; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.ResourceOperation; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.method.FluentConstructorByName; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.method.FluentCreateMethod; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.method.FluentDefineMethod; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.method.FluentMethod; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.method.FluentMethodParameterMethod; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.method.FluentMethodType; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.method.FluentModelPropertyMethod; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.method.FluentModelPropertyRegion; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.method.FluentParentMethod; +import com.microsoft.typespec.http.client.generator.mgmt.util.FluentUtils; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethodParameter; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientModel; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.IType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ProxyMethod; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ProxyMethodParameter; +import com.microsoft.typespec.http.client.generator.core.util.CodeNamer; +import org.slf4j.Logger; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class ResourceCreate extends ResourceOperation { + + private static final Logger LOGGER = new PluginLogger(FluentGen.getPluginInstance(), ResourceCreate.class); + + private List definitionStages; + + private FluentDefineMethod defineMethod; + +// public static final ResourceCreate NO_ASSOCIATION = new ResourceCreate(null, null, null, null, null); + + public ResourceCreate(FluentResourceModel resourceModel, FluentResourceCollection resourceCollection, + UrlPathSegments urlPathSegments, String methodName, ClientModel bodyParameterModel) { + super(resourceModel, resourceCollection, urlPathSegments, methodName, bodyParameterModel); + + if (resourceModel != null) { + LOGGER.info("ResourceCreate: Fluent model '{}', method reference '{}', body parameter '{}'", + resourceModel.getName(), methodName, bodyParameterModel.getName()); + } + } + + public List getDefinitionStages() { + if (definitionStages != null) { + return definitionStages; + } + + definitionStages = new ArrayList<>(); + + // blank + DefinitionStageBlank definitionStageBlank = new DefinitionStageBlank(); + + // parent + DefinitionStageParent definitionStageParent = null; + switch (this.getResourceModel().getCategory()) { + case RESOURCE_GROUP_AS_PARENT: + definitionStageParent = new DefinitionStageParent(deduplicateStageName("WithResourceGroup")); + break; + + case NESTED_CHILD: + case SCOPE_NESTED_CHILD: + definitionStageParent = new DefinitionStageParent(deduplicateStageName("WithParentResource")); + break; + + case SCOPE_AS_PARENT: + definitionStageParent = new DefinitionStageParent(deduplicateStageName("WithScope")); + break; + } + + // create + DefinitionStageCreate definitionStageCreate = new DefinitionStageCreate(); + + definitionStages.add(definitionStageBlank); + + // required properties + List requiredProperties = this.getRequiredProperties(); + + DefinitionStage lastStage = null; + if (!requiredProperties.isEmpty()) { + for (ModelProperty property : requiredProperties) { + DefinitionStage stage = new DefinitionStage("With" + CodeNamer.toPascalCase(property.getName()), property); + if (lastStage == null) { + // first property + if (isLocationProperty(property)) { + definitionStageBlank.setExtendStages(stage.getName()); + definitionStageBlank.setNextStage(stage); + + if (definitionStageParent != null) { + // insert parent stage as 2nd stage + definitionStages.add(stage); + + lastStage = stage; + stage = definitionStageParent; + } + } else if (definitionStageParent != null) { + // insert parent stage as 1st stage + definitionStageBlank.setExtendStages(definitionStageParent.getName()); + definitionStageBlank.setNextStage(definitionStageParent); + + definitionStages.add(definitionStageParent); + lastStage = definitionStageParent; + } else { + definitionStageBlank.setExtendStages(stage.getName()); + } + } + + if (lastStage != null) { + lastStage.setNextStage(stage); + } + + definitionStages.add(stage); + lastStage = stage; + } + } else { + if (definitionStageParent == null) { + definitionStageBlank.setExtendStages(definitionStageCreate.getName()); + lastStage = definitionStageBlank; + } else { + definitionStageBlank.setExtendStages(definitionStageParent.getName()); + lastStage = definitionStageParent; + definitionStages.add(definitionStageParent); + } + } + + lastStage.setNextStage(definitionStageCreate); + definitionStages.add(definitionStageCreate); + + for (DefinitionStage stage : definitionStages) { + if (stage.getModelProperty() != null) { + this.generatePropertyMethods(stage, requestBodyParameterModel, stage.getModelProperty()); + } + } + + // create method + definitionStageCreate.getMethods().add(this.getCreateMethod(false)); + definitionStageCreate.getMethods().add(this.getCreateMethod(true)); + + if (definitionStageParent != null) { + // existing parent method after all stages is connected. + definitionStageParent.setExistingParentMethod(this.getExistingParentMethod(definitionStageParent)); + } + + List optionalDefinitionStages = new ArrayList<>(); + // non-required properties + List nonRequiredProperties = this.getNonRequiredProperties(); + for (ModelProperty property : nonRequiredProperties) { + DefinitionStage stage = new DefinitionStage("With" + CodeNamer.toPascalCase(property.getName()), property); + stage.setNextStage(definitionStageCreate); + + this.generatePropertyMethods(stage, requestBodyParameterModel, property); + + optionalDefinitionStages.add(stage); + } + // header and query parameters + List miscParameters = this.getMiscParameters(); + for (ClientMethodParameter parameter : miscParameters) { + // it is possible that parameter got same name as one of the model properties + String parameterNameForMethodSignature = deduplicateParameterNameForMethodSignature( + definitionStages, optionalDefinitionStages, parameter.getName()); + + DefinitionStage stage = new DefinitionStageMisc("With" + CodeNamer.toPascalCase(parameterNameForMethodSignature), parameter); + stage.setNextStage(definitionStageCreate); + + stage.getMethods().add(this.getParameterSetterMethod(stage, parameter, parameterNameForMethodSignature)); + + optionalDefinitionStages.add(stage); + } + + if (!optionalDefinitionStages.isEmpty()) { + definitionStageCreate.setExtendStages(optionalDefinitionStages.stream() + .map(s -> String.format("%1$s.%2$s", ModelNaming.MODEL_FLUENT_INTERFACE_DEFINITION_STAGES, s.getName())) + .collect(Collectors.joining(", "))); + } + + definitionStages.addAll(optionalDefinitionStages); + + return definitionStages; + } + + private String deduplicateStageName(String stageName) { + Set propertyStageNames = this.getProperties().stream() + .map(p -> "With" + CodeNamer.toPascalCase(p.getName())) + .collect(Collectors.toSet()); + Set parameterStageNames = this.getMiscParameters().stream() + .map(p -> "With" + CodeNamer.toPascalCase(p.getName())) + .collect(Collectors.toSet()); + if (propertyStageNames.contains(stageName) || parameterStageNames.contains(stageName)) { + stageName += "Stage"; + } + return stageName; + } + + private List getRequiredProperties() { + return this.getProperties().stream() + .filter(p -> p.isRequired()) + .collect(Collectors.toList()); + } + + private List getNonRequiredProperties() { + return this.getProperties().stream() + .filter(p -> !p.isRequired()) + .collect(Collectors.toList()); + } + + @Override + protected List getProperties() { + return super.getProperties().stream() + .filter(p -> !p.isReadOnlyForCreate()) + .filter(p -> !isIdProperty(p)) // create should not be able to set id + .collect(Collectors.toList()); + } + + @Override + public List getFluentMethods() { + List methods = this.getDefinitionStages().stream() + .flatMap(s -> s.getMethods().stream()) + .collect(Collectors.toList()); + methods.add(this.getConstructor()); + return methods; + } + + @Override + public String getLocalVariablePrefix() { + return "create"; + } + + private FluentMethod getParameterSetterMethod(DefinitionStage stage, ClientMethodParameter parameter, + String parameterNameForMethodSignature) { + return new FluentMethodParameterMethod(this.getResourceModel(), FluentMethodType.CREATE_WITH, + stage, parameter, this.getLocalVariableByMethodParameter(parameter), + CodeNamer.getModelNamer().modelPropertySetterName(parameterNameForMethodSignature)); + } + + private String deduplicateParameterNameForMethodSignature( + List stages1, List stages2, String parameterName) { + String stageName = "With" + CodeNamer.toPascalCase(parameterName); + for (DefinitionStage stage : Stream.concat(stages1.stream(), stages2.stream()).collect(Collectors.toList())) { + if (stageName.equals(stage.getName())) { + return parameterName + "Parameter"; + } + } + return parameterName; + } + + public FluentDefineMethod getDefineMethod() { + if (defineMethod == null) { + String resourceName = this.getResourceName(); + LOGGER.info("ResourceCreate: Fluent model '{}', resource define method '{}'", resourceModel.getName(), "define" + resourceName); + + if (this.isConstantResourceNamePathParameter()) { + defineMethod = FluentDefineMethod.defineMethodWithConstantResourceName(this.getResourceModel(), FluentMethodType.DEFINE, resourceName); + } else { + ClientMethodParameter clientMethodParameter = this.getResourceNamePathParameter().getClientMethodParameter(); + defineMethod = new FluentDefineMethod(this.getResourceModel(), FluentMethodType.DEFINE, + resourceName, clientMethodParameter); + } + } + return defineMethod; + } + + private String getResourceName() { + String strCreateOrUpdate = "createOrUpdate"; + String strCreate = "create"; + String resourceName; + if (methodName.startsWith(strCreateOrUpdate)) { + resourceName = methodName.substring(strCreateOrUpdate.length()); + } else if (methodName.startsWith(strCreate)) { + resourceName = methodName.substring(strCreate.length()); + } else { + resourceName = resourceModel.getName(); + } + return CodeNamer.toPascalCase(resourceName); + } + + private FluentMethod getConstructor() { + if (this.isConstantResourceNamePathParameter()) { + return FluentConstructorByName.constructorMethodWithConstantResourceName(this.getResourceModel(), + FluentMethodType.CONSTRUCTOR, FluentStatic.getFluentManager().getType(), + this.getResourceLocalVariables()); + } else { + ClientMethodParameter resourceNamePathParameter = this.getResourceNamePathParameter().getClientMethodParameter(); + IType resourceNameType = resourceNamePathParameter.getClientType(); + String propertyName = resourceNamePathParameter.getName(); + return new FluentConstructorByName(this.getResourceModel(), FluentMethodType.CONSTRUCTOR, + resourceNameType, propertyName, FluentStatic.getFluentManager().getType(), + this.getResourceLocalVariables()); + } + } + + private boolean isConstantResourceNamePathParameter() { + // check whether the last segment in URL (resource name) is a constant parameter (which does not have a corresponding client method parameter) + + String parameterName = urlPathSegments.getReverseParameterSegments().iterator().next().getParameterName(); + FluentCollectionMethod method = this.getMethodReferencesOfFullParameters().iterator().next(); + ProxyMethod proxyMethod = method.getInnerProxyMethod(); + Optional resourceNamePathParameter = proxyMethod.getParameters().stream() + .filter(m -> parameterName.equals(m.getRequestParameterName())) + .findFirst(); + if (resourceNamePathParameter.isPresent()) { + return resourceNamePathParameter.get().isConstant() || resourceNamePathParameter.get().isFromClient(); + } else { + throw new IllegalStateException(String.format("Resource name parameter not found in proxy method %1$s, name segment %2$s", + proxyMethod.getName(), parameterName)); + } + } + + private MethodParameter getResourceNamePathParameter() { + // this only works when isConstantResourceNamePathParameter() == false + + String parameterName = urlPathSegments.getReverseParameterSegments().iterator().next().getParameterName(); + Optional pathParameter = this.getPathParameters().stream() + .filter(m -> parameterName.equals(m.getSerializedName())) + .findFirst(); + if (pathParameter.isPresent()) { + return pathParameter.get(); + } else { + FluentCollectionMethod method = this.getMethodReferencesOfFullParameters().iterator().next(); + throw new IllegalStateException(String.format("Resource name parameter not found in client method %1$s, name segment %2$s", + method.getInnerClientMethod().getName(), parameterName)); + } + } + + private void generatePropertyMethods(DefinitionStage stage, ClientModel model, ModelProperty property) { + if (FluentUtils.modelHasLocationProperty(getProperties()) && property.getName().equals(ResourceTypeName.FIELD_LOCATION)) { + String baseName = "region"; + if (getProperties().stream().anyMatch(p -> "region".equals(p.getName()))) { + baseName = ResourceTypeName.FIELD_LOCATION; + } + + // location -> region + stage.getMethods().add(new FluentModelPropertyRegion.FluentModelPropertyRegionMethod( + this.getResourceModel(), FluentMethodType.CREATE_WITH, + stage, model, property, + this.getLocalVariableByMethodParameter(this.getBodyParameter()), baseName)); + stage.getMethods().add(new FluentModelPropertyRegion.FluentModelPropertyRegionNameMethod( + this.getResourceModel(), FluentMethodType.CREATE_WITH, + stage, model, property, + this.getLocalVariableByMethodParameter(this.getBodyParameter()), baseName)); + } else { + stage.getMethods().add(getPropertyMethod(stage, model, property)); + } + } + + private FluentMethod getPropertyMethod(DefinitionStage stage, ClientModel model, ModelProperty property) { + return new FluentModelPropertyMethod(this.getResourceModel(), FluentMethodType.CREATE_WITH, + stage, model, property, + this.getLocalVariableByMethodParameter(this.getBodyParameter())); + } + + private FluentMethod getExistingParentMethod(DefinitionStageParent stage) { + // parameters for parent method + List parameters = this.getPathParameters(); + if (!this.isConstantResourceNamePathParameter()) { + MethodParameter resourceNamePathParameter = this.getResourceNamePathParameter(); + String serializedResourceNamePathParameterName = resourceNamePathParameter.getSerializedName(); + + parameters = parameters.stream() + .filter(p -> !p.getSerializedName().equals(serializedResourceNamePathParameterName)) + .collect(Collectors.toList()); + } + + // resource name of immediate parent + Set serializedParameterNames = parameters.stream() + .map(MethodParameter::getSerializedName) + .collect(Collectors.toSet()); + String resourceNameOfImmediateParent = null; + for (UrlPathSegments.ParameterSegment parameterSegment : urlPathSegments.getReverseParameterSegments()) { + if (serializedParameterNames.contains(parameterSegment.getParameterName())) { + if (parameterSegment.getSegmentName().isEmpty()) { + // segment name is empty for SCOPE_AS_PARENT and SCOPE_NESTED_CHILD + resourceNameOfImmediateParent = CodeNamer.toPascalCase(FluentUtils.getSingular(parameterSegment.getParameterName())); + } else { + resourceNameOfImmediateParent = CodeNamer.toPascalCase(FluentUtils.getSingular(parameterSegment.getSegmentName())); + } + break; + } + } + if (resourceNameOfImmediateParent == null) { + throw new IllegalStateException(String.format("Resource name of immediate parent not found for url %1$s, model %2$s, candidate parameter names %3$s", + urlPathSegments.getPath(), resourceModel.getName(), serializedParameterNames)); + } + // if parent is resourceGroup, just set it as such + if (resourceModel.getCategory() == ModelCategory.RESOURCE_GROUP_AS_PARENT) { + resourceNameOfImmediateParent = "ResourceGroup"; + } + + return new FluentParentMethod(resourceModel, FluentMethodType.CREATE_PARENT, + stage, resourceNameOfImmediateParent, + parameters.stream().map(MethodParameter::getClientMethodParameter).collect(Collectors.toList()), + this.getResourceLocalVariables()); + } + + private FluentMethod getCreateMethod(boolean hasContextParameter) { + List parameters = new ArrayList<>(); + Optional methodOpt = this.findMethod(true, parameters); + if (methodOpt.isPresent()) { + if (!hasContextParameter) { + parameters.clear(); + } + return new FluentCreateMethod(resourceModel, FluentMethodType.CREATE, + parameters, this.getResourceLocalVariables(), + resourceCollection, methodOpt.get()); + } else { + throw new IllegalStateException("Create method not found on model " + resourceModel.getName()); + } + } + + public void addImportsTo(Set imports, boolean includeImplementationImports) { + getDefinitionStages().forEach(s -> s.addImportsTo(imports, includeImplementationImports)); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/fluentmodel/delete/ResourceDelete.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/fluentmodel/delete/ResourceDelete.java new file mode 100644 index 0000000000..45e7898cb1 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/fluentmodel/delete/ResourceDelete.java @@ -0,0 +1,86 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.delete; + +import com.microsoft.typespec.http.client.generator.core.extension.plugin.PluginLogger; +import com.microsoft.typespec.http.client.generator.mgmt.FluentGen; +import com.microsoft.typespec.http.client.generator.mgmt.model.arm.UrlPathSegments; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentCollectionMethod; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentResourceCollection; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentResourceModel; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.examplemodel.MethodParameter; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.ResourceOperation; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.method.CollectionMethodOperationByIdTemplate; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.method.FluentMethod; +import com.microsoft.typespec.http.client.generator.mgmt.util.Utils; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethodParameter; +import com.microsoft.typespec.http.client.generator.core.template.prototype.MethodTemplate; +import org.slf4j.Logger; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +public class ResourceDelete extends ResourceOperation { + + private static final Logger LOGGER = new PluginLogger(FluentGen.getPluginInstance(), ResourceDelete.class); + + public ResourceDelete(FluentResourceModel resourceModel, FluentResourceCollection resourceCollection, + UrlPathSegments urlPathSegments, String methodName) { + super(resourceModel, resourceCollection, urlPathSegments, methodName, null); + + LOGGER.info("ResourceDelete: Fluent model '{}', method reference '{}'", + resourceModel.getName(), methodName); + } + + @Override + public List getFluentMethods() { + return Collections.emptyList(); + } + + @Override + public String getLocalVariablePrefix() { + return "local"; + } + + public List getDeleteByIdCollectionMethods() { + List methods = new ArrayList<>(); + List parameters = new ArrayList<>(); + Optional methodOpt = this.findMethod(true, parameters); + if (methodOpt.isPresent()) { + FluentCollectionMethod collectionMethod = methodOpt.get(); + + String name = getDeleteByIdMethodName(collectionMethod.getMethodName()); + if (!hasConflictingMethod(name)) { + List pathParameters = this.getPathParameters(); + + methods.add(new CollectionMethodOperationByIdTemplate( + resourceModel, name, + pathParameters, urlPathSegments, false, getResourceLocalVariables(), + collectionMethod) + .getMethodTemplate()); + + methods.add(new CollectionMethodOperationByIdTemplate( + resourceModel, name, + pathParameters, urlPathSegments, true, getResourceLocalVariables(), + collectionMethod) + .getMethodTemplate()); + } + } + return methods; + } + + private static String getDeleteByIdMethodName(String methodName) { + String getByIdMethodName = methodName; + int indexOfBy = methodName.indexOf("By"); + if (indexOfBy > 0) { + getByIdMethodName = methodName.substring(0, indexOfBy); + } else if (methodName.endsWith(Utils.METHOD_POSTFIX_WITH_RESPONSE)) { + getByIdMethodName = methodName.substring(0, methodName.length() - Utils.METHOD_POSTFIX_WITH_RESPONSE.length()); + } + getByIdMethodName += "ById"; + return getByIdMethodName; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/fluentmodel/get/ResourceRefresh.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/fluentmodel/get/ResourceRefresh.java new file mode 100644 index 0000000000..8cf71b4f1e --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/fluentmodel/get/ResourceRefresh.java @@ -0,0 +1,122 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.get; + +import com.microsoft.typespec.http.client.generator.core.extension.plugin.PluginLogger; +import com.microsoft.typespec.http.client.generator.mgmt.FluentGen; +import com.microsoft.typespec.http.client.generator.mgmt.model.arm.UrlPathSegments; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentCollectionMethod; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentResourceCollection; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentResourceModel; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.examplemodel.MethodParameter; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.ResourceOperation; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.method.CollectionMethodOperationByIdTemplate; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.method.FluentMethod; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.method.FluentMethodType; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.method.FluentRefreshMethod; +import com.microsoft.typespec.http.client.generator.mgmt.util.Utils; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethodParameter; +import com.microsoft.typespec.http.client.generator.core.template.prototype.MethodTemplate; +import org.slf4j.Logger; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +public class ResourceRefresh extends ResourceOperation { + + private static final Logger LOGGER = new PluginLogger(FluentGen.getPluginInstance(), ResourceRefresh.class); + + private List refreshMethods; + + public ResourceRefresh(FluentResourceModel resourceModel, FluentResourceCollection resourceCollection, + UrlPathSegments urlPathSegments, String methodName) { + super(resourceModel, resourceCollection, urlPathSegments, methodName, null); + + LOGGER.info("ResourceRefresh: Fluent model '{}', method reference '{}'", + resourceModel.getName(), methodName); + } + + @Override + public List getFluentMethods() { + return this.getRefreshMethods(); + } + + @Override + public String getLocalVariablePrefix() { + return "local"; + } + + public List getRefreshMethods() { + if (refreshMethods == null) { + refreshMethods = new ArrayList<>(); + + refreshMethods.add(this.getRefreshMethod(false)); + refreshMethods.add(this.getRefreshMethod(true)); + } + return refreshMethods; + } + + private FluentMethod getRefreshMethod(boolean hasContextParameter) { + List parameters = new ArrayList<>(); + Optional methodOpt = this.findMethod(true, parameters); + if (methodOpt.isPresent()) { + if (!hasContextParameter) { + parameters.clear(); + } + return new FluentRefreshMethod(resourceModel, FluentMethodType.REFRESH, + parameters, this.getResourceLocalVariables(), + resourceCollection, methodOpt.get(), + resourceModel.getResourceCreate().getResourceLocalVariables()); + } else { + throw new IllegalStateException("Refresh method not found on model " + resourceModel.getName()); + } + } + + public void addImportsTo(Set imports, boolean includeImplementationImports) { + if (includeImplementationImports) { + getRefreshMethods().forEach(m -> m.addImportsTo(imports, true)); + } + } + + public List getGetByIdCollectionMethods() { + List methods = new ArrayList<>(); + List parameters = new ArrayList<>(); + Optional methodOpt = this.findMethod(true, parameters); + if (methodOpt.isPresent()) { + FluentCollectionMethod collectionMethod = methodOpt.get(); + + String name = getGetByIdMethodName(collectionMethod.getMethodName()); + if (!hasConflictingMethod(name)) { + List pathParameters = this.getPathParameters(); + + methods.add(new CollectionMethodOperationByIdTemplate( + resourceModel, name, + pathParameters, urlPathSegments, false, getResourceLocalVariables(), + collectionMethod) + .getMethodTemplate()); + + methods.add(new CollectionMethodOperationByIdTemplate( + resourceModel, name, + pathParameters, urlPathSegments, true, getResourceLocalVariables(), + collectionMethod) + .getMethodTemplate()); + } + } + return methods; + } + + private static String getGetByIdMethodName(String methodName) { + String getByIdMethodName = methodName; + int indexOfBy = methodName.indexOf("By"); + if (indexOfBy > 0) { + getByIdMethodName = methodName.substring(0, indexOfBy); + } else if (methodName.endsWith(Utils.METHOD_POSTFIX_WITH_RESPONSE)) { + getByIdMethodName = methodName.substring(0, methodName.length() - Utils.METHOD_POSTFIX_WITH_RESPONSE.length()); + } + getByIdMethodName += "ById"; + return getByIdMethodName; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/fluentmodel/method/CollectionMethodOperationByIdTemplate.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/fluentmodel/method/CollectionMethodOperationByIdTemplate.java new file mode 100644 index 0000000000..a26b3d8cd8 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/fluentmodel/method/CollectionMethodOperationByIdTemplate.java @@ -0,0 +1,192 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.method; + +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.RequestParameterLocation; +import com.microsoft.typespec.http.client.generator.mgmt.model.arm.UrlPathSegments; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentCollectionMethod; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentResourceModel; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.examplemodel.MethodParameter; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.ModelNaming; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.LocalVariable; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.ResourceLocalVariables; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.immutablemodel.ImmutableMethod; +import com.microsoft.typespec.http.client.generator.mgmt.util.FluentUtils; +import com.microsoft.typespec.http.client.generator.mgmt.util.Utils; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClassType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethod; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethodParameter; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.IType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.PrimitiveType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ReturnValue; +import com.microsoft.typespec.http.client.generator.core.template.ClientMethodTemplate; +import com.microsoft.typespec.http.client.generator.core.template.prototype.MethodTemplate; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +public class CollectionMethodOperationByIdTemplate implements ImmutableMethod { + + private final MethodTemplate methodTemplate; + private final String name; + + public CollectionMethodOperationByIdTemplate(FluentResourceModel model, String name, + List pathParameters, UrlPathSegments urlPathSegments, boolean includeContextParameter, + ResourceLocalVariables resourceLocalVariables, + FluentCollectionMethod collectionMethod) { + if (includeContextParameter) { + name += Utils.METHOD_POSTFIX_WITH_RESPONSE; + } + this.name = name; + + final ResourceLocalVariables localVariables = resourceLocalVariables.getDeduplicatedLocalVariables(new HashSet<>(Collections.singleton(ModelNaming.METHOD_PARAMETER_NAME_ID))); + final boolean removeResponseInReturnType = !includeContextParameter; + final IType returnType = getReturnType(collectionMethod.getFluentReturnType(), removeResponseInReturnType); + final boolean responseInReturnTypeRemoved = returnType != collectionMethod.getFluentReturnType() && returnType != PrimitiveType.VOID; + + final List parameters = new ArrayList<>(); + // id parameter + parameters.add(new ClientMethodParameter.Builder() + .name(ModelNaming.METHOD_PARAMETER_NAME_ID) + .description("the resource ID.") + .wireType(ClassType.STRING) + .annotations(new ArrayList<>()) + .constant(false) + .defaultValue(null) + .fromClient(false) + .finalParameter(false) + .required(true) + .build()); + if (includeContextParameter) { + // optional parameters + Set pathParameterNames = pathParameters.stream() + .map(p -> p.getClientMethodParameter().getName()) + .collect(Collectors.toSet()); + parameters.addAll(collectionMethod.getInnerClientMethod().getMethodParameters().stream() + .filter(p -> !pathParameterNames.contains(p.getName())) + .collect(Collectors.toList())); + } + + // method invocation + Set parametersSet = new HashSet<>(parameters); + List methodParameters = collectionMethod.getInnerClientMethod().getMethodParameters(); + String argumentsLine = methodParameters.stream() + .map(p -> FluentUtils.getLocalMethodArgument(p, parametersSet, localVariables, model, collectionMethod)) + .collect(Collectors.joining(", ")); + String methodInvocation = String.format("%1$s(%2$s)", collectionMethod.getMethodName(), argumentsLine); + + List segments = urlPathSegments.getReverseParameterSegments(); + Collections.reverse(segments); + Map urlSegmentNameByParameterName = urlPathSegments.getReverseParameterSegments().stream() + .collect(Collectors.toMap(UrlPathSegments.ParameterSegment::getParameterName, UrlPathSegments.ParameterSegment::getSegmentName)); + + String afterInvocationCode = responseInReturnTypeRemoved ? ".getValue()" : ""; + + // a dummy client method only for generating javadoc + ClientMethod dummyClientMethodForJavadoc = new ClientMethod.Builder() + .proxyMethod(collectionMethod.getInnerProxyMethod()) + .name(name) + .returnValue(new ReturnValue(returnType == PrimitiveType.VOID + ? "" : collectionMethod.getInnerClientMethod().getReturnValue().getDescription(), returnType)) + .parameters(parameters) + .description(collectionMethod.getInnerClientMethod().getDescription()) + .build(); + + methodTemplate = MethodTemplate.builder() + .comment(commentBlock -> ClientMethodTemplate.generateJavadoc(dummyClientMethodForJavadoc, commentBlock, dummyClientMethodForJavadoc.getProxyMethod(), true)) + .methodSignature(this.getMethodSignature(returnType, parameters)) + .method(block -> { + // init path parameters from resource id + pathParameters.forEach(p -> { + String urlSegmentName = urlSegmentNameByParameterName.get(p.getSerializedName()); + String valueFromIdText; + if (urlPathSegments.hasScope()) { + valueFromIdText = String.format("%1$s.getValueFromIdByParameterName(%2$s, \"%3$s\", \"%4$s\")", + ModelNaming.CLASS_RESOURCE_MANAGER_UTILS, ModelNaming.METHOD_PARAMETER_NAME_ID, urlPathSegments.getPath(), p.getSerializedName()); + } else { + valueFromIdText = String.format("%1$s.getValueFromIdByName(%2$s, \"%3$s\")", + ModelNaming.CLASS_RESOURCE_MANAGER_UTILS, ModelNaming.METHOD_PARAMETER_NAME_ID, urlSegmentName); + } + LocalVariable var = localVariables.getLocalVariableByMethodParameter(p.getClientMethodParameter()); + // need additional conversion from String to LocalVariable.variableType + boolean needsLocalVar = var.getVariableType() != ClassType.STRING; + String varName = needsLocalVar ? var.getName() + "Local" : var.getName(); + block.line(String.format("%1$s %2$s = %3$s;", ClassType.STRING.getName(), varName, valueFromIdText)); + + String segmentNameForErrorPrompt = urlSegmentName.isEmpty() ? p.getSerializedName() : urlSegmentName; + block.ifBlock(String.format("%1$s == null", varName), ifBlock -> { + String errorMessageExpr = String.format("String.format(\"The resource ID '%%s' is not valid. Missing path segment '%1$s'.\", %2$s)", + segmentNameForErrorPrompt, ModelNaming.METHOD_PARAMETER_NAME_ID); + ifBlock.line(String.format( + "throw LOGGER.logExceptionAsError(new IllegalArgumentException(%1$s));", + errorMessageExpr)); + }); + if (needsLocalVar) { + // currently this works only for UUID or Enum + block.line(String.format("%1$s %2$s = %3$s.fromString(%4$s);", + var.getVariableType().toString(), + var.getName(), + var.getVariableType().toString(), + varName)); + } + }); + + if (!includeContextParameter) { + // init local variables to default value + for (LocalVariable var : localVariables.getLocalVariablesMap().values()) { + if (var.getParameterLocation() == RequestParameterLocation.QUERY) { + block.line(String.format("%1$s %2$s = %3$s;", var.getVariableType().toString(), var.getName(), var.getInitializeExpression())); + } + } + } + + if (returnType == PrimitiveType.VOID) { + block.line(String.format("this.%1$s%2$s;", + methodInvocation, + afterInvocationCode)); + } else { + block.methodReturn(String.format("this.%1$s%2$s", + methodInvocation, + afterInvocationCode)); + } + }) + .build(); + } + + @Override + public MethodTemplate getMethodTemplate() { + return methodTemplate; + } + + private static IType getReturnType(IType collectionMethodReturnType, boolean removeResponse) { + IType returnType; + if (removeResponse) { + if (FluentUtils.isResponseType(collectionMethodReturnType)) { + returnType = FluentUtils.getValueTypeFromResponseType(collectionMethodReturnType); + if (returnType == ClassType.VOID) { + returnType = PrimitiveType.VOID; + } + } else { + // LRO would not have Response for method takes Context, usually happens to delete method + returnType = collectionMethodReturnType; + } + } else { + returnType = collectionMethodReturnType; + } + return returnType; + } + + private String getMethodSignature(IType returnType, List parameters) { + String parameterText = parameters.stream() + .map(p -> String.format("%1$s %2$s", p.getClientType().toString(), p.getName())) + .collect(Collectors.joining(", ")); + return String.format("%1$s %2$s(%3$s)", + returnType.toString(), this.name, parameterText); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/fluentmodel/method/FluentActionMethod.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/fluentmodel/method/FluentActionMethod.java new file mode 100644 index 0000000000..500140345d --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/fluentmodel/method/FluentActionMethod.java @@ -0,0 +1,120 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.method; + +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.RequestParameterLocation; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentCollectionMethod; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentManagerProperty; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentResourceCollection; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentResourceModel; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentStatic; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.ModelNaming; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.ResourceLocalVariables; +import com.microsoft.typespec.http.client.generator.mgmt.util.FluentUtils; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethod; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethodParameter; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.IType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.PrimitiveType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ReturnValue; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaJavadocComment; +import com.microsoft.typespec.http.client.generator.core.template.ClientMethodTemplate; +import com.microsoft.typespec.http.client.generator.core.template.prototype.MethodTemplate; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Resource action method. + * + * E.g. start, stop, regenerateKey, listAccountSas etc. + */ +public class FluentActionMethod extends FluentMethod { + + private final FluentCollectionMethod collectionMethod; + private final ClientMethod dummyClientMethodForJavadoc; + + public FluentActionMethod(FluentResourceModel model, FluentMethodType type, + FluentResourceCollection collection, FluentCollectionMethod collectionMethod, + ResourceLocalVariables resourceLocalVariablesDefinedInClass) { + super(model, type); + + this.collectionMethod = collectionMethod; + this.name = collectionMethod.getMethodName(); + this.description = collectionMethod.getDescription(); + + IType returnType = collectionMethod.getFluentReturnType(); + boolean returnTypeIsVoid = returnType == PrimitiveType.VOID; + ReturnValue returnValue = new ReturnValue(returnTypeIsVoid ? "" : collectionMethod.getInnerClientMethod().getReturnValue().getDescription(), returnType); + this.interfaceReturnValue = returnValue; + this.implementationReturnValue = interfaceReturnValue; + + // remove path parameters from input parameter, as they are provided by the variables of resource model + ClientMethod method = collectionMethod.getInnerClientMethod(); + List parameters = new ArrayList<>(method.getMethodInputParameters()); + ResourceLocalVariables resourceLocalVariables = new ResourceLocalVariables(collectionMethod.getInnerClientMethod()); + parameters.removeAll(resourceLocalVariables.getLocalVariablesMap().entrySet().stream() + .filter(e -> e.getValue().getParameterLocation() == RequestParameterLocation.PATH) + .map(Map.Entry::getKey) + .collect(Collectors.toList())); + this.parameters = parameters; + + // a dummy client method only for generating javadoc + this.dummyClientMethodForJavadoc = new ClientMethod.Builder() + .proxyMethod(collectionMethod.getInnerProxyMethod()) + .name(name) + .returnValue(returnValue) + .parameters(parameters) + .description(collectionMethod.getInnerClientMethod().getDescription()) + .build(); + + // resource collection from manager + String collectionGetMethod = FluentStatic.getFluentManager().getProperties().stream() + .filter(p -> p.getFluentType().getName().equals(collection.getInterfaceType().getName())) + .map(FluentManagerProperty::getMethodName) + .findFirst().get(); + + // method invocation + Set parametersSet = new HashSet<>(parameters); + List methodParameters = method.getMethodInputParameters(); + String argumentsLine = methodParameters.stream() + .map(p -> FluentUtils.getLocalMethodArgument(p, parametersSet, resourceLocalVariables, model, collectionMethod, resourceLocalVariablesDefinedInClass)) + .collect(Collectors.joining(", ")); + String methodInvocation = String.format("%1$s(%2$s)", collectionMethod.getMethodName(), argumentsLine); + + this.implementationMethodTemplate = MethodTemplate.builder() + .methodSignature(this.getImplementationMethodSignature()) + .method(block -> { + String invocation = String.format("%1$s.%2$s().%3$s", + ModelNaming.MODEL_PROPERTY_MANAGER, + collectionGetMethod, + methodInvocation); + if (returnTypeIsVoid) { + block.line(invocation + ";"); + } else { + block.methodReturn(invocation); + } + }) + .build(); + } + + @Override + protected String getBaseMethodSignature() { + String parameterDeclaration = parameters.stream().map(ClientMethodParameter::getDeclaration).collect(Collectors.joining(", ")); + return String.format("%1$s(%2$s)", name, parameterDeclaration); + } + + @Override + public void writeJavadoc(JavaJavadocComment commentBlock) { + ClientMethodTemplate.generateJavadoc(dummyClientMethodForJavadoc, commentBlock, dummyClientMethodForJavadoc.getProxyMethod(), true); + } + + @Override + public void addImportsTo(Set imports, boolean includeImplementationImports) { + collectionMethod.addImportsTo(imports, includeImplementationImports); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/fluentmodel/method/FluentApplyMethod.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/fluentmodel/method/FluentApplyMethod.java new file mode 100644 index 0000000000..2cd09d5b04 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/fluentmodel/method/FluentApplyMethod.java @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.method; + +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentCollectionMethod; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentResourceCollection; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentResourceModel; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.ResourceLocalVariables; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethodParameter; + +import java.util.List; + +public class FluentApplyMethod extends FluentBaseMethod { + + public FluentApplyMethod(FluentResourceModel model, FluentMethodType type, + List parameters, ResourceLocalVariables resourceLocalVariables, + FluentResourceCollection collection, FluentCollectionMethod collectionMethod, + ResourceLocalVariables resourceLocalVariablesDefinedInClass) { + + super(model, type, "apply", "Executes the update request.", "the updated resource.", + parameters, resourceLocalVariables, collection, collectionMethod, resourceLocalVariablesDefinedInClass, false); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/fluentmodel/method/FluentBaseMethod.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/fluentmodel/method/FluentBaseMethod.java new file mode 100644 index 0000000000..2cad1f3962 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/fluentmodel/method/FluentBaseMethod.java @@ -0,0 +1,120 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.method; + +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.RequestParameterLocation; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentCollectionMethod; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentManagerProperty; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentResourceCollection; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentResourceModel; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentStatic; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.ModelNaming; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.LocalVariable; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.ResourceLocalVariables; +import com.microsoft.typespec.http.client.generator.mgmt.util.FluentUtils; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethodParameter; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.IType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ReturnValue; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaJavadocComment; +import com.microsoft.typespec.http.client.generator.core.template.prototype.MethodTemplate; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +abstract public class FluentBaseMethod extends FluentMethod { + + private final List parameters; + private final FluentCollectionMethod collectionMethod; + + public FluentBaseMethod(FluentResourceModel model, FluentMethodType type, String name, String description, String returnValueDescription, + List parameters, ResourceLocalVariables resourceLocalVariables, + FluentResourceCollection collection, FluentCollectionMethod collectionMethod) { + this(model, type, name, description, returnValueDescription, parameters, resourceLocalVariables, collection, collectionMethod, null, false); + } + + public FluentBaseMethod(FluentResourceModel model, FluentMethodType type, String name, String description, String returnValueDescription, + List parameters, ResourceLocalVariables resourceLocalVariables, + FluentResourceCollection collection, FluentCollectionMethod collectionMethod, + ResourceLocalVariables resourceLocalVariablesDefinedInClass, + // below used for refresh method + boolean initLocalVariables) { + super(model, type); + + this.name = name; + this.description = description; + this.interfaceReturnValue = new ReturnValue(returnValueDescription, model.getInterfaceType()); + this.implementationReturnValue = interfaceReturnValue; + + this.parameters = parameters; + this.collectionMethod = collectionMethod; + + IType returnType = collectionMethod.getInnerClientMethod().getReturnValue().getType(); + final boolean returnIsResponseType = FluentUtils.isResponseType(returnType); + + // resource collection from manager + String innerClientGetMethod = FluentStatic.getFluentManager().getProperties().stream() + .filter(p -> p.getFluentType().getName().equals(collection.getInterfaceType().getName())) + .map(FluentManagerProperty::getInnerClientGetMethod) + .findFirst().get(); + + // method invocation + Set parametersSet = new HashSet<>(parameters); + List methodParameters = collectionMethod.getInnerClientMethod().getMethodInputParameters(); + String argumentsLine = methodParameters.stream() + .map(p -> FluentUtils.getLocalMethodArgument(p, parametersSet, resourceLocalVariables, model, collectionMethod, resourceLocalVariablesDefinedInClass)) + .collect(Collectors.joining(", ")); + String methodInvocation = String.format("%1$s(%2$s)", collectionMethod.getMethodName(), argumentsLine); + + String afterInvocationCode = returnIsResponseType ? ".getValue()" : ""; + + this.implementationMethodTemplate = MethodTemplate.builder() + .methodSignature(this.getImplementationMethodSignature()) + .method(block -> { + if (initLocalVariables) { + for (LocalVariable var : resourceLocalVariables.getLocalVariablesMap().values()) { + if (var.getParameterLocation() == RequestParameterLocation.QUERY) { + block.line(String.format("%1$s %2$s = %3$s;", var.getVariableType().toString(), var.getName(), var.getInitializeExpression())); + } + } + } + + block.line("this.%1$s = %2$s.%3$s().%4$s().%5$s%6$s;", + ModelNaming.MODEL_PROPERTY_INNER, + ModelNaming.MODEL_PROPERTY_MANAGER, + ModelNaming.METHOD_SERVICE_CLIENT, + innerClientGetMethod, + methodInvocation, + afterInvocationCode); + block.methodReturn("this"); + }) + .build(); + } + + @Override + protected String getBaseMethodSignature() { + String parameterText = parameters.stream() + .map(p -> String.format("%1$s %2$s", p.getClientType().toString(), p.getName())) + .collect(Collectors.joining(", ")); + return String.format("%1$s(%2$s)", + this.name, parameterText); + } + + @Override + public void writeJavadoc(JavaJavadocComment commentBlock) { + commentBlock.description(description); + parameters.forEach(p -> commentBlock.param(p.getName(), p.getDescription())); + commentBlock.methodReturns(interfaceReturnValue.getDescription()); + } + + @Override + public void addImportsTo(Set imports, boolean includeImplementationImports) { + interfaceReturnValue.addImportsTo(imports, false); + parameters.forEach(p -> p.addImportsTo(imports, false)); + if (includeImplementationImports) { + collectionMethod.addImportsTo(imports, false); + } + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/fluentmodel/method/FluentConstructorByInner.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/fluentmodel/method/FluentConstructorByInner.java new file mode 100644 index 0000000000..998adfd3d2 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/fluentmodel/method/FluentConstructorByInner.java @@ -0,0 +1,96 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.method; + +import com.microsoft.typespec.http.client.generator.mgmt.model.arm.UrlPathSegments; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentResourceModel; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.examplemodel.MethodParameter; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.ModelNaming; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.ResourceLocalVariables; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClassType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ReturnValue; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaJavadocComment; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaVisibility; +import com.microsoft.typespec.http.client.generator.core.template.prototype.MethodTemplate; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +public class FluentConstructorByInner extends FluentMethod { + + private final List pathParameters; + private final ClassType managerType; + + public FluentConstructorByInner(FluentResourceModel model, FluentMethodType type, + List pathParameters, ResourceLocalVariables resourceLocalVariables, + ClassType managerType, UrlPathSegments urlPathSegments) { + super(model, type); + + this.pathParameters = pathParameters; + this.managerType = managerType; + + this.implementationReturnValue = new ReturnValue("", model.getImplementationType()); + + this.implementationMethodTemplate = MethodTemplate.builder() + .visibility(JavaVisibility.PackagePrivate) + .methodSignature(this.getImplementationMethodSignature()) + .method(block -> { + block.line(String.format("this.%1$s = %2$s;", ModelNaming.MODEL_PROPERTY_INNER, ModelNaming.MODEL_PROPERTY_INNER)); + block.line(String.format("this.%1$s = %2$s;", ModelNaming.MODEL_PROPERTY_MANAGER, ModelNaming.MODEL_PROPERTY_MANAGER)); + + List segments = urlPathSegments.getReverseParameterSegments(); + Collections.reverse(segments); + Map urlSegmentNameByParameterName = urlPathSegments.getReverseParameterSegments().stream() + .collect(Collectors.toMap(UrlPathSegments.ParameterSegment::getParameterName, UrlPathSegments.ParameterSegment::getSegmentName)); + + // init from resource id + pathParameters.forEach(p -> { + String valueFromIdText; + if (urlPathSegments.hasScope()) { + valueFromIdText = String.format("%1$s.getValueFromIdByParameterName(%2$s.id(), \"%3$s\", \"%4$s\")", + ModelNaming.CLASS_RESOURCE_MANAGER_UTILS, ModelNaming.MODEL_PROPERTY_INNER, urlPathSegments.getPath(), p.getSerializedName()); + } else { + valueFromIdText = String.format("%1$s.getValueFromIdByName(%2$s.id(), \"%3$s\")", + ModelNaming.CLASS_RESOURCE_MANAGER_UTILS, ModelNaming.MODEL_PROPERTY_INNER, urlSegmentNameByParameterName.get(p.getSerializedName())); + } + if (p.getClientMethodParameter().getClientType() != ClassType.STRING) { + valueFromIdText = String.format("%1$s.fromString(%2$s)", p.getClientMethodParameter().getClientType().toString(), valueFromIdText); + } + block.line(String.format("this.%1$s = %2$s;", resourceLocalVariables.getLocalVariableByMethodParameter(p.getClientMethodParameter()).getName(), valueFromIdText)); + }); + }) + .build(); + } + + @Override + public String getImplementationMethodSignature() { + return String.format("%1$s(%2$s %3$s, %4$s %5$s)", + implementationReturnValue.getType().toString(), + fluentResourceModel.getInnerModel().getName(), ModelNaming.MODEL_PROPERTY_INNER, + managerType.getFullName(), ModelNaming.MODEL_PROPERTY_MANAGER); + } + + @Override + protected String getBaseMethodSignature() { + throw new UnsupportedOperationException(); + } + + @Override + public void writeJavadoc(JavaJavadocComment commentBlock) { + // NOOP + } + + @Override + public void addImportsTo(Set imports, boolean includeImplementationImports) { + if (includeImplementationImports) { + pathParameters.forEach(p -> p.getClientMethodParameter().addImportsTo(imports, false)); + /* use full name for FooManager, to avoid naming conflict + managerType.addImportsTo(imports, false); + */ + } + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/fluentmodel/method/FluentConstructorByName.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/fluentmodel/method/FluentConstructorByName.java new file mode 100644 index 0000000000..9f29643a42 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/fluentmodel/method/FluentConstructorByName.java @@ -0,0 +1,96 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.method; + +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentResourceModel; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.ModelNaming; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.LocalVariable; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.ResourceLocalVariables; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClassType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.IType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ReturnValue; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaJavadocComment; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaVisibility; +import com.microsoft.typespec.http.client.generator.core.template.prototype.MethodTemplate; + +import java.util.Set; + +public class FluentConstructorByName extends FluentMethod { + + private final boolean constantResourceName; // resource name is constant, "name" is not needed + private final IType resourceNameType; + private final ClassType managerType; + + public static FluentConstructorByName constructorMethodWithConstantResourceName( + FluentResourceModel model, FluentMethodType type, ClassType managerType, ResourceLocalVariables resourceLocalVariables) { + return new FluentConstructorByName(model, type, null, null, managerType, resourceLocalVariables); + } + + public FluentConstructorByName(FluentResourceModel model, FluentMethodType type, + IType resourceNameType, String propertyNameForResourceName, ClassType managerType, + ResourceLocalVariables resourceLocalVariables) { + super(model, type); + + this.constantResourceName = resourceNameType == null; + this.resourceNameType = resourceNameType; + this.managerType = managerType; + + this.implementationReturnValue = new ReturnValue("", model.getImplementationType()); + + this.implementationMethodTemplate = MethodTemplate.builder() + .visibility(JavaVisibility.PackagePrivate) + .methodSignature(this.getImplementationMethodSignature()) + .method(block -> { + block.line(String.format("this.%1$s = new %2$s();", ModelNaming.MODEL_PROPERTY_INNER, model.getInnerModel().getName())); + block.line(String.format("this.%1$s = %2$s;", ModelNaming.MODEL_PROPERTY_MANAGER, ModelNaming.MODEL_PROPERTY_MANAGER)); + if (!constantResourceName) { + block.line(String.format("this.%1$s = name;", propertyNameForResourceName)); + } + + // init + resourceLocalVariables.getLocalVariablesMap().values().stream() + .filter(LocalVariable::isInitializeRequired) + .forEach(var -> { + block.line(String.format("this.%1$s = %2$s;", var.getName(), var.getInitializeExpression())); + }); + }) + .build(); + } + + @Override + public String getImplementationMethodSignature() { + if (constantResourceName) { + return String.format("%1$s(%2$s %3$s)", + implementationReturnValue.getType().toString(), + managerType.getFullName(), ModelNaming.MODEL_PROPERTY_MANAGER); + } else { + return String.format("%1$s(%2$s name, %3$s %4$s)", + implementationReturnValue.getType().toString(), + resourceNameType.toString(), + managerType.getFullName(), ModelNaming.MODEL_PROPERTY_MANAGER); + } + } + + @Override + protected String getBaseMethodSignature() { + throw new UnsupportedOperationException(); + } + + @Override + public void writeJavadoc(JavaJavadocComment commentBlock) { + // NOOP + } + + @Override + public void addImportsTo(Set imports, boolean includeImplementationImports) { + if (includeImplementationImports) { + if (resourceNameType != null) { + resourceNameType.addImportsTo(imports, false); + } + /* use full name for FooManager, to avoid naming conflict + managerType.addImportsTo(imports, false); + */ + } + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/fluentmodel/method/FluentCreateMethod.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/fluentmodel/method/FluentCreateMethod.java new file mode 100644 index 0000000000..0281f44d75 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/fluentmodel/method/FluentCreateMethod.java @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.method; + +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentCollectionMethod; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentResourceCollection; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentResourceModel; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.ResourceLocalVariables; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethodParameter; + +import java.util.List; + +public class FluentCreateMethod extends FluentBaseMethod { + + public FluentCreateMethod(FluentResourceModel model, FluentMethodType type, + List parameters, ResourceLocalVariables resourceLocalVariables, + FluentResourceCollection collection, FluentCollectionMethod collectionMethod) { + + super(model, type, "create", "Executes the create request.", "the created resource.", + parameters, resourceLocalVariables, collection, collectionMethod); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/fluentmodel/method/FluentDefineMethod.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/fluentmodel/method/FluentDefineMethod.java new file mode 100644 index 0000000000..756586cde5 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/fluentmodel/method/FluentDefineMethod.java @@ -0,0 +1,109 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.method; + +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentResourceModel; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.ModelNaming; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClassType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethodParameter; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.IType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ReturnValue; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaJavadocComment; +import com.microsoft.typespec.http.client.generator.core.template.prototype.MethodTemplate; + +import java.util.Set; + +public class FluentDefineMethod extends FluentMethod { + + private final boolean constantResourceName; // resource name is constant, "name" is not needed + private final ClientMethodParameter methodParameter; + private final IType resourceNameType; + + public static FluentDefineMethod defineMethodWithConstantResourceName( + FluentResourceModel model, FluentMethodType type, String resourceName) { + return new FluentDefineMethod(model, type, resourceName, null); + } + + public FluentDefineMethod(FluentResourceModel model, FluentMethodType type, + String resourceName, ClientMethodParameter methodParameter) { + super(model, type); + + this.constantResourceName = methodParameter == null; + this.methodParameter = methodParameter; + this.resourceNameType = constantResourceName ? null : methodParameter.getClientType(); + + this.name = "define" + resourceName; + String interfaceTypeName = model.getInterfaceType().getName(); + this.description = String.format("Begins definition for a new %1$s resource.", interfaceTypeName); + + this.interfaceReturnValue = new ReturnValue(String.format("the first stage of the new %1$s definition.", interfaceTypeName), + new ClassType.Builder() + .name(String.format("%1$s.%2$s.Blank", interfaceTypeName, ModelNaming.MODEL_FLUENT_INTERFACE_DEFINITION_STAGES)) + .build()); + this.implementationReturnValue = new ReturnValue("", model.getImplementationType()); + + if (methodParameter != null) { + this.parameters.add(methodParameter); + } + } + + public void setName(String name) { + this.name = name; + } + + @Override + public MethodTemplate getMethodTemplate() { + if (this.implementationMethodTemplate == null) { + this.implementationMethodTemplate = MethodTemplate.builder() + .methodSignature(this.getImplementationMethodSignature()) + .method(block -> { + if (constantResourceName) { + block.methodReturn(String.format("new %1$s(this.%2$s())", + fluentResourceModel.getImplementationType().toString(), + ModelNaming.METHOD_MANAGER)); + } else { + block.methodReturn(String.format("new %1$s(name, this.%2$s())", + fluentResourceModel.getImplementationType().toString(), + ModelNaming.METHOD_MANAGER)); + } + }) + .build(); + } + return this.implementationMethodTemplate; + } + + @Override + protected String getBaseMethodSignature() { + if (constantResourceName) { + return String.format("%1$s()", this.name); + } else { + return String.format("%1$s(%2$s name)", this.name, resourceNameType.toString()); + } + } + + @Override + public void writeJavadoc(JavaJavadocComment commentBlock) { + commentBlock.description(description); + if (!constantResourceName) { + commentBlock.param("name", "resource name."); + } + commentBlock.methodReturns(interfaceReturnValue.getDescription()); + } + + @Override + public void addImportsTo(Set imports, boolean includeImplementationImports) { + if (includeImplementationImports) { + fluentResourceModel.getInterfaceType().addImportsTo(imports, false); + } else { + fluentResourceModel.getImplementationType().addImportsTo(imports, false); + } + if (resourceNameType != null) { + resourceNameType.addImportsTo(imports, false); + } + } + + public ClientMethodParameter getMethodParameter() { + return methodParameter; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/fluentmodel/method/FluentMethod.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/fluentmodel/method/FluentMethod.java new file mode 100644 index 0000000000..32edeeb699 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/fluentmodel/method/FluentMethod.java @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.method; + +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentResourceModel; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.immutablemodel.ImmutableMethod; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethodParameter; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ReturnValue; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaJavadocComment; +import com.microsoft.typespec.http.client.generator.core.template.prototype.MethodTemplate; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +public abstract class FluentMethod implements ImmutableMethod { + + protected String name; + protected String description; + protected ReturnValue interfaceReturnValue; + protected ReturnValue implementationReturnValue; + protected List parameters = new ArrayList<>(); + + protected FluentResourceModel fluentResourceModel; + + protected FluentMethodType type; + + protected MethodTemplate implementationMethodTemplate; + + public FluentMethod(FluentResourceModel fluentResourceModel, FluentMethodType type) { + this.fluentResourceModel = fluentResourceModel; + this.type = type; + } + + public String getInterfaceMethodSignature() { + return String.format("%1$s %2$s", this.interfaceReturnValue.getType().toString(), this.getBaseMethodSignature()); + } + + public String getImplementationMethodSignature() { + return String.format("%1$s %2$s", this.implementationReturnValue.getType().toString(), this.getBaseMethodSignature()); + } + + protected abstract String getBaseMethodSignature(); + + public abstract void writeJavadoc(JavaJavadocComment commentBlock); + + public abstract void addImportsTo(Set imports, boolean includeImplementationImports); + + public MethodTemplate getMethodTemplate() { + return implementationMethodTemplate; + } + + public String getName() { + return name; + } + + public FluentMethodType getType() { + return type; + } + + public FluentResourceModel getFluentResourceModel() { + return fluentResourceModel; + } + + public List getParameters() { + return parameters; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/fluentmodel/method/FluentMethodParameterMethod.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/fluentmodel/method/FluentMethodParameterMethod.java new file mode 100644 index 0000000000..e1cebe58b1 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/fluentmodel/method/FluentMethodParameterMethod.java @@ -0,0 +1,103 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.method; + +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentResourceModel; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.FluentInterfaceStage; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.LocalVariable; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClassType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethodParameter; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ReturnValue; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaJavadocComment; +import com.microsoft.typespec.http.client.generator.core.template.prototype.MethodTemplate; +import com.microsoft.typespec.http.client.generator.core.util.CodeNamer; + +import java.util.Set; + +public class FluentMethodParameterMethod extends FluentMethod { + + private final ClientMethodParameter methodParameter; + private final LocalVariable localVariable; + + public FluentMethodParameterMethod(FluentResourceModel model, FluentMethodType type, + FluentInterfaceStage stage, + ClientMethodParameter methodParameter, LocalVariable localVariable) { + super(model, type); + + this.methodParameter = methodParameter; + this.localVariable = localVariable; + + this.name = CodeNamer.getModelNamer().modelPropertySetterName(methodParameter.getName()); + this.description = String.format("Specifies the %1$s property: %2$s.", methodParameter.getName(), methodParameter.getDescription()); + this.interfaceReturnValue = new ReturnValue("the next definition stage.", new ClassType.Builder().name(stage.getNextStage().getName()).build()); + this.implementationReturnValue = new ReturnValue("", model.getImplementationType()); + + this.implementationMethodTemplate = MethodTemplate.builder() + .methodSignature(this.getImplementationMethodSignature()) + .method(block -> { + block.line("this.%1$s = %2$s;", localVariable.getName(), methodParameter.getName()); + block.methodReturn("this"); + }) + .build(); + + this.parameters.add(methodParameter); + } + + public FluentMethodParameterMethod(FluentResourceModel model, FluentMethodType type, + FluentInterfaceStage stage, + ClientMethodParameter methodParameter, LocalVariable localVariable, + String methodName) { + super(model, type); + + this.methodParameter = methodParameter; + this.localVariable = localVariable; + + this.name = methodName; + this.description = String.format("Specifies the %1$s property: %2$s.", methodParameter.getName(), methodParameter.getDescription()); + this.interfaceReturnValue = new ReturnValue("the next definition stage.", new ClassType.Builder().name(stage.getNextStage().getName()).build()); + this.implementationReturnValue = new ReturnValue("", model.getImplementationType()); + + this.implementationMethodTemplate = MethodTemplate.builder() + .methodSignature(this.getImplementationMethodSignature()) + .method(block -> { + block.line("this.%1$s = %2$s;", localVariable.getName(), methodParameter.getName()); + block.methodReturn("this"); + }) + .build(); + } + + @Override + protected String getBaseMethodSignature() { + return String.format("%1$s(%2$s %3$s)", + this.name, + methodParameter.getClientType().toString(), + methodParameter.getName()); + } + + @Override + public void writeJavadoc(JavaJavadocComment commentBlock) { + commentBlock.description(description); + commentBlock.param(methodParameter.getName(), methodParameter.getDescription()); + commentBlock.methodReturns(interfaceReturnValue.getDescription()); + } + + @Override + public void addImportsTo(Set imports, boolean includeImplementationImports) { + methodParameter.addImportsTo(imports, false); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof FluentMethodParameterMethod) { + FluentMethodParameterMethod other = (FluentMethodParameterMethod) obj; + return this.methodParameter == other.methodParameter && this.localVariable == other.localVariable; + } else { + return false; + } + } + + public ClientMethodParameter getMethodParameter() { + return methodParameter; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/fluentmodel/method/FluentMethodType.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/fluentmodel/method/FluentMethodType.java new file mode 100644 index 0000000000..27e589836e --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/fluentmodel/method/FluentMethodType.java @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.method; + +public enum FluentMethodType { + CREATE_WITH, + CREATE_PARENT, + UPDATE_WITH, + UPDATE_WITHOUT, + + CONSTRUCTOR, + CREATE, + DEFINE, + + UPDATE, + APPLY, + + REFRESH, + + OTHER +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/fluentmodel/method/FluentModelPropertyMethod.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/fluentmodel/method/FluentModelPropertyMethod.java new file mode 100644 index 0000000000..4689a34e99 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/fluentmodel/method/FluentModelPropertyMethod.java @@ -0,0 +1,106 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.method; + +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentResourceModel; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.ModelNaming; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ModelProperty; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.FluentInterfaceStage; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.LocalVariable; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClassType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientModel; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ReturnValue; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaJavadocComment; +import com.microsoft.typespec.http.client.generator.core.template.prototype.MethodTemplate; + +import java.util.Objects; +import java.util.Set; + +public class FluentModelPropertyMethod extends FluentMethod { + + private final ClientModel clientModel; + protected final ModelProperty modelProperty; + private final LocalVariable localVariable; + + public FluentModelPropertyMethod(FluentResourceModel model, FluentMethodType type, + FluentInterfaceStage stage, ClientModel clientModel, + ModelProperty modelProperty, + LocalVariable localVariable) { + this(model, type, stage, clientModel, modelProperty, localVariable, + modelProperty.getSetterName(), + String.format("Specifies the %1$s property: %2$s.", modelProperty.getName(), modelProperty.getDescription())); + } + + public FluentModelPropertyMethod(FluentResourceModel model, FluentMethodType type, + FluentInterfaceStage stage, ClientModel clientModel, + ModelProperty modelProperty, + LocalVariable localVariable, + String name, String description) { + super(model, type); + + this.clientModel = clientModel; + this.modelProperty = modelProperty; + this.localVariable = localVariable; + + this.name = name; + this.description = description; + this.interfaceReturnValue = new ReturnValue("the next definition stage.", new ClassType.Builder().name(stage.getNextStage().getName()).build()); + this.implementationReturnValue = new ReturnValue("", model.getImplementationType()); + + this.implementationMethodTemplate = MethodTemplate.builder() + .methodSignature(this.getImplementationMethodSignature()) + .method(block -> { + if (fluentResourceModel.getInnerModel() == clientModel) { + block.line("this.%1$s().%2$s(%3$s);", ModelNaming.METHOD_INNER_MODEL, modelProperty.getSetterName(), modelProperty.getName()); + } else { + block.line("this.%1$s.%2$s(%3$s);", localVariable.getName(), modelProperty.getSetterName(), modelProperty.getName()); + } + block.methodReturn("this"); + }) + .build(); + } + + public ClientModel getClientModel() { + return clientModel; + } + + public ModelProperty getModelProperty() { + return modelProperty; + } + + @Override + protected String getBaseMethodSignature() { + return String.format("%1$s(%2$s %3$s)", + this.name, + modelProperty.getClientType().toString(), + modelProperty.getName()); + } + + @Override + public void writeJavadoc(JavaJavadocComment commentBlock) { + commentBlock.description(description); + commentBlock.param(modelProperty.getName(), modelProperty.getDescription()); + commentBlock.methodReturns(interfaceReturnValue.getDescription()); + } + + @Override + public void addImportsTo(Set imports, boolean includeImplementationImports) { + modelProperty.addImportsTo(imports); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof FluentModelPropertyMethod) { + FluentModelPropertyMethod other = (FluentModelPropertyMethod) obj; + return this.clientModel == other.clientModel && Objects.equals(this.modelProperty, other.modelProperty) && this.localVariable == other.localVariable; + } else { + return false; + } + } + + @Override + public int hashCode() { + return Objects.hash(clientModel, modelProperty, localVariable); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/fluentmodel/method/FluentModelPropertyRegion.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/fluentmodel/method/FluentModelPropertyRegion.java new file mode 100644 index 0000000000..bc7d509f4e --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/fluentmodel/method/FluentModelPropertyRegion.java @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.method; + +import com.microsoft.typespec.http.client.generator.mgmt.model.FluentType; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentResourceModel; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.ModelNaming; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ModelProperty; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.FluentInterfaceStage; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.LocalVariable; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientModel; +import com.microsoft.typespec.http.client.generator.core.template.prototype.MethodTemplate; +import com.microsoft.typespec.http.client.generator.core.util.CodeNamer; + +import java.util.Set; + +public class FluentModelPropertyRegion { + + private FluentModelPropertyRegion() { + } + + public static class FluentModelPropertyRegionMethod extends FluentModelPropertyMethod { + + public FluentModelPropertyRegionMethod(FluentResourceModel model, FluentMethodType type, + FluentInterfaceStage stage, ClientModel clientModel, + ModelProperty modelProperty, + LocalVariable localVariable, String baseName) { + super(model, type, stage, clientModel, modelProperty, localVariable, + CodeNamer.getModelNamer().modelPropertySetterName(baseName), + "Specifies the region for the resource."); + + this.implementationMethodTemplate = MethodTemplate.builder() + .methodSignature(this.getImplementationMethodSignature()) + .method(block -> { + if (fluentResourceModel.getInnerModel() == clientModel) { + block.line("this.%1$s().%2$s(%3$s.toString());", ModelNaming.METHOD_INNER_MODEL, modelProperty.getSetterName(), modelProperty.getName()); + } else { + block.line("this.%1$s.%2$s(%3$s.toString());", localVariable.getName(), modelProperty.getSetterName(), modelProperty.getName()); + } + block.methodReturn("this"); + }) + .build(); + } + + @Override + protected String getBaseMethodSignature() { + return String.format("%1$s(%2$s %3$s)", + this.name, + FluentType.REGION, + modelProperty.getName()); + } + + @Override + public void addImportsTo(Set imports, boolean includeImplementationImports) { + super.addImportsTo(imports, includeImplementationImports); + FluentType.REGION.addImportsTo(imports, false); + } + } + + public static class FluentModelPropertyRegionNameMethod extends FluentModelPropertyMethod { + + public FluentModelPropertyRegionNameMethod(FluentResourceModel model, FluentMethodType type, + FluentInterfaceStage stage, ClientModel clientModel, + ModelProperty modelProperty, + LocalVariable localVariable, String baseName) { + super(model, type, stage, clientModel, modelProperty, localVariable, + CodeNamer.getModelNamer().modelPropertySetterName(baseName), + "Specifies the region for the resource."); + } + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/fluentmodel/method/FluentParentMethod.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/fluentmodel/method/FluentParentMethod.java new file mode 100644 index 0000000000..b14e901b09 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/fluentmodel/method/FluentParentMethod.java @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.method; + +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentResourceModel; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.FluentInterfaceStage; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.ResourceLocalVariables; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClassType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethodParameter; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ReturnValue; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaJavadocComment; +import com.microsoft.typespec.http.client.generator.core.template.prototype.MethodTemplate; + +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +public class FluentParentMethod extends FluentMethod { + + public FluentParentMethod(FluentResourceModel model, FluentMethodType type, + FluentInterfaceStage stage, String parentResourceName, + List parameters, ResourceLocalVariables resourceLocalVariables) { + super(model, type); + + this.parameters = parameters; + + this.name = "withExisting" + parentResourceName; + this.description = String.format("Specifies %1$s.", parameters.stream().map(ClientMethodParameter::getName).collect(Collectors.joining(", "))); + this.interfaceReturnValue = new ReturnValue("the next definition stage.", new ClassType.Builder().name(stage.getNextStage().getName()).build()); + this.implementationReturnValue = new ReturnValue("", model.getImplementationType()); + + this.parameters = parameters; + + this.implementationMethodTemplate = MethodTemplate.builder() + .methodSignature(this.getImplementationMethodSignature()) + .method(block -> { + parameters.forEach(p -> block.line("this.%1$s = %2$s;", resourceLocalVariables.getLocalVariableByMethodParameter(p).getName(), p.getName())); + block.methodReturn("this"); + }) + .build(); + } + + @Override + public String getBaseMethodSignature() { + String parameterText = parameters.stream() + .map(p -> String.format("%1$s %2$s", p.getClientType().toString(), p.getName())) + .collect(Collectors.joining(", ")); + return String.format("%1$s(%2$s)", + this.name, parameterText); + } + + @Override + public void writeJavadoc(JavaJavadocComment commentBlock) { + commentBlock.description(description); + parameters.forEach(p -> commentBlock.param(p.getName(), p.getDescription())); + commentBlock.methodReturns(interfaceReturnValue.getDescription()); + } + + @Override + public void addImportsTo(Set imports, boolean includeImplementationImports) { + parameters.forEach(p -> p.addImportsTo(imports, false)); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/fluentmodel/method/FluentRefreshMethod.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/fluentmodel/method/FluentRefreshMethod.java new file mode 100644 index 0000000000..3ea255fa03 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/fluentmodel/method/FluentRefreshMethod.java @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.method; + +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentCollectionMethod; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentResourceCollection; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentResourceModel; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.ResourceLocalVariables; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethodParameter; + +import java.util.List; + +public class FluentRefreshMethod extends FluentBaseMethod { + + public FluentRefreshMethod(FluentResourceModel model, FluentMethodType type, + List parameters, ResourceLocalVariables resourceLocalVariables, + FluentResourceCollection collection, FluentCollectionMethod collectionMethod, + ResourceLocalVariables resourceLocalVariablesDefinedInClass) { + + super(model, type, "refresh", "Refreshes the resource to sync with Azure.", "the refreshed resource.", + parameters, resourceLocalVariables, collection, collectionMethod, resourceLocalVariablesDefinedInClass, true); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/fluentmodel/method/FluentUpdateMethod.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/fluentmodel/method/FluentUpdateMethod.java new file mode 100644 index 0000000000..c028ab4b6e --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/fluentmodel/method/FluentUpdateMethod.java @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.method; + +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentResourceModel; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.ModelNaming; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.LocalVariable; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.ResourceLocalVariables; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClassType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ReturnValue; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaJavadocComment; +import com.microsoft.typespec.http.client.generator.core.template.prototype.MethodTemplate; + +import java.util.Set; + +public class FluentUpdateMethod extends FluentMethod { + public FluentUpdateMethod(FluentResourceModel model, FluentMethodType type, + ResourceLocalVariables resourceLocalVariables) { + super(model, type); + + this.name = "update"; + String interfaceTypeName = model.getInterfaceType().getName(); + this.description = String.format("Begins update for the %1$s resource.", interfaceTypeName);; + + this.interfaceReturnValue = new ReturnValue("the stage of resource update", + new ClassType.Builder() + .name(String.format("%1$s.%2$s", interfaceTypeName, ModelNaming.MODEL_FLUENT_INTERFACE_UPDATE)) + .build()); + this.implementationReturnValue = new ReturnValue("", fluentResourceModel.getImplementationType()); + + this.implementationMethodTemplate = MethodTemplate.builder() + .methodSignature(this.getImplementationMethodSignature()) + .method(block -> { + // init + resourceLocalVariables.getLocalVariablesMap().values().stream() + .filter(LocalVariable::isInitializeRequired) + .forEach(var -> { + block.line(String.format("this.%1$s = %2$s;", var.getName(), var.getInitializeExpression())); + }); + + block.methodReturn("this"); + }) + .build(); + } + + @Override + protected String getBaseMethodSignature() { + return String.format("%1$s()", this.name); + } + + @Override + public void writeJavadoc(JavaJavadocComment commentBlock) { + commentBlock.description(description); + commentBlock.methodReturns(interfaceReturnValue.getDescription()); + } + + @Override + public void addImportsTo(Set imports, boolean includeImplementationImports) { + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/fluentmodel/update/ResourceUpdate.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/fluentmodel/update/ResourceUpdate.java new file mode 100644 index 0000000000..0578e99e08 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/fluentmodel/update/ResourceUpdate.java @@ -0,0 +1,238 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.update; + +import com.microsoft.typespec.http.client.generator.core.extension.plugin.PluginLogger; +import com.microsoft.typespec.http.client.generator.mgmt.FluentGen; +import com.microsoft.typespec.http.client.generator.mgmt.model.arm.UrlPathSegments; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentCollectionMethod; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentResourceCollection; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentResourceModel; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentStatic; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.examplemodel.MethodParameter; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ModelProperty; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.ResourceOperation; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.method.FluentApplyMethod; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.method.FluentConstructorByInner; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.method.FluentMethod; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.method.FluentMethodParameterMethod; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.method.FluentMethodType; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.method.FluentModelPropertyMethod; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.method.FluentUpdateMethod; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethodParameter; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientModel; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.IType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ListType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.MapType; +import com.microsoft.typespec.http.client.generator.core.util.CodeNamer; +import org.slf4j.Logger; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +public class ResourceUpdate extends ResourceOperation { + + private static final Logger LOGGER = new PluginLogger(FluentGen.getPluginInstance(), ResourceUpdate.class); + + private List updateStages; + + private FluentUpdateMethod updateMethod; + private List applyMethods; + + public ResourceUpdate(FluentResourceModel resourceModel, FluentResourceCollection resourceCollection, + UrlPathSegments urlPathSegments, String methodName, ClientModel bodyParameterModel) { + super(resourceModel, resourceCollection, urlPathSegments, methodName, bodyParameterModel); + + LOGGER.info("ResourceUpdate: Fluent model '{}', method reference '{}', body parameter '{}'", + resourceModel.getName(), methodName, bodyParameterModel.getName()); + } + + public List getUpdateStages() { + if (updateStages != null) { + return updateStages; + } + + updateStages = new ArrayList<>(); + + UpdateStage updateStageApply = new UpdateStageApply(); + // updateStageApply does not belong to updateStages + + List properties = this.getProperties(); + for (ModelProperty property : properties) { + UpdateStage stage = new UpdateStage("With" + CodeNamer.toPascalCase(property.getName()), property); + stage.setNextStage(updateStageApply); + + stage.getMethods().add(this.getPropertyMethod(stage, requestBodyParameterModel, property)); + + updateStages.add(stage); + } + // header and query parameters + List miscParameters = this.getMiscParameters(); + for (ClientMethodParameter parameter : miscParameters) { + String parameterNameForMethodSignature = deduplicateParameterNameForMethodSignature( + updateStages, parameter.getName()); + + UpdateStage stage = new UpdateStageMisc("With" + CodeNamer.toPascalCase(parameterNameForMethodSignature), parameter); + stage.setNextStage(updateStageApply); + + stage.getMethods().add(this.getParameterSetterMethod(stage, parameter, parameterNameForMethodSignature)); + + updateStages.add(stage); + } + + return updateStages; + } + + @Override + public List getFluentMethods() { + List methods = this.getUpdateStages().stream() + .flatMap(s -> s.getMethods().stream()) + .collect(Collectors.toList()); + methods.add(this.getUpdateMethod()); + methods.addAll(this.getApplyMethods()); + methods.add(this.getConstructor()); + return methods; + } + + @Override + public String getLocalVariablePrefix() { + return "update"; + } + + @Override + protected List getProperties() { + return super.getProperties().stream() + .filter(p -> !p.isReadOnlyForUpdate()) + .filter(p -> !isIdProperty(p) && !isLocationProperty(p)) // update should not be able to change id or location + .collect(Collectors.toList()); + } + + private FluentMethod getParameterSetterMethod(UpdateStage stage, ClientMethodParameter parameter, + String parameterNameForMethodSignature) { + return new FluentMethodParameterMethod(this.getResourceModel(), FluentMethodType.UPDATE_WITH, + stage, parameter, this.getLocalVariableByMethodParameter(parameter), + CodeNamer.getModelNamer().modelPropertySetterName(parameterNameForMethodSignature)); + } + + private String deduplicateParameterNameForMethodSignature(List stages, String parameterName) { + String stageName = "With" + CodeNamer.toPascalCase(parameterName); + for (UpdateStage stage : stages) { + if (stageName.equals(stage.getName())) { + return parameterName + "Parameter"; + } + } + return parameterName; + } + + private FluentMethod getPropertyMethod(UpdateStage stage, ClientModel model, ModelProperty property) { + if (hasDuplicateWithCreateMethodOnErasure(property)) { + return new FluentModelPropertyMethod(this.getResourceModel(), FluentMethodType.UPDATE_WITH, + stage, model, property, + this.getLocalVariableByMethodParameter(this.getBodyParameter()), + property.getSetterName() + "ForUpdate", + String.format("Specifies the %1$s property: %2$s.", property.getName(), property.getDescription())); + } else { + return new FluentModelPropertyMethod(this.getResourceModel(), FluentMethodType.UPDATE_WITH, + stage, model, property, + this.getLocalVariableByMethodParameter(this.getBodyParameter())); + } + } + + private boolean hasDuplicateWithCreateMethodOnErasure(ModelProperty property) { + // find duplicate on generic type with erasure, e.g. same property of different generic type List with List, but the generic type would be same under erasure. + boolean hasDuplicate = false; + String methodName = property.getSetterName(); + IType type = property.getClientType(); + if ((type instanceof ListType || type instanceof MapType) && resourceModel.getResourceCreate() != null) { + IType valueType = null; + if (type instanceof ListType) { + valueType = ((ListType) type).getElementType(); + } else if (type instanceof MapType) { + valueType = ((MapType) type).getValueType(); + } + IType valueTypeFinal = valueType; + + hasDuplicate = resourceModel.getResourceCreate().getFluentMethods().stream() + .filter(m -> m.getType() == FluentMethodType.CREATE_WITH) + .filter(m -> methodName.equals(m.getName())) + .map(m -> { + IType t = null; + if (m instanceof FluentModelPropertyMethod) { + t = ((FluentModelPropertyMethod) m).getModelProperty().getClientType(); + } else if (m instanceof FluentMethodParameterMethod) { + t = ((FluentMethodParameterMethod) m).getMethodParameter().getClientType(); + } + return t; + }) + .filter(Objects::nonNull) + // generic type + .map(t -> { + IType valueType1 = null; + if (t instanceof ListType) { + valueType1 = ((ListType) t).getElementType(); + } else if (type instanceof MapType) { + valueType1 = ((MapType) t).getValueType(); + } + return valueType1; + }) + .filter(Objects::nonNull) + // different type + .anyMatch(v -> !Objects.equals(valueTypeFinal.toString(), v.toString())); + } + return hasDuplicate; + } + + public FluentMethod getUpdateMethod() { + if (updateMethod == null) { + updateMethod = new FluentUpdateMethod(resourceModel, FluentMethodType.UPDATE, this.getResourceLocalVariables()); + } + return updateMethod; + } + + public List getApplyMethods() { + if (applyMethods == null) { + applyMethods = new ArrayList<>(); + + applyMethods.add(this.getApplyMethod(false)); + applyMethods.add(this.getApplyMethod(true)); + } + return applyMethods; + } + + private FluentMethod getConstructor() { + List pathParameters = this.getPathParameters(); + return new FluentConstructorByInner(resourceModel, FluentMethodType.CONSTRUCTOR, + pathParameters, this.getResourceLocalVariables(), + FluentStatic.getFluentManager().getType(), urlPathSegments); + } + + private FluentMethod getApplyMethod(boolean hasContextParameter) { + List parameters = new ArrayList<>(); + Optional methodOpt = this.findMethod(true, parameters); + if (methodOpt.isPresent()) { + if (!hasContextParameter) { + parameters.clear(); + } + return new FluentApplyMethod(resourceModel, FluentMethodType.APPLY, + parameters, this.getResourceLocalVariables(), + resourceCollection, methodOpt.get(), + resourceModel.getResourceCreate().getResourceLocalVariables()); + } else { + throw new IllegalStateException("Update method not found on model " + resourceModel.getName()); + } + } + + public void addImportsTo(Set imports, boolean includeImplementationImports) { + getUpdateStages().forEach(s -> s.addImportsTo(imports, includeImplementationImports)); + if (includeImplementationImports) { + getConstructor().addImportsTo(imports, true); + getUpdateMethod().addImportsTo(imports, true); + getApplyMethods().forEach(m -> m.addImportsTo(imports, true)); + } + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/fluentmodel/update/UpdateStage.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/fluentmodel/update/UpdateStage.java new file mode 100644 index 0000000000..3dad38532b --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/fluentmodel/update/UpdateStage.java @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.update; + +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ModelProperty; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.FluentInterfaceStage; + +public class UpdateStage extends FluentInterfaceStage { + + public UpdateStage(String name) { + super(name); + } + + protected UpdateStage(String name, ModelProperty property) { + super(name, property); + } + + public String getDescription(String modelName) { + return property == null + ? String.format("The stage of the %1$s update.", modelName) + : String.format("The stage of the %1$s update allowing to specify %2$s.", modelName, property.getName()); + } + + public ModelProperty getModelProperty() { + return this.property; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/fluentmodel/update/UpdateStageApply.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/fluentmodel/update/UpdateStageApply.java new file mode 100644 index 0000000000..96f77888a6 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/fluentmodel/update/UpdateStageApply.java @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.update; + +public class UpdateStageApply extends UpdateStage { + + public UpdateStageApply() { + super("Update"); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/fluentmodel/update/UpdateStageMisc.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/fluentmodel/update/UpdateStageMisc.java new file mode 100644 index 0000000000..c9fcbd5485 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/fluentmodel/update/UpdateStageMisc.java @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.update; + +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethodParameter; + +public class UpdateStageMisc extends UpdateStage { + + public UpdateStageMisc(String name, ClientMethodParameter parameter) { + super(name); + this.parameter = parameter; + } + + public String getDescription(String modelName) { + return String.format("The stage of the %1$s update allowing to specify %2$s.", modelName, parameter.getName()); + } + + public ClientMethodParameter getMethodParameter() { + return parameter; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/immutablemodel/CollectionMethodTemplate.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/immutablemodel/CollectionMethodTemplate.java new file mode 100644 index 0000000000..1b221bfe72 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/immutablemodel/CollectionMethodTemplate.java @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.immutablemodel; + +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentCollectionMethod; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.ModelNaming; +import com.microsoft.typespec.http.client.generator.mgmt.util.TypeConversionUtils; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.IType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ListType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.MapType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.PrimitiveType; +import com.microsoft.typespec.http.client.generator.core.template.prototype.MethodTemplate; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +// Implementation method template for simple return type +// E.g. "return this.inner().checkExistence(...)" +public class CollectionMethodTemplate implements ImmutableMethod { + + private final MethodTemplate implementationMethodTemplate; + + public CollectionMethodTemplate(FluentCollectionMethod fluentMethod, IType innerType) { + Set imports = new HashSet<>(); + fluentMethod.addImportsTo(imports, false); + // Type inner = ... + innerType.addImportsTo(imports, false); + if (innerType instanceof ListType || innerType instanceof MapType) { + // Collections.unmodifiableList + imports.add(Collections.class.getName()); + } + + implementationMethodTemplate = MethodTemplate.builder() + .imports(imports) + .methodSignature(fluentMethod.getMethodSignature()) + .method(block -> { + String expression = String.format("this.%1$s().%2$s", ModelNaming.METHOD_SERVICE_CLIENT, fluentMethod.getMethodInvocation()); + if (innerType == PrimitiveType.VOID || innerType == PrimitiveType.VOID.asNullable()) { + block.line(String.format("this.%1$s().%2$s;", ModelNaming.METHOD_SERVICE_CLIENT, fluentMethod.getMethodInvocation())); + } else { + if (innerType instanceof ListType || innerType instanceof MapType) { + block.line(String.format("%1$s %2$s = %3$s;", innerType, TypeConversionUtils.tempVariableName(), expression)); + block.ifBlock(String.format("%1$s != null", TypeConversionUtils.tempVariableName()), ifBlock -> { + block.methodReturn(TypeConversionUtils.objectOrUnmodifiableCollection(innerType, TypeConversionUtils.tempVariableName())); + }).elseBlock(elseBlock -> { + block.methodReturn(TypeConversionUtils.nullOrEmptyCollection(innerType)); + }); + } else { + block.methodReturn(expression); + } + } + }) + .build(); + } + + @Override + public MethodTemplate getMethodTemplate() { + return implementationMethodTemplate; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/immutablemodel/CollectionMethodTypeConversionTemplate.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/immutablemodel/CollectionMethodTypeConversionTemplate.java new file mode 100644 index 0000000000..6b2f8f4e0f --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/immutablemodel/CollectionMethodTypeConversionTemplate.java @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.immutablemodel; + +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentCollectionMethod; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.ModelNaming; +import com.microsoft.typespec.http.client.generator.mgmt.util.FluentUtils; +import com.microsoft.typespec.http.client.generator.mgmt.util.TypeConversionUtils; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.IType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ListType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.MapType; +import com.microsoft.typespec.http.client.generator.core.template.prototype.MethodTemplate; +import com.azure.core.http.rest.SimpleResponse; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import java.util.stream.Collectors; + +// Implementation method template for return type requires conversion. +// E.g. +// PagedIterable inner = this.inner().list(); +// return ResourceManagerUtils.mapPage(inner, inner1 -> new StorageAccountImpl(inner1, this.manager())); +public class CollectionMethodTypeConversionTemplate implements ImmutableMethod { + + private final MethodTemplate conversionMethodTemplate; + + public CollectionMethodTypeConversionTemplate(FluentCollectionMethod fluentMethod, IType innerType) { + Set imports = new HashSet<>(); + fluentMethod.addImportsTo(imports, false); + // Type inner = ... + innerType.addImportsTo(imports, false); + if (innerType instanceof ListType || innerType instanceof MapType) { + // Collectors.toList + imports.add(Collectors.class.getName()); + + // Collections.unmodifiableList + imports.add(Collections.class.getName()); + } + if (FluentUtils.isResponseType(innerType)) { + imports.add(SimpleResponse.class.getName()); + } + + conversionMethodTemplate = MethodTemplate.builder() + .imports(imports) + .methodSignature(fluentMethod.getMethodSignature()) + .method(block -> { + block.line(String.format("%1$s %2$s = this.%3$s().%4$s;", innerType, TypeConversionUtils.tempVariableName(), ModelNaming.METHOD_SERVICE_CLIENT, fluentMethod.getMethodInvocation())); + if (TypeConversionUtils.isPagedIterable(innerType)) { + block.methodReturn(TypeConversionUtils.conversionExpression(innerType, TypeConversionUtils.tempVariableName())); + } else { + block.ifBlock(String.format("%1$s != null", TypeConversionUtils.tempVariableName()), ifBlock -> { + String expression = TypeConversionUtils.conversionExpression(innerType, TypeConversionUtils.tempVariableName()); + block.methodReturn(TypeConversionUtils.objectOrUnmodifiableCollection(innerType, expression)); + }).elseBlock(elseBlock -> { + block.methodReturn(TypeConversionUtils.nullOrEmptyCollection(innerType)); + }); + } + }) + .build(); + } + + @Override + public MethodTemplate getMethodTemplate() { + return conversionMethodTemplate; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/immutablemodel/ImmutableMethod.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/immutablemodel/ImmutableMethod.java new file mode 100644 index 0000000000..74226ed2eb --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/immutablemodel/ImmutableMethod.java @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.immutablemodel; + +import com.microsoft.typespec.http.client.generator.core.template.prototype.MethodTemplate; + +public interface ImmutableMethod { + + MethodTemplate getMethodTemplate(); +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/immutablemodel/PropertyTemplate.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/immutablemodel/PropertyTemplate.java new file mode 100644 index 0000000000..4baf7524e5 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/immutablemodel/PropertyTemplate.java @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.immutablemodel; + +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentModelProperty; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.ModelNaming; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ModelProperty; +import com.microsoft.typespec.http.client.generator.mgmt.util.TypeConversionUtils; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ListType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.MapType; +import com.microsoft.typespec.http.client.generator.core.template.prototype.MethodTemplate; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +// Implementation method template for simple property +// E.g. "return this.inner().sku()" +public class PropertyTemplate implements ImmutableMethod { + + private final MethodTemplate implementationMethodTemplate; + + public PropertyTemplate(FluentModelProperty fluentProperty, ModelProperty property) { + Set imports = new HashSet<>(); + fluentProperty.getFluentType().addImportsTo(imports, false); + if (property.getClientType() instanceof ListType || property.getClientType() instanceof MapType) { + // Type inner = ... + property.getClientType().addImportsTo(imports, false); + + // Collections.unmodifiableList + imports.add(Collections.class.getName()); + } + + implementationMethodTemplate = MethodTemplate.builder() + .imports(imports) + .methodSignature(fluentProperty.getMethodSignature()) + .method(block -> { + if (property.getClientType() instanceof ListType || property.getClientType() instanceof MapType) { + block.line(String.format("%1$s %2$s = this.%3$s().%4$s();", property.getClientType().toString(), TypeConversionUtils.tempVariableName(), ModelNaming.METHOD_INNER_MODEL, property.getGetterName())); + block.ifBlock(String.format("%1$s != null", TypeConversionUtils.tempVariableName()), ifBlock -> { + block.methodReturn(TypeConversionUtils.objectOrUnmodifiableCollection(property.getClientType(), TypeConversionUtils.tempVariableName())); + }).elseBlock(elseBlock -> { + block.methodReturn(TypeConversionUtils.nullOrEmptyCollection(property.getClientType())); + }); + } else { + block.methodReturn(String.format("this.%1$s().%2$s()", ModelNaming.METHOD_INNER_MODEL, property.getGetterName())); + } + }) + .build(); + } + + @Override + public MethodTemplate getMethodTemplate() { + return implementationMethodTemplate; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/immutablemodel/PropertyTypeConversionTemplate.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/immutablemodel/PropertyTypeConversionTemplate.java new file mode 100644 index 0000000000..2841e650b4 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/clientmodel/immutablemodel/PropertyTypeConversionTemplate.java @@ -0,0 +1,63 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.immutablemodel; + +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentModelProperty; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.ModelNaming; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ModelProperty; +import com.microsoft.typespec.http.client.generator.mgmt.util.TypeConversionUtils; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ListType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.MapType; +import com.microsoft.typespec.http.client.generator.core.template.prototype.MethodTemplate; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import java.util.stream.Collectors; + +// Implementation method template for property requires conversion. +// E.g. +// BlobRestoreStatusInner inner = this.inner().blobRestoreStatus(); +// if (inner != null) { +// return new BlobRestoreStatusImpl(inner, this.manager()); +// } else { +// return null; +// } +public class PropertyTypeConversionTemplate implements ImmutableMethod { + + private final MethodTemplate conversionMethodTemplate; + + public PropertyTypeConversionTemplate(FluentModelProperty fluentProperty, ModelProperty property) { + Set imports = new HashSet<>(); + fluentProperty.getFluentType().addImportsTo(imports, false); + // Type inner = ... + property.getClientType().addImportsTo(imports, false); + if (property.getClientType() instanceof ListType || property.getClientType() instanceof MapType) { + // Collectors.toList + imports.add(Collectors.class.getName()); + + // Collections.unmodifiableList + imports.add(Collections.class.getName()); + } + + conversionMethodTemplate = MethodTemplate.builder() + .imports(imports) + .methodSignature(fluentProperty.getMethodSignature()) + .method(block -> { + block.line(String.format("%1$s %2$s = this.%3$s().%4$s();", property.getClientType().toString(), TypeConversionUtils.tempVariableName(), ModelNaming.METHOD_INNER_MODEL, property.getGetterName())); + block.ifBlock(String.format("%1$s != null", TypeConversionUtils.tempVariableName()), ifBlock -> { + String expression = TypeConversionUtils.conversionExpression(property.getClientType(), TypeConversionUtils.tempVariableName()); + block.methodReturn(TypeConversionUtils.objectOrUnmodifiableCollection(property.getClientType(), expression)); + }).elseBlock(elseBlock -> { + block.methodReturn(TypeConversionUtils.nullOrEmptyCollection(property.getClientType())); + }); + }) + .build(); + } + + @Override + public MethodTemplate getMethodTemplate() { + return conversionMethodTemplate; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/javamodel/FluentJavaPackage.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/javamodel/FluentJavaPackage.java new file mode 100644 index 0000000000..fb5e77514a --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/javamodel/FluentJavaPackage.java @@ -0,0 +1,143 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.model.javamodel; + +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; +import com.microsoft.typespec.http.client.generator.core.extension.plugin.NewPlugin; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentExample; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentLiveTests; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentManager; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentResourceCollection; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentResourceModel; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.ModelNaming; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.examplemodel.FluentMethodMockUnitTest; +import com.microsoft.typespec.http.client.generator.mgmt.model.projectmodel.Changelog; +import com.microsoft.typespec.http.client.generator.mgmt.model.projectmodel.FluentProject; +import com.microsoft.typespec.http.client.generator.mgmt.template.FluentMethodMockTestTemplate; +import com.microsoft.typespec.http.client.generator.mgmt.template.FluentLiveTestsTemplate; +import com.microsoft.typespec.http.client.generator.core.model.projectmodel.TextFile; +import com.microsoft.typespec.http.client.generator.mgmt.template.ChangelogTemplate; +import com.microsoft.typespec.http.client.generator.mgmt.template.FluentExampleTemplate; +import com.microsoft.typespec.http.client.generator.mgmt.template.FluentManagerTemplate; +import com.microsoft.typespec.http.client.generator.mgmt.template.FluentResourceCollectionImplementationTemplate; +import com.microsoft.typespec.http.client.generator.mgmt.template.FluentResourceCollectionInterfaceTemplate; +import com.microsoft.typespec.http.client.generator.mgmt.template.FluentResourceModelImplementationTemplate; +import com.microsoft.typespec.http.client.generator.mgmt.template.FluentResourceModelInterfaceTemplate; +import com.microsoft.typespec.http.client.generator.mgmt.template.ReadmeTemplate; +import com.microsoft.typespec.http.client.generator.mgmt.template.SampleTemplate; +import com.microsoft.typespec.http.client.generator.mgmt.template.ResourceManagerUtilsTemplate; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaFile; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaPackage; +import com.microsoft.typespec.http.client.generator.core.util.ClassNameUtil; +import com.microsoft.typespec.http.client.generator.core.util.CodeNamer; + +import java.util.List; + +public class FluentJavaPackage extends JavaPackage { + + public FluentJavaPackage(NewPlugin host) { + super(host); + } + + public void addReadmeMarkdown(FluentProject project) { + TextFile textFile = new TextFile("README.md", new ReadmeTemplate().write(project)); + this.checkDuplicateFile(textFile.getFilePath()); + textFiles.add(textFile); + } + + public void addChangelogMarkdown(Changelog changelog) { + TextFile textFile = new TextFile("CHANGELOG.md", new ChangelogTemplate().write(changelog)); + this.checkDuplicateFile(textFile.getFilePath()); + textFiles.add(textFile); + } + + public final void addSampleMarkdown(List examples, List sampleJavaFiles) { + TextFile textFile = new TextFile("SAMPLE.md", new SampleTemplate().write(examples, sampleJavaFiles)); + this.checkDuplicateFile(textFile.getFilePath()); + textFiles.add(textFile); + } + + public final void addFluentResourceModel(FluentResourceModel model) { + JavaFile javaFile = getJavaFileFactory().createSourceFile( + model.getInterfaceType().getPackage(), + model.getInterfaceType().getName()); + FluentResourceModelInterfaceTemplate.getInstance().write(model, javaFile); + addJavaFile(javaFile); + + javaFile = getJavaFileFactory().createSourceFile( + model.getImplementationType().getPackage(), + model.getImplementationType().getName()); + FluentResourceModelImplementationTemplate.getInstance().write(model, javaFile); + addJavaFile(javaFile); + } + + public final void addFluentResourceCollection(FluentResourceCollection collection) { + JavaFile javaFile = getJavaFileFactory().createSourceFile( + collection.getInterfaceType().getPackage(), + collection.getInterfaceType().getName()); + FluentResourceCollectionInterfaceTemplate.getInstance().write(collection, javaFile); + addJavaFile(javaFile); + + javaFile = getJavaFileFactory().createSourceFile( + collection.getImplementationType().getPackage(), + collection.getImplementationType().getName()); + FluentResourceCollectionImplementationTemplate.getInstance().write(collection, javaFile); + addJavaFile(javaFile); + } + + public final void addFluentManager(FluentManager model, FluentProject project) { + JavaFile javaFile = getJavaFileFactory().createSourceFile( + model.getType().getPackage(), + model.getType().getName()); + FluentManagerTemplate.getInstance().write(model, project, javaFile); + addJavaFile(javaFile); + } + + public final void addResourceManagerUtils() { + JavaSettings settings = JavaSettings.getInstance(); + JavaFile javaFile = getJavaFileFactory().createSourceFile( + settings.getPackage(settings.getImplementationSubpackage()), + ModelNaming.CLASS_RESOURCE_MANAGER_UTILS); + ResourceManagerUtilsTemplate.getInstance().write(javaFile); + addJavaFile(javaFile); + } + + public final JavaFile addSample(FluentExample example) { + JavaFile javaFile = getJavaFileFactory().createSampleFile( + example.getPackageName(), example.getClassName()); + FluentExampleTemplate.getInstance().write(example, javaFile); + addJavaFile(javaFile); + return javaFile; + } + + public void addOperationUnitTest(FluentMethodMockUnitTest unitTest) { + final String packageName = JavaSettings.getInstance().getPackage("generated"); + String className = unitTest.getResourceCollection().getInterfaceType().getName() + + CodeNamer.toPascalCase(unitTest.getCollectionMethod().getMethodName()); + + final String classNameSuffix = "MockTests"; + + className = ClassNameUtil.truncateClassName( + JavaSettings.getInstance().getPackage(), + "src/tests/java" + // a hack to count "MockTests" suffix into the length of the full path + + classNameSuffix, + packageName, className); + + className += classNameSuffix; + + JavaFile javaFile = getJavaFileFactory().createTestFile(packageName, className); + FluentMethodMockTestTemplate.ClientMethodInfo info = new FluentMethodMockTestTemplate.ClientMethodInfo( + className, unitTest); + FluentMethodMockTestTemplate.getInstance().write(info, javaFile); + addJavaFile(javaFile); + } + + public void addLiveTests(FluentLiveTests liveTests) { + JavaFile javaFile = getJavaFileFactory().createTestFile( + liveTests.getPackageName(), liveTests.getClassName()); + FluentLiveTestsTemplate.getInstance().write(liveTests, javaFile); + addJavaFile(javaFile); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/projectmodel/Changelog.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/projectmodel/Changelog.java new file mode 100644 index 0000000000..e9c232a387 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/projectmodel/Changelog.java @@ -0,0 +1,141 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.model.projectmodel; + +import com.microsoft.typespec.http.client.generator.core.extension.plugin.PluginLogger; +import com.microsoft.typespec.http.client.generator.mgmt.FluentGen; +import com.microsoft.typespec.http.client.generator.mgmt.util.FluentUtils; +import com.microsoft.typespec.http.client.generator.core.util.TemplateUtil; +import org.slf4j.Logger; + +import java.io.BufferedReader; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; +import java.time.format.SignStyle; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.time.temporal.ChronoField; + +public class Changelog { + + private static final Logger LOGGER = new PluginLogger(FluentGen.getPluginInstance(), Changelog.class); + + private static final Pattern UNRELEASED_VERSION_PATTERN = + Pattern.compile("^## ([0-9][-.a-z|0-9]+) \\(Unreleased\\)"); + + private final List lines; + + public Changelog(FluentProject project) { + this(FluentUtils.loadTextFromResource("Changelog.txt", + TemplateUtil.SERVICE_NAME, project.getServiceName(), + TemplateUtil.SERVICE_DESCRIPTION, project.getServiceDescriptionForMarkdown(), + TemplateUtil.ARTIFACT_VERSION, project.getVersion(), + TemplateUtil.DATE_UTC, getDateUtc() + )); + } + + public Changelog(String content) { + this.lines = Arrays.stream(content.split("\r?\n")).collect(Collectors.toList()); + } + + public Changelog(BufferedReader reader) { + this.lines = reader.lines().collect(Collectors.toList()); + } + + public void updateForVersion(FluentProject project) { + List sectionBefore = new ArrayList<>(); + List sectionAfter = new ArrayList<>(); + String previousUnreleasedVersion = null; + List previousChangelog = new ArrayList<>(); + + Pattern currentVersionPattern = Pattern.compile("^## " + Pattern.quote(project.getVersion())+ " \\(.*\\)"); + + boolean beforeUnreleasedSection = true; + boolean afterUnreleasedSection = false; + for (String line : this.lines) { + if (line.trim().startsWith("## ")) { + if (beforeUnreleasedSection) { + beforeUnreleasedSection = false; + + if (line.trim().endsWith("(Unreleased)")) { + Matcher m = UNRELEASED_VERSION_PATTERN.matcher(line.trim()); + if (m.find()) { + previousUnreleasedVersion = m.group(1); + LOGGER.info("Found last unreleased version '{}'", previousUnreleasedVersion); + } + } else if (currentVersionPattern.matcher(line.trim()).find()) { + previousUnreleasedVersion = project.getVersion(); + LOGGER.info("Found last version '{}', which is same as current version", previousUnreleasedVersion); + } else { + afterUnreleasedSection = true; + } + } else { + afterUnreleasedSection = true; + } + } else if (!beforeUnreleasedSection && !afterUnreleasedSection) { + if (!previousChangelog.isEmpty() || !line.isEmpty()) { + previousChangelog.add(line); + } + } + + if (beforeUnreleasedSection) { + sectionBefore.add(line); + } + if (afterUnreleasedSection) { + sectionAfter.add(line); + } + } + + String currentChangelog = String.format("- Azure Resource Manager %1$s client library for Java. %2$s", project.getServiceName(), project.getServiceDescriptionForMarkdown()); + + this.lines.clear(); + + this.lines.addAll(sectionBefore); + this.lines.add(String.format("## %1$s (%2$s)", project.getVersion(), getDateUtc())); + this.lines.add(""); + this.lines.add(currentChangelog); + if (!previousChangelog.isEmpty() && !previousChangelog.iterator().next().startsWith("- ")) { + // blank line when first line is not unordered list + this.lines.add(""); + } + for (String line : previousChangelog) { + if (!line.trim().equals(currentChangelog)) { + this.lines.add(line); + } + } + if (previousChangelog.isEmpty() || !previousChangelog.get(previousChangelog.size() - 1).trim().isEmpty()) { + // blank line when last line is not blank line (or no line at all) + this.lines.add(""); + } + this.lines.addAll(sectionAfter); + } + + public String getContent() { + return String.join("\n", lines) + "\n"; + } + + List getLines() { + return lines; + } + + private static final DateTimeFormatter FORMATTER = new DateTimeFormatterBuilder() + .appendValue(ChronoField.YEAR, 4, 10, SignStyle.EXCEEDS_PAD) + .appendLiteral('-') + .appendValue(ChronoField.MONTH_OF_YEAR, 2) + .appendLiteral('-') + .appendValue(ChronoField.DAY_OF_MONTH, 2) + .toFormatter(Locale.ROOT); + + static String getDateUtc() { + OffsetDateTime now = OffsetDateTime.now(ZoneOffset.UTC); + return now.format(FORMATTER); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/projectmodel/CodeSample.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/projectmodel/CodeSample.java new file mode 100644 index 0000000000..94bec1cae9 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/projectmodel/CodeSample.java @@ -0,0 +1,131 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.model.projectmodel; + +import com.microsoft.typespec.http.client.generator.core.extension.plugin.PluginLogger; +import com.microsoft.typespec.http.client.generator.mgmt.FluentGen; +import org.slf4j.Logger; + +import java.io.BufferedReader; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +public class CodeSample { + + private static final Logger LOGGER = new PluginLogger(FluentGen.getPluginInstance(), CodeSample.class); + + private static final String TEST_ANNOTATION = "@Test"; + private static final String EMBEDME_START_COMMENT = "// @embedmeStart"; + private static final String EMBEDME_END_COMMENT = "// @embedmeEnd"; + + private String code; + + protected CodeSample() { + } + + public static CodeSample fromTestFile(Path testFilePath) { + // the assumption is there is a embedme block in a @Test method + // for now, only extract first block + + CodeSample codeSample = new CodeSample(); + + try (BufferedReader reader = Files.newBufferedReader(testFilePath, StandardCharsets.UTF_8)) { + List codeLines = new ArrayList<>(); + + boolean testMethodBegin = false; + boolean embedmeBlockBegin = false; + String testMethodIndent = ""; + String embedmeBlockIndent = ""; + for (String line : reader.lines().collect(Collectors.toList())) { + if (!testMethodBegin) { + if (line.trim().equals(TEST_ANNOTATION)) { + // first get inside @Test method + + testMethodBegin = true; + int indent = line.indexOf(TEST_ANNOTATION); + char[] chars = new char[indent]; + Arrays.fill(chars, ' '); + testMethodIndent = String.valueOf(chars); + } + } else if (!embedmeBlockBegin) { + if (line.startsWith(testMethodIndent + "}")) { + // method ends without embedme block + + testMethodBegin = false; + // continue + } else if (line.trim().equals(EMBEDME_START_COMMENT)) { + // next get inside embedme block, similar to https://github.com/zakhenry/embedme/issues/48 + + embedmeBlockBegin = true; + int indent = line.indexOf(EMBEDME_START_COMMENT); + char[] chars = new char[indent]; + Arrays.fill(chars, ' '); + embedmeBlockIndent = String.valueOf(chars); + } + } else { + if (line.startsWith(embedmeBlockIndent + EMBEDME_END_COMMENT)) { + // embedme block ends + + embedmeBlockBegin = false; + break; + // for now, only extract one block + } else { + // extract the code line (except Assertions) + + if (!line.trim().startsWith("Assertions.") && !line.trim().startsWith("assert")) { + codeLines.add(line); + } + } + } + } + + if (!codeLines.isEmpty() && !embedmeBlockBegin) { + codeLines = removeIndent(codeLines); + codeSample.code = String.join("\n", codeLines) + "\n"; + + LOGGER.info("Read {} lines of code sample from test file '{}'", codeLines.size(), testFilePath); + } + } catch (IOException e) { + LOGGER.warn("Failed to read '" + testFilePath + "'", e); + } + + return codeSample; + } + + public String getCode() { + return code; + } + + private static List removeIndent(List codeLines) { + int minIndent = Integer.MAX_VALUE; + for (String line : codeLines) { + String trimmedLine = line.trim(); + if (!trimmedLine.isEmpty()) { + int indent = line.indexOf(trimmedLine); + if (indent < minIndent) { + minIndent = indent; + } + } + } + + List lines = codeLines; + if (minIndent > 0) { + lines = new ArrayList<>(); + for (String line : codeLines) { + if (line.length() > minIndent) { + lines.add(line.substring(minIndent)); + } else { + lines.add(""); + } + } + } + return lines; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/projectmodel/FluentProject.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/projectmodel/FluentProject.java new file mode 100644 index 0000000000..1753acc9d5 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/model/projectmodel/FluentProject.java @@ -0,0 +1,180 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.model.projectmodel; + +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; +import com.microsoft.typespec.http.client.generator.core.extension.plugin.PluginLogger; +import com.microsoft.typespec.http.client.generator.mgmt.FluentGen; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentClient; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentStatic; +import com.microsoft.typespec.http.client.generator.mgmt.util.FluentJavaSettings; +import com.microsoft.typespec.http.client.generator.mgmt.util.FluentUtils; +import com.microsoft.typespec.http.client.generator.core.model.projectmodel.Project; +import org.slf4j.Logger; + +import java.io.BufferedReader; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; + +public class FluentProject extends Project { + + private static final Logger LOGGER = new PluginLogger(FluentGen.getPluginInstance(), FluentProject.class); + + protected final ServiceDescription serviceDescription = new ServiceDescription(); + + private Changelog changelog; + private final List codeSamples = new ArrayList<>(); + + private static class ServiceDescription { + private String simpleDescription; + private String clientDescription; + private String tagDescription; + + private String getServiceDescription() { + return String.format("%1$s %2$s %3$s", + simpleDescription, + clientDescription, + tagDescription).trim(); + } + + public String getServiceDescriptionForPom() { + return String.format("%1$s %2$s %3$s %4$s", + simpleDescription, + "For documentation on how to use this package, please see https://aka.ms/azsdk/java/mgmt.", + clientDescription, + tagDescription).trim(); + } + + public String getServiceDescriptionForMarkdown() { + return this.getServiceDescription() + " For documentation on how to use this package, please see [Azure Management Libraries for Java](https://aka.ms/azsdk/java/mgmt)."; + } + } + + public FluentProject(FluentClient fluentClient) { + this(fluentClient.getManager().getServiceName(), fluentClient.getInnerClient().getClientDescription()); + } + + protected FluentProject(String serviceName, String clientDescription) { + this.groupId = "com.azure.resourcemanager"; + + this.serviceName = serviceName; + this.namespace = JavaSettings.getInstance().getPackage(); + this.artifactId = FluentUtils.getArtifactId(); + + FluentStatic.getFluentJavaSettings().getArtifactVersion().ifPresent(version -> this.version = version); + + if (clientDescription == null) { + clientDescription = ""; + } + if (!clientDescription.isEmpty() && !clientDescription.endsWith(".")) { + clientDescription += "."; + } + + final String simpleDescriptionTemplate = "This package contains Microsoft Azure SDK for %1$s Management SDK."; + final String tagDescriptionTemplate = "Package tag %1$s."; + + this.serviceDescription.simpleDescription = String.format(simpleDescriptionTemplate, serviceName); + this.serviceDescription.clientDescription = clientDescription; + String autorestTag = JavaSettings.getInstance().getAutorestSettings().getTag(); + // SDK from TypeSpec does not contain autorest tag. + this.serviceDescription.tagDescription = autorestTag == null + ? "" + : String.format(tagDescriptionTemplate, autorestTag); + + this.changelog = new Changelog(this); + } + + @Override + public void integrateWithSdk() { +// FluentPomTemplate.setProject(this); + + findPackageVersions(); + + findPomDependencies(); + + updateChangelog(); + + findCodeSamples(); + + findSdkRepositoryUri(); + } + + private void updateChangelog() { + String outputFolder = JavaSettings.getInstance().getAutorestSettings().getOutputFolder(); + if (outputFolder != null && Paths.get(outputFolder).isAbsolute()) { + Path changelogPath = Paths.get(outputFolder, "CHANGELOG.md"); + + if (Files.isReadable(changelogPath)) { + try (BufferedReader reader = Files.newBufferedReader(changelogPath, StandardCharsets.UTF_8)) { + this.changelog = new Changelog(reader); + LOGGER.info("Update 'CHANGELOG.md' for version '{}'", version); + this.changelog.updateForVersion(this); + } catch (IOException e) { + LOGGER.warn("Failed to parse 'CHANGELOG.md'", e); + } + } else { + LOGGER.info("'CHANGELOG.md' not found or not readable"); + } + } else { + LOGGER.warn("'output-folder' parameter is not an absolute path, fallback to default CHANGELOG.md"); + } + } + + private void findCodeSamples() { + String outputFolder = JavaSettings.getInstance().getAutorestSettings().getOutputFolder(); + if (outputFolder != null && Paths.get(outputFolder).isAbsolute()) { + Path srcTestJavaPath = Paths.get(outputFolder).resolve(Paths.get("src", "test", "java")); + if (Files.isDirectory(srcTestJavaPath)) { + try { + Files.walk(srcTestJavaPath).forEach(path -> { + if (!Files.isDirectory(path) && Files.isReadable(path) + && (path.getFileName().toString().endsWith("Tests.java") + || path.getFileName().toString().endsWith("Test.java"))) { + LOGGER.info("Attempt to find code sample from test file '{}'", path); + codeSamples.add(CodeSample.fromTestFile(path)); + } + }); + } catch (IOException e) { + LOGGER.warn("Failed to walk path '" + srcTestJavaPath + "'", e); + } + } + } else { + LOGGER.warn("'output-folder' parameter is not an absolute path, skip code samples"); + } + } + + @Override + public String getServiceDescription() { + return this.serviceDescription.getServiceDescription(); + } + + @Override + public String getServiceDescriptionForPom() { + return this.serviceDescription.getServiceDescriptionForPom(); + } + + @Override + public String getServiceDescriptionForMarkdown() { + return this.serviceDescription.getServiceDescriptionForMarkdown(); + } + + public Changelog getChangelog() { + return changelog; + } + + public List getCodeSamples() { + return codeSamples; + } + + @Override + public boolean isGenerateSamples() { + FluentJavaSettings settings = FluentStatic.getFluentJavaSettings(); + return settings.isGenerateSamples(); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/namer/FluentModelNamer.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/namer/FluentModelNamer.java new file mode 100644 index 0000000000..a207ddf984 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/namer/FluentModelNamer.java @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.namer; + +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientModelProperty; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.IType; +import com.microsoft.typespec.http.client.generator.core.util.CodeNamer; +import com.microsoft.typespec.http.client.generator.core.util.ModelNamer; + +public class FluentModelNamer extends ModelNamer { + + @Override + public String modelPropertyGetterName(ClientModelProperty property) { + String propertyName = property.getName(); + return this.modelPropertyGetterName(propertyName); + } + + @Override + public String modelPropertyGetterName(IType clientType, String propertyName) { + return this.modelPropertyGetterName(propertyName); + } + + @Override + public String modelPropertyGetterName(String propertyName) { + return CodeNamer.toCamelCase(propertyName); + } + + @Override + public String modelPropertySetterName(ClientModelProperty property) { + String propertyName = property.getName(); + return this.modelPropertySetterName(propertyName); + } + + public String modelPropertySetterName(String propertyName) { + return "with" + CodeNamer.toPascalCase(propertyName); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/namer/FluentNamerFactory.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/namer/FluentNamerFactory.java new file mode 100644 index 0000000000..a011b0412e --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/namer/FluentNamerFactory.java @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.namer; + +import com.microsoft.typespec.http.client.generator.mgmt.util.FluentJavaSettings; +import com.microsoft.typespec.http.client.generator.core.util.ModelNamer; +import com.microsoft.typespec.http.client.generator.core.util.NamerFactory; + +public class FluentNamerFactory implements NamerFactory { + + private final ModelNamer modelNamer; + + public FluentNamerFactory(FluentJavaSettings settings) { + modelNamer = settings.isTrack1Naming() ? new FluentModelNamer() : new ModelNamer(); + } + + @Override + public ModelNamer getModelNamer() { + return modelNamer; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/template/ChangelogTemplate.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/template/ChangelogTemplate.java new file mode 100644 index 0000000000..b1961b342c --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/template/ChangelogTemplate.java @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.template; + +import com.microsoft.typespec.http.client.generator.mgmt.model.projectmodel.Changelog; + +public class ChangelogTemplate extends com.microsoft.typespec.http.client.generator.core.template.ChangelogTemplate { + + public String write(Changelog changelog) { + return changelog.getContent(); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/template/FluentClientMethodTemplate.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/template/FluentClientMethodTemplate.java new file mode 100644 index 0000000000..94a233a359 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/template/FluentClientMethodTemplate.java @@ -0,0 +1,308 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.template; + +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClassType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethod; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.GenericType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.IType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ProxyMethod; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaType; +import com.microsoft.typespec.http.client.generator.core.template.ClientMethodTemplate; +import com.microsoft.typespec.http.client.generator.core.util.CodeNamer; +import com.microsoft.typespec.http.client.generator.core.util.MethodNamer; + +public class FluentClientMethodTemplate extends ClientMethodTemplate { + + private static final FluentClientMethodTemplate INSTANCE = new FluentClientMethodTemplate(); + + public static FluentClientMethodTemplate getInstance() { + return INSTANCE; + } + + @Override + protected void generatePagedAsyncSinglePage(ClientMethod clientMethod, JavaType typeBlock, ProxyMethod restAPIMethod, JavaSettings settings) { + boolean addContextParameter = !contextInParameters(clientMethod); + boolean mergeContextParameter = contextInParameters(clientMethod); + boolean isLroPagination = GenericType.Mono(GenericType.Response(GenericType.FLUX_BYTE_BUFFER)).equals(restAPIMethod.getReturnType().getClientType()); + String endOfLine = addContextParameter ? "" : ";"; + String contextParam = mergeContextParameter ? "context" : String.format("%s.getContext()", clientMethod.getClientReference()); + + typeBlock.annotation("ServiceMethod(returns = ReturnType.SINGLE)"); + String restAPIMethodArgumentList = String.join(", ", clientMethod.getProxyMethodArguments(settings)); + String serviceMethodCall = String.format("service.%s(%s)", restAPIMethod.getName(), restAPIMethodArgumentList); + if (clientMethod.getMethodPageDetails().nonNullNextLink()) { + writeMethod(typeBlock, clientMethod.getMethodVisibility(), clientMethod.getDeclaration(), function -> { + addValidations(function, clientMethod.getRequiredNullableParameterExpressions(), clientMethod.getValidateExpressions(), settings); + addOptionalAndConstantVariables(function, clientMethod, restAPIMethod.getParameters(), settings); + applyParameterTransformations(function, clientMethod, settings); + convertClientTypesToWireTypes(function, clientMethod, restAPIMethod.getParameters()); + if (mergeContextParameter) { + function.line(String.format("context = %s.mergeContext(context);", clientMethod.getClientReference())); + } + if (addContextParameter) { + if (!isLroPagination) { + function.line(String.format("return FluxUtil.withContext(context -> %s)", + serviceMethodCall)); + } else { + function.line("return FluxUtil.withContext(context -> {"); + function.indent(() -> { + function.line(String.format("%s mono = %s.cache();", + clientMethod.getProxyMethod().getReturnType().toString(), + serviceMethodCall)); + + IType classType = clientMethod.getMethodPageDetails().getLroIntermediateType(); + function.line(String.format("return Mono.zip(mono, %1$s.<%2$s, %2$s>getLroResult(mono, %1$s.getHttpPipeline(), %2$s.class, %2$s.class, %3$s).last().flatMap(%1$s::getLroFinalResultOrError));", + clientMethod.getClientReference(), classType, contextParam)); + }); + function.line("})"); + } + } else { + if (!isLroPagination) { + function.line(String.format("return %s", + serviceMethodCall)); + } else { + function.line(String.format("%s mono = %s.cache();", + clientMethod.getProxyMethod().getReturnType().toString(), + serviceMethodCall)); + + IType classType = clientMethod.getMethodPageDetails().getLroIntermediateType(); + function.line(String.format("return Mono.zip(mono, %1$s.<%2$s, %2$s>getLroResult(mono, %1$s.getHttpPipeline(), %2$s.class, %2$s.class, %3$s).last().flatMap(%1$s::getLroFinalResultOrError))", + clientMethod.getClientReference(), classType, contextParam)); + } + } + function.indent(() -> { + if (addContextParameter) { + function.line(String.format(".<%s>map(res -> new PagedResponseBase<>(", + returnTypeWithoutMono(clientMethod.getReturnValue().getType()))); + } else { + function.line(".map(res -> new PagedResponseBase<>("); + } + function.indent(() -> { + if (!isLroPagination) { + function.line("res.getRequest(),"); + function.line("res.getStatusCode(),"); + function.line("res.getHeaders(),"); + function.line("res.getValue().%s(),", CodeNamer.getModelNamer().modelPropertyGetterName(clientMethod.getMethodPageDetails().getItemName())); + function.line(nextLinkLine(clientMethod)); + IType responseType = ((GenericType) clientMethod.getProxyMethod().getReturnType()).getTypeArguments()[0]; + if (responseType instanceof ClassType) { + function.line(String.format("res.getDeserializedHeaders()))%s", endOfLine)); + } else { + function.line(String.format("null))%s", endOfLine)); + } + } else { + function.line("res.getT1().getRequest(),"); + function.line("res.getT1().getStatusCode(),"); + function.line("res.getT1().getHeaders(),"); + function.line("res.getT2().%s(),", CodeNamer.getModelNamer().modelPropertyGetterName(clientMethod.getMethodPageDetails().getItemName())); + function.line(nextLinkLine(clientMethod, "getT2()")); + IType responseType = ((GenericType) clientMethod.getProxyMethod().getReturnType()).getTypeArguments()[0]; + if (responseType instanceof ClassType) { + function.line(String.format("res.getT2().getDeserializedHeaders()))%s", endOfLine)); + } else { + function.line(String.format("null))%s", endOfLine)); + } + } + }); + if (addContextParameter) { + function.line(String.format(".contextWrite(context -> context.putAll(FluxUtil.toReactorContext(%s.getContext()).readOnly()));", clientMethod.getClientReference())); + } + }); + }); + } else { + writeMethod(typeBlock, clientMethod.getMethodVisibility(), clientMethod.getDeclaration(), function -> { + addValidations(function, clientMethod.getRequiredNullableParameterExpressions(), clientMethod.getValidateExpressions(), settings); + addOptionalAndConstantVariables(function, clientMethod, restAPIMethod.getParameters(), settings); + applyParameterTransformations(function, clientMethod, settings); + convertClientTypesToWireTypes(function, clientMethod, restAPIMethod.getParameters()); + if (mergeContextParameter) { + function.line(String.format("context = %s.mergeContext(context);", clientMethod.getClientReference())); + } + if (addContextParameter) { + if (!isLroPagination) { + function.line(String.format("return FluxUtil.withContext(context -> %s)", + serviceMethodCall)); + } else { + function.line("return FluxUtil.withContext(context -> {"); + function.indent(() -> { + function.line(String.format("%s mono = %s.cache();", + clientMethod.getProxyMethod().getReturnType().toString(), + serviceMethodCall)); + + IType classType = clientMethod.getMethodPageDetails().getLroIntermediateType(); + function.line(String.format("return Mono.zip(mono, %s.<%s, %s>getLroResult(mono, %s.getHttpPipeline(), %s.class, %s.class, %s).last().flatMap(%s::getLroFinalResultOrError));", + clientMethod.getClientReference(), classType.toString(), classType, clientMethod.getClientReference(), + classType, classType, contextParam, clientMethod.getClientReference())); + }); + function.line("})"); + } + } else { + if (!isLroPagination) { + function.line(String.format("return %s", + serviceMethodCall)); + } else { + function.line(String.format("%s mono = %s.cache();", + clientMethod.getProxyMethod().getReturnType().toString(), + serviceMethodCall)); + + IType classType = clientMethod.getMethodPageDetails().getLroIntermediateType(); + function.line(String.format("return Mono.zip(mono, %s.<%s, %s>getLroResult(mono, %s.getHttpPipeline(), %s.class, %s.class, %s).last().flatMap(%s::getLroFinalResultOrError))", + clientMethod.getClientReference(), classType.toString(), classType, clientMethod.getClientReference(), + classType, + classType, contextParam, clientMethod.getClientReference())); + } + } + function.indent(() -> { + if (addContextParameter) { + function.line(String.format(".<%s>map(res -> new PagedResponseBase<>(", + returnTypeWithoutMono(clientMethod.getReturnValue().getType()))); + } else { + function.line(".map(res -> new PagedResponseBase<>("); + } + function.indent(() -> { + if (!isLroPagination) { + function.line("res.getRequest(),"); + function.line("res.getStatusCode(),"); + function.line("res.getHeaders(),"); + function.line("res.getValue().%s(),", CodeNamer.getModelNamer().modelPropertyGetterName(clientMethod.getMethodPageDetails().getItemName())); + function.line("null,"); + IType responseType = ((GenericType) clientMethod.getProxyMethod().getReturnType()).getTypeArguments()[0]; + if (responseType instanceof ClassType) { + function.line(String.format("res.getDeserializedHeaders()))%s", endOfLine)); + } else { + function.line(String.format("null))%s", endOfLine)); + } + } else { + function.line("res.getT1().getRequest(),"); + function.line("res.getT1().getStatusCode(),"); + function.line("res.getT1().getHeaders(),"); + function.line("res.getT2().%s(),", CodeNamer.getModelNamer().modelPropertyGetterName(clientMethod.getMethodPageDetails().getItemName())); + function.line("null,"); + IType responseType = ((GenericType) clientMethod.getProxyMethod().getReturnType()).getTypeArguments()[0]; + if (responseType instanceof ClassType) { + function.line(String.format("res.getT2().getDeserializedHeaders()))%s", endOfLine)); + } else { + function.line(String.format("null))%s", endOfLine)); + } + } + }); + if (addContextParameter) { + function.line(String.format(".contextWrite(context -> context.putAll(FluxUtil.toReactorContext(%s.getContext()).readOnly()));", clientMethod.getClientReference())); + } + }); + }); + } + } + + @Override + protected void generateSimpleAsyncRestResponse(ClientMethod clientMethod, JavaType typeBlock, ProxyMethod restAPIMethod, JavaSettings settings) { + boolean addContextParameter = !contextInParameters(clientMethod); + boolean mergeContextParameter = !addContextParameter; + + typeBlock.annotation("ServiceMethod(returns = ReturnType.SINGLE)"); + writeMethod(typeBlock, clientMethod.getMethodVisibility(), clientMethod.getDeclaration(), function -> { + addValidations(function, clientMethod.getRequiredNullableParameterExpressions(), clientMethod.getValidateExpressions(), settings); + addOptionalAndConstantVariables(function, clientMethod, restAPIMethod.getParameters(), settings); + applyParameterTransformations(function, clientMethod, settings); + convertClientTypesToWireTypes(function, clientMethod, restAPIMethod.getParameters()); + + String restAPIMethodArgumentList = String.join(", ", clientMethod.getProxyMethodArguments(settings)); + String serviceMethodCall = String.format("service.%s(%s)", restAPIMethod.getName(), restAPIMethodArgumentList); + if (mergeContextParameter) { + function.line(String.format("context = %s.mergeContext(context);", clientMethod.getClientReference())); + } + if (addContextParameter) { + function.line(String.format("return FluxUtil.withContext(context -> %s)", + serviceMethodCall)); + function.indent(() -> { + function.line(String.format(".contextWrite(context -> context.putAll(FluxUtil.toReactorContext(%s.getContext()).readOnly()));", clientMethod.getClientReference())); + }); + } else { + function.methodReturn(serviceMethodCall); + } + }); + } + + @Override + protected void generateLongRunningAsync(ClientMethod clientMethod, JavaType typeBlock, ProxyMethod restAPIMethod, JavaSettings settings) { + typeBlock.annotation("ServiceMethod(returns = ReturnType.SINGLE)"); + String beginAsyncMethodName = MethodNamer.getLroBeginAsyncMethodName(restAPIMethod.getName()); + writeMethod(typeBlock, clientMethod.getMethodVisibility(), clientMethod.getDeclaration(), function -> { + addOptionalVariables(function, clientMethod); + function.line("return %s(%s)", beginAsyncMethodName, clientMethod.getArgumentList()); + function.indent(() -> { + function.line(".last()"); + function.line(String.format(".flatMap(%s::getLroFinalResultOrError);", clientMethod.getClientReference())); + }); + }); + } + + @Override + protected void generateLongRunningSync(ClientMethod clientMethod, JavaType typeBlock, ProxyMethod restAPIMethod, JavaSettings settings) { + super.generateSyncMethod(clientMethod, typeBlock, restAPIMethod, settings); +// typeBlock.annotation("ServiceMethod(returns = ReturnType.SINGLE)"); +// String asyncMethodName = clientMethod.getSimpleAsyncMethodName(); +// writeMethod(typeBlock, clientMethod.getMethodVisibility(), clientMethod.getDeclaration(), function -> { +// addOptionalVariables(function, clientMethod, restAPIMethod.getParameters(), settings); +// if (clientMethod.getReturnValue().getType() != PrimitiveType.Void) { +// function.methodReturn(String.format("%s(%s).block()", asyncMethodName, clientMethod.getArgumentList())); +// } else { +// function.line("%s(%s).block();", asyncMethodName, clientMethod.getArgumentList()); +// } +// }); + } + + @Override + protected void generateLongRunningBeginAsync(ClientMethod clientMethod, JavaType typeBlock, ProxyMethod restAPIMethod, JavaSettings settings) { + boolean mergeContextParameter = contextInParameters(clientMethod); + String contextParam = mergeContextParameter ? "context" : String.format("%s.getContext()", clientMethod.getClientReference());; + + typeBlock.annotation("ServiceMethod(returns = ReturnType.LONG_RUNNING_OPERATION)"); + writeMethod(typeBlock, clientMethod.getMethodVisibility(), clientMethod.getDeclaration(), function -> { + IType classType = ((GenericType) clientMethod.getReturnValue().getType().getClientType()).getTypeArguments()[1]; + + addOptionalVariables(function, clientMethod); + if (mergeContextParameter) { + function.line(String.format("context = %s.mergeContext(context);", clientMethod.getClientReference())); + } + function.line("%s mono = %s(%s);", clientMethod.getProxyMethod().getReturnType().toString(), clientMethod.getProxyMethod().getSimpleAsyncRestResponseMethodName(), clientMethod.getArgumentList()); + if (classType instanceof GenericType) { + // pageable LRO + String typeReferenceGetType; + if (settings.isStreamStyleSerialization()) { + typeReferenceGetType = "getJavaType"; + } else { + typeReferenceGetType = "getType"; + } + function.line("return %1$s.<%2$s, %2$s>getLroResult(mono, %1$s.getHttpPipeline(), new TypeReference<%2$s>() {}.%3$s(), new TypeReference<%2$s>() {}.%3$s(), %4$s);", clientMethod.getClientReference(), classType.toString(), typeReferenceGetType, contextParam); + } else { + function.line("return %s.<%s, %s>getLroResult(mono, %s.getHttpPipeline(), %s.class, %s.class, %s);", clientMethod.getClientReference(), classType.toString(), classType.toString(), clientMethod.getClientReference(), classType.toString(), classType.toString(), contextParam); + } + }); + } + + @Override + protected void generateLongRunningBeginSync(ClientMethod clientMethod, JavaType typeBlock, ProxyMethod restAPIMethod, JavaSettings settings) { + typeBlock.annotation("ServiceMethod(returns = ReturnType.LONG_RUNNING_OPERATION)"); + String beginAsyncMethodName = MethodNamer.getLroBeginAsyncMethodName(restAPIMethod.getName()); + typeBlock.publicMethod(clientMethod.getDeclaration(), function -> { + addOptionalVariables(function, clientMethod); + function.line("return %s(%s)", beginAsyncMethodName, clientMethod.getArgumentList()); + function.indent((() -> { + function.text(".getSyncPoller();"); + })); + }); + } + + private static IType returnTypeWithoutMono(IType returnType) { + // need e.g. PagedResponse + IType returnTypeWithoutMono = returnType; + if (returnType instanceof GenericType && ((GenericType) returnType).getName().equals("Mono")) { + returnTypeWithoutMono = ((GenericType) returnType).getTypeArguments()[0]; + } + return returnTypeWithoutMono; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/template/FluentExampleTemplate.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/template/FluentExampleTemplate.java new file mode 100644 index 0000000000..f3649e7d2e --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/template/FluentExampleTemplate.java @@ -0,0 +1,266 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.template; + +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentStatic; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.examplemodel.FluentCollectionMethodExample; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.examplemodel.FluentExample; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.examplemodel.FluentMethodExample; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.examplemodel.FluentResourceCreateExample; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.examplemodel.FluentResourceUpdateExample; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.examplemodel.ParameterExample; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClassType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.IType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.PrimitiveType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.examplemodel.ExampleHelperFeature; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaFile; +import com.microsoft.typespec.http.client.generator.core.template.example.ModelExampleWriter; +import com.microsoft.typespec.http.client.generator.core.util.CodeNamer; +import com.azure.core.util.CoreUtils; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +public class FluentExampleTemplate { + + private static final FluentExampleTemplate INSTANCE = new FluentExampleTemplate(); + + public static FluentExampleTemplate getInstance() { + return INSTANCE; + } + + public final void write(com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentExample example, JavaFile javaFile) { + String className = example.getClassName(); + + List exampleMethods = getExampleMethods(example); + + Set imports = exampleMethods.stream().flatMap(em -> em.getImports().stream()).collect(Collectors.toSet()); + javaFile.declareImport(imports); + + Set helperFeatures = exampleMethods.stream().flatMap(em -> em.getHelperFeatures().stream()).collect(Collectors.toSet()); + + javaFile.javadocComment(commentBlock -> { + commentBlock.description(String.format("Samples for %1$s %2$s", example.getGroupName(), example.getMethodName())); + }); + javaFile.publicFinalClass(className, classBlock -> { + for (ExampleMethod exampleMethod : exampleMethods) { + if (!CoreUtils.isNullOrEmpty(exampleMethod.getExample().getOriginalFileName())) { + classBlock.blockComment(getExampleTag(exampleMethod.getExample())); + } + + classBlock.javadocComment(commentBlock -> { + commentBlock.description(String.format("Sample code: %1$s", exampleMethod.getExample().getName())); + commentBlock.param(exampleMethod.getExample().getEntryName(), + exampleMethod.getExample().getEntryDescription()); + }); + String methodSignature = exampleMethod.getMethodSignature(); + if (exampleMethod.getHelperFeatures().contains(ExampleHelperFeature.ThrowsIOException)) { + methodSignature += " throws IOException"; + } + classBlock.publicStaticMethod(methodSignature, methodBlock -> { + methodBlock.line(exampleMethod.getMethodContent()); + }); + } + + if (helperFeatures.contains(ExampleHelperFeature.MapOfMethod)) { + ModelExampleWriter.writeMapOfMethod(classBlock); + } + }); + } + + private List getExampleMethods(com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentExample example) { + List exampleMethods = new ArrayList<>(); + exampleMethods.addAll( + example.getResourceCreateExamples().stream() + .map(this::generateExampleMethod) + .collect(Collectors.toList())); + exampleMethods.addAll( + example.getResourceUpdateExamples().stream() + .map(this::generateExampleMethod) + .collect(Collectors.toList())); + exampleMethods.addAll( + example.getCollectionMethodExamples().stream() + .map(this::generateExampleMethod) + .collect(Collectors.toList())); + exampleMethods.addAll( + example.getClientMethodExamples().stream() + .map(this::generateExampleMethod) + .collect(Collectors.toList())); + return exampleMethods; + } + + private String getExampleTag(FluentExample example) { + return "x-ms-original-file: " + example.getOriginalFileName(); + } + + public ExampleMethod generateExampleMethod(FluentMethodExample methodExample) { + String methodName = CodeNamer.toCamelCase(CodeNamer.removeInvalidCharacters(methodExample.getName())); + String managerName = methodExample.getEntryName(); + + ExampleNodeVisitor visitor = new ExampleNodeVisitor(); + String parameterInvocations = methodExample.getParameters().stream() + .map(p -> visitor.accept(p.getExampleNode())) + .collect(Collectors.joining(", ")); + + String snippet = String.format("%1$s.%2$s.%3$s(%4$s);", + managerName, + methodExample.getMethodReference(), + methodExample.getMethodName(), + parameterInvocations); + + ExampleMethod exampleMethod = new ExampleMethod() + .setExample(methodExample) + .setImports(visitor.getImports()) + .setMethodSignature(String.format("void %1$s(%2$s %3$s)", methodName, methodExample.getEntryType().getFullName(), managerName)) + .setMethodContent(snippet) + .setHelperFeatures(visitor.getHelperFeatures()); + return exampleMethod; + } + + public ExampleMethod generateExampleMethod(FluentResourceCreateExample resourceCreateExample) { + String methodName = CodeNamer.toCamelCase(CodeNamer.removeInvalidCharacters(resourceCreateExample.getName())); + String managerName = resourceCreateExample.getEntryName(); + + ExampleNodeVisitor visitor = new ExampleNodeVisitor(); + StringBuilder sb = new StringBuilder(managerName) + .append(".").append(CodeNamer.toCamelCase(resourceCreateExample.getResourceCollection().getInterfaceType().getName())).append("()"); + for (ParameterExample parameter : resourceCreateExample.getParameters()) { + String parameterInvocations = parameter.getExampleNodes().stream() + .map(visitor::accept) + .collect(Collectors.joining(", ")); + if (parameter.getExampleNodes().size() == 1 && parameterInvocations.equals("null")) { + // more likely this is an invalid example, as these properties/parameters is required and cannot be null + + IType clientType = parameter.getExampleNodes().iterator().next().getClientType(); + if (clientType instanceof PrimitiveType) { + // for primitive type, use language default value + parameterInvocations = String.format("%1$s", clientType.defaultValueExpression()); + } else { + // avoid ambiguous type on "null" + parameterInvocations = String.format("(%1$s) %2$s", clientType, parameterInvocations); + } + } + sb.append(".").append(parameter.getFluentMethod().getName()) + .append("(").append(parameterInvocations).append(")"); + } + sb.append(".create();"); + + ExampleMethod exampleMethod = new ExampleMethod() + .setExample(resourceCreateExample) + .setImports(visitor.getImports()) + .setMethodSignature(String.format("void %1$s(%2$s %3$s)", methodName, FluentStatic.getFluentManager().getType().getFullName(), managerName)) + .setMethodContent(sb.toString()) + .setHelperFeatures(visitor.getHelperFeatures()); + return exampleMethod; + } + + public ExampleMethod generateExampleMethod(FluentResourceUpdateExample resourceUpdateExample) { + String methodName = CodeNamer.toCamelCase(CodeNamer.removeInvalidCharacters(resourceUpdateExample.getName())); + String managerName = resourceUpdateExample.getEntryName(); + + ExampleNodeVisitor visitor = new ExampleNodeVisitor(); + + FluentCollectionMethodExample resourceGetExample = resourceUpdateExample.getResourceGetExample(); + String parameterInvocations = resourceGetExample.getParameters().stream() + .map(p -> visitor.accept(p.getExampleNode())) + .collect(Collectors.joining(", ")); + + String resourceGetSnippet = String.format("%1$s %2$s = %3$s.%4$s().%5$s(%6$s).getValue();\n", + resourceUpdateExample.getResourceUpdate().getResourceModel().getInterfaceType().getName(), + "resource", + managerName, + CodeNamer.toCamelCase(resourceGetExample.getResourceCollection().getInterfaceType().getName()), + resourceGetExample.getCollectionMethod().getMethodName(), + parameterInvocations); + + StringBuilder sb = new StringBuilder(resourceGetSnippet); + sb.append("resource").append(".update()"); + for (ParameterExample parameter : resourceUpdateExample.getParameters()) { + parameterInvocations = parameter.getExampleNodes().stream() + .map(visitor::accept) + .collect(Collectors.joining(", ")); + sb.append(".").append(parameter.getFluentMethod().getName()) + .append("(").append(parameterInvocations).append(")"); + } + sb.append(".apply();"); + + resourceUpdateExample.getResourceUpdate().getResourceModel().getInterfaceType().addImportsTo(visitor.getImports(), false); + + ExampleMethod exampleMethod = new ExampleMethod() + .setExample(resourceUpdateExample) + .setImports(visitor.getImports()) + .setMethodSignature(String.format("void %1$s(%2$s %3$s)", methodName, FluentStatic.getFluentManager().getType().getFullName(), managerName)) + .setMethodContent(sb.toString()) + .setHelperFeatures(visitor.getHelperFeatures()); + return exampleMethod; + } + + private static class ExampleNodeVisitor extends ModelExampleWriter.ExampleNodeModelInitializationVisitor { + + @Override + protected String codeDeserializeJsonString(String jsonStr) { + imports.add(com.azure.core.management.serializer.SerializerFactory.class.getName()); + imports.add(com.azure.core.util.serializer.SerializerEncoding.class.getName()); + imports.add(java.io.IOException.class.getName()); + + return String.format("SerializerFactory.createDefaultManagementSerializerAdapter().deserialize(%s, Object.class, SerializerEncoding.JSON)", + ClassType.STRING.defaultValueExpression(jsonStr)); + } + } + + public static class ExampleMethod { + private FluentExample example; + private Set imports; + private String methodSignature; + private String methodContent; + private Set helperFeatures; + + public FluentExample getExample() { + return example; + } + + public ExampleMethod setExample(FluentExample example) { + this.example = example; + return this; + } + + public Set getImports() { + return imports; + } + + public ExampleMethod setImports(Set imports) { + this.imports = imports; + return this; + } + + private String getMethodSignature() { + return methodSignature; + } + + private ExampleMethod setMethodSignature(String methodSignature) { + this.methodSignature = methodSignature; + return this; + } + + String getMethodContent() { + return methodContent; + } + + private ExampleMethod setMethodContent(String methodContent) { + this.methodContent = methodContent; + return this; + } + + public Set getHelperFeatures() { + return helperFeatures; + } + + public ExampleMethod setHelperFeatures(Set helperFeatures) { + this.helperFeatures = helperFeatures; + return this; + } + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/template/FluentLiveTestsTemplate.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/template/FluentLiveTestsTemplate.java new file mode 100644 index 0000000000..9d3784312f --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/template/FluentLiveTestsTemplate.java @@ -0,0 +1,81 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.template; + +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentExampleLiveTestStep; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentLiveTestCase; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentLiveTestStep; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentLiveTests; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.examplemodel.ExampleHelperFeature; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaFile; +import com.microsoft.typespec.http.client.generator.core.template.example.ModelExampleWriter; +import com.azure.core.util.CoreUtils; + +import java.util.ArrayList; + +public class FluentLiveTestsTemplate { + + private static final FluentLiveTestsTemplate INSTANCE = new FluentLiveTestsTemplate(); + + public static FluentLiveTestsTemplate getInstance(){ + return INSTANCE; + } + + public void write(FluentLiveTests liveTests, JavaFile javaFile) { + // write class + addImports(liveTests, javaFile); + javaFile.publicClass(new ArrayList<>(), liveTests.getClassName() + " extends TestBase", classBlock->{ + for (FluentLiveTestCase testCase : liveTests.getTestCases()) { + // write manager field + classBlock.privateMemberVariable(liveTests.getManagerType().getName(), liveTests.getManagerName()); + // write setup + classBlock.annotation("Override"); + classBlock.publicMethod("void beforeTest()", methodBlock -> methodBlock.line( + String.format( + "%s = %s.configure().withLogOptions(new HttpLogOptions().setLogLevel(HttpLogDetailLevel.BASIC))" + + ".authenticate(" + + "new DefaultAzureCredentialBuilder().build(), new AzureProfile(AzureEnvironment.AZURE)" + + ");", liveTests.getManagerName(), liveTests.getManagerType().getName()) + )); + // write method signature + if (!CoreUtils.isNullOrEmpty(testCase.getDescription())) { + classBlock.javadocComment(testCase.getDescription()); + } + classBlock.annotation("Test"); + classBlock.annotation("DoNotRecord(skipInPlayback = true)"); + String methodSignature = String.format("%s %s()", "void", getTestMethodName(testCase.getMethodName())); + if (testCase.getHelperFeatures().contains(ExampleHelperFeature.ThrowsIOException)) { + methodSignature += " throws IOException"; + } + classBlock.publicMethod(methodSignature, methodBlock -> { + for (FluentLiveTestStep step : testCase.getSteps()) { + if (step instanceof FluentExampleLiveTestStep) { + if (!CoreUtils.isNullOrEmpty(step.getDescription())) { + methodBlock.line("// " + step.getDescription()); + } + FluentExampleLiveTestStep exampleStep = (FluentExampleLiveTestStep) step; + methodBlock.line(exampleStep.getExampleMethod().getMethodContent()); + } + methodBlock.line(); + } + }); + } + if (liveTests.getHelperFeatures().contains(ExampleHelperFeature.MapOfMethod)) { + ModelExampleWriter.writeMapOfMethod(classBlock); + } + }); + } + private String getTestMethodName(String methodName) { + return methodName.endsWith("Test") ? methodName : methodName + "Test"; + } + + private void addImports(FluentLiveTests liveTests, JavaFile javaFile) { + javaFile.declareImport(liveTests.getImports()); + javaFile.declareImport(liveTests.getManagerType().getFullName()); + javaFile.declareImport("org.junit.jupiter.api.Test", "org.junit.jupiter.api.BeforeEach"); + javaFile.declareImport("com.azure.identity.DefaultAzureCredentialBuilder", "com.azure.core.management.profile.AzureProfile", "com.azure.core.management.AzureEnvironment"); + javaFile.declareImport("com.azure.core.test.annotation.DoNotRecord", "com.azure.core.test.TestBase"); + javaFile.declareImport("com.azure.core.http.policy.HttpLogOptions", "com.azure.core.http.policy.HttpLogDetailLevel"); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/template/FluentManagerTemplate.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/template/FluentManagerTemplate.java new file mode 100644 index 0000000000..baf746c0e3 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/template/FluentManagerTemplate.java @@ -0,0 +1,239 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.template; + +import com.microsoft.typespec.http.client.generator.core.extension.plugin.PluginLogger; +import com.microsoft.typespec.http.client.generator.mgmt.FluentGen; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentManager; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.ModelNaming; +import com.microsoft.typespec.http.client.generator.mgmt.model.projectmodel.FluentProject; +import com.microsoft.typespec.http.client.generator.mgmt.util.FluentUtils; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClassType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.IType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ServiceClient; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ServiceClientProperty; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaFile; +import com.microsoft.typespec.http.client.generator.core.util.ClientModelUtil; +import com.microsoft.typespec.http.client.generator.core.util.TemplateUtil; +import com.azure.core.credential.TokenCredential; +import com.azure.core.http.HttpClient; +import com.azure.core.http.HttpPipeline; +import com.azure.core.http.HttpPipelineBuilder; +import com.azure.core.http.HttpPipelinePosition; +import com.azure.core.http.policy.AddDatePolicy; +import com.azure.core.http.policy.AddHeadersFromContextPolicy; +import com.azure.core.http.policy.HttpLogOptions; +import com.azure.core.http.policy.HttpLoggingPolicy; +import com.azure.core.http.policy.HttpPipelinePolicy; +import com.azure.core.http.policy.HttpPolicyProviders; +import com.azure.core.http.policy.RequestIdPolicy; +import com.azure.core.http.policy.RetryOptions; +import com.azure.core.http.policy.RetryPolicy; +import com.azure.core.http.policy.UserAgentPolicy; +import com.azure.core.management.http.policy.ArmChallengeAuthenticationPolicy; +import com.azure.core.management.profile.AzureProfile; +import com.azure.core.util.Configuration; +import com.azure.core.util.logging.ClientLogger; +import org.slf4j.Logger; + +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +public class FluentManagerTemplate { + + private static final Logger LOGGER = new PluginLogger(FluentGen.getPluginInstance(), FluentManagerTemplate.class); + + private static final FluentManagerTemplate INSTANCE = new FluentManagerTemplate(); + + public static FluentManagerTemplate getInstance() { + return INSTANCE; + } + + public void write(FluentManager manager, FluentProject project, JavaFile javaFile) { + ServiceClient serviceClient = manager.getClient().getServiceClient(); + + final boolean hasEndpointParameter = serviceClient.getProperties().stream() + .anyMatch(p -> p.getName().equals("endpoint")); + if (!hasEndpointParameter) { + LOGGER.warn("'endpoint' (or '$host') is required in ServiceClient properties, candidate properties {}", + serviceClient.getProperties().stream().map(ServiceClientProperty::getName).collect(Collectors.toList())); + } + + final boolean endpointAvailable = serviceClient.getProperties().stream() + .anyMatch(p -> p.getName().equals("endpoint")); + final boolean requiresSubscriptionIdParameter = serviceClient.getProperties().stream() + .anyMatch(p -> p.getName().equals("subscriptionId")); + final IType subscriptionIdParameterType = serviceClient.getProperties().stream() + .filter(p -> p.getName().equals("subscriptionId")) + .map(ServiceClientProperty::getType) + .findFirst().orElse(null); + + String builderPackageName = ClientModelUtil.getServiceClientBuilderPackageName(serviceClient); + String builderTypeName = serviceClient.getInterfaceName() + ClientModelUtil.getBuilderSuffix(); + String serviceClientPackageName = ClientModelUtil.getServiceClientInterfacePackageName(); + String serviceClientTypeName = serviceClient.getInterfaceName(); + + String managerName = manager.getType().getName(); + + Set imports = new HashSet<>(Arrays.asList( + // java + Objects.class.getName(), + Duration.class.getName(), + ChronoUnit.class.getName(), + List.class.getName(), + ArrayList.class.getName(), + Collectors.class.getName(), + // azure-core + TokenCredential.class.getName(), + ClientLogger.class.getName(), + Configuration.class.getName(), + HttpClient.class.getName(), + HttpPipeline.class.getName(), + HttpPipelineBuilder.class.getName(), + HttpPipelinePolicy.class.getName(), + HttpPipelinePosition.class.getName(), + HttpPolicyProviders.class.getName(), + RetryOptions.class.getName(), + AddHeadersFromContextPolicy.class.getName(), + RequestIdPolicy.class.getName(), + RetryPolicy.class.getName(), + AddDatePolicy.class.getName(), + HttpLoggingPolicy.class.getName(), + HttpLogOptions.class.getName(), + ArmChallengeAuthenticationPolicy.class.getName(), + UserAgentPolicy.class.getName(), + // azure-core-management + AzureProfile.class.getName() + )); + + if (requiresSubscriptionIdParameter && subscriptionIdParameterType != null) { + subscriptionIdParameterType.addImportsTo(imports, false); + } + + imports.add(String.format("%1$s.%2$s", builderPackageName, builderTypeName)); + imports.add(String.format("%1$s.%2$s", serviceClientPackageName, serviceClientTypeName)); + + manager.getProperties().forEach(property -> { + imports.add(property.getFluentType().getFullName()); + imports.add(property.getFluentImplementType().getFullName()); + }); + javaFile.declareImport(imports); + + javaFile.javadocComment(comment -> { + comment.description(manager.getDescription()); + }); + + javaFile.publicFinalClass(managerName, classBlock -> { + manager.getProperties().forEach(property -> { + classBlock.privateMemberVariable(property.getFluentType().getName(), property.getName()); + }); + + classBlock.privateFinalMemberVariable(serviceClientTypeName, ModelNaming.MANAGER_PROPERTY_CLIENT); + + // Constructor + classBlock.privateConstructor(String.format("%1$s(HttpPipeline httpPipeline, AzureProfile profile, Duration defaultPollInterval)", managerName) , methodBlock -> { + methodBlock.line("Objects.requireNonNull(httpPipeline, \"'httpPipeline' cannot be null.\");"); + methodBlock.line("Objects.requireNonNull(profile, \"'profile' cannot be null.\");"); + methodBlock.line(String.format("this.%1$s = new %2$s()", ModelNaming.MANAGER_PROPERTY_CLIENT, builderTypeName)); + methodBlock.indent(() -> { + methodBlock.line(".pipeline(httpPipeline)"); + if (endpointAvailable) { + methodBlock.line(".endpoint(profile.getEnvironment().getResourceManagerEndpoint())"); + } + if (requiresSubscriptionIdParameter) { + if (subscriptionIdParameterType == ClassType.UUID) { + methodBlock.line(".subscriptionId(UUID.fromString(profile.getSubscriptionId()))"); + } else { + methodBlock.line(".subscriptionId(profile.getSubscriptionId())"); + } + } + methodBlock.line(".defaultPollInterval(defaultPollInterval)"); + methodBlock.line(".buildClient();"); + }); + }); + + // authenticate() + classBlock.javadocComment(comment -> { + comment.description(String.format("Creates an instance of %1$s service API entry point.", manager.getServiceName())); + comment.param("credential", "the credential to use"); + comment.param("profile", "the Azure profile for client"); + comment.methodReturns(String.format("the %1$s service API instance", manager.getServiceName())); + }); + classBlock.publicStaticMethod(String.format("%1$s authenticate(TokenCredential credential, AzureProfile profile)", managerName), methodBlock -> { + methodBlock.line("Objects.requireNonNull(credential, \"'credential' cannot be null.\");"); + methodBlock.line("Objects.requireNonNull(profile, \"'profile' cannot be null.\");"); + methodBlock.methodReturn("configure().authenticate(credential, profile)"); + }); + + classBlock.javadocComment(comment -> { + comment.description(String.format("Creates an instance of %1$s service API entry point.", manager.getServiceName())); + comment.param("httpPipeline", "the {@link HttpPipeline} configured with Azure authentication credential"); + comment.param("profile", "the Azure profile for client"); + comment.methodReturns(String.format("the %1$s service API instance", manager.getServiceName())); + }); + classBlock.publicStaticMethod(String.format("%1$s authenticate(HttpPipeline httpPipeline, AzureProfile profile)", managerName), methodBlock -> { + methodBlock.line("Objects.requireNonNull(httpPipeline, \"'httpPipeline' cannot be null.\");"); + methodBlock.line("Objects.requireNonNull(profile, \"'profile' cannot be null.\");"); + methodBlock.methodReturn(String.format("new %1$s(httpPipeline, profile, null)", managerName)); + }); + + // configure() + classBlock.javadocComment(comment -> { + comment.description(String.format("Gets a Configurable instance that can be used to create %1$s with optional configuration.", managerName)); + comment.methodReturns("the Configurable instance allowing configurations"); + }); + classBlock.publicStaticMethod("Configurable configure()", methodBlock -> { + methodBlock.methodReturn(String.format("new %1$s.Configurable()", managerName)); + }); + + // Configurable class + javaFile.line(); + String configurableClassText = FluentUtils.loadTextFromResource("Manager_Configurable.txt", + TemplateUtil.SERVICE_NAME, manager.getServiceName(), + TemplateUtil.MANAGER_CLASS, manager.getType().getName(), + TemplateUtil.PACKAGE_NAME, project.getNamespace(), + TemplateUtil.ARTIFACT_VERSION, project.getVersion() + ); + javaFile.text(configurableClassText); + + manager.getProperties().forEach(property -> { + classBlock.javadocComment(comment -> { + String resourceModelsDescription = ""; + if (!property.getResourceModelTypes().isEmpty()) { + resourceModelsDescription = " It manages " + property.getResourceModelTypes().stream() + .map(ClassType::getName).collect(Collectors.joining(", ")) + "."; + } + comment.description(String.format("Gets the resource collection API of %1$s.", property.getFluentType().getName()) + + resourceModelsDescription); + comment.methodReturns(String.format("Resource collection API of %1$s.", property.getFluentType().getName())); + }); + + classBlock.publicMethod(String.format("%1$s %2$s()", property.getFluentType().getName(), property.getMethodName()), methodBlock -> { + methodBlock.ifBlock(String.format("this.%1$s == null", property.getName()), ifBlock -> { + methodBlock.line(String.format("this.%1$s = new %2$s(%3$s.%4$s(), this);", + property.getName(), + property.getFluentImplementType().getName(), + ModelNaming.MANAGER_PROPERTY_CLIENT, property.getInnerClientGetMethod())); + }); + methodBlock.methodReturn(property.getName()); + }); + }); + + classBlock.javadocComment(comment -> { + comment.description(String.format("Gets wrapped service client %1$s providing direct access to the underlying auto-generated API implementation, based on Azure REST API.", serviceClientTypeName)); + comment.methodReturns(String.format("Wrapped service client %1$s.", serviceClientTypeName)); + }); + classBlock.publicMethod(String.format("%1$s %2$s()", serviceClientTypeName, ModelNaming.METHOD_SERVICE_CLIENT), methodBlock -> { + methodBlock.methodReturn(String.format("this.%1$s", ModelNaming.MANAGER_PROPERTY_CLIENT)); + }); + }); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/template/FluentMethodMockTestTemplate.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/template/FluentMethodMockTestTemplate.java new file mode 100644 index 0000000000..8dc987bfb4 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/template/FluentMethodMockTestTemplate.java @@ -0,0 +1,173 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.template; + +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.examplemodel.FluentMethodMockUnitTest; +import com.microsoft.typespec.http.client.generator.mgmt.util.FluentUtils; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClassType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethod; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.IType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.examplemodel.ExampleHelperFeature; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.examplemodel.ExampleNode; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaFile; +import com.microsoft.typespec.http.client.generator.core.template.IJavaTemplate; +import com.microsoft.typespec.http.client.generator.core.template.example.ModelExampleWriter; +import com.microsoft.typespec.http.client.generator.core.util.CodeNamer; +import com.azure.core.credential.AccessToken; +import com.azure.core.http.HttpResponse; +import com.azure.core.management.profile.AzureProfile; +import com.azure.json.JsonProviders; +import com.azure.json.JsonWriter; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.time.OffsetDateTime; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +public class FluentMethodMockTestTemplate + implements IJavaTemplate { + + public static class ClientMethodInfo { + private final String className; + + private final FluentMethodMockUnitTest fluentMethodMockUnitTest; + + public ClientMethodInfo(String className, FluentMethodMockUnitTest fluentMethodMockUnitTest) { + this.className = className; + this.fluentMethodMockUnitTest = fluentMethodMockUnitTest; + } + } + + private static final FluentMethodMockTestTemplate INSTANCE = new FluentMethodMockTestTemplate(); + + private FluentMethodMockTestTemplate() { + } + + public static FluentMethodMockTestTemplate getInstance() { + return INSTANCE; + } + + @Override + public void write(ClientMethodInfo info, JavaFile javaFile) { + Set imports = new HashSet<>( + Arrays.asList(AccessToken.class.getName(), ClassType.HTTP_CLIENT.getFullName(), + ClassType.HTTP_HEADERS.getFullName(), ClassType.HTTP_REQUEST.getFullName(), + HttpResponse.class.getName(), "com.azure.core.test.http.MockHttpResponse", + ClassType.AZURE_ENVIRONMENT.getFullName(), AzureProfile.class.getName(), "org.junit.jupiter.api.Test", + ByteBuffer.class.getName(), Mono.class.getName(), Flux.class.getName(), + StandardCharsets.class.getName(), OffsetDateTime.class.getName())); + + String className = info.className; + FluentMethodMockUnitTest fluentMethodMockUnitTest = info.fluentMethodMockUnitTest; + ClientMethod clientMethod = fluentMethodMockUnitTest.getCollectionMethod().getInnerClientMethod(); + IType fluentReturnType = fluentMethodMockUnitTest.getFluentReturnType(); + final boolean isResponseType = FluentUtils.isResponseType(fluentReturnType); + if (isResponseType) { + fluentReturnType = FluentUtils.getValueTypeFromResponseType(fluentReturnType); + } + final boolean hasReturnValue = fluentReturnType.asNullable() != ClassType.VOID; + + // method invocation + String clientMethodInvocationWithResponse; + FluentExampleTemplate.ExampleMethod exampleMethod; + if (fluentMethodMockUnitTest.getFluentResourceCreateExample() != null) { + exampleMethod = FluentExampleTemplate.getInstance() + .generateExampleMethod(fluentMethodMockUnitTest.getFluentResourceCreateExample()); + } else if (fluentMethodMockUnitTest.getFluentMethodExample() != null) { + exampleMethod = FluentExampleTemplate.getInstance() + .generateExampleMethod(fluentMethodMockUnitTest.getFluentMethodExample()); + } else { + throw new IllegalStateException(); + } + String clientMethodInvocation = exampleMethod.getMethodContent(); + if (hasReturnValue) { + // hack on replaceResponseForValue, as in "update" case, "exampleMethod.getMethodContent()" would be a code + // block, not a single line of code invocation. + clientMethodInvocationWithResponse = fluentReturnType + " response = " + (isResponseType + ? replaceResponseForValue(clientMethodInvocation) + : clientMethodInvocation); + } else { + clientMethodInvocationWithResponse = clientMethodInvocation; + } + imports.addAll(exampleMethod.getImports()); + exampleMethod.getExample().getEntryType().addImportsTo(imports, false); + fluentReturnType.addImportsTo(imports, false); + + // create response body with mocked data + int statusCode = fluentMethodMockUnitTest.getResponse().getStatusCode(); + Object jsonObject = fluentMethodMockUnitTest.getResponse().getBody(); + ExampleNode verificationNode = fluentMethodMockUnitTest.getResponseVerificationNode(); + String verificationObjectName = fluentMethodMockUnitTest.getResponseVerificationVariableName(); + String jsonStr; + try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + JsonWriter jsonWriter = JsonProviders.createWriter(outputStream)) { + jsonWriter.writeUntyped(jsonObject).flush(); + jsonStr = outputStream.toString(StandardCharsets.UTF_8); + } catch (IOException e) { + throw new IllegalStateException("Failed to serialize Object to JSON string", e); + } + + // prepare assertions + ModelExampleWriter.ExampleNodeAssertionVisitor assertionVisitor + = new ModelExampleWriter.ExampleNodeAssertionVisitor(); + if (hasReturnValue) { + imports.add("org.junit.jupiter.api.Assertions"); + + assertionVisitor.accept(verificationNode, verificationObjectName); + imports.addAll(assertionVisitor.getImports()); + } + + javaFile.declareImport(imports); + + javaFile.publicFinalClass(className, classBlock -> { + classBlock.annotation("Test"); + classBlock.publicMethod( + "void test" + CodeNamer.toPascalCase(clientMethod.getName()) + "() throws Exception", + methodBlock -> { + // response + methodBlock.line("String responseStr = " + ClassType.STRING.defaultValueExpression(jsonStr) + ";"); + methodBlock.line(); + + // prepare mock class + methodBlock.line( + "HttpClient httpClient = response -> Mono.just(new MockHttpResponse(response, " + statusCode + + ", responseStr.getBytes(StandardCharsets.UTF_8)));"); + + // initialize manager + String exampleMethodName = exampleMethod.getExample().getEntryType().getName(); + methodBlock.line(exampleMethodName + " manager = " + exampleMethodName + ".configure()" + + ".withHttpClient(httpClient).authenticate(tokenRequestContext -> " + + "Mono.just(new AccessToken(\"this_is_a_token\", OffsetDateTime.MAX)), " + + "new AzureProfile(\"\", \"\", AzureEnvironment.AZURE));"); + methodBlock.line(); + // method invocation + methodBlock.line(clientMethodInvocationWithResponse); + methodBlock.line(); + // verification + if (hasReturnValue) { + assertionVisitor.getAssertions().forEach(methodBlock::line); + } + }); + + // helper method + if (exampleMethod.getHelperFeatures().contains(ExampleHelperFeature.MapOfMethod)) { + ModelExampleWriter.writeMapOfMethod(classBlock); + } + }); + } + + private static String replaceResponseForValue(String clientMethodInvocation) { + if (clientMethodInvocation.endsWith(";")) { + clientMethodInvocation = clientMethodInvocation.substring(0, clientMethodInvocation.length() - 1); + clientMethodInvocation += ".getValue();"; + } + return clientMethodInvocation; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/template/FluentModelTemplate.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/template/FluentModelTemplate.java new file mode 100644 index 0000000000..ec0ae61cf3 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/template/FluentModelTemplate.java @@ -0,0 +1,139 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.template; + +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; +import com.microsoft.typespec.http.client.generator.mgmt.model.FluentType; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentStatic; +import com.microsoft.typespec.http.client.generator.mgmt.util.FluentUtils; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientModel; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientModelProperty; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientModelPropertyReference; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.MapType; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaClass; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaContext; +import com.microsoft.typespec.http.client.generator.core.template.ModelTemplate; +import com.microsoft.typespec.http.client.generator.core.util.ModelNamer; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +public class FluentModelTemplate extends ModelTemplate { + + private static final FluentModelTemplate INSTANCE = new FluentModelTemplate(); + + private static ModelNamer modelNamer; + + protected FluentModelTemplate() { + } + + public static FluentModelTemplate getInstance() { + return INSTANCE; + } + + @Override + protected void addSerializationImports(Set imports, ClientModel model, JavaSettings settings) { + super.addSerializationImports(imports, model, settings); + + imports.add("com.fasterxml.jackson.annotation.JsonInclude"); + } + + @Override + protected void addFieldAnnotations(ClientModel model, ClientModelProperty property, JavaClass classBlock, JavaSettings settings) { + super.addFieldAnnotations(model, property, classBlock, settings); + + // JsonInclude + if (!property.isAdditionalProperties()) { + String propertyName = model.getName() + "." + property.getName(); + Set propertiesAllowNull = FluentStatic.getFluentJavaSettings().getJavaNamesForPropertyIncludeAlways(); + final boolean propertyAllowNull = propertiesAllowNull.contains(propertyName); + + if (property.getClientType() instanceof MapType) { + String value = propertyAllowNull ? "JsonInclude.Include.ALWAYS" : "JsonInclude.Include.NON_NULL"; + classBlock.annotation(String.format("JsonInclude(value = %1$s, content = JsonInclude.Include.ALWAYS)", value)); + } else { + if (propertyAllowNull) { + classBlock.annotation("JsonInclude(value = JsonInclude.Include.ALWAYS)"); + } + } + } + } + + @Override + protected boolean parentModelHasValidate(String parentModelName) { + return parentModelName != null + && FluentType.nonResourceType(parentModelName) + && FluentType.nonManagementError(parentModelName); + } + + @Override + protected String getGetterName(ClientModel model, ClientModelProperty property) { + if (FluentType.MANAGEMENT_ERROR.getName().equals(model.getParentModelName())) { + // subclass of ManagementError + + if (modelNamer == null) { + modelNamer = new ModelNamer(); + } + return modelNamer.modelPropertyGetterName(property); + + // disabled for now, as e.g. https://github.com/Azure/azure-rest-api-specs/blob/8fa9b5051129dd4808c9be1f5b753af226b044db/specification/iothub/resource-manager/Microsoft.Devices/stable/2023-06-30/iothub.json#L298-L303 makes it usage=output +// if (model.getImplementationDetails() != null +// && model.getImplementationDetails().isException() +// && !model.getImplementationDetails().isOutput() +// && !model.getImplementationDetails().isInput()) { +// // model used in Exception, also not in any non-Exception input or output +// +// if (modelNamer == null) { +// modelNamer = new ModelNamer(); +// } +// return modelNamer.modelPropertyGetterName(property); +// } else { +// return super.getGetterName(model, property); +// } + } else { + return super.getGetterName(model, property); + } + } + + @Override + protected List getClientModelPropertyReferences(ClientModel model) { + List propertyReferences = new ArrayList<>(); + + String lastParentName = model.getName(); + String parentModelName = model.getParentModelName(); + while (parentModelName != null && !lastParentName.equals(parentModelName)) { + ClientModel parentModel = FluentUtils.getClientModel(parentModelName); + if (parentModel != null) { + if (parentModel.getProperties() != null) { + propertyReferences.addAll(parentModel.getProperties().stream() + .filter(p -> !p.getClientFlatten() && !p.isAdditionalProperties()) + .map(ClientModelPropertyReference::ofParentProperty) + .collect(Collectors.toList())); + } + + if (parentModel.getPropertyReferences() != null) { + propertyReferences.addAll(parentModel.getPropertyReferences().stream() + .filter(ClientModelPropertyReference::isFromFlattenedProperty) + .map(ClientModelPropertyReference::ofParentProperty) + .collect(Collectors.toList())); + } + } + + lastParentName = parentModelName; + parentModelName = parentModel == null ? null : parentModel.getParentModelName(); + } + + return propertyReferences; + } + + @Override + protected void addGeneratedImport(Set imports) { + } + + @Override + protected void addGeneratedAnnotation(JavaContext classBlock) { + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/template/FluentPomTemplate.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/template/FluentPomTemplate.java new file mode 100644 index 0000000000..54cf82a829 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/template/FluentPomTemplate.java @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.template; + +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.Pom; +import com.microsoft.typespec.http.client.generator.core.model.xmlmodel.XmlBlock; +import com.microsoft.typespec.http.client.generator.core.template.PomTemplate; + +public class FluentPomTemplate extends PomTemplate { + + private static final FluentPomTemplate INSTANCE = new FluentPomTemplate(); + + protected FluentPomTemplate() { + } + + public static FluentPomTemplate getInstance() { + return INSTANCE; + } + + @Override + protected void writeJacoco(XmlBlock propertiesBlock) { + super.writeJacoco(propertiesBlock); + + propertiesBlock.tag("jacoco.min.linecoverage", "0"); + propertiesBlock.tag("jacoco.min.branchcoverage", "0"); + } + + @Override + protected void writeRevapi(XmlBlock propertiesBlock, Pom pom) { + super.writeRevapi(propertiesBlock, pom); + + // skip revapi if preview + if (pom.getVersion().contains("-beta.")) { + propertiesBlock.tag("revapi.skip", "true"); + } + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/template/FluentProxyTemplate.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/template/FluentProxyTemplate.java new file mode 100644 index 0000000000..f645fae591 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/template/FluentProxyTemplate.java @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.template; + +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.RequestParameterLocation; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ProxyMethod; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ProxyMethodParameter; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaInterface; +import com.microsoft.typespec.http.client.generator.core.template.ProxyTemplate; + +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +public class FluentProxyTemplate extends ProxyTemplate { + + private static final FluentProxyTemplate INSTANCE = new FluentProxyTemplate(); + + public static FluentProxyTemplate getInstance() { + return INSTANCE; + } + + @Override + protected void writeProxyMethodHeaders(ProxyMethod restAPIMethod, JavaInterface interfaceBlock) { + Map headers = new HashMap<>(); + headers.put("Content-Type", restAPIMethod.getRequestContentType()); + headers.put("Accept", String.join(",", restAPIMethod.getResponseContentTypes())); + + Set headerParameterNames = restAPIMethod.getParameters().stream() + .filter(p -> p.getRequestParameterLocation() == RequestParameterLocation.HEADER) + .map(ProxyMethodParameter::getRequestParameterName) + .map(String::toLowerCase) + .collect(Collectors.toSet()); + + headers = headers.entrySet().stream() + .filter(e -> !headerParameterNames.contains(e.getKey().toLowerCase(Locale.ROOT))) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + + if (!headers.isEmpty()) { + String headersString = headers.entrySet().stream() + .map(e -> String.format("\"%s: %s\"", e.getKey(), e.getValue())) + .collect(Collectors.joining(", ")); + interfaceBlock.annotation(String.format("Headers({ %s })", + headersString)); + } + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/template/FluentResourceCollectionImplementationTemplate.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/template/FluentResourceCollectionImplementationTemplate.java new file mode 100644 index 0000000000..e8d3f8b116 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/template/FluentResourceCollectionImplementationTemplate.java @@ -0,0 +1,95 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.template; + +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentResourceCollection; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentStatic; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.ModelNaming; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.method.FluentDefineMethod; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.method.FluentMethod; +import com.microsoft.typespec.http.client.generator.mgmt.util.FluentUtils; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClassType; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaFile; +import com.microsoft.typespec.http.client.generator.core.template.IJavaTemplate; +import com.microsoft.typespec.http.client.generator.core.template.prototype.MethodTemplate; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class FluentResourceCollectionImplementationTemplate implements IJavaTemplate { + + private static final FluentResourceCollectionImplementationTemplate INSTANCE = new FluentResourceCollectionImplementationTemplate(); + + public static FluentResourceCollectionImplementationTemplate getInstance() { + return INSTANCE; + } + + @Override + public void write(FluentResourceCollection collection, JavaFile javaFile) { + ClassType managerType = FluentStatic.getFluentManager().getType(); + + Set imports = new HashSet<>(); + // ClientLogger + ClassType.CLIENT_LOGGER.addImportsTo(imports, false); + /* use full name for FooManager, to avoid naming conflict + // manager + imports.add(managerType.getFullName()); + */ + // resource collection + collection.addImportsTo(imports, true); + if (collection.getResourceCreates() != null) { + collection.getResourceCreates().forEach(rc -> rc.getDefineMethod().addImportsTo(imports, true)); + } + javaFile.declareImport(imports); + + List methodTemplates = new ArrayList<>(); + collection.getMethodsForTemplate().forEach(p -> methodTemplates.add(p.getImplementationMethodTemplate())); + methodTemplates.addAll(collection.getAdditionalMethods()); + + javaFile.publicFinalClass(String.format("%1$s implements %2$s", collection.getImplementationType().getName(), collection.getInterfaceType().getName()), classBlock -> { + // logger + classBlock.privateStaticFinalVariable(String.format("%1$s LOGGER = new ClientLogger(%2$s.class)", + ClassType.CLIENT_LOGGER, collection.getImplementationType().getName())); + + // variable for inner model + classBlock.privateFinalMemberVariable(collection.getInnerClientType().getName(), ModelNaming.COLLECTION_PROPERTY_INNER); + + // variable for manager + classBlock.privateFinalMemberVariable(managerType.getFullName(), ModelNaming.COLLECTION_PROPERTY_MANAGER); + + // constructor + classBlock.publicConstructor(String.format("%1$s(%2$s %3$s, %4$s %5$s)", collection.getImplementationType().getName(), collection.getInnerClientType().getName(), ModelNaming.COLLECTION_PROPERTY_INNER, managerType.getFullName(), ModelNaming.MODEL_PROPERTY_MANAGER), methodBlock -> { + methodBlock.line(String.format("this.%1$s = %2$s;", ModelNaming.COLLECTION_PROPERTY_INNER, ModelNaming.COLLECTION_PROPERTY_INNER)); + methodBlock.line(String.format("this.%1$s = %2$s;", ModelNaming.COLLECTION_PROPERTY_MANAGER, ModelNaming.COLLECTION_PROPERTY_MANAGER)); + }); + + // method for properties + methodTemplates.forEach(m -> m.writeMethodWithoutJavadoc(classBlock)); + + // method for inner model + classBlock.privateMethod(collection.getInnerMethodSignature(), methodBlock -> { + methodBlock.methodReturn(String.format("this.%s", ModelNaming.COLLECTION_PROPERTY_INNER)); + }); + + // method for manager + classBlock.privateMethod(String.format("%1$s %2$s()", managerType.getFullName(), FluentUtils.getGetterName(ModelNaming.METHOD_MANAGER)), methodBlock -> { + methodBlock.methodReturn(String.format("this.%s", ModelNaming.MODEL_PROPERTY_MANAGER)); + }); + + // method for define resource + int resourceCount = collection.getResourceCreates().size(); + collection.getResourceCreates() + .forEach(rc -> { + FluentMethod defineMethod = rc.getDefineMethod(); + if (resourceCount == 1) { + ((FluentDefineMethod) defineMethod).setName("define"); + } + + defineMethod.getMethodTemplate().writeMethod(classBlock); + }); + }); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/template/FluentResourceCollectionInterfaceTemplate.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/template/FluentResourceCollectionInterfaceTemplate.java new file mode 100644 index 0000000000..43b0a01151 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/template/FluentResourceCollectionInterfaceTemplate.java @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.template; + +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentResourceCollection; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.method.FluentDefineMethod; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaFile; +import com.microsoft.typespec.http.client.generator.core.template.ClientMethodTemplate; +import com.microsoft.typespec.http.client.generator.core.template.IJavaTemplate; + +import java.util.HashSet; +import java.util.Set; + +public class FluentResourceCollectionInterfaceTemplate implements IJavaTemplate { + + private static final FluentResourceCollectionInterfaceTemplate INSTANCE = new FluentResourceCollectionInterfaceTemplate(); + + public static FluentResourceCollectionInterfaceTemplate getInstance() { + return INSTANCE; + } + + @Override + public void write(FluentResourceCollection collection, JavaFile javaFile) { + Set imports = new HashSet<>(); + collection.addImportsTo(imports, false); + collection.getResourceCreates().forEach(rc -> rc.getDefineMethod().addImportsTo(imports, false)); + javaFile.declareImport(imports); + + javaFile.javadocComment(comment -> { + comment.description(collection.getDescription()); + }); + + javaFile.publicInterface(collection.getInterfaceType().getName(), interfaceBlock -> { + // methods + collection.getMethodsForTemplate().forEach(method -> { + ClientMethodTemplate.generateJavadoc(method.getInnerClientMethod(), interfaceBlock, method.getInnerProxyMethod(), true); + + interfaceBlock.publicMethod(method.getMethodSignature()); + }); + + collection.getAdditionalMethods().forEach(method -> method.writeMethodInterface(interfaceBlock)); + +// // method for inner client +// interfaceBlock.javadocComment(comment -> { +// comment.description(String.format("Gets the inner %s client", collection.getInnerClientType().getFullName())); +// comment.methodReturns("the inner client"); +// }); +// interfaceBlock.publicMethod(collection.getInnerMethodSignature()); + + // method for define resource + int resourceCount = collection.getResourceCreates().size(); + collection.getResourceCreates() + .forEach(rc -> { + FluentDefineMethod defineMethod = rc.getDefineMethod(); + if (resourceCount == 1) { + defineMethod.setName("define"); + } + + interfaceBlock.javadocComment(defineMethod::writeJavadoc); + interfaceBlock.publicMethod(defineMethod.getInterfaceMethodSignature()); + }); + }); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/template/FluentResourceModelImplementationTemplate.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/template/FluentResourceModelImplementationTemplate.java new file mode 100644 index 0000000000..630dd5a5b1 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/template/FluentResourceModelImplementationTemplate.java @@ -0,0 +1,101 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.template; + +import com.microsoft.typespec.http.client.generator.mgmt.model.arm.ModelCategory; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentResourceModel; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentStatic; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.ModelNaming; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.LocalVariable; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.ResourceImplementation; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.immutablemodel.ImmutableMethod; +import com.microsoft.typespec.http.client.generator.mgmt.util.FluentUtils; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClassType; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaFile; +import com.microsoft.typespec.http.client.generator.core.template.IJavaTemplate; +import com.microsoft.typespec.http.client.generator.core.template.prototype.MethodTemplate; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class FluentResourceModelImplementationTemplate implements IJavaTemplate { + + private static final FluentResourceModelImplementationTemplate INSTANCE = new FluentResourceModelImplementationTemplate(); + + public static FluentResourceModelImplementationTemplate getInstance() { + return INSTANCE; + } + + @Override + public void write(FluentResourceModel model, JavaFile javaFile) { + ClassType managerType = FluentStatic.getFluentManager().getType(); + + List methodTemplates = new ArrayList<>(); + model.getProperties().forEach(p -> methodTemplates.add(p.getImplementationMethodTemplate())); + methodTemplates.addAll(model.getAdditionalMethods()); + + Set imports = new HashSet<>(); + /* use full name for FooManager, to avoid naming conflict + // manager + imports.add(managerType.getFullName()); + */ + model.addImportsTo(imports, true); + javaFile.declareImport(imports); + + List implementInterfaces = new ArrayList<>(); + implementInterfaces.add(model.getInterfaceType().getName()); + if (model.getResourceCreate() != null) { + implementInterfaces.add(String.format("%1$s.%2$s", model.getInterfaceType().getName(), ModelNaming.MODEL_FLUENT_INTERFACE_DEFINITION)); + } + if (model.getResourceUpdate() != null) { + implementInterfaces.add(String.format("%1$s.%2$s", model.getInterfaceType().getName(), ModelNaming.MODEL_FLUENT_INTERFACE_UPDATE)); + } + + javaFile.publicFinalClass(String.format("%1$s implements %2$s", model.getImplementationType().getName(), String.join(", ", implementInterfaces)), classBlock -> { + // variable for inner model + classBlock.privateMemberVariable(model.getInnerModel().getName(), ModelNaming.MODEL_PROPERTY_INNER); + + // variable for manager + classBlock.privateFinalMemberVariable(managerType.getFullName(), ModelNaming.MODEL_PROPERTY_MANAGER); + + // if resource is updatable, use the constructor from resourceUpdate + if (model.getCategory() == ModelCategory.IMMUTABLE || model.getResourceUpdate() == null) { + // constructor + classBlock.packagePrivateConstructor(String.format("%1$s(%2$s %3$s, %4$s %5$s)", model.getImplementationType().getName(), model.getInnerModel().getName(), ModelNaming.MODEL_PROPERTY_INNER, managerType.getFullName(), ModelNaming.MODEL_PROPERTY_MANAGER), methodBlock -> { + methodBlock.line(String.format("this.%1$s = %2$s;", ModelNaming.MODEL_PROPERTY_INNER, ModelNaming.MODEL_PROPERTY_INNER)); + methodBlock.line(String.format("this.%1$s = %2$s;", ModelNaming.MODEL_PROPERTY_MANAGER, ModelNaming.MODEL_PROPERTY_MANAGER)); + }); + } + + // method for properties + methodTemplates.forEach(m -> m.writeMethodWithoutJavadoc(classBlock)); + + // method for inner model + classBlock.publicMethod(model.getInnerMethodSignature(), methodBlock -> { + methodBlock.methodReturn(String.format("this.%s", ModelNaming.MODEL_PROPERTY_INNER)); + }); + + // method for manager + classBlock.privateMethod(String.format("%1$s %2$s()", managerType.getFullName(), FluentUtils.getGetterName(ModelNaming.METHOD_MANAGER)), methodBlock -> { + methodBlock.methodReturn(String.format("this.%s", ModelNaming.MODEL_PROPERTY_MANAGER)); + }); + + // methods for fluent interfaces + // class variables + if (model.getCategory() != ModelCategory.IMMUTABLE) { + ResourceImplementation resourceImplementation = model.getResourceImplementation(); + List fluentMethods = resourceImplementation.getMethods(); + List localVariables = resourceImplementation.getLocalVariables(); + + localVariables.forEach(p -> classBlock.privateMemberVariable(p.getVariableType().toString(), p.getName())); + + fluentMethods.forEach(m -> { + m.getMethodTemplate().writeMethod(classBlock); + }); + } + }); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/template/FluentResourceModelInterfaceDefinitionTemplate.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/template/FluentResourceModelInterfaceDefinitionTemplate.java new file mode 100644 index 0000000000..a9bfa805ea --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/template/FluentResourceModelInterfaceDefinitionTemplate.java @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.template; + +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.ModelNaming; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.method.FluentMethod; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.create.DefinitionStage; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.create.ResourceCreate; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaInterface; +import com.microsoft.typespec.http.client.generator.core.template.IJavaTemplate; + +import java.util.List; +import java.util.stream.Collectors; + +public class FluentResourceModelInterfaceDefinitionTemplate implements IJavaTemplate { + + @Override + public void write(ResourceCreate resourceCreate, JavaInterface interfaceBlock) { + List definitionStages = resourceCreate.getDefinitionStages(); + + final String modelName = resourceCreate.getResourceModel().getInterfaceType().getName(); + + // Definition interface + interfaceBlock.javadocComment(commentBlock -> { + commentBlock.description(String.format("The entirety of the %1$s definition.", modelName)); + }); + String definitionInterfaceSignature = String.format("%1$s extends %2$s", + ModelNaming.MODEL_FLUENT_INTERFACE_DEFINITION, + definitionStages.stream() + .filter(DefinitionStage::isMandatoryStage) + .map(s -> String.format("%1$s.%2$s", ModelNaming.MODEL_FLUENT_INTERFACE_DEFINITION_STAGES, s.getName())) + .collect(Collectors.joining(", "))); + interfaceBlock.interfaceBlock(definitionInterfaceSignature, block1 -> { + }); + + // DefinitionStages interface + interfaceBlock.javadocComment(commentBlock -> { + commentBlock.description(String.format("The %1$s definition stages.", modelName)); + }); + interfaceBlock.interfaceBlock(ModelNaming.MODEL_FLUENT_INTERFACE_DEFINITION_STAGES, block1 -> { + for (DefinitionStage stage : definitionStages) { + block1.javadocComment(commentBlock -> { + commentBlock.description(stage.getDescription(modelName)); + }); + String interfaceSignature = stage.getName(); + if (stage.getExtendStages() != null) { + interfaceSignature += " extends " + stage.getExtendStages(); + } + block1.interfaceBlock(interfaceSignature, block2 -> { + for (FluentMethod method : stage.getMethods()) { + block2.javadocComment(method::writeJavadoc); + block2.publicMethod(method.getInterfaceMethodSignature()); + } + }); + } + }); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/template/FluentResourceModelInterfaceTemplate.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/template/FluentResourceModelInterfaceTemplate.java new file mode 100644 index 0000000000..cfc7ffa78d --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/template/FluentResourceModelInterfaceTemplate.java @@ -0,0 +1,85 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.template; + +import com.microsoft.typespec.http.client.generator.mgmt.model.arm.ModelCategory; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentResourceModel; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaFile; +import com.microsoft.typespec.http.client.generator.core.template.IJavaTemplate; + +import java.util.HashSet; +import java.util.Set; + +public class FluentResourceModelInterfaceTemplate implements IJavaTemplate { + + private static final FluentResourceModelInterfaceTemplate INSTANCE = new FluentResourceModelInterfaceTemplate(); + + public static FluentResourceModelInterfaceTemplate getInstance() { + return INSTANCE; + } + + private static final FluentResourceModelInterfaceDefinitionTemplate DEFINITION_TEMPLATE = new FluentResourceModelInterfaceDefinitionTemplate(); + private static final FluentResourceModelInterfaceUpdateTemplate UPDATE_TEMPLATE = new FluentResourceModelInterfaceUpdateTemplate(); + + @Override + public void write(FluentResourceModel model, JavaFile javaFile) { + Set imports = new HashSet<>(); + //imports.add(Immutable.class.getName()); + model.addImportsTo(imports, false); + javaFile.declareImport(imports); + + javaFile.javadocComment(comment -> { + comment.description(model.getDescription()); + }); + + //javaFile.annotation("Immutable"); + javaFile.publicInterface(model.getInterfaceType().getName(), interfaceBlock -> { + // method for properties + model.getProperties().forEach(property -> { + interfaceBlock.javadocComment(comment -> { + comment.description(String.format("Gets the %1$s property: %2$s", property.getName(), property.getDescription())); + comment.methodReturns(String.format("the %1$s value", property.getName())); + }); + interfaceBlock.publicMethod(property.getMethodSignature()); + }); + + // additional methods + model.getAdditionalMethods().forEach(m -> m.writeMethodInterface(interfaceBlock)); + + // method for inner model + interfaceBlock.javadocComment(comment -> { + comment.description(String.format("Gets the inner %s object", model.getInnerModel().getFullName())); + comment.methodReturns("the inner object"); + }); + interfaceBlock.publicMethod(model.getInnerMethodSignature()); + + // Fluent interfaces and methods + if (model.getCategory() != ModelCategory.IMMUTABLE) { + // create flow + if (model.getResourceCreate() != null) { + DEFINITION_TEMPLATE.write(model.getResourceCreate(), interfaceBlock); + } + // update flow + if (model.getResourceUpdate() != null) { + UPDATE_TEMPLATE.write(model.getResourceUpdate(), interfaceBlock); + } + // refresh + if (model.getResourceRefresh() != null) { + model.getResourceRefresh().getFluentMethods().forEach( + refreshMethod -> { + interfaceBlock.javadocComment(refreshMethod::writeJavadoc); + interfaceBlock.publicMethod(refreshMethod.getInterfaceMethodSignature()); + }); + } + if (model.getResourceActions() != null) { + model.getResourceActions().getFluentMethods().forEach( + refreshMethod -> { + interfaceBlock.javadocComment(refreshMethod::writeJavadoc); + interfaceBlock.publicMethod(refreshMethod.getInterfaceMethodSignature()); + }); + } + } + }); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/template/FluentResourceModelInterfaceUpdateTemplate.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/template/FluentResourceModelInterfaceUpdateTemplate.java new file mode 100644 index 0000000000..181f1dbb63 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/template/FluentResourceModelInterfaceUpdateTemplate.java @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.template; + +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.ModelNaming; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.method.FluentMethod; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.update.ResourceUpdate; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.update.UpdateStage; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaInterface; +import com.microsoft.typespec.http.client.generator.core.template.IJavaTemplate; + +import java.util.List; +import java.util.stream.Collectors; + +public class FluentResourceModelInterfaceUpdateTemplate implements IJavaTemplate { + + @Override + public void write(ResourceUpdate resourceUpdate, JavaInterface interfaceBlock) { + FluentMethod updateMethod = resourceUpdate.getUpdateMethod(); + + interfaceBlock.javadocComment(updateMethod::writeJavadoc); + interfaceBlock.publicMethod(updateMethod.getInterfaceMethodSignature()); + + List updateStages = resourceUpdate.getUpdateStages(); + + final String modelName = resourceUpdate.getResourceModel().getInterfaceType().getName(); + + // Update interface + interfaceBlock.javadocComment(commentBlock -> { + commentBlock.description(String.format("The template for %1$s update.", modelName)); + }); + String definitionInterfaceSignature = ModelNaming.MODEL_FLUENT_INTERFACE_UPDATE; + String updateExtendsStr = updateStages.stream() + .map(s -> String.format("%1$s.%2$s", ModelNaming.MODEL_FLUENT_INTERFACE_UPDATE_STAGES, s.getName())) + .collect(Collectors.joining(", ")); + if (!updateExtendsStr.isEmpty()) { + definitionInterfaceSignature += String.format(" extends %1$s", + updateExtendsStr); + } + interfaceBlock.interfaceBlock(definitionInterfaceSignature, block1 -> { + List applyMethods = resourceUpdate.getApplyMethods(); + applyMethods.forEach(method -> { + block1.javadocComment(method::writeJavadoc); + block1.publicMethod(method.getInterfaceMethodSignature()); + }); + }); + + // UpdateStages interface + interfaceBlock.javadocComment(commentBlock -> { + commentBlock.description(String.format("The %1$s update stages.", modelName)); + }); + interfaceBlock.interfaceBlock(ModelNaming.MODEL_FLUENT_INTERFACE_UPDATE_STAGES, block1 -> { + for (UpdateStage stage : updateStages) { + block1.javadocComment(commentBlock -> { + commentBlock.description(stage.getDescription(modelName)); + }); + String interfaceSignature = stage.getName(); + block1.interfaceBlock(interfaceSignature, block2 -> { + for (FluentMethod method : stage.getMethods()) { + block2.javadocComment(method::writeJavadoc); + block2.publicMethod(method.getInterfaceMethodSignature()); + } + }); + } + }); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/template/FluentServiceClientBuilderTemplate.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/template/FluentServiceClientBuilderTemplate.java new file mode 100644 index 0000000000..baaa0816bd --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/template/FluentServiceClientBuilderTemplate.java @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.template; + +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.AsyncSyncClient; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaBlock; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaContext; +import com.microsoft.typespec.http.client.generator.core.template.ServiceClientBuilderTemplate; + +import java.util.Set; + +public class FluentServiceClientBuilderTemplate extends ServiceClientBuilderTemplate { + + private static final FluentServiceClientBuilderTemplate INSTANCE = new FluentServiceClientBuilderTemplate(); + + public static FluentServiceClientBuilderTemplate getInstance() { + return INSTANCE; + } + + @Override + protected void writeSyncClientBuildMethod(AsyncSyncClient syncClient, AsyncSyncClient asyncClient, JavaBlock function, + String buildMethodName, boolean wrapServiceClient) { + writeSyncClientBuildMethodFromInnerClient(syncClient, function, buildMethodName, wrapServiceClient); + } + + @Override + protected void addGeneratedImport(Set imports) { + } + + @Override + protected void addGeneratedAnnotation(JavaContext classBlock) { + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/template/FluentServiceClientTemplate.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/template/FluentServiceClientTemplate.java new file mode 100644 index 0000000000..8673b70f52 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/template/FluentServiceClientTemplate.java @@ -0,0 +1,173 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.template; + +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; +import com.microsoft.typespec.http.client.generator.mgmt.util.FluentUtils; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaClass; +import com.microsoft.typespec.http.client.generator.core.template.ServiceClientTemplate; +import com.microsoft.typespec.http.client.generator.core.template.prototype.MethodTemplate; +import com.azure.core.http.HttpHeaders; +import com.azure.core.http.HttpResponse; +import com.azure.core.http.rest.Response; +import com.azure.core.management.exception.ManagementError; +import com.azure.core.management.exception.ManagementException; +import com.azure.core.management.polling.PollResult; +import com.azure.core.management.polling.PollerFactory; +import com.azure.core.util.Context; +import com.azure.core.util.CoreUtils; +import com.azure.core.util.polling.AsyncPollResponse; +import com.azure.core.util.polling.LongRunningOperationStatus; +import com.azure.core.util.polling.PollerFlux; +import com.azure.core.util.serializer.SerializerEncoding; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.io.IOException; +import java.lang.reflect.Type; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Collections; +import java.util.Map; + +public class FluentServiceClientTemplate extends ServiceClientTemplate { + + private static final FluentServiceClientTemplate INSTANCE = new FluentServiceClientTemplate(); + static { + if (JavaSettings.getInstance().isFluentLite()) { + MethodTemplate getContextMethod = MethodTemplate.builder() + .imports(Collections.singleton(Context.class.getName())) + .methodSignature("Context getContext()") + .comment(comment -> { + comment.description("Gets default client context."); + comment.methodReturns("the default client context."); + }) + .method(method -> method.methodReturn("Context.NONE")) + .build(); + + MethodTemplate mergeContextMethod = MethodTemplate.builder() + .imports(Arrays.asList( + Context.class.getName(), + CoreUtils.class.getName(), + Map.class.getName())) + .methodSignature("Context mergeContext(Context context)") + .comment(comment -> { + comment.description("Merges default client context with provided context."); + comment.param("context", "the context to be merged with default client context."); + comment.methodReturns("the merged context."); + }) + .method(method -> method.methodReturn("CoreUtils.mergeContexts(this.getContext(), context)")) + .build(); + + MethodTemplate getLroResultMethod = MethodTemplate.builder() + .imports(Arrays.asList( + PollerFlux.class.getName(), + PollResult.class.getName(), + Mono.class.getName(), + Flux.class.getName(), + Response.class.getName(), + ByteBuffer.class.getName(), + Type.class.getName(), + PollerFactory.class.getName())) + .methodSignature(" PollerFlux, U> getLroResult(Mono>> activationResponse, HttpPipeline httpPipeline, Type pollResultType, Type finalResultType, Context context)") + .comment(comment -> { + comment.description("Gets long running operation result."); + comment.param("activationResponse", "the response of activation operation."); + comment.param("httpPipeline", "the http pipeline."); + comment.param("pollResultType", "type of poll result."); + comment.param("finalResultType", "type of final result."); + comment.param("context", "the context shared by all requests."); + comment.param("", "type of poll result."); + comment.param("", "type of final result."); + comment.methodReturns("poller flux for poll result and final result."); + }) + .method(method -> method.methodReturn("PollerFactory.create(serializerAdapter, httpPipeline, pollResultType, finalResultType, defaultPollInterval, activationResponse, context)")) + .build(); + + MethodTemplate getLroFinalResultOrErrorMethod = MethodTemplate.builder() + .imports(Arrays.asList( + PollerFlux.class.getName(), + PollResult.class.getName(), + Mono.class.getName(), + AsyncPollResponse.class.getName(), + ManagementError.class.getName(), + ManagementException.class.getName(), + HttpResponse.class.getName(), + LongRunningOperationStatus.class.getName(), + SerializerEncoding.class.getName(), + IOException.class.getName(), + // below import is actually used in HttpResponseImpl + HttpHeaders.class.getName(), + Charset.class.getName(), + StandardCharsets.class.getName())) + .methodSignature(" Mono getLroFinalResultOrError(AsyncPollResponse, U> response)") + .comment(comment -> { + comment.description("Gets the final result, or an error, based on last async poll response."); + comment.param("response", "the last async poll response."); + comment.param("", "type of poll result."); + comment.param("", "type of final result."); + comment.methodReturns("the final result, or an error."); + }) + .method(method -> method.text(FluentUtils.loadTextFromResource("Client_getLroFinalResultOrError.txt"))) + .build(); + + INSTANCE.additionalMethods.add(getContextMethod); + INSTANCE.additionalMethods.add(mergeContextMethod); + INSTANCE.additionalMethods.add(getLroResultMethod); + INSTANCE.additionalMethods.add(getLroFinalResultOrErrorMethod); + } + } + + public static FluentServiceClientTemplate getInstance() { + return INSTANCE; + } + + @Override + protected void writeAdditionalClassBlock(JavaClass classBlock) { + if (JavaSettings.getInstance().isFluentLite()) { + classBlock.privateStaticFinalClass("HttpResponseImpl extends HttpResponse", block -> { + block.privateFinalMemberVariable("int", "statusCode"); + block.privateFinalMemberVariable("byte[]", "responseBody"); + block.privateFinalMemberVariable("HttpHeaders", "httpHeaders"); + + block.packagePrivateConstructor("HttpResponseImpl(int statusCode, HttpHeaders httpHeaders, String responseBody)", code -> { + code.line("super(null);"); + code.line("this.statusCode = statusCode;"); + code.line("this.httpHeaders = httpHeaders;"); + code.line("this.responseBody = responseBody == null ? null : responseBody.getBytes(StandardCharsets.UTF_8);"); + }); + + block.publicMethod("int getStatusCode()", code -> { + code.methodReturn("statusCode"); + }); + + block.publicMethod("String getHeaderValue(String s)", code -> { + code.methodReturn("httpHeaders.getValue(HttpHeaderName.fromString(s))"); + }); + + block.publicMethod("HttpHeaders getHeaders()", code -> { + code.methodReturn("httpHeaders"); + }); + + block.publicMethod("Flux getBody()", code -> { + code.methodReturn("Flux.just(ByteBuffer.wrap(responseBody))"); + }); + + block.publicMethod("Mono getBodyAsByteArray()", code -> { + code.methodReturn("Mono.just(responseBody)"); + }); + + block.publicMethod("Mono getBodyAsString()", code -> { + code.methodReturn("Mono.just(new String(responseBody, StandardCharsets.UTF_8))"); + }); + + block.publicMethod("Mono getBodyAsString(Charset charset)", code -> { + code.methodReturn("Mono.just(new String(responseBody, charset))"); + }); + }); + } + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/template/FluentStreamStyleSerializationModelTemplate.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/template/FluentStreamStyleSerializationModelTemplate.java new file mode 100644 index 0000000000..13b6181b1d --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/template/FluentStreamStyleSerializationModelTemplate.java @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.template; + +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; +import com.microsoft.typespec.http.client.generator.mgmt.model.arm.ErrorClientModel; +import com.microsoft.typespec.http.client.generator.mgmt.util.FluentUtils; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientModel; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientModelProperty; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientModelPropertyReference; +import com.microsoft.typespec.http.client.generator.core.template.StreamSerializationModelTemplate; +import com.azure.core.util.CoreUtils; + +import java.util.List; + +public class FluentStreamStyleSerializationModelTemplate extends StreamSerializationModelTemplate { + private static final FluentModelTemplate FLUENT_MODEL_TEMPLATE = FluentModelTemplate.getInstance(); + + public static FluentStreamStyleSerializationModelTemplate getInstance() { + return new FluentStreamStyleSerializationModelTemplate(); + } + + @Override + protected String getGetterName(ClientModel model, ClientModelProperty property) { + return FLUENT_MODEL_TEMPLATE.getGetterName(model, property); + } + + @Override + protected boolean parentModelHasValidate(String parentModelName) { + return FLUENT_MODEL_TEMPLATE.parentModelHasValidate(parentModelName); + } + + @Override + protected boolean isManagementErrorSubclass(ClientModel model, JavaSettings settings) { + if (CoreUtils.isNullOrEmpty(model.getParentModelName())) { + return false; + } + boolean manageErrorParent = false; + String parentModelName = model.getParentModelName(); + while (parentModelName != null) { + ClientModel parentModel = FluentUtils.getClientModel(parentModelName); + if (parentModel == ErrorClientModel.MANAGEMENT_ERROR) { + manageErrorParent = true; + break; + } + parentModelName = parentModel.getParentModelName(); + } + return manageErrorParent; + } + + @Override + protected List getClientModelPropertyReferences(ClientModel model) { + return FLUENT_MODEL_TEMPLATE.getClientModelPropertyReferences(model); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/template/FluentTemplateFactory.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/template/FluentTemplateFactory.java new file mode 100644 index 0000000000..dcc4bf8f65 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/template/FluentTemplateFactory.java @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.template; + +import com.microsoft.typespec.http.client.generator.core.template.ClientMethodTemplate; +import com.microsoft.typespec.http.client.generator.core.template.DefaultTemplateFactory; +import com.microsoft.typespec.http.client.generator.core.template.ModelTemplate; +import com.microsoft.typespec.http.client.generator.core.template.PomTemplate; +import com.microsoft.typespec.http.client.generator.core.template.ProxyTemplate; +import com.microsoft.typespec.http.client.generator.core.template.ServiceClientBuilderTemplate; +import com.microsoft.typespec.http.client.generator.core.template.ServiceClientTemplate; +import com.microsoft.typespec.http.client.generator.core.template.StreamSerializationModelTemplate; + +public class FluentTemplateFactory extends DefaultTemplateFactory { + + @Override + public ProxyTemplate getProxyTemplate() { + return FluentProxyTemplate.getInstance(); + } + + @Override + public ClientMethodTemplate getClientMethodTemplate() { + return FluentClientMethodTemplate.getInstance(); + } + + @Override + public ServiceClientBuilderTemplate getServiceClientBuilderTemplate() { + return FluentServiceClientBuilderTemplate.getInstance(); + } + + @Override + public ModelTemplate getModelTemplate() { + return FluentModelTemplate.getInstance(); + } + + @Override + public StreamSerializationModelTemplate getStreamStyleModelTemplate() { + return FluentStreamStyleSerializationModelTemplate.getInstance(); + } + + @Override + public ServiceClientTemplate getServiceClientTemplate() { + return FluentServiceClientTemplate.getInstance(); + } + + @Override + public PomTemplate getPomTemplate() { + return FluentPomTemplate.getInstance(); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/template/ReadmeTemplate.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/template/ReadmeTemplate.java new file mode 100644 index 0000000000..f8c9f23625 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/template/ReadmeTemplate.java @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.template; + +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentStatic; +import com.microsoft.typespec.http.client.generator.mgmt.model.projectmodel.CodeSample; +import com.microsoft.typespec.http.client.generator.mgmt.model.projectmodel.FluentProject; +import com.microsoft.typespec.http.client.generator.mgmt.util.FluentUtils; +import com.microsoft.typespec.http.client.generator.core.util.TemplateUtil; + +public class ReadmeTemplate extends com.microsoft.typespec.http.client.generator.core.template.ReadmeTemplate { + + public String write(FluentProject project) { + StringBuilder sampleCodesBuilder = new StringBuilder(); + for (CodeSample codeSample : project.getCodeSamples()) { + if (codeSample.getCode() != null) { + sampleCodesBuilder.append("```java\n") + .append(codeSample.getCode()) + .append("```\n"); + } + } + + if (project.isGenerateSamples() && project.getSdkRepositoryUri().isPresent()) { + sampleCodesBuilder.append("[Code snippets and samples]") + .append("(").append(project.getSdkRepositoryUri().get()).append("/SAMPLE.md").append(")") + .append("\n"); + } + + return FluentUtils.loadTextFromResource("Readme.txt", + TemplateUtil.SERVICE_NAME, project.getServiceName(), + TemplateUtil.SERVICE_DESCRIPTION, project.getServiceDescriptionForMarkdown(), + TemplateUtil.GROUP_ID, project.getGroupId(), + TemplateUtil.ARTIFACT_ID, project.getArtifactId(), + TemplateUtil.ARTIFACT_VERSION, project.getVersion(), + TemplateUtil.MANAGER_CLASS, FluentStatic.getFluentManager().getType().getName(), + TemplateUtil.SAMPLE_CODES, sampleCodesBuilder.toString(), + TemplateUtil.IMPRESSION_PIXEL, getImpression(project) + ); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/template/ResourceManagerUtilsTemplate.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/template/ResourceManagerUtilsTemplate.java new file mode 100644 index 0000000000..d55a51152e --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/template/ResourceManagerUtilsTemplate.java @@ -0,0 +1,101 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.template; + +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.ModelNaming; +import com.microsoft.typespec.http.client.generator.mgmt.util.FluentUtils; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaFile; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaModifier; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaVisibility; +import com.microsoft.typespec.http.client.generator.core.template.IJavaTemplate; +import com.microsoft.typespec.http.client.generator.core.template.prototype.MethodTemplate; +import com.azure.core.http.rest.PagedFlux; +import com.azure.core.http.rest.PagedIterable; +import com.azure.core.http.rest.PagedResponse; +import com.azure.core.http.rest.PagedResponseBase; +import com.azure.core.util.CoreUtils; +import reactor.core.publisher.Flux; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class ResourceManagerUtilsTemplate implements IJavaTemplate { + + private static final ResourceManagerUtilsTemplate INSTANCE = new ResourceManagerUtilsTemplate(); + + public static ResourceManagerUtilsTemplate getInstance() { + return INSTANCE; + } + + private static final List METHOD_TEMPLATES = new ArrayList<>(); + static { + MethodTemplate getValueFromIdByNameMethod = MethodTemplate.builder() + .imports(Arrays.asList( + Arrays.class.getName(), + Iterator.class.getName())) + .visibility(JavaVisibility.PackagePrivate) + .modifiers(Collections.singletonList(JavaModifier.Static)) + .methodSignature("String getValueFromIdByName(String id, String name)") + .method(block -> block.line(FluentUtils.loadTextFromResource("ResourceManagerUtils_getValueFromIdByName.txt"))) + .build(); + METHOD_TEMPLATES.add(getValueFromIdByNameMethod); + + MethodTemplate getValueFromIdByParameterNameMethod = MethodTemplate.builder() + .imports(Arrays.asList( + Arrays.class.getName(), + Iterator.class.getName(), + List.class.getName(), + ArrayList.class.getName(), + CoreUtils.class.getName(), + Collections.class.getName())) + .visibility(JavaVisibility.PackagePrivate) + .modifiers(Collections.singletonList(JavaModifier.Static)) + .methodSignature("String getValueFromIdByParameterName(String id, String pathTemplate, String parameterName)") + .method(block -> block.line(FluentUtils.loadTextFromResource("ResourceManagerUtils_getValueFromIdByParameterName.txt"))) + .build(); + METHOD_TEMPLATES.add(getValueFromIdByParameterNameMethod); + } + + private static final List IMPORTS_UTILS_PAGED_ITERABLE = Arrays.asList( + PagedFlux.class.getName(), + PagedIterable.class.getName(), + PagedResponse.class.getName(), + PagedResponseBase.class.getName(), + Flux.class.getName(), + Iterator.class.getName(), + Function.class.getName(), + Collectors.class.getName(), + Stream.class.getName() + ); + + public void write(JavaFile javaFile) { + write(null, javaFile); + } + + @Override + public void write(Void ignored, JavaFile javaFile) { + Set imports = new HashSet<>(); + METHOD_TEMPLATES.forEach(mt -> mt.addImportsTo(imports)); + imports.addAll(IMPORTS_UTILS_PAGED_ITERABLE); + javaFile.declareImport(imports); + + javaFile.classBlock(JavaVisibility.PackagePrivate, Collections.singletonList(JavaModifier.Final), ModelNaming.CLASS_RESOURCE_MANAGER_UTILS, classBlock -> { + classBlock.constructor(JavaVisibility.Private, String.format("%s()", ModelNaming.CLASS_RESOURCE_MANAGER_UTILS), (constructorBlock) -> {}); + METHOD_TEMPLATES.forEach(mt -> mt.writeMethod(classBlock)); + + // mapPage and PagedIterableImpl class + javaFile.line(); + String configurableClassText = FluentUtils.loadTextFromResource("ResourceManagerUtils_PagedIterableImpl.txt"); + javaFile.text(configurableClassText); + }); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/template/SampleTemplate.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/template/SampleTemplate.java new file mode 100644 index 0000000000..b158d2b13d --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/template/SampleTemplate.java @@ -0,0 +1,115 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.template; + +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentExample; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaFile; +import com.microsoft.typespec.http.client.generator.core.postprocessor.implementation.CodeFormatterUtil; + +import java.net.URL; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +public class SampleTemplate { + + private final StringBuilder builder = new StringBuilder(); + + private static final String NEW_LINE = System.lineSeparator(); + + public String write(List examples, List sampleJavaFiles) { + assert examples.size() == sampleJavaFiles.size(); + + // clean up copyright etc. + List> javaFiles = sampleJavaFiles.stream() + .map(e -> Map.entry(e.getFilePath(), cleanJavaFile(e))) + .collect(Collectors.toList()); + // format code + List javaFileContents; + try { + javaFileContents = CodeFormatterUtil.formatCode(javaFiles); + } catch (Exception e) { + throw new RuntimeException(e); + } + + heading("Code snippets and samples", 1); + + List sectionNames = new ArrayList<>(); + String groupName = null; + for (FluentExample example : examples) { + if (!Objects.equals(groupName, example.getGroupName())) { + newLine(); + heading(example.getGroupName(), 2); + } + + groupName = example.getGroupName(); + String sectionName = example.getGroupName() + "_" + example.getMethodName(); + sectionNames.add(sectionName); + + unorderedList(linkSection(example.getMethodName(), sectionName)); + } + + int index = 0; + for (String javaFileContent : javaFileContents) { + String sectionName = sectionNames.get(index); + heading(sectionName, 3); + + builder.append("```java"); + newLine(); + builder.append(javaFileContent); + builder.append("```"); + newLine(); + newLine(); + + ++index; + } + + return builder.toString(); + } + + private static String cleanJavaFile(JavaFile javaFile) { + String content = javaFile.getContents().toString(); + + // remove copyright and package statement + List formattedLines = new ArrayList<>(); + String[] lines = content.split("\r?\n", -1); + boolean skipCopyright = true; + for (String line : lines) { + if (skipCopyright) { + if (!line.trim().isEmpty() && !line.trim().startsWith("//") && !line.trim().startsWith("package")) { + skipCopyright = false; + } + } + + if (!skipCopyright) { + formattedLines.add(line); + } + } + return String.join(System.lineSeparator(), formattedLines); + } + + private static String link(String text, URL url) { + return '[' + text + ']' + '(' + url.toString() + ')'; + } + + private static String linkSection(String text, String section) { + return '[' + text + ']' + "(#" + section.toLowerCase(Locale.ROOT) + ')'; + } + + private void heading(String text, int level) { + builder.append("#".repeat(Math.max(0, level))); + builder.append(' ').append(text).append(NEW_LINE).append(NEW_LINE); + } + + private void unorderedList(String text) { + builder.append("- ").append(text).append(NEW_LINE); + } + + private void newLine() { + builder.append(NEW_LINE); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/transformer/ConstantSchemaOptimization.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/transformer/ConstantSchemaOptimization.java new file mode 100644 index 0000000000..a357955dc4 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/transformer/ConstantSchemaOptimization.java @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.transformer; + +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.CodeModel; +import com.microsoft.typespec.http.client.generator.core.preprocessor.Preprocessor; + +public class ConstantSchemaOptimization { + + public CodeModel process(CodeModel codeModel) { + return Preprocessor.convertOptionalConstantsToEnum(codeModel); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/transformer/ErrorTypeNormalization.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/transformer/ErrorTypeNormalization.java new file mode 100644 index 0000000000..e3c1c54849 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/transformer/ErrorTypeNormalization.java @@ -0,0 +1,272 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.transformer; + +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.ArraySchema; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.CodeModel; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Language; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Languages; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.ObjectSchema; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Property; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Relations; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Response; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Schema; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.SchemaContext; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Value; +import com.microsoft.typespec.http.client.generator.core.extension.plugin.PluginLogger; +import com.microsoft.typespec.http.client.generator.mgmt.model.FluentType; +import com.microsoft.typespec.http.client.generator.mgmt.util.Utils; +import com.microsoft.typespec.http.client.generator.mgmt.FluentNamer; +import com.azure.core.util.CoreUtils; +import org.slf4j.Logger; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +public class ErrorTypeNormalization { + + private static final Logger LOGGER = new PluginLogger(FluentNamer.getPluginInstance(), ErrorTypeNormalization.class); + + private static final String ERROR_PROPERTY_NAME = "error"; + + public CodeModel process(CodeModel codeModel) { + codeModel.getOperationGroups().stream() + .flatMap(og -> og.getOperations().stream()) + .flatMap(o -> o.getExceptions().stream()) + .map(Response::getSchema) + .filter(Objects::nonNull) + .distinct() + .forEach(s -> process((ObjectSchema) s)); + + return codeModel; + } + + private static final Set MANAGEMENT_ERROR_FIELDS = new HashSet<>(Arrays.asList("code", "message", "target", "details", "additionalInfo")); + private static final Set MANAGEMENT_ERROR_FIELDS_MIN_REQUIRED = new HashSet<>(Arrays.asList("code", "message")); + + private static final ObjectSchema DUMMY_ERROR = dummyManagementError(); + + private static ObjectSchema dummyManagementError() { + ObjectSchema schema = new ObjectSchema(); + schema.setLanguage(new Languages()); + schema.getLanguage().setJava(new Language()); + schema.getLanguage().getJava().setName(FluentType.MANAGEMENT_ERROR.getName()); + schema.setProperties(new ArrayList<>()); + schema.getProperties().add(new Property()); + schema.getProperties().get(0).setSerializedName("code"); + schema.getProperties().add(new Property()); + schema.getProperties().get(1).setSerializedName("message"); + return schema; + } + + private void process(ObjectSchema error) { + ObjectSchema errorSchema = error; + + Optional errorSchemaOpt = error.getProperties().stream() + .filter(p -> ERROR_PROPERTY_NAME.equalsIgnoreCase(p.getSerializedName())) + .map(Value::getSchema) + .filter(s -> s instanceof ObjectSchema) + .map(s -> (ObjectSchema) s) + .findFirst(); + + if (errorSchemaOpt.isPresent()) { + errorSchema = errorSchemaOpt.get(); + } + + normalizeErrorType(error, errorSchema); + } + + private void normalizeErrorType(ObjectSchema error, ObjectSchema errorSchema) { + switch (getErrorType(errorSchema)) { + case MANAGEMENT_ERROR: + final boolean updateChildrenParent = errorSchema != error && existNoneExceptionChildren(error); + + LOGGER.info("Rename error from '{}' to 'ManagementError'", Utils.getJavaName(error)); + + error.getLanguage().getJava().setName(FluentType.MANAGEMENT_ERROR.getName()); + + if (errorSchema != error) { + errorSchema.getLanguage().getJava().setName(FluentType.MANAGEMENT_ERROR.getName()); + } + + if (updateChildrenParent) { + // update its subclass of usage=input/output, to avoid inherit from this error model "ErrorResponse" + error.getChildren().getAll().stream().filter(ErrorTypeNormalization::usedMoreThanException).forEach(o -> { + if (o instanceof ObjectSchema) { + adaptForParentSchema((ObjectSchema) o, error); + } + }); + } + + if (errorSchema != error && !updateChildrenParent) { + error.setChildren(errorSchema.getChildren()); + } + + normalizeSubclass(errorSchema); + + break; + + case SUBCLASS_MANAGEMENT_ERROR: + LOGGER.info("Modify error '{}' as subclass of 'ManagementError'", Utils.getJavaName(error)); + + error.getLanguage().getJava().setName(Utils.getJavaName(errorSchema)); + + // make it a subclass of ManagementError + Relations parents = new Relations(); + parents.setAll(Collections.singletonList(DUMMY_ERROR)); + parents.setImmediate(Collections.singletonList(DUMMY_ERROR)); + errorSchema.setParents(parents); + + if (errorSchema != error) { + error.setParents(parents); + error.setChildren(errorSchema.getChildren()); + } + + filterProperties(errorSchema); + + if (errorSchema != error) { + error.setProperties(errorSchema.getProperties()); + } + + normalizeSubclass(errorSchema); + + break; + + case GENERIC: + break; + } + } + + private void adaptForParentSchema(ObjectSchema compositeType, ObjectSchema error) { + // remove "ErrorResponse" from its parents + Iterator itor = compositeType.getParents().getImmediate().iterator(); + while (itor.hasNext()) { + Schema type = itor.next(); + if (type == error) { + itor.remove(); + break; + } + } + itor = compositeType.getParents().getAll().iterator(); + while (itor.hasNext()) { + Schema type = itor.next(); + if (type == error) { + itor.remove(); + break; + } + } + + // move "error" to subclass, make it composite with "error", instead of inherit from "ErrorResponse" + if (compositeType.getProperties() == null || compositeType.getProperties().stream().noneMatch(p -> ERROR_PROPERTY_NAME.equalsIgnoreCase(p.getSerializedName()))) { + if (compositeType.getProperties() == null) { + compositeType.setProperties(new ArrayList<>()); + } + compositeType.getProperties().add(error.getProperties().stream().filter(p -> ERROR_PROPERTY_NAME.equalsIgnoreCase(p.getSerializedName())).findFirst().get()); + } + } + + private static boolean existNoneExceptionChildren(ObjectSchema error) { + return error.getChildren() != null && error.getChildren().getAll().stream() + .anyMatch(ErrorTypeNormalization::usedMoreThanException); + } + + private static boolean usedMoreThanException(Schema schema) { + return !CoreUtils.isNullOrEmpty(schema.getUsage()) + && (schema.getUsage().contains(SchemaContext.INPUT) || schema.getUsage().contains(SchemaContext.OUTPUT)); + } + + private void normalizeSubclass(ObjectSchema errorSchema) { + if (errorSchema.getChildren() != null && errorSchema.getChildren().getImmediate() != null) { + for (Schema schema : errorSchema.getChildren().getImmediate()) { + if (schema instanceof ObjectSchema) { + ObjectSchema error = (ObjectSchema) schema; + + LOGGER.info("Modify type '{}' as subclass of '{}'", Utils.getJavaName(error), Utils.getJavaName(errorSchema)); + + filterProperties(error); + } + } + } + } + + private void filterProperties(ObjectSchema errorSchema) { + List properties = new ArrayList<>(); + errorSchema.getProperties().forEach(p -> { + if (!MANAGEMENT_ERROR_FIELDS.contains(p.getSerializedName())) { + p.setReadOnly(true); + properties.add(p); + } else if (p.getSerializedName().equals("details")) { + normalizeErrorDetailType(p); + if (FluentType.nonManagementError(Utils.getJavaName(((ArraySchema) p.getSchema()).getElementType()))) { + p.setReadOnly(true); + properties.add(p); + } + } + }); + errorSchema.setProperties(properties); + } + + private void normalizeErrorDetailType(Property details) { + Schema detailsSchema = details.getSchema(); + if (detailsSchema instanceof ArraySchema && ((ArraySchema) detailsSchema).getElementType() instanceof ObjectSchema ) { + ObjectSchema error = (ObjectSchema) ((ArraySchema) detailsSchema).getElementType(); + if (error.getParents() == null || FluentType.nonManagementError(Utils.getJavaName(error.getParents().getImmediate().get(0)))) { + // if not subclass of ManagementError, normalize it + + switch (getErrorType(error)) { + case MANAGEMENT_ERROR: + error.getLanguage().getJava().setName(FluentType.MANAGEMENT_ERROR.getName()); + break; + + case SUBCLASS_MANAGEMENT_ERROR: + case GENERIC: + Relations parents = new Relations(); + parents.setAll(Collections.singletonList(DUMMY_ERROR)); + parents.setImmediate(Collections.singletonList(DUMMY_ERROR)); + error.setParents(parents); + + filterProperties(error); + break; + } + } + } else { + ArraySchema arraySchema = new ArraySchema(); + arraySchema.setLanguage(new Languages()); + arraySchema.getLanguage().setJava(new Language()); + arraySchema.getLanguage().getJava().setName("ManagementErrorDetails"); + + arraySchema.setElementType(DUMMY_ERROR); + + details.setSchema(arraySchema); + } + } + + private ErrorType getErrorType(ObjectSchema error) { + Set propertyNames = error.getProperties().stream() + .map(Property::getSerializedName) + .collect(Collectors.toSet()); + + ErrorType type; + if (MANAGEMENT_ERROR_FIELDS.containsAll(propertyNames)) { + type = ErrorType.MANAGEMENT_ERROR; + } else if (propertyNames.containsAll(MANAGEMENT_ERROR_FIELDS_MIN_REQUIRED)) { + type = ErrorType.SUBCLASS_MANAGEMENT_ERROR; + } else { + type = ErrorType.GENERIC; + } + return type; + } + + private enum ErrorType { + MANAGEMENT_ERROR, SUBCLASS_MANAGEMENT_ERROR, GENERIC + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/transformer/FluentTransformer.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/transformer/FluentTransformer.java new file mode 100644 index 0000000000..11938fca28 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/transformer/FluentTransformer.java @@ -0,0 +1,183 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.transformer; + +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.CodeModel; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Operation; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Parameter; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.RequestParameterLocation; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Schema; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.StringSchema; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.UuidSchema; +import com.microsoft.typespec.http.client.generator.core.extension.plugin.PluginLogger; +import com.microsoft.typespec.http.client.generator.mgmt.util.FluentJavaSettings; +import com.microsoft.typespec.http.client.generator.mgmt.util.Utils; +import com.microsoft.typespec.http.client.generator.mgmt.FluentNamer; +import com.azure.core.util.CoreUtils; +import org.slf4j.Logger; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.function.Function; +import java.util.stream.Collectors; + +public class FluentTransformer { + + private final FluentJavaSettings fluentJavaSettings; + + private final Logger logger = new PluginLogger(FluentNamer.getPluginInstance(), FluentTransformer.class); + + public FluentTransformer(FluentJavaSettings fluentJavaSettings) { + this.fluentJavaSettings = fluentJavaSettings; + } + + public CodeModel preTransform(CodeModel codeModel) { + codeModel = removeXml(codeModel); + codeModel = deduplicateOperations(codeModel); + codeModel = normalizeParameterLocation(codeModel); + codeModel = renameUngroupedOperationGroup(codeModel, fluentJavaSettings); + codeModel = new SchemaNameNormalization(fluentJavaSettings.getNamingOverride()).process(codeModel); + codeModel = new ConstantSchemaOptimization().process(codeModel); + codeModel = renameHostParameter(codeModel); + codeModel = transformSubscriptionIdUuid(codeModel); + return codeModel; + } + + public CodeModel postTransform(CodeModel codeModel) { + codeModel = new OperationGroupFilter(fluentJavaSettings.getJavaNamesForRemoveOperationGroup()).process(codeModel); + codeModel = new OperationGroupRenamer(fluentJavaSettings.getJavaNamesForRenameOperationGroup()).process(codeModel); + codeModel = new NamingConflictResolver().process(codeModel); + codeModel = new SchemaRenamer(fluentJavaSettings.getJavaNamesForRenameModel()).process(codeModel); + codeModel = new OperationNameNormalization().process(codeModel); + codeModel = new ResourceTypeNormalization().process(codeModel); + codeModel = new ErrorTypeNormalization().process(codeModel); + codeModel = new ResponseStatusCodeNormalization().process(codeModel); + if (fluentJavaSettings.isResourcePropertyAsSubResource()) { + codeModel = new ResourcePropertyNormalization().process(codeModel); + } + codeModel = new SchemaCleanup(fluentJavaSettings.getJavaNamesForPreserveModel()).process(codeModel); + return codeModel; + } + + protected CodeModel deduplicateOperations(CodeModel codeModel) { + // avoid duplicate Operations_List, which is common in management-plane + codeModel.getOperationGroups().stream() + .filter(og -> "Operations".equalsIgnoreCase(Utils.getDefaultName(og))) + .findFirst().ifPresent(og -> { + List deduplicatedOperations = og.getOperations().stream() + .filter(o -> Utils.getDefaultName(o) != null) + .collect(Collectors.toMap(Utils::getDefaultName, Function.identity(), (p, q) -> p)).values() + .stream().filter(Objects::nonNull).distinct().collect(Collectors.toList()); + deduplicatedOperations.addAll(og.getOperations().stream() + .filter(o -> Utils.getDefaultName(o) == null) + .collect(Collectors.toList())); + + if (deduplicatedOperations.size() < og.getOperations().size()) { + logger.warn("Duplicate operations found in operation group 'Operations'"); + og.setOperations(deduplicatedOperations); + } + }); + + return codeModel; + } + + protected CodeModel normalizeParameterLocation(CodeModel codeModel) { + List modifiedGlobalParameters = new ArrayList<>(); + codeModel.getGlobalParameters().stream().filter(p -> p.getImplementation() == Parameter.ImplementationLocation.CLIENT + && p.getProtocol() != null && p.getProtocol().getHttp() != null).forEach(p -> { + String serializedName = p.getLanguage().getDefault().getSerializedName(); + if ((p.getProtocol().getHttp().getIn() == RequestParameterLocation.PATH && !"subscriptionId".equalsIgnoreCase(serializedName)) + || (p.getProtocol().getHttp().getIn() == RequestParameterLocation.QUERY && !"api-version".equalsIgnoreCase(serializedName))) { + logger.warn("Modify parameter '{}' implementation from CLIENT to METHOD", serializedName); + p.setImplementation(Parameter.ImplementationLocation.METHOD); + modifiedGlobalParameters.add(p); + } + }); + if (!modifiedGlobalParameters.isEmpty()) { + // add now METHOD parameter to signature parameters + codeModel.getOperationGroups().stream().flatMap(og -> og.getOperations().stream()).forEach(o -> { + List parameters = o.getParameters(); + List signatureParameters = o.getSignatureParameters(); + for (Parameter parameter : modifiedGlobalParameters) { + if (!signatureParameters.contains(parameter) && parameters.contains(parameter)) { + signatureParameters.add(parameter); + } + } + }); + } + return codeModel; + } + + protected CodeModel renameUngroupedOperationGroup(CodeModel codeModel, FluentJavaSettings settings) { + final String nameForUngroupedOperations = Utils.getNameForUngroupedOperations(codeModel, settings); + if (nameForUngroupedOperations == null) { + return codeModel; + } + + codeModel.getOperationGroups().stream() + .filter(og -> Utils.getDefaultName(og) == null || Utils.getDefaultName(og).isEmpty()) + .forEach(og -> { + logger.info("Rename ungrouped operation group to '{}'", nameForUngroupedOperations); + og.set$key(nameForUngroupedOperations); + og.getLanguage().getDefault().setName(nameForUngroupedOperations); + }); + return codeModel; + } + + /** + * Renames $host to endpoint. + * + * @param codeModel Code model. + * @return Processed code model. + */ + protected CodeModel renameHostParameter(CodeModel codeModel) { + codeModel.getGlobalParameters().stream() + .filter(p -> "$host".equals(p.getLanguage().getDefault().getSerializedName())) + .forEach(p -> { + p.getLanguage().getDefault().setName("endpoint"); + }); + return codeModel; + } + + private CodeModel transformSubscriptionIdUuid(CodeModel codeModel) { + // if globalParameter has "subscriptionId" and is UuidSchema, then make the schema StringSchema + codeModel.getGlobalParameters().stream() + .filter(p -> "subscriptionId".equals(p.getLanguage().getDefault().getSerializedName()) + && p.getSchema() instanceof UuidSchema) + .forEach(p -> { + Schema oldSchema = p.getSchema(); + StringSchema newSchema = new StringSchema(); + // copy schema metadata + newSchema.setLanguage(oldSchema.getLanguage()); + newSchema.setProtocol(oldSchema.getProtocol()); + newSchema.setExtensions(oldSchema.getExtensions()); + + newSchema.setType(Schema.AllSchemaTypes.STRING); + newSchema.setSummary(oldSchema.getSummary()); + newSchema.setExample(oldSchema.getExample()); + newSchema.setSerialization(oldSchema.getSerialization()); + newSchema.set$key(oldSchema.get$key()); + newSchema.setUid(oldSchema.getUid()); + newSchema.setDescription(oldSchema.getDescription()); + newSchema.setApiVersions(oldSchema.getApiVersions()); + newSchema.setDeprecated(oldSchema.getDeprecated()); + newSchema.setExternalDocs(oldSchema.getExternalDocs()); + p.setSchema(newSchema); + }); + return codeModel; + } + + private static CodeModel removeXml(CodeModel codeModel) { + // remove xml from serializationFormats, as mgmt currently does not have dependency on jackson-dataformat-xml package + if (!CoreUtils.isNullOrEmpty(codeModel.getSchemas().getObjects())) { + codeModel.getSchemas().getObjects().forEach(o -> { + if (!CoreUtils.isNullOrEmpty(o.getSerializationFormats())) { + o.getSerializationFormats().remove("xml"); + } + }); + } + return codeModel; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/transformer/NamingConflictResolver.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/transformer/NamingConflictResolver.java new file mode 100644 index 0000000000..43fba89635 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/transformer/NamingConflictResolver.java @@ -0,0 +1,95 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.transformer; + +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.CodeModel; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Metadata; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.ValueSchema; +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; +import com.microsoft.typespec.http.client.generator.core.extension.plugin.PluginLogger; +import com.microsoft.typespec.http.client.generator.mgmt.util.Constants; +import com.microsoft.typespec.http.client.generator.mgmt.util.Utils; +import com.microsoft.typespec.http.client.generator.mgmt.FluentNamer; +import com.microsoft.typespec.http.client.generator.core.preprocessor.namer.CodeNamer; +import org.slf4j.Logger; + +import java.util.HashSet; +import java.util.Locale; +import java.util.Set; +import java.util.stream.Collectors; + +public class NamingConflictResolver { + + private static final Logger LOGGER = new PluginLogger(FluentNamer.getPluginInstance(), NamingConflictResolver.class); + + public CodeModel process(CodeModel codeModel) { + // conform to lowercase, to avoid problem on Windows system, where file name is case-insensitive + Set methodGroupNamesLowerCase = new HashSet<>(); + Set objectNamesLowerCase = codeModel.getSchemas().getObjects().stream() + .map(Utils::getJavaName) + .map(n -> n.toLowerCase(Locale.ROOT)) + .collect(Collectors.toSet()); + codeModel.getOperationGroups().forEach(og -> { + String methodGroupName = CodeNamer.getPlural(Utils.getJavaName(og)); + String newMethodGroupName = methodGroupName; + if (objectNamesLowerCase.contains(methodGroupName.toLowerCase(Locale.ROOT))) { + // deduplicate from objects + String newName = renameOperationGroup(og); + newMethodGroupName = CodeNamer.getPlural(CodeNamer.getMethodGroupName(newName)); + } else if (methodGroupNamesLowerCase.contains(methodGroupName.toLowerCase(Locale.ROOT))) { + // deduplicate from other operation groups + String newName = renameOperationGroup(og); + newMethodGroupName = CodeNamer.getPlural(CodeNamer.getMethodGroupName(newName)); + } + + methodGroupNamesLowerCase.add(newMethodGroupName.toLowerCase(Locale.ROOT)); + if (JavaSettings.getInstance().isGenerateClientInterfaces()) { + methodGroupNamesLowerCase.add((newMethodGroupName + "Client").toLowerCase(Locale.ROOT)); + } + }); + + String clientNameLowerCase = Utils.getJavaName(codeModel).toLowerCase(Locale.ROOT); + if (methodGroupNamesLowerCase.contains(clientNameLowerCase) || objectNamesLowerCase.contains(clientNameLowerCase)) { + String name = Utils.getJavaName(codeModel); + String newName; + + final String keywordManagementClient = "ManagementClient"; + final String keywordClient = "Client"; + if (name.endsWith(keywordClient) && !name.endsWith(keywordManagementClient)) { + newName = name.substring(0, name.length() - keywordClient.length()) + keywordManagementClient; + } else if (name.endsWith(keywordManagementClient)) { + newName = name.substring(0, name.length() - keywordManagementClient.length()) + "Main" + keywordManagementClient; + } else { + newName = name + keywordManagementClient; + } + + LOGGER.info("Rename code model from '{}' to '{}'", name, newName); + codeModel.getLanguage().getJava().setName(newName); + } + + // deduplicate enums from objects + codeModel.getSchemas().getChoices().forEach(c -> validateChoiceName(c, objectNamesLowerCase)); + codeModel.getSchemas().getSealedChoices().forEach(c -> validateChoiceName(c, objectNamesLowerCase)); + + return codeModel; + } + + private static String renameOperationGroup(Metadata m) { + String name = Utils.getJavaName(m); + String newName = name + Constants.OPERATION_GROUP_DEDUPLICATE_SUFFIX; + LOGGER.info("Rename operation group from '{}' to '{}'", name, newName); + m.getLanguage().getJava().setName(newName); + return newName; + } + + private static void validateChoiceName(ValueSchema choice, Set objectNames) { + String name = Utils.getJavaName(choice); + if (objectNames.contains(name.toLowerCase(Locale.ROOT))) { + String newName = name + "Value"; + LOGGER.warn("Name conflict of choice with object '{}'", name); + LOGGER.info("Rename choice from '{}' to '{}'", name, newName); + choice.getLanguage().getJava().setName(newName); + } + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/transformer/OperationGroupFilter.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/transformer/OperationGroupFilter.java new file mode 100644 index 0000000000..907557c847 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/transformer/OperationGroupFilter.java @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.transformer; + +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.CodeModel; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.OperationGroup; +import com.microsoft.typespec.http.client.generator.core.extension.plugin.PluginLogger; +import com.microsoft.typespec.http.client.generator.mgmt.util.Utils; +import com.microsoft.typespec.http.client.generator.mgmt.FluentNamer; +import com.microsoft.typespec.http.client.generator.core.preprocessor.namer.CodeNamer; +import org.slf4j.Logger; + +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +public class OperationGroupFilter { + + private final Logger logger = new PluginLogger(FluentNamer.getPluginInstance(), OperationGroupFilter.class); + + private final Set javaNamesForPreserveModel; + + public OperationGroupFilter(Set javaNamesForPreserveModel) { + this.javaNamesForPreserveModel = javaNamesForPreserveModel; + } + + public CodeModel process(CodeModel codeModel) { + // remove operation group + List operationGroups = codeModel.getOperationGroups().stream().filter(og -> { + String methodGroupName = CodeNamer.getPlural(Utils.getJavaName(og)); + boolean remove = javaNamesForPreserveModel.contains(methodGroupName); + if (remove) { + logger.info("Removed operation group '{}'", methodGroupName); + } + return !remove; + }).collect(Collectors.toList()); + if (operationGroups.size() < codeModel.getOperationGroups().size()) { + codeModel.setOperationGroups(operationGroups); + } + + return codeModel; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/transformer/OperationGroupRenamer.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/transformer/OperationGroupRenamer.java new file mode 100644 index 0000000000..8e47ae8e02 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/transformer/OperationGroupRenamer.java @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.transformer; + +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.CodeModel; +import com.microsoft.typespec.http.client.generator.core.extension.plugin.PluginLogger; +import com.microsoft.typespec.http.client.generator.mgmt.util.Utils; +import com.microsoft.typespec.http.client.generator.mgmt.FluentNamer; +import com.microsoft.typespec.http.client.generator.core.preprocessor.namer.CodeNamer; +import org.slf4j.Logger; + +import java.util.Map; + +public class OperationGroupRenamer { + + private final Logger logger = new PluginLogger(FluentNamer.getPluginInstance(), OperationGroupRenamer.class); + + private final Map renameOperationGroup; + + public OperationGroupRenamer(Map renameOperationGroup) { + this.renameOperationGroup = renameOperationGroup; + } + + public CodeModel process(CodeModel codeModel) { + // rename operation group + codeModel.getOperationGroups().forEach(og -> { + String methodGroupName = CodeNamer.getPlural(Utils.getJavaName(og)); + String rename = renameOperationGroup.get(methodGroupName); + if (rename != null) { + og.getLanguage().getJava().setName(rename); + logger.info("Renamed operation group from '{}' to '{}'.", methodGroupName, rename); + } + }); + return codeModel; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/transformer/OperationNameNormalization.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/transformer/OperationNameNormalization.java new file mode 100644 index 0000000000..f37f52cd0f --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/transformer/OperationNameNormalization.java @@ -0,0 +1,224 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.transformer; + +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.ArraySchema; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.CodeModel; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.ObjectSchema; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Operation; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.OperationGroup; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Parameter; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.RequestParameterLocation; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Response; +import com.microsoft.typespec.http.client.generator.core.extension.plugin.PluginLogger; +import com.microsoft.typespec.http.client.generator.mgmt.model.WellKnownMethodName; +import com.microsoft.typespec.http.client.generator.mgmt.util.Utils; +import com.microsoft.typespec.http.client.generator.mgmt.FluentNamer; +import com.azure.core.http.HttpMethod; +import org.slf4j.Logger; + +import java.util.Arrays; +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.function.Function; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +/** + * Normalizes the names of common operations (list, get, delete). + */ +class OperationNameNormalization { + + private static final Logger LOGGER = new PluginLogger(FluentNamer.getPluginInstance(), OperationNameNormalization.class); + + private static final Pattern TRIM_LEADING_AND_TRAILING_FORWARD_SLASH = Pattern.compile("^(?:/*)?(.*?)(?:/*)?$"); + + public CodeModel process(CodeModel codeModel) { + codeModel.getOperationGroups().forEach(OperationNameNormalization::process); + return codeModel; + } + + private static final String SEGMENT_SUBSCRIPTIONS = "subscriptions"; + private static final String SEGMENT_RESOURCE_GROUPS = "resourceGroups"; + private static final String SEGMENT_PROVIDERS = "providers"; + + private static void process(OperationGroup operationGroup) { + Map renamePlan = makeRenamePlan(operationGroup); + applyRename(operationGroup, renamePlan); + } + + private static void applyRename(OperationGroup operationGroup, Map renamePlan) { + Optional> conflictNames = checkConflict(operationGroup, renamePlan); + conflictNames.ifPresent(names -> { + LOGGER.warn("Conflict operation name found after attempted rename '{}', in operation group '{}'", names, Utils.getJavaName(operationGroup)); + renamePlan.values().removeAll(names); + }); + + rename(operationGroup, renamePlan); + } + + private static Optional> checkConflict(OperationGroup operationGroup, Map renamePlan) { + List names = operationGroup.getOperations().stream() + .map(Utils::getJavaName) + .map(name -> renamePlan.getOrDefault(name, name)) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + + Set namesWithConflict = names.stream() + .collect(Collectors.groupingBy(Function.identity())) + .entrySet().stream() + .filter(e -> e.getValue().size() > 1) + .map(Map.Entry::getKey) + .collect(Collectors.toSet()); + + return namesWithConflict.isEmpty() ? Optional.empty() : Optional.of(namesWithConflict); + } + + private static void rename(OperationGroup operationGroup, Map renamePlan) { + operationGroup.getOperations().stream() + .filter(operation -> renamePlan.containsKey(Utils.getJavaName(operation))) + .forEach(operation -> { + String newName = renamePlan.get(Utils.getJavaName(operation)); + LOGGER.info("Rename operation from '{}' to '{}', in operation group '{}'", Utils.getJavaName(operation), newName, Utils.getJavaName(operationGroup)); + operation.getLanguage().getJava().setName(newName); + if (operation.getConvenienceApi() != null) { + operation.getConvenienceApi().getLanguage().getJava().setName(newName); + } + }); + } + + private static Map makeRenamePlan(OperationGroup operationGroup) { + final Set candidateWellKnownName = new HashSet<>(Arrays.asList( + WellKnownMethodName.LIST, + WellKnownMethodName.LIST_BY_RESOURCE_GROUP, + WellKnownMethodName.GET_BY_RESOURCE_GROUP, + WellKnownMethodName.DELETE)); + + Map renamePlan = new HashMap<>(); + + for (Operation operation : operationGroup.getOperations()) { + String path = operation.getRequests().iterator().next().getProtocol().getHttp().getPath().trim(); + Matcher matcher = TRIM_LEADING_AND_TRAILING_FORWARD_SLASH.matcher(path); + if (matcher.matches()) { + path = matcher.group(1); + } + String[] urlSegments = path.split(Pattern.quote("/")); + + String newName = null; + if (HttpMethod.GET.name().equalsIgnoreCase(operation.getRequests().iterator().next().getProtocol().getHttp().getMethod())) { + if (urlSegments.length == 8 // e.g. subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.AzureSphere/catalogs/{catalogName} + && urlSegments[0].equalsIgnoreCase(SEGMENT_SUBSCRIPTIONS) + && urlSegments[2].equalsIgnoreCase(SEGMENT_RESOURCE_GROUPS) + && urlSegments[4].equalsIgnoreCase(SEGMENT_PROVIDERS)) { + if (candidateWellKnownName.contains(WellKnownMethodName.GET_BY_RESOURCE_GROUP)) { + newName = WellKnownMethodName.GET_BY_RESOURCE_GROUP.getMethodName(); + + normalizePathParameterOrder(operation, urlSegments); + } + } else if ((urlSegments.length == 5 || urlSegments.length == 7) + && urlSegments[0].equalsIgnoreCase(SEGMENT_SUBSCRIPTIONS) + && isPossiblePagedList(operation) + && hasArrayInResponse(operation.getResponses())) { + if (candidateWellKnownName.contains(WellKnownMethodName.LIST_BY_RESOURCE_GROUP)) { + if ((urlSegments.length == 7 && urlSegments[2].equalsIgnoreCase(SEGMENT_RESOURCE_GROUPS)) + || (urlSegments.length == 5 && !urlSegments[2].equalsIgnoreCase(SEGMENT_PROVIDERS))) { + newName = WellKnownMethodName.LIST_BY_RESOURCE_GROUP.getMethodName(); + } + } + if (candidateWellKnownName.contains(WellKnownMethodName.LIST)) { + if (urlSegments.length == 5 && urlSegments[2].equalsIgnoreCase(SEGMENT_PROVIDERS)) { + // e.g. subscriptions/{subscriptionId}/providers/Microsoft.AzureSphere/catalogs + newName = WellKnownMethodName.LIST.getMethodName(); + } + } + } + } else if (HttpMethod.DELETE.name().equalsIgnoreCase(operation.getRequests().iterator().next().getProtocol().getHttp().getMethod())) { + if (urlSegments.length == 8 + && urlSegments[0].equalsIgnoreCase(SEGMENT_SUBSCRIPTIONS) + && urlSegments[2].equalsIgnoreCase(SEGMENT_RESOURCE_GROUPS) + && urlSegments[4].equalsIgnoreCase(SEGMENT_PROVIDERS)) { + if (candidateWellKnownName.contains(WellKnownMethodName.DELETE)) { + newName = WellKnownMethodName.DELETE.getMethodName(); + + normalizePathParameterOrder(operation, urlSegments); + } + } + } + + if (newName != null) { + if (!newName.equals(Utils.getJavaName(operation))) { + renamePlan.put(Utils.getJavaName(operation), newName); + } + candidateWellKnownName.remove(WellKnownMethodName.fromMethodName(newName)); + } + } + + return renamePlan; + } + + private static void normalizePathParameterOrder(Operation operation, String[] urlSegments) { + // check path parameter order + String resourceGroupParameterName = parameterSerializedName(urlSegments[3]); + operation.getRequests().forEach(request -> { + List pathMethodParameters = request.getParameters().stream() + .filter(OperationNameNormalization::isPathParameterInMethod) + .collect(Collectors.toList()); + if (pathMethodParameters.size() == 2 + && resourceGroupParameterName.equals(pathMethodParameters.get(1).getLanguage().getDefault().getSerializedName())) { + // resourceGroup parameter and resourceName parameter in reverse order + String resourceNameParameterName = parameterSerializedName(urlSegments[7]); + + LOGGER.info("Reorder '{}' parameter and '{}' parameter, in operation '{}'", resourceGroupParameterName, resourceNameParameterName, Utils.getJavaName(operation)); + + int rgIndex = -1; + int nameIndex = -1; + for (int i = 0; i < request.getParameters().size(); ++i) { + Parameter p = request.getParameters().get(i); + if (isPathParameterInMethod(p)) { + if (resourceGroupParameterName.equals(p.getLanguage().getDefault().getSerializedName())) { + rgIndex = i; + } else if (resourceNameParameterName.equals(p.getLanguage().getDefault().getSerializedName())) { + nameIndex = i; + } + } + } + if (rgIndex >= 0 && nameIndex >= 0) { + Collections.swap(request.getParameters(), rgIndex, nameIndex); + } + } + }); + } + + private static boolean isPossiblePagedList(Operation operation) { + return (operation.getExtensions() != null && operation.getExtensions().getXmsPageable() != null); +// || (Utils.getJavaName(operation).equals(WellKnownMethodName.LIST) || Utils.getJavaName(operation).equals(WellKnownMethodName.LIST_BY_RESOURCE_GROUP)); + } + + private static boolean hasArrayInResponse(List responses) { + return responses.stream() + .anyMatch(r -> r.getSchema() instanceof ObjectSchema + && ((ObjectSchema) r.getSchema()).getProperties().stream().anyMatch(p -> p.getSerializedName().equals("value") && p.getSchema() instanceof ArraySchema)); + } + + private static boolean isPathParameterInMethod(Parameter parameter) { + return parameter.getImplementation() == Parameter.ImplementationLocation.METHOD + && parameter.getProtocol() != null + && parameter.getProtocol().getHttp() != null + && parameter.getProtocol().getHttp().getIn() == RequestParameterLocation.PATH; + } + + private static String parameterSerializedName(String parameterNameInUrl) { + if (parameterNameInUrl.startsWith("{") && parameterNameInUrl.endsWith("}")) { + parameterNameInUrl = parameterNameInUrl.substring(1, parameterNameInUrl.length() - 1); + } + return parameterNameInUrl; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/transformer/ResourcePropertyNormalization.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/transformer/ResourcePropertyNormalization.java new file mode 100644 index 0000000000..e93cd55aab --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/transformer/ResourcePropertyNormalization.java @@ -0,0 +1,108 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.transformer; + +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.ArraySchema; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.CodeModel; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.DictionarySchema; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.ObjectSchema; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Parameter; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Property; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Schema; +import com.microsoft.typespec.http.client.generator.core.extension.plugin.PluginLogger; +import com.microsoft.typespec.http.client.generator.mgmt.model.FluentType; +import com.microsoft.typespec.http.client.generator.mgmt.model.ResourceTypeName; +import com.microsoft.typespec.http.client.generator.mgmt.util.Utils; +import com.microsoft.typespec.http.client.generator.mgmt.FluentNamer; +import org.slf4j.Logger; + +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Normalizes resource properties as SubResource. + */ +public class ResourcePropertyNormalization { + + private static final Logger LOGGER = new PluginLogger(FluentNamer.getPluginInstance(), ResourcePropertyNormalization.class); + + public CodeModel process(CodeModel codeModel) { + // Heuristic, only consider type used in request parameter. + // Better to compare with sample request. + Set typesUsedInRequestParameters = codeModel.getOperationGroups().stream() + .flatMap(og -> og.getOperations().stream()) + .flatMap(o -> o.getRequests().stream()) + .flatMap(r -> r.getParameters().stream()) + .filter(Parameter::isRequired) + .filter(Utils::nonFlattenedParameter) + .map(Parameter::getSchema) + .filter(s -> s instanceof ObjectSchema) + .map(s -> (ObjectSchema) s) + .collect(Collectors.toSet()); + // And its 1st level properties. + typesUsedInRequestParameters.addAll(typesUsedInRequestParameters.stream() + .flatMap(s -> s.getProperties().stream()) + .map(Property::getSchema) + .filter(s -> s instanceof ObjectSchema) + .map(s -> (ObjectSchema) s) + .collect(Collectors.toSet()) + ); + + codeModel.getSchemas().getObjects().stream() + .filter(FluentType::nonResourceType) + .filter(typesUsedInRequestParameters::contains) + .forEach(compositeType -> { + List candidateProperties = compositeType.getProperties().stream() + .filter(p -> !p.isReadOnly()) + .collect(Collectors.toList()); + + candidateProperties.forEach(p -> { + Schema type = p.getSchema(); + if (type instanceof ObjectSchema) { + ObjectSchema candidateType = (ObjectSchema) type; + if (checkOnParentConvertToSubResource(candidateType)) { + p.setSchema(ResourceTypeNormalization.subResourceSchema()); + LOGGER.info("SubResource for property '{}.{}'", Utils.getJavaName(compositeType), p.getSerializedName()); + } + } else if (type instanceof ArraySchema && ((ArraySchema) type).getElementType() instanceof ObjectSchema) { + ArraySchema arrayType = ((ArraySchema) type); + ObjectSchema candidateType = (ObjectSchema) (arrayType.getElementType()); + if (checkConvertToSubResource(candidateType)) { + arrayType.setElementType(ResourceTypeNormalization.subResourceSchema()); + LOGGER.info("Array of SubResource for property '{}.{}'", Utils.getJavaName(compositeType), p.getSerializedName()); + } + } else if (type instanceof DictionarySchema && ((DictionarySchema) type).getElementType() instanceof ObjectSchema) { + DictionarySchema dictType = ((DictionarySchema) type); + ObjectSchema candidateType = (ObjectSchema) (dictType.getElementType()); + if (checkConvertToSubResource(candidateType)) { + dictType.setElementType(ResourceTypeNormalization.subResourceSchema()); + LOGGER.info("Dictionary of SubResource for property '{}.{}'", Utils.getJavaName(compositeType), p.getSerializedName()); + } + } + }); + }); + + return codeModel; + } + + private static boolean checkOnParentConvertToSubResource(ObjectSchema candidateType) { + boolean convert = false; + if (candidateType != null && candidateType.getParents() != null) { + Schema parentType = candidateType.getParents().getImmediate().get(0); + if (parentType instanceof ObjectSchema && !FluentType.nonResourceType((ObjectSchema) parentType)) { + convert = true; + } + } + return convert; + } + + private static boolean checkConvertToSubResource(ObjectSchema candidateType) { + boolean convert = false; + if (candidateType != null && !FluentType.nonResourceType(candidateType) && !ResourceTypeName.SUB_RESOURCE.equals(Utils.getJavaName(candidateType))) { + convert = true; + } + return convert; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/transformer/ResourceTypeNormalization.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/transformer/ResourceTypeNormalization.java new file mode 100644 index 0000000000..11a408100e --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/transformer/ResourceTypeNormalization.java @@ -0,0 +1,475 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.transformer; + +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.CodeModel; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.DictionarySchema; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Language; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Languages; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.ObjectSchema; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Property; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Relations; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Schema; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.StringSchema; +import com.microsoft.typespec.http.client.generator.core.extension.model.extensionmodel.XmsExtensions; +import com.microsoft.typespec.http.client.generator.core.extension.plugin.PluginLogger; +import com.microsoft.typespec.http.client.generator.mgmt.model.FluentType; +import com.microsoft.typespec.http.client.generator.mgmt.model.ResourceType; +import com.microsoft.typespec.http.client.generator.mgmt.model.ResourceTypeName; +import com.microsoft.typespec.http.client.generator.mgmt.util.Utils; +import com.microsoft.typespec.http.client.generator.mgmt.FluentNamer; +import com.azure.core.util.CoreUtils; +import org.slf4j.Logger; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Normalizes the base resource types based on its base type and properties. + */ +class ResourceTypeNormalization { + + private static final Logger LOGGER = new PluginLogger(FluentNamer.getPluginInstance(), ResourceTypeNormalization.class); + + // Move Resource, ProxyResource, TrackedResource as last to process. + // This provides chance for extra properties in these schemas to be extracted. + // Example: CustomResource extends ProxyResource extends Resource (systemData) + // If ProxyResource is processed before CustomResource, it would be replaced by the standard ProxyResource, hence lost systemData property. + // Hence, we need to have CustomResource processed first. + private static final Set LAST_TO_PROCESS_SCHEMA_NAMES = new HashSet<>(Arrays.asList( + ResourceTypeName.PROXY_RESOURCE, + ResourceTypeName.PROXY_RESOURCE_AUTO_GENERATED, + ResourceTypeName.TRACKED_RESOURCE, + ResourceTypeName.TRACKED_RESOURCE_AUTO_GENERATED, + ResourceTypeName.RESOURCE, + ResourceTypeName.RESOURCE_AUTO_GENERATED, + ResourceTypeName.AZURE_RESOURCE, + ResourceTypeName.AZURE_RESOURCE_AUTO_GENERATED + )); + + public CodeModel process(CodeModel codeModel) { + List objectSchemas = codeModel.getSchemas().getObjects(); + List moveToLast = objectSchemas.stream() + .filter(o -> LAST_TO_PROCESS_SCHEMA_NAMES.contains(Utils.getJavaName(o))) + .collect(Collectors.toList()); + objectSchemas.removeAll(moveToLast); + objectSchemas.addAll(moveToLast); + + objectSchemas.forEach(compositeType -> { + Optional parentType = getObjectParent(compositeType); + if (parentType.isPresent()) { + getSchemaResourceType(parentType.get()) + .ifPresent(type -> adaptForParentSchema(compositeType, parentType.get(), type)); + + if (FluentType.SYSTEM_DATA.getName().equals(Utils.getJavaName(parentType.get()))) { + adaptAsSystemData(compositeType); + } + } else { + if (compositeType.getExtensions() != null && compositeType.getExtensions().isXmsAzureResource()) { + tryAdaptAsResource(compositeType); + } + } + }); + + return codeModel; + } + + public static ObjectSchema subResourceSchema() { + return DUMMY_SUB_RESOURCE; + } + + private static final Set SUB_RESOURCE_FIELDS = new HashSet<>(Arrays.asList(ResourceTypeName.FIELD_ID)); + private static final Set PROXY_RESOURCE_FIELDS = new HashSet<>(Arrays.asList(ResourceTypeName.FIELD_ID, ResourceTypeName.FIELD_NAME, ResourceTypeName.FIELD_TYPE)); + private static final Set RESOURCE_FIELDS = new HashSet<>(Arrays.asList(ResourceTypeName.FIELD_ID, ResourceTypeName.FIELD_NAME, ResourceTypeName.FIELD_TYPE, ResourceTypeName.FIELD_LOCATION, ResourceTypeName.FIELD_TAGS)); + + private static final Set RESOURCE_EXTRA_FIELDS = new HashSet<>(Arrays.asList(ResourceTypeName.FIELD_LOCATION, ResourceTypeName.FIELD_TAGS)); + + private static final ObjectSchema DUMMY_SUB_RESOURCE = dummyResourceSchema(ResourceTypeName.SUB_RESOURCE); + private static final ObjectSchema DUMMY_PROXY_RESOURCE = dummyResourceSchema(ResourceTypeName.PROXY_RESOURCE); + private static final ObjectSchema DUMMY_RESOURCE = dummyResourceSchema(ResourceTypeName.RESOURCE); + + private static ObjectSchema dummyResourceSchema(String javaName) { + // follow https://github.com/Azure/azure-rest-api-specs/blob/main/specification/common-types/resource-management/v2/types.json + + ObjectSchema schema = new ObjectSchema(); + schema.setLanguage(new Languages()); + schema.getLanguage().setJava(new Language()); + schema.getLanguage().getJava().setName(javaName); + schema.setExtensions(new XmsExtensions()); + schema.getExtensions().setXmsAzureResource(true); + schema.setProperties(new ArrayList<>()); + + switch (javaName) { + case ResourceTypeName.SUB_RESOURCE: + addProperty(schema, ResourceTypeName.FIELD_ID, false); + break; + + case ResourceTypeName.PROXY_RESOURCE: + addProperty(schema, ResourceTypeName.FIELD_ID, true); + addProperty(schema, ResourceTypeName.FIELD_NAME, true); + addProperty(schema, ResourceTypeName.FIELD_TYPE, true); + break; + + case ResourceTypeName.RESOURCE: + addProperty(schema, ResourceTypeName.FIELD_ID, true); + addProperty(schema, ResourceTypeName.FIELD_NAME, true); + addProperty(schema, ResourceTypeName.FIELD_TYPE, true); + addProperty(schema, ResourceTypeName.FIELD_LOCATION, false); + addProperty(schema, ResourceTypeName.FIELD_TAGS, false); + break; + } + + return schema; + } + + private static void addProperty(ObjectSchema schema, String propertyName, boolean readOnly) { + Property property = new Property(); + property.setReadOnly(readOnly); + property.setSerializedName(propertyName); + + property.setLanguage(new Languages()); + property.getLanguage().setJava(new Language()); + property.getLanguage().getJava().setName(propertyName); + + // description + String description = ""; + switch (propertyName) { + case ResourceTypeName.FIELD_ID: + description = "the fully qualified resource ID for the resource"; + break; + case ResourceTypeName.FIELD_NAME: + description = "the name of the resource"; + break; + case ResourceTypeName.FIELD_TYPE: + description = "the type of the resource"; + break; + case ResourceTypeName.FIELD_LOCATION: + description = "the geo-location where the resource live"; + break; + case ResourceTypeName.FIELD_TAGS: + description = "the tags of the resource"; + break; + } + property.getLanguage().getJava().setDescription(description); + + // schema + if (ResourceTypeName.FIELD_TAGS.equals(propertyName)) { + DictionarySchema propertySchema = new DictionarySchema(); + propertySchema.setElementType(new StringSchema()); + property.setSchema(propertySchema); + } else { + property.setSchema(new StringSchema()); + } + + // x-ms-mutability + if (ResourceTypeName.FIELD_LOCATION.equals(propertyName)) { + property.setExtensions(new XmsExtensions()); + property.getExtensions().setXmsMutability(Arrays.asList("read", "create")); + } + + schema.getProperties().add(property); + } + + private static Optional getObjectParent(ObjectSchema compositeType) { + if (compositeType.getParents() == null || compositeType.getParents().getImmediate() == null) { + return Optional.empty(); + } else { + return compositeType.getParents().getImmediate().stream() + .filter(s -> s instanceof ObjectSchema) + .map(s -> (ObjectSchema) s) + .findFirst(); + } + } + + private static void tryAdaptAsResource(ObjectSchema compositeType) { + if (!getSchemaResourceType(compositeType).isPresent()) { + if (hasProperties(compositeType, RESOURCE_FIELDS)) { + addDummyParentType(compositeType, DUMMY_RESOURCE); + + compositeType.getProperties().removeIf(p -> (PROXY_RESOURCE_FIELDS.contains(p.getSerializedName()) && p.isReadOnly()) + || RESOURCE_EXTRA_FIELDS.contains(p.getSerializedName())); + + LOGGER.info("Add parent Resource, for '{}'", Utils.getJavaName(compositeType)); + } else if (hasProperties(compositeType, PROXY_RESOURCE_FIELDS)) { + addDummyParentType(compositeType, DUMMY_PROXY_RESOURCE); + + compositeType.getProperties().removeIf(p -> PROXY_RESOURCE_FIELDS.contains(p.getSerializedName()) && p.isReadOnly()); + + LOGGER.info("Add parent ProxyResource, for '{}'", Utils.getJavaName(compositeType)); + } + } + } + + private static void adaptAsSystemData(ObjectSchema compositeType) { + String previousName = Utils.getJavaName(compositeType); + compositeType.getLanguage().getJava().setName(FluentType.SYSTEM_DATA.getName()); + + LOGGER.info("Rename system data from '{}' to 'SystemData'", previousName); + + if (CoreUtils.isNullOrEmpty(compositeType.getProperties())) { + LOGGER.warn("Ignored properties {}, for {}", + compositeType.getProperties().stream().map(Utils::getJavaName).collect(Collectors.toList()), + previousName); + } + } + + private static Optional getSchemaResourceType(ObjectSchema compositeType) { + ResourceType type = null; + + String javaName = Utils.getJavaName(compositeType); + if (javaName.equals(ResourceTypeName.SUB_RESOURCE) || javaName.startsWith(ResourceTypeName.SUB_RESOURCE_AUTO_GENERATED)) { + type = ResourceType.SUB_RESOURCE; + } else if ( + javaName.equals(ResourceTypeName.PROXY_RESOURCE) + || javaName.startsWith(ResourceTypeName.PROXY_RESOURCE_AUTO_GENERATED) + || javaName.equals(ResourceTypeName.EXTENSION_RESOURCE) + ) { + type = ResourceType.PROXY_RESOURCE; + } else if (javaName.equals(ResourceTypeName.TRACKED_RESOURCE) || javaName.startsWith(ResourceTypeName.TRACKED_RESOURCE_AUTO_GENERATED)) { + type = ResourceType.RESOURCE; + } else if (javaName.equals(ResourceTypeName.RESOURCE) || javaName.startsWith(ResourceTypeName.RESOURCE_AUTO_GENERATED) + || javaName.equals(ResourceTypeName.AZURE_RESOURCE) || javaName.startsWith(ResourceTypeName.AZURE_RESOURCE_AUTO_GENERATED)) { + if (hasProperties(compositeType, RESOURCE_EXTRA_FIELDS)) { + type = ResourceType.RESOURCE; + } else if (hasProperties(compositeType, PROXY_RESOURCE_FIELDS)) { + type = ResourceType.PROXY_RESOURCE; + } else if (hasProperties(compositeType, SUB_RESOURCE_FIELDS)) { + type = ResourceType.SUB_RESOURCE; + } + } + + return Optional.ofNullable(type); + } + + private static void adaptForParentSchema(ObjectSchema compositeType, ObjectSchema parentType, ResourceType type) { + switch (type) { + case SUB_RESOURCE: + { + List extraProperties = getDeclaredProperties(parentType).stream() + .filter(p -> !SUB_RESOURCE_FIELDS.contains(p.getSerializedName())) + .filter(p -> !hasProperty(compositeType, p)) + .collect(Collectors.toList()); + compositeType.getProperties().addAll(extraProperties); + break; + } + case PROXY_RESOURCE: + { + List extraProperties = getDeclaredProperties(parentType).stream() + .filter(p -> !PROXY_RESOURCE_FIELDS.contains(p.getSerializedName())) + .filter(p -> !hasProperty(compositeType, p)) + .collect(Collectors.toList()); + compositeType.getProperties().addAll(extraProperties); + + List mutableProperties = getDeclaredProperties(parentType).stream() + .filter(p -> PROXY_RESOURCE_FIELDS.contains(p.getSerializedName())) + .filter(p -> !p.isReadOnly()) + .filter(p -> !hasProperty(compositeType, p)) + .collect(Collectors.toList()); + compositeType.getProperties().addAll(mutableProperties); + break; + } + case RESOURCE: + { + List extraProperties = getDeclaredProperties(parentType).stream() + .filter(p -> !RESOURCE_FIELDS.contains(p.getSerializedName())) + .filter(p -> !hasProperty(compositeType, p)) // avoid conflict with property in this type + .collect(Collectors.toList()); + compositeType.getProperties().addAll(extraProperties); + + // extra 2 properties in Resource is defined as mutable. So only check for properties in ProxyResource. + List mutableProperties = getDeclaredProperties(parentType).stream() + .filter(p -> PROXY_RESOURCE_FIELDS.contains(p.getSerializedName())) + .filter(p -> !p.isReadOnly()) + .filter(p -> !hasProperty(compositeType, p)) + .collect(Collectors.toList()); + compositeType.getProperties().addAll(mutableProperties); + break; + } + } + + if (!type.getClassName().equals(Utils.getJavaName(parentType))) { + switch (type) { + case RESOURCE: + { + replaceDummyParentType(compositeType, DUMMY_RESOURCE); + break; + } + case PROXY_RESOURCE: + { + replaceDummyParentType(compositeType, DUMMY_PROXY_RESOURCE); + break; + } + case SUB_RESOURCE: + { + replaceDummyParentType(compositeType, DUMMY_SUB_RESOURCE); + break; + } + } + + LOGGER.info("Change parent from '{}' to '{}', for '{}'", Utils.getJavaName(parentType), type.getClassName(), Utils.getJavaName(compositeType)); + } + + if (type.getClassName().equals(Utils.getJavaName(compositeType))) { + // replace the compositeType to the ResourceType + compositeType.getParents().getImmediate().clear(); + compositeType.getParents().getAll().clear(); + + String previousName = Utils.getJavaName(compositeType); + compositeType.getLanguage().getJava().setName(type.getClassName()); + + switch (type) { + case RESOURCE: + { + compositeType.setProperties(DUMMY_RESOURCE.getProperties()); + break; + } + case PROXY_RESOURCE: + { + compositeType.setProperties(DUMMY_PROXY_RESOURCE.getProperties()); + break; + } + case SUB_RESOURCE: + { + compositeType.setProperties(DUMMY_SUB_RESOURCE.getProperties()); + break; + } + } + + LOGGER.info("Rename schema from '{}' to '{}'", previousName, type.getClassName()); + } + } + + /* + * Recursively get all properties and all its parents' properties. + */ + private static List getDeclaredProperties(ObjectSchema parentType) { + return parentType == null + ? Collections.emptyList() + : Stream.concat( + parentType.getProperties().stream(), + getDeclaredProperties(getObjectParent(parentType).orElse(null)).stream() + ).collect(Collectors.toList()); + } + + private static void addDummyParentType(ObjectSchema compositeType, ObjectSchema parentType) { + if (compositeType.getParents() == null) { + compositeType.setParents(new Relations()); + } + if (compositeType.getParents().getImmediate() == null) { + compositeType.getParents().setImmediate(new ArrayList<>()); + } + if (compositeType.getParents().getAll() == null) { + compositeType.getParents().setAll(new ArrayList<>()); + } + compositeType.getParents().getImmediate().add(0, parentType); + compositeType.getParents().getAll().add(0, parentType); + + if (compositeType.getChildren() != null && !CoreUtils.isNullOrEmpty(compositeType.getChildren().getAll())) { + // add parent to children of this type as well + compositeType.getChildren().getAll().stream() + .filter(o -> o instanceof ObjectSchema) + .map(o -> (ObjectSchema) o) + .forEach(o -> o.getParents().getAll().add(parentType)); + + // try to make the Resource/ProxyResource as the first parent, for multiple inheritance (as only first parent is kept) + compositeType.getChildren().getAll().stream() + .filter(o -> o instanceof ObjectSchema) + .map(o -> (ObjectSchema) o) + .filter(o -> o.getParents().getImmediate().stream().filter(o1 -> o1 instanceof ObjectSchema).count() >= 2) + .forEach(child -> { + int indexFirstParent = -1; + int index; + for (index = 0; index < child.getParents().getImmediate().size(); ++index) { + Schema parent = child.getParents().getImmediate().get(index); + if (parent instanceof ObjectSchema) { + if (indexFirstParent == -1) { + indexFirstParent = index; + } + if (((ObjectSchema) parent).getParents() != null && ((ObjectSchema) parent).getParents().getAll().contains(compositeType)) { + break; + } + } + } + if (indexFirstParent >= 0 + && index < child.getParents().getImmediate().size() + && index > indexFirstParent) { + LOGGER.info("Change parent order between '{}' and '{}', for '{}'", + Utils.getJavaName(child.getParents().getImmediate().get(indexFirstParent)), + Utils.getJavaName(child.getParents().getImmediate().get(index)), + Utils.getJavaName(child)); + Collections.swap(child.getParents().getImmediate(), indexFirstParent, index); + } + }); + } + } + + private static void replaceDummyParentType(ObjectSchema compositeType, ObjectSchema parentType) { + ObjectSchema currentParentType = getObjectParent(compositeType).get(); + + // remove parent from type + Iterator itor = compositeType.getParents().getImmediate().iterator(); + while (itor.hasNext()) { + Schema type = itor.next(); + if (type == currentParentType) { + itor.remove(); + break; + } + } + itor = compositeType.getParents().getAll().iterator(); + while (itor.hasNext()) { + Schema type = itor.next(); + if (type == currentParentType) { + itor.remove(); + break; + } + } + + // remove type from parent + if (currentParentType.getChildren() != null) { + if (currentParentType.getChildren().getImmediate() != null) { + itor = currentParentType.getChildren().getImmediate().iterator(); + while (itor.hasNext()) { + Schema type = itor.next(); + if (type == compositeType) { + itor.remove(); + break; + } + } + } + if (currentParentType.getChildren().getAll() != null) { + itor = currentParentType.getChildren().getAll().iterator(); + while (itor.hasNext()) { + Schema type = itor.next(); + if (type == compositeType) { + itor.remove(); + break; + } + } + } + } + + // add parent type + addDummyParentType(compositeType, parentType); + } + + private static boolean hasProperties(ObjectSchema compositeType, Set fieldNames) { + if (compositeType.getProperties() == null) { + return false; + } + return compositeType.getProperties().stream().map(Property::getSerializedName).collect(Collectors.toSet()).containsAll(fieldNames); + } + + private static boolean hasProperty(ObjectSchema compositeType, Property property) { + return compositeType.getProperties() != null && compositeType.getProperties().stream() + .anyMatch(p -> Utils.getJavaName(p) != null && Utils.getJavaName(p).equals(Utils.getJavaName(property))); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/transformer/ResponseStatusCodeNormalization.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/transformer/ResponseStatusCodeNormalization.java new file mode 100644 index 0000000000..32b5ea0d36 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/transformer/ResponseStatusCodeNormalization.java @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.transformer; + +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.CodeModel; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Response; +import com.microsoft.typespec.http.client.generator.core.extension.plugin.PluginLogger; +import com.microsoft.typespec.http.client.generator.mgmt.util.Utils; +import com.microsoft.typespec.http.client.generator.mgmt.FluentNamer; +import com.azure.core.http.HttpMethod; +import org.slf4j.Logger; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +public class ResponseStatusCodeNormalization { + + private static final Logger LOGGER = new PluginLogger(FluentNamer.getPluginInstance(), ResponseStatusCodeNormalization.class); + + private static final boolean REMOVE_404_IN_GET_RESPONSE = true; + + public CodeModel process(CodeModel codeModel) { + codeModel.getOperationGroups().stream().flatMap(og -> og.getOperations().stream()) + // only for GET method + .filter(o -> o.getRequests().stream() + .anyMatch(r -> r.getProtocol() != null && r.getProtocol().getHttp() != null + && HttpMethod.GET.name().equalsIgnoreCase(r.getProtocol().getHttp().getMethod()))) + .forEach(operation -> { + List responsesToRemove = new ArrayList<>(); + for (Response response : operation.getResponses()) { + if (response.getProtocol() != null && response.getProtocol().getHttp() != null && response.getProtocol().getHttp().getStatusCodes() != null) { + if (response.getProtocol().getHttp().getStatusCodes().contains("404")) { + LOGGER.warn("Operation '{}' expect '404' status code, in group '{}'", + Utils.getJavaName(operation), Utils.getJavaName(operation.getOperationGroup())); + + if (REMOVE_404_IN_GET_RESPONSE) { + String operationNameInLower = Utils.getJavaName(operation).toLowerCase(Locale.ROOT); + if (operationNameInLower.startsWith("get") || operationNameInLower.startsWith("list")) { + LOGGER.info("Remove '404' status code in operation '{}', in group '{}'", + Utils.getJavaName(operation), Utils.getJavaName(operation.getOperationGroup())); + if (response.getProtocol().getHttp().getStatusCodes().size() == 1) { + // remove the response with only 404 + responsesToRemove.add(response); + } else { + response.getProtocol().getHttp().getStatusCodes().remove("404"); + } + } + } + } + } + } + if (!responsesToRemove.isEmpty()) { + operation.getResponses().removeAll(responsesToRemove); + } + }); + + return codeModel; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/transformer/SchemaCleanup.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/transformer/SchemaCleanup.java new file mode 100644 index 0000000000..b0ace43fbe --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/transformer/SchemaCleanup.java @@ -0,0 +1,213 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.transformer; + +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.ArraySchema; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.ChoiceSchema; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.CodeModel; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.DictionarySchema; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.ObjectSchema; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Parameter; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Property; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Response; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Schema; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.SealedChoiceSchema; +import com.microsoft.typespec.http.client.generator.core.extension.plugin.PluginLogger; +import com.microsoft.typespec.http.client.generator.mgmt.model.FluentType; +import com.microsoft.typespec.http.client.generator.mgmt.util.Utils; +import com.microsoft.typespec.http.client.generator.mgmt.FluentNamer; +import org.slf4j.Logger; + +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Cleans up unused flattened types. + */ +public class SchemaCleanup { + + private static final Logger LOGGER = new PluginLogger(FluentNamer.getPluginInstance(), SchemaCleanup.class); + + private final Set javaNamesForPreserveModel; + + public SchemaCleanup(Set javaNamesForPreserveModel) { + this.javaNamesForPreserveModel = javaNamesForPreserveModel; + } + + public CodeModel process(CodeModel codeModel) { + final int maxTryCount = 5; // try a few time for recursive removal (e.g., 1st pass model removed, 2nd pass model used as its properties removed) + + boolean codeModelModified = true; + for (int i = 0; i < maxTryCount && codeModelModified; ++i) { + codeModelModified = tryCleanup(codeModel, javaNamesForPreserveModel); + } + + return codeModel; + } + + private static boolean tryCleanup(CodeModel codeModel, Set javaNamesForPreserveModel) { + Set schemasNotInUse = codeModel.getSchemas().getObjects().stream() +// .filter(SchemaCleanup::hasFlattenedExtension) + .filter(schema -> schema.getChildren() == null || schema.getChildren().getImmediate() == null + || schema.getChildren().getImmediate().isEmpty()) // no children + .filter(schema -> schema.getParents() == null || schema.getParents().getImmediate() == null + || schema.getParents().getImmediate().stream().allMatch(s -> { + if (s instanceof ObjectSchema) { + return !FluentType.nonResourceType((ObjectSchema) s); + } else { + return false; + } + })) + .collect(Collectors.toSet()); + + Set choicesSchemasNotInUse = new HashSet<>(codeModel.getSchemas().getSealedChoices()); + choicesSchemasNotInUse.addAll(codeModel.getSchemas().getChoices()); + + Set schemasInUse = new HashSet<>(); + if (!schemasNotInUse.isEmpty() || !choicesSchemasNotInUse.isEmpty()) { + // properties of object + Set propertiesOfObject = codeModel.getSchemas().getObjects().stream() + .filter(o -> { + String name = Utils.getJavaName(o); + return FluentType.nonSystemData(name) && FluentType.nonManagementError(name); + }) + .flatMap(s -> s.getProperties().stream() +// .filter(Utils::nonFlattenedProperty) + .map(Property::getSchema) + .map(SchemaCleanup::schemaOrElementInCollection) + .filter(Objects::nonNull) + .filter(s1 -> !Objects.equals(s, s1)) // schema of property is not the same of itself, solve the simplest recursive reference case + ) + .collect(Collectors.toSet()); + schemasNotInUse.removeAll(propertiesOfObject); + choicesSchemasNotInUse.removeAll(propertiesOfObject); + schemasInUse.addAll(propertiesOfObject); + } + if (!schemasNotInUse.isEmpty() || !choicesSchemasNotInUse.isEmpty()) { + // operation requests + Set requests = codeModel.getOperationGroups().stream() + .flatMap(og -> og.getOperations().stream()) + .flatMap(o -> o.getRequests().stream()) + .flatMap(r -> r.getParameters().stream()) + .map(Parameter::getSchema) + .map(SchemaCleanup::schemaOrElementInCollection) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + schemasNotInUse.removeAll(requests); + choicesSchemasNotInUse.removeAll(requests); + schemasInUse.addAll(requests); + } + if (!schemasNotInUse.isEmpty() || !choicesSchemasNotInUse.isEmpty()) { + // operation responses + Set responses = codeModel.getOperationGroups().stream() + .flatMap(og -> og.getOperations().stream()) + .flatMap(o -> o.getResponses().stream()) + .map(Response::getSchema) + .map(SchemaCleanup::schemaOrElementInCollection) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + schemasNotInUse.removeAll(responses); + choicesSchemasNotInUse.removeAll(responses); + schemasInUse.addAll(responses); + } + if (!schemasNotInUse.isEmpty() || !choicesSchemasNotInUse.isEmpty()) { + // operation exception + Set exceptions = codeModel.getOperationGroups().stream() + .flatMap(og -> og.getOperations().stream()) + .flatMap(o -> o.getExceptions().stream()) + .map(Response::getSchema) + .map(SchemaCleanup::schemaOrElementInCollection) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + schemasNotInUse.removeAll(exceptions); + choicesSchemasNotInUse.removeAll(exceptions); + schemasInUse.addAll(exceptions); + } + if (!schemasNotInUse.isEmpty() || !choicesSchemasNotInUse.isEmpty()) { + // parent schema as Dictionary or Array + Set elementsInParentCollection = schemasInUse.stream() + .flatMap(s -> { + if (s instanceof ObjectSchema) { + ObjectSchema objectSchema = (ObjectSchema) s; + if (objectSchema.getParents() == null || objectSchema.getParents().getAll() == null) { + return Stream.empty(); + } + return objectSchema.getParents().getAll() + .stream() + .filter(p -> p instanceof DictionarySchema || p instanceof ArraySchema) + .map(SchemaCleanup::schemaOrElementInCollection); + } + return Stream.empty(); + }) + .collect(Collectors.toSet()); + schemasNotInUse.removeAll(elementsInParentCollection); + choicesSchemasNotInUse.removeAll(elementsInParentCollection); + + // discriminators + Set discriminators = schemasInUse.stream() + .map(s -> { + if (s instanceof ObjectSchema && ((ObjectSchema) s).getDiscriminator() != null) { + return ((ObjectSchema) s).getDiscriminator().getProperty().getSchema(); + } else { + return null; + } + }) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + schemasNotInUse.removeAll(discriminators); + choicesSchemasNotInUse.removeAll(discriminators); + } + + AtomicBoolean codeModelModified = new AtomicBoolean(false); + + codeModel.getSchemas().getObjects().removeIf(s -> { + boolean unused = schemasNotInUse.contains(s) && !javaNamesForPreserveModel.contains(Utils.getJavaName(s)); + if (unused) { + LOGGER.info("Remove unused object schema '{}'", Utils.getJavaName(s)); + codeModelModified.set(true); + } + return unused; + }); + + codeModel.getSchemas().getSealedChoices().removeIf(s -> { + boolean unused = choicesSchemasNotInUse.contains(s) && !javaNamesForPreserveModel.contains(Utils.getJavaName(s)); + if (unused) { + LOGGER.info("Remove unused sealed choice schema '{}'", Utils.getJavaName(s)); + codeModelModified.set(true); + } + return unused; + }); + + codeModel.getSchemas().getChoices().removeIf(s -> { + boolean unused = choicesSchemasNotInUse.contains(s) && !javaNamesForPreserveModel.contains(Utils.getJavaName(s)); + if (unused) { + LOGGER.info("Remove unused choice schema '{}'", Utils.getJavaName(s)); + codeModelModified.set(true); + } + return unused; + }); + + return codeModelModified.get(); + } + + private static Schema schemaOrElementInCollection(Schema schema) { + if (schema instanceof ArraySchema) { + return schemaOrElementInCollection(((ArraySchema) schema).getElementType()); + } else if (schema instanceof DictionarySchema) { + return schemaOrElementInCollection(((DictionarySchema) schema).getElementType()); + } else if (schema instanceof ObjectSchema || schema instanceof ChoiceSchema || schema instanceof SealedChoiceSchema) { + return schema; + } else { + return null; + } + } + +// private static boolean hasFlattenedExtension(Schema schema) { +// return schema.getExtensions() != null && schema.getExtensions().isXmsFlattened(); +// } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/transformer/SchemaNameNormalization.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/transformer/SchemaNameNormalization.java new file mode 100644 index 0000000000..32a60cb0cb --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/transformer/SchemaNameNormalization.java @@ -0,0 +1,393 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.transformer; + +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.ArraySchema; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.ChoiceSchema; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.CodeModel; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Metadata; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.ObjectSchema; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Operation; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.OperationGroup; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Parameter; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Property; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.RequestParameterLocation; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Schema; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.SealedChoiceSchema; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Value; +import com.microsoft.typespec.http.client.generator.core.extension.plugin.PluginLogger; +import com.microsoft.typespec.http.client.generator.mgmt.util.Utils; +import com.microsoft.typespec.http.client.generator.mgmt.FluentNamer; +import com.microsoft.typespec.http.client.generator.core.preprocessor.namer.CodeNamer; +import com.azure.core.util.CoreUtils; +import org.slf4j.Logger; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Normalize the names of some unnamed schemas. + */ +public class SchemaNameNormalization { + + private static final Logger LOGGER = new PluginLogger(FluentNamer.getPluginInstance(), SchemaNameNormalization.class); + + private final Map nameOverridePlan = new HashMap<>(); + + public SchemaNameNormalization(Map nameOverridePlan) { + nameOverridePlan.forEach((k, v) -> { + char[] kCharArray = k.toCharArray(); + char[] vCharArray = v.toCharArray(); + + kCharArray[0] = Character.toLowerCase(kCharArray[0]); + vCharArray[0] = Character.toLowerCase(vCharArray[0]); + this.nameOverridePlan.put(new String(kCharArray), new String(vCharArray)); + + kCharArray[0] = Character.toUpperCase(kCharArray[0]); + vCharArray[0] = Character.toUpperCase(vCharArray[0]); + this.nameOverridePlan.put(new String(kCharArray), new String(vCharArray)); + }); + } + + public CodeModel process(CodeModel codeModel) { + codeModel = namingOverride(codeModel); + Set names = new HashSet<>(); + codeModel = normalizeUnnamedAdditionalProperties(codeModel, names); + codeModel = normalizeUnnamedBaseType(codeModel, names); + codeModel = normalizeUnnamedObjectTypeInArray(codeModel, names); // after normalizeUnnamedBaseType + codeModel = normalizeUnnamedChoiceType(codeModel, names); + codeModel = normalizeUnnamedRequestBody(codeModel, names); + return codeModel; + } + + protected CodeModel normalizeUnnamedObjectTypeInArray(CodeModel codeModel, Set names) { + final String prefix = "Components"; + final String postfix = "Items"; + + List unnamedObjectSchemas = codeModel.getSchemas().getObjects().stream() + .filter(s -> { + String name = Utils.getDefaultName(s); + return name.startsWith(prefix) && name.endsWith(postfix); + }) + .collect(Collectors.toList()); + if (!unnamedObjectSchemas.isEmpty()) { + unnamedObjectSchemas.forEach(s -> renameSchema(codeModel, s, names)); + } + return codeModel; + } + + protected CodeModel normalizeUnnamedChoiceType(CodeModel codeModel, Set names) { + List unnamedChoiceSchemas = codeModel.getSchemas().getChoices().stream() + .filter(s -> isUnnamedChoice(Utils.getDefaultName(s))) + .collect(Collectors.toList()); + if (!unnamedChoiceSchemas.isEmpty()) { + unnamedChoiceSchemas.forEach(s -> renameSchema(codeModel, s, names)); + } + + List unnamedSealedChoiceSchemas = codeModel.getSchemas().getSealedChoices().stream() + .filter(s -> isUnnamedChoice(Utils.getDefaultName(s))) + .collect(Collectors.toList()); + if (!unnamedSealedChoiceSchemas.isEmpty()) { + unnamedSealedChoiceSchemas.forEach(s -> renameSchema(codeModel, s, names)); + } + + return codeModel; + } + + private static boolean isUnnamedChoice(String name) { + // unnamed choice type is named by modelerfour as e.g. Enum11 + final String prefix = "Enum"; + + boolean unnamed = false; + if (name.startsWith(prefix)) { + String restName = name.substring(prefix.length()); + try { + Integer.parseInt(restName); + unnamed = true; + } catch (NumberFormatException e) { + // ignore + } + } + return unnamed; + } + + private static void renameSchema(CodeModel codeModel, Schema schema, Set names) { + final boolean deduplicate = false; + + // rename based on schema and property + for (ObjectSchema compositeType : codeModel.getSchemas().getObjects()) { + Optional property = compositeType.getProperties().stream() + .filter(p -> p.getSchema() == schema) + .findFirst(); + if (property.isPresent()) { + String newName = Utils.getDefaultName(compositeType) + CodeNamer.toPascalCase(property.get().getSerializedName()); + newName = rename(newName, names, deduplicate); + LOGGER.warn("Rename schema from '{}' to '{}', based on parent schema '{}' and property '{}'", + Utils.getDefaultName(schema), newName, Utils.getDefaultName(compositeType), property.get().getSerializedName()); + schema.getLanguage().getDefault().setName(newName); + return; + } + } + + // rename based for object in array + for (ObjectSchema compositeType : codeModel.getSchemas().getObjects()) { + Optional arrayProperty = compositeType.getProperties().stream() + .filter(p -> p.getSchema() instanceof ArraySchema) + .filter(p -> ((ArraySchema) p.getSchema()).getElementType() == schema) + .findFirst(); + if (arrayProperty.isPresent()) { + String newName = Utils.getDefaultName(compositeType) + CodeNamer.toPascalCase(Utils.getSingular(arrayProperty.get().getSerializedName())); + newName = rename(newName, names, deduplicate); + LOGGER.warn("Rename schema from '{}' to '{}', based on parent schema '{}' and property '{}'", + Utils.getDefaultName(schema), newName, Utils.getDefaultName(compositeType), arrayProperty.get().getSerializedName()); + schema.getLanguage().getDefault().setName(newName); + return; + } + } + + // rename based on operation and parameter + for (OperationGroup operationGroup : codeModel.getOperationGroups()) { + for (Operation operation : operationGroup.getOperations()) { + Optional parameter = Stream.concat(operation.getParameters().stream(), operation.getRequests().stream().flatMap(r -> r.getParameters().stream())) + .filter(p -> p.getSchema() == schema) + .findFirst(); + if (parameter.isPresent()) { + String newName = Utils.getDefaultName(operationGroup) + CodeNamer.toPascalCase(Utils.getDefaultName(parameter.get())); + newName = rename(newName, names, deduplicate); + LOGGER.warn("Rename schema from '{}' to '{}', based on operation group '{}'", Utils.getDefaultName(schema), newName, Utils.getDefaultName(operationGroup)); + schema.getLanguage().getDefault().setName(newName); + return; + } + } + } + for (OperationGroup operationGroup : codeModel.getOperationGroups()) { + for (Operation operation : operationGroup.getOperations()) { + Optional parameter = Stream.concat(operation.getParameters().stream(), operation.getRequests().stream().flatMap(r -> r.getParameters().stream())) + .filter(p -> (p.getSchema() instanceof ArraySchema) && ((ArraySchema) p.getSchema()).getElementType() == schema) + .findFirst(); + if (parameter.isPresent()) { + String newName = Utils.getDefaultName(operationGroup) + CodeNamer.toPascalCase(Utils.getDefaultName(parameter.get())); + newName = rename(newName, names, deduplicate); + LOGGER.warn("Rename schema from '{}' to '{}', based on operation group '{}'", Utils.getDefaultName(schema), newName, Utils.getDefaultName(operationGroup)); + schema.getLanguage().getDefault().setName(newName); + return; + } + } + } + } + + protected CodeModel normalizeUnnamedAdditionalProperties(CodeModel codeModel, Set names) { + // unnamed type is named by modelerfour as e.g. ComponentsQit0EtSchemasManagedclusterpropertiesPropertiesIdentityprofileAdditionalproperties + + final String prefix = "Components"; + final String postfix = "Additionalproperties"; + + codeModel.getSchemas().getDictionaries().stream() + .filter(s -> s.getElementType() instanceof ObjectSchema) + .forEach(dict -> { + ObjectSchema schema = (ObjectSchema) dict.getElementType(); + + List subtypes = new ArrayList<>(); + subtypes.add(schema); + if (schema.getChildren() != null && schema.getChildren().getAll() != null) { + subtypes.addAll(schema.getChildren().getAll()); + } + + for (Schema type : subtypes) { + String name = Utils.getDefaultName(type); + if (name.startsWith(prefix) && name.endsWith(postfix)) { + String newName = Utils.getDefaultName(dict); + newName = rename(newName, names); + type.getLanguage().getDefault().setName(newName); + LOGGER.warn("Rename schema default name, from '{}' to '{}'", name, newName); + } + } + }); + + return codeModel; + } + + protected CodeModel normalizeUnnamedBaseType(CodeModel codeModel, Set names) { + // unnamed base type is named by modelerfour as e.g. Components1Q1Og48SchemasManagedclusterAllof1 + + final String prefix = "Components"; + final String allOf = "Allof"; + + codeModel.getSchemas().getObjects().forEach(schema -> { + String name = Utils.getDefaultName(schema); + if (schema.getChildren() != null && !CoreUtils.isNullOrEmpty(schema.getChildren().getImmediate()) + && name.startsWith(prefix) && name.contains(allOf)) { + int index = name.lastIndexOf(allOf) + allOf.length(); + boolean unnamed = false; + String restName = name.substring(index); + if (restName.isEmpty()) { + unnamed = true; + } else { + try { + Integer.parseInt(restName); + unnamed = true; + } catch (NumberFormatException e) { + // ignore + } + } + + if (unnamed) { + Schema firstChild = schema.getChildren().getImmediate().iterator().next(); + String newName = "Base" + Utils.getDefaultName(firstChild); + newName = rename(newName, names); + schema.getLanguage().getDefault().setName(newName); + LOGGER.warn("Rename schema default name, from '{}' to '{}'", name, newName); + } + } + }); + + return codeModel; + } + + protected CodeModel normalizeUnnamedRequestBody(CodeModel codeModel, Set names) { + // unnamed request body is named by modelerfour as e.g. Paths1Ezr0XyApplicationsApplicationIdMicrosoftGraphGetmembergroupsPostRequestbodyContentApplicationJsonSchema + + final String prefix = "Paths"; + final String postfix = "Schema"; + final String requestBody = "Requestbody"; + + codeModel.getOperationGroups().forEach(og -> { + og.getOperations().forEach(operation -> { + operation.getRequests().forEach(request -> { + Optional bodySchemaOpt = request.getParameters().stream() + .filter(p -> p.getSchema() != null && p.getProtocol() != null && p.getProtocol().getHttp() != null && p.getProtocol().getHttp().getIn() == RequestParameterLocation.BODY) + .map(Value::getSchema) + .findFirst(); + if (bodySchemaOpt.isPresent()) { + Schema schema = bodySchemaOpt.get(); + String name = Utils.getDefaultName(schema); + if (name.startsWith(prefix) && name.endsWith(postfix) && name.contains(requestBody)) { + String newName = Utils.getDefaultName(og) + Utils.getDefaultName(operation) + "RequestBody"; + newName = rename(newName, names); + schema.getLanguage().getDefault().setName(newName); + LOGGER.warn("Rename schema default name, from '{}' to '{}'", name, newName); + } + } + }); + }); + }); + + return codeModel; + } + + private static String rename(String name, Set names) { + return rename(name, names, true); + } + + private static String rename(String name, Set names, boolean deduplicate) { + // modelerfour does a bad job of deduplicate on unnamed Enum, so deduplicate=false when processing unnamed Enum + if (!deduplicate || !names.contains(name)) { + names.add(name); + } else { + final int maxTry = 100; + int index = 1; + while (index < maxTry) { + String name1 = name + index; + if (!names.contains(name1)) { + names.add(name1); + return name1; + } + ++index; + } + } + return name; + } + + private CodeModel namingOverride(CodeModel codeModel) { + if (!nameOverridePlan.isEmpty()) { + overrideName(codeModel); + + codeModel.getSchemas().getObjects().forEach(this::overrideName); + codeModel.getSchemas().getObjects().stream() + .flatMap(o -> o.getProperties().stream()) + .forEach(this::overrideName); + + codeModel.getSchemas().getAnds().forEach(this::overrideName); + codeModel.getSchemas().getChoices().forEach(this::overrideName); + codeModel.getSchemas().getSealedChoices().forEach(this::overrideName); + codeModel.getSchemas().getDictionaries().forEach(this::overrideName); + + codeModel.getOperationGroups().forEach(this::overrideName); + codeModel.getOperationGroups().stream() + .flatMap(og -> og.getOperations().stream()) + .forEach(this::overrideName); + codeModel.getOperationGroups().stream() + .flatMap(og -> og.getOperations().stream()) + .flatMap(o -> o.getParameters().stream()) + .forEach(this::overrideName); + codeModel.getOperationGroups().stream() + .flatMap(og -> og.getOperations().stream()) + .flatMap(o -> o.getRequests().stream()) + .flatMap(r -> r.getParameters().stream()) + .forEach(this::overrideName); + + // hack, http header is case insensitive + codeModel.getOperationGroups().stream() + .flatMap(og -> og.getOperations().stream()) + .flatMap(o -> o.getResponses().stream()) + .filter(r -> r.getProtocol().getHttp().getHeaders() != null) + .flatMap(r -> r.getProtocol().getHttp().getHeaders().stream()) + .forEach(h -> { + String name = h.getHeader(); + String newName = overrideName(name); + if (!name.equals(newName)) { + if (name.equalsIgnoreCase(newName)) { + LOGGER.info("Override response header, from '{}' to '{}'", name, newName); + h.setHeader(newName); + } else { + LOGGER.info("Abort override response header, from '{}' to '{}'", name, newName); + } + } + }); + } + return codeModel; + } + + private void overrideName(Metadata m) { + String name = Utils.getDefaultName(m); + String newName = overrideName(name); + if (!name.equals(newName)) { + m.getLanguage().getDefault().setName(newName); + LOGGER.info("Override default name, from '{}' to '{}'", name, newName); + } + } + + private String overrideName(String name) { + String newName = name; + for (Map.Entry entry : nameOverridePlan.entrySet()) { + int index = newName.indexOf(entry.getKey()); + if (index >= 0) { + int endIndex = index + entry.getKey().length(); + if (wordMatch(newName, index, endIndex)) { + newName = newName.replace(entry.getKey(), entry.getValue()); + } + } + } + return newName; + } + + // Whether the match is the whole word in the name. + // E.g. "lower": "loWer", and the actual name is "flower". We won't replace it to be "floWer". + private boolean wordMatch(String name, int index, int endIndex) { + return !((index > 0 && isSameCase(name.charAt(index - 1), name.charAt(index))) + || (endIndex < name.length() && isSameCase(name.charAt(endIndex - 1), name.charAt(endIndex)))); + } + + private static boolean isSameCase(char c1, char c2) { + return (Character.isUpperCase(c1) && Character.isUpperCase(c2)) + || (Character.isLowerCase(c1) && Character.isLowerCase(c2)); + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/transformer/SchemaRenamer.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/transformer/SchemaRenamer.java new file mode 100644 index 0000000000..cad8ace498 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/transformer/SchemaRenamer.java @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.transformer; + +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.CodeModel; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Metadata; +import com.microsoft.typespec.http.client.generator.core.extension.plugin.PluginLogger; +import com.microsoft.typespec.http.client.generator.mgmt.util.Utils; +import com.microsoft.typespec.http.client.generator.mgmt.FluentNamer; +import com.azure.core.util.CoreUtils; +import org.slf4j.Logger; + +import java.util.Map; + +public class SchemaRenamer { + + private static final Logger LOGGER = new PluginLogger(FluentNamer.getPluginInstance(), SchemaRenamer.class); + + private final Map renameModel; + + public SchemaRenamer(Map renameModel) { + this.renameModel = renameModel; + } + + public CodeModel process(CodeModel codeModel) { + if (renameModel == null || renameModel.isEmpty()) { + return codeModel; + } + + codeModel.getSchemas().getObjects().forEach(s -> checkRename(s, renameModel)); + codeModel.getSchemas().getChoices().forEach(s -> checkRename(s, renameModel)); + codeModel.getSchemas().getSealedChoices().forEach(s -> checkRename(s, renameModel)); + return codeModel; + } + + private static void checkRename(Metadata m, Map renameModel) { + String name = Utils.getJavaName(m); + String newName = renameModel.get(name); + if (!CoreUtils.isNullOrEmpty(newName)) { + LOGGER.info("Rename model from '{}' to '{}'", name, newName); + m.getLanguage().getJava().setName(newName); + } + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/util/Constants.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/util/Constants.java new file mode 100644 index 0000000000..1ab3b636cc --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/util/Constants.java @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.util; + +public class Constants { + + public static final String DEFAULT_NAME_FOR_UNGROUPED_OPERATIONS = "ResourceProvider"; + public static final String OPERATION_GROUP_DEDUPLICATE_SUFFIX = "Operation"; + +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/util/FluentJavaSettings.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/util/FluentJavaSettings.java new file mode 100644 index 0000000000..39de733946 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/util/FluentJavaSettings.java @@ -0,0 +1,282 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.util; + +import com.microsoft.typespec.http.client.generator.core.extension.plugin.NewPlugin; +import com.microsoft.typespec.http.client.generator.core.extension.plugin.PluginLogger; +import com.microsoft.typespec.http.client.generator.mgmt.model.ResourceCollectionAssociation; +import com.azure.core.util.CoreUtils; +import com.azure.json.JsonReader; +import org.slf4j.Logger; + +import java.util.ArrayList; +import java.util.Arrays; +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.function.Consumer; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +public class FluentJavaSettings { + + private final Logger logger; + + private final NewPlugin host; + + /** + * Java class names for extra Inner classes. + */ + private final Set javaNamesForAddInner = new HashSet<>(); + + /** + * Java class names for excluded Inner classes. + */ + private final Set javaNamesForRemoveInner = new HashSet<>(); + + private final Set javaNamesForRemoveModel = new HashSet<>(); + + private final Set javaNamesForPreserveModel = new HashSet<>(); + + private final Set javaNamesForRemoveOperationGroup = new HashSet<>(); + + private final List resourceCollectionAssociations = new ArrayList<>(); + +// /** +// * Whether to generate property method with track1 naming (e.g. foo, withFoo), instead of track2 naming (e.g. getFoo, setFoo). +// */ +// private boolean track1Naming = true; +// +// /** +// * Whether to treat read-only resource property as SubResource type. +// */ +// private boolean resourcePropertyAsSubResource = false; + + /** + * Operation group name for ungrouped operations. + */ + private String nameForUngroupedOperations; + + /** + * Naming override. + */ + private final Map namingOverride = new HashMap<>(); + + private final Map renameModel = new HashMap<>(); + + private final Set javaNamesForPropertyIncludeAlways = new HashSet<>(); + + private final Map renameOperationGroup = new HashMap<>(); + + private String pomFilename = "pom.xml"; + + private String artifactVersion; + + private boolean generateAsyncMethods = false; + + private SampleGeneration generateSamples = SampleGeneration.NONE; + + private String graalVmConfigSuffix = null; + + private boolean sdkIntegration = false; + + private enum SampleGeneration { + NONE, + AGGREGATED + } + + public FluentJavaSettings(NewPlugin host) { + Objects.requireNonNull(host); + this.host = host; + this.logger = new PluginLogger(host, FluentJavaSettings.class); + loadSettings(); + } + + public Set getJavaNamesForAddInner() { + return javaNamesForAddInner; + } + + public Set getJavaNamesForRemoveInner() { + return javaNamesForRemoveInner; + } + + public boolean isTrack1Naming() { + return true; + //return track1Naming; + } + + public boolean isResourcePropertyAsSubResource() { + return false; + //return resourcePropertyAsSubResource; + } + + public Optional getNameForUngroupedOperations() { + return Optional.ofNullable(nameForUngroupedOperations); + } + + public Map getNamingOverride() { + return namingOverride; + } + + public Map getJavaNamesForRenameModel() { + return renameModel; + } + + public Set getJavaNamesForRemoveModel() { + return javaNamesForRemoveModel; + } + + public Set getJavaNamesForPreserveModel() { + return javaNamesForPreserveModel; + } + + public Set getJavaNamesForRemoveOperationGroup() { + return javaNamesForRemoveOperationGroup; + } + + public Set getJavaNamesForPropertyIncludeAlways() { + return javaNamesForPropertyIncludeAlways; + } + + public Map getJavaNamesForRenameOperationGroup() { + return renameOperationGroup; + } + + public List getResourceCollectionAssociations() { + return resourceCollectionAssociations; + } + + public String getPomFilename() { + return pomFilename; + } + + public Optional getArtifactVersion() { + return Optional.ofNullable(artifactVersion); + } + + public boolean isGenerateAsyncMethods() { + return generateAsyncMethods; + } + + public boolean isGenerateSamples() { + return generateSamples != SampleGeneration.NONE; + } + + public Optional getGraalVmConfigSuffix() { + return Optional.ofNullable(graalVmConfigSuffix); + } + + public boolean isSdkIntegration() { + return sdkIntegration; + } + + private void loadSettings() { + loadStringSetting("add-inner", s -> splitStringToSet(s, javaNamesForAddInner)); + + loadStringSetting("remove-inner", s -> splitStringToSet(s, javaNamesForRemoveInner)); + + loadStringSetting("rename-model", s -> { + if (!CoreUtils.isNullOrEmpty(s)) { + String[] renamePairs = s.split(Pattern.quote(",")); + for (String pair : renamePairs) { + String[] fromAndTo = pair.split(Pattern.quote(":")); + if (fromAndTo.length == 2) { + String from = fromAndTo[0]; + String to = fromAndTo[1]; + if (!CoreUtils.isNullOrEmpty(from) && !CoreUtils.isNullOrEmpty(to)) { + renameModel.put(from, to); + } + } + } + } + }); + + loadStringSetting("remove-model", s -> splitStringToSet(s, javaNamesForRemoveModel)); + + loadStringSetting("preserve-model", s -> splitStringToSet(s, javaNamesForPreserveModel)); + + loadStringSetting("remove-operation-group", s -> splitStringToSet(s, javaNamesForRemoveOperationGroup)); + + loadStringSetting("rename-operation-group", s -> { + if (!CoreUtils.isNullOrEmpty(s)) { + String[] renamePairs = s.split(Pattern.quote(",")); + for (String pair : renamePairs) { + String[] fromAndTo = pair.split(Pattern.quote(":")); + if (fromAndTo.length == 2) { + String from = fromAndTo[0]; + String to = fromAndTo[1]; + if (!CoreUtils.isNullOrEmpty(from) && !CoreUtils.isNullOrEmpty(to)) { + renameOperationGroup.put(from, to); + } + } + } + } + }); +// loadBooleanSetting("track1-naming", b -> track1Naming = b); +// loadBooleanSetting("resource-property-as-subresource", b -> resourcePropertyAsSubResource = b); + + loadStringSetting("name-for-ungrouped-operations", s -> nameForUngroupedOperations = s); + + loadStringSetting("property-include-always", s -> splitStringToSet(s, javaNamesForPropertyIncludeAlways)); + + loadResourceCollectionAssociationSetting(resourceCollectionAssociations::addAll); + + loadStringSetting("pom-file", s -> pomFilename = s); + loadStringSetting("package-version", s -> artifactVersion = s); + + loadBooleanSetting("generate-async-methods", s -> generateAsyncMethods = s); + + loadBooleanSetting("generate-samples", s -> generateSamples = (s ? SampleGeneration.AGGREGATED : SampleGeneration.NONE)); + + loadStringSetting("graalvm-config-suffix", s -> graalVmConfigSuffix = s); + + loadBooleanSetting("sdk-integration", b -> sdkIntegration = b); + + Map namingOverride = host.getValueWithJsonReader("pipeline.fluentgen.naming.override", + jsonReader -> jsonReader.readMap(JsonReader::getString)); + + if (namingOverride != null) { + this.namingOverride.putAll(namingOverride); + } + } + + private void splitStringToSet(String s, Set set) { + if (!CoreUtils.isNullOrEmpty(s)) { + set.addAll(Arrays.stream(s.split(Pattern.quote(","))) + .map(String::trim) + .filter(s1 -> !s1.isEmpty()) + .collect(Collectors.toSet())); + } + } + + private void loadBooleanSetting(String settingName, Consumer action) { + Boolean settingValue = host.getBooleanValue(settingName); + if (settingValue != null) { + logger.debug("Option, boolean, {} : {}", settingName, settingValue); + action.accept(settingValue); + } + } + + private void loadStringSetting(String settingName, Consumer action) { + String settingValue = host.getStringValue(settingName); + if (settingValue != null) { + logger.debug("Option, string, {} : {}", settingName, settingValue); + action.accept(settingValue); + } + } + + private void loadResourceCollectionAssociationSetting(Consumer> action) { + String settingName = "resource-collection-associations"; + List settingValue = host.getValueWithJsonReader(settingName, + jsonReader -> jsonReader.readArray(ResourceCollectionAssociation::fromJson)); + if (settingValue != null) { + logger.debug("Option, array, {} : {}", settingName, settingValue); + action.accept(settingValue); + } + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/util/FluentUtils.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/util/FluentUtils.java new file mode 100644 index 0000000000..132ce02e1b --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/util/FluentUtils.java @@ -0,0 +1,407 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.util; + +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; +import com.microsoft.typespec.http.client.generator.core.extension.plugin.PluginLogger; +import com.microsoft.typespec.http.client.generator.mgmt.FluentGen; +import com.microsoft.typespec.http.client.generator.mgmt.model.ResourceTypeName; +import com.microsoft.typespec.http.client.generator.mgmt.model.arm.ErrorClientModel; +import com.microsoft.typespec.http.client.generator.mgmt.model.arm.ResourceClientModel; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentCollectionMethod; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentResourceModel; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.FluentStatic; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.ModelNaming; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.LocalVariable; +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.fluentmodel.ResourceLocalVariables; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClassType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethod; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethodParameter; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientMethodType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientModel; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientModels; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientResponse; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.GenericType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.IType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ListType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.MapType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ModelProperty; +import com.microsoft.typespec.http.client.generator.core.util.ClientModelUtil; +import com.microsoft.typespec.http.client.generator.core.util.CodeNamer; +import com.microsoft.typespec.http.client.generator.core.util.TemplateUtil; +import com.microsoft.typespec.http.client.generator.core.util.TypeUtil; +import com.azure.core.http.rest.PagedIterable; +import com.azure.core.http.rest.Response; +import com.azure.core.http.rest.ResponseBase; +import com.azure.core.http.rest.SimpleResponse; +import com.azure.core.http.rest.StreamResponse; +import com.azure.core.util.Context; +import com.azure.core.util.CoreUtils; +import org.slf4j.Logger; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +public class FluentUtils { + + private static final Logger LOGGER = new PluginLogger(FluentGen.getPluginInstance(), FluentUtils.class); + + private static final Set RESERVED_CLASS_NAMES = Collections.unmodifiableSet(new HashSet<>(Arrays.asList( + Response.class.getSimpleName(), + Context.class.getSimpleName() + ))); + + private FluentUtils() { + } + + public static void log(String format) { + LOGGER.info(format); + } + + public static void log(String format, Object... arguments) { + LOGGER.info(format, arguments); + } + + public static Set reservedClassNames() { + return RESERVED_CLASS_NAMES; + } + + public static boolean isInnerClassType(ClassType classType) { + return isInnerClassType(classType.getPackage(), classType.getName()); + } + + public static boolean isInnerClassType(String packageName, String name) { + JavaSettings settings = JavaSettings.getInstance(); + String innerPackageName = settings.getPackage(settings.getFluentModelsSubpackage()); + return packageName.equals(innerPackageName) && name.endsWith("Inner"); + } + + public static ClassType resourceModelInterfaceClassType(ClassType innerModelClassType) { + return resourceModelInterfaceClassType(innerModelClassType.getName()); + } + + public static ClassType resourceModelInterfaceClassType(String innerModelClassName) { + JavaSettings settings = JavaSettings.getInstance(); + String modelName = innerModelClassName.substring(0, innerModelClassName.length() - "Inner".length()); + if (reservedClassNames().contains(modelName)) { + modelName += "Model"; + } + return new ClassType.Builder() + .packageName(settings.getPackage(settings.getModelsSubpackage())) + .name(modelName) + .build(); + } + + public static String getGetterName(String propertyName) { + return CodeNamer.getModelNamer().modelPropertyGetterName(propertyName); + } + + public static String getServiceName(String clientName) { + JavaSettings settings = JavaSettings.getInstance(); + String serviceName = settings.getServiceName(); + if (CoreUtils.isNullOrEmpty(serviceName)) { + serviceName = getServiceNameFromClientName(clientName, settings.getPackage()); + } + return serviceName; + } + + static String getServiceNameFromClientName(String clientName, String packageName) { + String serviceName = null; + String packageLastName = getPackageLastName(packageName); + + if (clientName != null) { + if (clientName.toLowerCase(Locale.ROOT).startsWith(packageLastName.toLowerCase(Locale.ROOT))) { + serviceName = clientName.substring(0, packageLastName.length()); + } else { + final String keywordManagementClient = "ManagementClient"; + final String keywordClient = "Client"; + if (clientName.endsWith(keywordManagementClient)) { + serviceName = clientName.substring(0, clientName.length() - keywordManagementClient.length()); + } else if (clientName.endsWith(keywordClient)) { + serviceName = clientName.substring(0, clientName.length() - keywordClient.length()); + } + } + } + + if (CoreUtils.isNullOrEmpty(serviceName)) { + serviceName = packageLastName; + } + return serviceName; + } + + public static String getArtifactId() { + JavaSettings settings = JavaSettings.getInstance(); + String artifactId = ClientModelUtil.getArtifactId(); + if (CoreUtils.isNullOrEmpty(artifactId)) { + artifactId = getArtifactIdFromPackageName(settings.getPackage().toLowerCase(Locale.ROOT)); + } + return artifactId; + } + + static String getArtifactIdFromPackageName(String packageName) { + String artifactId; + if (packageName.startsWith("com.azure.resourcemanager")) { + // if namespace looks good, convert it to artifactId directly + artifactId = packageName.substring("com.".length()).replace(".", "-"); + } else { + String packageLastName = getPackageLastName(packageName).toLowerCase(Locale.ROOT); + artifactId = String.format("azure-resourcemanager-%1$s-generated", packageLastName); + } + return artifactId; + } + + private static String getPackageLastName(String packageName) { + String packageLastName = packageName; + if (packageLastName.endsWith(".generated")) { + packageLastName = packageLastName.substring(0, packageLastName.lastIndexOf(".")); + } + int pos = packageLastName.lastIndexOf("."); + if (pos != -1 && pos != packageLastName.length() - 1) { + packageLastName = packageLastName.substring(pos + 1); + } + return packageLastName; + } + + public static IType getFluentWrapperType(IType clientType) { + IType wrapperType = clientType; + if (clientType instanceof ClassType) { + ClassType type = (ClassType) clientType; + if (FluentUtils.isInnerClassType(type)) { + wrapperType = FluentUtils.resourceModelInterfaceClassType(type); + } else if (FluentUtils.isResponseType(type)) { + IType bodyType = FluentUtils.getValueTypeFromResponseType(type); + IType wrapperItemType = getFluentWrapperType(bodyType); + wrapperType = wrapperItemType == bodyType ? type : GenericType.Response(wrapperItemType); + } + } else if (clientType instanceof ListType) { + ListType type = (ListType) clientType; + IType wrapperElementType = getFluentWrapperType(type.getElementType()); + wrapperType = wrapperElementType == type.getElementType() ? type : new ListType(wrapperElementType); + } else if (clientType instanceof MapType) { + MapType type = (MapType) clientType; + IType wrapperElementType = getFluentWrapperType(type.getValueType()); + wrapperType = wrapperElementType == type.getValueType() ? type : new MapType(wrapperElementType); + } else if (clientType instanceof GenericType) { + GenericType type = (GenericType) clientType; + if (PagedIterable.class.getSimpleName().equals(type.getName())) { + IType wrapperItemType = getFluentWrapperType(type.getTypeArguments()[0]); + wrapperType = wrapperItemType == type.getTypeArguments()[0] ? type : GenericType.PagedIterable(wrapperItemType); + } else if (Response.class.getSimpleName().equals(type.getName())) { + IType wrapperItemType = getFluentWrapperType(type.getTypeArguments()[0]); + wrapperType = wrapperItemType == type.getTypeArguments()[0] ? type : GenericType.Response(wrapperItemType); + } + } + return wrapperType; + } + + public static String getSingular(String name) { + return Utils.getSingular(name); + } + + public static boolean isContextParameter(ClientMethodParameter parameter) { + return ClassType.CONTEXT.getName().equals(parameter.getClientType().toString()); + } + + public static ClientModel getClientModel(String name) { + if (name == null) { + return null; + } + + ClientModel clientModel = null; + if (FluentStatic.getClient() == null) { + clientModel = ClientModels.getInstance().getModel(name); + } else { + for (ClientModel model : FluentStatic.getClient().getModels()) { + if (name.equals(model.getName())) { + clientModel = model; + break; + } + } + } + if (clientModel == null) { + clientModel = ResourceClientModel.getResourceClientModel(name) + .or(() -> ErrorClientModel.getErrorClientModel(name)) + .orElse(null); + } + return clientModel; + } + + public static String loadTextFromResource(String filename, String... replacements) { + return TemplateUtil.loadTextFromResource(filename, replacements); + } + + /** + * Get the name of the argument for the method call. + * + * If the parameter is provided by the caller, the name is unchanged and directly passed to the method call. + * If the parameter is provided by class variable or local variable, the name is unchanged, or might need simple conversion as the type might not align exactly. + * If the parameter is same as innerModel of the resource model, use innerModel. + * If the parameter is Context, use Context.NONE. + * + * @param parameter the client method parameter that requires the argument + * @param inputParametersSet the parameters that provided by the caller + * @param localVariables the local variables that defined in the class + * @param resourceModel the resource model, usually its innerModel + * @param collectionMethod the method + * @return the name of the argument + */ + public static String getLocalMethodArgument(ClientMethodParameter parameter, + Set inputParametersSet, ResourceLocalVariables localVariables, + FluentResourceModel resourceModel, FluentCollectionMethod collectionMethod) { + return getLocalMethodArgument(parameter, inputParametersSet, localVariables, resourceModel, collectionMethod, null); + } + + public static String getLocalMethodArgument(ClientMethodParameter parameter, + Set inputParametersSet, ResourceLocalVariables localVariables, + FluentResourceModel resourceModel, FluentCollectionMethod collectionMethod, + ResourceLocalVariables resourceLocalVariablesDefinedInClass) { + if (inputParametersSet.contains(parameter)) { + // input parameter + return parameter.getName(); + } else if (resourceModel.getInnerModel().getName().equals(parameter.getClientType().toString())) { + // body payload, use innerModel + return String.format("this.%1$s()", ModelNaming.METHOD_INNER_MODEL); + } else if (ClassType.CONTEXT == parameter.getClientType()) { + // context not in input, use NONE + return "Context.NONE"; + } else { + // local variables + LocalVariable localVariable = localVariables.getLocalVariableByMethodParameter(parameter); + if (localVariable == null) { + throw new IllegalStateException(String.format("Local variable not found for method %1$s, model %2$s, parameter %3$s, available local variables %4$s", + collectionMethod.getMethodName(), + resourceModel.getName(), + parameter.getName(), + localVariables.getLocalVariablesMap().entrySet().stream().collect(Collectors.toMap(e -> e.getKey().getName(), e -> e.getValue().getName())))); + } + String name = localVariable.getName(); + + // there could be case that the variable used in method (ResourceUpdate or ResourceRefresh) is different from the one defined in class (by ResourceCreate) + LocalVariable localVariableDefinedInClass = resourceLocalVariablesDefinedInClass == null + ? null + : resourceLocalVariablesDefinedInClass.getLocalVariablesMap().values().stream() + .filter(var -> localVariable.getName().equals(var.getName())).findFirst().orElse(null); + if (localVariableDefinedInClass != null + && !Objects.equals(localVariableDefinedInClass.getVariableType().toString(), localVariable.getVariableType().toString())) { + if (localVariableDefinedInClass.getVariableType() == ClassType.STRING) { + name = String.format("%1$s.fromString(%2$s)", localVariable.getVariableType().toString(), name); + } else if (localVariable.getVariableType() == ClassType.STRING) { + name = String.format("%1$s.toString()", name); + } + } + return name; + } + } + + public static boolean modelHasLocationProperty(FluentResourceModel resourceModel) { + return resourceModel.hasProperty(ResourceTypeName.FIELD_LOCATION) + && resourceModel.getProperty(ResourceTypeName.FIELD_LOCATION).getFluentType() == ClassType.STRING; + } + + public static boolean modelHasLocationProperty(List properties) { + return properties.stream() + .anyMatch(p -> ResourceTypeName.FIELD_LOCATION.equals(p.getName()) && p.getClientType() == ClassType.STRING); + } + + public static boolean isResponseType(IType clientType) { + boolean ret = false; + if (clientType instanceof GenericType) { + // Response<> + GenericType type = (GenericType) clientType; + if (Response.class.getSimpleName().equals(type.getName())) { + ret = true; + } else { + ret = TypeUtil.isGenericTypeClassSubclassOf(type, Response.class); + } + } else if (clientType instanceof ClassType) { + // ClientResponse is type of a subclass of Response<> + ClassType type = (ClassType) clientType; + Optional clientResponse = FluentStatic.getClient().getResponseModels().stream() + .filter(r -> r.getName().equals(type.getName())) + .findAny(); + ret = clientResponse.isPresent(); + } + return ret; + } + + public static IType getValueTypeFromResponseType(IType clientType) { + IType bodyType = null; + if (clientType instanceof GenericType) { + GenericType type = (GenericType) clientType; + if (Response.class.getSimpleName().equals(type.getName())) { + bodyType = type.getTypeArguments()[0]; + } else if (TypeUtil.isGenericTypeClassSubclassOf(type, Response.class)) { + bodyType = getValueTypeFromResponseTypeSubType(type); + } + } else if (clientType instanceof ClassType) { + ClassType type = (ClassType) clientType; + Optional clientResponse = FluentStatic.getClient().getResponseModels().stream() + .filter(r -> r.getName().equals(type.getName())) + .findFirst(); + if (clientResponse.isPresent()) { + bodyType = clientResponse.get().getBodyType(); + } + } + return bodyType; + } + + private static IType getValueTypeFromResponseTypeSubType(GenericType type) { + IType bodyType; + if (ResponseBase.class.getSimpleName().equals(type.getName())) { + bodyType = type.getTypeArguments()[1]; + } else if (SimpleResponse.class.getSimpleName().equals(type.getName())) { + bodyType = type.getTypeArguments()[0]; + } else if (StreamResponse.class.getSimpleName().equals(type.getName())) { + bodyType = GenericType.FLUX_BYTE_BUFFER; + } else { + log("Unable to determine value type for Response subtype: %s, fallback to typeArguments[0].", type); + bodyType = type.getTypeArguments()[0]; + } + return bodyType; + } + + public static List splitFlattenedSerializedName(String serializedName) { + return ClientModelUtil.splitFlattenedSerializedName(serializedName); + } + + public static boolean exampleIsUpdate(String name) { + name = name.toLowerCase(Locale.ROOT); + return name.contains("update") && !name.contains("create"); + } + + public static boolean validRequestContentTypeToGenerateExample(ClientMethod clientMethod) { + // for now, only accept JSON as request body + + String requestContentType = clientMethod.getProxyMethod().getRequestContentType(); + return clientMethod.getProxyMethod().getExamples() != null + && requiresExample(clientMethod) + // currently only generate for json payload, i.e. "text/json", "application/json" + && requestContentType != null && requestContentType.contains("json"); + } + + public static boolean validResponseContentTypeToGenerateExample(ClientMethod clientMethod) { + // for now, avoid binary as response body + + IType responseBodyType = clientMethod.getProxyMethod().getResponseBodyType(); + return !(responseBodyType == ClassType.BINARY_DATA || responseBodyType == GenericType.FLUX_BYTE_BUFFER); + } + + public static boolean requiresExample(ClientMethod clientMethod) { + if (clientMethod.getType() == ClientMethodType.SimpleSync + || clientMethod.getType() == ClientMethodType.SimpleSyncRestResponse + || clientMethod.getType() == ClientMethodType.PagingSync + || clientMethod.getType() == ClientMethodType.LongRunningSync) { + // generate example for the method with full parameters + return clientMethod.getParameters().stream().anyMatch(p -> ClassType.CONTEXT.equals(p.getClientType())); + } + return false; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/util/TypeConversionUtils.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/util/TypeConversionUtils.java new file mode 100644 index 0000000000..125502f3f2 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/util/TypeConversionUtils.java @@ -0,0 +1,131 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.util; + +import com.microsoft.typespec.http.client.generator.mgmt.model.clientmodel.ModelNaming; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClassType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.GenericType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.IType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ListType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.MapType; +import com.azure.core.http.rest.PagedIterable; +import com.azure.core.http.rest.Response; + +import java.util.Objects; + +public class TypeConversionUtils { + + private TypeConversionUtils() { + } + + /** + * Get expression that converts the response of client method to the response of the collection method. + * + * It converts innerModel to implementation of resource model. + * It transfers the conversion along chain of generic types. + * It converts list and map to immutable. + * + * @param clientType the type of the response of client method + * @param variableName the variable name of the response of client method + * @return the expression that converts the response of client method to the response of the collection method + */ + public static String conversionExpression(IType clientType, String variableName) { + String expression = null; + if (clientType instanceof ClassType) { + ClassType type = (ClassType) clientType; + if (FluentUtils.isInnerClassType(type)) { + expression = String.format("new %1$s(%2$s, this.%3$s())", getModelImplName(type), variableName, ModelNaming.METHOD_MANAGER); + } else if (FluentUtils.isResponseType(type)) { + IType valueType = FluentUtils.getValueTypeFromResponseType(type); + if (valueType instanceof ClassType || valueType instanceof GenericType) { + String valuePropertyName = variableName + ".getValue()"; + expression = String.format("new SimpleResponse<>(%1$s.getRequest(), %1$s.getStatusCode(), %1$s.getHeaders(), %2$s)", variableName, conversionExpression(valueType, valuePropertyName)); + } else { + expression = variableName; + } + } + } else if (clientType instanceof ListType) { + ListType type = (ListType) clientType; + String nestedPropertyName = nextPropertyName(variableName); + expression = String.format("%1$s.stream().map(%2$s -> %3$s).collect(Collectors.toList())", variableName, nestedPropertyName, conversionExpression(type.getElementType(), nestedPropertyName)); + } else if (clientType instanceof MapType) { + MapType type = (MapType) clientType; + String nestedPropertyName = nextPropertyName(variableName); + String valuePropertyName = nestedPropertyName + ".getValue()"; + expression = String.format("%1$s.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, %2$s -> %3$s))", variableName, nestedPropertyName, conversionExpression(type.getValueType(), valuePropertyName)); + } else if (clientType instanceof GenericType) { + GenericType type = (GenericType) clientType; + if (PagedIterable.class.getSimpleName().equals(type.getName())) { + IType valueType = type.getTypeArguments()[0]; + if (valueType instanceof ClassType) { + String nestedPropertyName = nextPropertyName(variableName); + expression = String.format("%1$s.mapPage(%2$s, %3$s -> new %4$s(%5$s, this.%6$s()))", + ModelNaming.CLASS_RESOURCE_MANAGER_UTILS, variableName, nestedPropertyName, getModelImplName((ClassType) valueType), nestedPropertyName, ModelNaming.METHOD_MANAGER); + } + } else if (Response.class.getSimpleName().equals(type.getName())) { + IType valueType = type.getTypeArguments()[0]; + if (valueType instanceof ClassType || valueType instanceof GenericType) { + String valuePropertyName = variableName + ".getValue()"; + expression = String.format("new SimpleResponse<>(%1$s.getRequest(), %1$s.getStatusCode(), %1$s.getHeaders(), %2$s)", variableName, conversionExpression(valueType, valuePropertyName)); + } else { + expression = variableName; + } + } + } + Objects.requireNonNull(expression, "Unexpected scenario in WrapperTypeConversionMethod.conversionExpression. ClientType is " + clientType); + return expression; + } + + public static String objectOrUnmodifiableCollection(IType clientType, String expression) { + String unmodifiableMethodName = null; + if (clientType instanceof ListType) { + unmodifiableMethodName = "unmodifiableList"; + } else if (clientType instanceof MapType) { + unmodifiableMethodName = "unmodifiableMap"; + } + return (unmodifiableMethodName == null) + ? expression + : String.format("Collections.%1$s(%2$s)", unmodifiableMethodName, expression); + } + + public static String nullOrEmptyCollection(IType clientType) { + String emptyExpression = "null"; + if (clientType instanceof ListType) { + emptyExpression = "Collections.emptyList()"; + } else if (clientType instanceof MapType) { + emptyExpression = "Collections.emptyMap()"; + } + return emptyExpression; + } + + public static boolean isPagedIterable(IType clientType) { + boolean ret = false; + if (clientType instanceof GenericType) { + GenericType type = (GenericType) clientType; + if (PagedIterable.class.getSimpleName().equals(type.getName())) { + ret = true; + } + } + return ret; + } + + public static String tempVariableName() { + return "inner"; + } + + private static String nextPropertyName(String propertyName) { + if (propertyName.indexOf('.') > 0) { + propertyName = propertyName.substring(0, propertyName.indexOf('.')); + } + if (propertyName.equals(tempVariableName())) { + return tempVariableName() + "1"; + } else { + return tempVariableName() + (Integer.parseInt(propertyName.substring(tempVariableName().length())) + 1); + } + } + + private static String getModelImplName(ClassType classType) { + return FluentUtils.resourceModelInterfaceClassType(classType).getName() + ModelNaming.MODEL_IMPL_SUFFIX; + } +} diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/util/Utils.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/util/Utils.java new file mode 100644 index 0000000000..9e53280219 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/util/Utils.java @@ -0,0 +1,94 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mgmt.util; + +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Client; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Metadata; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Parameter; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Property; +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; +import com.microsoft.typespec.http.client.generator.core.util.SchemaUtil; +import org.slf4j.Logger; + +import java.lang.reflect.Field; +import java.util.Set; +import java.util.stream.Collectors; + +public class Utils { + + public final static String METHOD_POSTFIX_WITH_RESPONSE = "WithResponse"; + + public static String getDefaultName(Metadata m) { + return SchemaUtil.getDefaultName(m); + } + + public static String getJavaName(Metadata m) { + return SchemaUtil.getJavaName(m); + } + + public static boolean nonFlattenedProperty(Property p) { + return p.getFlattenedNames() == null || p.getFlattenedNames().isEmpty(); + } + + public static boolean nonFlattenedParameter(Parameter p) { + return !p.isFlattened(); + } + + public static void shallowCopy(T obj, T newObj, Class clazz, Logger logger) { + while (clazz != Object.class) { + Field[] fields = clazz.getDeclaredFields(); + for (Field f : fields) { + try { + Field t = clazz.getDeclaredField(f.getName()); + + if (t.getType() == f.getType()) { + f.setAccessible(true); + t.setAccessible(true); + t.set(newObj, f.get(obj)); + } + } catch (NoSuchFieldException ex) { + // skip it + } catch (IllegalAccessException ex) { + logger.error("Failed to copy field '{}'", f.getName()); + } + } + + clazz = clazz.getSuperclass(); + } + } + + public static String getNameForUngroupedOperations(Client client, FluentJavaSettings settings) { + String nameForUngroupOperations = null; + if (settings.getNameForUngroupedOperations().isPresent()) { + nameForUngroupOperations = settings.getNameForUngroupedOperations().get(); + } else if (JavaSettings.getInstance().isFluentLite()) { + nameForUngroupOperations = Constants.DEFAULT_NAME_FOR_UNGROUPED_OPERATIONS; + + Set operationGroupNames = client.getOperationGroups().stream() + .map(Utils::getDefaultName) + .collect(Collectors.toSet()); + if (operationGroupNames.contains(nameForUngroupOperations)) { + nameForUngroupOperations += Constants.OPERATION_GROUP_DEDUPLICATE_SUFFIX; + } + } + return nameForUngroupOperations; + } + + + public static String getSingular(String name) { + if (name == null) { + return null; + } + + if (name.endsWith("ies")) { + return name.substring(0, name.length() - 3) + 'y'; + } else if (name.endsWith("sses")) { + return name.substring(0, name.length() - 2); + } else if (name.endsWith("s") && !name.endsWith("ss")) { + return name.substring(0, name.length() - 1); + } else { + return name; + } + } +} diff --git a/packages/http-client-java/generator/http-client-generator/pom.xml b/packages/http-client-java/generator/http-client-generator/pom.xml index 78061d9fde..d34a1df78a 100644 --- a/packages/http-client-java/generator/http-client-generator/pom.xml +++ b/packages/http-client-java/generator/http-client-generator/pom.xml @@ -13,6 +13,12 @@ + + + com.microsoft.typespec + http-client-generator-mgmt + 1.0.0-beta.1 + org.junit.jupiter junit-jupiter-api diff --git a/packages/http-client-java/generator/http-client-generator/src/main/java/com/microsoft/typespec/http/client/generator/JavaSettingsAccessor.java b/packages/http-client-java/generator/http-client-generator/src/main/java/com/microsoft/typespec/http/client/generator/JavaSettingsAccessor.java new file mode 100644 index 0000000000..8adf08faaf --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator/src/main/java/com/microsoft/typespec/http/client/generator/JavaSettingsAccessor.java @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator; + +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; +import com.microsoft.typespec.http.client.generator.core.extension.plugin.NewPlugin; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +public class JavaSettingsAccessor { + + public static void setHost(NewPlugin host) { + try { + Method setHost = JavaSettings.class.getDeclaredMethod("setHost", NewPlugin.class); + setHost.setAccessible(true); + setHost.invoke(null, host); + } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + e.printStackTrace(); + } + } +} diff --git a/packages/http-client-java/generator/http-client-generator/src/main/java/com/microsoft/typespec/http/client/generator/Main.java b/packages/http-client-java/generator/http-client-generator/src/main/java/com/microsoft/typespec/http/client/generator/Main.java index b50baadc6a..ab15eb8552 100644 --- a/packages/http-client-java/generator/http-client-generator/src/main/java/com/microsoft/typespec/http/client/generator/Main.java +++ b/packages/http-client-java/generator/http-client-generator/src/main/java/com/microsoft/typespec/http/client/generator/Main.java @@ -1,37 +1,261 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + package com.microsoft.typespec.http.client.generator; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.AnnotatedPropertyUtils; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.CodeModel; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.CodeModelCustomConstructor; +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; +import com.microsoft.typespec.http.client.generator.fluent.TypeSpecFluentPlugin; +import com.microsoft.typespec.http.client.generator.mgmt.model.javamodel.FluentJavaPackage; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.Client; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ProxyMethodExample; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaFile; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaPackage; +import com.microsoft.typespec.http.client.generator.core.postprocessor.Postprocessor; +import com.microsoft.typespec.http.client.generator.core.util.ClientModelUtil; +import com.azure.core.util.Configuration; +import com.azure.core.util.CoreUtils; +import com.azure.json.JsonProviders; +import com.azure.json.JsonReader; +import com.microsoft.typespec.http.client.generator.model.EmitterOptions; +import com.microsoft.typespec.http.client.generator.util.TspLocationUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.yaml.snakeyaml.DumperOptions; +import org.yaml.snakeyaml.LoaderOptions; +import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.constructor.Constructor; +import org.yaml.snakeyaml.inspector.TrustedTagInspector; +import org.yaml.snakeyaml.representer.Representer; + import java.io.IOException; -import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.nio.file.attribute.FileAttribute; +import java.util.Arrays; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Collectors; +import java.util.stream.Stream; -/** - * The main class for TypeSpec Java code generator. - */ public class Main { - /** - * The main method for TypeSpec Java code generator. - * @param args The arguments for TypeSpec Java code generator. - */ - public static void main(String[] args) throws IOException { - Main main = new Main(); - System.out.println(main.sayHello("TypeSpec Java code generator")); - Path file = Files.createFile(Paths.get("test.txt")); - Files.write(file, "Test file".getBytes(StandardCharsets.UTF_8)); - System.out.println("Created file at " + file.toAbsolutePath()); - boolean deleteIfExists = Files.deleteIfExists(file); - System.out.println("Deleted file at " + file.toAbsolutePath() + " " + deleteIfExists); - - } - - /** - * The method to say hello. - * @param name The name to say hello. - * @return The hello message. - */ - public String sayHello(String name) { - return "Hello friends from " + name; - } + private static final Logger LOGGER = LoggerFactory.getLogger(Main.class); + + private static final String TSP_LOCATION_FILENAME = "tsp-location.yaml"; + + private static Yaml yaml = null; + + // java -jar target/azure-typespec-extension-jar-with-dependencies.jar + public static void main(String[] args) throws IOException { + // parameters + String inputYamlFileName = "typespec-tests/tsp-output/code-model.yaml"; + if (args.length >= 1) { + inputYamlFileName = args[0]; + } + + LOGGER.info("Code model file: {}", inputYamlFileName); + + // load code-model.yaml + CodeModel codeModel = loadCodeModel(inputYamlFileName); + + EmitterOptions emitterOptions = loadEmitterOptions(codeModel); + + boolean sdkIntegration = true; + String outputDir = emitterOptions.getOutputDir(); + Path outputDirPath = Paths.get(outputDir); + if (Files.exists(outputDirPath)) { + if (emitterOptions.getArm()) { + // check ../../parents/azure-client-sdk-parent + sdkIntegration = Files.exists(Paths.get(outputDir, "../../parents/azure-client-sdk-parent")); + } else { + try (Stream filestream = Files.list(outputDirPath)) { + Set filenames = filestream.map(p -> p.getFileName().toString()) + .map(name -> name.toLowerCase(Locale.ROOT)) + .collect(Collectors.toSet()); + + // if there is already pom and source, do not overwrite them (includes README.md, CHANGELOG.md etc.) + sdkIntegration = !filenames.containsAll(Arrays.asList("pom.xml", "src")); + } + } + + // load tsp-location.yaml + try (Stream filestream = Files.list(outputDirPath)) { + Set filenames = filestream.map(p -> p.getFileName().toString()) + .map(name -> name.toLowerCase(Locale.ROOT)) + .collect(Collectors.toSet()); + if (filenames.contains(TSP_LOCATION_FILENAME)) { + String directory = TspLocationUtil.getDirectory(getYaml(), + outputDirPath.resolve(TSP_LOCATION_FILENAME)); + if (!CoreUtils.isNullOrEmpty(directory)) { + ProxyMethodExample.setTspDirectory(directory); + } + } + } + } + + if (emitterOptions.getArm()) { + handleFluent(codeModel, emitterOptions, sdkIntegration); + } else { + handleDPG(codeModel, emitterOptions, sdkIntegration, outputDir); + } + } + + private static void handleFluent(CodeModel codeModel, EmitterOptions emitterOptions, boolean sdkIntegration) { + // initialize plugin + TypeSpecFluentPlugin fluentPlugin = new TypeSpecFluentPlugin(emitterOptions, sdkIntegration); + + codeModel = fluentPlugin.preProcess(codeModel); + + // client + Client client = fluentPlugin.processClient(codeModel); + + // template + FluentJavaPackage javaPackage = fluentPlugin.processTemplates(codeModel, client); + + // write + + // java files + Postprocessor.writeToFiles(javaPackage.getJavaFiles() + .stream() + .collect(Collectors.toMap(JavaFile::getFilePath, file -> file.getContents().toString())), fluentPlugin, + fluentPlugin.getLogger()); + + // XML include POM + javaPackage.getXmlFiles() + .forEach(xmlFile -> fluentPlugin.writeFile(xmlFile.getFilePath(), xmlFile.getContents().toString(), null)); + // Others + javaPackage.getTextFiles() + .forEach(textFile -> fluentPlugin.writeFile(textFile.getFilePath(), textFile.getContents(), null)); + } + + private static void handleDPG(CodeModel codeModel, EmitterOptions emitterOptions, boolean sdkIntegration, + String outputDir) { + // initialize plugin + TypeSpecPlugin typeSpecPlugin = new TypeSpecPlugin(emitterOptions, sdkIntegration); + + // client + Client client = typeSpecPlugin.processClient(codeModel); + + // template + JavaPackage javaPackage = typeSpecPlugin.processTemplates(codeModel, client, JavaSettings.getInstance()); + + LOGGER.info("Count of Java files: {}", javaPackage.getJavaFiles().size()); + LOGGER.info("Count of XML files: {}", javaPackage.getXmlFiles().size()); + LOGGER.info("Count of text files: {}", javaPackage.getTextFiles().size()); + + // handle partial update + Map javaFiles = new ConcurrentHashMap<>(); + JavaSettings settings = JavaSettings.getInstance(); + javaPackage.getJavaFiles() + .parallelStream() + .forEach(javaFile -> javaFiles.put(javaFile.getFilePath(), javaFile.getContents().toString())); + + // handle customization + // write output + // java files + new Postprocessor(typeSpecPlugin).postProcess(javaFiles); + + // XML include POM + javaPackage.getXmlFiles() + .forEach( + xmlFile -> typeSpecPlugin.writeFile(xmlFile.getFilePath(), xmlFile.getContents().toString(), null)); + // Others + javaPackage.getTextFiles() + .forEach(textFile -> typeSpecPlugin.writeFile(textFile.getFilePath(), textFile.getContents(), null)); + // resources + String artifactId = ClientModelUtil.getArtifactId(); + if (settings.isBranded()) { + if (!CoreUtils.isNullOrEmpty(artifactId)) { + typeSpecPlugin.writeFile("src/main/resources/" + artifactId + ".properties", + "name=${project.artifactId}\nversion=${project.version}\n", null); + } + } + + boolean includeApiViewProperties = emitterOptions.includeApiViewProperties() != null + && emitterOptions.includeApiViewProperties(); + if (includeApiViewProperties && !CoreUtils.isNullOrEmpty(typeSpecPlugin.getCrossLanguageDefinitionMap())) { + String flavor = emitterOptions.getFlavor() == null ? "azure" : emitterOptions.getFlavor(); + StringBuilder sb = new StringBuilder( + "{\n \"flavor\": \"" + flavor + "\", \n \"CrossLanguageDefinitionId\": {\n"); + AtomicBoolean first = new AtomicBoolean(true); + typeSpecPlugin.getCrossLanguageDefinitionMap().forEach((key, value) -> { + if (first.get()) { + first.set(false); + } else { + sb.append(",\n"); + } + sb.append(" \"").append(key).append("\": \"").append(value).append("\""); + }); + sb.append("\n }\n}\n"); + + typeSpecPlugin.writeFile("src/main/resources/META-INF/" + artifactId + "_apiview_properties.json", + sb.toString(), null); + } + System.exit(0); + } + + private static EmitterOptions loadEmitterOptions(CodeModel codeModel) { + + EmitterOptions options = null; + String emitterOptionsJson = Configuration.getGlobalConfiguration().get("emitterOptions"); + + if (emitterOptionsJson != null) { + try (JsonReader jsonReader = JsonProviders.createReader(emitterOptionsJson)) { + options = EmitterOptions.fromJson(jsonReader); + // namespace + if (CoreUtils.isNullOrEmpty(options.getNamespace())) { + if (codeModel.getLanguage().getJava() != null && !CoreUtils.isNullOrEmpty( + codeModel.getLanguage().getJava().getNamespace())) { + options.setNamespace(codeModel.getLanguage().getJava().getNamespace()); + } + } + + // output path + if (CoreUtils.isNullOrEmpty(options.getOutputDir())) { + options.setOutputDir("typespec-tests/tsp-output/"); + } else if (!options.getOutputDir().endsWith("/")) { + options.setOutputDir(options.getOutputDir() + "/"); + } + } catch (IOException e) { + LOGGER.info("Read emitter options failed, emitter options json: {}", emitterOptionsJson); + } + } + + if (options == null) { + // default if emitterOptions fails + options = new EmitterOptions(); + options.setOutputDir("typespec-tests/tsp-output/"); + if (codeModel.getLanguage().getJava() != null && !CoreUtils.isNullOrEmpty( + codeModel.getLanguage().getJava().getNamespace())) { + options.setNamespace(codeModel.getLanguage().getJava().getNamespace()); + } + } + return options; + } + + private static CodeModel loadCodeModel(String filename) throws IOException { + String file = Files.readString(Paths.get(filename)); + return getYaml().loadAs(file, CodeModel.class); + } + + private static Yaml getYaml() { + if (yaml == null) { + Representer representer = new Representer(new DumperOptions()); + representer.setPropertyUtils(new AnnotatedPropertyUtils()); + representer.getPropertyUtils().setSkipMissingProperties(true); + LoaderOptions loaderOptions = new LoaderOptions(); + loaderOptions.setCodePointLimit(50 * 1024 * 1024); + loaderOptions.setMaxAliasesForCollections(Integer.MAX_VALUE); + loaderOptions.setNestingDepthLimit(Integer.MAX_VALUE); + loaderOptions.setTagInspector(new TrustedTagInspector()); + Constructor constructor = new CodeModelCustomConstructor(loaderOptions); + yaml = new Yaml(constructor, representer, new DumperOptions(), loaderOptions); + } + return yaml; + } } diff --git a/packages/http-client-java/generator/http-client-generator/src/main/java/com/microsoft/typespec/http/client/generator/TypeSpecPlugin.java b/packages/http-client-java/generator/http-client-generator/src/main/java/com/microsoft/typespec/http/client/generator/TypeSpecPlugin.java new file mode 100644 index 0000000000..34ef4db928 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator/src/main/java/com/microsoft/typespec/http/client/generator/TypeSpecPlugin.java @@ -0,0 +1,328 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator; + +import com.microsoft.typespec.http.client.generator.core.Javagen; +import com.microsoft.typespec.http.client.generator.core.extension.jsonrpc.Connection; +import com.microsoft.typespec.http.client.generator.core.extension.model.Message; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.CodeModel; +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; +import com.microsoft.typespec.http.client.generator.core.mapper.Mappers; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.AsyncSyncClient; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.Client; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientModel; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ConvenienceMethod; +import com.microsoft.typespec.http.client.generator.core.model.javamodel.JavaPackage; +import com.microsoft.typespec.http.client.generator.core.preprocessor.Preprocessor; +import com.microsoft.typespec.http.client.generator.core.preprocessor.tranformer.Transformer; +import com.microsoft.typespec.http.client.generator.core.util.ClientModelUtil; +import com.azure.core.util.CoreUtils; +import com.azure.json.JsonReader; +import com.azure.json.ReadValueCallback; +import com.microsoft.typespec.http.client.generator.mapper.TypeSpecMapperFactory; +import com.microsoft.typespec.http.client.generator.model.EmitterOptions; +import com.microsoft.typespec.http.client.generator.util.FileUtil; +import com.microsoft.typespec.http.client.generator.util.ModelUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.OutputStream; +import java.nio.file.Paths; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.stream.Collectors; + +public class TypeSpecPlugin extends Javagen { + + private static final Logger LOGGER = LoggerFactory.getLogger(TypeSpecPlugin.class); + + private final EmitterOptions emitterOptions; + + private final Map crossLanguageDefinitionsMap = new TreeMap<>(); + + public Client processClient(CodeModel codeModel) { + // transform code model + codeModel = new Transformer().transform(Preprocessor.convertOptionalConstantsToEnum(codeModel)); + + // map to client model + Client client = Mappers.getClientMapper().map(codeModel); + + client.getAsyncClients() + .forEach(asyncClient -> crossLanguageDefinitionsMap + .put(asyncClient.getPackageName() + "." + asyncClient.getClassName(), asyncClient.getCrossLanguageDefinitionId())); + + client.getSyncClients() + .forEach(syncClient -> crossLanguageDefinitionsMap + .put(syncClient.getPackageName() + "." + syncClient.getClassName(), syncClient.getCrossLanguageDefinitionId())); + + client.getClientBuilders() + .forEach(clientBuilder -> crossLanguageDefinitionsMap + .put(clientBuilder.getPackageName() + "." + clientBuilder.getClassName(), clientBuilder.getCrossLanguageDefinitionId())); + + for (AsyncSyncClient asyncClient : client.getAsyncClients()) { + List convenienceMethods = asyncClient.getConvenienceMethods(); + for (ConvenienceMethod convenienceMethod : convenienceMethods) { + convenienceMethod.getConvenienceMethods() + .stream() + .filter(method -> !method.getName().endsWith("Async")) + .forEach(method -> crossLanguageDefinitionsMap.put(asyncClient.getPackageName() + "." + asyncClient.getClassName() + "." + method.getName(), method.getCrossLanguageDefinitionId())); + if (!convenienceMethod.getProtocolMethod().getName().endsWith("Async")) { + crossLanguageDefinitionsMap.put(asyncClient.getPackageName() + "." + asyncClient.getClassName() + "." + convenienceMethod.getProtocolMethod().getName(), + convenienceMethod.getProtocolMethod().getCrossLanguageDefinitionId()); + } + } + } + + for (AsyncSyncClient syncClient : client.getSyncClients()) { + List convenienceMethods = syncClient.getConvenienceMethods(); + for (ConvenienceMethod convenienceMethod : convenienceMethods) { + convenienceMethod.getConvenienceMethods() + .stream() + .filter(method -> !method.getName().endsWith("Async")) + .forEach(method -> crossLanguageDefinitionsMap.put(syncClient.getPackageName() + "." + syncClient.getClassName() + "." + method.getName(), method.getCrossLanguageDefinitionId())); + + if (!convenienceMethod.getProtocolMethod().getName().endsWith("Async")) { + crossLanguageDefinitionsMap.put(syncClient.getPackageName() + "." + syncClient.getClassName() + "." + convenienceMethod.getProtocolMethod().getName(), + convenienceMethod.getProtocolMethod().getCrossLanguageDefinitionId()); + } + + } + } + return client; + } + + public JavaPackage processTemplates(CodeModel codeModel, Client client, JavaSettings settings) { + return super.writeToTemplates(codeModel, client, settings, false); + } + + @Override + protected void writeClientModels(Client client, JavaPackage javaPackage, JavaSettings settings) { + // Client model + client.getModels().stream() + .filter(ModelUtil::isGeneratingModel) + .forEach(model -> { + crossLanguageDefinitionsMap.put(model.getPackage() + "." + model.getName(), model.getCrossLanguageDefinitionId()); + javaPackage.addModel(model.getPackage(), model.getName(), model); + }); + + // Enum + client.getEnums().stream() + .filter(ModelUtil::isGeneratingModel) + .forEach(model -> { + crossLanguageDefinitionsMap.put(model.getPackage() + "." + model.getName(), model.getCrossLanguageDefinitionId()); + javaPackage.addEnum(model.getPackage(), model.getName(), model); + }); + + // Response + client.getResponseModels().stream() + .filter(ModelUtil::isGeneratingModel) + .forEach(model -> javaPackage.addClientResponse(model.getPackage(), model.getName(), model)); + + // Union + client.getUnionModels().stream() + .filter(ModelUtil::isGeneratingModel) + .forEach(javaPackage::addUnionModel); + } + + @Override + protected void writeHelperClasses(Client client, CodeModel codeModel, JavaPackage javaPackage, JavaSettings settings) { + // JsonMergePatchHelper + List jsonMergePatchModels = client.getModels().stream() + .filter(model -> ModelUtil.isGeneratingModel(model) && ClientModelUtil.isJsonMergePatchModel(model, settings)) + .collect(Collectors.toList()); + if (!jsonMergePatchModels.isEmpty()) { + javaPackage.addJsonMergePatchHelper(jsonMergePatchModels); + } + + // MultipartFormDataHelper + final boolean generateMultipartFormDataHelper = client.getModels().stream() + .filter(ModelUtil::isGeneratingModel) + .anyMatch(ClientModelUtil::isMultipartModel); + if (generateMultipartFormDataHelper) { + if (JavaSettings.getInstance().isBranded()) { + javaPackage.addJavaFromResources(settings.getPackage(settings.getImplementationSubpackage()), ClientModelUtil.MULTI_PART_FORM_DATA_HELPER_CLASS_NAME); + } else { + javaPackage.addJavaFromResources(settings.getPackage(settings.getImplementationSubpackage()), + ClientModelUtil.GENERIC_MULTI_PART_FORM_DATA_HELPER_CLASS_NAME, + ClientModelUtil.MULTI_PART_FORM_DATA_HELPER_CLASS_NAME); + } + } + + // OperationLocationPollingStrategy + if (ClientModelUtil.requireOperationLocationPollingStrategy(codeModel)) { + javaPackage.addJavaFromResources(settings.getPackage(settings.getImplementationSubpackage()), + ClientModelUtil.OPERATION_LOCATION_POLLING_STRATEGY); + javaPackage.addJavaFromResources(settings.getPackage(settings.getImplementationSubpackage()), + ClientModelUtil.SYNC_OPERATION_LOCATION_POLLING_STRATEGY); + javaPackage.addJavaFromResources(settings.getPackage(settings.getImplementationSubpackage()), + ClientModelUtil.POLLING_UTILS); + } + } + + @Override + public void writeFile(String fileName, String content, List sourceMap) { + File outputFile = FileUtil.writeToFile(emitterOptions.getOutputDir(), fileName, content); + LOGGER.info("Write file: {}", outputFile.getAbsolutePath()); + } + + private static final Map SETTINGS_MAP = new HashMap<>(); + + static { + SETTINGS_MAP.put("data-plane", true); + + SETTINGS_MAP.put("sdk-integration", true); + SETTINGS_MAP.put("regenerate-pom", true); + + SETTINGS_MAP.put("license-header", "MICROSOFT_MIT_SMALL_TYPESPEC"); + SETTINGS_MAP.put("generate-client-interfaces", false); + SETTINGS_MAP.put("generate-client-as-impl", true); + SETTINGS_MAP.put("generate-sync-async-clients", true); + SETTINGS_MAP.put("generate-builder-per-client", false); + SETTINGS_MAP.put("sync-methods", "all"); + SETTINGS_MAP.put("enable-sync-stack", true); + SETTINGS_MAP.put("enable-page-size", true); + + SETTINGS_MAP.put("use-default-http-status-code-to-exception-type-mapping", true); + SETTINGS_MAP.put("polling", new HashMap()); + + SETTINGS_MAP.put("client-logger", true); + SETTINGS_MAP.put("required-fields-as-ctor-args", true); + SETTINGS_MAP.put("required-parameter-client-methods", true); + SETTINGS_MAP.put("generic-response-type", true); + SETTINGS_MAP.put("output-model-immutable", true); + SETTINGS_MAP.put("client-flattened-annotation-target", "disabled"); + SETTINGS_MAP.put("disable-required-property-annotation", true); + // Defaulting to KeyCredential and not providing TypeSpec services to generate with AzureKeyCredential. + SETTINGS_MAP.put("use-key-credential", true); + } + + public Map getCrossLanguageDefinitionMap() { + return this.crossLanguageDefinitionsMap; + } + + public static class MockConnection extends Connection { + public MockConnection() { + super(new OutputStream() { + @Override + public void write(int b) { + // NO-OP + } + }, null); + } + } + + public TypeSpecPlugin(EmitterOptions options, boolean sdkIntegration) { + super(new MockConnection(), "dummy", "dummy"); + this.emitterOptions = options; + SETTINGS_MAP.put("namespace", options.getNamespace()); + if (!CoreUtils.isNullOrEmpty(options.getOutputDir())) { + SETTINGS_MAP.put("output-folder", options.getOutputDir()); + } + if (!CoreUtils.isNullOrEmpty(options.getServiceName())) { + SETTINGS_MAP.put("service-name", options.getServiceName()); + } + if (options.getPartialUpdate() != null) { + SETTINGS_MAP.put("partial-update", options.getPartialUpdate()); + } + if (!CoreUtils.isNullOrEmpty(options.getServiceVersions())) { + SETTINGS_MAP.put("service-versions", options.getServiceVersions()); + } + if (options.getGenerateSamples() != null) { + SETTINGS_MAP.put("generate-samples", options.getGenerateSamples()); + } + if (options.getGenerateTests() != null) { + SETTINGS_MAP.put("generate-tests", options.getGenerateTests()); + } + if (options.getEnableSyncStack() != null) { + SETTINGS_MAP.put("enable-sync-stack", options.getEnableSyncStack()); + } + if (options.getStreamStyleSerialization() != null) { + SETTINGS_MAP.put("stream-style-serialization", options.getStreamStyleSerialization()); + } + + SETTINGS_MAP.put("sdk-integration", sdkIntegration); + SETTINGS_MAP.put("regenerate-pom", sdkIntegration); + + if (options.getCustomTypes() != null) { + SETTINGS_MAP.put("custom-types", options.getCustomTypes()); + } + + if (options.getCustomTypeSubpackage() != null) { + SETTINGS_MAP.put("custom-types-subpackage", options.getCustomTypeSubpackage()); + } + + if (options.getModelsSubpackage() != null) { + SETTINGS_MAP.put("models-subpackage", options.getModelsSubpackage()); + } + + if (options.getCustomizationClass() != null) { + SETTINGS_MAP.put("customization-class", + Paths.get(options.getOutputDir()).resolve(options.getCustomizationClass()).toAbsolutePath().toString()); + } + + if (emitterOptions.getPolling() != null) { + SETTINGS_MAP.put("polling", options.getPolling()); + } + + if (options.getFlavor() != null) { + SETTINGS_MAP.put("flavor", options.getFlavor()); + } + + if (options.getFlavor() != null && !"azure".equalsIgnoreCase(options.getFlavor())) { + SETTINGS_MAP.put("sdk-integration", false); + SETTINGS_MAP.put("license-header", "SMALL_TYPESPEC"); + + SETTINGS_MAP.put("sync-methods", "sync-only"); + SETTINGS_MAP.put("generate-samples", false); + SETTINGS_MAP.put("generate-tests", false); + } + JavaSettingsAccessor.setHost(this); + LOGGER.info("Output folder: {}", options.getOutputDir()); + LOGGER.info("Namespace: {}", JavaSettings.getInstance().getPackage()); + + Mappers.setFactory(new TypeSpecMapperFactory()); + } + + @SuppressWarnings("unchecked") + @Override + public T getValue(String key, ReadValueCallback converter) { + return (T) SETTINGS_MAP.get(key); + } + + @SuppressWarnings("unchecked") + @Override + public T getValueWithJsonReader(String key, ReadValueCallback converter) { + return (T) SETTINGS_MAP.get(key); + } + + @Override + public void message(Message message) { + String log = message.getText(); + switch (message.getChannel()) { + case INFORMATION: + LOGGER.info(log); + break; + + case WARNING: + LOGGER.warn(log); + break; + + case ERROR: + case FATAL: + LOGGER.error(log); + break; + + case DEBUG: + LOGGER.debug(log); + break; + + default: + LOGGER.info(log); + break; + } + } +} diff --git a/packages/http-client-java/generator/http-client-generator/src/main/java/com/microsoft/typespec/http/client/generator/fluent/TypeSpecFluentMapperFactory.java b/packages/http-client-java/generator/http-client-generator/src/main/java/com/microsoft/typespec/http/client/generator/fluent/TypeSpecFluentMapperFactory.java new file mode 100644 index 0000000000..9779f6830a --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator/src/main/java/com/microsoft/typespec/http/client/generator/fluent/TypeSpecFluentMapperFactory.java @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.fluent; + +import com.microsoft.typespec.http.client.generator.mgmt.mapper.FluentMapperFactory; +import com.microsoft.typespec.http.client.generator.core.mapper.ClientMapper; +import com.microsoft.typespec.http.client.generator.core.mapper.ModelMapper; +import com.microsoft.typespec.http.client.generator.core.mapper.ModelPropertyMapper; +import com.microsoft.typespec.http.client.generator.core.mapper.ObjectMapper; +import com.microsoft.typespec.http.client.generator.core.mapper.PrimitiveMapper; +import com.microsoft.typespec.http.client.generator.mapper.TypeSpecClientMapper; +import com.microsoft.typespec.http.client.generator.mapper.TypeSpecPrimitiveMapper; + +public class TypeSpecFluentMapperFactory extends FluentMapperFactory { + @Override + public ClientMapper getClientMapper() { + return TypeSpecClientMapper.getInstance(); + } + + @Override + public PrimitiveMapper getPrimitiveMapper() { + return TypeSpecPrimitiveMapper.getInstance(); + } + + @Override + public ObjectMapper getObjectMapper() { + return TypeSpecFluentObjectMapper.getInstance(); + } + + @Override + public ModelMapper getModelMapper() { + return TypeSpecFluentModelMapper.getInstance(); + } + + @Override + public ModelPropertyMapper getModelPropertyMapper() { + return TypeSpecFluentModelPropertyMapper.getInstance(); + } +} diff --git a/packages/http-client-java/generator/http-client-generator/src/main/java/com/microsoft/typespec/http/client/generator/fluent/TypeSpecFluentModelMapper.java b/packages/http-client-java/generator/http-client-generator/src/main/java/com/microsoft/typespec/http/client/generator/fluent/TypeSpecFluentModelMapper.java new file mode 100644 index 0000000000..dc1c521a50 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator/src/main/java/com/microsoft/typespec/http/client/generator/fluent/TypeSpecFluentModelMapper.java @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.fluent; + +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.ObjectSchema; +import com.microsoft.typespec.http.client.generator.mgmt.mapper.FluentModelMapper; + +public class TypeSpecFluentModelMapper extends FluentModelMapper { + private static final TypeSpecFluentModelMapper INSTANCE = new TypeSpecFluentModelMapper(); + + public static TypeSpecFluentModelMapper getInstance() { + return INSTANCE; + } + + @Override + public boolean isPlainObject(ObjectSchema compositeType) { + return false; + } +} diff --git a/packages/http-client-java/generator/http-client-generator/src/main/java/com/microsoft/typespec/http/client/generator/fluent/TypeSpecFluentModelPropertyMapper.java b/packages/http-client-java/generator/http-client-generator/src/main/java/com/microsoft/typespec/http/client/generator/fluent/TypeSpecFluentModelPropertyMapper.java new file mode 100644 index 0000000000..a005b9deb0 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator/src/main/java/com/microsoft/typespec/http/client/generator/fluent/TypeSpecFluentModelPropertyMapper.java @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.fluent; + +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.ObjectSchema; +import com.microsoft.typespec.http.client.generator.core.mapper.ModelPropertyMapper; + +public class TypeSpecFluentModelPropertyMapper extends ModelPropertyMapper { + private static final TypeSpecFluentModelPropertyMapper INSTANCE = new TypeSpecFluentModelPropertyMapper(); + + public static TypeSpecFluentModelPropertyMapper getInstance() { + return INSTANCE; + } + + @Override + public boolean isPlainObject(ObjectSchema compositeType) { + return false; + } +} diff --git a/packages/http-client-java/generator/http-client-generator/src/main/java/com/microsoft/typespec/http/client/generator/fluent/TypeSpecFluentNamer.java b/packages/http-client-java/generator/http-client-generator/src/main/java/com/microsoft/typespec/http/client/generator/fluent/TypeSpecFluentNamer.java new file mode 100644 index 0000000000..01b8305344 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator/src/main/java/com/microsoft/typespec/http/client/generator/fluent/TypeSpecFluentNamer.java @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.fluent; + +import com.microsoft.typespec.http.client.generator.TypeSpecPlugin; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.CodeModel; +import com.microsoft.typespec.http.client.generator.core.extension.plugin.NewPlugin; +import com.azure.json.JsonReader; +import com.azure.json.ReadValueCallback; +import com.microsoft.typespec.http.client.generator.mgmt.FluentNamer; + +import java.nio.file.Path; +import java.util.Map; + +public class TypeSpecFluentNamer extends FluentNamer { + private final Map settingsMap; + private final CodeModel codeModel; + public TypeSpecFluentNamer(NewPlugin plugin, String pluginName, String sessionId, Map settingsMap, CodeModel codeModel) { + super(plugin, new TypeSpecPlugin.MockConnection(), pluginName, sessionId); + this.settingsMap = settingsMap; + this.codeModel = codeModel; + } + + @Override + protected CodeModel getCodeModelAndWriteToTargetFolder(Path codeModelFolder) { + return this.codeModel; + } + + @SuppressWarnings("unchecked") + @Override + public T getValue(String key, ReadValueCallback converter) { + // in case parent class constructor calls this method, e.g. new PluginLogger() + if (settingsMap == null) { + return null; + } + return (T) settingsMap.get(key); + } + + @SuppressWarnings("unchecked") + @Override + public T getValueWithJsonReader(String key, ReadValueCallback converter) { + // in case parent class constructor calls this method, e.g. new PluginLogger() + if (settingsMap == null) { + return null; + } + return (T) settingsMap.get(key); + } +} diff --git a/packages/http-client-java/generator/http-client-generator/src/main/java/com/microsoft/typespec/http/client/generator/fluent/TypeSpecFluentObjectMapper.java b/packages/http-client-java/generator/http-client-generator/src/main/java/com/microsoft/typespec/http/client/generator/fluent/TypeSpecFluentObjectMapper.java new file mode 100644 index 0000000000..6069419ca2 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator/src/main/java/com/microsoft/typespec/http/client/generator/fluent/TypeSpecFluentObjectMapper.java @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.fluent; + +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.ObjectSchema; +import com.microsoft.typespec.http.client.generator.mgmt.mapper.FluentObjectMapper; + +public class TypeSpecFluentObjectMapper extends FluentObjectMapper { + private static final TypeSpecFluentObjectMapper INSTANCE = new TypeSpecFluentObjectMapper(); + public static TypeSpecFluentObjectMapper getInstance() { + return INSTANCE; + } + + @Override + public boolean isPlainObject(ObjectSchema compositeType) { + return false; + } +} diff --git a/packages/http-client-java/generator/http-client-generator/src/main/java/com/microsoft/typespec/http/client/generator/fluent/TypeSpecFluentPlugin.java b/packages/http-client-java/generator/http-client-generator/src/main/java/com/microsoft/typespec/http/client/generator/fluent/TypeSpecFluentPlugin.java new file mode 100644 index 0000000000..6915c9f2a1 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator/src/main/java/com/microsoft/typespec/http/client/generator/fluent/TypeSpecFluentPlugin.java @@ -0,0 +1,173 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.fluent; + +import com.microsoft.typespec.http.client.generator.TypeSpecPlugin; +import com.microsoft.typespec.http.client.generator.core.extension.model.Message; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.CodeModel; +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; +import com.microsoft.typespec.http.client.generator.mgmt.FluentGen; +import com.microsoft.typespec.http.client.generator.mgmt.mapper.FluentMapper; +import com.microsoft.typespec.http.client.generator.mgmt.model.javamodel.FluentJavaPackage; +import com.microsoft.typespec.http.client.generator.mgmt.FluentNamer; +import com.microsoft.typespec.http.client.generator.core.mapper.Mappers; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.Client; +import com.azure.core.util.CoreUtils; +import com.azure.json.JsonReader; +import com.azure.json.ReadValueCallback; +import com.microsoft.typespec.http.client.generator.model.EmitterOptions; +import com.microsoft.typespec.http.client.generator.util.FileUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class TypeSpecFluentPlugin extends FluentGen { + private static final Logger LOGGER = LoggerFactory.getLogger(TypeSpecFluentPlugin.class); + private final EmitterOptions emitterOptions; + + public TypeSpecFluentPlugin(EmitterOptions emitterOptions, boolean sdkIntegration) { + super(new TypeSpecPlugin.MockConnection(), "dummy", "dummy"); + this.emitterOptions = emitterOptions; + SETTINGS_MAP.put("namespace", emitterOptions.getNamespace()); + if (!CoreUtils.isNullOrEmpty(emitterOptions.getOutputDir())) { + SETTINGS_MAP.put("output-folder", emitterOptions.getOutputDir()); + } + if (!CoreUtils.isNullOrEmpty(emitterOptions.getServiceName())) { + SETTINGS_MAP.put("service-name", emitterOptions.getServiceName()); + } + if (emitterOptions.getGenerateSamples() != null) { + SETTINGS_MAP.put("generate-samples", emitterOptions.getGenerateSamples()); + } + if (emitterOptions.getGenerateTests() != null) { + SETTINGS_MAP.put("generate-tests", emitterOptions.getGenerateTests()); + } + if (emitterOptions.getArm()) { + SETTINGS_MAP.put("fluent", "lite"); + } + SETTINGS_MAP.put("sdk-integration", sdkIntegration); + SETTINGS_MAP.put("output-model-immutable", true); + SETTINGS_MAP.put("uuid-as-string", true); + SETTINGS_MAP.put("stream-style-serialization", emitterOptions.getStreamStyleSerialization()); + + LOGGER.info("Output folder: {}", emitterOptions.getOutputDir()); + LOGGER.info("Namespace: {}", JavaSettings.getInstance().getPackage()); + } + + public CodeModel preProcess(CodeModel codeModel) { + // transform code model + FluentNamer fluentNamer = new TypeSpecFluentNamer(this, pluginName, sessionId, SETTINGS_MAP, codeModel); + return fluentNamer.processCodeModel(); + } + + public Client processClient(CodeModel codeModel) { + + // call FluentGen.handleMap + + return handleMap(codeModel); + } + + public FluentJavaPackage processTemplates(CodeModel codeModel, Client client) { + FluentJavaPackage javaPackage = handleTemplate(client); + handleFluentLite(codeModel, client, javaPackage); + return javaPackage; + } + + @Override + public void writeFile(String fileName, String content, List sourceMap) { + File outputFile = FileUtil.writeToFile(emitterOptions.getOutputDir(), fileName, content); + LOGGER.info("Write file: {}", outputFile.getAbsolutePath()); + } + + @Override + protected FluentMapper getFluentMapper() { + FluentMapper fluentMapper = super.getFluentMapper(); + Mappers.setFactory(new TypeSpecFluentMapperFactory()); + return fluentMapper; + } + + private static final Map SETTINGS_MAP = new HashMap<>(); + + // from fluentnamer/readme.md + static { + SETTINGS_MAP.put("data-plane", false); + + SETTINGS_MAP.put("sdk-integration", true); + SETTINGS_MAP.put("regenerate-pom", true); + + SETTINGS_MAP.put("license-header", "MICROSOFT_MIT_SMALL_TYPESPEC"); + + SETTINGS_MAP.put("generic-response-type", false); + SETTINGS_MAP.put("generate-client-interfaces", true); + SETTINGS_MAP.put("client-logger", true); + + SETTINGS_MAP.put("required-parameter-client-methods", true); + SETTINGS_MAP.put("client-flattened-annotation-target", "none"); + SETTINGS_MAP.put("null-byte-array-maps-to-empty-array", true); + SETTINGS_MAP.put("graal-vm-config", true); + SETTINGS_MAP.put("sync-methods", "all"); + SETTINGS_MAP.put("client-side-validations", true); + SETTINGS_MAP.put("stream-style-serialization", false); +// SETTINGS_MAP.put("pipeline.fluentgen.naming.override", getNamingOverrides()); + } + + private static Map getNamingOverrides() { + Map namingOverrides = new HashMap<>(); + namingOverrides.put("eTag", "etag"); + namingOverrides.put("userName", "username"); + namingOverrides.put("metaData", "metadata"); + namingOverrides.put("timeStamp", "timestamp"); + namingOverrides.put("hostName", "hostname"); + namingOverrides.put("webHook", "webhook"); + namingOverrides.put("coolDown", "cooldown"); + namingOverrides.put("resourceregion", "resourceRegion"); + namingOverrides.put("sTag", "stag"); + namingOverrides.put("tagname", "tagName"); + namingOverrides.put("tagvalue", "tagValue"); + + return namingOverrides; + } + + @SuppressWarnings("unchecked") + @Override + public T getValue(String key, ReadValueCallback converter) { + return (T) SETTINGS_MAP.get(key); + } + + @SuppressWarnings("unchecked") + @Override + public T getValueWithJsonReader(String key, ReadValueCallback converter) { + return (T) SETTINGS_MAP.get(key); + } + + @Override + public void message(Message message) { + String log = message.getText(); + switch (message.getChannel()) { + case INFORMATION: + LOGGER.info(log); + break; + + case WARNING: + LOGGER.warn(log); + break; + + case ERROR: + case FATAL: + LOGGER.error(log); + break; + + case DEBUG: + LOGGER.debug(log); + break; + + default: + LOGGER.info(log); + break; + } + } +} diff --git a/packages/http-client-java/generator/http-client-generator/src/main/java/com/microsoft/typespec/http/client/generator/mapper/TypeSpecClientMapper.java b/packages/http-client-java/generator/http-client-generator/src/main/java/com/microsoft/typespec/http/client/generator/mapper/TypeSpecClientMapper.java new file mode 100644 index 0000000000..ffaf9f1559 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator/src/main/java/com/microsoft/typespec/http/client/generator/mapper/TypeSpecClientMapper.java @@ -0,0 +1,63 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mapper; + +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Client; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.CodeModel; +import com.microsoft.typespec.http.client.generator.core.mapper.ClientMapper; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientModel; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientResponse; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.EnumType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ServiceClient; +import com.microsoft.typespec.http.client.generator.util.ModelUtil; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +public class TypeSpecClientMapper extends ClientMapper { + + private static final ClientMapper INSTANCE = new TypeSpecClientMapper(); + + public static ClientMapper getInstance() { + return INSTANCE; + } + + protected TypeSpecClientMapper() { + } + + @Override + protected Map processClients(List clients, CodeModel codeModel) { + Map serviceClientsMap = new LinkedHashMap<>(); + TypeSpecServiceClientMapper mapper = new TypeSpecServiceClientMapper(); + for (Client client : clients) { + serviceClientsMap.put(mapper.map(client, codeModel), client); + } + return serviceClientsMap; + } + + @Override + protected List getModelsPackages(List clientModels, List enumTypes, List responseModels) { + + Set packages = clientModels.stream() + .filter(ModelUtil::isGeneratingModel) + .map(ClientModel::getPackage) + .collect(Collectors.toSet()); + + packages.addAll(enumTypes.stream() + .filter(ModelUtil::isGeneratingModel) + .map(EnumType::getPackage) + .collect(Collectors.toSet())); + + packages.addAll(responseModels.stream() + .filter(ModelUtil::isGeneratingModel) + .map(ClientResponse::getPackage) + .collect(Collectors.toSet())); + + return new ArrayList<>(packages); + } +} diff --git a/packages/http-client-java/generator/http-client-generator/src/main/java/com/microsoft/typespec/http/client/generator/mapper/TypeSpecMapperFactory.java b/packages/http-client-java/generator/http-client-generator/src/main/java/com/microsoft/typespec/http/client/generator/mapper/TypeSpecMapperFactory.java new file mode 100644 index 0000000000..2552fbcba9 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator/src/main/java/com/microsoft/typespec/http/client/generator/mapper/TypeSpecMapperFactory.java @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mapper; + +import com.microsoft.typespec.http.client.generator.core.mapper.ClientMapper; +import com.microsoft.typespec.http.client.generator.core.mapper.DefaultMapperFactory; +import com.microsoft.typespec.http.client.generator.core.mapper.PrimitiveMapper; + +public class TypeSpecMapperFactory extends DefaultMapperFactory { + + @Override + public ClientMapper getClientMapper() { + return TypeSpecClientMapper.getInstance(); + } + + @Override + public PrimitiveMapper getPrimitiveMapper() { + return TypeSpecPrimitiveMapper.getInstance(); + } +} diff --git a/packages/http-client-java/generator/http-client-generator/src/main/java/com/microsoft/typespec/http/client/generator/mapper/TypeSpecPrimitiveMapper.java b/packages/http-client-java/generator/http-client-generator/src/main/java/com/microsoft/typespec/http/client/generator/mapper/TypeSpecPrimitiveMapper.java new file mode 100644 index 0000000000..fe2214aab8 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator/src/main/java/com/microsoft/typespec/http/client/generator/mapper/TypeSpecPrimitiveMapper.java @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mapper; + +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.PrimitiveSchema; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Schema; +import com.microsoft.typespec.http.client.generator.core.mapper.PrimitiveMapper; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClassType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.IType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.PrimitiveType; + +public class TypeSpecPrimitiveMapper extends PrimitiveMapper { + + private static final PrimitiveMapper INSTANCE = new TypeSpecPrimitiveMapper(); + + public static PrimitiveMapper getInstance() { + return INSTANCE; + } + + @Override + protected IType createPrimitiveType(PrimitiveSchema primaryType) { + if (primaryType.getType() == Schema.AllSchemaTypes.DATE) { + return ClassType.LOCAL_DATE; + } else if (primaryType.getType() == Schema.AllSchemaTypes.UNIXTIME) { + return PrimitiveType.UNIX_TIME_LONG; + } else { + return super.createPrimitiveType(primaryType); + } + } +} diff --git a/packages/http-client-java/generator/http-client-generator/src/main/java/com/microsoft/typespec/http/client/generator/mapper/TypeSpecServiceClientMapper.java b/packages/http-client-java/generator/http-client-generator/src/main/java/com/microsoft/typespec/http/client/generator/mapper/TypeSpecServiceClientMapper.java new file mode 100644 index 0000000000..eeecda393a --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator/src/main/java/com/microsoft/typespec/http/client/generator/mapper/TypeSpecServiceClientMapper.java @@ -0,0 +1,82 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.mapper; + +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.Client; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.CodeModel; +import com.microsoft.typespec.http.client.generator.core.extension.model.codemodel.OperationGroup; +import com.microsoft.typespec.http.client.generator.core.mapper.Mappers; +import com.microsoft.typespec.http.client.generator.core.mapper.ServiceClientMapper; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.MethodGroupClient; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.PipelinePolicyDetails; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.Proxy; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ServiceClient; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ServiceClientProperty; +import com.microsoft.typespec.http.client.generator.core.util.ClientModelUtil; +import com.microsoft.typespec.http.client.generator.core.util.SchemaUtil; +import com.azure.core.util.CoreUtils; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class TypeSpecServiceClientMapper extends ServiceClientMapper { + + public ServiceClient map(Client client, CodeModel codeModel) { + ServiceClient.Builder builder = createClientBuilder(); + + String baseName = SchemaUtil.getJavaName(client); + String className = ClientModelUtil.getClientImplementClassName(baseName); + String packageName = ClientModelUtil.getServiceClientPackageName(className); + builder.interfaceName(baseName) + .className(className) + .packageName(packageName); + if (client.getLanguage().getJava() != null && !CoreUtils.isNullOrEmpty(client.getLanguage().getJava().getNamespace())) { + builder.builderPackageName(client.getLanguage().getJava().getNamespace()); + } + + Proxy proxy = null; + OperationGroup clientOperationGroup = client.getOperationGroups().stream() + .filter(og -> CoreUtils.isNullOrEmpty(SchemaUtil.getJavaName(og))) + .findFirst().orElse(null); + if (clientOperationGroup != null) { + proxy = processClientOperations(builder, clientOperationGroup.getOperations(), baseName); + } else { + builder.clientMethods(Collections.emptyList()); + } + + List properties = processClientProperties(client, + client.getServiceVersion() == null ? null : client.getServiceVersion().getLanguage().getJava().getName()); + + List methodGroupClients = new ArrayList<>(); + client.getOperationGroups().stream() + .filter(og -> !CoreUtils.isNullOrEmpty(SchemaUtil.getJavaName(og))) + .forEach(og -> methodGroupClients.add(Mappers.getMethodGroupMapper().map(og, properties))); + builder.methodGroupClients(methodGroupClients); + + if (proxy == null) { + proxy = methodGroupClients.iterator().next().getProxy(); + } + + // TODO (weidxu): security definition could be different for different client + processParametersAndConstructors(builder, client, codeModel, properties, proxy); + + processPipelinePolicyDetails(builder, client); + + builder.crossLanguageDefinitionId(client.getCrossLanguageDefinitionId()); + + return builder.build(); + } + + private static void processPipelinePolicyDetails(ServiceClient.Builder builder, Client client) { + // handle corner case of RequestIdPolicy with header name "client-request-id" + final String clientRequestIdHeaderName = "client-request-id"; + final boolean clientRequestIdHeaderInClient = client.getOperationGroups().stream() + .flatMap(og -> og.getOperations().stream()) + .anyMatch(o -> o.getSpecialHeaders() != null && o.getSpecialHeaders().contains(clientRequestIdHeaderName)); + if (clientRequestIdHeaderInClient) { + builder.pipelinePolicyDetails(new PipelinePolicyDetails().setRequestIdHeaderName(clientRequestIdHeaderName)); + } + } +} diff --git a/packages/http-client-java/generator/http-client-generator/src/main/java/com/microsoft/typespec/http/client/generator/model/DevOptions.java b/packages/http-client-java/generator/http-client-generator/src/main/java/com/microsoft/typespec/http/client/generator/model/DevOptions.java new file mode 100644 index 0000000000..368a103ac3 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator/src/main/java/com/microsoft/typespec/http/client/generator/model/DevOptions.java @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.model; + +import com.microsoft.typespec.http.client.generator.core.extension.base.util.JsonUtils; +import com.azure.json.JsonReader; +import com.azure.json.JsonSerializable; +import com.azure.json.JsonWriter; + +import java.io.IOException; + +public class DevOptions implements JsonSerializable { + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + return jsonWriter.writeStartObject().writeEndObject(); + } + + public static DevOptions fromJson(JsonReader jsonReader) throws IOException { + return JsonUtils.readEmptyObject(jsonReader, DevOptions::new); + } +} diff --git a/packages/http-client-java/generator/http-client-generator/src/main/java/com/microsoft/typespec/http/client/generator/model/EmitterOptions.java b/packages/http-client-java/generator/http-client-generator/src/main/java/com/microsoft/typespec/http/client/generator/model/EmitterOptions.java new file mode 100644 index 0000000000..1c42ca90f8 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator/src/main/java/com/microsoft/typespec/http/client/generator/model/EmitterOptions.java @@ -0,0 +1,195 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.model; + +import com.microsoft.typespec.http.client.generator.core.extension.plugin.JavaSettings; +import com.microsoft.typespec.http.client.generator.core.extension.base.util.JsonUtils; +import com.azure.core.util.CoreUtils; +import com.azure.json.JsonReader; +import com.azure.json.JsonSerializable; +import com.azure.json.JsonWriter; + +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class EmitterOptions implements JsonSerializable { + private String namespace; + private String outputDir; + private String flavor = "Azure"; + private String serviceName; + private List serviceVersions; + private Boolean generateTests = true; + private Boolean generateSamples = true; + private Boolean enableSyncStack = true; + private Boolean streamStyleSerialization = true; + private Boolean partialUpdate; + private String customTypes; + private String customTypeSubpackage; + private String customizationClass; + private Boolean includeApiViewProperties = true; + private Map polling = new HashMap<>(); + private Boolean arm = false; + private String modelsSubpackage; + private DevOptions devOptions; + + public String getNamespace() { + return namespace; + } + + public String getOutputDir() { + return outputDir; + } + + public String getServiceName() { + return serviceName; + } + + public Boolean getPartialUpdate() { + return partialUpdate; + } + + public Boolean getGenerateTests() { + return generateTests; + } + + public Boolean getGenerateSamples() { + return generateSamples; + } + + public Boolean getEnableSyncStack() { + return enableSyncStack; + } + + public Boolean getStreamStyleSerialization() { + return streamStyleSerialization; + } + + public EmitterOptions setNamespace(String namespace) { + this.namespace = namespace; + return this; + } + + public EmitterOptions setOutputDir(String outputDir) { + this.outputDir = outputDir; + return this; + } + + public List getServiceVersions() { + return serviceVersions; + } + + public DevOptions getDevOptions() { + return devOptions; + } + + public String getCustomTypes() { + return customTypes; + } + + public String getCustomTypeSubpackage() { + return customTypeSubpackage; + } + + public String getCustomizationClass() { + return customizationClass; + } + + public Boolean includeApiViewProperties() { + return includeApiViewProperties; + } + + public Map getPolling() { + return polling; + } + + public void setPolling(Map polling) { + this.polling = polling; + } + + public Boolean getArm() { + return arm; + } + + public String getModelsSubpackage() { + return modelsSubpackage; + } + + public String getFlavor() { + return flavor; + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + return jsonWriter.writeStartObject() + .writeStringField("namespace", namespace) + .writeStringField("output-dir", outputDir) + .writeStringField("flavor", flavor) + .writeStringField("service-name", serviceName) + .writeArrayField("service-versions", serviceVersions, JsonWriter::writeString) + .writeBooleanField("generate-tests", generateTests) + .writeBooleanField("generate-samples", generateSamples) + .writeBooleanField("enable-sync-stack", enableSyncStack) + .writeBooleanField("stream-style-serialization", streamStyleSerialization) + .writeBooleanField("partial-update", partialUpdate) + .writeStringField("custom-types", customTypes) + .writeStringField("custom-types-subpackage", customTypeSubpackage) + .writeStringField("customization-class", customizationClass) + .writeBooleanField("include-api-view-properties", includeApiViewProperties) + .writeMapField("polling", polling, JsonWriter::writeJson) + .writeBooleanField("arm", arm) + .writeStringField("models-subpackage", modelsSubpackage) + .writeJsonField("dev-options", devOptions) + .writeEndObject(); + } + + public static EmitterOptions fromJson(JsonReader jsonReader) throws IOException { + return JsonUtils.readObject(jsonReader, EmitterOptions::new, (options, fieldName, reader) -> { + if ("namespace".equals(fieldName)) { + options.namespace = emptyToNull(reader.getString()); + } else if ("output-dir".equals(fieldName)) { + options.outputDir = emptyToNull(reader.getString()); + } else if ("flavor".equals(fieldName)) { + options.flavor = emptyToNull(reader.getString()); + } else if ("service-name".equals(fieldName)) { + options.serviceName = emptyToNull(reader.getString()); + } else if ("service-versions".equals(fieldName)) { + options.serviceVersions = reader.readArray(JsonReader::getString); + } else if ("generate-tests".equals(fieldName)) { + options.generateTests = reader.getNullable(JsonReader::getBoolean); + } else if ("generate-samples".equals(fieldName)) { + options.generateSamples = reader.getNullable(JsonReader::getBoolean); + } else if ("enable-sync-stack".equals(fieldName)) { + options.enableSyncStack = reader.getNullable(JsonReader::getBoolean); + } else if ("stream-style-serialization".equals(fieldName)) { + options.streamStyleSerialization = reader.getNullable(JsonReader::getBoolean); + } else if ("partial-update".equals(fieldName)) { + options.partialUpdate = reader.getNullable(JsonReader::getBoolean); + } else if ("custom-types".equals(fieldName)) { + options.customTypes = emptyToNull(reader.getString()); + } else if ("custom-types-subpackage".equals(fieldName)) { + options.customTypeSubpackage = emptyToNull(reader.getString()); + } else if ("customization-class".equals(fieldName)) { + options.customizationClass = emptyToNull(reader.getString()); + } else if ("include-api-view-properties".equals(fieldName)) { + options.includeApiViewProperties = reader.getNullable(JsonReader::getBoolean); + } else if ("polling".equals(fieldName)) { + options.polling = reader.readMap(JavaSettings.PollingDetails::fromJson); + } else if ("arm".equals(fieldName)) { + options.arm = reader.getNullable(JsonReader::getBoolean); + } else if ("models-subpackage".equals(fieldName)) { + options.modelsSubpackage = emptyToNull(reader.getString()); + } else if ("dev-options".equals(fieldName)) { + options.devOptions = DevOptions.fromJson(reader); + } else { + reader.skipChildren(); + } + }); + } + + private static String emptyToNull(String str) { + return CoreUtils.isNullOrEmpty(str) ? null : str; + } +} diff --git a/packages/http-client-java/generator/http-client-generator/src/main/java/com/microsoft/typespec/http/client/generator/util/FileUtil.java b/packages/http-client-java/generator/http-client-generator/src/main/java/com/microsoft/typespec/http/client/generator/util/FileUtil.java new file mode 100644 index 0000000000..1f9fe13ed0 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator/src/main/java/com/microsoft/typespec/http/client/generator/util/FileUtil.java @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.util; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; + +public class FileUtil { + + private FileUtil() {} + /** + * Write given content to the given file under given path. + * + * @param outputDir output directory, will create if not exist + * @param fileName filename + * @param content content to write to the file + * @return the file + */ + public static File writeToFile(String outputDir, String fileName, String content) { + File outputFile = Paths.get(outputDir, fileName).toAbsolutePath().toFile(); + File parentFile = outputFile.getParentFile(); + if (!parentFile.exists()) { + parentFile.mkdirs(); + } + + try { + Files.writeString(outputFile.toPath(), content); + } catch (IOException e) { + throw new IllegalStateException(e); + } + return outputFile; + } +} diff --git a/packages/http-client-java/generator/http-client-generator/src/main/java/com/microsoft/typespec/http/client/generator/util/ModelUtil.java b/packages/http-client-java/generator/http-client-generator/src/main/java/com/microsoft/typespec/http/client/generator/util/ModelUtil.java new file mode 100644 index 0000000000..a31acd7596 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator/src/main/java/com/microsoft/typespec/http/client/generator/util/ModelUtil.java @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.util; + +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClassType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientModel; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ClientResponse; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.EnumType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.IType; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ImplementationDetails; +import com.microsoft.typespec.http.client.generator.core.model.clientmodel.UnionModel; +import com.microsoft.typespec.http.client.generator.core.util.ClientModelUtil; + +public class ModelUtil { + + public static boolean isGeneratingModel(ClientModel model) { + return model.getImplementationDetails() != null + && (model.getImplementationDetails().isPublic() || model.getImplementationDetails().isInternal()) + && !(isModelUsedOnlyInException(model.getImplementationDetails())) + && !(isExternalModel(model.getImplementationDetails())) + && !(isPagedModel(model.getImplementationDetails())); + } + + public static boolean isGeneratingModel(EnumType model) { + return model.getImplementationDetails() != null + && (model.getImplementationDetails().isPublic() || model.getImplementationDetails().isInternal()) + && !(isModelUsedOnlyInException(model.getImplementationDetails())); + } + + public static boolean isGeneratingModel(ClientResponse response) { + IType bodyType = response.getBodyType(); + boolean ret = ClientModelUtil.isClientModel(bodyType); + if (ret) { + ClassType classType = (ClassType) bodyType; + ClientModel model = ClientModelUtil.getClientModel(classType.getName()); + if (model != null) { + ret = isGeneratingModel(model); + } + } + return ret; + } + + public static boolean isGeneratingModel(UnionModel model) { + return model.getImplementationDetails() != null + && (model.getImplementationDetails().isPublic() || model.getImplementationDetails().isInternal()) + && !(isModelUsedOnlyInException(model.getImplementationDetails())) + && !(isExternalModel(model.getImplementationDetails())); + } + + private static boolean isModelUsedOnlyInException(ImplementationDetails implementationDetails) { + return (implementationDetails.isException() && !implementationDetails.isInput() && !implementationDetails.isOutput()); + } + + private static boolean isPagedModel(ImplementationDetails implementationDetails) { + return (implementationDetails.getUsages() != null && implementationDetails.getUsages().contains(ImplementationDetails.Usage.PAGED)); + } + + private static boolean isExternalModel(ImplementationDetails implementationDetails) { + return (implementationDetails.getUsages() != null && implementationDetails.getUsages().contains(ImplementationDetails.Usage.EXTERNAL)); + } +} diff --git a/packages/http-client-java/generator/http-client-generator/src/main/java/com/microsoft/typespec/http/client/generator/util/TspLocationUtil.java b/packages/http-client-java/generator/http-client-generator/src/main/java/com/microsoft/typespec/http/client/generator/util/TspLocationUtil.java new file mode 100644 index 0000000000..18ca9f0b86 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator/src/main/java/com/microsoft/typespec/http/client/generator/util/TspLocationUtil.java @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.typespec.http.client.generator.util; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.yaml.snakeyaml.Yaml; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +public class TspLocationUtil { + + private static final Logger LOGGER = LoggerFactory.getLogger(TspLocationUtil.class); + + public static class TspLocation { + private String directory; + + public String getDirectory() { + return directory; + } + + public void setDirectory(String directory) { + this.directory = directory; + } + } + + public static String getDirectory(Yaml yaml, Path tspLocationPath) { + String directory = null; + try { + LOGGER.info("tsp-location.yaml file: {}", tspLocationPath.toString()); + String file = Files.readString(tspLocationPath); + directory = yaml.loadAs(file, TspLocation.class).getDirectory(); + } catch (IOException e) { + LOGGER.error("Failed to read tsp-location.yaml"); + } + return directory; + } +} diff --git a/packages/http-client-java/generator/http-client-generator/src/main/resources/readme/eclipse-format-azure-sdk-for-java.xml b/packages/http-client-java/generator/http-client-generator/src/main/resources/readme/eclipse-format-azure-sdk-for-java.xml new file mode 100644 index 0000000000..8e5fa985b2 --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator/src/main/resources/readme/eclipse-format-azure-sdk-for-java.xml @@ -0,0 +1,401 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/http-client-java/generator/http-client-generator/src/main/resources/readme/pom.xml b/packages/http-client-java/generator/http-client-generator/src/main/resources/readme/pom.xml new file mode 100644 index 0000000000..6ef31a2dca --- /dev/null +++ b/packages/http-client-java/generator/http-client-generator/src/main/resources/readme/pom.xml @@ -0,0 +1,61 @@ + + + + + azure-autorest-parent + com.azure.tools + 1.0.0-beta.5 + + + com.azure + customization-loader + 1.0.0-beta.1 + 4.0.0 + + + + com.azure.tools + azure-autorest-customization + 1.0.0-beta.8 + + + + + + spotless + + + spotless + + + + + + + com.diffplug.spotless + spotless-maven-plugin + 2.40.0 + + + + src/main/java/**/*.java + src/samples/java/**/*.java + src/test/java/**/*.java + + + + + + 4.21 + eclipse-format-azure-sdk-for-java.xml + + + + + + + + + diff --git a/packages/http-client-java/generator/http-client-generator/src/test/java/com/microsoft/typespec/http/client/generator/MainTest.java b/packages/http-client-java/generator/http-client-generator/src/test/java/com/microsoft/typespec/http/client/generator/MainTest.java index ed666c7ea6..6d3121b63a 100644 --- a/packages/http-client-java/generator/http-client-generator/src/test/java/com/microsoft/typespec/http/client/generator/MainTest.java +++ b/packages/http-client-java/generator/http-client-generator/src/test/java/com/microsoft/typespec/http/client/generator/MainTest.java @@ -2,13 +2,9 @@ import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.assertEquals; - public class MainTest { @Test public void testHello() { - Main main = new Main(); - assertEquals("Hello friends from TypeSpec Java code generator", main.sayHello("TypeSpec Java code generator")); } } diff --git a/packages/http-client-java/generator/pom.xml b/packages/http-client-java/generator/pom.xml index ead6fb6125..0ef4755186 100644 --- a/packages/http-client-java/generator/pom.xml +++ b/packages/http-client-java/generator/pom.xml @@ -17,5 +17,7 @@ http-client-generator + http-client-generator-mgmt + http-client-generator-core