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/**",
diff --git a/eng/.docsettings.yml b/eng/.docsettings.yml
index c8846a66660b4..08395509fd39b 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/clientcore/optional-dependency-tests/README.md', '#3113']
- ['sdk/core/azure-core-experimental/README.md', '#3113']
- ['sdk/cosmos/faq/README.md', '#3113']
diff --git a/eng/versioning/external_dependencies.txt b/eng/versioning/external_dependencies.txt
index d8870096d2519..a15204e1903da 100644
--- a/eng/versioning/external_dependencies.txt
+++ b/eng/versioning/external_dependencies.txt
@@ -40,8 +40,10 @@ com.microsoft.azure:azure-functions-maven-plugin;1.30.0
com.microsoft.azure.functions:azure-functions-java-library;2.2.0
com.mysql:mysql-connector-j;9.0.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: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/pom.xml b/sdk/clientcore/pom.xml
index 7cb48778cd93d..252a22840d50d 100644
--- a/sdk/clientcore/pom.xml
+++ b/sdk/clientcore/pom.xml
@@ -14,5 +14,6 @@
http-okhttp3
optional-dependency-tests
http-stress
+ tools
diff --git a/sdk/clientcore/tools/annotation-processor/README.md b/sdk/clientcore/tools/annotation-processor/README.md
new file mode 100644
index 0000000000000..e677bb0e2b3db
--- /dev/null
+++ b/sdk/clientcore/tools/annotation-processor/README.md
@@ -0,0 +1,136 @@
+# 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
+
+1. Add the plugin dependency:
+ ```xml
+
+
+ io.clientcore.tools
+ annotation-processor
+ 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 ExampleServiceVersion serviceVersion;
+
+ private String apiVersion;
+
+ public ExampleServiceImpl (HttpPipeline defaultPipeline, ObjectSerializer serializer,
+ String endpoint, ExampleServiceVersion 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 ExampleServiceVersion 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/annotation-processor/pom.xml b/sdk/clientcore/tools/annotation-processor/pom.xml
new file mode 100644
index 0000000000000..f835b22673470
--- /dev/null
+++ b/sdk/clientcore/tools/annotation-processor/pom.xml
@@ -0,0 +1,112 @@
+
+
+
+ 4.0.0
+
+ io.clientcore.tools
+ annotation-processor
+ 1.0.0-beta.1
+
+ Client Core Compile-Time Annotation Processor
+ The client-core annotation processor for introducing compile-time code generation for libraries based on client core
+
+
+
+ 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/annotation-processor/src/main/java/io/clientcore/tools/codegen/AnnotationProcessor.java b/sdk/clientcore/tools/annotation-processor/src/main/java/io/clientcore/tools/codegen/AnnotationProcessor.java
new file mode 100644
index 0000000000000..b4b28fba71cf4
--- /dev/null
+++ b/sdk/clientcore/tools/annotation-processor/src/main/java/io/clientcore/tools/codegen/AnnotationProcessor.java
@@ -0,0 +1,205 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package io.clientcore.tools.codegen;
+
+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 io.clientcore.core.http.models.HttpHeaderName;
+import io.clientcore.core.http.models.HttpHeaders;
+import io.clientcore.core.http.models.HttpMethod;
+import io.clientcore.core.http.models.HttpRequest;
+import io.clientcore.core.http.models.HttpResponse;
+import io.clientcore.core.http.models.Response;
+import io.clientcore.core.http.pipeline.HttpPipeline;
+import io.clientcore.core.util.Context;
+import io.clientcore.core.util.binarydata.BinaryData;
+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 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 javax.lang.model.element.VariableElement;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+@SupportedAnnotationTypes("io.clientcore.core.annotation.*")
+@SupportedSourceVersion(SourceVersion.RELEASE_8)
+public class AnnotationProcessor extends AbstractProcessor {
+
+ @Override
+ public boolean process(Set extends TypeElement> 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) {
+ if (serviceInterface == null || serviceInterface.getKind() != ElementKind.INTERFACE) {
+ throw new IllegalArgumentException("Invalid service interface provided.");
+ }
+
+ TemplateInput templateInput = new TemplateInput();
+
+ // Determine the fully qualified name (FQN) and package name
+ final String serviceInterfaceFQN = serviceInterface.asType().toString();
+ int lastDot = serviceInterfaceFQN.lastIndexOf('.');
+ String packageName = (lastDot > 0) ? serviceInterfaceFQN.substring(0, lastDot) : "default.generated";
+
+ 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 the ServiceInterface.host() value from annotations
+ ServiceInterface annotation = serviceInterface.getAnnotation(ServiceInterface.class);
+ if (annotation != null && annotation.host() != null) {
+ templateInput.setHost(annotation.host());
+ }
+
+ // Add all required imports
+ addImports(templateInput);
+
+ // Collect methods annotated with @HttpRequestInformation
+ List httpRequestMethods = serviceInterface.getEnclosedElements().stream()
+ .filter(element -> element.getKind() == ElementKind.METHOD)
+ .filter(element -> element.getAnnotation(HttpRequestInformation.class) != null)
+ .map(ExecutableElement.class::cast)
+ .collect(Collectors.toList());
+
+ // Generate HTTP request contexts
+ templateInput.setHttpRequestContexts(httpRequestMethods.stream()
+ .map(e -> createHttpRequestContext(e, templateInput))
+ .filter(Objects::nonNull) // Exclude null contexts
+ .collect(Collectors.toList()));
+
+ // Set UnexpectedResponseExceptionDetails
+ templateInput.setUnexpectedResponseExceptionDetails(httpRequestMethods.stream()
+ .map(e -> e.getAnnotation(UnexpectedResponseExceptionDetail.class))
+ .filter(Objects::nonNull) // Exclude null annotations
+ .collect(Collectors.toList()));
+
+ // Process the template
+ TemplateProcessor.getInstance().process(templateInput, processingEnv);
+
+ // Additional formatting or logging if necessary
+ }
+
+ private void addImports(TemplateInput templateInput) {
+ templateInput.addImport(Context.class.getName());
+ templateInput.addImport(BinaryData.class.getName());
+ templateInput.addImport(HttpHeaders.class.getName());
+ templateInput.addImport(HttpPipeline.class.getName());
+ templateInput.addImport(HttpHeaderName.class.getName());
+ templateInput.addImport(HttpMethod.class.getName());
+ templateInput.addImport(HttpResponse.class.getName());
+ templateInput.addImport(HttpRequest.class.getName());
+ templateInput.addImport(Response.class.getName());
+ templateInput.addImport(Map.class.getName());
+ templateInput.addImport(HashMap.class.getName());
+ templateInput.addImport(Arrays.class.getName());
+ templateInput.addImport(Void.class.getName());
+ templateInput.addImport(List.class.getName());
+ }
+
+ private HttpRequestContext createHttpRequestContext(ExecutableElement requestMethod, TemplateInput templateInput) {
+ HttpRequestContext method = new HttpRequestContext();
+ method.setHost(templateInput.getHost());
+ method.setMethodName(requestMethod.getSimpleName().toString());
+
+ // Extract @HttpRequestInformation annotation details
+ final HttpRequestInformation httpRequestInfo = requestMethod.getAnnotation(HttpRequestInformation.class);
+ method.setPath(httpRequestInfo.path());
+ method.setHttpMethod(httpRequestInfo.method());
+ method.setExpectedStatusCodes(httpRequestInfo.expectedStatusCodes());
+
+ // Add return type as an import
+ String returnTypeShortName = templateInput.addImport(requestMethod.getReturnType());
+ method.setMethodReturnType(returnTypeShortName);
+
+ // Process parameters
+ for (VariableElement param : requestMethod.getParameters()) {
+ // Cache annotations for each parameter
+ 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);
+
+ // Switch based on annotations
+ if (hostParam != null) {
+ method.addSubstitution(new Substitution(
+ hostParam.value(),
+ param.getSimpleName().toString(),
+ hostParam.encoded()));
+ } else if (pathParam != null) {
+ 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) {
+ method.addQueryParam(queryParam.value(), param.getSimpleName().toString());
+ // TODO: Add support for multipleQueryParams and encoded handling
+ } else if (bodyParam != null) {
+ method.setBody(new HttpRequestContext.Body(
+ bodyParam.value(),
+ param.asType().toString(),
+ param.getSimpleName().toString()));
+ }
+
+ // Add parameter details to method context
+ String shortParamName = templateInput.addImport(param.asType());
+ method.addParameter(new HttpRequestContext.MethodParameter(param.asType(), shortParamName, param.getSimpleName().toString()));
+ }
+
+ // Pre-compute host substitutions
+ method.setHost(getHost(templateInput, method));
+
+ 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/annotation-processor/src/main/java/io/clientcore/tools/codegen/exceptions/MissingSubstitutionException.java b/sdk/clientcore/tools/annotation-processor/src/main/java/io/clientcore/tools/codegen/exceptions/MissingSubstitutionException.java
new file mode 100644
index 0000000000000..112b101e95705
--- /dev/null
+++ b/sdk/clientcore/tools/annotation-processor/src/main/java/io/clientcore/tools/codegen/exceptions/MissingSubstitutionException.java
@@ -0,0 +1,18 @@
+// 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.
+ */
+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/annotation-processor/src/main/java/io/clientcore/tools/codegen/models/HttpRequestContext.java b/sdk/clientcore/tools/annotation-processor/src/main/java/io/clientcore/tools/codegen/models/HttpRequestContext.java
new file mode 100644
index 0000000000000..d6d18b110d6a2
--- /dev/null
+++ b/sdk/clientcore/tools/annotation-processor/src/main/java/io/clientcore/tools/codegen/models/HttpRequestContext.java
@@ -0,0 +1,370 @@
+// 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;
+
+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/annotation-processor/src/main/java/io/clientcore/tools/codegen/models/Substitution.java b/sdk/clientcore/tools/annotation-processor/src/main/java/io/clientcore/tools/codegen/models/Substitution.java
new file mode 100644
index 0000000000000..adb282d06ed98
--- /dev/null
+++ b/sdk/clientcore/tools/annotation-processor/src/main/java/io/clientcore/tools/codegen/models/Substitution.java
@@ -0,0 +1,62 @@
+// 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:
+ * "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/annotation-processor/src/main/java/io/clientcore/tools/codegen/models/TemplateInput.java b/sdk/clientcore/tools/annotation-processor/src/main/java/io/clientcore/tools/codegen/models/TemplateInput.java
new file mode 100644
index 0000000000000..b40d9c0b0aa6b
--- /dev/null
+++ b/sdk/clientcore/tools/annotation-processor/src/main/java/io/clientcore/tools/codegen/models/TemplateInput.java
@@ -0,0 +1,225 @@
+// 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;
+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 extends TypeMirror> 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/annotation-processor/src/main/java/io/clientcore/tools/codegen/templating/JavaPoetTemplateProcessor.java b/sdk/clientcore/tools/annotation-processor/src/main/java/io/clientcore/tools/codegen/templating/JavaPoetTemplateProcessor.java
new file mode 100644
index 0000000000000..129fc54adf159
--- /dev/null
+++ b/sdk/clientcore/tools/annotation-processor/src/main/java/io/clientcore/tools/codegen/templating/JavaPoetTemplateProcessor.java
@@ -0,0 +1,511 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+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;
+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/annotation-processor/src/main/java/io/clientcore/tools/codegen/templating/TemplateProcessor.java b/sdk/clientcore/tools/annotation-processor/src/main/java/io/clientcore/tools/codegen/templating/TemplateProcessor.java
new file mode 100644
index 0000000000000..cb15935bffeba
--- /dev/null
+++ b/sdk/clientcore/tools/annotation-processor/src/main/java/io/clientcore/tools/codegen/templating/TemplateProcessor.java
@@ -0,0 +1,16 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package io.clientcore.tools.codegen.templating;
+
+import io.clientcore.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/annotation-processor/src/main/java/io/clientcore/tools/codegen/utils/PathBuilder.java b/sdk/clientcore/tools/annotation-processor/src/main/java/io/clientcore/tools/codegen/utils/PathBuilder.java
new file mode 100644
index 0000000000000..5877213caf671
--- /dev/null
+++ b/sdk/clientcore/tools/annotation-processor/src/main/java/io/clientcore/tools/codegen/utils/PathBuilder.java
@@ -0,0 +1,102 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+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.Objects;
+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");
+ }
+
+ boolean hasQueryParams = !method.getQueryParams().isEmpty();
+
+ // Pattern for substitution placeholders
+ 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) {
+ String substitutionValue = substitution.getParameterVariableName();
+ String replacementValue = substitutionValue != null
+ ? Objects.toString(substitutionValue, "null")
+ : "";
+
+ matcher.appendReplacement(buffer, "");
+ if (buffer.length() != 0) {
+ buffer.append("\" + ");
+ }
+ buffer.append(replacementValue).append(" + \"");
+ } else {
+ throw new MissingSubstitutionException("Could not find substitution for '" + paramName + "' in method '" + method.getMethodName() + "'");
+ }
+ }
+
+ matcher.appendTail(buffer);
+
+ if (hasQueryParams) {
+ buffer.append("?");
+
+ method.getQueryParams().forEach((key, value) -> {
+ if (key.isEmpty() || value.isEmpty()) {
+ throw new IllegalArgumentException("Query parameter key and value must not be empty");
+ }
+ buffer.append(key).append("=\" + ").append(Objects.toString(value, "null")).append(" + \"&");
+ });
+
+ // Remove the trailing '&'
+ buffer.setLength(buffer.length() - 1);
+ }
+
+ // Ensure the output is properly quoted
+ if (buffer.charAt(0) != '"' && !rawHost.startsWith("{")) {
+ buffer.insert(0, '"');
+ }
+ if (!hasQueryParams && buffer.charAt(buffer.length() - 1) != '"' && !rawHost.endsWith("}")) {
+ buffer.append('"');
+ }
+
+ // Clean unnecessary `+ ""` in the buffer
+ String result = buffer.toString().replaceAll(" \\+ \"\"", "");
+
+ // Remove trailing ' + ' if it exists
+ if (result.endsWith(" + ")) {
+ result = result.substring(0, result.length() - 3);
+ }
+
+ // Remove trailing ' + "' if it exists
+ if (result.endsWith(" + \"")) {
+ result = result.substring(0, result.length() - 4);
+ }
+
+ // Check for missing or incorrect braces
+ long openingBracesCount = rawHost.chars().filter(ch -> ch == '{').count();
+ long closingBracesCount = rawHost.chars().filter(ch -> ch == '}').count();
+
+ if (openingBracesCount != closingBracesCount) {
+ throw new MissingSubstitutionException("Mismatched braces in raw host: " + rawHost);
+ }
+
+ return result;
+ }
+}
diff --git a/sdk/clientcore/tools/annotation-processor/src/main/java/io/clientcore/tools/codegen/utils/ResponseBodyModeGeneration.java b/sdk/clientcore/tools/annotation-processor/src/main/java/io/clientcore/tools/codegen/utils/ResponseBodyModeGeneration.java
new file mode 100644
index 0000000000000..187f4d57ce91c
--- /dev/null
+++ b/sdk/clientcore/tools/annotation-processor/src/main/java/io/clientcore/tools/codegen/utils/ResponseBodyModeGeneration.java
@@ -0,0 +1,39 @@
+// 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;
+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/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/annotation-processor/src/test/java/io/clientcore/tools/codegen/models/TemplateInputTest.java b/sdk/clientcore/tools/annotation-processor/src/test/java/io/clientcore/tools/codegen/models/TemplateInputTest.java
new file mode 100644
index 0000000000000..5deedb7b0ba62
--- /dev/null
+++ b/sdk/clientcore/tools/annotation-processor/src/test/java/io/clientcore/tools/codegen/models/TemplateInputTest.java
@@ -0,0 +1,100 @@
+// 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 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/annotation-processor/src/test/java/io/clientcore/tools/codegen/templating/APIGenerationTest.java b/sdk/clientcore/tools/annotation-processor/src/test/java/io/clientcore/tools/codegen/templating/APIGenerationTest.java
new file mode 100644
index 0000000000000..ebd90fbc74d71
--- /dev/null
+++ b/sdk/clientcore/tools/annotation-processor/src/test/java/io/clientcore/tools/codegen/templating/APIGenerationTest.java
@@ -0,0 +1,60 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package io.clientcore.tools.codegen.templating;
+
+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;
+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/annotation-processor/src/test/java/io/clientcore/tools/codegen/templating/BodyContentTypeProcessorTest.java b/sdk/clientcore/tools/annotation-processor/src/test/java/io/clientcore/tools/codegen/templating/BodyContentTypeProcessorTest.java
new file mode 100644
index 0000000000000..d2efb5dcb83d7
--- /dev/null
+++ b/sdk/clientcore/tools/annotation-processor/src/test/java/io/clientcore/tools/codegen/templating/BodyContentTypeProcessorTest.java
@@ -0,0 +1,145 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package io.clientcore.tools.codegen.templating;
+
+import io.clientcore.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/annotation-processor/src/test/java/io/clientcore/tools/codegen/templating/HttpPipelineBuilderMethodTest.java b/sdk/clientcore/tools/annotation-processor/src/test/java/io/clientcore/tools/codegen/templating/HttpPipelineBuilderMethodTest.java
new file mode 100644
index 0000000000000..857b221106e8d
--- /dev/null
+++ b/sdk/clientcore/tools/annotation-processor/src/test/java/io/clientcore/tools/codegen/templating/HttpPipelineBuilderMethodTest.java
@@ -0,0 +1,124 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package io.clientcore.tools.codegen.templating;
+
+import io.clientcore.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/annotation-processor/src/test/java/io/clientcore/tools/codegen/utils/PathBuilderTest.java b/sdk/clientcore/tools/annotation-processor/src/test/java/io/clientcore/tools/codegen/utils/PathBuilderTest.java
new file mode 100644
index 0000000000000..21c4d9bd7d115
--- /dev/null
+++ b/sdk/clientcore/tools/annotation-processor/src/test/java/io/clientcore/tools/codegen/utils/PathBuilderTest.java
@@ -0,0 +1,441 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+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 org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+/**
+ * Tests for the PathBuilder class.
+ */
+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", ""));
+ String result = PathBuilder.buildPath("https://{endpoint}/keys", context);
+ assertEquals("\"https://\" + + \"/keys\"", result);
+ }
+
+ @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);
+ }
+
+ // TODO: Currently, the context adds subsitition using the parameter name as key so i
+ // Is this a valid case?
+// @Test
+// public void buildsPathWithHostAndPathUsingSameSubstitutionName() {
+// HttpRequestContext context = new HttpRequestContext();
+// context.addSubstitution(new Substitution("sub1", "hostSub1"));
+// context.addSubstitution(new Substitution("sub1", "pathSub1"));
+//
+// String result = PathBuilder.buildPath("https://{sub1}.host.com/keys/{sub1}", context);
+// assertEquals("\"https://\" + hostSub1 + \".host.com/keys/\" + pathSub1", result);
+// }
+}
diff --git a/sdk/clientcore/tools/annotation-processor/src/test/java/io/clientcore/tools/codegen/utils/ResponseBodyModeGenerationTest.java b/sdk/clientcore/tools/annotation-processor/src/test/java/io/clientcore/tools/codegen/utils/ResponseBodyModeGenerationTest.java
new file mode 100644
index 0000000000000..418cfc6edcbc0
--- /dev/null
+++ b/sdk/clientcore/tools/annotation-processor/src/test/java/io/clientcore/tools/codegen/utils/ResponseBodyModeGenerationTest.java
@@ -0,0 +1,43 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package io.clientcore.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);"));
+ //}
+}
diff --git a/sdk/clientcore/tools/pom.xml b/sdk/clientcore/tools/pom.xml
new file mode 100644
index 0000000000000..c4a6300b10085
--- /dev/null
+++ b/sdk/clientcore/tools/pom.xml
@@ -0,0 +1,14 @@
+
+
+ 4.0.0
+ io.clientcore
+ clientcore-tools-parent
+ pom
+ 1.0.0
+
+ annotation-processor
+
+