Skip to content

Commit

Permalink
feat: provide http status code API (#1936)
Browse files Browse the repository at this point in the history
* feat: provide http status code API

* adds scala API, use overloaded method

* add integration test

* rename to .withStatusCode
  • Loading branch information
efgpinto authored Jan 10, 2024
1 parent d8b2e3b commit 3040769
Show file tree
Hide file tree
Showing 11 changed files with 234 additions and 17 deletions.
18 changes: 18 additions & 0 deletions sdk/java-sdk-protobuf/src/main/java/kalix/javasdk/Metadata.java
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,24 @@ public interface Metadata extends Iterable<Metadata.MetadataEntry> {
*/
Principals principals();

/**
* Add an HTTP response code to this metadata.
* This will only take effect when HTTP transcoding is in use. It will be ignored for gRPC requests.
*
* @param httpStatusCode The success status code to add.
* @return a copy of this metadata with the HTTP response code set.
*/
Metadata withStatusCode(StatusCode.Success httpStatusCode);

/**
* Add an HTTP response code to this metadata.
* This will only take effect when HTTP transcoding is in use. It will be ignored for gRPC requests.
*
* @param httpStatusCode The redirect status code to add.
* @return a copy of this metadata with the HTTP response code set.
*/
Metadata withStatusCode(StatusCode.Redirect httpStatusCode);

/** A metadata entry. */
interface MetadataEntry {
/**
Expand Down
83 changes: 72 additions & 11 deletions sdk/java-sdk-protobuf/src/main/java/kalix/javasdk/StatusCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,81 @@
package kalix.javasdk;

/**
* Interface used to represent a status code, typically used when replying with an error. **NOT**
* for user extension.
* Interface used to represent HTTP status code. **NOT** for user extension.
*/
public interface StatusCode {

// return the value of the status code
int value();

enum Success implements StatusCode {
OK(200),
CREATED(201),
ACCEPTED(202),
NON_AUTHORITATIVE_INFORMATION(203),
NO_CONTENT(204),
RESET_CONTENT(205),
PARTIAL_CONTENT(206),
MULTI_STATUS(207),
ALREADY_REPORTED(208),
IM_USED(226);

private final int value;

Success(int value) {
this.value = value;
}

@Override
public int value() {
return value;
}
}

enum Redirect implements StatusCode {
MULTIPLE_CHOICES(300),
MOVED_PERMANENTLY(301),
FOUND(302),
SEE_OTHER(303),
NOT_MODIFIED(304),
USE_PROXY(305),
TEMPORARY_REDIRECT(307),
PERMANENT_REDIRECT(308);

private final int value;

Redirect(int value) {
this.value = value;
}

@Override
public int value() {
return value;
}
}


/** The supported HTTP error codes that can be used when replying from the Kalix user function. */
enum ErrorCode implements StatusCode {
BAD_REQUEST,
UNAUTHORIZED,
FORBIDDEN,
NOT_FOUND,
CONFLICT,
TOO_MANY_REQUESTS,
INTERNAL_SERVER_ERROR,
SERVICE_UNAVAILABLE,
GATEWAY_TIMEOUT
BAD_REQUEST(400),
UNAUTHORIZED(401),
FORBIDDEN(403),
NOT_FOUND(404),
CONFLICT(409),
TOO_MANY_REQUESTS(429),
INTERNAL_SERVER_ERROR(500),
SERVICE_UNAVAILABLE(503),
GATEWAY_TIMEOUT(504);

private final int value;

ErrorCode(int value) {
this.value = value;
}

@Override
public int value() {
return value;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import kalix.javasdk.JwtClaims
import kalix.javasdk.Metadata
import kalix.javasdk.Principal
import kalix.javasdk.Principals
import kalix.javasdk.StatusCode
import kalix.javasdk.StatusCode.ErrorCode
import kalix.javasdk.impl.MetadataImpl.JwtClaimPrefix
import kalix.protocol.component
import kalix.protocol.component.MetadataEntry
Expand Down Expand Up @@ -194,6 +196,12 @@ private[kalix] class MetadataImpl(val entries: Seq[MetadataEntry]) extends Metad

override def clearTime(): MetadataImpl = remove(MetadataImpl.CeTime)

override def withStatusCode(code: StatusCode.Redirect): MetadataImpl =
set("_kalix-http-code", code.value.toString)

override def withStatusCode(code: StatusCode.Success): MetadataImpl =
set("_kalix-http-code", code.value.toString)

override def asMetadata(): Metadata = this

// The reason we don't just implement JwtClaims ourselves is that some of the methods clash with CloudEvent
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@

package kalix.javasdk.impl

import kalix.javasdk.StatusCode.Redirect
import kalix.javasdk.StatusCode.Success

import java.time.Instant
import java.util.Optional
import kalix.javasdk.{ Metadata, Principal }
Expand Down Expand Up @@ -148,6 +151,14 @@ class MetadataImplSpec extends AnyWordSpec with Matchers with OptionValues {
meta.principals().get().asScala should have size 0
}
}

"support setting a HTTP status code" in {
val md = Metadata.EMPTY.withStatusCode(Success.CREATED)
md.get("_kalix-http-code").toScala.value shouldBe "201"

val mdRedirect = md.withStatusCode(Redirect.MOVED_PERMANENTLY)
mdRedirect.get("_kalix-http-code").toScala.value shouldBe "301"
}
}

private def metadata(entries: (String, String)*): Metadata = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,20 +44,23 @@
import com.google.protobuf.any.Any;
import kalix.javasdk.DeferredCall;
import kalix.javasdk.Metadata;
import kalix.javasdk.StatusCode;
import kalix.javasdk.client.ComponentClient;
import kalix.javasdk.client.EventSourcedEntityCallBuilder;
import kalix.spring.KalixConfigurationTest;
import org.hamcrest.core.IsEqual;
import org.hamcrest.core.IsNull;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.TestPropertySource;
import org.springframework.web.reactive.function.client.ClientResponse;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.client.WebClientResponseException;
import reactor.core.publisher.Mono;
Expand Down Expand Up @@ -220,6 +223,17 @@ public void verifyEchoActionConcatBodyWithSeparator() {
assertThat(response.text).isEqualTo("foo/bar");
}

@Test
public void verifyEchoActionWithCustomCode() {
ClientResponse response =
webClient
.post()
.uri("/echo/message/customCode/hello")
.exchangeToMono(Mono::just)
.block(timeout);
Assertions.assertEquals(StatusCode.Success.ACCEPTED.value(), response.statusCode().value());
}

@Test
public void verifyEchoActionRequestParamWithForward() {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.TestPropertySource;
import org.springframework.web.reactive.function.client.ClientResponse;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.client.WebClientResponseException;
import reactor.core.publisher.Mono;
Expand Down Expand Up @@ -104,14 +105,13 @@ public void verifyValueEntityCurrentStateAfterRestart() {
}

private void createUser(TestUser user) {
String userCreation =
ClientResponse response =
webClient
.post()
.uri("/user/" + user.id + "/" + user.email + "/" + user.name)
.retrieve()
.bodyToMono(String.class)
.exchangeToMono(Mono::just)
.block(timeout);
Assertions.assertEquals("\"Ok\"", userCreation);
Assertions.assertEquals(201, response.statusCode().value());
}

private void changeEmail(TestUser user) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@

package com.example.wiring.actions.echo;

import kalix.javasdk.Metadata;
import kalix.javasdk.StatusCode;
import kalix.javasdk.action.Action;
import kalix.javasdk.action.ActionCreationContext;
import kalix.javasdk.client.ComponentClient;
Expand Down Expand Up @@ -92,4 +94,11 @@ public Flux<Effect<Message>> stringMessageRepeat(
i -> Mono.fromCompletionStage(CompletableFuture.supplyAsync(() -> parrot.repeat(msg))))
.map(m -> effects().reply(new Message(m)));
}

@PostMapping("/echo/message/customCode/{msg}")
public Effect<Message> stringMessageCustomCode(@PathVariable String msg) {
String response = this.parrot.repeat(msg);
return effects().reply(new Message(response),
Metadata.EMPTY.withStatusCode(StatusCode.Success.ACCEPTED));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package com.example.wiring.valueentities.user;

import kalix.javasdk.Metadata;
import kalix.javasdk.StatusCode;
import kalix.javasdk.eventsourcedentity.EventSourcedEntity;
import kalix.javasdk.valueentity.ValueEntity;
Expand Down Expand Up @@ -49,7 +50,8 @@ public Effect<User> getUser() {

@PostMapping("/{email}/{name}")
public Effect<String> createOrUpdateUser(@PathVariable String email, @PathVariable String name) {
return effects().updateState(new User(email, name)).thenReply("Ok");
return effects().updateState(new User(email, name)).thenReply("Ok",
Metadata.EMPTY.withStatusCode(StatusCode.Success.CREATED));
}

@PutMapping("/{email}/{name}")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,28 @@ trait Metadata extends Iterable[MetadataEntry] {
* Get the Principals associated with this request.
*/
def principals: Principals

/**
* Add an HTTP response code to this metadata. This will only take effect when HTTP transcoding is in use. It will be
* ignored for gRPC requests.
*
* @param httpStatusCode
* The success status code to add.
* @return
* a copy of this metadata with the HTTP response code set.
*/
def withStatusCode(httpStatusCode: StatusCode.Success): Metadata

/**
* Add an HTTP response code to this metadata. This will only take effect when HTTP transcoding is in use. It will be
* ignored for gRPC requests.
*
* @param httpStatusCode
* The redirect status code to add.
* @return
* a copy of this metadata with the HTTP response code set.
*/
def withStatusCode(httpStatusCode: StatusCode.Redirect): Metadata
}

object Metadata {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* Copyright 2021 Lightbend Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package kalix.scalasdk

/**
* A sealed trait representing HTTP status codes.
*/
sealed trait StatusCode

/**
* Companion object for the `StatusCode` trait, containing various HTTP status code definitions.
*/
object StatusCode {
sealed abstract class Success(val value: Int) extends StatusCode

case object Ok extends Success(200)

case object Created extends Success(201)

case object Accepted extends Success(202)

case object NonAuthoritativeInformation extends Success(203)

case object NoContent extends Success(204)

case object ResetContent extends Success(205)

case object PartialContent extends Success(206)

case object MultiStatus extends Success(207)

case object AlreadyReported extends Success(208)

case object IMUsed extends Success(226)

sealed abstract class Redirect(val value: Int) extends StatusCode

case object MultipleChoices extends Redirect(300)

case object MovedPermanently extends Redirect(301)

case object Found extends Redirect(302)

case object SeeOther extends Redirect(303)

case object NotModified extends Redirect(304)

case object UseProxy extends Redirect(305)

case object TemporaryRedirect extends Redirect(307)

case object PermanentRedirect extends Redirect(308)
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@

package kalix.scalasdk.impl
import java.nio.ByteBuffer

import scala.collection.immutable.Seq
import kalix.scalasdk.{ CloudEvent, JwtClaims, Metadata, MetadataEntry, Principal, Principals }
import kalix.protocol.component.{ MetadataEntry => ProtocolMetadataEntry }
import kalix.scalasdk.StatusCode

import scala.jdk.OptionConverters._
import scala.jdk.CollectionConverters._
Expand Down Expand Up @@ -95,4 +95,9 @@ private[kalix] class MetadataImpl(val impl: kalix.javasdk.impl.MetadataImpl) ext
override def localService: Option[String] = impl.principals.getLocalService.toScala
override def apply: Seq[Principal] = impl.principals.get.asScala.map(Principal.toScala).toSeq
}
override def withStatusCode(code: StatusCode.Success): Metadata =
set("_kalix-http-code", code.value.toString)

override def withStatusCode(code: StatusCode.Redirect): Metadata =
set("_kalix-http-code", code.value.toString)
}

0 comments on commit 3040769

Please sign in to comment.