From 4683e223ebfdd48dc49c360649139e3b0aaa4417 Mon Sep 17 00:00:00 2001 From: "Sameeksha Vaity (from Dev Box)" Date: Tue, 17 Dec 2024 15:50:37 -0800 Subject: [PATCH 1/5] Add compile time codegen tool --- eng/versioning/external_dependencies.txt | 2 + sdk/clientcore/pom.xml | 1 + sdk/clientcore/tools/pom.xml | 14 + .../tools/sdk-codegen-tool/README.md | 135 +++++ sdk/clientcore/tools/sdk-codegen-tool/pom.xml | 112 ++++ .../tools/codegen/AnnotationProcessor.java | 193 +++++++ .../MissingSubstitutionException.java | 15 + .../codegen/models/HttpRequestContext.java | 367 +++++++++++++ .../tools/codegen/models/Substitution.java | 59 ++ .../tools/codegen/models/TemplateInput.java | 222 ++++++++ .../templating/JavaPoetTemplateProcessor.java | 508 ++++++++++++++++++ .../codegen/templating/TemplateProcessor.java | 13 + .../tools/codegen/utils/PathBuilder.java | 83 +++ .../utils/ResponseBodyModeGeneration.java | 36 ++ .../javax.annotation.processing.Processor | 1 + .../codegen/models/TemplateInputTest.java | 100 ++++ .../codegen/templating/APIGenerationTest.java | 60 +++ .../BodyContentTypeProcessorTest.java | 145 +++++ .../HttpPipelineBuilderMethodTest.java | 124 +++++ .../tools/codegen/utils/PathBuilderTest.java | 425 +++++++++++++++ .../utils/ResponseBodyModeGenerationTest.java | 43 ++ 21 files changed, 2658 insertions(+) create mode 100644 sdk/clientcore/tools/pom.xml create mode 100644 sdk/clientcore/tools/sdk-codegen-tool/README.md create mode 100644 sdk/clientcore/tools/sdk-codegen-tool/pom.xml create mode 100644 sdk/clientcore/tools/sdk-codegen-tool/src/main/java/io/generation/tools/codegen/AnnotationProcessor.java create mode 100644 sdk/clientcore/tools/sdk-codegen-tool/src/main/java/io/generation/tools/codegen/exceptions/MissingSubstitutionException.java create mode 100644 sdk/clientcore/tools/sdk-codegen-tool/src/main/java/io/generation/tools/codegen/models/HttpRequestContext.java create mode 100644 sdk/clientcore/tools/sdk-codegen-tool/src/main/java/io/generation/tools/codegen/models/Substitution.java create mode 100644 sdk/clientcore/tools/sdk-codegen-tool/src/main/java/io/generation/tools/codegen/models/TemplateInput.java create mode 100644 sdk/clientcore/tools/sdk-codegen-tool/src/main/java/io/generation/tools/codegen/templating/JavaPoetTemplateProcessor.java create mode 100644 sdk/clientcore/tools/sdk-codegen-tool/src/main/java/io/generation/tools/codegen/templating/TemplateProcessor.java create mode 100644 sdk/clientcore/tools/sdk-codegen-tool/src/main/java/io/generation/tools/codegen/utils/PathBuilder.java create mode 100644 sdk/clientcore/tools/sdk-codegen-tool/src/main/java/io/generation/tools/codegen/utils/ResponseBodyModeGeneration.java create mode 100644 sdk/clientcore/tools/sdk-codegen-tool/src/main/resources/META-INF/maven/services/javax.annotation.processing.Processor create mode 100644 sdk/clientcore/tools/sdk-codegen-tool/src/test/java/io/generation/tools/codegen/models/TemplateInputTest.java create mode 100644 sdk/clientcore/tools/sdk-codegen-tool/src/test/java/io/generation/tools/codegen/templating/APIGenerationTest.java create mode 100644 sdk/clientcore/tools/sdk-codegen-tool/src/test/java/io/generation/tools/codegen/templating/BodyContentTypeProcessorTest.java create mode 100644 sdk/clientcore/tools/sdk-codegen-tool/src/test/java/io/generation/tools/codegen/templating/HttpPipelineBuilderMethodTest.java create mode 100644 sdk/clientcore/tools/sdk-codegen-tool/src/test/java/io/generation/tools/codegen/utils/PathBuilderTest.java create mode 100644 sdk/clientcore/tools/sdk-codegen-tool/src/test/java/io/generation/tools/codegen/utils/ResponseBodyModeGenerationTest.java diff --git a/eng/versioning/external_dependencies.txt b/eng/versioning/external_dependencies.txt index 46b2935401bc8..f412192ab5bfe 100644 --- a/eng/versioning/external_dependencies.txt +++ b/eng/versioning/external_dependencies.txt @@ -53,8 +53,10 @@ com.microsoft.azure.functions:azure-functions-java-library;2.2.0 com.mysql:mysql-connector-j;9.0.0 com.squareup.okhttp3:mockwebserver;4.12.0 com.squareup.okhttp3:okhttp;4.12.0 +com.squareup:javapoet;1.13.0 commons-codec:commons-codec;1.15 commons-net:commons-net;3.9.0 +io.clientcore.tools:sdk-codegen-tool;1.0.0-beta.1 io.cloudevents:cloudevents-api;2.2.0 io.cloudevents:cloudevents-core;2.2.0 io.fabric8:kubernetes-client;6.12.1 diff --git a/sdk/clientcore/pom.xml b/sdk/clientcore/pom.xml index b27a9a86912b1..b52f6d246d2e1 100644 --- a/sdk/clientcore/pom.xml +++ b/sdk/clientcore/pom.xml @@ -13,5 +13,6 @@ core http-okhttp3 http-stress + tools diff --git a/sdk/clientcore/tools/pom.xml b/sdk/clientcore/tools/pom.xml new file mode 100644 index 0000000000000..b4ca842dc7971 --- /dev/null +++ b/sdk/clientcore/tools/pom.xml @@ -0,0 +1,14 @@ + + + 4.0.0 + io.clientcore + clientcore-tools-service + pom + 1.0.0 + + sdk-codegen-tool + + diff --git a/sdk/clientcore/tools/sdk-codegen-tool/README.md b/sdk/clientcore/tools/sdk-codegen-tool/README.md new file mode 100644 index 0000000000000..f3d6386edb4b5 --- /dev/null +++ b/sdk/clientcore/tools/sdk-codegen-tool/README.md @@ -0,0 +1,135 @@ +# Codegen Compile time generator Plugin + +A Java annotation processor for generating HTTP service implementations based on annotated interfaces. + +## Usage + +1. Add the plugin dependency: + ```xml + + + io.clientcore.tools + sdk-codegen-tool + 1.0.0.beta.1 + provided + + + ``` + 1.1. Add the plugin configuration to your `pom.xml`: + ```xml + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + ${project.build.directory}/generated-sources/ + + io.generation.tools.codegen.AnnotationProcessor + + + + + ``` +2. Annotate your interfaces with `@ServiceInterface`, `@HttpRequestInformation` and + `@UnexpectedResponseExceptionDetail` such annotations: + ```java + @ServiceInterface(name = "ExampleClient", host = "{endpoint}/example") + public interface ExampleService { + @HttpRequestInformation(method = HttpMethod.GET, path = "/user/{userId}", expectedStatusCodes = { 200 }) + @UnexpectedResponseExceptionDetail(exceptionTypeName = "CLIENT_AUTHENTICATION", statusCode = { 401 }) + @UnexpectedResponseExceptionDetail(exceptionTypeName = "RESOURCE_NOT_FOUND", statusCode = { 404 }) + @UnexpectedResponseExceptionDetail(exceptionTypeName = "RESOURCE_MODIFIED", statusCode = { 409 }) + User getUser(@PathParam("userId") String userId); + } + ``` + +3. Build your project and the plugin will generate an implementation of the annotated interface. + The processor would generate an implementation: + ```java + public class ExampleServiceImpl implements ExampleService { + private static final ClientLogger LOGGER = new ClientLogger(OpenAIClientServiceImpl.class); + + private final HttpPipeline defaultPipeline; + + private final ObjectSerializer serializer; + + private final String endpoint; + + private final OpenAIServiceVersion serviceVersion; + + private String apiVersion; + + public OpenAIClientServiceImpl(HttpPipeline defaultPipeline, ObjectSerializer serializer, + String endpoint, OpenAIServiceVersion serviceVersion) { + this.defaultPipeline = defaultPipeline; + this.serializer = serializer; + this.endpoint = endpoint; + this.apiVersion = serviceVersion.getVersion(); + this.serviceVersion = serviceVersion; + } + + public String getEndpoint() { + return endpoint; + } + + public HttpPipeline getPipeline() { + return defaultPipeline; + } + + public OpenAIServiceVersion getServiceVersion() { + return serviceVersion; + } + + private final HttpPipeline pipeline; + + public ExampleServiceImpl(HttpPipeline pipeline) { + this.pipeline = pipeline; + } + + public Response getUser(String userId, Context context) { + return getUser(endpoint, apiVersion, userId, context); + } + + @Override + private Response getUser(String endpoint, String apiVersion, String userId, Context context) { + HttpPipeline pipeline = this.getPipeline(); + String host = endpoint + "/example/users/" + userId + "?api-version=" + apiVersion; + + // create the request + HttpRequest httpRequest = new HttpRequest(HttpMethod.GET, host); + + // set the headers + HttpHeaders headers = new HttpHeaders(); + httpRequest.setHeaders(headers); + + // add RequestOptions to the request + httpRequest.setRequestOptions(requestOptions); + + // set the body content if present + + // send the request through the pipeline + Response response = pipeline.send(httpRequest); + + final int responseCode = response.getStatusCode(); + boolean expectedResponse = responseCode == 200; + if (!expectedResponse) { + throw new RuntimeException("Unexpected response code: " + responseCode); + } + ResponseBodyMode responseBodyMode = ResponseBodyMode.IGNORE; + if (requestOptions != null) { + responseBodyMode = requestOptions.getResponseBodyMode(); + } + if (responseBodyMode == ResponseBodyMode.DESERIALIZE) { + BinaryData responseBody = response.getBody(); + HttpResponseAccessHelper.setValue((HttpResponse) response, responseBody); + } else { + BinaryData responseBody = response.getBody(); + HttpResponseAccessHelper.setBodyDeserializer((HttpResponse) response, (body) -> responseBody); + } + return (Response) response; + } + } + ``` +This implementation eliminates reflection and integrates directly with your HTTP client infrastructure. + diff --git a/sdk/clientcore/tools/sdk-codegen-tool/pom.xml b/sdk/clientcore/tools/sdk-codegen-tool/pom.xml new file mode 100644 index 0000000000000..c4a8ef4812092 --- /dev/null +++ b/sdk/clientcore/tools/sdk-codegen-tool/pom.xml @@ -0,0 +1,112 @@ + + + + 4.0.0 + + io.clientcore.tools + sdk-codegen-tool + 1.0.0-beta.1 + + Clientcore codegen tool + A Java annotation processor tool for generating HTTP service implementations + + + + ossrh + https://oss.sonatype.org/content/repositories/snapshots + + + ossrh + https://oss.sonatype.org/service/local/staging/deploy/maven2/ + + + + https://github.com/azure/azure-sdk-for-java + + Microsoft Corporation + http://microsoft.com + + + + The MIT License (MIT) + http://opensource.org/licenses/MIT + repo + + + + + + microsoft + Microsoft Corporation + + + + + GitHub + https://github.com/Azure/azure-sdk-for-java/issues + + + + https://github.com/Azure/azure-sdk-for-java + scm:git:https://github.com/Azure/azure-sdk-for-java.git + + HEAD + + + + 8 + 8 + ${project.build.directory} + + + + + com.squareup + javapoet + 1.13.0 + + + io.clientcore + core + 1.0.0-beta.1 + compile + + + + + org.junit.jupiter + junit-jupiter-api + 5.11.2 + test + + + org.junit.jupiter + junit-jupiter-engine + 5.11.2 + test + + + org.junit.jupiter + junit-jupiter-params + 5.11.2 + test + + + org.mockito + mockito-core + 4.11.0 + test + + + + + + + src/main/resources + true + + + + diff --git a/sdk/clientcore/tools/sdk-codegen-tool/src/main/java/io/generation/tools/codegen/AnnotationProcessor.java b/sdk/clientcore/tools/sdk-codegen-tool/src/main/java/io/generation/tools/codegen/AnnotationProcessor.java new file mode 100644 index 0000000000000..f36a0bf3031be --- /dev/null +++ b/sdk/clientcore/tools/sdk-codegen-tool/src/main/java/io/generation/tools/codegen/AnnotationProcessor.java @@ -0,0 +1,193 @@ +package io.generation.tools.codegen; + +import io.generation.tools.codegen.models.HttpRequestContext; +import io.generation.tools.codegen.models.Substitution; +import io.generation.tools.codegen.models.TemplateInput; +import io.generation.tools.codegen.templating.TemplateProcessor; +import io.generation.tools.codegen.utils.PathBuilder; +import io.clientcore.core.annotation.ServiceInterface; +import io.clientcore.core.http.annotation.BodyParam; +import io.clientcore.core.http.annotation.HeaderParam; +import io.clientcore.core.http.annotation.HostParam; +import io.clientcore.core.http.annotation.HttpRequestInformation; +import io.clientcore.core.http.annotation.PathParam; +import io.clientcore.core.http.annotation.QueryParam; + +import io.clientcore.core.http.annotation.UnexpectedResponseExceptionDetail; +import javax.annotation.processing.AbstractProcessor; +import javax.annotation.processing.RoundEnvironment; +import javax.annotation.processing.SupportedAnnotationTypes; +import javax.annotation.processing.SupportedSourceVersion; +import javax.lang.model.SourceVersion; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.TypeElement; +import java.util.Set; +import java.util.stream.Collectors; + +@SupportedAnnotationTypes("io.clientcore.core.annotation.*") +@SupportedSourceVersion(SourceVersion.RELEASE_8) +public class AnnotationProcessor extends AbstractProcessor { + + @Override + public boolean process(Set annotations, RoundEnvironment roundEnv) { + // We iterate through each interface annotated with @ServiceInterface separately. + // This outer for-loop is not strictly necessary, as we only have one annotation that we care about + // (@ServiceInterface), but we'll leave it here for now + annotations.stream() + .map(roundEnv::getElementsAnnotatedWith) + .flatMap(Set::stream) + .filter(element -> element.getKind().isInterface()) + .forEach(element -> { + if (element.getAnnotation(ServiceInterface.class) != null) { + this.processServiceInterface(element); + } + }); + + return true; + } + + + private void processServiceInterface(Element serviceInterface) { + TemplateInput templateInput = new TemplateInput(); + + // work out some global details about this service interface + final String serviceInterfaceFQN = serviceInterface.asType().toString(); + + String packageName = null; + int lastDot = serviceInterfaceFQN.lastIndexOf('.'); + if (lastDot > 0) { + packageName = serviceInterfaceFQN.substring(0, lastDot); + } + + final String serviceInterfaceShortName = serviceInterfaceFQN.substring(lastDot + 1); + final String serviceInterfaceImplFQN = serviceInterfaceFQN + "Impl"; + final String serviceInterfaceImplShortName = serviceInterfaceImplFQN.substring(lastDot + 1); + + templateInput.setPackageName(packageName); + templateInput.setServiceInterfaceFQN(serviceInterfaceFQN); + templateInput.setServiceInterfaceShortName(serviceInterfaceShortName); + templateInput.setServiceInterfaceImplShortName(serviceInterfaceImplShortName); + + // Read ServiceInterface.host() value from any enclosed interfaces + if (serviceInterface.getAnnotation(ServiceInterface.class) != null) { + templateInput.setHost(serviceInterface.getAnnotation(ServiceInterface.class).host()); + } + + // add all imports + addImports(templateInput); + + // iterate through all methods in the interface, collecting all that are @HttpRequestInformation + // and then generating a method implementation for each one + templateInput.setHttpRequestContexts(serviceInterface.getEnclosedElements().stream() + .filter(element -> element.getKind() == ElementKind.METHOD) + .filter(element -> element.getAnnotation(HttpRequestInformation.class) != null) + .map(ExecutableElement.class::cast) + .map(e -> createHttpRequestContext(e, templateInput)) + .collect(Collectors.toList())); + + // template input set UnexpectedResponseExceptionDetails + templateInput.setUnexpectedResponseExceptionDetails(serviceInterface.getEnclosedElements().stream() + .filter(element -> element.getKind() == ElementKind.METHOD) + .filter(element -> element.getAnnotation(HttpRequestInformation.class) != null) + .map(ExecutableElement.class::cast) + .map(e -> e.getAnnotation(UnexpectedResponseExceptionDetail.class)) + .collect(Collectors.toList())); + + TemplateProcessor.getInstance().process(templateInput, processingEnv); + + // format the generated code + } + + private void addImports(TemplateInput templateInput) { + templateInput.addImport("io.clientcore.core.util.Context"); + templateInput.addImport("io.clientcore.core.util.binarydata.BinaryData"); + templateInput.addImport("io.clientcore.core.http.models.HttpHeaders"); + templateInput.addImport("io.clientcore.core.http.pipeline.HttpPipeline"); + templateInput.addImport("io.clientcore.core.http.models.HttpHeaderName"); + templateInput.addImport("io.clientcore.core.http.models.HttpMethod"); + templateInput.addImport("io.clientcore.core.http.models.HttpResponse"); + templateInput.addImport("io.clientcore.core.http.models.HttpRequest"); + templateInput.addImport("io.clientcore.core.http.models.Response"); + templateInput.addImport("java.util.Map"); + templateInput.addImport("java.util.HashMap"); + templateInput.addImport("java.util.Arrays"); + templateInput.addImport("java.lang.Void"); + templateInput.addImport("java.util.List"); + } + + private HttpRequestContext createHttpRequestContext(ExecutableElement requestMethod, TemplateInput templateInput) { + HttpRequestContext method = new HttpRequestContext(); + method.setHost(templateInput.getHost()); + method.setMethodName(requestMethod.getSimpleName().toString()); + + final HttpRequestInformation httpRequestInfo = requestMethod.getAnnotation(HttpRequestInformation.class); + method.setPath(httpRequestInfo.path()); + method.setHttpMethod(httpRequestInfo.method()); + method.setExpectedStatusCodes(httpRequestInfo.expectedStatusCodes()); + + templateInput.addImport(requestMethod.getReturnType()); + method.setMethodReturnType(requestMethod.getReturnType().toString()); + requestMethod.getParameters().forEach(param -> { + HostParam hostParam = param.getAnnotation(HostParam.class); + PathParam pathParam = param.getAnnotation(PathParam.class); + HeaderParam headerParam = param.getAnnotation(HeaderParam.class); + QueryParam queryParam = param.getAnnotation(QueryParam.class); + BodyParam bodyParam = param.getAnnotation(BodyParam.class); + + // check if the parameter has an annotation, and if so, add it to the appropriate list + if (hostParam != null) { + // a HostParam is a substitution into the global @Host value. Depending on the @Host value, + // there may be zero or more {} delimited substitutions, so we need to handle this + // appropriately + method.addSubstitution(new Substitution( + hostParam.value(), + param.getSimpleName().toString(), + hostParam.encoded())); + } else if (pathParam != null) { + // a PathParam is a substitution into the path value. Depending on the path value, + // there may be zero or more {} delimited substitutions, so we need to handle this + // appropriately + method.addSubstitution(new Substitution( + pathParam.value(), + param.getSimpleName().toString(), + pathParam.encoded())); + } else if (headerParam != null) { + method.addHeader(headerParam.value(), param.getSimpleName().toString()); + } else if (queryParam != null) { + // we do not support query param substitutions, so we just add the query param name and value + method.addQueryParam(queryParam.value(), param.getSimpleName().toString()); + } else if (false) { + // TODO support FormParam + } else if (bodyParam != null) { + // This is the content type as specified in the @BodyParam annotation + String contentType = bodyParam.value(); + + // This is the type of the parameter that has been annotated with @BodyParam. + // This is used to determine which setBody method to call on HttpRequest. + String parameterType = param.asType().toString(); + + // This is the parameter name, so we can refer to it when setting the body on the HttpRequest. + String parameterName = param.getSimpleName().toString(); + + method.setBody(new HttpRequestContext.Body(contentType, parameterType, parameterName)); + } + + String shortImportName = templateInput.addImport(param.asType()); + method.addParameter(new HttpRequestContext.MethodParameter(param.asType(), shortImportName, param.getSimpleName().toString())); + }); + + // we can reduce the amount of regex matching on the host by doing as many substitutions as possible here + String rawHost = getHost(templateInput, method); + method.setHost(rawHost); + + return method; + } + + private static String getHost(TemplateInput templateInput, HttpRequestContext method) { + String rawHost = templateInput.getHost() + method.getPath(); + + return PathBuilder.buildPath(rawHost, method); + } +} diff --git a/sdk/clientcore/tools/sdk-codegen-tool/src/main/java/io/generation/tools/codegen/exceptions/MissingSubstitutionException.java b/sdk/clientcore/tools/sdk-codegen-tool/src/main/java/io/generation/tools/codegen/exceptions/MissingSubstitutionException.java new file mode 100644 index 0000000000000..a836d33f0814e --- /dev/null +++ b/sdk/clientcore/tools/sdk-codegen-tool/src/main/java/io/generation/tools/codegen/exceptions/MissingSubstitutionException.java @@ -0,0 +1,15 @@ +package io.generation.tools.codegen.exceptions; + +/** + * Exception thrown when a substitution is missing from the template. + */ +public class MissingSubstitutionException extends RuntimeException { + + /** + * Creates a new instance of the exception. + * @param message The exception message. + */ + public MissingSubstitutionException(String message) { + super(message); + } +} diff --git a/sdk/clientcore/tools/sdk-codegen-tool/src/main/java/io/generation/tools/codegen/models/HttpRequestContext.java b/sdk/clientcore/tools/sdk-codegen-tool/src/main/java/io/generation/tools/codegen/models/HttpRequestContext.java new file mode 100644 index 0000000000000..9fd8e29b2bb95 --- /dev/null +++ b/sdk/clientcore/tools/sdk-codegen-tool/src/main/java/io/generation/tools/codegen/models/HttpRequestContext.java @@ -0,0 +1,367 @@ +package io.generation.tools.codegen.models; + +import io.clientcore.core.http.models.HttpMethod; + +import javax.lang.model.type.TypeMirror; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * Represents the context of an HTTP request, including its configuration, parameters, headers, and other details. + */ +public final class HttpRequestContext { + + // Request Configuration + private String methodName; + private String methodReturnType; + private final List parameters; + private HttpMethod httpMethod; + + // This comes from the @Host annotation that is applied to the entire service interface, it will likely have one + // or more substitutions in it, which will be replaced with the appropriate parameter values annotated with @HostParam. + private String host; + + // This comes from the @HttpRequestInformation.path annotation that is applied to each method in the service interface. + // It will likely have one or more substitutions in it, which will be replaced with the appropriate parameter values + // annotated with @PathParam. + private String path; + + private final Map headers; + private final Map queryParams; + + private final Map substitutions; + + private int[] expectedStatusCodes; + + /** + * Constructs a new HttpRequestContext with default values. + */ + public HttpRequestContext() { + this.parameters = new ArrayList<>(); + this.headers = new HashMap<>(); + this.queryParams = new HashMap<>(); + this.substitutions = new HashMap<>(); + } + + private Body body; + + /** + * Gets the method name. + * + * @return the method name. + */ + public String getMethodName() { + return methodName; + } + + /** + * Sets the method name. + * + * @param methodName the method name to set. + */ + public void setMethodName(String methodName) { + this.methodName = methodName; + } + + /** + * Gets the method return type. + * + * @return the method return type. + */ + public String getMethodReturnType() { + return methodReturnType; + } + + /** + * Sets the method return type. + * + * @param methodReturnType the method return type to set. + */ + public void setMethodReturnType(String methodReturnType) { + this.methodReturnType = methodReturnType; + } + + /** + * Adds a parameter to the method. + * + * @param parameter the parameter to add. + */ + public void addParameter(MethodParameter parameter) { + this.parameters.add(parameter); + } + + /** + * Gets the list of parameters. + * + * @return the list of parameters. + */ + public List getParameters() { + return parameters; + } + + /** + * Gets the host. + * + * @return the host. + */ + public String getHost() { + return host; + } + + /** + * Sets the host. + * + * @param host the host to set. + */ + public void setHost(String host) { + this.host = host; + } + + /** + * Gets the path. + * + * @return the path. + */ + public String getPath() { + return path; + } + + /** + * Sets the path. + * + * @param path the path to set. + */ + public void setPath(String path) { + this.path = path; + } + + /** + * Gets the HTTP method. + * + * @return the HTTP method. + */ + public HttpMethod getHttpMethod() { + return httpMethod; + } + + /** + * Sets the HTTP method. + * + * @param httpMethod the HTTP method to set. + */ + public void setHttpMethod(HttpMethod httpMethod) { + this.httpMethod = httpMethod; + } + + /** + * Gets the headers. + * + * @return the headers. + */ + public Map getHeaders() { + return headers; + } + + /** + * Adds a header. + * + * @param key the header key. + * @param value the header value. + */ + public void addHeader(String key, String value) { + headers.put(key, value); + } + + /** + * Gets the query parameters. + * + * @return the query parameters. + */ + public Map getQueryParams() { + return queryParams; + } + + /** + * Adds a query parameter. + * + * @param key the query parameter key. + * @param value the query parameter value. + * @throws IllegalArgumentException if a duplicate query parameter is added. + */ + public void addQueryParam(String key, String value) { + if (queryParams.containsKey(key)) { + throw new IllegalArgumentException("Cannot add duplicate query parameter '" + key + "'"); + } + queryParams.put(key, value); + } + + /** + * Adds a substitution. + * + * @param substitution the substitution to add. + * @throws IllegalArgumentException if a duplicate substitution is added. + */ + public void addSubstitution(Substitution substitution) { + if (substitutions.containsKey(substitution.getParameterName())) { + throw new IllegalArgumentException("Cannot add duplicate substitution for parameter '" + substitution.getParameterName() + "'"); + } + substitutions.put(substitution.getParameterName(), substitution); + } + + /** + * Gets a substitution by parameter name. + * + * @param parameterName the parameter name. + * @return the substitution. + */ + public Substitution getSubstitution(String parameterName) { + return substitutions.get(parameterName); + } + + /** + * Sets the body. + * + * @param body the body to set. + */ + public void setBody(Body body) { + this.body = body; + } + + /** + * Gets the body. + * + * @return the body. + */ + public Body getBody() { + return body; + } + + /** + * Sets the expected status codes. + * + * @param expectedStatusCodes the expected status codes to set. + */ + public void setExpectedStatusCodes(int[] expectedStatusCodes) { + if (expectedStatusCodes != null) { + Arrays.sort(expectedStatusCodes); + } + this.expectedStatusCodes = expectedStatusCodes; + } + + /** + * Gets the expected status codes. + * + * @return the expected status codes. + */ + public List getExpectedStatusCodes() { + return Arrays.stream(expectedStatusCodes).boxed().collect(Collectors.toList()); + } + + /** + * Represents a method parameter. + */ + public static class MethodParameter { + private final TypeMirror type; + private final String shortTypeName; + private final String name; + + /** + * Constructs a new MethodParameter. + * + * @param type the type of the parameter. + * @param shortTypeName the short type name of the parameter. + * @param name the name of the parameter. + */ + public MethodParameter(TypeMirror type, String shortTypeName, String name) { + this.type = type; + this.shortTypeName = shortTypeName; + this.name = name; + } + + /** + * Gets the type mirror. + * + * @return the type mirror. + */ + public TypeMirror getTypeMirror() { + return type; + } + + /** + * Gets the short type name. + * + * @return the short type name. + */ + public String getShortTypeName() { + return shortTypeName; + } + + /** + * Gets the name. + * + * @return the name. + */ + public String getName() { + return name; + } + } + + /** + * Represents the body of an HTTP request. + */ + public static class Body { + // This is the content type as specified in the @BodyParam annotation + private final String contentType; + + // This is the type of the parameter that has been annotated with @BodyParam. + // This is used to determine which setBody method to call on HttpRequest. + private final String parameterType; + + // This is the parameter name, so we can refer to it when setting the body on the HttpRequest. + private final String parameterName; + + /** + * Constructs a new Body. + * + * @param contentType the content type. + * @param parameterType the parameter type. + * @param parameterName the parameter name. + */ + public Body(String contentType, String parameterType, String parameterName) { + this.contentType = contentType; + this.parameterType = parameterType; + this.parameterName = parameterName; + } + + /** + * Gets the content type. + * + * @return the content type. + */ + public String getContentType() { + return contentType; + } + + /** + * Gets the parameter type. + * + * @return the parameter type. + */ + public String getParameterType() { + return parameterType; + } + + /** + * Gets the parameter name. + * + * @return the parameter name. + */ + public String getParameterName() { + return parameterName; + } + } +} diff --git a/sdk/clientcore/tools/sdk-codegen-tool/src/main/java/io/generation/tools/codegen/models/Substitution.java b/sdk/clientcore/tools/sdk-codegen-tool/src/main/java/io/generation/tools/codegen/models/Substitution.java new file mode 100644 index 0000000000000..7515e98cc5680 --- /dev/null +++ b/sdk/clientcore/tools/sdk-codegen-tool/src/main/java/io/generation/tools/codegen/models/Substitution.java @@ -0,0 +1,59 @@ +package io.generation.tools.codegen.models; + +/** + * A Substitution is a value that can be used to replace placeholder values in a URL. Placeholders look like: + * "http://{host}.com/{fileName}.html", where "{host}" and "{fileName}" are the placeholders. + */ +public class Substitution { + private final String parameterName; + private final String parameterVariableName; + private final boolean shouldEncode; + + /** + * Create a new Substitution. + * + * @param parameterName The name that is used between curly quotes as a placeholder in the target URL. + * @param parameterVariableName The name of the variable that will be used to replace the placeholder in the target + */ + public Substitution(String parameterName, String parameterVariableName) { + this(parameterName, parameterVariableName, false); + } + + /** + * Create a new Substitution. + * + * @param parameterName The name that is used between curly quotes as a placeholder in the target URL. + * @param parameterVariableName The name of the variable that will be used to replace the placeholder in the target + * @param shouldEncode Whether the value from the method's argument should be encoded when the substitution is + * taking place. + */ + public Substitution(String parameterName, String parameterVariableName, boolean shouldEncode) { + this.parameterName = parameterName; + this.parameterVariableName = parameterVariableName; + this.shouldEncode = shouldEncode; + } + + /** + * Get the placeholder's name. + * + * @return The name of the placeholder. + */ + public String getParameterName() { + return parameterName; + } + + public String getParameterVariableName() { + return parameterVariableName; + } + + /** + * Whether the replacement value from the method argument needs to be encoded when the substitution is taking + * place. + * + * @return Whether the replacement value from the method argument needs to be encoded when the substitution is + * taking place. + */ + public boolean shouldEncode() { + return shouldEncode; + } +} diff --git a/sdk/clientcore/tools/sdk-codegen-tool/src/main/java/io/generation/tools/codegen/models/TemplateInput.java b/sdk/clientcore/tools/sdk-codegen-tool/src/main/java/io/generation/tools/codegen/models/TemplateInput.java new file mode 100644 index 0000000000000..6cb2ae4673645 --- /dev/null +++ b/sdk/clientcore/tools/sdk-codegen-tool/src/main/java/io/generation/tools/codegen/models/TemplateInput.java @@ -0,0 +1,222 @@ +package io.generation.tools.codegen.models; + +import io.clientcore.core.http.annotation.UnexpectedResponseExceptionDetail; +import javax.lang.model.element.TypeElement; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +/** + * Represents the input required for generating a template. + */ +public class TemplateInput { + // A map of fully-qualified class names to their short names + private final Map imports = new TreeMap<>(); + + private String packageName; + private String serviceInterfaceFQN; + private String serviceInterfaceShortName; + private String serviceInterfaceImplShortName; + private String host; + private List httpRequestContexts; + private List unexpectedResponseExceptionDetails; + + /** + * Gets the host. + * + * @return the host. + */ + public String getHost() { + return host; + } + + /** + * Sets the host. + * + * @param host the host to set. + */ + public void setHost(String host) { + this.host = host; + } + + /** + * Gets the imports map. + * + * @return the imports map. + */ + public Map getImports() { + return imports; + } + + /** + * Gets the package name. + * + * @return the package name. + */ + public String getPackageName() { + return packageName; + } + + /** + * Sets the package name. + * + * @param packageName the package name to set. + */ + public void setPackageName(String packageName) { + this.packageName = packageName; + } + + /** + * Gets the short name of the service interface. + * + * @return the short name of the service interface. + */ + public String getServiceInterfaceShortName() { + return serviceInterfaceShortName; + } + + /** + * Sets the short name of the service interface. + * + * @param serviceInterfaceShortName the short name of the service interface to set. + */ + public void setServiceInterfaceShortName(String serviceInterfaceShortName) { + this.serviceInterfaceShortName = serviceInterfaceShortName; + } + + /** + * Gets the short name of the service interface implementation. + * + * @return the short name of the service interface implementation. + */ + public String getServiceInterfaceImplShortName() { + return serviceInterfaceImplShortName; + } + + /** + * Sets the short name of the service interface implementation. + * + * @param serviceInterfaceImplShortName the short name of the service interface implementation to set. + */ + public void setServiceInterfaceImplShortName(String serviceInterfaceImplShortName) { + this.serviceInterfaceImplShortName = serviceInterfaceImplShortName; + } + + /** + * Converts a fully-qualified class name to its short name. + * + * @param fqcn the fully-qualified class name. + * @return the short name of the class. + */ + private static String toShortName(String fqcn) { + int lastDot = fqcn.lastIndexOf('.'); + if (lastDot > 0) { + return fqcn.substring(lastDot + 1); + } + return fqcn; + } + + /** + * Adds an import to the imports map. + * + * @param importFQN the fully-qualified name of the import. + * @return the short name of the class. + */ + public String addImport(String importFQN) { + if (importFQN != null && !importFQN.isEmpty()) { + String shortName = toShortName(importFQN); + imports.put(importFQN, shortName); + return shortName; + } + return null; + } + + /** + * Adds an import to the imports map based on the type mirror. + * + * @param type the type mirror. + * @return the short name of the class. + */ + public String addImport(TypeMirror type) { + String longName = type.toString(); + String shortName = null; + + if (type.getKind().isPrimitive()) { + shortName = toShortName(longName); + imports.put(longName, shortName); + } else if (imports.containsKey(type.toString())) { + shortName = imports.get(longName); + } else if (type.getKind() == TypeKind.DECLARED) { + // Check if this type is a generic type, and if it is, recursively check the type arguments + TypeElement typeElement = (TypeElement) ((DeclaredType) type).asElement(); + List typeArguments = ((DeclaredType) type).getTypeArguments(); + if (typeArguments != null && !typeArguments.isEmpty()) { + longName = typeElement.getQualifiedName().toString(); + shortName = toShortName(typeElement.getQualifiedName().toString()); + imports.put(longName, shortName); + } else { + shortName = toShortName(longName); + imports.put(longName, shortName); + } + } + + return shortName; + } + + /** + * Sets the HTTP request contexts. + * + * @param httpRequestContexts the list of HTTP request contexts to set. + */ + public void setHttpRequestContexts(List httpRequestContexts) { + this.httpRequestContexts = httpRequestContexts; + } + + /** + * Gets the list of HTTP request contexts. + * + * @return the list of HTTP request contexts. + */ + public List getHttpRequestContexts() { + return httpRequestContexts; + } + + /** + * Sets the fully-qualified name of the service interface. + * + * @param serviceInterfaceFQN the fully-qualified name of the service interface to set. + */ + public void setServiceInterfaceFQN(String serviceInterfaceFQN) { + this.serviceInterfaceFQN = serviceInterfaceFQN; + } + + /** + * Gets the fully-qualified name of the service interface. + * + * @return the fully-qualified name of the service interface. + */ + public String getServiceInterfaceFQN() { + return serviceInterfaceFQN; + } + + /** + * Gets the list of unexpected response exception details. + * + * @return the list of unexpected response exception details. + */ + public List getUnexpectedResponseExceptionDetails() { + return unexpectedResponseExceptionDetails; + } + + /** + * Sets the list of unexpected response exception details. + * + * @param unexpectedResponseExceptionDetails the list of unexpected response exception details to set. + */ + public void setUnexpectedResponseExceptionDetails(List unexpectedResponseExceptionDetails) { + this.unexpectedResponseExceptionDetails = unexpectedResponseExceptionDetails; + } +} diff --git a/sdk/clientcore/tools/sdk-codegen-tool/src/main/java/io/generation/tools/codegen/templating/JavaPoetTemplateProcessor.java b/sdk/clientcore/tools/sdk-codegen-tool/src/main/java/io/generation/tools/codegen/templating/JavaPoetTemplateProcessor.java new file mode 100644 index 0000000000000..c2a360245d38e --- /dev/null +++ b/sdk/clientcore/tools/sdk-codegen-tool/src/main/java/io/generation/tools/codegen/templating/JavaPoetTemplateProcessor.java @@ -0,0 +1,508 @@ +package io.generation.tools.codegen.templating; + +import io.generation.tools.codegen.models.HttpRequestContext; +import io.generation.tools.codegen.models.TemplateInput; +import io.generation.tools.codegen.utils.ResponseBodyModeGeneration; +import com.squareup.javapoet.ClassName; +import com.squareup.javapoet.FieldSpec; +import com.squareup.javapoet.JavaFile; +import com.squareup.javapoet.MethodSpec; +import com.squareup.javapoet.ParameterizedTypeName; +import com.squareup.javapoet.TypeName; +import com.squareup.javapoet.TypeSpec; +import io.clientcore.core.http.models.ContentType; +import io.clientcore.core.http.models.HttpHeaderName; +import io.clientcore.core.http.models.HttpMethod; +import io.clientcore.core.util.binarydata.BinaryData; +import io.clientcore.core.util.serializer.ObjectSerializer; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.util.Map; +import java.util.stream.Collectors; +import javax.annotation.processing.ProcessingEnvironment; +import javax.lang.model.element.Modifier; + +public class JavaPoetTemplateProcessor implements TemplateProcessor { + private static final ClassName HTTP_HEADER_NAME = ClassName.get("io.clientcore.core.http.models", "HttpHeaderName"); + private static final ClassName CONTENT_TYPE = ClassName.get("io.clientcore.core.http.models", "ContentType"); + + private final ClassName HTTP_REQUEST = ClassName.get("io.clientcore.core.http.models", "HttpRequest"); + private final ClassName RESPONSE = ClassName.get("io.clientcore.core.http" + + ".models", "Response"); + private final ClassName HTTP_METHOD = ClassName.get("io.clientcore.core.http.models", "HttpMethod"); + + private TypeSpec.Builder classBuilder; + final ClassName HTTP_PIPELINE = ClassName.get("io.clientcore.core.http.pipeline", "HttpPipeline"); + static ClassName SERVICE_VERSION_TYPE; + final ClassName CLIENTLOGGER_NAME = ClassName.get("io.clientcore.core.util", "ClientLogger"); + + @Override + public void process(TemplateInput templateInput, ProcessingEnvironment processingEnv) { + String packageName = templateInput.getPackageName(); + String serviceInterfaceImplShortName = templateInput.getServiceInterfaceImplShortName(); + String serviceInterfaceShortName = templateInput.getServiceInterfaceShortName(); + + ClassName interfaceType = ClassName.get(packageName, serviceInterfaceShortName); + + // add LoggerField + FieldSpec loggerField = getLoggerField(packageName, serviceInterfaceShortName); + + // Create the defaultPipeline field + FieldSpec defaultPipeline = + FieldSpec.builder(HTTP_PIPELINE, "defaultPipeline", Modifier.PRIVATE, Modifier.FINAL) + .build(); + + // Create the serializer field + FieldSpec serializer = FieldSpec.builder(ObjectSerializer.class, "serializer", Modifier.PRIVATE, Modifier.FINAL) + .build(); + + // Create the endpoint field + FieldSpec endpoint = FieldSpec.builder(String.class, "endpoint", Modifier.PRIVATE, Modifier.FINAL) + .build(); + + // Create the serviceVersion field + ClassName serviceVersionType = getServiceVersionType(packageName, serviceInterfaceShortName); + FieldSpec serviceVersion = + FieldSpec.builder(serviceVersionType, "serviceVersion", Modifier.PRIVATE, Modifier.FINAL) + .build(); + + // Create the constructor + MethodSpec constructor = getServiceImplConstructor(packageName, serviceInterfaceShortName); + + FieldSpec apiVersion = FieldSpec.builder(String.class, "apiVersion") + .addModifiers(Modifier.PRIVATE) + .build(); + + classBuilder = TypeSpec.classBuilder(serviceInterfaceImplShortName) + .addModifiers(Modifier.PUBLIC) + .addSuperinterface(interfaceType) + .addField(loggerField) + .addField(defaultPipeline) + .addField(serializer) + .addField(endpoint) + .addField(serviceVersion) + .addField(apiVersion) + .addMethod(getEndpointMethod()) + .addMethod(getPipelineMethod()) + .addMethod(getServiceVersionMethod()) + .addMethod(constructor); + + getGeneratedServiceMethods(templateInput); + + TypeSpec typeSpec = classBuilder.build(); + + // Sets the indentation for the generated source file to four spaces. + JavaFile javaFile = JavaFile.builder(packageName, typeSpec) + .indent(" ") // four spaces + .build(); + + try { + javaFile.writeTo(processingEnv.getFiler()); + } catch (IOException e) { + e.printStackTrace(); + } + } + + void getGeneratedServiceMethods(TemplateInput templateInput) { + for (HttpRequestContext method : templateInput.getHttpRequestContexts()) { + classBuilder.addMethod(generatePublicMethod(method)); + generateInternalMethod(method); + } + } + + FieldSpec getLoggerField(String packageName, String serviceInterfaceShortName) { + return FieldSpec.builder(CLIENTLOGGER_NAME, "LOGGER", Modifier.PRIVATE, Modifier.STATIC, Modifier.FINAL) + .initializer("new $T($T.class)", CLIENTLOGGER_NAME, ClassName.get(packageName, serviceInterfaceShortName)) + .build(); + } + + MethodSpec getServiceImplConstructor(String packageName, String serviceInterfaceShortName) { + return MethodSpec.constructorBuilder() + .addModifiers(Modifier.PUBLIC) + .addParameter(HTTP_PIPELINE, "defaultPipeline") + .addStatement("this.defaultPipeline = defaultPipeline") + .addParameter(ClassName.get("io.clientcore.core.util.serializer", "ObjectSerializer"), "serializer") + .addStatement("this.serializer = serializer") + .addParameter(String.class, "endpoint") + .addStatement("this.endpoint = endpoint") + .addParameter(getServiceVersionType(packageName, serviceInterfaceShortName), + "serviceVersion") + .addStatement("this.apiVersion = serviceVersion.getVersion()") + .addStatement("this.serviceVersion = serviceVersion") + .build(); + } + + static ClassName getServiceVersionType(String packageName, String serviceInterfaceShortName) { + SERVICE_VERSION_TYPE = ClassName.get(packageName, serviceInterfaceShortName.substring(0, + serviceInterfaceShortName.indexOf("ClientService")) + "ServiceVersion"); + return SERVICE_VERSION_TYPE; + } + + MethodSpec getEndpointMethod() { + return MethodSpec.methodBuilder("getEndpoint") + .addModifiers(Modifier.PUBLIC) + .returns(String.class) + .addStatement("return endpoint") + .build(); + } + + MethodSpec getPipelineMethod() { + return MethodSpec.methodBuilder("getPipeline") + .addModifiers(Modifier.PUBLIC) + .returns(HTTP_PIPELINE) + .addStatement("return defaultPipeline") + .build(); + } + + MethodSpec getServiceVersionMethod() { + return MethodSpec.methodBuilder("getServiceVersion") + .addModifiers(Modifier.PUBLIC) + .returns(SERVICE_VERSION_TYPE) + .addStatement("return serviceVersion") + .build(); + } + + MethodSpec generatePublicMethod(HttpRequestContext method) { + + MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder(method.getMethodName()) + .addModifiers(Modifier.PUBLIC) + .returns(inferTypeNameFromReturnType(method.getMethodReturnType())); + + // add method parameters, with Context at the end + for (HttpRequestContext.MethodParameter parameter : method.getParameters()) { + if (parameter.getName().equals("endpoint") || parameter.getName().equals("apiVersion")) { + continue; + } + methodBuilder.addParameter(TypeName.get(parameter.getTypeMirror()), parameter.getName()); + } + + // add call to the overloaded version of this method + String params = method.getParameters().stream() + .map(HttpRequestContext.MethodParameter::getName) + .reduce((a, b) -> a + ", " + b) + .orElse(""); + + if (!"void".equals(method.getMethodReturnType())) { + methodBuilder.addStatement("return $L($L)", + method.getMethodName(), params); + } else { + methodBuilder.addStatement("$L($L)", + method.getMethodName(), params); + } + + return methodBuilder.build(); + } + + private void generateInternalMethod(HttpRequestContext method) { + TypeName returnTypeName = inferTypeNameFromReturnType(method.getMethodReturnType()); + MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder(method.getMethodName()) + .addModifiers(Modifier.PRIVATE) + .addAnnotation(Override.class) + .returns(returnTypeName); + + // add method parameters, as well as the HttpPipeline at the front + for (HttpRequestContext.MethodParameter parameter : method.getParameters()) { + methodBuilder.addParameter(TypeName.get(parameter.getTypeMirror()), parameter.getName()); + } + + // add field pipeline + methodBuilder.addStatement("HttpPipeline pipeline = this.getPipeline()"); + + methodBuilder + .addStatement("String host = $L", method.getHost()) + .addCode("\n") + .addComment("create the request") + .addStatement("$T httpRequest = new $T($T.$L, host)", HTTP_REQUEST, HTTP_REQUEST, HTTP_METHOD, + method.getHttpMethod()); + + // add headers + if (!method.getHeaders().isEmpty()) { + methodBuilder + .addCode("\n") + .addComment("set the headers") + .addStatement("$T headers = new $T()", ClassName.get("io.clientcore.core.http.models", "HttpHeaders"), + ClassName.get("io.clientcore.core.http.models", "HttpHeaders")); + for (Map.Entry header : method.getHeaders().entrySet()) { + String enumHeaderKey = header.getKey().toUpperCase().replace("-", "_"); + boolean isEnumExists = false; + for (HttpHeaderName httpHeaderName : HttpHeaderName.values()) { + if (httpHeaderName.getCaseInsensitiveName().equals(header.getKey().toLowerCase())) { + isEnumExists = true; + break; + } + } + if (isEnumExists) { + methodBuilder.addStatement("headers.add($T.$L, $L)", + HTTP_HEADER_NAME, enumHeaderKey, header.getValue()); + } else { + methodBuilder.addStatement("headers.add($T.fromString($S), $L)", + HTTP_HEADER_NAME, header.getKey(), header.getValue()); + } + } + + methodBuilder.addStatement("httpRequest.setHeaders(headers)"); + } + + methodBuilder + .addCode("\n") + .addComment("add RequestOptions to the request") + .addStatement("httpRequest.setRequestOptions(requestOptions)"); + + // [TODO] set SSE listener if available + + // set the body + methodBuilder + .addCode("\n") + .addComment("set the body content if present"); + if (method.getBody() != null) { + HttpRequestContext.Body body = method.getBody(); + String contentType = body.getContentType(); + String parameterType = body.getParameterType(); + String parameterName = body.getParameterName(); + + configureRequestWithBodyAndContentType(methodBuilder, parameterType, contentType, parameterName); + } else { + methodBuilder + .addStatement("httpRequest.getHeaders().set($T.CONTENT_LENGTH, $S)", HttpHeaderName.class, "0"); + methodBuilder.addComment("no body content to set"); + } + + // send request through pipeline + methodBuilder + .addCode("\n") + .addComment("send the request through the pipeline") + .addStatement("$T response = pipeline.send(httpRequest)", RESPONSE); + + // check for expected status codes + if (!method.getExpectedStatusCodes().isEmpty()) { + methodBuilder + .addCode("\n") + .addStatement("final int responseCode = response.getStatusCode()"); + if (method.getExpectedStatusCodes().size() == 1) { + methodBuilder.addStatement("boolean expectedResponse = responseCode == $L", + method.getExpectedStatusCodes().get(0)); + } else { + String statusCodes = method.getExpectedStatusCodes().stream() + .map(code -> "responseCode == " + code) + .collect(Collectors.joining(" || ")); + methodBuilder.addStatement("boolean expectedResponse = " + statusCodes); + } + methodBuilder.beginControlFlow("if (!expectedResponse)") + .addStatement("throw new $T(\"Unexpected response code: \" + responseCode)", RuntimeException.class) + .endControlFlow(); + } + + // add return statement if method return type is not "void" + if (returnTypeName.toString().contains("void") && returnTypeName.toString().contains("Void")) { + methodBuilder.addStatement("return"); + } else if (returnTypeName.toString().contains("Response")) { + if (returnTypeName.toString().contains("Void")) { + methodBuilder.beginControlFlow("try") + .addStatement("response.close()") + .nextControlFlow("catch ($T e)", IOException.class) + .addStatement("throw LOGGER.logThrowableAsError(new $T(e))", UncheckedIOException.class) + .endControlFlow(); + createResponseIfNecessary(returnTypeName, methodBuilder); + } else { + // Step 1: Generate ResponseBodyMode assignment + ResponseBodyModeGeneration.generateResponseBodyModeAssignment(methodBuilder); + + // Step 2: Generate DESERIALIZE handling + ResponseBodyModeGeneration.generateDeserializeResponseHandling(methodBuilder); + + // Step 3: Generate non-DESERIALIZE handling + ResponseBodyModeGeneration.generateNonDeserializeResponseHandling(methodBuilder); + + // Step 4: Create the response if necessary + createResponseIfNecessary(returnTypeName, methodBuilder); + } + } else { + handleResponseModeToCreateResponse(method, returnTypeName, methodBuilder); + } + + classBuilder.addMethod(methodBuilder.build()); + } + + private static void createResponseIfNecessary(TypeName returnTypeName, MethodSpec.Builder methodBuilder) { + // TODO: Fix me + methodBuilder.addStatement("return ($T) response", returnTypeName); + } + + private static void handleResponseModeToCreateResponse(HttpRequestContext method, TypeName returnTypeName, + MethodSpec.Builder methodBuilder) { + HttpMethod httpMethod = method.getHttpMethod(); + if (httpMethod == HttpMethod.HEAD && + (returnTypeName.toString().contains("Boolean") || returnTypeName.toString().contains("boolean"))) { + methodBuilder.addStatement("return (responseCode / 100) == 2"); + } else if (returnTypeName.toString().contains("byte[]")) { + methodBuilder + .addStatement("$T responseBody = response.getBody()", BinaryData.class) + .addStatement("byte[] responseBodyBytes = responseBody != null ? responseBody.toBytes() : null") + .addStatement( + "return responseBodyBytes != null ? (responseBodyBytes.length == 0 ? null : responseBodyBytes) : null"); + } else if (returnTypeName.toString().contains("InputStream")) { + methodBuilder + .addStatement("$T responseBody = response.getBody()", BinaryData.class) + .addStatement("return responseBody.toStream()"); + } else if (returnTypeName.toString().contains("BinaryData")) { + methodBuilder + .addStatement("$T responseBody = response.getBody()", BinaryData.class); + } else { + methodBuilder + .addStatement("$T responseBody = response.getBody()", BinaryData.class) + .addStatement("return decodeByteArray(responseBody.toBytes(), response, serializer, methodParser)"); + } + } + + public void configureRequestWithBodyAndContentType(MethodSpec.Builder methodBuilder, String parameterType, + String contentType, String parameterName) { + if (parameterType == null) { + // No body content to set + methodBuilder + .addStatement("httpRequest.getHeaders().set($T.CONTENT_LENGTH, $S))", HttpHeaderName.class, 0); + } else { + + if (contentType == null || contentType.isEmpty()) { + if (parameterType.equals("byte[]") || parameterType.equals("String")) { + + contentType = ContentType.APPLICATION_OCTET_STREAM; + } else { + + contentType = ContentType.APPLICATION_JSON; + } + } + setContentTypeHeader(methodBuilder, contentType); + if (parameterType.equals("io.clientcore.core.util.binarydata.BinaryData")) { + methodBuilder + .addStatement("$T binaryData = ($T) $L", BinaryData.class, BinaryData.class, parameterName) + .beginControlFlow("if (binaryData.getLength() != null)") + .addStatement( + "httpRequest.getHeaders().set($T.CONTENT_LENGTH, String.valueOf(binaryData.getLength()))", + HttpHeaderName.class) + .addStatement("httpRequest.setBody(binaryData)") + .endControlFlow(); + return; + } + + boolean isJson = false; + final String[] contentTypeParts = contentType.split(";"); + + for (final String contentTypePart : contentTypeParts) { + if (contentTypePart.trim().equalsIgnoreCase(ContentType.APPLICATION_JSON)) { + isJson = true; + + break; + } + } + updateRequestWithBodyContent(methodBuilder, isJson, parameterType, parameterName); + } + } + + private static void setContentTypeHeader(MethodSpec.Builder methodBuilder, String contentType) { + switch (contentType) { + case ContentType.APPLICATION_JSON: + methodBuilder.addStatement("httpRequest.getHeaders().set($T.$L, $T.$L)", + ClassName.get("io.clientcore.core.http.models", "HttpHeaderName"), + "CONTENT_TYPE", + CONTENT_TYPE, + "APPLICATION_JSON"); + break; + case ContentType.APPLICATION_OCTET_STREAM: + methodBuilder.addStatement("httpRequest.getHeaders().set($T.$L, $T.$L)", + ClassName.get("io.clientcore.core.http.models", "HttpHeaderName"), + "CONTENT_TYPE", + CONTENT_TYPE, + "APPLICATION_OCTET_STREAM"); + break; + case ContentType.APPLICATION_X_WWW_FORM_URLENCODED: + methodBuilder.addStatement("httpRequest.getHeaders().set($T.$L, $T.$L)", + ClassName.get("io.clientcore.core.http.models", "HttpHeaderName"), + "CONTENT_TYPE", + CONTENT_TYPE, + "APPLICATION_X_WWW_FORM_URLENCODED"); + break; + case ContentType.TEXT_EVENT_STREAM: + methodBuilder.addStatement("httpRequest.getHeaders().set($T.$L, $T.$L)", + ClassName.get("io.clientcore.core.http.models", "HttpHeaderName"), + "CONTENT_TYPE", + CONTENT_TYPE, + "TEXT_EVENT_STREAM"); + break; + } + } + + private void updateRequestWithBodyContent(MethodSpec.Builder methodBuilder, boolean isJson, String parameterType, + String parameterName) { + if (parameterType == null) { + return; + } + if (isJson) { + methodBuilder + .addStatement("httpRequest.setBody($T.fromObject($L, serializer))", BinaryData.class, parameterName); + } else if (parameterType.equals("byte[]")) { + methodBuilder + .addStatement("httpRequest.setBody($T.fromBytes((byte[]) $L))", BinaryData.class, parameterName); + } else if (parameterType.equals("String")) { + methodBuilder + .addStatement("httpRequest.setBody($T.fromString((String) $L))", BinaryData.class, parameterName); + } else if (parameterType.equals("ByteBuffer")) { + // TODO: confirm behavior + //if (((ByteBuffer) bodyContentObject).hasArray()) { + // methodBuilder + // .addStatement("httpRequest.setBody($T.fromBytes(((ByteBuffer) $L).array()))", BinaryData.class, parameterName); + //} else { + // byte[] array = new byte[((ByteBuffer) bodyContentObject).remaining()]; + // + // ((ByteBuffer) bodyContentObject).get(array); + // methodBuilder + // .addStatement("httpRequest.setBody($T.fromBytes($L))", BinaryData.class, array); + //} + methodBuilder + .addStatement("httpRequest.setBody($T.fromBytes(((ByteBuffer) $L).array()))", BinaryData.class, + parameterName); + + } else { + methodBuilder + .addStatement("httpRequest.setBody($T.fromObject($L, serializer))", BinaryData.class, parameterName); + } + } + + /* + * Get a TypeName for a parameterized type, given the raw type and type arguments as Class objects. + */ + private static TypeName inferTypeNameFromReturnType(String typeString) { + // Split the string into raw type and type arguments + int angleBracketIndex = typeString.indexOf('<'); + if (angleBracketIndex == -1) { + // No type arguments + return ClassName.get("", typeString); + } + String rawTypeString = typeString.substring(0, angleBracketIndex); + String typeArgumentsString = typeString.substring(angleBracketIndex + 1, typeString.length() - 1); + + // Get the Class objects for the raw type and type arguments + Class rawType; + Class typeArgument; + try { + rawType = Class.forName(rawTypeString); + typeArgument = Class.forName(typeArgumentsString); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + // Use the inferTypeNameFromReturnType method to create a ParameterizedTypeName + return getParameterizedTypeNameFromRawArguments(rawType, typeArgument); + } + + /* + * Get a TypeName for a parameterized type, given the raw type and type arguments as Class objects. + */ + private static ParameterizedTypeName getParameterizedTypeNameFromRawArguments(Class rawType, + Class... typeArguments) { + ClassName rawTypeName = ClassName.get(rawType); + TypeName[] typeArgumentNames = new TypeName[typeArguments.length]; + for (int i = 0; i < typeArguments.length; i++) { + typeArgumentNames[i] = ClassName.get(typeArguments[i]); + } + return ParameterizedTypeName.get(rawTypeName, typeArgumentNames); + } +} diff --git a/sdk/clientcore/tools/sdk-codegen-tool/src/main/java/io/generation/tools/codegen/templating/TemplateProcessor.java b/sdk/clientcore/tools/sdk-codegen-tool/src/main/java/io/generation/tools/codegen/templating/TemplateProcessor.java new file mode 100644 index 0000000000000..68d9d8cbc2469 --- /dev/null +++ b/sdk/clientcore/tools/sdk-codegen-tool/src/main/java/io/generation/tools/codegen/templating/TemplateProcessor.java @@ -0,0 +1,13 @@ +package io.generation.tools.codegen.templating; + +import io.generation.tools.codegen.models.TemplateInput; + +import javax.annotation.processing.ProcessingEnvironment; + +public interface TemplateProcessor { + static TemplateProcessor getInstance() { + return new JavaPoetTemplateProcessor(); + } + + void process(TemplateInput templateInput, ProcessingEnvironment processingEnv); +} diff --git a/sdk/clientcore/tools/sdk-codegen-tool/src/main/java/io/generation/tools/codegen/utils/PathBuilder.java b/sdk/clientcore/tools/sdk-codegen-tool/src/main/java/io/generation/tools/codegen/utils/PathBuilder.java new file mode 100644 index 0000000000000..15d3b73dc1b67 --- /dev/null +++ b/sdk/clientcore/tools/sdk-codegen-tool/src/main/java/io/generation/tools/codegen/utils/PathBuilder.java @@ -0,0 +1,83 @@ +package io.generation.tools.codegen.utils; + +import io.generation.tools.codegen.exceptions.MissingSubstitutionException; +import io.generation.tools.codegen.models.HttpRequestContext; +import io.generation.tools.codegen.models.Substitution; + +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class PathBuilder { + // this class takes a 'raw host' string that contains {} delimited parameters, and needs to convert it into a + // Java string concatenation that can be used in the generated code. For example, the raw host string: + // https://{endpoint}/keys/{path1} + // would be converted into: + // "https://" + endpointParam + "/keys/" + pathValue + // Note that query parameters may also exist, and should be appended to the end of the URL string using + // a Map containing key-value pairs. + // Note that the 'endpoint' parameter is special - it is always the first parameter, and is always a host parameter. + public static String buildPath(String rawHost, HttpRequestContext method) { + if (method == null) { + throw new NullPointerException("method cannot be null"); + } + + final boolean hasQueryParams = !method.getQueryParams().isEmpty(); + + Pattern pattern = Pattern.compile("\\{(.+?)\\}"); + Matcher matcher = pattern.matcher(rawHost); + StringBuffer buffer = new StringBuffer(); + + while (matcher.find()) { + String paramName = matcher.group(1); + Substitution substitution = method.getSubstitution(paramName); + if (substitution != null && !substitution.getParameterVariableName().isEmpty()) { + matcher.appendReplacement(buffer, ""); + + if (buffer.length() != 0) { + buffer.append("\" + "); + } + buffer.append(substitution.getParameterVariableName()).append(" + \""); + } else { + throw new MissingSubstitutionException("Could not find substitution for '" + paramName + "' in method '" + method.getMethodName() + "'"); + } + } + + // Remove the last " + \"" + if (matcher.hitEnd()) { + matcher.appendTail(buffer); + + if (!hasQueryParams) { + buffer.append("\""); + } + } + + if (hasQueryParams) { + buffer.append("?"); + for (Map.Entry entry : method.getQueryParams().entrySet()) { + final String key = entry.getKey(); + final String value = entry.getValue(); + if (key.isEmpty() || value.isEmpty()) { + throw new IllegalArgumentException("Query parameter key and value must not be empty"); + } + buffer.append(entry.getKey()).append("=\" + ").append(entry.getValue()).append(" + \"&"); + } + // Remove the last "&\"" + buffer.delete(buffer.length() - 5, buffer.length()); + } + + // Add opening and closing quotes if they are not present and the first/last part is not a substitution + if (buffer.charAt(0) != '\"' && !rawHost.startsWith("{")) { + buffer.insert(0, '\"'); + } + if (!hasQueryParams && buffer.charAt(buffer.length() - 1) != '\"' && !rawHost.endsWith("}")) { + buffer.append('\"'); + } + + // strip out unnecessary `+ ""` in the buffer + String result = buffer.toString(); + result = result.replaceAll(" \\+ \"\"", ""); + + return result; + } +} diff --git a/sdk/clientcore/tools/sdk-codegen-tool/src/main/java/io/generation/tools/codegen/utils/ResponseBodyModeGeneration.java b/sdk/clientcore/tools/sdk-codegen-tool/src/main/java/io/generation/tools/codegen/utils/ResponseBodyModeGeneration.java new file mode 100644 index 0000000000000..2372c578a5d67 --- /dev/null +++ b/sdk/clientcore/tools/sdk-codegen-tool/src/main/java/io/generation/tools/codegen/utils/ResponseBodyModeGeneration.java @@ -0,0 +1,36 @@ +package io.generation.tools.codegen.utils; + +import com.squareup.javapoet.MethodSpec; +import io.clientcore.core.http.models.HttpResponse; +import io.clientcore.core.http.models.ResponseBodyMode; +import io.clientcore.core.implementation.http.HttpResponseAccessHelper; +import io.clientcore.core.util.binarydata.BinaryData; + +/* + * Utility class to generate response body mode assignment and response handling based on the response body mode. + */ +public class ResponseBodyModeGeneration { + public static void generateResponseBodyModeAssignment(MethodSpec.Builder methodBuilder) { + methodBuilder.addStatement("$T responseBodyMode = null", ResponseBodyMode.class) + .beginControlFlow("if (requestOptions != null)") + .addStatement("responseBodyMode = requestOptions.getResponseBodyMode()") + .endControlFlow(); + } + + public static void generateDeserializeResponseHandling(MethodSpec.Builder methodBuilder) { + methodBuilder.beginControlFlow("if (responseBodyMode == $T.DESERIALIZE)", ResponseBodyMode.class) + .addStatement("$T responseBody = response.getBody()", BinaryData.class) + .addStatement("$T.setValue(($T) response, responseBody)", + HttpResponseAccessHelper.class, HttpResponse.class) + .endControlFlow(); + } + + public static void generateNonDeserializeResponseHandling(MethodSpec.Builder methodBuilder) { + methodBuilder.nextControlFlow("else") + .addStatement("$T responseBody = response.getBody()", BinaryData.class) + .addStatement("$T.setBodyDeserializer(($T) response, (body) -> responseBody)", + HttpResponseAccessHelper.class, HttpResponse.class) + .endControlFlow(); + } +} + diff --git a/sdk/clientcore/tools/sdk-codegen-tool/src/main/resources/META-INF/maven/services/javax.annotation.processing.Processor b/sdk/clientcore/tools/sdk-codegen-tool/src/main/resources/META-INF/maven/services/javax.annotation.processing.Processor new file mode 100644 index 0000000000000..94311acc04cdd --- /dev/null +++ b/sdk/clientcore/tools/sdk-codegen-tool/src/main/resources/META-INF/maven/services/javax.annotation.processing.Processor @@ -0,0 +1 @@ +io.generation.tools.codegen.AnnotationProcessor diff --git a/sdk/clientcore/tools/sdk-codegen-tool/src/test/java/io/generation/tools/codegen/models/TemplateInputTest.java b/sdk/clientcore/tools/sdk-codegen-tool/src/test/java/io/generation/tools/codegen/models/TemplateInputTest.java new file mode 100644 index 0000000000000..38aa4774b2fd7 --- /dev/null +++ b/sdk/clientcore/tools/sdk-codegen-tool/src/test/java/io/generation/tools/codegen/models/TemplateInputTest.java @@ -0,0 +1,100 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package io.generation.tools.codegen.models; + +import io.clientcore.core.http.annotation.UnexpectedResponseExceptionDetail; +import java.util.Collections; +import java.util.List; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Tests for the {@link TemplateInput} class. + */ +public class TemplateInputTest { + + @Test + void getHostReturnsCorrectHost() { + TemplateInput templateInput = new TemplateInput(); + templateInput.setHost("localhost"); + assertEquals("localhost", templateInput.getHost()); + } + + @Test + void setHostUpdatesHost() { + TemplateInput templateInput = new TemplateInput(); + templateInput.setHost("127.0.0.1"); + assertEquals("127.0.0.1", templateInput.getHost()); + } + + @Test + void addImportAddsValidImport() { + TemplateInput templateInput = new TemplateInput(); + String shortName = templateInput.addImport("java.util.List"); + assertEquals("List", shortName); + assertTrue(templateInput.getImports().containsKey("java.util.List")); + } + + @Test + void addImportIgnoresNullImport() { + TemplateInput templateInput = new TemplateInput(); + String shortName = templateInput.addImport((String) null); + assertNull(shortName); + assertTrue(templateInput.getImports().isEmpty()); + } + + @Test + void addImportIgnoresEmptyImport() { + TemplateInput templateInput = new TemplateInput(); + String shortName = templateInput.addImport(""); + assertNull(shortName); + assertTrue(templateInput.getImports().isEmpty()); + } + + @Test + void addImportTypeMirrorAddsValidImport() { + TemplateInput templateInput = new TemplateInput(); + DeclaredType declaredType = mock(DeclaredType.class); + when(declaredType.toString()).thenReturn("java.util.Map"); + when(declaredType.getKind()).thenReturn(TypeKind.DECLARED); + String shortName = templateInput.addImport(declaredType); + assertEquals("Map", shortName); + assertTrue(templateInput.getImports().containsKey("java.util.Map")); + } + + @Test + void addImportTypeMirrorHandlesPrimitiveType() { + TemplateInput templateInput = new TemplateInput(); + TypeMirror typeMirror = mock(TypeMirror.class); + when(typeMirror.toString()).thenReturn("int"); + when(typeMirror.getKind()).thenReturn(TypeKind.INT); + String shortName = templateInput.addImport(typeMirror); + assertEquals("int", shortName); + assertTrue(templateInput.getImports().containsKey("int")); + } + + @Test + void setAndGetServiceInterfaceFQN() { + TemplateInput templateInput = new TemplateInput(); + templateInput.setServiceInterfaceFQN("com.example.Service"); + assertEquals("com.example.Service", templateInput.getServiceInterfaceFQN()); + } + + @Test + void setAndGetUnexpectedResponseExceptionDetails() { + TemplateInput templateInput = new TemplateInput(); + List details = + Collections.singletonList(mock(UnexpectedResponseExceptionDetail.class)); + templateInput.setUnexpectedResponseExceptionDetails(details); + assertEquals(details, templateInput.getUnexpectedResponseExceptionDetails()); + } +} diff --git a/sdk/clientcore/tools/sdk-codegen-tool/src/test/java/io/generation/tools/codegen/templating/APIGenerationTest.java b/sdk/clientcore/tools/sdk-codegen-tool/src/test/java/io/generation/tools/codegen/templating/APIGenerationTest.java new file mode 100644 index 0000000000000..3e39698a19c65 --- /dev/null +++ b/sdk/clientcore/tools/sdk-codegen-tool/src/test/java/io/generation/tools/codegen/templating/APIGenerationTest.java @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package io.generation.tools.codegen.templating; + +import io.generation.tools.codegen.models.HttpRequestContext; +import io.generation.tools.codegen.models.Substitution; +import io.generation.tools.codegen.models.TemplateInput; +import com.squareup.javapoet.MethodSpec; +import io.clientcore.core.http.models.HttpMethod; +import java.util.Collections; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; + +/* + * This class tests the methods generated from the provided ServiceInterface Template. + */ +public class APIGenerationTest { + + private JavaPoetTemplateProcessor processor; + private TemplateInput templateInput; + + @BeforeEach + public void setUp() { + processor = new JavaPoetTemplateProcessor(); + templateInput = mock(TemplateInput.class); + } + + @Test + public void testPublicAPIUserMethodGeneration() { + //@HttpRequestInformation( + // method = HttpMethod.GET, + // path = "/users/{userId}", + // expectedStatusCodes = {200} + // ) + // User getUser(@PathParam("userId") String userId); + HttpRequestContext getUserMethodContext = new HttpRequestContext(); + + getUserMethodContext.setHttpMethod(HttpMethod.GET); + getUserMethodContext.setPath("/users/{userId}"); + getUserMethodContext.setExpectedStatusCodes(new int[]{200}); + getUserMethodContext.setMethodName("getUser"); + getUserMethodContext.setMethodReturnType("User"); + getUserMethodContext.addSubstitution(new Substitution( + "String", + "userId", + false)); + getUserMethodContext.setBody(new HttpRequestContext.Body("multipart/form-data", "BinaryData", "audioTranscriptionOptions")); + templateInput.setHttpRequestContexts(Collections.singletonList(getUserMethodContext)); + + MethodSpec getUserMethodGenerationSpec = processor.generatePublicMethod(getUserMethodContext); + assertEquals("getUser", getUserMethodGenerationSpec.name); + assertEquals("User", getUserMethodGenerationSpec.returnType.toString()); + // assert code block contains the expected method body + assertEquals("return getUser();\n", getUserMethodGenerationSpec.code.toString()); + } +} diff --git a/sdk/clientcore/tools/sdk-codegen-tool/src/test/java/io/generation/tools/codegen/templating/BodyContentTypeProcessorTest.java b/sdk/clientcore/tools/sdk-codegen-tool/src/test/java/io/generation/tools/codegen/templating/BodyContentTypeProcessorTest.java new file mode 100644 index 0000000000000..e44923dd6ae0e --- /dev/null +++ b/sdk/clientcore/tools/sdk-codegen-tool/src/test/java/io/generation/tools/codegen/templating/BodyContentTypeProcessorTest.java @@ -0,0 +1,145 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package io.generation.tools.codegen.templating; + +import io.generation.tools.codegen.models.HttpRequestContext; +import com.squareup.javapoet.MethodSpec; +import java.util.stream.Stream; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class BodyContentTypeProcessorTest { + private JavaPoetTemplateProcessor processor; + + @BeforeEach + public void setup() { + processor = new JavaPoetTemplateProcessor(); + } + + /** + * Test for the method configureRequestWithBodyAndContentType + */ + @Test + public void bodyParamAnnotationPriorityOverContentTypeHeaderTest() { + // Create a new HttpRequestContext + HttpRequestContext context = new HttpRequestContext(); + byte[] bytes = "hello".getBytes(); + + // Set the body + // BodyParam annotation is set to "application/octet-stream" + context.setBody(new HttpRequestContext.Body("application/octet-stream", "ByteBuffer", "request")); + + // Add headers + // Content-Type header is set to "application/json" + context.addHeader("Content-Type", "application/json"); + context.addHeader("Content-Length", String.valueOf((long) bytes.length)); + HttpRequestContext.Body body = context.getBody(); + + MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("testMethod"); + processor.configureRequestWithBodyAndContentType(methodBuilder, body.getParameterType(), body.getContentType(), + body.getParameterName()); + MethodSpec methodSpec = methodBuilder.build(); + + // Expected output + String expectedOutput = + "httpRequest.setBody(io.clientcore.core.util.binarydata.BinaryData.fromBytes(((ByteBuffer) request).array()));"; + + // Actual output + String actualOutput = methodSpec.toString(); + + assertTrue(actualOutput.contains(expectedOutput)); + // Verify headers in a separate test request content type header is set to application/octet-stream + + } + + @ParameterizedTest + @MethodSource("knownParameterTypesProvider") + public void testConfigureRequestWithBodyAndParameterType(HttpRequestContext.Body body, String expectedOutput) { + MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("testMethod"); + processor.configureRequestWithBodyAndContentType(methodBuilder, body.getParameterType(), body.getContentType(), + body.getParameterName()); + MethodSpec methodSpec = methodBuilder.build(); + + // Actual output + String actualOutput = methodSpec.toString(); + assertTrue(actualOutput.contains(expectedOutput)); + } + + @ParameterizedTest + @MethodSource("knownContentTypesProvider") + public void testConfigureRequestWithBodyAndContentType(String parameterType, String expectedContentType) { + // Create a new HttpRequestContext + HttpRequestContext context = new HttpRequestContext(); + + // Set the body without specifying ContentType + context.setBody(new HttpRequestContext.Body(null, parameterType, "request")); + + MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("testMethod"); + processor.configureRequestWithBodyAndContentType(methodBuilder, context.getBody().getParameterType(), + context.getBody().getContentType(), context.getBody().getParameterName()); + MethodSpec methodSpec = methodBuilder.build(); + + // Expected output + String expectedOutput = + "httpRequest.getHeaders().set(io.clientcore.core.http.models.HttpHeaderName.CONTENT_TYPE, " + + expectedContentType; + + // Actual output + String actualOutput = methodSpec.toString(); + + assertTrue(actualOutput.contains(expectedOutput)); + } + + private static Stream knownContentTypesProvider() { + return Stream.of( + Arguments.of("byte[]", "io.clientcore.core.http.models.ContentType.APPLICATION_OCTET_STREAM"), + Arguments.of("String", "io.clientcore.core.http.models.ContentType.APPLICATION_OCTET_STREAM"), + Arguments.of("BinaryData", "io.clientcore.core.http.models.ContentType.APPLICATION_JSON"), + Arguments.of("Object", "io.clientcore.core.http.models.ContentType.APPLICATION_JSON"), + Arguments.of("ByteBuffer", "io.clientcore.core.http.models.ContentType.APPLICATION_JSON") + ); + } + + private static Stream knownParameterTypesProvider() { + return Stream.of( + // scenario for isJson = true and parameterType == "ByteBuffer" + Arguments.of(new HttpRequestContext.Body(null, "ByteBuffer", "request"), + "httpRequest.setBody(io.clientcore.core.util.binarydata.BinaryData.fromObject(request, serializer));"), + Arguments.of(new HttpRequestContext.Body("application/octet-stream", "BinaryData", "request"), + "httpRequest.setBody(io.clientcore.core.util.binarydata.BinaryData.fromObject(request, serializer));"), + Arguments.of(new HttpRequestContext.Body("application/json", "BinaryData", "request"), + "httpRequest.setBody(io.clientcore.core.util.binarydata.BinaryData.fromObject(request, serializer));"), + Arguments.of(new HttpRequestContext.Body("application/json", "serializable", "request"), + "httpRequest.setBody(io.clientcore.core.util.binarydata.BinaryData.fromObject(request, serializer))"), + Arguments.of(new HttpRequestContext.Body("application/octet-stream", "byte[]", "request"), + "httpRequest.setBody(io.clientcore.core.util.binarydata.BinaryData.fromBytes((byte[]) request))"), + Arguments.of(new HttpRequestContext.Body("application/octet-stream", "String", "request"), + "httpRequest.setBody(io.clientcore.core.util.binarydata.BinaryData.fromString((String) request))"), + Arguments.of(new HttpRequestContext.Body("application/octet-stream", "ByteBuffer", "request"), + "httpRequest.setBody(io.clientcore.core.util.binarydata.BinaryData.fromBytes(((ByteBuffer) request).array()))"), + Arguments.of(new HttpRequestContext.Body("application/octet-stream", "Object", "request"), + "httpRequest.setBody(io.clientcore.core.util.binarydata.BinaryData.fromObject(request, serializer))"), + // scenario for isJson = false and parameterType == "String" + Arguments.of(new HttpRequestContext.Body("text/html", "String", "request"), + "httpRequest.setBody(io.clientcore.core.util.binarydata.BinaryData.fromString((String) request));"), + // scenario for isJson = false and parameterType == "ByteBuffer" + Arguments.of(new HttpRequestContext.Body("text/html", "ByteBuffer", "request"), + "httpRequest.setBody(io.clientcore.core.util.binarydata.BinaryData.fromBytes(((ByteBuffer) request).array()));"), + // scenario for parameterType = null + Arguments.of(new HttpRequestContext.Body("application/json", null, "request"), + "httpRequest.getHeaders().set(io.clientcore.core.http.models.HttpHeaderName.CONTENT_LENGTH, \"0\"));"), + // scenario for parameterType == "byte[]" + Arguments.of(new HttpRequestContext.Body("application/octet-stream", "byte[]", "request"), + "httpRequest.setBody(io.clientcore.core.util.binarydata.BinaryData.fromBytes((byte[]) request));"), + // Add scenario for parameterType == "String" + Arguments.of(new HttpRequestContext.Body("application/octet-stream", "String", "request"), + "httpRequest.setBody(io.clientcore.core.util.binarydata.BinaryData.fromString((String) request));") + ); + } +} diff --git a/sdk/clientcore/tools/sdk-codegen-tool/src/test/java/io/generation/tools/codegen/templating/HttpPipelineBuilderMethodTest.java b/sdk/clientcore/tools/sdk-codegen-tool/src/test/java/io/generation/tools/codegen/templating/HttpPipelineBuilderMethodTest.java new file mode 100644 index 0000000000000..2ad387d509eb6 --- /dev/null +++ b/sdk/clientcore/tools/sdk-codegen-tool/src/test/java/io/generation/tools/codegen/templating/HttpPipelineBuilderMethodTest.java @@ -0,0 +1,124 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package io.generation.tools.codegen.templating; + +import io.generation.tools.codegen.models.TemplateInput; +import com.squareup.javapoet.ClassName; +import com.squareup.javapoet.FieldSpec; +import com.squareup.javapoet.MethodSpec; +import java.io.IOException; +import java.io.Writer; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import javax.annotation.processing.Filer; +import javax.annotation.processing.ProcessingEnvironment; +import javax.lang.model.element.Modifier; +import javax.tools.JavaFileObject; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/* + * Tests for builder/helper methods generated in ServiceClientImpl class. + */ +public class HttpPipelineBuilderMethodTest { + + private static final String PACKAGE_NAME = "com.example"; + private static final String SERVICE_INTERFACE_SHORT_NAME = getExampleClientServiceImpl(); + private JavaPoetTemplateProcessor processor; + private ProcessingEnvironment processingEnv; + private TemplateInput templateInput; + + @BeforeEach + public void setUp() { + processor = new JavaPoetTemplateProcessor(); + processingEnv = mock(ProcessingEnvironment.class); + templateInput = mock(TemplateInput.class); + + when(templateInput.getPackageName()).thenReturn(PACKAGE_NAME); + when(templateInput.getServiceInterfaceImplShortName()).thenReturn(SERVICE_INTERFACE_SHORT_NAME); + when(templateInput.getServiceInterfaceShortName()).thenReturn("ExampleClientService"); + when(templateInput.getHttpRequestContexts()).thenReturn(Collections.emptyList()); + } + + private static String getExampleClientServiceImpl() { + return "ExampleClientServiceImpl"; + } + + @Test + public void testProcess() throws IOException { + Filer filer = mock(Filer.class); + JavaFileObject filerSourceFile = mock(JavaFileObject.class); + when(processingEnv.getFiler()).thenReturn(filer); + when(filer.createSourceFile(anyString())).thenReturn(filerSourceFile); + when(filerSourceFile.openWriter()).thenReturn(mock(Writer.class)); + + processor.process(templateInput, processingEnv); + + // Verify that the JavaFile.writeTo was called + verify(processingEnv, times(1)).getFiler(); + } + + @Test + public void testGetEndpointMethod() { + MethodSpec method = processor.getEndpointMethod(); + assertEquals("getEndpoint", method.name); + assertEquals(Modifier.PUBLIC, method.modifiers.iterator().next()); + assertEquals(ClassName.get("java.lang", "String"), method.returnType); + } + + @Test + public void testGetPipelineMethod() { + MethodSpec method = processor.getPipelineMethod(); + assertEquals("getPipeline", method.name); + assertEquals(Modifier.PUBLIC, method.modifiers.iterator().next()); + assertEquals(processor.HTTP_PIPELINE, method.returnType); + } + + @Test + public void testGetServiceVersionMethod() { + MethodSpec method = processor.getServiceVersionMethod(); + assertEquals("getServiceVersion", method.name); + assertEquals(Modifier.PUBLIC, method.modifiers.iterator().next()); + when(templateInput.getServiceInterfaceShortName()).thenReturn("ExampleClientService"); + assertTrue(method.code.toString().contains("return serviceVersion")); + } + + @Test + public void testGetServiceVersionType() { + assertEquals("com.example.ExampleServiceVersion", processor.getServiceVersionType(PACKAGE_NAME, + SERVICE_INTERFACE_SHORT_NAME).toString()); + } + + @Test + public void testServiceImplConstructorGeneration() { + MethodSpec constructor = processor.getServiceImplConstructor(PACKAGE_NAME, + SERVICE_INTERFACE_SHORT_NAME); + assertEquals(Modifier.PUBLIC, constructor.modifiers.iterator().next()); + assertEquals(4, constructor.parameters.size()); + assertTrue(constructor.code.toString().contains("this.defaultPipeline = defaultPipeline")); + assertTrue(constructor.code.toString().contains("this.serializer = serializer")); + assertTrue(constructor.code.toString().contains("this.endpoint = endpoint")); + assertTrue(constructor.code.toString().contains("this.apiVersion = serviceVersion.getVersion()")); + assertTrue(constructor.code.toString().contains("this.serviceVersion = serviceVersion")); + } + + @Test + public void testLoggerFieldGeneration() { + FieldSpec loggerField = processor.getLoggerField(PACKAGE_NAME, SERVICE_INTERFACE_SHORT_NAME); + assertEquals(new HashSet<>(Arrays.asList(Modifier.PRIVATE, Modifier.STATIC, Modifier.FINAL)), + loggerField.modifiers); + assertEquals(processor.CLIENTLOGGER_NAME, loggerField.type); + assertEquals("LOGGER", loggerField.name); + assertTrue(loggerField.initializer.toString().contains("new io.clientcore.core.util.ClientLogger(com.example.ExampleClientServiceImpl.class)")); + } +} diff --git a/sdk/clientcore/tools/sdk-codegen-tool/src/test/java/io/generation/tools/codegen/utils/PathBuilderTest.java b/sdk/clientcore/tools/sdk-codegen-tool/src/test/java/io/generation/tools/codegen/utils/PathBuilderTest.java new file mode 100644 index 0000000000000..efcdf7d489f57 --- /dev/null +++ b/sdk/clientcore/tools/sdk-codegen-tool/src/test/java/io/generation/tools/codegen/utils/PathBuilderTest.java @@ -0,0 +1,425 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package io.generation.tools.codegen.utils; + +import io.generation.tools.codegen.exceptions.MissingSubstitutionException; +import io.generation.tools.codegen.models.HttpRequestContext; +import io.generation.tools.codegen.models.Substitution; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class PathBuilderTest { + + @Test + public void buildsPathWithHostSubstitution() { + HttpRequestContext context = new HttpRequestContext(); + context.addSubstitution(new Substitution("endpoint", "myEndpoint")); + String result = PathBuilder.buildPath("https://{endpoint}/keys", context); + assertEquals("\"https://\" + myEndpoint + \"/keys\"", result); + } + + @Test + public void buildsPathWithPathSubstitution() { + HttpRequestContext context = new HttpRequestContext(); + context.addSubstitution(new Substitution("endpoint", "myEndpoint")); + context.addSubstitution(new Substitution("path1", "myPath")); + String result = PathBuilder.buildPath("https://{endpoint}/keys/{path1}", context); + assertEquals("\"https://\" + myEndpoint + \"/keys/\" + myPath", result); + } + + @Test + public void buildsPathWithQueryParameters() { + HttpRequestContext context = new HttpRequestContext(); + context.addSubstitution(new Substitution("endpoint", "myEndpoint")); + context.addQueryParam("key1", "value1"); + context.addQueryParam("key2", "value2"); + String result = PathBuilder.buildPath("https://{endpoint}/keys", context); + assertEquals("\"https://\" + myEndpoint + \"/keys?key1=\" + value1 + \"&key2=\" + value2", result); + } + + @Test + public void buildsPathWithEmptySubstitutions() { + HttpRequestContext context = new HttpRequestContext(); + context.addSubstitution(new Substitution("endpoint", "myEndpoint")); + + try { + String result = PathBuilder.buildPath("https://{endpoint}/keys/{path1}", context); + } catch (MissingSubstitutionException e) { + assertEquals("Could not find substitution for 'path1' in method 'null'", e.getMessage()); + } + } + + @Test + public void buildsPathWithNullSubstitutions() { + try { + String result = PathBuilder.buildPath("https://{endpoint}/keys/{path1}", null); + } catch (NullPointerException e) { + assertEquals("method cannot be null", e.getMessage()); + } + } + + @Test + public void buildsPathWithMultipleSubstitutions() { + HttpRequestContext context = new HttpRequestContext(); + context.addSubstitution(new Substitution("endpoint", "myEndpoint")); + context.addSubstitution(new Substitution("path1", "myPath")); + context.addSubstitution(new Substitution("path2", "myPath2")); + String result = PathBuilder.buildPath("https://{endpoint}/keys/{path1}/{path2}", context); + assertEquals("\"https://\" + myEndpoint + \"/keys/\" + myPath + \"/\" + myPath2", result); + } + + @Test + public void buildsPathWithMultipleQueryParameters() { + HttpRequestContext context = new HttpRequestContext(); + context.addSubstitution(new Substitution("endpoint", "myEndpoint")); + context.addQueryParam("key1", "value1"); + context.addQueryParam("key2", "value2"); + context.addQueryParam("key3", "value3"); + String result = PathBuilder.buildPath("https://{endpoint}/keys", context); + assertEquals("\"https://\" + myEndpoint + \"/keys?key1=\" + value1 + \"&key2=\" + value2 + \"&key3=\" + value3", result); + } + + @Test + public void buildsPathWithNoSubstitutions() { + HttpRequestContext context = new HttpRequestContext(); + String result = PathBuilder.buildPath("https://keys", context); + assertEquals("\"https://keys\"", result); + } + + @Test + public void buildsPathWithNoQueryParameters() { + HttpRequestContext context = new HttpRequestContext(); + context.addSubstitution(new Substitution("endpoint", "myEndpoint")); + String result = PathBuilder.buildPath("https://{endpoint}/keys", context); + assertEquals("\"https://\" + myEndpoint + \"/keys\"", result); + } + + @Test + public void buildsPathWithMultipleSameSubstitutions() { + HttpRequestContext context = new HttpRequestContext(); + context.addSubstitution(new Substitution("endpoint", "myEndpoint")); + context.addSubstitution(new Substitution("path", "myPath")); + String result = PathBuilder.buildPath("https://{endpoint}/keys/{path}/{path}", context); + assertEquals("\"https://\" + myEndpoint + \"/keys/\" + myPath + \"/\" + myPath", result); + } + + @Test + public void buildsPathWithMultipleSameQueryParameters() { + HttpRequestContext context = new HttpRequestContext(); + context.addSubstitution(new Substitution("endpoint", "myEndpoint")); + context.addQueryParam("key", "value1"); + assertThrows(IllegalArgumentException.class, () -> context.addQueryParam("key", "value2")); + } + + @Test + public void buildsPathWithClashingSubstitutionNames() { + HttpRequestContext context = new HttpRequestContext(); + context.addSubstitution(new Substitution("endpoint", "myEndpoint")); + assertThrows(IllegalArgumentException.class, () -> context.addSubstitution(new Substitution("endpoint", "myEndpoint2"))); + } + + @Test + public void buildsPathWithMultipleSubstitutionsAndQueryParameters() { + HttpRequestContext context = new HttpRequestContext(); + context.addSubstitution(new Substitution("endpoint", "myEndpoint")); + context.addSubstitution(new Substitution("path1", "myPath")); + context.addSubstitution(new Substitution("path2", "myPath2")); + context.addQueryParam("key1", "value1"); + context.addQueryParam("key2", "value2"); + context.addQueryParam("key3", "value3"); + String result = PathBuilder.buildPath("https://{endpoint}/keys/{path1}/{path2}", context); + assertEquals("\"https://\" + myEndpoint + \"/keys/\" + myPath + \"/\" + myPath2 + \"?key1=\" + value1 + \"&key2=\" + value2 + \"&key3=\" + value3", result); + } + + @Test + public void buildsPathWithMissingSubstitution() { + HttpRequestContext context = new HttpRequestContext(); + context.addSubstitution(new Substitution("endpoint", "myEndpoint")); + assertThrows(MissingSubstitutionException.class, () -> PathBuilder.buildPath("https://{endpoint}/keys/{path1}", context)); + } + + @Test + public void buildsPathWithMissingQueryParameter() { + HttpRequestContext context = new HttpRequestContext(); + context.addSubstitution(new Substitution("endpoint", "myEndpoint")); + context.addQueryParam("key1", "value1"); + assertThrows(MissingSubstitutionException.class, () -> PathBuilder.buildPath("https://{endpoint}/keys?key2={value2}", context)); + } + + @Test + public void buildsPathWithEmptySubstitutionValue() { + HttpRequestContext context = new HttpRequestContext(); + context.addSubstitution(new Substitution("endpoint", "")); + assertThrows(MissingSubstitutionException.class, () -> PathBuilder.buildPath("https://{endpoint}/keys", context)); + } + + @Test + public void buildsPathWithEmptyQueryParameterValue() { + HttpRequestContext context = new HttpRequestContext(); + context.addSubstitution(new Substitution("endpoint", "myEndpoint")); + context.addQueryParam("key1", ""); + assertThrows(IllegalArgumentException.class, () -> PathBuilder.buildPath("https://{endpoint}/keys", context)); + } + + @Test + public void buildsPathWithSubstitutionNotSurroundedBySlashes() { + HttpRequestContext context = new HttpRequestContext(); + context.addSubstitution(new Substitution("endpoint", "myEndpoint")); + String result = PathBuilder.buildPath("https://{endpoint}.azure.com/keys", context); + assertEquals("\"https://\" + myEndpoint + \".azure.com/keys\"", result); + } + + @Test + public void buildsPathWithSubstitutionFollowedByDot() { + HttpRequestContext context = new HttpRequestContext(); + context.addSubstitution(new Substitution("endpoint", "myEndpoint")); + context.addSubstitution(new Substitution("domain", "azure")); + String result = PathBuilder.buildPath("https://{endpoint}.{domain}.com/keys", context); + assertEquals("\"https://\" + myEndpoint + \".\" + azure + \".com/keys\"", result); + } + + @Test + public void buildsPathWithSubstitutionFollowedByColon() { + HttpRequestContext context = new HttpRequestContext(); + context.addSubstitution(new Substitution("protocol", "protocol")); + context.addSubstitution(new Substitution("endpoint", "myEndpoint")); + String result = PathBuilder.buildPath("{protocol}://{endpoint}/keys", context); + assertEquals("protocol + \"://\" + myEndpoint + \"/keys\"", result); + } + + @Test + public void buildsPathWithSubstitutionFollowedByQuestionMark() { + HttpRequestContext context = new HttpRequestContext(); + context.addSubstitution(new Substitution("endpoint", "myEndpoint")); + context.addSubstitution(new Substitution("query", "myQuery")); + String result = PathBuilder.buildPath("https://{endpoint}/keys?{query}", context); + assertEquals("\"https://\" + myEndpoint + \"/keys?\" + myQuery", result); + } + + @Test + public void buildsPathWithSubstitutionFollowedBySlash() { + HttpRequestContext context = new HttpRequestContext(); + context.addSubstitution(new Substitution("endpoint", "myEndpoint")); + context.addSubstitution(new Substitution("path", "myPath")); + String result = PathBuilder.buildPath("https://{endpoint}/{path}/keys", context); + assertEquals("\"https://\" + myEndpoint + \"/\" + myPath + \"/keys\"", result); + } + + @Test + public void buildsPathWithSubstitutionFollowedByHyphen() { + HttpRequestContext context = new HttpRequestContext(); + context.addSubstitution(new Substitution("endpoint", "myEndpoint")); + context.addSubstitution(new Substitution("path", "myPath")); + String result = PathBuilder.buildPath("https://{endpoint}-{path}/keys", context); + assertEquals("\"https://\" + myEndpoint + \"-\" + myPath + \"/keys\"", result); + } + + @Test + public void buildsPathWithSubstitutionFollowedByUnderscore() { + HttpRequestContext context = new HttpRequestContext(); + context.addSubstitution(new Substitution("endpoint", "myEndpoint")); + context.addSubstitution(new Substitution("path", "myPath")); + String result = PathBuilder.buildPath("https://{endpoint}_{path}/keys", context); + assertEquals("\"https://\" + myEndpoint + \"_\" + myPath + \"/keys\"", result); + } + + @Test + public void buildsPathWithSubstitutionFollowedByPercent() { + HttpRequestContext context = new HttpRequestContext(); + context.addSubstitution(new Substitution("endpoint", "myEndpoint")); + context.addSubstitution(new Substitution("path", "myPath")); + String result = PathBuilder.buildPath("https://{endpoint}%{path}/keys", context); + assertEquals("\"https://\" + myEndpoint + \"%\" + myPath + \"/keys\"", result); + } + + @Test + public void buildsPathWithSubstitutionFollowedByPlus() { + HttpRequestContext context = new HttpRequestContext(); + context.addSubstitution(new Substitution("endpoint", "myEndpoint")); + context.addSubstitution(new Substitution("path", "myPath")); + String result = PathBuilder.buildPath("https://{endpoint}+{path}/keys", context); + assertEquals("\"https://\" + myEndpoint + \"+\" + myPath + \"/keys\"", result); + } + + @Test + public void buildsPathWithSubstitutionFollowedByNumber() { + HttpRequestContext context = new HttpRequestContext(); + context.addSubstitution(new Substitution("endpoint", "myEndpoint")); + context.addSubstitution(new Substitution("path", "myPath")); + String result = PathBuilder.buildPath("https://{endpoint}1{path}/keys", context); + assertEquals("\"https://\" + myEndpoint + \"1\" + myPath + \"/keys\"", result); + } + + @Test + public void buildsPathWithSubstitutionFollowedBySpecialCharacter() { + HttpRequestContext context = new HttpRequestContext(); + context.addSubstitution(new Substitution("endpoint", "myEndpoint")); + context.addSubstitution(new Substitution("path", "myPath")); + String result = PathBuilder.buildPath("https://{endpoint}*{path}/keys", context); + assertEquals("\"https://\" + myEndpoint + \"*\" + myPath + \"/keys\"", result); + } + + @Test + public void buildsPathWithSubstitutionFollowedBySpace() { + HttpRequestContext context = new HttpRequestContext(); + context.addSubstitution(new Substitution("endpoint", "myEndpoint")); + context.addSubstitution(new Substitution("path", "myPath")); + String result = PathBuilder.buildPath("https://{endpoint} {path}/keys", context); + assertEquals("\"https://\" + myEndpoint + \" \" + myPath + \"/keys\"", result); + } + + @Test + public void buildsPathWithSubstitutionFollowedByLetter() { + HttpRequestContext context = new HttpRequestContext(); + context.addSubstitution(new Substitution("endpoint", "myEndpoint")); + context.addSubstitution(new Substitution("path", "myPath")); + String result = PathBuilder.buildPath("https://{endpoint}a{path}/keys", context); + assertEquals("\"https://\" + myEndpoint + \"a\" + myPath + \"/keys\"", result); + } + + @Test + public void buildsPathWithSubstitutionFollowedByUnicodeCharacter() { + HttpRequestContext context = new HttpRequestContext(); + context.addSubstitution(new Substitution("endpoint", "myEndpoint")); + context.addSubstitution(new Substitution("path", "myPath")); + String result = PathBuilder.buildPath("https://{endpoint}\u00A9{path}/keys", context); + assertEquals("\"https://\" + myEndpoint + \"\u00A9\" + myPath + \"/keys\"", result); + } + + @Test + public void buildsPathWithSubstitutionFollowedBySpecialAndAlphanumericCharacters() { + HttpRequestContext context = new HttpRequestContext(); + context.addSubstitution(new Substitution("endpoint", "myEndpoint")); + context.addSubstitution(new Substitution("path", "myPath")); + String result = PathBuilder.buildPath("https://{endpoint}*1a{path}/keys", context); + assertEquals("\"https://\" + myEndpoint + \"*1a\" + myPath + \"/keys\"", result); + } + + @Test + public void buildsPathWithSubstitutionFollowedBySpecialAlphanumericAndUnicodeCharacters() { + HttpRequestContext context = new HttpRequestContext(); + context.addSubstitution(new Substitution("endpoint", "myEndpoint")); + context.addSubstitution(new Substitution("path", "myPath")); + String result = PathBuilder.buildPath("https://{endpoint}*1a\u00A9{path}/keys", context); + assertEquals("\"https://\" + myEndpoint + \"*1a\u00A9\" + myPath + \"/keys\"", result); + } + + @Test + public void buildsPathWithSubstitutionFollowedBySpecialAlphanumericUnicodeCharactersAndSpaces() { + HttpRequestContext context = new HttpRequestContext(); + context.addSubstitution(new Substitution("endpoint", "myEndpoint")); + context.addSubstitution(new Substitution("path", "myPath")); + String result = PathBuilder.buildPath("https://{endpoint}*1a\u00A9 {path}/keys", context); + assertEquals("\"https://\" + myEndpoint + \"*1a\u00A9 \" + myPath + \"/keys\"", result); + } + + @Test + public void buildsPathWithMultipleSubstitutionsFollowedByDifferentCharacters() { + HttpRequestContext context = new HttpRequestContext(); + context.addSubstitution(new Substitution("endpoint", "myEndpoint")); + context.addSubstitution(new Substitution("path1", "myPath1")); + context.addSubstitution(new Substitution("path2", "myPath2")); + context.addSubstitution(new Substitution("path3", "myPath3")); + String result = PathBuilder.buildPath("https://{endpoint}*1a{path1}\u00A9 {path2}/keys/{path3}", context); + assertEquals("\"https://\" + myEndpoint + \"*1a\" + myPath1 + \"\u00A9 \" + myPath2 + \"/keys/\" + myPath3", result); + } + + @Test + public void buildsPathWithSubstitutionValueContainingSpecialCharacter() { + HttpRequestContext context = new HttpRequestContext(); + context.addSubstitution(new Substitution("endpoint", "myEndpoint*")); + String result = PathBuilder.buildPath("https://{endpoint}/keys", context); + assertEquals("\"https://\" + myEndpoint* + \"/keys\"", result); + } + +// @Test +// public void buildsPathWithNestedSubstitutions() { +// HttpRequestContext context = new HttpRequestContext(); +// context.addSubstitution(new Substitution("endpoint", "myEndpoint")); +// context.addSubstitution(new Substitution("path", "myPath")); +// assertThrows(MissingSubstitutionException.class, () -> PathBuilder.buildPath("https://{{endpoint}/keys/{path}", context)); +// } + +// @Test +// public void buildsPathWithMissingClosingBrace() { +// HttpRequestContext context = new HttpRequestContext(); +// context.addSubstitution(new Substitution("endpoint", "myEndpoint")); +// context.addSubstitution(new Substitution("path", "myPath")); +// assertThrows(MissingSubstitutionException.class, () -> PathBuilder.buildPath("https://{endpoint/keys/{path}", context)); +// } + +// @Test +// public void buildsPathWithMissingOpeningBrace() { +// HttpRequestContext context = new HttpRequestContext(); +// context.addSubstitution(new Substitution("endpoint", "myEndpoint")); +// context.addSubstitution(new Substitution("path", "myPath")); +// assertThrows(MissingSubstitutionException.class, () -> PathBuilder.buildPath("https://endpoint}/keys/{path}", context)); +// } + +// @Test +// public void buildsPathWithSubstitutionContainingOpeningBrace() { +// HttpRequestContext context = new HttpRequestContext(); +// context.addSubstitution(new Substitution("endpoint{", "myEndpoint")); +// assertThrows(MissingSubstitutionException.class, () -> PathBuilder.buildPath("https://{endpoint{/keys", context)); +// } + + @Test + public void buildsPathWithSubstitutionContainingClosingBrace() { + HttpRequestContext context = new HttpRequestContext(); + context.addSubstitution(new Substitution("endpoint}", "myEndpoint")); + assertThrows(MissingSubstitutionException.class, () -> PathBuilder.buildPath("https://{endpoint}/keys", context)); + } + + @Test + public void buildsPathWithSubstitutionContainingBothBraces() { + HttpRequestContext context = new HttpRequestContext(); + context.addSubstitution(new Substitution("endpoint{}", "myEndpoint")); + assertThrows(MissingSubstitutionException.class, () -> PathBuilder.buildPath("https://{endpoint{}}/keys", context)); + } + + @Test + public void buildsPathWithSubstitutionContainingMultipleBraces() { + HttpRequestContext context = new HttpRequestContext(); + context.addSubstitution(new Substitution("endpoint{{}}", "myEndpoint")); + assertThrows(MissingSubstitutionException.class, () -> PathBuilder.buildPath("https://{endpoint{{}}}/keys", context)); + } + + @Test + public void buildsPathWithoutProtocolWithNoSubstitutions() { + HttpRequestContext context = new HttpRequestContext(); + assertThrows(MissingSubstitutionException.class, () -> PathBuilder.buildPath("{endpoint}/keys", context)); + } + + @Test + public void buildsPathWithoutProtocolWithSubstitutions() { + HttpRequestContext context = new HttpRequestContext(); + context.addSubstitution(new Substitution("endpoint", "myEndpoint")); + context.addSubstitution(new Substitution("path", "myPath")); + String result = PathBuilder.buildPath("{endpoint}/keys/{path}", context); + assertEquals("myEndpoint + \"/keys/\" + myPath", result); + } + + @Test + public void buildsPathWithoutProtocolWithQueryParameters() { + HttpRequestContext context = new HttpRequestContext(); + context.addSubstitution(new Substitution("endpoint", "myEndpoint")); + context.addQueryParam("key1", "value1"); + context.addQueryParam("key2", "value2"); + String result = PathBuilder.buildPath("{endpoint}/keys", context); + assertEquals("myEndpoint + \"/keys?key1=\" + value1 + \"&key2=\" + value2", result); + } + + @Test + public void buildsPathWithoutProtocolWithSubstitutionsAndQueryParameters() { + HttpRequestContext context = new HttpRequestContext(); + context.addSubstitution(new Substitution("endpoint", "myEndpoint")); + context.addSubstitution(new Substitution("path", "myPath")); + context.addQueryParam("key1", "value1"); + context.addQueryParam("key2", "value2"); + String result = PathBuilder.buildPath("{endpoint}/keys/{path}", context); + assertEquals("myEndpoint + \"/keys/\" + myPath + \"?key1=\" + value1 + \"&key2=\" + value2", result); + } +} diff --git a/sdk/clientcore/tools/sdk-codegen-tool/src/test/java/io/generation/tools/codegen/utils/ResponseBodyModeGenerationTest.java b/sdk/clientcore/tools/sdk-codegen-tool/src/test/java/io/generation/tools/codegen/utils/ResponseBodyModeGenerationTest.java new file mode 100644 index 0000000000000..6dd997a056e4e --- /dev/null +++ b/sdk/clientcore/tools/sdk-codegen-tool/src/test/java/io/generation/tools/codegen/utils/ResponseBodyModeGenerationTest.java @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package io.generation.tools.codegen.utils; + +import com.squareup.javapoet.MethodSpec; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +class ResponseBodyModeGenerationTest { + + private MethodSpec.Builder methodBuilder; + + @BeforeEach + void setUp() { + methodBuilder = MethodSpec.methodBuilder("testMethod"); + } + + @Test + void generateResponseBodyModeAssignment_withRequestOptions() { + ResponseBodyModeGeneration.generateResponseBodyModeAssignment(methodBuilder); + MethodSpec methodSpec = methodBuilder.build(); + assertTrue(methodSpec.toString().contains("responseBodyMode = requestOptions.getResponseBodyMode()")); + } + + @Test + void generateResponseHandling_withDeserializeMode() { + ResponseBodyModeGeneration.generateDeserializeResponseHandling(methodBuilder); + MethodSpec methodSpec = methodBuilder.build(); + // verify generation calls HttpResponseAccessHelper.setValue() with the correct parameters; + assertTrue(methodSpec.toString().contains("HttpResponseAccessHelper.setValue((io.clientcore.core.http.models.HttpResponse) response, responseBody);")); + } + + //@Test + //void generateResponseHandling_withNonDeserializeMode() { + // ResponseBodyModeGeneration.generateNonDeserializeResponseHandling(methodBuilder); + // MethodSpec methodSpec = methodBuilder.build(); + // // verify generation calls HttpResponseAccessHelper.setValue() with the correct parameters; + // assertTrue(methodSpec.toString().contains("HttpResponseAccessHelper.setValue((io.clientcore.core.http.models.HttpResponse) response, responseBody);")); + //} +} From 149a750b5ddbd5504aea9e0c312722bba30ffeb8 Mon Sep 17 00:00:00 2001 From: "Sameeksha Vaity (from Dev Box)" Date: Thu, 19 Dec 2024 14:39:24 -0800 Subject: [PATCH 2/5] update naming and groupid --- eng/versioning/external_dependencies.txt | 2 +- .../README.md | 16 ++++++++-------- .../pom.xml | 8 ++++---- .../tools/codegen/AnnotationProcessor.java | 15 +++++++++------ .../exceptions/MissingSubstitutionException.java | 5 ++++- .../tools/codegen/models/HttpRequestContext.java | 7 +++++-- .../tools/codegen/models/Substitution.java | 5 ++++- .../tools/codegen/models/TemplateInput.java | 5 ++++- .../templating/JavaPoetTemplateProcessor.java | 11 +++++++---- .../codegen/templating/TemplateProcessor.java | 7 +++++-- .../tools/codegen/utils/PathBuilder.java | 11 +++++++---- .../utils/ResponseBodyModeGeneration.java | 5 ++++- .../javax.annotation.processing.Processor | 1 + .../tools/codegen/models/TemplateInputTest.java | 2 +- .../codegen/templating/APIGenerationTest.java | 8 ++++---- .../templating/BodyContentTypeProcessorTest.java | 4 ++-- .../HttpPipelineBuilderMethodTest.java | 4 ++-- .../tools/codegen/utils/PathBuilderTest.java | 8 ++++---- .../utils/ResponseBodyModeGenerationTest.java | 2 +- sdk/clientcore/tools/pom.xml | 4 ++-- .../javax.annotation.processing.Processor | 1 - 21 files changed, 79 insertions(+), 52 deletions(-) rename sdk/clientcore/tools/{sdk-codegen-tool => annotation-processor}/README.md (89%) rename sdk/clientcore/tools/{sdk-codegen-tool => annotation-processor}/pom.xml (91%) rename sdk/clientcore/tools/{sdk-codegen-tool/src/main/java/io/generation => annotation-processor/src/main/java/io/clientcore}/tools/codegen/AnnotationProcessor.java (95%) rename sdk/clientcore/tools/{sdk-codegen-tool/src/main/java/io/generation => annotation-processor/src/main/java/io/clientcore}/tools/codegen/exceptions/MissingSubstitutionException.java (70%) rename sdk/clientcore/tools/{sdk-codegen-tool/src/main/java/io/generation => annotation-processor/src/main/java/io/clientcore}/tools/codegen/models/HttpRequestContext.java (97%) rename sdk/clientcore/tools/{sdk-codegen-tool/src/main/java/io/generation => annotation-processor/src/main/java/io/clientcore}/tools/codegen/models/Substitution.java (93%) rename sdk/clientcore/tools/{sdk-codegen-tool/src/main/java/io/generation => annotation-processor/src/main/java/io/clientcore}/tools/codegen/models/TemplateInput.java (97%) rename sdk/clientcore/tools/{sdk-codegen-tool/src/main/java/io/generation => annotation-processor/src/main/java/io/clientcore}/tools/codegen/templating/JavaPoetTemplateProcessor.java (98%) rename sdk/clientcore/tools/{sdk-codegen-tool/src/main/java/io/generation => annotation-processor/src/main/java/io/clientcore}/tools/codegen/templating/TemplateProcessor.java (58%) rename sdk/clientcore/tools/{sdk-codegen-tool/src/main/java/io/generation => annotation-processor/src/main/java/io/clientcore}/tools/codegen/utils/PathBuilder.java (91%) rename sdk/clientcore/tools/{sdk-codegen-tool/src/main/java/io/generation => annotation-processor/src/main/java/io/clientcore}/tools/codegen/utils/ResponseBodyModeGeneration.java (92%) create mode 100644 sdk/clientcore/tools/annotation-processor/src/main/resources/META-INF/maven/services/javax.annotation.processing.Processor rename sdk/clientcore/tools/{sdk-codegen-tool/src/test/java/io/generation => annotation-processor/src/test/java/io/clientcore}/tools/codegen/models/TemplateInputTest.java (98%) rename sdk/clientcore/tools/{sdk-codegen-tool/src/test/java/io/generation => annotation-processor/src/test/java/io/clientcore}/tools/codegen/templating/APIGenerationTest.java (90%) rename sdk/clientcore/tools/{sdk-codegen-tool/src/test/java/io/generation => annotation-processor/src/test/java/io/clientcore}/tools/codegen/templating/BodyContentTypeProcessorTest.java (98%) rename sdk/clientcore/tools/{sdk-codegen-tool/src/test/java/io/generation => annotation-processor/src/test/java/io/clientcore}/tools/codegen/templating/HttpPipelineBuilderMethodTest.java (98%) rename sdk/clientcore/tools/{sdk-codegen-tool/src/test/java/io/generation => annotation-processor/src/test/java/io/clientcore}/tools/codegen/utils/PathBuilderTest.java (98%) rename sdk/clientcore/tools/{sdk-codegen-tool/src/test/java/io/generation => annotation-processor/src/test/java/io/clientcore}/tools/codegen/utils/ResponseBodyModeGenerationTest.java (97%) delete mode 100644 sdk/clientcore/tools/sdk-codegen-tool/src/main/resources/META-INF/maven/services/javax.annotation.processing.Processor diff --git a/eng/versioning/external_dependencies.txt b/eng/versioning/external_dependencies.txt index f412192ab5bfe..c51e423d592ad 100644 --- a/eng/versioning/external_dependencies.txt +++ b/eng/versioning/external_dependencies.txt @@ -56,7 +56,7 @@ com.squareup.okhttp3:okhttp;4.12.0 com.squareup:javapoet;1.13.0 commons-codec:commons-codec;1.15 commons-net:commons-net;3.9.0 -io.clientcore.tools:sdk-codegen-tool;1.0.0-beta.1 +io.clientcore.tools:annotation-processor;1.0.0-beta.1 io.cloudevents:cloudevents-api;2.2.0 io.cloudevents:cloudevents-core;2.2.0 io.fabric8:kubernetes-client;6.12.1 diff --git a/sdk/clientcore/tools/sdk-codegen-tool/README.md b/sdk/clientcore/tools/annotation-processor/README.md similarity index 89% rename from sdk/clientcore/tools/sdk-codegen-tool/README.md rename to sdk/clientcore/tools/annotation-processor/README.md index f3d6386edb4b5..38a01d4ff61b5 100644 --- a/sdk/clientcore/tools/sdk-codegen-tool/README.md +++ b/sdk/clientcore/tools/annotation-processor/README.md @@ -1,6 +1,6 @@ -# Codegen Compile time generator Plugin +# Client Core Compile-Time Annotation Processor -A Java annotation processor for generating HTTP service implementations based on annotated interfaces. +The client-core annotation processor for introducing compile-time code generation for libraries based on client core ## Usage @@ -9,8 +9,8 @@ A Java annotation processor for generating HTTP service implementations based on io.clientcore.tools - sdk-codegen-tool - 1.0.0.beta.1 + annotation-processor + 1.0.0.beta.1 provided @@ -56,12 +56,12 @@ A Java annotation processor for generating HTTP service implementations based on private final String endpoint; - private final OpenAIServiceVersion serviceVersion; + private final ExampleServiceVersion serviceVersion; private String apiVersion; - public OpenAIClientServiceImpl(HttpPipeline defaultPipeline, ObjectSerializer serializer, - String endpoint, OpenAIServiceVersion serviceVersion) { + public ExampleServiceImpl (HttpPipeline defaultPipeline, ObjectSerializer serializer, + String endpoint, ExampleServiceVersion serviceVersion) { this.defaultPipeline = defaultPipeline; this.serializer = serializer; this.endpoint = endpoint; @@ -77,7 +77,7 @@ A Java annotation processor for generating HTTP service implementations based on return defaultPipeline; } - public OpenAIServiceVersion getServiceVersion() { + public ExampleServiceVersion getServiceVersion() { return serviceVersion; } diff --git a/sdk/clientcore/tools/sdk-codegen-tool/pom.xml b/sdk/clientcore/tools/annotation-processor/pom.xml similarity index 91% rename from sdk/clientcore/tools/sdk-codegen-tool/pom.xml rename to sdk/clientcore/tools/annotation-processor/pom.xml index c4a8ef4812092..f835b22673470 100644 --- a/sdk/clientcore/tools/sdk-codegen-tool/pom.xml +++ b/sdk/clientcore/tools/annotation-processor/pom.xml @@ -6,11 +6,11 @@ 4.0.0 io.clientcore.tools - sdk-codegen-tool - 1.0.0-beta.1 + annotation-processor + 1.0.0-beta.1 - Clientcore codegen tool - A Java annotation processor tool for generating HTTP service implementations + Client Core Compile-Time Annotation Processor + The client-core annotation processor for introducing compile-time code generation for libraries based on client core diff --git a/sdk/clientcore/tools/sdk-codegen-tool/src/main/java/io/generation/tools/codegen/AnnotationProcessor.java b/sdk/clientcore/tools/annotation-processor/src/main/java/io/clientcore/tools/codegen/AnnotationProcessor.java similarity index 95% rename from sdk/clientcore/tools/sdk-codegen-tool/src/main/java/io/generation/tools/codegen/AnnotationProcessor.java rename to sdk/clientcore/tools/annotation-processor/src/main/java/io/clientcore/tools/codegen/AnnotationProcessor.java index f36a0bf3031be..d02c8e92c94db 100644 --- a/sdk/clientcore/tools/sdk-codegen-tool/src/main/java/io/generation/tools/codegen/AnnotationProcessor.java +++ b/sdk/clientcore/tools/annotation-processor/src/main/java/io/clientcore/tools/codegen/AnnotationProcessor.java @@ -1,10 +1,13 @@ -package io.generation.tools.codegen; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. -import io.generation.tools.codegen.models.HttpRequestContext; -import io.generation.tools.codegen.models.Substitution; -import io.generation.tools.codegen.models.TemplateInput; -import io.generation.tools.codegen.templating.TemplateProcessor; -import io.generation.tools.codegen.utils.PathBuilder; +package io.clientcore.tools.codegen; + +import io.clientcore.tools.codegen.models.HttpRequestContext; +import io.clientcore.tools.codegen.models.Substitution; +import io.clientcore.tools.codegen.models.TemplateInput; +import io.clientcore.tools.codegen.templating.TemplateProcessor; +import io.clientcore.tools.codegen.utils.PathBuilder; import io.clientcore.core.annotation.ServiceInterface; import io.clientcore.core.http.annotation.BodyParam; import io.clientcore.core.http.annotation.HeaderParam; diff --git a/sdk/clientcore/tools/sdk-codegen-tool/src/main/java/io/generation/tools/codegen/exceptions/MissingSubstitutionException.java b/sdk/clientcore/tools/annotation-processor/src/main/java/io/clientcore/tools/codegen/exceptions/MissingSubstitutionException.java similarity index 70% rename from sdk/clientcore/tools/sdk-codegen-tool/src/main/java/io/generation/tools/codegen/exceptions/MissingSubstitutionException.java rename to sdk/clientcore/tools/annotation-processor/src/main/java/io/clientcore/tools/codegen/exceptions/MissingSubstitutionException.java index a836d33f0814e..112b101e95705 100644 --- a/sdk/clientcore/tools/sdk-codegen-tool/src/main/java/io/generation/tools/codegen/exceptions/MissingSubstitutionException.java +++ b/sdk/clientcore/tools/annotation-processor/src/main/java/io/clientcore/tools/codegen/exceptions/MissingSubstitutionException.java @@ -1,4 +1,7 @@ -package io.generation.tools.codegen.exceptions; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package io.clientcore.tools.codegen.exceptions; /** * Exception thrown when a substitution is missing from the template. diff --git a/sdk/clientcore/tools/sdk-codegen-tool/src/main/java/io/generation/tools/codegen/models/HttpRequestContext.java b/sdk/clientcore/tools/annotation-processor/src/main/java/io/clientcore/tools/codegen/models/HttpRequestContext.java similarity index 97% rename from sdk/clientcore/tools/sdk-codegen-tool/src/main/java/io/generation/tools/codegen/models/HttpRequestContext.java rename to sdk/clientcore/tools/annotation-processor/src/main/java/io/clientcore/tools/codegen/models/HttpRequestContext.java index 9fd8e29b2bb95..d6d18b110d6a2 100644 --- a/sdk/clientcore/tools/sdk-codegen-tool/src/main/java/io/generation/tools/codegen/models/HttpRequestContext.java +++ b/sdk/clientcore/tools/annotation-processor/src/main/java/io/clientcore/tools/codegen/models/HttpRequestContext.java @@ -1,4 +1,7 @@ -package io.generation.tools.codegen.models; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package io.clientcore.tools.codegen.models; import io.clientcore.core.http.models.HttpMethod; @@ -33,7 +36,7 @@ public final class HttpRequestContext { private final Map headers; private final Map queryParams; - private final Map substitutions; + private final Map substitutions; private int[] expectedStatusCodes; diff --git a/sdk/clientcore/tools/sdk-codegen-tool/src/main/java/io/generation/tools/codegen/models/Substitution.java b/sdk/clientcore/tools/annotation-processor/src/main/java/io/clientcore/tools/codegen/models/Substitution.java similarity index 93% rename from sdk/clientcore/tools/sdk-codegen-tool/src/main/java/io/generation/tools/codegen/models/Substitution.java rename to sdk/clientcore/tools/annotation-processor/src/main/java/io/clientcore/tools/codegen/models/Substitution.java index 7515e98cc5680..adb282d06ed98 100644 --- a/sdk/clientcore/tools/sdk-codegen-tool/src/main/java/io/generation/tools/codegen/models/Substitution.java +++ b/sdk/clientcore/tools/annotation-processor/src/main/java/io/clientcore/tools/codegen/models/Substitution.java @@ -1,4 +1,7 @@ -package io.generation.tools.codegen.models; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package io.clientcore.tools.codegen.models; /** * A Substitution is a value that can be used to replace placeholder values in a URL. Placeholders look like: diff --git a/sdk/clientcore/tools/sdk-codegen-tool/src/main/java/io/generation/tools/codegen/models/TemplateInput.java b/sdk/clientcore/tools/annotation-processor/src/main/java/io/clientcore/tools/codegen/models/TemplateInput.java similarity index 97% rename from sdk/clientcore/tools/sdk-codegen-tool/src/main/java/io/generation/tools/codegen/models/TemplateInput.java rename to sdk/clientcore/tools/annotation-processor/src/main/java/io/clientcore/tools/codegen/models/TemplateInput.java index 6cb2ae4673645..b40d9c0b0aa6b 100644 --- a/sdk/clientcore/tools/sdk-codegen-tool/src/main/java/io/generation/tools/codegen/models/TemplateInput.java +++ b/sdk/clientcore/tools/annotation-processor/src/main/java/io/clientcore/tools/codegen/models/TemplateInput.java @@ -1,4 +1,7 @@ -package io.generation.tools.codegen.models; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package io.clientcore.tools.codegen.models; import io.clientcore.core.http.annotation.UnexpectedResponseExceptionDetail; import javax.lang.model.element.TypeElement; diff --git a/sdk/clientcore/tools/sdk-codegen-tool/src/main/java/io/generation/tools/codegen/templating/JavaPoetTemplateProcessor.java b/sdk/clientcore/tools/annotation-processor/src/main/java/io/clientcore/tools/codegen/templating/JavaPoetTemplateProcessor.java similarity index 98% rename from sdk/clientcore/tools/sdk-codegen-tool/src/main/java/io/generation/tools/codegen/templating/JavaPoetTemplateProcessor.java rename to sdk/clientcore/tools/annotation-processor/src/main/java/io/clientcore/tools/codegen/templating/JavaPoetTemplateProcessor.java index c2a360245d38e..129fc54adf159 100644 --- a/sdk/clientcore/tools/sdk-codegen-tool/src/main/java/io/generation/tools/codegen/templating/JavaPoetTemplateProcessor.java +++ b/sdk/clientcore/tools/annotation-processor/src/main/java/io/clientcore/tools/codegen/templating/JavaPoetTemplateProcessor.java @@ -1,8 +1,11 @@ -package io.generation.tools.codegen.templating; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. -import io.generation.tools.codegen.models.HttpRequestContext; -import io.generation.tools.codegen.models.TemplateInput; -import io.generation.tools.codegen.utils.ResponseBodyModeGeneration; +package io.clientcore.tools.codegen.templating; + +import io.clientcore.tools.codegen.models.HttpRequestContext; +import io.clientcore.tools.codegen.models.TemplateInput; +import io.clientcore.tools.codegen.utils.ResponseBodyModeGeneration; import com.squareup.javapoet.ClassName; import com.squareup.javapoet.FieldSpec; import com.squareup.javapoet.JavaFile; diff --git a/sdk/clientcore/tools/sdk-codegen-tool/src/main/java/io/generation/tools/codegen/templating/TemplateProcessor.java b/sdk/clientcore/tools/annotation-processor/src/main/java/io/clientcore/tools/codegen/templating/TemplateProcessor.java similarity index 58% rename from sdk/clientcore/tools/sdk-codegen-tool/src/main/java/io/generation/tools/codegen/templating/TemplateProcessor.java rename to sdk/clientcore/tools/annotation-processor/src/main/java/io/clientcore/tools/codegen/templating/TemplateProcessor.java index 68d9d8cbc2469..cb15935bffeba 100644 --- a/sdk/clientcore/tools/sdk-codegen-tool/src/main/java/io/generation/tools/codegen/templating/TemplateProcessor.java +++ b/sdk/clientcore/tools/annotation-processor/src/main/java/io/clientcore/tools/codegen/templating/TemplateProcessor.java @@ -1,6 +1,9 @@ -package io.generation.tools.codegen.templating; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. -import io.generation.tools.codegen.models.TemplateInput; +package io.clientcore.tools.codegen.templating; + +import io.clientcore.tools.codegen.models.TemplateInput; import javax.annotation.processing.ProcessingEnvironment; diff --git a/sdk/clientcore/tools/sdk-codegen-tool/src/main/java/io/generation/tools/codegen/utils/PathBuilder.java b/sdk/clientcore/tools/annotation-processor/src/main/java/io/clientcore/tools/codegen/utils/PathBuilder.java similarity index 91% rename from sdk/clientcore/tools/sdk-codegen-tool/src/main/java/io/generation/tools/codegen/utils/PathBuilder.java rename to sdk/clientcore/tools/annotation-processor/src/main/java/io/clientcore/tools/codegen/utils/PathBuilder.java index 15d3b73dc1b67..749bad3daa738 100644 --- a/sdk/clientcore/tools/sdk-codegen-tool/src/main/java/io/generation/tools/codegen/utils/PathBuilder.java +++ b/sdk/clientcore/tools/annotation-processor/src/main/java/io/clientcore/tools/codegen/utils/PathBuilder.java @@ -1,8 +1,11 @@ -package io.generation.tools.codegen.utils; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. -import io.generation.tools.codegen.exceptions.MissingSubstitutionException; -import io.generation.tools.codegen.models.HttpRequestContext; -import io.generation.tools.codegen.models.Substitution; +package io.clientcore.tools.codegen.utils; + +import io.clientcore.tools.codegen.exceptions.MissingSubstitutionException; +import io.clientcore.tools.codegen.models.HttpRequestContext; +import io.clientcore.tools.codegen.models.Substitution; import java.util.Map; import java.util.regex.Matcher; diff --git a/sdk/clientcore/tools/sdk-codegen-tool/src/main/java/io/generation/tools/codegen/utils/ResponseBodyModeGeneration.java b/sdk/clientcore/tools/annotation-processor/src/main/java/io/clientcore/tools/codegen/utils/ResponseBodyModeGeneration.java similarity index 92% rename from sdk/clientcore/tools/sdk-codegen-tool/src/main/java/io/generation/tools/codegen/utils/ResponseBodyModeGeneration.java rename to sdk/clientcore/tools/annotation-processor/src/main/java/io/clientcore/tools/codegen/utils/ResponseBodyModeGeneration.java index 2372c578a5d67..187f4d57ce91c 100644 --- a/sdk/clientcore/tools/sdk-codegen-tool/src/main/java/io/generation/tools/codegen/utils/ResponseBodyModeGeneration.java +++ b/sdk/clientcore/tools/annotation-processor/src/main/java/io/clientcore/tools/codegen/utils/ResponseBodyModeGeneration.java @@ -1,4 +1,7 @@ -package io.generation.tools.codegen.utils; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package io.clientcore.tools.codegen.utils; import com.squareup.javapoet.MethodSpec; import io.clientcore.core.http.models.HttpResponse; diff --git a/sdk/clientcore/tools/annotation-processor/src/main/resources/META-INF/maven/services/javax.annotation.processing.Processor b/sdk/clientcore/tools/annotation-processor/src/main/resources/META-INF/maven/services/javax.annotation.processing.Processor new file mode 100644 index 0000000000000..7966f81cda3f7 --- /dev/null +++ b/sdk/clientcore/tools/annotation-processor/src/main/resources/META-INF/maven/services/javax.annotation.processing.Processor @@ -0,0 +1 @@ +io.clientcore.tools.codegen.AnnotationProcessor diff --git a/sdk/clientcore/tools/sdk-codegen-tool/src/test/java/io/generation/tools/codegen/models/TemplateInputTest.java b/sdk/clientcore/tools/annotation-processor/src/test/java/io/clientcore/tools/codegen/models/TemplateInputTest.java similarity index 98% rename from sdk/clientcore/tools/sdk-codegen-tool/src/test/java/io/generation/tools/codegen/models/TemplateInputTest.java rename to sdk/clientcore/tools/annotation-processor/src/test/java/io/clientcore/tools/codegen/models/TemplateInputTest.java index 38aa4774b2fd7..5deedb7b0ba62 100644 --- a/sdk/clientcore/tools/sdk-codegen-tool/src/test/java/io/generation/tools/codegen/models/TemplateInputTest.java +++ b/sdk/clientcore/tools/annotation-processor/src/test/java/io/clientcore/tools/codegen/models/TemplateInputTest.java @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -package io.generation.tools.codegen.models; +package io.clientcore.tools.codegen.models; import io.clientcore.core.http.annotation.UnexpectedResponseExceptionDetail; import java.util.Collections; diff --git a/sdk/clientcore/tools/sdk-codegen-tool/src/test/java/io/generation/tools/codegen/templating/APIGenerationTest.java b/sdk/clientcore/tools/annotation-processor/src/test/java/io/clientcore/tools/codegen/templating/APIGenerationTest.java similarity index 90% rename from sdk/clientcore/tools/sdk-codegen-tool/src/test/java/io/generation/tools/codegen/templating/APIGenerationTest.java rename to sdk/clientcore/tools/annotation-processor/src/test/java/io/clientcore/tools/codegen/templating/APIGenerationTest.java index 3e39698a19c65..ebd90fbc74d71 100644 --- a/sdk/clientcore/tools/sdk-codegen-tool/src/test/java/io/generation/tools/codegen/templating/APIGenerationTest.java +++ b/sdk/clientcore/tools/annotation-processor/src/test/java/io/clientcore/tools/codegen/templating/APIGenerationTest.java @@ -1,11 +1,11 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -package io.generation.tools.codegen.templating; +package io.clientcore.tools.codegen.templating; -import io.generation.tools.codegen.models.HttpRequestContext; -import io.generation.tools.codegen.models.Substitution; -import io.generation.tools.codegen.models.TemplateInput; +import io.clientcore.tools.codegen.models.HttpRequestContext; +import io.clientcore.tools.codegen.models.Substitution; +import io.clientcore.tools.codegen.models.TemplateInput; import com.squareup.javapoet.MethodSpec; import io.clientcore.core.http.models.HttpMethod; import java.util.Collections; diff --git a/sdk/clientcore/tools/sdk-codegen-tool/src/test/java/io/generation/tools/codegen/templating/BodyContentTypeProcessorTest.java b/sdk/clientcore/tools/annotation-processor/src/test/java/io/clientcore/tools/codegen/templating/BodyContentTypeProcessorTest.java similarity index 98% rename from sdk/clientcore/tools/sdk-codegen-tool/src/test/java/io/generation/tools/codegen/templating/BodyContentTypeProcessorTest.java rename to sdk/clientcore/tools/annotation-processor/src/test/java/io/clientcore/tools/codegen/templating/BodyContentTypeProcessorTest.java index e44923dd6ae0e..d2efb5dcb83d7 100644 --- a/sdk/clientcore/tools/sdk-codegen-tool/src/test/java/io/generation/tools/codegen/templating/BodyContentTypeProcessorTest.java +++ b/sdk/clientcore/tools/annotation-processor/src/test/java/io/clientcore/tools/codegen/templating/BodyContentTypeProcessorTest.java @@ -1,9 +1,9 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -package io.generation.tools.codegen.templating; +package io.clientcore.tools.codegen.templating; -import io.generation.tools.codegen.models.HttpRequestContext; +import io.clientcore.tools.codegen.models.HttpRequestContext; import com.squareup.javapoet.MethodSpec; import java.util.stream.Stream; import org.junit.jupiter.api.BeforeEach; diff --git a/sdk/clientcore/tools/sdk-codegen-tool/src/test/java/io/generation/tools/codegen/templating/HttpPipelineBuilderMethodTest.java b/sdk/clientcore/tools/annotation-processor/src/test/java/io/clientcore/tools/codegen/templating/HttpPipelineBuilderMethodTest.java similarity index 98% rename from sdk/clientcore/tools/sdk-codegen-tool/src/test/java/io/generation/tools/codegen/templating/HttpPipelineBuilderMethodTest.java rename to sdk/clientcore/tools/annotation-processor/src/test/java/io/clientcore/tools/codegen/templating/HttpPipelineBuilderMethodTest.java index 2ad387d509eb6..857b221106e8d 100644 --- a/sdk/clientcore/tools/sdk-codegen-tool/src/test/java/io/generation/tools/codegen/templating/HttpPipelineBuilderMethodTest.java +++ b/sdk/clientcore/tools/annotation-processor/src/test/java/io/clientcore/tools/codegen/templating/HttpPipelineBuilderMethodTest.java @@ -1,9 +1,9 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -package io.generation.tools.codegen.templating; +package io.clientcore.tools.codegen.templating; -import io.generation.tools.codegen.models.TemplateInput; +import io.clientcore.tools.codegen.models.TemplateInput; import com.squareup.javapoet.ClassName; import com.squareup.javapoet.FieldSpec; import com.squareup.javapoet.MethodSpec; diff --git a/sdk/clientcore/tools/sdk-codegen-tool/src/test/java/io/generation/tools/codegen/utils/PathBuilderTest.java b/sdk/clientcore/tools/annotation-processor/src/test/java/io/clientcore/tools/codegen/utils/PathBuilderTest.java similarity index 98% rename from sdk/clientcore/tools/sdk-codegen-tool/src/test/java/io/generation/tools/codegen/utils/PathBuilderTest.java rename to sdk/clientcore/tools/annotation-processor/src/test/java/io/clientcore/tools/codegen/utils/PathBuilderTest.java index efcdf7d489f57..064400b6727e5 100644 --- a/sdk/clientcore/tools/sdk-codegen-tool/src/test/java/io/generation/tools/codegen/utils/PathBuilderTest.java +++ b/sdk/clientcore/tools/annotation-processor/src/test/java/io/clientcore/tools/codegen/utils/PathBuilderTest.java @@ -1,11 +1,11 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -package io.generation.tools.codegen.utils; +package io.clientcore.tools.codegen.utils; -import io.generation.tools.codegen.exceptions.MissingSubstitutionException; -import io.generation.tools.codegen.models.HttpRequestContext; -import io.generation.tools.codegen.models.Substitution; +import io.clientcore.tools.codegen.exceptions.MissingSubstitutionException; +import io.clientcore.tools.codegen.models.HttpRequestContext; +import io.clientcore.tools.codegen.models.Substitution; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; diff --git a/sdk/clientcore/tools/sdk-codegen-tool/src/test/java/io/generation/tools/codegen/utils/ResponseBodyModeGenerationTest.java b/sdk/clientcore/tools/annotation-processor/src/test/java/io/clientcore/tools/codegen/utils/ResponseBodyModeGenerationTest.java similarity index 97% rename from sdk/clientcore/tools/sdk-codegen-tool/src/test/java/io/generation/tools/codegen/utils/ResponseBodyModeGenerationTest.java rename to sdk/clientcore/tools/annotation-processor/src/test/java/io/clientcore/tools/codegen/utils/ResponseBodyModeGenerationTest.java index 6dd997a056e4e..418cfc6edcbc0 100644 --- a/sdk/clientcore/tools/sdk-codegen-tool/src/test/java/io/generation/tools/codegen/utils/ResponseBodyModeGenerationTest.java +++ b/sdk/clientcore/tools/annotation-processor/src/test/java/io/clientcore/tools/codegen/utils/ResponseBodyModeGenerationTest.java @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -package io.generation.tools.codegen.utils; +package io.clientcore.tools.codegen.utils; import com.squareup.javapoet.MethodSpec; import org.junit.jupiter.api.BeforeEach; diff --git a/sdk/clientcore/tools/pom.xml b/sdk/clientcore/tools/pom.xml index b4ca842dc7971..c4a6300b10085 100644 --- a/sdk/clientcore/tools/pom.xml +++ b/sdk/clientcore/tools/pom.xml @@ -5,10 +5,10 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 io.clientcore - clientcore-tools-service + clientcore-tools-parent pom 1.0.0 - sdk-codegen-tool + annotation-processor diff --git a/sdk/clientcore/tools/sdk-codegen-tool/src/main/resources/META-INF/maven/services/javax.annotation.processing.Processor b/sdk/clientcore/tools/sdk-codegen-tool/src/main/resources/META-INF/maven/services/javax.annotation.processing.Processor deleted file mode 100644 index 94311acc04cdd..0000000000000 --- a/sdk/clientcore/tools/sdk-codegen-tool/src/main/resources/META-INF/maven/services/javax.annotation.processing.Processor +++ /dev/null @@ -1 +0,0 @@ -io.generation.tools.codegen.AnnotationProcessor From ddf15fba84837cc545d66f51252ba48d3f80951e Mon Sep 17 00:00:00 2001 From: "Sameeksha Vaity (from Dev Box)" Date: Thu, 19 Dec 2024 15:01:41 -0800 Subject: [PATCH 3/5] update for unknown words --- .vscode/cspell.json | 1 + 1 file changed, 1 insertion(+) diff --git a/.vscode/cspell.json b/.vscode/cspell.json index 85b3c51666611..a047849643896 100644 --- a/.vscode/cspell.json +++ b/.vscode/cspell.json @@ -203,6 +203,7 @@ "sdk/loadtesting/azure-developer-loadtesting/**", "sdk/clientcore/core/**", "sdk/clientcore/http-okhttp3/**", + "sdk/clientcore/tools/**", "sdk/serialization/azure-json-gson/**", "sdk/serialization/azure-json/**", "sdk/serialization/azure-xml/**", From a6ed31396b9a68f15356052e469ef114bc12a997 Mon Sep 17 00:00:00 2001 From: "Sameeksha Vaity (from Dev Box)" Date: Thu, 19 Dec 2024 15:43:07 -0800 Subject: [PATCH 4/5] skip for readme --- eng/.docsettings.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/eng/.docsettings.yml b/eng/.docsettings.yml index 12064f887d6a5..caf3118679b07 100644 --- a/eng/.docsettings.yml +++ b/eng/.docsettings.yml @@ -108,6 +108,7 @@ known_content_issues: - ['sdk/clientcore/README.md', '#3113'] - ['sdk/clientcore/core/README.md', '#3113'] - ['sdk/clientcore/http-okhttp3/README.md', '#3113'] + - ['sdk/clientcore/tools/annotation-processor/README.md', '#3113'] - ['sdk/core/azure-core-experimental/README.md', '#3113'] - ['sdk/cosmos/faq/README.md', '#3113'] - ['sdk/cosmos/azure-cosmos-benchmark/README.md', '#3113'] From cdea14e8720823730727e14f59e02346f72ec5c4 Mon Sep 17 00:00:00 2001 From: "Sameeksha Vaity (from Dev Box)" Date: Fri, 20 Dec 2024 09:56:43 -0800 Subject: [PATCH 5/5] add readme note --- sdk/clientcore/tools/annotation-processor/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/sdk/clientcore/tools/annotation-processor/README.md b/sdk/clientcore/tools/annotation-processor/README.md index 38a01d4ff61b5..e677bb0e2b3db 100644 --- a/sdk/clientcore/tools/annotation-processor/README.md +++ b/sdk/clientcore/tools/annotation-processor/README.md @@ -1,6 +1,7 @@ # Client Core Compile-Time Annotation Processor The client-core annotation processor for introducing compile-time code generation for libraries based on client core +>Note: This project is for experimentation and exploring new ideas that may or may not make it into a supported GA release. ## Usage