Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Azure HTTP Functions compliant with TCK #509

Merged
merged 32 commits into from
Jun 30, 2023
Merged
Show file tree
Hide file tree
Changes from 30 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
b007407
Rewrite Azure http to use Servlet core
timyates Jun 21, 2023
4f64262
Checkstyle and spotless
timyates Jun 21, 2023
6aa701c
Merge branch 'master' into fix-tck
timyates Jun 21, 2023
22e1e72
Fix compilation... tests still fail though
timyates Jun 21, 2023
4f129ae
Prefer project modules
timyates Jun 21, 2023
edc24e8
Apply context path fix from https://github.com/micronaut-projects/mic…
timyates Jun 21, 2023
d19fa65
Use typesafe project accessor
timyates Jun 21, 2023
7604207
Fix nativeResponse
timyates Jun 22, 2023
cbf0115
Fix http-test
timyates Jun 23, 2023
c73892d
Accept removal of AzureServletBodyBinder
timyates Jun 23, 2023
6cdb614
Fix sonar error
timyates Jun 23, 2023
01349ea
Remove unused field and test context-path in http-test
timyates Jun 23, 2023
80e72d4
Remove unused static member
timyates Jun 23, 2023
f263fdb
Appease sonar's hatred of parentheses
timyates Jun 23, 2023
1fdeb0c
Remove more unused fields
timyates Jun 23, 2023
1fc2d8c
Javadoc
timyates Jun 23, 2023
3c1188a
Cleanup
timyates Jun 23, 2023
2efaf7e
Remove aws from the javadoc
timyates Jun 26, 2023
a2a8720
Add invoke() back to HttpRequestMessageBuilder
timyates Jun 26, 2023
3b64830
Merge branch 'master' into fix-tck
timyates Jun 26, 2023
6863efb
Only use a single, correct ResponseBuilder -- fixes headers
timyates Jun 26, 2023
6e32492
update Micronaut gradle plugin to M6
sdelamo Jun 26, 2023
bbfd5eb
Update types
sdelamo Jun 26, 2023
e611d49
use headers collection HttpHeaders
sdelamo Jun 26, 2023
dec1d1e
Sonar smell removal
timyates Jun 26, 2023
33e96cc
delete multi single String maps utilities
sdelamo Jun 26, 2023
bc0a181
Wildcard imports and unused imports
timyates Jun 26, 2023
2eb0de5
add javadoc
sdelamo Jun 26, 2023
a707238
prefix with slash api
sdelamo Jun 26, 2023
b18b688
assume users disables route prefix
sdelamo Jun 27, 2023
04ad2a1
Move ResponseMessageAdaptor to TCK module as it's only used there
timyates Jun 27, 2023
b8028f8
Add TCK tests for http-test (#515)
timyates Jun 27, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2017-2020 original authors
* Copyright 2017-2023 original authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -20,6 +20,8 @@
import com.microsoft.azure.functions.HttpResponseMessage;
import com.microsoft.azure.functions.HttpStatusType;
import io.micronaut.azure.function.http.AzureFunctionHttpRequest;
import io.micronaut.azure.function.http.AzureFunctionHttpResponse;
import io.micronaut.azure.function.http.BinaryContentConfiguration;
import io.micronaut.azure.function.http.HttpRequestMessageBuilder;
import io.micronaut.context.ApplicationContext;
import io.micronaut.context.env.Environment;
Expand All @@ -38,6 +40,7 @@
import io.micronaut.servlet.http.BodyBuilder;
import io.micronaut.servlet.http.ServletExchange;
import io.micronaut.servlet.http.ServletHttpHandler;
import io.micronaut.servlet.http.ServletHttpResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.eclipse.jetty.server.Request;
Expand Down Expand Up @@ -125,7 +128,7 @@ public EmbeddedServer start() {
context.setContextPath(contextPath);
context.setResourceBase(".");
context.setClassLoader(Thread.currentThread().getContextClassLoader());
context.setHandler(new AzureHandler(getApplicationContext(), contextPath, conversionService));
context.setHandler(new AzureHandler(getApplicationContext(), conversionService));
server.setHandler(context);
this.server.setHandler(context);
this.server.start();
Expand Down Expand Up @@ -215,15 +218,12 @@ public boolean isRunning() {
private static final class AzureHandler extends AbstractHandler {

private final ServletHttpHandler<HttpRequestMessage<Optional<String>>, HttpResponseMessage> httpHandler;
private final String contextPath;

/**
* Default constructor.
* @param applicationContext The app context
* @param contextPath The context path
*/
AzureHandler(ApplicationContext applicationContext, String contextPath, ConversionService conversionService) {
this.contextPath = contextPath;
AzureHandler(ApplicationContext applicationContext, ConversionService conversionService) {
httpHandler = new ServletHttpHandler<>(applicationContext, conversionService) {
@Override
public boolean isRunning() {
Expand Down Expand Up @@ -274,30 +274,45 @@ public void handle(String target, Request baseRequest, HttpServletRequest reques
}

HttpRequestMessage<Optional<String>> requestMessage = requestMessageBuilder.buildEncoded();
ConversionService handlerConversionService = httpHandler.getApplicationContext().getBean(ConversionService.class);
BinaryContentConfiguration binaryContentConfiguration = httpHandler.getApplicationContext().getBean(BinaryContentConfiguration.class);
AzureFunctionHttpRequest<?> azureFunctionHttpRequest =
new AzureFunctionHttpRequest<>(
contextPath,
requestMessage,
httpHandler.getMediaTypeCodecRegistry(),
new AzureFunctionHttpResponse<>(
requestMessage,
handlerConversionService,
binaryContentConfiguration
),
new DefaultExecutionContext(),
httpHandler.getApplicationContext().getBean(ConversionService.class),
handlerConversionService,
binaryContentConfiguration,
httpHandler.getApplicationContext().getBean(BodyBuilder.class)
);

ServletExchange<HttpRequestMessage<Optional<String>>, HttpResponseMessage> exchange =
httpHandler.exchange(azureFunctionHttpRequest);

HttpResponseMessage httpResponseMessage = exchange.getResponse().getNativeResponse();
ServletHttpResponse<HttpResponseMessage, ?> exchangeResponse = exchange.getResponse();
HttpResponseMessage httpResponseMessage = exchangeResponse.getNativeResponse();
HttpStatusType httpStatus = httpResponseMessage.getStatus();
byte[] bodyAsBytes = (byte[]) httpResponseMessage.getBody();

Object bodyObject = httpResponseMessage.getBody();
byte[] bodyAsBytes = null;
if (bodyObject instanceof CharSequence charBody) {
bodyAsBytes = charBody.toString().getBytes(exchangeResponse.getCharacterEncoding());
} else if (bodyObject instanceof byte[] byteBody) {
bodyAsBytes = byteBody;
}
response.setStatus(httpStatus.value());
final boolean hasBody = bodyAsBytes != null;
response.setContentLength(hasBody ? bodyAsBytes.length : 0);
if (httpResponseMessage instanceof HttpHeaders) {
HttpHeaders headers = (HttpHeaders) httpResponseMessage;
if (httpResponseMessage instanceof HttpHeaders headers) {
headers.forEach((name, values) -> {
for (String value : values) {
response.addHeader(name, value);
if (!response.containsHeader(name)) {
response.addHeader(name, value);
}
}
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import jakarta.inject.Inject
import static io.micronaut.http.HttpHeaders.*

@MicronautTest
@Property(name = "micronaut.server.context-path", value = "/api")
class AzureFunctionCorsSpec extends Specification implements TestPropertyProvider {

@Inject @Client("/") HttpClient client
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package io.micronaut.azure.function.http.test

import io.micronaut.context.annotation.Property
import io.micronaut.context.annotation.Requires
import io.micronaut.http.HttpRequest
import io.micronaut.http.MediaType
import io.micronaut.http.annotation.Body
Expand All @@ -14,7 +16,10 @@ import spock.lang.Specification
import jakarta.inject.Inject

@MicronautTest
@Property(name = "spec.name", value = "AzureFunctionEmbeddedServerSpec")
@Property(name = "micronaut.server.context-path", value = "/api")
class AzureFunctionEmbeddedServerSpec extends Specification {

@Inject
@Client('/')
HttpClient client
Expand All @@ -36,6 +41,7 @@ class AzureFunctionEmbeddedServerSpec extends Specification {
result == 'goodbody'
}

@Requires(property = 'spec.name', value = 'AzureFunctionEmbeddedServerSpec')
@Controller('/test')
static class TestController {
@Get(value = '/', produces = MediaType.TEXT_PLAIN)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package io.micronaut.azure.function.http.test

import io.micronaut.context.annotation.Property
import io.micronaut.context.annotation.Requires
import io.micronaut.http.HttpRequest
import io.micronaut.http.MediaType
import io.micronaut.http.annotation.Body
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get
import io.micronaut.http.annotation.Post
import io.micronaut.http.client.HttpClient
import io.micronaut.http.client.annotation.Client
import io.micronaut.test.extensions.spock.annotation.MicronautTest
import jakarta.inject.Inject
import spock.lang.Specification

@MicronautTest
@Property(name = "spec.name", value = "ContextAzureFunctionEmbeddedServerSpec")
@Property(name = "micronaut.server.context-path", value = "/woo")
class ContextAzureFunctionEmbeddedServerSpec extends Specification {
@Inject
@Client('/')
HttpClient client

void 'test invoke function via server'() {
when:
def result = client.toBlocking().retrieve('/woo/test')

then:
result == 'good'
}

void 'test invoke post via server'() {
when:
def result = client.toBlocking().retrieve(HttpRequest.POST('/woo/test', "body")
.contentType(MediaType.TEXT_PLAIN), String)

then:
result == 'goodbody'
}

@Requires(property = 'spec.name', value = 'ContextAzureFunctionEmbeddedServerSpec')
@Controller('/test')
static class TestController {
@Get(value = '/', produces = MediaType.TEXT_PLAIN)
String test() {
return 'good'
}

@Post(value = '/', processes = MediaType.TEXT_PLAIN)
String test(@Body String body) {
return 'good' + body
}
}
}
6 changes: 6 additions & 0 deletions azure-function-http/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,9 @@ dependencies {
testCompileOnly(mn.micronaut.inject.groovy)
testAnnotationProcessor(mn.micronaut.inject.java)
}

spotless {
java {
targetExclude("**/io/micronaut/azure/function/http/QueryStringDecoder.java")
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2017-2020 original authors
* Copyright 2017-2023 original authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -30,54 +30,35 @@
import io.micronaut.http.bind.binders.TypedRequestArgumentBinder;
import io.micronaut.http.codec.MediaTypeCodecRegistry;
import io.micronaut.servlet.http.ServletBinderRegistry;

import io.micronaut.servlet.http.ServletBodyBinder;
import jakarta.inject.Singleton;

import java.util.List;
import java.util.Optional;
import java.util.logging.Logger;

/**
* Implementation of {@link ServletBinderRegistry} for Azure.
*
* @param <T> The body type
*
* @author graemerocher
* @since 1.2.0
*/
@Singleton
@Replaces(DefaultRequestBinderRegistry.class)
@Internal
public class AzureBinderRegistry<T> extends ServletBinderRegistry<T> {
@Replaces(DefaultRequestBinderRegistry.class)
class AzureBinderRegistry<T> extends ServletBinderRegistry<T> {

private static final Argument<ExecutionContext> EXECUTION_CONTEXT_ARGUMENT = Argument.of(ExecutionContext.class);
private static final Argument<TraceContext> TRACE_CONTEXT_ARGUMENT = Argument.of(TraceContext.class);
private static final Argument<Logger> LOGGER_ARGUMENT = Argument.of(Logger.class);
private static final Argument<HttpRequestMessage> REQUEST_MESSAGE_ARGUMENT = Argument.of(HttpRequestMessage.class);
protected final DefaultBodyAnnotationBinder<T> defaultBodyAnnotationBinder;

/**
* Default constructor.
*
* @param mediaTypeCodecRegistry The media type codec registry
* @param conversionService The conversion service
* @param binders Any registered binders
* @param defaultBodyAnnotationBinder The delegate default body binder
*/
AzureBinderRegistry(
MediaTypeCodecRegistry mediaTypeCodecRegistry,
ConversionService conversionService,
List<RequestArgumentBinder> binders,
DefaultBodyAnnotationBinder<T> defaultBodyAnnotationBinder
MediaTypeCodecRegistry mediaTypeCodecRegistry,
ConversionService conversionService,
List<RequestArgumentBinder> binders,
DefaultBodyAnnotationBinder<T> defaultBodyAnnotationBinder
) {
super(mediaTypeCodecRegistry, conversionService, binders, defaultBodyAnnotationBinder);
this.defaultBodyAnnotationBinder = defaultBodyAnnotationBinder;
this.byType.put(HttpRequestMessage.class, new TypedRequestArgumentBinder<HttpRequestMessage>() {
@Override
public BindingResult<HttpRequestMessage> bind(
ArgumentConversionContext<HttpRequestMessage> context, HttpRequest<?> source) {
if (source instanceof AzureFunctionHttpRequest) {
return () -> Optional.of(((AzureFunctionHttpRequest<?>) source).getNativeRequest());
ArgumentConversionContext<HttpRequestMessage> context, HttpRequest<?> source) {
if (source instanceof AzureFunctionHttpRequest<?> req) {
return () -> Optional.of(req.getNativeRequest());
} else {
return BindingResult.EMPTY;
}
Expand All @@ -91,9 +72,9 @@ public Argument<HttpRequestMessage> argumentType() {
this.byType.put(ExecutionContext.class, new TypedRequestArgumentBinder<ExecutionContext>() {
@Override
public BindingResult<ExecutionContext> bind(
ArgumentConversionContext<ExecutionContext> context, HttpRequest<?> source) {
if (source instanceof AzureFunctionHttpRequest) {
return () -> Optional.of(((AzureFunctionHttpRequest<?>) source).getExecutionContext());
ArgumentConversionContext<ExecutionContext> context, HttpRequest<?> source) {
if (source instanceof AzureFunctionHttpRequest<?> req) {
return () -> Optional.of(req.getExecutionContext());
} else {
return BindingResult.EMPTY;
}
Expand All @@ -107,9 +88,9 @@ public Argument<ExecutionContext> argumentType() {
this.byType.put(Logger.class, new TypedRequestArgumentBinder<Logger>() {
@Override
public BindingResult<Logger> bind(
ArgumentConversionContext<Logger> context, HttpRequest<?> source) {
if (source instanceof AzureFunctionHttpRequest) {
return () -> Optional.of(((AzureFunctionHttpRequest<?>) source).getExecutionContext().getLogger());
ArgumentConversionContext<Logger> context, HttpRequest<?> source) {
if (source instanceof AzureFunctionHttpRequest<?> req) {
return () -> Optional.of(req.getExecutionContext().getLogger());
} else {
return BindingResult.EMPTY;
}
Expand All @@ -123,9 +104,9 @@ public Argument<Logger> argumentType() {
this.byType.put(TraceContext.class, new TypedRequestArgumentBinder<TraceContext>() {
@Override
public BindingResult<TraceContext> bind(
ArgumentConversionContext<TraceContext> context, HttpRequest<?> source) {
if (source instanceof AzureFunctionHttpRequest) {
return () -> Optional.ofNullable(((AzureFunctionHttpRequest<?>) source).getExecutionContext().getTraceContext());
ArgumentConversionContext<TraceContext> context, HttpRequest<?> source) {
if (source instanceof AzureFunctionHttpRequest<?> req) {
return () -> Optional.ofNullable(req.getExecutionContext().getTraceContext());
} else {
return BindingResult.EMPTY;
}
Expand All @@ -137,12 +118,4 @@ public Argument<TraceContext> argumentType() {
}
});
}

@Override
protected ServletBodyBinder<T> newServletBodyBinder(
MediaTypeCodecRegistry mediaTypeCodecRegistry,
ConversionService conversionService,
DefaultBodyAnnotationBinder<T> defaultBodyAnnotationBinder) {
return new AzureServletBodyBinder<>(conversionService, mediaTypeCodecRegistry, defaultBodyAnnotationBinder);
}
}
Loading