Skip to content

Commit 7441146

Browse files
authored
Merge pull request #40 from DIMO-Network/add-data-base64-support
Add data_base64 support and simplify CE JSON marshaling
2 parents fd6b3af + 8085982 commit 7441146

3 files changed

Lines changed: 469 additions & 30 deletions

File tree

cloudevent.go

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,14 @@
22
package cloudevent
33

44
import (
5+
"encoding/base64"
56
"encoding/json"
7+
"fmt"
8+
"mime"
9+
"strings"
610
"time"
11+
12+
"github.com/tidwall/sjson"
713
)
814

915
const (
@@ -95,10 +101,90 @@ type CloudEvent[A any] struct {
95101
CloudEventHeader
96102
// Data contains domain-specific information about the event.
97103
Data A `json:"data"`
104+
105+
DataBase64 string `json:"data_base64,omitempty"`
98106
}
99107

100108
// RawEvent is a cloudevent with a json.RawMessage data field.
101-
type RawEvent = CloudEvent[json.RawMessage]
109+
// It supports both "data" and "data_base64" (CloudEvents JSON spec).
110+
type RawEvent struct {
111+
CloudEventHeader
112+
Data json.RawMessage `json:"data,omitempty"`
113+
114+
// DataBase64 is the raw "data_base64" string when the event was received with
115+
// data_base64 (CloudEvents spec). When set, MarshalJSON emits data_base64 for
116+
// round-trip; otherwise wire form is chosen from DataContentType and Data.
117+
DataBase64 string `json:"data_base64,omitempty"`
118+
}
119+
120+
// BytesForSignature returns the bytes that were signed (wire form of data or data_base64).
121+
// Use for signature verification; not the same as Data when the CE used data_base64.
122+
func (r RawEvent) BytesForSignature() []byte {
123+
if r.DataBase64 != "" {
124+
return []byte(r.DataBase64)
125+
}
126+
return r.Data
127+
}
128+
129+
// UnmarshalJSON implements json.Unmarshaler so that both "data" and "data_base64"
130+
// are supported; Data is always set to the resolved payload bytes.
131+
func (r *RawEvent) UnmarshalJSON(data []byte) error {
132+
var dataRaw json.RawMessage
133+
var dataBase64 string
134+
header, err := unmarshalCloudEventWithPayload(data, func(d json.RawMessage, b64 string) error {
135+
dataRaw = d
136+
dataBase64 = b64
137+
return nil
138+
})
139+
if err != nil {
140+
return err
141+
}
142+
r.CloudEventHeader = header
143+
if dataRaw != nil && dataBase64 != "" {
144+
return fmt.Errorf("cloudevent: both \"data\" and \"data_base64\" present; only one allowed")
145+
}
146+
if dataBase64 != "" {
147+
decoded, err := base64.StdEncoding.DecodeString(dataBase64)
148+
if err != nil {
149+
return err
150+
}
151+
r.Data = decoded
152+
r.DataBase64 = dataBase64
153+
} else {
154+
r.Data = dataRaw
155+
r.DataBase64 = ""
156+
}
157+
return nil
158+
}
159+
160+
// IsJSONDataContentType returns true if the MIME type indicates a JSON payload.
161+
// Matches "application/json" and any "+json" suffix type (e.g. "application/cloudevents+json").
162+
func IsJSONDataContentType(ct string) bool {
163+
parsed, _, err := mime.ParseMediaType(strings.TrimSpace(ct))
164+
return err == nil && (parsed == "application/json" || strings.HasSuffix(parsed, "+json"))
165+
}
166+
167+
// MarshalJSON implements json.Marshaler. Uses DataContentType to choose wire form:
168+
// application/json -> "data"; otherwise -> "data_base64" (CloudEvents spec).
169+
func (r RawEvent) MarshalJSON() ([]byte, error) {
170+
data, err := json.Marshal(r.CloudEventHeader)
171+
if err != nil {
172+
return nil, err
173+
}
174+
if len(r.Data) > 0 || r.DataBase64 != "" {
175+
if r.DataBase64 != "" {
176+
data, err = sjson.SetBytes(data, "data_base64", r.DataBase64)
177+
} else if IsJSONDataContentType(r.DataContentType) || (r.DataContentType == "" && json.Valid(r.Data)) {
178+
data, err = sjson.SetRawBytes(data, "data", r.Data)
179+
} else {
180+
data, err = sjson.SetBytes(data, "data_base64", base64.StdEncoding.EncodeToString(r.Data))
181+
}
182+
if err != nil {
183+
return nil, err
184+
}
185+
}
186+
return data, nil
187+
}
102188

103189
// Equals returns true if the two CloudEventHeaders share the same IndexKey.
104190
func (c *CloudEventHeader) Equals(other CloudEventHeader) bool {

cloudevent_json.go

Lines changed: 63 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -2,31 +2,56 @@ package cloudevent
22

33
import (
44
"encoding/json"
5+
"fmt"
56
"reflect"
67
"strings"
78

89
"github.com/tidwall/sjson"
910
)
1011

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

1314
type cloudEventHeader CloudEventHeader
1415

1516
// UnmarshalJSON implements custom JSON unmarshaling for CloudEvent.
17+
// It transparently handles both "data" and "data_base64" wire formats.
1618
func (c *CloudEvent[A]) UnmarshalJSON(data []byte) error {
17-
var err error
18-
c.CloudEventHeader, err = unmarshalCloudEvent(data, c.setDataField)
19-
return err
19+
var dataRaw json.RawMessage
20+
var dataBase64 string
21+
header, err := unmarshalCloudEventWithPayload(data, func(d json.RawMessage, b64 string) error {
22+
dataRaw = d
23+
dataBase64 = b64
24+
return nil
25+
})
26+
if err != nil {
27+
return err
28+
}
29+
c.CloudEventHeader = header
30+
if dataRaw != nil && dataBase64 != "" {
31+
return fmt.Errorf("cloudevent: both \"data\" and \"data_base64\" present; only one allowed")
32+
}
33+
if dataBase64 != "" {
34+
c.DataBase64 = dataBase64
35+
} else if dataRaw != nil {
36+
if err := json.Unmarshal(dataRaw, &c.Data); err != nil {
37+
return err
38+
}
39+
}
40+
return nil
2041
}
2142

22-
// MarshalJSON implements custom JSON marshaling for CloudEventHeader.
43+
// MarshalJSON implements custom JSON marshaling for CloudEvent[A].
44+
// When DataBase64 is set, emits "data_base64"; otherwise emits "data".
2345
func (c CloudEvent[A]) MarshalJSON() ([]byte, error) {
24-
// Marshal the base struct
2546
data, err := json.Marshal(c.CloudEventHeader)
2647
if err != nil {
2748
return nil, err
2849
}
29-
data, err = sjson.SetBytes(data, "data", c.Data)
50+
if c.DataBase64 != "" {
51+
data, err = sjson.SetBytes(data, "data_base64", c.DataBase64)
52+
} else {
53+
data, err = sjson.SetBytes(data, "data", c.Data)
54+
}
3055
if err != nil {
3156
return nil, err
3257
}
@@ -42,21 +67,18 @@ func (c *CloudEventHeader) UnmarshalJSON(data []byte) error {
4267

4368
// MarshalJSON implements custom JSON marshaling for CloudEventHeader.
4469
func (c CloudEventHeader) MarshalJSON() ([]byte, error) {
45-
// Marshal the base struct
4670
aux := (cloudEventHeader)(c)
4771
aux.SpecVersion = SpecVersion
4872
data, err := json.Marshal(aux)
4973
if err != nil {
5074
return nil, err
5175
}
52-
// Add all extras using sjson]
5376
for k, v := range c.Extras {
5477
data, err = sjson.SetBytes(data, k, v)
5578
if err != nil {
5679
return nil, err
5780
}
5881
}
59-
6082
return data, nil
6183
}
6284

@@ -90,30 +112,48 @@ func getJSONFieldNames(t reflect.Type) map[string]struct{} {
90112

91113
// unmarshalCloudEvent unmarshals the CloudEventHeader and data field.
92114
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 {
115+
return unmarshalCloudEventWithPayload(data, func(dataRaw json.RawMessage, _ string) error {
116+
return dataFunc(dataRaw)
117+
})
118+
}
119+
120+
// unmarshalCloudEventWithPayload unmarshals the CloudEventHeader and returns both
121+
// "data" and "data_base64" for RawEvent.
122+
func unmarshalCloudEventWithPayload(data []byte, payloadFunc func(dataRaw json.RawMessage, dataBase64 string) error) (CloudEventHeader, error) {
123+
// Unmarshal known header fields via the type alias (no custom UnmarshalJSON).
124+
var c CloudEventHeader
125+
if err := json.Unmarshal(data, (*cloudEventHeader)(&c)); err != nil {
97126
return c, err
98127
}
99-
aux.SpecVersion = SpecVersion
100-
c = (CloudEventHeader)(aux)
101-
// Create a map to hold all JSON fields
128+
c.SpecVersion = SpecVersion
129+
130+
// Second pass into raw map to extract data, data_base64, and extras.
102131
rawFields := make(map[string]json.RawMessage)
103132
if err := json.Unmarshal(data, &rawFields); err != nil {
104133
return c, err
105134
}
106135

107-
// Separate known and unknown fields
136+
var dataRaw json.RawMessage
137+
var dataBase64 string
138+
if raw, ok := rawFields["data_base64"]; ok && len(raw) > 0 {
139+
if err := json.Unmarshal(raw, &dataBase64); err != nil {
140+
return c, err
141+
}
142+
}
143+
if raw, ok := rawFields["data"]; ok {
144+
dataRaw = raw
145+
}
146+
if dataRaw != nil || dataBase64 != "" {
147+
if err := payloadFunc(dataRaw, dataBase64); err != nil {
148+
return c, err
149+
}
150+
}
151+
108152
for key, rawValue := range rawFields {
109153
if _, ok := definedCloudeEventHdrFields[key]; ok {
110-
// Skip defined fields
111154
continue
112155
}
113-
if key == "data" {
114-
if err := dataFunc(rawValue); err != nil {
115-
return c, err
116-
}
156+
if key == "data" || key == "data_base64" {
117157
continue
118158
}
119159
if c.Extras == nil {
@@ -131,9 +171,3 @@ func unmarshalCloudEvent(data []byte, dataFunc func(json.RawMessage) error) (Clo
131171
// ignoreDataField is a function that ignores the data field.
132172
// It is used when unmarshalling the CloudEventHeader so that the data field is not added to the Extras map.
133173
func ignoreDataField(json.RawMessage) error { return nil }
134-
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)
139-
}

0 commit comments

Comments
 (0)