diff --git a/examples/src/main/java/my/restate/sdk/examples/Counter.java b/examples/src/main/java/my/restate/sdk/examples/Counter.java index baf2c7bb..a278e1ea 100644 --- a/examples/src/main/java/my/restate/sdk/examples/Counter.java +++ b/examples/src/main/java/my/restate/sdk/examples/Counter.java @@ -19,6 +19,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +/** Counter virtual object */ @VirtualObject(name = "Counter") public class Counter { @@ -26,11 +27,13 @@ public class Counter { private static final StateKey TOTAL = StateKey.of("total", JsonSerdes.LONG); + /** Reset the counter. */ @Handler public void reset(ObjectContext ctx) { ctx.clearAll(); } + /** Add the given value to the count. */ @Handler public void add(ObjectContext ctx, long request) { long currentValue = ctx.get(TOTAL).orElse(0L); @@ -38,12 +41,14 @@ public void add(ObjectContext ctx, long request) { ctx.set(TOTAL, newValue); } + /** Get the current counter value. */ @Shared @Handler public long get(SharedObjectContext ctx) { return ctx.get(TOTAL).orElse(0L); } + /** Add a value, and get both the previous value and the new value. */ @Handler public CounterUpdateResult getAndAdd(ObjectContext ctx, long request) { LOG.info("Invoked get and add with {}", request); diff --git a/sdk-api-gen-common/src/main/java/dev/restate/sdk/gen/model/Handler.java b/sdk-api-gen-common/src/main/java/dev/restate/sdk/gen/model/Handler.java index 67bbf5df..70c0f41d 100644 --- a/sdk-api-gen-common/src/main/java/dev/restate/sdk/gen/model/Handler.java +++ b/sdk-api-gen-common/src/main/java/dev/restate/sdk/gen/model/Handler.java @@ -18,18 +18,21 @@ public class Handler { private final @Nullable String inputAccept; private final PayloadType inputType; private final PayloadType outputType; + private final @Nullable String documentation; public Handler( CharSequence name, HandlerType handlerType, @Nullable String inputAccept, PayloadType inputType, - PayloadType outputType) { + PayloadType outputType, + @Nullable String documentation) { this.name = name; this.handlerType = handlerType; this.inputAccept = inputAccept; this.inputType = inputType; this.outputType = outputType; + this.documentation = documentation; } public CharSequence getName() { @@ -40,6 +43,7 @@ public HandlerType getHandlerType() { return handlerType; } + @Nullable public String getInputAccept() { return inputAccept; } @@ -52,6 +56,10 @@ public PayloadType getOutputType() { return outputType; } + public @Nullable String getDocumentation() { + return documentation; + } + public static Builder builder() { return new Builder(); } @@ -62,6 +70,7 @@ public static class Builder { private String inputAccept; private PayloadType inputType; private PayloadType outputType; + private String documentation; public Builder withName(CharSequence name) { this.name = name; @@ -88,6 +97,11 @@ public Builder withOutputType(PayloadType outputType) { return this; } + public Builder withDocumentation(String documentation) { + this.documentation = documentation; + return this; + } + public CharSequence getName() { return name; } @@ -117,7 +131,8 @@ public Handler validateAndBuild() { Objects.requireNonNull(handlerType), inputAccept, inputType, - outputType); + outputType, + documentation); } } } diff --git a/sdk-api-gen-common/src/main/java/dev/restate/sdk/gen/model/Service.java b/sdk-api-gen-common/src/main/java/dev/restate/sdk/gen/model/Service.java index 37439ec5..dc6de910 100644 --- a/sdk-api-gen-common/src/main/java/dev/restate/sdk/gen/model/Service.java +++ b/sdk-api-gen-common/src/main/java/dev/restate/sdk/gen/model/Service.java @@ -14,6 +14,7 @@ import java.util.List; import java.util.Objects; import java.util.stream.Collectors; +import org.jspecify.annotations.Nullable; public class Service { @@ -22,19 +23,22 @@ public class Service { private final String serviceName; private final ServiceType serviceType; private final List handlers; + private final @Nullable String documentation; public Service( CharSequence targetPkg, CharSequence targetFqcn, String serviceName, ServiceType serviceType, - List handlers) { + List handlers, + @Nullable String documentation) { this.targetPkg = targetPkg; this.targetFqcn = targetFqcn; this.serviceName = serviceName; this.serviceType = serviceType; this.handlers = handlers; + this.documentation = documentation; } public CharSequence getTargetPkg() { @@ -68,6 +72,10 @@ public List getMethods() { return handlers; } + public @Nullable String getDocumentation() { + return documentation; + } + public static Builder builder() { return new Builder(); } @@ -78,6 +86,7 @@ public static class Builder { private String serviceName; private ServiceType serviceType; private final List handlers = new ArrayList<>(); + private String documentation; public Builder withTargetPkg(CharSequence targetPkg) { this.targetPkg = targetPkg; @@ -109,6 +118,11 @@ public Builder withHandler(Handler handler) { return this; } + public Builder withDocumentation(String documentation) { + this.documentation = documentation; + return this; + } + public CharSequence getTargetPkg() { return targetPkg; } @@ -155,7 +169,8 @@ public Service validateAndBuild() { Objects.requireNonNull(targetFqcn), Objects.requireNonNull(serviceName), Objects.requireNonNull(serviceType), - handlers); + handlers, + documentation); } } } diff --git a/sdk-api-gen-common/src/main/java/dev/restate/sdk/gen/template/HandlebarsTemplateEngine.java b/sdk-api-gen-common/src/main/java/dev/restate/sdk/gen/template/HandlebarsTemplateEngine.java index e0a0cdb6..23bbf1f2 100644 --- a/sdk-api-gen-common/src/main/java/dev/restate/sdk/gen/template/HandlebarsTemplateEngine.java +++ b/sdk-api-gen-common/src/main/java/dev/restate/sdk/gen/template/HandlebarsTemplateEngine.java @@ -13,6 +13,7 @@ import com.github.jknack.handlebars.Template; import com.github.jknack.handlebars.context.FieldValueResolver; import com.github.jknack.handlebars.helper.StringHelpers; +import com.github.jknack.handlebars.internal.text.StringEscapeUtils; import com.github.jknack.handlebars.io.TemplateLoader; import dev.restate.sdk.common.ServiceType; import dev.restate.sdk.common.function.ThrowingFunction; @@ -60,6 +61,7 @@ public HandlebarsTemplateEngine( } throw new IllegalStateException(); }); + handlebars.registerHelpers(StringEscapeUtils.class); this.templates = templates.entrySet().stream() @@ -104,6 +106,8 @@ static class ServiceTemplateModel { public final String generatedClassSimpleNamePrefix; public final String generatedClassSimpleName; public final String serviceName; + public final String documentation; + public final String serviceType; public final boolean isWorkflow; public final boolean isObject; @@ -119,6 +123,8 @@ private ServiceTemplateModel( this.generatedClassSimpleName = this.generatedClassSimpleNamePrefix + baseTemplateName; this.serviceName = inner.getFullyQualifiedServiceName(); + this.documentation = inner.getDocumentation(); + this.serviceType = inner.getServiceType().toString(); this.isWorkflow = inner.getServiceType() == ServiceType.WORKFLOW; this.isObject = inner.getServiceType() == ServiceType.VIRTUAL_OBJECT; @@ -149,6 +155,7 @@ static class HandlerTemplateModel { private final ServiceType serviceType; private final String definitionsClass; + public final String documentation; public final boolean inputEmpty; public final String inputFqcn; @@ -180,6 +187,7 @@ private HandlerTemplateModel( this.serviceType = serviceType; this.definitionsClass = definitionsClass; + this.documentation = inner.getDocumentation(); this.inputEmpty = inner.getInputType().isEmpty(); this.inputFqcn = inner.getInputType().getName(); diff --git a/sdk-api-gen/src/main/java/dev/restate/sdk/gen/ElementConverter.java b/sdk-api-gen/src/main/java/dev/restate/sdk/gen/ElementConverter.java index ba39da95..b7e9cf85 100644 --- a/sdk-api-gen/src/main/java/dev/restate/sdk/gen/ElementConverter.java +++ b/sdk-api-gen/src/main/java/dev/restate/sdk/gen/ElementConverter.java @@ -100,6 +100,7 @@ Service fromTypeElement(MetaRestateAnnotation metaAnnotation, TypeElement elemen .withTargetPkg(targetPkg) .withTargetFqcn(targetFqcn) .withServiceName(serviceName) + .withDocumentation(sanitizeJavadoc(elements.getDocComment(element))) .withServiceType(metaAnnotation.getServiceType()) .withHandlers(handlers) .validateAndBuild(); @@ -181,6 +182,7 @@ private Handler fromExecutableElement(ServiceType serviceType, ExecutableElement return new Handler.Builder() .withName(element.getSimpleName()) .withHandlerType(handlerType) + .withDocumentation(sanitizeJavadoc(elements.getDocComment(element))) .withInputAccept(inputAcceptFromParameterList(element.getParameters())) .withInputType(inputPayloadFromParameterList(element.getParameters())) .withOutputType(outputPayloadFromExecutableElement(element)) @@ -390,4 +392,10 @@ private static String boxedType(TypeMirror ty) { return ty.toString(); } } + + private static String sanitizeJavadoc(String documentation) { + // TODO this needs probably a bit more work, but eventually people will use markdown for + // javadocs anyway! + return documentation == null ? null : documentation.trim().replaceAll("[\t\n\r] *", "\n"); + } } diff --git a/sdk-api-gen/src/main/resources/templates/ServiceDefinitionFactory.hbs b/sdk-api-gen/src/main/resources/templates/ServiceDefinitionFactory.hbs index 0599736c..5c1a9202 100644 --- a/sdk-api-gen/src/main/resources/templates/ServiceDefinitionFactory.hbs +++ b/sdk-api-gen/src/main/resources/templates/ServiceDefinitionFactory.hbs @@ -15,12 +15,12 @@ public class {{generatedClassSimpleName}} implements dev.restate.sdk.common.sysc {{#if isExclusive}}dev.restate.sdk.common.HandlerType.EXCLUSIVE{{else if isWorkflow}}dev.restate.sdk.common.HandlerType.WORKFLOW{{else}}dev.restate.sdk.common.HandlerType.SHARED{{/if}}, {{inputSerdeRef}}, {{outputSerdeRef}} - ){{#if inputAcceptContentType}}.withAcceptContentType("{{inputAcceptContentType}}"){{/if}}, + ){{#if inputAcceptContentType}}.withAcceptContentType("{{inputAcceptContentType}}"){{/if}}{{#if documentation}}.withDocumentation("{{escapeJava documentation}}"){{/if}}, dev.restate.sdk.HandlerRunner.of(bindableService::{{name}}) ){{#unless @last}},{{/unless}} {{/handlers}} ) - ); + ){{#if documentation}}.withDocumentation("{{escapeJava documentation}}"){{/if}}; } @java.lang.Override diff --git a/sdk-common/src/main/java/dev/restate/sdk/common/syscalls/HandlerSpecification.java b/sdk-common/src/main/java/dev/restate/sdk/common/syscalls/HandlerSpecification.java index 73ab9b9e..cee10c9d 100644 --- a/sdk-common/src/main/java/dev/restate/sdk/common/syscalls/HandlerSpecification.java +++ b/sdk-common/src/main/java/dev/restate/sdk/common/syscalls/HandlerSpecification.java @@ -10,6 +10,8 @@ import dev.restate.sdk.common.HandlerType; import dev.restate.sdk.common.Serde; +import java.util.Collections; +import java.util.Map; import java.util.Objects; import org.jspecify.annotations.Nullable; @@ -20,23 +22,30 @@ public final class HandlerSpecification { private final @Nullable String acceptContentType; private final Serde requestSerde; private final Serde responseSerde; + private final @Nullable String documentation; + private final Map metadata; HandlerSpecification( String name, HandlerType handlerType, @Nullable String acceptContentType, Serde requestSerde, - Serde responseSerde) { + Serde responseSerde, + @Nullable String documentation, + Map metadata) { this.name = name; this.handlerType = handlerType; this.acceptContentType = acceptContentType; this.requestSerde = requestSerde; this.responseSerde = responseSerde; + this.documentation = documentation; + this.metadata = metadata; } public static HandlerSpecification of( String method, HandlerType handlerType, Serde requestSerde, Serde responseSerde) { - return new HandlerSpecification<>(method, handlerType, null, requestSerde, responseSerde); + return new HandlerSpecification<>( + method, handlerType, null, requestSerde, responseSerde, null, Collections.emptyMap()); } public String getName() { @@ -59,32 +68,47 @@ public Serde getResponseSerde() { return responseSerde; } + public @Nullable String getDocumentation() { + return documentation; + } + + public Map getMetadata() { + return metadata; + } + public HandlerSpecification withAcceptContentType(String acceptContentType) { return new HandlerSpecification<>( - name, handlerType, acceptContentType, requestSerde, responseSerde); + name, handlerType, acceptContentType, requestSerde, responseSerde, documentation, metadata); + } + + public HandlerSpecification withDocumentation(@Nullable String documentation) { + return new HandlerSpecification<>( + name, handlerType, acceptContentType, requestSerde, responseSerde, documentation, metadata); + } + + public HandlerSpecification withMetadata(Map metadata) { + return new HandlerSpecification<>( + name, handlerType, acceptContentType, requestSerde, responseSerde, documentation, metadata); } @Override public boolean equals(Object o) { if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - + if (!(o instanceof HandlerSpecification)) return false; HandlerSpecification that = (HandlerSpecification) o; return Objects.equals(name, that.name) && handlerType == that.handlerType && Objects.equals(acceptContentType, that.acceptContentType) && Objects.equals(requestSerde, that.requestSerde) - && Objects.equals(responseSerde, that.responseSerde); + && Objects.equals(responseSerde, that.responseSerde) + && Objects.equals(documentation, that.documentation) + && Objects.equals(metadata, that.metadata); } @Override public int hashCode() { - int result = Objects.hashCode(name); - result = 31 * result + Objects.hashCode(handlerType); - result = 31 * result + Objects.hashCode(acceptContentType); - result = 31 * result + Objects.hashCode(requestSerde); - result = 31 * result + Objects.hashCode(responseSerde); - return result; + return Objects.hash( + name, handlerType, acceptContentType, requestSerde, responseSerde, documentation, metadata); } @Override @@ -102,6 +126,10 @@ public String toString() { + requestSerde.contentType() + ", responseContentType=" + responseSerde.contentType() + + ", documentation=" + + documentation + + ", metadata=" + + metadata + '}'; } } diff --git a/sdk-common/src/main/java/dev/restate/sdk/common/syscalls/ServiceDefinition.java b/sdk-common/src/main/java/dev/restate/sdk/common/syscalls/ServiceDefinition.java index a4f644cb..e22da211 100644 --- a/sdk-common/src/main/java/dev/restate/sdk/common/syscalls/ServiceDefinition.java +++ b/sdk-common/src/main/java/dev/restate/sdk/common/syscalls/ServiceDefinition.java @@ -12,19 +12,27 @@ import java.util.*; import java.util.function.Function; import java.util.stream.Collectors; +import org.jspecify.annotations.Nullable; public final class ServiceDefinition { private final String serviceName; private final ServiceType serviceType; private final Map> handlers; + private final @Nullable String documentation; + private final Map metadata; - ServiceDefinition(String name, ServiceType ty, Collection> handlers) { - this.serviceName = name; - this.serviceType = ty; - this.handlers = - handlers.stream() - .collect(Collectors.toMap(h -> h.getSpec().getName(), Function.identity())); + private ServiceDefinition( + String serviceName, + ServiceType serviceType, + Map> handlers, + @Nullable String documentation, + Map metadata) { + this.serviceName = serviceName; + this.serviceType = serviceType; + this.handlers = handlers; + this.documentation = documentation; + this.metadata = metadata; } public String getServiceName() { @@ -43,6 +51,22 @@ public ServiceType getServiceType() { return handlers.get(name); } + public @Nullable String getDocumentation() { + return documentation; + } + + public Map getMetadata() { + return metadata; + } + + public ServiceDefinition withDocumentation(@Nullable String documentation) { + return new ServiceDefinition<>(serviceName, serviceType, handlers, documentation, metadata); + } + + public ServiceDefinition withMetadata(Map metadata) { + return new ServiceDefinition<>(serviceName, serviceType, handlers, documentation, metadata); + } + @Override public boolean equals(Object object) { if (this == object) return true; @@ -60,6 +84,12 @@ public int hashCode() { public static ServiceDefinition of( String name, ServiceType ty, Collection> handlers) { - return new ServiceDefinition<>(name, ty, handlers); + return new ServiceDefinition<>( + name, + ty, + handlers.stream() + .collect(Collectors.toMap(h -> h.getSpec().getName(), Function.identity())), + null, + Collections.emptyMap()); } } diff --git a/sdk-core/src/main/java/dev/restate/sdk/core/EndpointManifest.java b/sdk-core/src/main/java/dev/restate/sdk/core/EndpointManifest.java index 40fac531..acc9935b 100644 --- a/sdk-core/src/main/java/dev/restate/sdk/core/EndpointManifest.java +++ b/sdk-core/src/main/java/dev/restate/sdk/core/EndpointManifest.java @@ -45,6 +45,19 @@ public EndpointManifest( new Service() .withName(svc.getServiceName()) .withTy(convertServiceType(svc.getServiceType())) + .withDocumentation(svc.getDocumentation()) + .withMetadata( + svc.getMetadata().entrySet().stream() + .reduce( + new Metadata__1(), + (meta, entry) -> + meta.withAdditionalProperty( + entry.getKey(), entry.getValue()), + (m1, m2) -> { + m2.getAdditionalProperties() + .forEach(m1::setAdditionalProperty); + return m1; + })) .withHandlers( svc.getHandlers().stream() .map(EndpointManifest::convertHandler) @@ -74,7 +87,17 @@ private static Handler convertHandler(HandlerDefinition handler) { .withName(spec.getName()) .withTy(convertHandlerType(spec.getHandlerType())) .withInput(convertHandlerInput(spec)) - .withOutput(convertHandlerOutput(spec)); + .withOutput(convertHandlerOutput(spec)) + .withDocumentation(spec.getDocumentation()) + .withMetadata( + spec.getMetadata().entrySet().stream() + .reduce( + new Metadata(), + (meta, entry) -> meta.withAdditionalProperty(entry.getKey(), entry.getValue()), + (m1, m2) -> { + m2.getAdditionalProperties().forEach(m1::setAdditionalProperty); + return m1; + })); } private static Input convertHandlerInput(HandlerSpecification spec) { diff --git a/sdk-core/src/main/java/dev/restate/sdk/core/ServiceProtocol.java b/sdk-core/src/main/java/dev/restate/sdk/core/ServiceProtocol.java index 66db14db..6aa0033b 100644 --- a/sdk-core/src/main/java/dev/restate/sdk/core/ServiceProtocol.java +++ b/sdk-core/src/main/java/dev/restate/sdk/core/ServiceProtocol.java @@ -8,11 +8,16 @@ // https://github.com/restatedev/sdk-java/blob/main/LICENSE package dev.restate.sdk.core; +import com.fasterxml.jackson.annotation.JsonFilter; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter; +import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider; import dev.restate.generated.service.discovery.Discovery; import dev.restate.generated.service.protocol.Protocol; import dev.restate.sdk.core.manifest.EndpointManifestSchema; +import dev.restate.sdk.core.manifest.Handler; +import dev.restate.sdk.core.manifest.Service; import java.util.Objects; import java.util.Optional; @@ -25,7 +30,7 @@ class ServiceProtocol { static final Discovery.ServiceDiscoveryProtocolVersion MIN_SERVICE_DISCOVERY_PROTOCOL_VERSION = Discovery.ServiceDiscoveryProtocolVersion.V1; static final Discovery.ServiceDiscoveryProtocolVersion MAX_SERVICE_DISCOVERY_PROTOCOL_VERSION = - Discovery.ServiceDiscoveryProtocolVersion.V1; + Discovery.ServiceDiscoveryProtocolVersion.V2; static Protocol.ServiceProtocolVersion parseServiceProtocolVersion(String version) { version = version.trim(); @@ -113,6 +118,9 @@ static Optional parseServiceDiscovery if (versionString.equals("application/vnd.restate.endpointmanifest.v1+json")) { return Optional.of(Discovery.ServiceDiscoveryProtocolVersion.V1); } + if (versionString.equals("application/vnd.restate.endpointmanifest.v2+json")) { + return Optional.of(Discovery.ServiceDiscoveryProtocolVersion.V2); + } return Optional.empty(); } @@ -121,6 +129,9 @@ static String serviceDiscoveryProtocolVersionToHeaderValue( if (Objects.requireNonNull(version) == Discovery.ServiceDiscoveryProtocolVersion.V1) { return "application/vnd.restate.endpointmanifest.v1+json"; } + if (Objects.requireNonNull(version) == Discovery.ServiceDiscoveryProtocolVersion.V2) { + return "application/vnd.restate.endpointmanifest.v2+json"; + } throw new IllegalArgumentException( String.format( "Service discovery protocol version '%s' has no header value", version.getNumber())); @@ -128,23 +139,31 @@ static String serviceDiscoveryProtocolVersionToHeaderValue( private static final ObjectMapper MANIFEST_OBJECT_MAPPER = new ObjectMapper(); + @JsonFilter("V2FieldsFilter") + interface V2Mixin {} + + static { + // Mixin to add fields filter, used to filter v2 fields + MANIFEST_OBJECT_MAPPER.addMixIn(Service.class, V2Mixin.class); + MANIFEST_OBJECT_MAPPER.addMixIn(Handler.class, V2Mixin.class); + } + static byte[] serializeManifest( Discovery.ServiceDiscoveryProtocolVersion serviceDiscoveryProtocolVersion, EndpointManifestSchema response) throws ProtocolException { - if (serviceDiscoveryProtocolVersion == Discovery.ServiceDiscoveryProtocolVersion.V1) { - try { - return MANIFEST_OBJECT_MAPPER.writeValueAsBytes(response); - } catch (JsonProcessingException e) { - throw new ProtocolException( - "Error when serializing the manifest", ProtocolException.INTERNAL_CODE, e); - } + try { + // Don't serialize the documentation and metadata fields for V1! + SimpleBeanPropertyFilter filter = + serviceDiscoveryProtocolVersion == Discovery.ServiceDiscoveryProtocolVersion.V1 + ? SimpleBeanPropertyFilter.serializeAllExcept("documentation", "metadata") + : SimpleBeanPropertyFilter.serializeAll(); + return MANIFEST_OBJECT_MAPPER + .writer(new SimpleFilterProvider().addFilter("V2FieldsFilter", filter)) + .writeValueAsBytes(response); + } catch (JsonProcessingException e) { + throw new ProtocolException( + "Error when serializing the manifest", ProtocolException.INTERNAL_CODE, e); } - - throw new ProtocolException( - String.format( - "DiscoveryResponseSerializer does not support service discovery protocol '%s'", - serviceDiscoveryProtocolVersion.getNumber()), - ProtocolException.UNSUPPORTED_MEDIA_TYPE_CODE); } }