Skip to content

Commit 4c9f4cb

Browse files
Add data_base64 support with minimal API
- CloudEvent[A]: add DataBase64 field; unmarshal/marshal both data and data_base64 - RawEvent = CloudEvent[json.RawMessage]: same type alias, supports data_base64 - BytesForSignature(RawEvent) for signature verification - DecodeHeader(data) for header-only parse (scaling) - IsJSONDataContentType(ct) for wire-form choice - Single unmarshal path (unmarshalCloudEventWithPayload), setHeaderField - No RawCloudEvent type; no validation/constructors/conversion helpers Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent fd6b3af commit 4c9f4cb

3 files changed

Lines changed: 673 additions & 36 deletions

File tree

cloudevent.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package cloudevent
33

44
import (
55
"encoding/json"
6+
"mime"
7+
"strings"
68
"time"
79
)
810

@@ -95,11 +97,30 @@ type CloudEvent[A any] struct {
9597
CloudEventHeader
9698
// Data contains domain-specific information about the event.
9799
Data A `json:"data"`
100+
101+
DataBase64 string `json:"data_base64,omitempty"`
98102
}
99103

100104
// RawEvent is a cloudevent with a json.RawMessage data field.
105+
// It supports both "data" and "data_base64" (CloudEvents JSON spec).
101106
type RawEvent = CloudEvent[json.RawMessage]
102107

108+
// BytesForSignature returns the bytes that were signed (wire form of data or data_base64) for a RawEvent.
109+
// Use for signature verification; not the same as Data when the CE used data_base64.
110+
func BytesForSignature(ev RawEvent) []byte {
111+
if ev.DataBase64 != "" {
112+
return []byte(ev.DataBase64)
113+
}
114+
return ev.Data
115+
}
116+
117+
// IsJSONDataContentType returns true if the MIME type indicates a JSON payload.
118+
// Matches "application/json" and any "+json" suffix type (e.g. "application/cloudevents+json").
119+
func IsJSONDataContentType(ct string) bool {
120+
parsed, _, err := mime.ParseMediaType(strings.TrimSpace(ct))
121+
return err == nil && (parsed == "application/json" || strings.HasSuffix(parsed, "+json"))
122+
}
123+
103124
// Equals returns true if the two CloudEventHeaders share the same IndexKey.
104125
func (c *CloudEventHeader) Equals(other CloudEventHeader) bool {
105126
return c.Key() == other.Key()

cloudevent_json.go

Lines changed: 155 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,99 @@
11
package cloudevent
22

33
import (
4+
"encoding/base64"
45
"encoding/json"
6+
"fmt"
57
"reflect"
68
"strings"
79

810
"github.com/tidwall/sjson"
911
)
1012

11-
var definedCloudeEventHdrFields = getJSONFieldNames(reflect.TypeOf(CloudEventHeader{}))
13+
var definedCloudeEventHdrFields = getJSONFieldNames(reflect.TypeFor[CloudEventHeader]())
1214

1315
type cloudEventHeader CloudEventHeader
1416

1517
// UnmarshalJSON implements custom JSON unmarshaling for CloudEvent.
18+
// It transparently handles both "data" and "data_base64" wire formats.
19+
// For RawEvent (CloudEvent[json.RawMessage]), Data is set to the raw payload bytes.
1620
func (c *CloudEvent[A]) UnmarshalJSON(data []byte) error {
17-
var err error
18-
c.CloudEventHeader, err = unmarshalCloudEvent(data, c.setDataField)
19-
return err
21+
var dataRaw json.RawMessage
22+
var dataBase64 string
23+
header, err := unmarshalCloudEventWithPayload(data, func(d json.RawMessage, b64 string) error {
24+
dataRaw = d
25+
dataBase64 = b64
26+
return nil
27+
})
28+
if err != nil {
29+
return err
30+
}
31+
c.CloudEventHeader = header
32+
if dataRaw != nil && dataBase64 != "" {
33+
return fmt.Errorf("cloudevent: both \"data\" and \"data_base64\" present; only one allowed")
34+
}
35+
// RawEvent: assign payload bytes directly to Data
36+
if ptr, ok := (any)(&c.Data).(*json.RawMessage); ok {
37+
if dataBase64 != "" {
38+
decoded, err := base64.StdEncoding.DecodeString(dataBase64)
39+
if err != nil {
40+
return err
41+
}
42+
*ptr = decoded
43+
c.DataBase64 = dataBase64
44+
} else {
45+
*ptr = dataRaw
46+
c.DataBase64 = ""
47+
}
48+
return nil
49+
}
50+
// Typed CloudEvent: unmarshal into Data
51+
if dataBase64 != "" {
52+
decoded, err := base64.StdEncoding.DecodeString(dataBase64)
53+
if err != nil {
54+
return err
55+
}
56+
if err := json.Unmarshal(decoded, &c.Data); err != nil {
57+
return err
58+
}
59+
c.DataBase64 = dataBase64
60+
} else if dataRaw != nil {
61+
if err := json.Unmarshal(dataRaw, &c.Data); err != nil {
62+
return err
63+
}
64+
}
65+
return nil
2066
}
2167

22-
// MarshalJSON implements custom JSON marshaling for CloudEventHeader.
68+
// MarshalJSON implements custom JSON marshaling for CloudEvent[A].
69+
// When DataBase64 is set, emits "data_base64"; otherwise emits "data".
70+
// For RawEvent, uses DataContentType to choose wire form when DataBase64 is empty.
2371
func (c CloudEvent[A]) MarshalJSON() ([]byte, error) {
24-
// Marshal the base struct
2572
data, err := json.Marshal(c.CloudEventHeader)
2673
if err != nil {
2774
return nil, err
2875
}
29-
data, err = sjson.SetBytes(data, "data", c.Data)
76+
// RawEvent: choose data vs data_base64 by content type when DataBase64 not set
77+
if raw, ok := (any)(c.Data).(json.RawMessage); ok {
78+
if len(raw) > 0 || c.DataBase64 != "" {
79+
if c.DataBase64 != "" {
80+
data, err = sjson.SetBytes(data, "data_base64", c.DataBase64)
81+
} else if IsJSONDataContentType(c.DataContentType) || (c.DataContentType == "" && json.Valid(raw)) {
82+
data, err = sjson.SetRawBytes(data, "data", raw)
83+
} else {
84+
data, err = sjson.SetBytes(data, "data_base64", base64.StdEncoding.EncodeToString(raw))
85+
}
86+
if err != nil {
87+
return nil, err
88+
}
89+
}
90+
return data, nil
91+
}
92+
if c.DataBase64 != "" {
93+
data, err = sjson.SetBytes(data, "data_base64", c.DataBase64)
94+
} else {
95+
data, err = sjson.SetBytes(data, "data", c.Data)
96+
}
3097
if err != nil {
3198
return nil, err
3299
}
@@ -42,21 +109,18 @@ func (c *CloudEventHeader) UnmarshalJSON(data []byte) error {
42109

43110
// MarshalJSON implements custom JSON marshaling for CloudEventHeader.
44111
func (c CloudEventHeader) MarshalJSON() ([]byte, error) {
45-
// Marshal the base struct
46112
aux := (cloudEventHeader)(c)
47113
aux.SpecVersion = SpecVersion
48114
data, err := json.Marshal(aux)
49115
if err != nil {
50116
return nil, err
51117
}
52-
// Add all extras using sjson]
53118
for k, v := range c.Extras {
54119
data, err = sjson.SetBytes(data, k, v)
55120
if err != nil {
56121
return nil, err
57122
}
58123
}
59-
60124
return data, nil
61125
}
62126

@@ -90,50 +154,105 @@ func getJSONFieldNames(t reflect.Type) map[string]struct{} {
90154

91155
// unmarshalCloudEvent unmarshals the CloudEventHeader and data field.
92156
func unmarshalCloudEvent(data []byte, dataFunc func(json.RawMessage) error) (CloudEventHeader, error) {
93-
c := CloudEventHeader{}
94-
aux := cloudEventHeader{}
95-
// Unmarshal known fields directly into the struct
96-
if err := json.Unmarshal(data, &aux); err != nil {
97-
return c, err
98-
}
99-
aux.SpecVersion = SpecVersion
100-
c = (CloudEventHeader)(aux)
101-
// Create a map to hold all JSON fields
157+
return unmarshalCloudEventWithPayload(data, func(dataRaw json.RawMessage, _ string) error {
158+
return dataFunc(dataRaw)
159+
})
160+
}
161+
162+
// unmarshalCloudEventWithPayload unmarshals the CloudEventHeader and returns both
163+
// "data" and "data_base64" for CloudEvent payload. Single pass: parse once into
164+
// map[string]json.RawMessage, then fill header and payload from the map.
165+
func unmarshalCloudEventWithPayload(data []byte, payloadFunc func(dataRaw json.RawMessage, dataBase64 string) error) (CloudEventHeader, error) {
102166
rawFields := make(map[string]json.RawMessage)
103167
if err := json.Unmarshal(data, &rawFields); err != nil {
104-
return c, err
168+
return CloudEventHeader{}, err
105169
}
106170

107-
// Separate known and unknown fields
108-
for key, rawValue := range rawFields {
109-
if _, ok := definedCloudeEventHdrFields[key]; ok {
110-
// Skip defined fields
171+
var c CloudEventHeader
172+
for key, raw := range rawFields {
173+
if key == "data" || key == "data_base64" {
111174
continue
112175
}
113-
if key == "data" {
114-
if err := dataFunc(rawValue); err != nil {
176+
if _, defined := definedCloudeEventHdrFields[key]; defined {
177+
if err := setHeaderField(&c, key, raw); err != nil {
115178
return c, err
116179
}
117-
continue
180+
} else {
181+
if c.Extras == nil {
182+
c.Extras = make(map[string]any)
183+
}
184+
var value any
185+
if err := json.Unmarshal(raw, &value); err != nil {
186+
return c, err
187+
}
188+
c.Extras[key] = value
118189
}
119-
if c.Extras == nil {
120-
c.Extras = make(map[string]any)
190+
}
191+
c.SpecVersion = SpecVersion
192+
193+
var dataRaw json.RawMessage
194+
var dataBase64 string
195+
if raw, ok := rawFields["data_base64"]; ok && len(raw) > 0 {
196+
if err := json.Unmarshal(raw, &dataBase64); err != nil {
197+
return c, err
121198
}
122-
var value any
123-
if err := json.Unmarshal(rawValue, &value); err != nil {
199+
}
200+
if raw, ok := rawFields["data"]; ok {
201+
dataRaw = raw
202+
}
203+
if dataRaw != nil || dataBase64 != "" {
204+
if err := payloadFunc(dataRaw, dataBase64); err != nil {
124205
return c, err
125206
}
126-
c.Extras[key] = value
127207
}
128208
return c, nil
129209
}
130210

211+
// setHeaderField unmarshals raw into the CloudEventHeader field for key.
212+
func setHeaderField(c *CloudEventHeader, key string, raw json.RawMessage) error {
213+
switch key {
214+
case "id":
215+
return json.Unmarshal(raw, &c.ID)
216+
case "source":
217+
return json.Unmarshal(raw, &c.Source)
218+
case "producer":
219+
return json.Unmarshal(raw, &c.Producer)
220+
case "specversion":
221+
return json.Unmarshal(raw, &c.SpecVersion)
222+
case "subject":
223+
return json.Unmarshal(raw, &c.Subject)
224+
case "time":
225+
return json.Unmarshal(raw, &c.Time)
226+
case "type":
227+
return json.Unmarshal(raw, &c.Type)
228+
case "datacontenttype":
229+
return json.Unmarshal(raw, &c.DataContentType)
230+
case "dataschema":
231+
return json.Unmarshal(raw, &c.DataSchema)
232+
case "dataversion":
233+
return json.Unmarshal(raw, &c.DataVersion)
234+
case "signature":
235+
return json.Unmarshal(raw, &c.Signature)
236+
case "tags":
237+
return json.Unmarshal(raw, &c.Tags)
238+
default:
239+
return nil
240+
}
241+
}
242+
131243
// ignoreDataField is a function that ignores the data field.
132244
// It is used when unmarshalling the CloudEventHeader so that the data field is not added to the Extras map.
133245
func ignoreDataField(json.RawMessage) error { return nil }
134246

135-
// setDataField is a function that sets the data field.
136-
// It is used to unmarshal the data field into the CloudEvent[A].Data field.
137-
func (c *CloudEvent[A]) setDataField(data json.RawMessage) error {
138-
return json.Unmarshal(data, &c.Data)
247+
// DecodeHeader parses only the CloudEvent header fields from JSON, skipping the data payload.
248+
// More efficient than unmarshaling a full event when only metadata is needed (e.g. for scaling).
249+
//
250+
// Equivalent to json.Unmarshal(data, &header) since CloudEventHeader.UnmarshalJSON
251+
// already ignores the data field, but provided as an explicit API for discoverability.
252+
func DecodeHeader(data []byte) (CloudEventHeader, error) {
253+
var hdr CloudEventHeader
254+
if err := json.Unmarshal(data, &hdr); err != nil {
255+
return CloudEventHeader{}, err
256+
}
257+
return hdr, nil
139258
}

0 commit comments

Comments
 (0)