Skip to content
This repository was archived by the owner on Dec 9, 2024. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
762b8ff
HTTP CloudEvent coercion
RaeesBhatti May 9, 2018
48155e1
Event FromRequest: Dont require path param
RaeesBhatti May 9, 2018
79f1cc3
Event: Remove reduant CloudEvent parsing logic
RaeesBhatti May 9, 2018
ed07412
Event mapHeadersToEvent: Add Extensions if transforming
RaeesBhatti May 9, 2018
69c8e84
Router: emitSystemEventReceived on successful event parsing
RaeesBhatti May 9, 2018
9d85ec1
Event mapHeadersToEvent: Headers to Extensions
RaeesBhatti May 10, 2018
c4c594c
Event: Rename a var
RaeesBhatti May 11, 2018
2113cf2
Event: New parsing method
RaeesBhatti May 11, 2018
cac69d6
event.FromRequest: Make data an HTTPEvent if an required
RaeesBhatti May 14, 2018
fd1eced
Fix some tests
RaeesBhatti May 14, 2018
fb0057c
Event: Move extensions attachment to New func
RaeesBhatti May 14, 2018
e32db68
event.FromRequest: use isJSONContent instead of switch
RaeesBhatti May 14, 2018
45104ce
event: Simplify a condition
RaeesBhatti May 14, 2018
6aba28e
event: Update mapHeadersToEvent func
RaeesBhatti May 14, 2018
062cee0
Revert "event: Simplify a condition"
RaeesBhatti May 14, 2018
f811b6a
Formatting
RaeesBhatti May 14, 2018
51fa22c
event: Remove eventType param from parseAsCloudEvent
RaeesBhatti May 14, 2018
f058d1c
event: Add tests for event.FromRequest
RaeesBhatti May 14, 2018
b22d3c9
event: Rename HTTPEvent struct to HTTPRequestData
RaeesBhatti May 15, 2018
1c2d356
event: Rename func isJSONContent to isJSONMimeType
RaeesBhatti May 15, 2018
aa265cf
event: Rename func mapHeadersToEvent to parseAsCloudEventBinary
RaeesBhatti May 15, 2018
5927f91
event: Remove redundant tests for event.go
RaeesBhatti May 15, 2018
d6163ab
event: Rename NewHTTPEvent to NewHTTPRequestData
RaeesBhatti May 15, 2018
c17fa39
router: Fix a struct type in test
RaeesBhatti May 15, 2018
ceb7240
Format
RaeesBhatti May 15, 2018
e514de7
event: Merge request.go into event.go
RaeesBhatti May 15, 2018
40d0568
event.parseAsCloudEventBinary: Attach Data too
RaeesBhatti May 15, 2018
b0616aa
event: Update test for CloudEvent from headers
RaeesBhatti May 15, 2018
971bb7a
event: Add message for body equality clause TestFromRequest
RaeesBhatti May 15, 2018
9fa4dbb
event test: Dont use TransformationVersion as CloudEventsVersion
RaeesBhatti May 15, 2018
b8e4907
event TestFromRequest: Assert equality of Event.Data when not http event
RaeesBhatti May 15, 2018
450030a
event TestFromRequest: Add test for custom event
RaeesBhatti May 15, 2018
16600de
Event TestFromRequest: Add test for valid custom CloudEvent
RaeesBhatti May 15, 2018
7ec93dc
event TestFromRequest: Change url to example.com
RaeesBhatti May 15, 2018
3a17dd2
event TestFromRequest: Add test for invalid custom CloudEvent
RaeesBhatti May 15, 2018
1082d05
event.parseAsCloudEventBinary: Change params
RaeesBhatti May 15, 2018
7bb9a74
event TestFromRequest: Add loggin for error assertion
RaeesBhatti May 15, 2018
d7829db
Merge branch 'master' into httpCloudEvents
mthenw May 15, 2018
bf63c59
inernet/http.FlattenHeader: Make header keys lowercase
RaeesBhatti May 15, 2018
7762757
event.parseAsCloudEventBinary: Fix Event.Extensions
RaeesBhatti May 15, 2018
fc3aa46
event TestFromRequest: Correct expected headers
RaeesBhatti May 15, 2018
e22b1ff
event TestFromRequest: Assert SchemaURL and Extensions
RaeesBhatti May 15, 2018
e1933a1
internal/http: Fix tests for FlattenHeader
RaeesBhatti May 15, 2018
4ffa7f0
event.FromRequest: Mark http event with JSON content-type
RaeesBhatti May 15, 2018
69eb6e5
event.FromRequest: Simplify a condition
RaeesBhatti May 16, 2018
8547ba0
event.FromRequest: Move event.Data coercion in
RaeesBhatti May 16, 2018
428c0c5
Fix an encoding issue
RaeesBhatti May 23, 2018
e8cb681
Better content-type detection
RaeesBhatti May 23, 2018
6b08ab8
Put the request in http event wrapper
RaeesBhatti May 23, 2018
5d1b5a3
Update tests
RaeesBhatti May 23, 2018
8297f95
Introduce new flow
May 24, 2018
929e690
reduce complexity
May 24, 2018
f853c32
Fix tests
May 24, 2018
9c7b3db
Add missing file
May 24, 2018
7f3d0e3
fix remaining tests
May 24, 2018
18437c3
fix tests
May 24, 2018
f0a134d
Merge branch 'master' into httpCloudEvents
mthenw May 24, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions event/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package event

import "fmt"

// ErrParsingCloudEvent occurs when payload is not valid CloudEvent.
type ErrParsingCloudEvent struct {
Message string
}

func (e ErrParsingCloudEvent) Error() string {
return fmt.Sprintf("CloudEvent doesn't validate: %s", e.Message)
}
205 changes: 153 additions & 52 deletions event/event.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,19 @@ package event
import (
"encoding/json"
"errors"
"io/ioutil"
"mime"
"net/http"
"strings"
"time"
"unicode"

"go.uber.org/zap/zapcore"

uuid "github.com/satori/go.uuid"
"github.com/satori/go.uuid"
ihttp "github.com/serverless/event-gateway/internal/http"
"github.com/serverless/event-gateway/internal/zap"
validator "gopkg.in/go-playground/validator.v9"
"gopkg.in/go-playground/validator.v9"
)

// Type uniquely identifies an event type.
Expand All @@ -19,19 +24,15 @@ type Type string
const (
// TypeInvoke is a special type of event for sync function invocation.
TypeInvoke = Type("invoke")

// TypeHTTP is a special type of event for sync http subscriptions.
TypeHTTP = Type("http")
)

// TransformationVersion is indicative of the revision of how Event Gateway transforms a request into CloudEvents format.
const (
// TransformationVersion is indicative of the revision of how Event Gateway transforms a request into CloudEvents format.
TransformationVersion = "0.1"
)

const (
mimeJSON = "application/json"
mimeFormMultipart = "multipart/form-data"
mimeFormURLEncoded = "application/x-www-form-urlencoded"
// CloudEventsVersion currently supported by Event Gateway
CloudEventsVersion = "0.1"
)

// Event is a default event structure. All data that passes through the Event Gateway
Expand All @@ -50,44 +51,81 @@ type Event struct {
}

// New return new instance of Event.
func New(eventType Type, mime string, payload interface{}) *Event {
func New(eventType Type, mimeType string, payload interface{}) *Event {
event := &Event{
EventType: eventType,
CloudEventsVersion: "0.1",
CloudEventsVersion: CloudEventsVersion,
Source: "https://serverless.com/event-gateway/#transformationVersion=" + TransformationVersion,
EventID: uuid.NewV4().String(),
EventTime: time.Now(),
ContentType: mime,
ContentType: mimeType,
Data: payload,
Extensions: map[string]interface{}{
"eventgateway": map[string]interface{}{
"transformed": true,
"transformation-version": TransformationVersion,
},
},
}

// it's a custom event, possibly CloudEvent
if eventType != TypeHTTP && eventType != TypeInvoke {
cloudEvent, err := parseAsCloudEvent(eventType, mime, payload)
if err == nil {
event = cloudEvent
} else {
event.Extensions = zap.MapStringInterface{
"eventgateway": map[string]interface{}{
"transformed": true,
"transformation-version": TransformationVersion,
},
}
event.enhanceEventData()
return event
}

// FromRequest takes an HTTP request and returns an Event along with path. Most of the implementation
// is based on https://github.com/cloudevents/spec/blob/master/http-transport-binding.md.
// This function also supports legacy mode where event type is sent in Event header.
func FromRequest(r *http.Request) (*Event, error) {
contentType := r.Header.Get("Content-Type")
mimeType, _, err := mime.ParseMediaType(contentType)
if err != nil {
if err.Error() != "mime: no media type" {
return nil, err
}
mimeType = "application/octet-stream"
}
// Read request body
body := []byte{}
if r.Body != nil {
body, err = ioutil.ReadAll(r.Body)
if err != nil {
return nil, err
}
}

// Because event.Data is []bytes here, it will be base64 encoded by default when being sent to remote function,
// which is why we change the event.Data type to "string" for forms, so that, it is left intact.
if eventBody, ok := event.Data.([]byte); ok && len(eventBody) > 0 {
switch {
case isJSONContent(mime):
json.Unmarshal(eventBody, &event.Data)
case strings.HasPrefix(mime, mimeFormMultipart), mime == mimeFormURLEncoded:
event.Data = string(eventBody)
var event *Event
if mimeType == mimeCloudEventsJSON { // CloudEvents Structured Content Mode
return parseAsCloudEvent(mimeType, body)
} else if isCloudEventsBinaryContentMode(r.Header) { // CloudEvents Binary Content Mode
return parseAsCloudEventBinary(r.Header, body)
} else if isLegacyMode(r.Header) {
if mimeType == mimeJSON { // CloudEvent in Legacy Mode
event, err = parseAsCloudEvent(mimeType, body)
if err != nil {
return New(Type(r.Header.Get("event")), mimeType, body), nil
}
return event, err
}

return New(Type(r.Header.Get("event")), mimeType, body), nil
}

return event
return New(TypeHTTP, mimeCloudEventsJSON, NewHTTPRequestData(r, body)), nil
}

// Validate Event struct
func (e *Event) Validate() error {
validate := validator.New()
err := validate.Struct(e)
if err != nil {
return &ErrParsingCloudEvent{err.Error()}
}
return nil
}

// IsSystem indicates if the event is a system event.
func (e *Event) IsSystem() bool {
return strings.HasPrefix(string(e.EventType), "gateway.")
}

// MarshalLogObject is a part of zapcore.ObjectMarshaler interface
Expand Down Expand Up @@ -116,40 +154,103 @@ func (e Event) MarshalLogObject(enc zapcore.ObjectEncoder) error {
return nil
}

// IsSystem indicates if the event is a system event.
func (e Event) IsSystem() bool {
return strings.HasPrefix(string(e.EventType), "gateway.")
func isLegacyMode(headers http.Header) bool {
if headers.Get("Event") != "" {
return true
}

return false
}

func parseAsCloudEvent(eventType Type, mime string, payload interface{}) (*Event, error) {
if !isJSONContent(mime) {
return nil, errors.New("content type is not json")
func isCloudEventsBinaryContentMode(headers http.Header) bool {
if headers.Get("CE-EventType") != "" &&
headers.Get("CE-CloudEventsVersion") != "" &&
headers.Get("CE-Source") != "" &&
headers.Get("CE-EventID") != "" {
return true
}

return false
}

func parseAsCloudEventBinary(headers http.Header, payload interface{}) (*Event, error) {
event := &Event{
EventType: Type(headers.Get("CE-EventType")),
EventTypeVersion: headers.Get("CE-EventTypeVersion"),
CloudEventsVersion: headers.Get("CE-CloudEventsVersion"),
Source: headers.Get("CE-Source"),
EventID: headers.Get("CE-EventID"),
ContentType: headers.Get("Content-Type"),
Data: payload,
}

err := event.Validate()
if err != nil {
return nil, err
}

if val, err := time.Parse(time.RFC3339, headers.Get("CE-EventTime")); err == nil {
event.EventTime = val
}

if val := headers.Get("CE-SchemaURL"); len(val) > 0 {
event.SchemaURL = val
}

event.Extensions = map[string]interface{}{}
for key, val := range ihttp.FlattenHeader(headers) {
if strings.HasPrefix(key, "Ce-X-") {
key = strings.TrimLeft(key, "Ce-X-")
// Make first character lowercase
runes := []rune(key)
runes[0] = unicode.ToLower(runes[0])
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The HTTP spec says that the header keys are case-insensitive. So, I think it will be better to do a strings.ToLower here for consistency.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, that's true, but in the same time, when you look at the example in HTTP binding spec you will see that desired behavior is to preserve case (testExtension).

event.Extensions[string(runes)] = val
}
}

event.enhanceEventData()
return event, nil
}

func parseAsCloudEvent(mime string, payload interface{}) (*Event, error) {
body, ok := payload.([]byte)
if ok {
validate := validator.New()

customEvent := &Event{}
err := json.Unmarshal(body, customEvent)
event := &Event{}
err := json.Unmarshal(body, event)
if err != nil {
return nil, err
}

err = validate.Struct(customEvent)
err = event.Validate()
if err != nil {
return nil, err
}

if eventType != customEvent.EventType {
return nil, errors.New("wrong event type")
}

return customEvent, nil
event.enhanceEventData()
return event, nil
}

return nil, errors.New("couldn't cast to []byte")
}

func isJSONContent(mime string) bool {
return (mime == mimeJSON || strings.HasSuffix(mime, "+json"))
const (
mimeJSON = "application/json"
mimeFormMultipart = "multipart/form-data"
mimeFormURLEncoded = "application/x-www-form-urlencoded"
mimeCloudEventsJSON = "application/cloudevents+json"
)

// Because event.Data is []byte, it will be base64 encoded by default when being sent to remote function,
// which is why we change the event.Data type to "string" for forms or to map[string]interface{} for JSON
// so that, it is left intact.
func (e *Event) enhanceEventData() {
contentType := e.ContentType
if eventBody, ok := e.Data.([]byte); ok && len(eventBody) > 0 {
switch {
case contentType == mimeJSON || strings.HasSuffix(contentType, "+json"):
json.Unmarshal(eventBody, &e.Data)
case strings.HasPrefix(contentType, mimeFormMultipart), contentType == mimeFormURLEncoded:
e.Data = string(eventBody)
}
}
}
Loading