Skip to content

Commit 4a594e9

Browse files
committed
Add support for custom events
Add the ability to produce and consume custom events, including testing of the custom/conformance.json from the spec. This does not include validation against customSchemaUri yet, that will be implemented as a separate feature as it applies to both regular and custom events. Signed-off-by: Andrea Frittoli <[email protected]>
1 parent 9ac1c56 commit 4a594e9

File tree

109 files changed

+1295
-342
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

109 files changed

+1295
-342
lines changed

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,14 @@ func main() {
7777

7878
See the [CloudEvents](https://github.com/cloudevents/sdk-go#send-your-first-cloudevent) docs as well.
7979

80+
## Documentation
81+
82+
More examples are available in the [docs](./docs) folder.
83+
Online API Reference:
84+
- [SDK Root](https://pkg.go.dev/github.com/cdevents/sdk-go/pkg/api)
85+
- [v03 Specific](https://pkg.go.dev/github.com/cdevents/sdk-go/pkg/api/v03)
86+
- [v04 Specific](https://pkg.go.dev/github.com/cdevents/sdk-go/pkg/api/v04)
87+
8088
## Contributing
8189

8290
If you would like to contribute, see our [development](DEVELOPMENT.md) guide.

docs/README.md

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
# CDEvents Go SDK Docs
2+
3+
This folder contains example of how to use this SDK.
4+
5+
## Create a Custom CDEvent
6+
7+
If a tool wants to emit events that are not supported by the CDEvents specification,
8+
they can do so via [custom events](https://github.com/cdevents/spec/tree/main/custom).
9+
10+
Custom events are follow the CDEvents format and can be defined via the
11+
`CustomTypeEvent` object, available since v0.4, as well as using the `CustomCDEventReader`
12+
and `CustomCDEventWriter` interfaces.
13+
14+
Let's consider the following scenario: a tool called "MyRegistry" has a concept of "Quota"
15+
which can be "exceeded" by users of the system. We want to use events to notify when that
16+
happens, but CDEvents does not define any quota related subject.
17+
18+
```golang
19+
type Quota struct {
20+
User string `json:"user,omitempty"` // The use the quota applies ot
21+
Limit string `json:"limit,omitempty"` // The limit enforced by the quota e.g. 100Gb
22+
Current int `json:"current,omitempty"` // The current % of the quota used e.g. 90%
23+
Threshold int `json:"threshold,omitempty"` // The threshold for warning event e.g. 85%
24+
Level string `json:"level,omitempty"` // INFO: <threshold, WARNING: >threshold, <quota, CRITICAL: >quota
25+
}
26+
```
27+
For this scenario we will need a few imports:
28+
29+
```golang
30+
import (
31+
"context"
32+
"log"
33+
34+
cdevents "github.com/cdevents/sdk-go/pkg/api"
35+
cdeventsv04 "github.com/cdevents/sdk-go/pkg/api/v04"
36+
cloudevents "github.com/cloudevents/sdk-go/v2"
37+
)
38+
```
39+
40+
Let's define a custom event type for this scenario.
41+
This is our first iteration, so the event will have version "0.1.0".
42+
43+
```golang
44+
eventType := cdevents.CDEventType{
45+
Subject: "quota",
46+
Predicate: "exceeded",
47+
Version: "0.1.0",
48+
Custom: "myregistry",
49+
}
50+
```
51+
52+
With a `Quota` object, let's create a CDEvent for it:
53+
54+
```golang
55+
quotaRule123 := Quota{
56+
User: "heavy_user",
57+
Limit: "50Tb",
58+
Current: 90,
59+
Threshold: 85,
60+
Level: "WARNING",
61+
}
62+
63+
// Create the base event
64+
event, err := cdeventsv04.NewCustomTypeEvent()
65+
if err != nil {
66+
log.Fatalf("could not create a cdevent, %v", err)
67+
}
68+
event.SetEventType(eventType)
69+
70+
// Set the required context fields
71+
event.SetSubjectId("quotaRule123")
72+
event.SetSource("myregistry/region/staging")
73+
74+
// Set the required subject content
75+
event.SetSubjectContent(quotaRule123)
76+
77+
// If we host a schema for the overall custom CDEvent, we can add it
78+
// to the event so that the receiver may validate custom fields like
79+
// the event type and subject content
80+
event.SetSchemaUri("https://myregistry.dev/schemas/cdevents/quota-exceeded/0_1_0")
81+
```
82+
83+
To see the event, let's render it as JSON and log it:
84+
85+
```golang
86+
// Render the event as JSON
87+
eventJson, err := cdevents.AsJsonString(event)
88+
if err != nil {
89+
log.Fatalf("failed to marshal the CDEvent, %v", err)
90+
}
91+
// Print the event
92+
log.Printf("event: %v", eventJson)
93+
```
94+
95+
To send the event, let's setup a test sink, for instance using [smee.io/](https://smee.io/).
96+
Then let's render the event as CloudEvent and send it to the sink:
97+
98+
```golang
99+
ce, err = cdevents.AsCloudEvent(event)
100+
if err != nil {
101+
log.Fatalf("failed to create cloudevent, %v", err)
102+
}
103+
104+
// Set send options
105+
ctx := cloudevents.ContextWithTarget(context.Background(), "https://smee.io/<you-channel-id>")
106+
ctx = cloudevents.WithEncodingBinary(ctx)
107+
108+
c, err = cloudevents.NewClientHTTP()
109+
if err != nil {
110+
log.Fatalf("failed to create client, %v", err)
111+
}
112+
113+
// Send the CloudEvent
114+
if result := c.Send(ctx, *ce); cloudevents.IsUndelivered(result) {
115+
log.Fatalf("failed to send, %v", result)
116+
}
117+
```

pkg/api/bindings.go

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,10 @@ import (
3030
"golang.org/x/mod/semver"
3131
)
3232

33-
const SCHEMA_ID_REGEX = `^https://cdevents.dev/([0-9]\.[0-9])\.[0-9]/schema/([^ ]*)$`
33+
const (
34+
SCHEMA_ID_REGEX = `^https://cdevents.dev/([0-9]\.[0-9])\.[0-9]/schema/([^ ]*)$`
35+
CustomEventMapKey = "custom"
36+
)
3437

3538
var (
3639
// Validation helper as singleton
@@ -172,24 +175,29 @@ func NewFromJsonBytesContext[CDEventType CDEvent](event []byte, cdeventsMap map[
172175
eventAux := &struct {
173176
Context Context `json:"context"`
174177
}{}
175-
var nilReturn CDEventType
178+
var nilReturn, receiver CDEventType
179+
var ok bool
176180
err := json.Unmarshal(event, eventAux)
177181
if err != nil {
178182
return nilReturn, err
179183
}
180184
eventType := eventAux.Context.GetType()
181-
receiver, ok := cdeventsMap[eventType.UnversionedString()]
182-
if !ok {
183-
// This should not happen as unmarshalling and validate checks if the type is known to the SDK
184-
return nilReturn, fmt.Errorf("unknown event type %s", eventAux.Context.GetType())
185-
}
186-
// Check if the receiver is compatible. It must have the same subject and predicate
187-
// and share the same major version.
188-
// If the minor version is different and the message received as a version that is
189-
// greater than the SDK one, some fields may be lost, as newer versions may add new
190-
// fields to the event specification.
191-
if !eventType.IsCompatible(receiver.GetType()) {
192-
return nilReturn, fmt.Errorf("sdk event version %s not compatible with %s", receiver.GetType().Version, eventType.Version)
185+
if eventType.Custom != "" {
186+
receiver = cdeventsMap[CustomEventMapKey] // Custom type receiver does not have a predefined type
187+
} else {
188+
receiver, ok = cdeventsMap[eventType.UnversionedString()]
189+
if !ok {
190+
// This should not happen as unmarshalling and validate checks if the type is known to the SDK
191+
return nilReturn, fmt.Errorf("unknown event type %s", eventAux.Context.GetType())
192+
}
193+
// Check if the receiver is compatible. It must have the same subject and predicate
194+
// and share the same major version.
195+
// If the minor version is different and the message received as a version that is
196+
// greater than the SDK one, some fields may be lost, as newer versions may add new
197+
// fields to the event specification.
198+
if !eventType.IsCompatible(receiver.GetType()) {
199+
return nilReturn, fmt.Errorf("sdk event version %s not compatible with %s", receiver.GetType().Version, eventType.Version)
200+
}
193201
}
194202
err = json.Unmarshal(event, receiver)
195203
if err != nil {

pkg/api/bindings_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -346,7 +346,7 @@ func TestInvalidEvent(t *testing.T) {
346346
Context: api.ContextV04{
347347
Context: api.Context{
348348
Type: api.CDEventType{Subject: "not-a-valid-type"},
349-
Version: api.CDEventsSpecVersion,
349+
Version: testapi.SpecVersion,
350350
},
351351
},
352352
Subject: testapi.FooSubjectBarPredicateSubject{

pkg/api/schemas.go

Lines changed: 9 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/api/spec-v0.4

pkg/api/types.go

Lines changed: 95 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -31,19 +31,22 @@ import (
3131
)
3232

3333
const (
34-
EventTypeRoot = "dev.cdevents"
35-
CDEventsSpecVersion = "0.3.0"
36-
CDEventsSchemaURLTemplate = "https://cdevents.dev/%s/schema/%s-%s-event"
37-
CDEventsTypeRegex = "^dev\\.cdevents\\.(?P<subject>[a-z]+)\\.(?P<predicate>[a-z]+)\\.(?P<version>.*)$"
34+
EventTypeRoot = "dev.cdevents"
35+
CustomEventTypeRoot = "dev.cdeventsx"
36+
CDEventsSchemaURLTemplate = "https://cdevents.dev/%s/schema/%s-%s-event"
37+
CDEventsCustomSchemaURLTemplate = "https://cdevents.dev/%s/schema/custom"
38+
CDEventsTypeRegex = "^dev\\.cdevents\\.(?P<subject>[a-z]+)\\.(?P<predicate>[a-z]+)\\.(?P<version>.*)$"
39+
CDEventsCustomTypeRegex = "^dev\\.cdeventsx\\.(?P<tool>[a-z]+)-(?P<subject>[a-z]+)\\.(?P<predicate>[a-z]+)\\.(?P<version>.*)$"
3840

3941
LinkTypePath LinkType = "PATH"
4042
LinkTypeEnd LinkType = "END"
4143
LinkTypeRelation LinkType = "RELATION"
4244
)
4345

4446
var (
45-
CDEventsTypeCRegex = regexp.MustCompile(CDEventsTypeRegex)
46-
LinkTypes = map[LinkType]interface{}{
47+
CDEventsTypeCRegex = regexp.MustCompile(CDEventsTypeRegex)
48+
CDEventsCustomTypeCRegex = regexp.MustCompile(CDEventsCustomTypeRegex)
49+
LinkTypes = map[LinkType]interface{}{
4750
LinkTypePath: "",
4851
LinkTypeEnd: "",
4952
LinkTypeRelation: "",
@@ -380,18 +383,59 @@ type CDEventType struct {
380383

381384
// Version is a semantic version in the form <major>.<minor>.<patch>
382385
Version string
386+
387+
// Custom holds the tool name in case of custom events
388+
Custom string
389+
}
390+
391+
func (t CDEventType) Root() string {
392+
root := EventTypeRoot
393+
if t.Custom != "" {
394+
root = CustomEventTypeRoot
395+
}
396+
return root
397+
}
398+
399+
// FQSubject returns the fully qualified subject, which includes
400+
// the tool name from t.Custom in case of custom events
401+
func (t CDEventType) FQSubject() string {
402+
s := t.Subject
403+
if s == "" {
404+
s = "<undefined-subject>"
405+
}
406+
if t.Custom != "" {
407+
s = t.Custom + "-" + s
408+
}
409+
return s
383410
}
384411

385412
func (t CDEventType) String() string {
386-
return EventTypeRoot + "." + t.Subject + "." + t.Predicate + "." + t.Version
413+
predicate := t.Predicate
414+
if predicate == "" {
415+
predicate = "<undefined-predicate>"
416+
}
417+
version := t.Version
418+
if version == "" {
419+
version = "<undefined-version>"
420+
}
421+
return t.Root() + "." + t.FQSubject() + "." + predicate + "." + version
387422
}
388423

389424
func (t CDEventType) UnversionedString() string {
390-
return EventTypeRoot + "." + t.Subject + "." + t.Predicate
425+
predicate := t.Predicate
426+
if predicate == "" {
427+
predicate = "<undefined-predicate>"
428+
}
429+
return t.Root() + "." + t.FQSubject() + "." + predicate
391430
}
392431

393432
func (t CDEventType) Short() string {
394-
return t.Subject + "_" + t.Predicate
433+
s := t.FQSubject()
434+
p := t.Predicate
435+
if s == "" || p == "" {
436+
return ""
437+
}
438+
return t.FQSubject() + "_" + t.Predicate
395439
}
396440

397441
// Two CDEventTypes are compatible if the subject and predicates
@@ -420,15 +464,32 @@ func (t CDEventType) MarshalJSON() ([]byte, error) {
420464
}
421465

422466
func CDEventTypeFromString(cdeventType string) (*CDEventType, error) {
467+
names := CDEventsTypeCRegex.SubexpNames()
423468
parts := CDEventsTypeCRegex.FindStringSubmatch(cdeventType)
424469
if len(parts) != 4 {
425-
return nil, fmt.Errorf("cannot parse event type %s", cdeventType)
470+
names = CDEventsCustomTypeCRegex.SubexpNames()
471+
parts = CDEventsCustomTypeCRegex.FindStringSubmatch(cdeventType)
472+
if len(parts) != 5 {
473+
return nil, fmt.Errorf("cannot parse event type %s", cdeventType)
474+
}
475+
}
476+
returnType := CDEventType{}
477+
for i, matchName := range names {
478+
if i == 0 {
479+
continue
480+
}
481+
switch matchName {
482+
case "subject":
483+
returnType.Subject = parts[i]
484+
case "predicate":
485+
returnType.Predicate = parts[i]
486+
case "version":
487+
returnType.Version = parts[i]
488+
case "tool":
489+
returnType.Custom = parts[i]
490+
}
426491
}
427-
return &CDEventType{
428-
Subject: parts[1],
429-
Predicate: parts[2],
430-
Version: parts[3],
431-
}, nil
492+
return &returnType, nil
432493
}
433494

434495
type CDEventReader interface {
@@ -459,6 +520,11 @@ type CDEventReader interface {
459520
// for direct access to the content fields
460521
GetSubject() Subject
461522

523+
// The event specific subject. It is possible to use a type assertion with
524+
// the generic Subject to obtain an event specific implementation of Subject
525+
// for direct access to the content fields
526+
GetSubjectContent() interface{}
527+
462528
// The URL and content of the schema file associated to the event type
463529
GetSchema() (string, *jsonschema.Schema, error)
464530

@@ -531,6 +597,20 @@ type CDEventWriterV04 interface {
531597
SetSchemaUri(schema string)
532598
}
533599

600+
type CustomCDEventReader interface {
601+
CDEventReaderV04
602+
}
603+
604+
type CustomCDEventWriter interface {
605+
CDEventWriterV04
606+
607+
// CustomCDEvent can represent different event types
608+
SetEventType(eventType CDEventType)
609+
610+
// CustomCDEvent types can have different subject fields
611+
SetSubjectContent(subjectContent interface{})
612+
}
613+
534614
type CDEventCustomDataEncoding string
535615

536616
func (t CDEventCustomDataEncoding) String() string {

0 commit comments

Comments
 (0)