Skip to content

Commit cda07ef

Browse files
committed
Add logic to convert from Legacy Events to CloudEvents.
This allows received Legacy Events to be handled by function code that expects a CloudEvent input. The translation logic and tests are based closely on the corresponding code in https://github.com/GoogleCloudPlatform/functions-framework-dotnet. The new code also passes the relevant conformance tests in https://github.com/GoogleCloudPlatform/functions-framework-conformance, once GoogleCloudPlatform/functions-framework-conformance#30 is applied. For now I'm running those tests manually. I plan at least to commit the script and test functions that allow me to do so.
1 parent f3e0994 commit cda07ef

File tree

13 files changed

+619
-26
lines changed

13 files changed

+619
-26
lines changed

invoker/core/src/main/java/com/google/cloud/functions/invoker/BackgroundFunctionExecutor.java

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import io.cloudevents.core.message.impl.UnknownEncodingMessageReader;
3333
import java.io.BufferedReader;
3434
import java.io.IOException;
35+
import java.io.Reader;
3536
import java.lang.reflect.Type;
3637
import java.time.OffsetDateTime;
3738
import java.time.format.DateTimeFormatter;
@@ -171,17 +172,21 @@ static Optional<Type> backgroundFunctionTypeArgument(
171172

172173
private static Event parseLegacyEvent(HttpServletRequest req) throws IOException {
173174
try (BufferedReader bodyReader = req.getReader()) {
174-
// A Type Adapter is required to set the type of the JsonObject because CloudFunctionsContext
175-
// is abstract and Gson default behavior instantiates the type provided.
176-
TypeAdapter<CloudFunctionsContext> typeAdapter =
177-
CloudFunctionsContext.typeAdapter(new Gson());
178-
Gson gson = new GsonBuilder()
179-
.registerTypeAdapter(CloudFunctionsContext.class, typeAdapter)
180-
.registerTypeAdapter(Event.class, new Event.EventDeserializer())
181-
.create();
182-
return gson.fromJson(bodyReader, Event.class);
175+
return parseLegacyEvent(bodyReader);
183176
}
184177
}
178+
179+
static Event parseLegacyEvent(Reader reader) throws IOException {
180+
// A Type Adapter is required to set the type of the JsonObject because CloudFunctionsContext
181+
// is abstract and Gson default behavior instantiates the type provided.
182+
TypeAdapter<CloudFunctionsContext> typeAdapter =
183+
CloudFunctionsContext.typeAdapter(new Gson());
184+
Gson gson = new GsonBuilder()
185+
.registerTypeAdapter(CloudFunctionsContext.class, typeAdapter)
186+
.registerTypeAdapter(Event.class, new Event.EventDeserializer())
187+
.create();
188+
return gson.fromJson(reader, Event.class);
189+
}
185190

186191
private static Context contextFromCloudEvent(CloudEvent cloudEvent) {
187192
OffsetDateTime timestamp = Optional.ofNullable(cloudEvent.getTime()).orElse(OffsetDateTime.now());
@@ -301,8 +306,8 @@ private static class CloudEventFunctionExecutor extends FunctionExecutor<Void>{
301306

302307
@Override
303308
void serviceLegacyEvent(Event legacyEvent) throws Exception {
304-
throw new UnsupportedOperationException(
305-
"Conversion from legacy events to CloudEvents not yet implemented");
309+
CloudEvent cloudEvent = GcfEvents.convertToCloudEvent(legacyEvent);
310+
function.accept(cloudEvent);
306311
}
307312

308313
@Override

invoker/core/src/main/java/com/google/cloud/functions/invoker/CloudFunctionsContext.java

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,12 @@
1717
import com.google.auto.value.AutoValue;
1818
import com.google.cloud.functions.Context;
1919
import com.google.gson.Gson;
20+
import com.google.gson.GsonBuilder;
2021
import com.google.gson.TypeAdapter;
2122
import java.lang.annotation.Retention;
2223
import java.lang.annotation.RetentionPolicy;
2324
import java.util.Collections;
25+
import java.util.List;
2426
import java.util.Map;
2527

2628
/** Event context (metadata) for events handled by Cloud Functions. */
@@ -46,6 +48,9 @@ abstract class CloudFunctionsContext implements Context {
4648
@Nullable
4749
public abstract String resource();
4850

51+
// TODO: expose this in the Context interface (as a default method).
52+
abstract Map<String, String> params();
53+
4954
@Override
5055
public abstract Map<String, String> attributes();
5156

@@ -55,6 +60,7 @@ public static TypeAdapter<CloudFunctionsContext> typeAdapter(Gson gson) {
5560

5661
static Builder builder() {
5762
return new AutoValue_CloudFunctionsContext.Builder()
63+
.setParams(Collections.emptyMap())
5864
.setAttributes(Collections.emptyMap());
5965
}
6066

@@ -64,8 +70,49 @@ abstract static class Builder {
6470
abstract Builder setTimestamp(String x);
6571
abstract Builder setEventType(String x);
6672
abstract Builder setResource(String x);
73+
abstract Builder setParams(Map<String, String> x);
6774
abstract Builder setAttributes(Map<String, String> value);
6875

6976
abstract CloudFunctionsContext build();
7077
}
78+
79+
/**
80+
* Depending on the event type, the {@link Context#resource()} field is either a plain string or a string
81+
* representing a JSON object. In the latter case, this class allows us to redeserialize that JSON string
82+
* into its components.
83+
*/
84+
@AutoValue
85+
abstract static class Resource {
86+
abstract @Nullable String service();
87+
abstract String name();
88+
abstract @Nullable String type();
89+
90+
static TypeAdapter<Resource> typeAdapter(Gson gson) {
91+
return new AutoValue_CloudFunctionsContext_Resource.GsonTypeAdapter(gson);
92+
}
93+
94+
static Resource from(String s) {
95+
if (s.startsWith("\"") && s.endsWith("\"")) {
96+
return builder().setName(s.substring(1, s.length() - 1)).build();
97+
}
98+
if (s.startsWith("{") && (s.endsWith("}") || s.endsWith("}\n"))) {
99+
TypeAdapter<Resource> typeAdapter = typeAdapter(new Gson());
100+
Gson gson = new GsonBuilder().registerTypeAdapter(Resource.class, typeAdapter).create();
101+
return gson.fromJson(s, Resource.class);
102+
}
103+
throw new IllegalArgumentException("Unexpected resource syntax: " + s);
104+
}
105+
106+
static Builder builder() {
107+
return new AutoValue_CloudFunctionsContext_Resource.Builder();
108+
}
109+
110+
@AutoValue.Builder
111+
abstract static class Builder {
112+
abstract Builder setService(String x);
113+
abstract Builder setName(String x);
114+
abstract Builder setType(String x);
115+
abstract Resource build();
116+
}
117+
}
71118
}

invoker/core/src/main/java/com/google/cloud/functions/invoker/Event.java

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
package com.google.cloud.functions.invoker;
1616

17+
import com.google.auto.value.AutoValue;
1718
import com.google.gson.JsonDeserializationContext;
1819
import com.google.gson.JsonDeserializer;
1920
import com.google.gson.JsonElement;
@@ -25,23 +26,15 @@
2526
* Represents an event that should be handled by a background function. This is an internal format
2627
* which is later converted to actual background function parameter types.
2728
*/
28-
class Event {
29-
30-
private JsonElement data;
31-
private CloudFunctionsContext context;
32-
33-
Event(JsonElement data, CloudFunctionsContext context) {
34-
this.data = data;
35-
this.context = context;
29+
@AutoValue
30+
abstract class Event {
31+
static Event of(JsonElement data, CloudFunctionsContext context) {
32+
return new AutoValue_Event(data, context);
3633
}
3734

38-
JsonElement getData() {
39-
return data;
40-
}
35+
abstract JsonElement getData();
4136

42-
CloudFunctionsContext getContext() {
43-
return context;
44-
}
37+
abstract CloudFunctionsContext getContext();
4538

4639
/** Custom deserializer that supports both GCF beta and GCF GA event formats. */
4740
static class EventDeserializer implements JsonDeserializer<Event> {
@@ -67,7 +60,7 @@ public Event deserialize(
6760
jsonDeserializationContext.deserialize(
6861
adjustContextResource(rootCopy), CloudFunctionsContext.class);
6962
}
70-
return new Event(data, context);
63+
return Event.of(data, context);
7164
}
7265

7366
/**

0 commit comments

Comments
 (0)