Skip to content

Commit

Permalink
feat: error support (WIP)
Browse files Browse the repository at this point in the history
  • Loading branch information
ngyewch committed Jul 23, 2024
1 parent 3dd8e43 commit 6023e87
Show file tree
Hide file tree
Showing 10 changed files with 236 additions and 154 deletions.
148 changes: 4 additions & 144 deletions generator/templates/Twirp.java.tmpl
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
{{if .ProtoFile.JavaPackage}}package {{.ProtoFile.JavaPackage}};
{{end}}
public class {{.JavaOuterClassName}} {
private static final String PROTOBUF_CONTENT_TYPE = "application/protobuf";
private static final String JSON_CONTENT_TYPE = "application/json";

{{range .ProtoFile.Services}}
public interface {{.JavaClassName}} {
{{range .Methods}}
Expand All @@ -16,102 +13,20 @@ public class {{.JavaOuterClassName}} {

{{if or .Options.GenerateHelidonServer .Options.GenerateHelidonClient}}
public static class Helidon {
private static final io.helidon.common.http.MediaType PROTOBUF_MEDIA_TYPE =
io.helidon.common.http.MediaType.parse(PROTOBUF_CONTENT_TYPE);
private static final io.helidon.common.http.MediaType JSON_MEDIA_TYPE =
io.helidon.common.http.MediaType.parse(JSON_CONTENT_TYPE);
{{if .Options.GenerateHelidonClient}}
public static class Client {
private abstract static class AbstractService {
private final io.helidon.webclient.WebClient webClient;
private final io.helidon.common.http.MediaType contentType;

public AbstractService(String baseUri, io.helidon.common.http.MediaType contentType) {
super();

webClient = io.helidon.webclient.WebClient.builder().baseUri(baseUri).build();
this.contentType = contentType;
}

protected void doRequest(
String path,
com.google.protobuf.Message input,
com.google.protobuf.Message.Builder outputBuilder) {
if (contentType.equals(PROTOBUF_MEDIA_TYPE)) {
doProtobufRequest(path, input, outputBuilder);
} else if (contentType.equals(JSON_MEDIA_TYPE)) {
doJsonRequest(path, input, outputBuilder);
} else {
throw new IllegalArgumentException("unsupported content type");
}
}

private void doProtobufRequest(
String path,
com.google.protobuf.Message input,
com.google.protobuf.Message.Builder outputBuilder) {
try {
webClient
.post()
.path(path)
.contentType(io.helidon.common.http.MediaType.parse(PROTOBUF_CONTENT_TYPE))
.submit(input.toByteArray(), byte[].class)
.map(
responseBytes -> {
try {
return outputBuilder.mergeFrom(responseBytes);
} catch (com.google.protobuf.InvalidProtocolBufferException e) {
throw new RuntimeException(e);
}
})
.get();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} catch (java.util.concurrent.ExecutionException e) {
throw new RuntimeException(e.getCause());
}
}

private void doJsonRequest(
String path,
com.google.protobuf.Message input,
com.google.protobuf.Message.Builder outputBuilder) {
try {
final String requestJson = com.google.protobuf.util.JsonFormat.printer().print(input);
webClient
.post()
.path(path)
.contentType(io.helidon.common.http.MediaType.parse(JSON_CONTENT_TYPE))
.submit(requestJson, String.class)
.map(
responseJson -> {
try {
com.google.protobuf.util.JsonFormat.parser().merge(responseJson, outputBuilder);
return outputBuilder;
} catch (com.google.protobuf.InvalidProtocolBufferException e) {
throw new RuntimeException(e);
}
})
.get();
} catch (InterruptedException | com.google.protobuf.InvalidProtocolBufferException e) {
throw new RuntimeException(e);
} catch (java.util.concurrent.ExecutionException e) {
throw new RuntimeException(e.getCause());
}
}
}
{{range .ProtoFile.Services}}
public static class {{.JavaClassName}} extends AbstractService implements {{$.JavaOuterClassName}}.{{.JavaClassName}} {
public static class {{.JavaClassName}} extends io.github.ngyewch.twirp.helidon.client.AbstractService implements {{$.JavaOuterClassName}}.{{.JavaClassName}} {
private {{.JavaClassName}}(String baseUri, io.helidon.common.http.MediaType contentType) {
super(baseUri, contentType);
}

public static {{$.JavaOuterClassName}}.{{.JavaClassName}} newProtobufClient(String baseUri) {
return new {{.JavaClassName}}(baseUri, PROTOBUF_MEDIA_TYPE);
return new {{.JavaClassName}}(baseUri, io.github.ngyewch.twirp.helidon.MediaTypes.PROTOBUF_MEDIA_TYPE);
}

public static {{$.JavaOuterClassName}}.{{.JavaClassName}} newJSONClient(String baseUri) {
return new {{.JavaClassName}}(baseUri, JSON_MEDIA_TYPE);
return new {{.JavaClassName}}(baseUri, io.github.ngyewch.twirp.helidon.MediaTypes.JSON_MEDIA_TYPE);
}
{{range .Methods}}
{{if not (or .Descriptor.IsStreamingClient .Descriptor.IsStreamingServer)}}
Expand All @@ -138,68 +53,13 @@ public class {{.JavaOuterClassName}} {
{{$requestType := (index .JavaMethod.Arguments 0).Type}}
rules.post("{{.TwirpPath}}", (req, res) -> {
final {{$requestType}}.Builder requestBuilder = {{$requestType}}.newBuilder();
handleTwirp(req, res, requestBuilder, () -> service.{{.JavaMethod.Name}}(requestBuilder.build()));
io.github.ngyewch.twirp.helidon.server.Handler.handleTwirp(req, res, requestBuilder, () -> service.{{.JavaMethod.Name}}(requestBuilder.build()));
});
{{end}}
{{end}}
}
}
{{end}}
private static void handleTwirp(
io.helidon.webserver.ServerRequest req,
io.helidon.webserver.ServerResponse res,
com.google.protobuf.Message.Builder messageBuilder,
java.util.function.Supplier<com.google.protobuf.Message> serviceInvoker) {
if (req.headers().contentType().isEmpty()) {
res.status(io.helidon.common.http.Http.Status.BAD_REQUEST_400)
.send("Content-Type not specified");
return;
}
final io.helidon.common.http.MediaType contentType = req.headers().contentType().get();
if (contentType.equals(PROTOBUF_MEDIA_TYPE)) {
req.content()
.as(byte[].class)
.thenAccept(
contentBytes -> {
try {
messageBuilder.mergeFrom(contentBytes);
final com.google.protobuf.Message response = serviceInvoker.get();
res.headers().contentType(contentType);
res.send(response.toByteArray());
} catch (com.google.protobuf.InvalidProtocolBufferException e) {
res.status(io.helidon.common.http.Http.Status.BAD_REQUEST_400)
.send("Malformed content");
} catch (Exception e) {
res.status(io.helidon.common.http.Http.Status.INTERNAL_SERVER_ERROR_500)
.send(e.toString());
}
});
} else if (contentType.equals(JSON_MEDIA_TYPE)) {
req.content()
.as(String.class)
.thenAccept(
contentString -> {
try {
com.google.protobuf.util.JsonFormat.parser()
.merge(contentString, messageBuilder);
final com.google.protobuf.Message response = serviceInvoker.get();
final String json =
com.google.protobuf.util.JsonFormat.printer().print(response);
res.headers().contentType(contentType);
res.send(json);
} catch (com.google.protobuf.InvalidProtocolBufferException e) {
res.status(io.helidon.common.http.Http.Status.BAD_REQUEST_400)
.send("Malformed content");
} catch (Exception e) {
res.status(io.helidon.common.http.Http.Status.INTERNAL_SERVER_ERROR_500)
.send(e.toString());
}
});
} else {
res.status(io.helidon.common.http.Http.Status.BAD_REQUEST_400)
.send("Content-Type not supported");
}
}
}
{{end}}
}
Expand Down
2 changes: 1 addition & 1 deletion java/gradle.properties
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
group=io.github.ngyewch.twirp
version=0.1.1
version=0.2.0
7 changes: 7 additions & 0 deletions java/twirp-common/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@ repositories {
mavenCentral()
}

dependencies {
implementation(platform("com.fasterxml.jackson:jackson-bom:2.17.2"))

implementation("com.fasterxml.jackson.core:jackson-annotations")
implementation("com.fasterxml.jackson.core:jackson-databind")
}

publishing {
publications {
create<MavenPublication>("maven") {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package io.github.ngyewch.twirp;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.util.Map;

public class TwirpError {
private String code;
private String msg;
private Map<String, String> meta;

public String getCode() {
return code;
}

public void setCode(String code) {
this.code = code;
}

public String getMsg() {
return msg;
}

public void setMsg(String msg) {
this.msg = msg;
}

public Map<String, String> getMeta() {
return meta;
}

public void setMeta(Map<String, String> meta) {
this.meta = meta;
}

public static TwirpError from(String code, String msg, Map<String, String> meta) {
final TwirpError error = new TwirpError();
error.setCode(code);
error.setMsg(msg);
error.setMeta(meta);
return error;
}

public static TwirpError from(TwirpErrorCode errorCode, String msg, Map<String, String> meta) {
return from(errorCode.getCode(), msg, meta);
}

public static TwirpError fromJson(String s) throws IOException {
final ObjectMapper objectMapper = new ObjectMapper();
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
return objectMapper.readValue(s, TwirpError.class);
}

public static String toJson(TwirpError error) throws IOException {
final ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
return objectMapper.writeValueAsString(error);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package io.github.ngyewch.twirp;

public enum TwirpErrorCode {
CANCELED("canceled", 408),
UNKNOWN("unknown", 500),
INVALID_ARGUMENT("invalid_argument", 400),
MALFORMED("malformed", 400),
DEADLINE_EXCEEDED("deadline_exceeded", 408),
NOT_FOUND("not_found", 404),
BAD_ROUTE("bad_route", 404),
ALREADY_EXISTS("already_exists", 409),
PERMISSION_DENIED("permission_denied", 403),
UNAUTHENTICATED("unauthenticated", 401),
RESOURCE_EXHAUSTED("resource_exhausted", 429),
FAILED_PRECONDITION("failed_precondition", 412),
ABORTED("aborted", 409),
OUT_OF_RANGE("out_of_range", 400),
UNIMPLEMENTED("unimplemented", 501),
INTERNAL("internal", 500),
UNAVAILABLE("unavailable", 503),
DATALOSS("dataloss", 500);

private final String code;
private final int httpStatus;

TwirpErrorCode(String code, int httpStatus) {
this.code = code;
this.httpStatus = httpStatus;
}

public String getCode() {
return code;
}

public int getHttpStatus() {
return httpStatus;
}

public static TwirpErrorCode fromCode(String code) {
for (final TwirpErrorCode errorCode : TwirpErrorCode.values()) {
if (errorCode.code.equals(code)) {
return errorCode;
}
}
return null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package io.github.ngyewch.twirp;

import java.util.Map;

public class TwirpException extends RuntimeException {
private final TwirpError error;

public TwirpException(TwirpError error) {
super();

this.error = error;
}

public TwirpException(String code, String msg, Map<String, String> meta) {
this(TwirpError.from(code, msg, meta));
}

public TwirpException(TwirpErrorCode errorCode, String msg, Map<String, String> meta) {
this(TwirpError.from(errorCode, msg, meta));
}

public TwirpError getError() {
return error;
}

@Override
public String getMessage() {
final TwirpErrorCode errorCode = TwirpErrorCode.fromCode(error.getCode());
String s = "";
if (errorCode != null) {
s += String.format("%s (%d): ", errorCode.getCode(), errorCode.getHttpStatus());
} else if (error.getCode() != null) {
s += String.format("%s: ", error.getCode());
}
if (error.getMsg() != null) {
s += error.getMsg();
}
return s;
}
}
2 changes: 2 additions & 0 deletions java/twirp-helidon-client/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,14 @@ repositories {
dependencies {
api(platform(project(":twirp-bom")))

implementation(project(":twirp-common"))
implementation(project(":twirp-helidon-common"))

api("com.google.protobuf:protobuf-java")
implementation("com.google.protobuf:protobuf-java-util")
api("io.helidon.common:helidon-common-http")
implementation("io.helidon.common:helidon-common-reactive")
implementation("io.helidon.media:helidon-media-common")
implementation("io.helidon.webclient:helidon-webclient")
}

Expand Down
Loading

0 comments on commit 6023e87

Please sign in to comment.