This adds a simple cloudevents-go sample. (#1987)

This example is intended to showcase how to start an event receiver as a Knative
Service, and then have that service either send a new event to a `$K_SINK` or
respond with it (e.g. for working with a Broker).

Once we're happy with this, we should broaden this to cover more languages.
This commit is contained in:
Matt Moore 2019-11-15 23:06:02 -08:00 committed by Knative Prow Robot
parent 9902cb7140
commit e07e9bab0d
51 changed files with 2742 additions and 300 deletions

16
Gopkg.lock generated
View File

@ -26,7 +26,7 @@
revision = "3a771d992973f24aa725d07868b467d1ddfceafb"
[[projects]]
digest = "1:d3c3de7c1ad57f795e5c409afa842fea27899f124622bcdce83f8bea8721b9db"
digest = "1:902544577dcb868a5ae31529d73a1ce5031e224923290caedf6176241ee304e0"
name = "github.com/cloudevents/sdk-go"
packages = [
".",
@ -35,6 +35,7 @@
"pkg/cloudevents/context",
"pkg/cloudevents/datacodec",
"pkg/cloudevents/datacodec/json",
"pkg/cloudevents/datacodec/text",
"pkg/cloudevents/datacodec/xml",
"pkg/cloudevents/observability",
"pkg/cloudevents/transport",
@ -42,8 +43,8 @@
"pkg/cloudevents/types",
]
pruneopts = "NUT"
revision = "4cc108a637ff4bf2d1848c60d5fbb0f711fd1b8c"
version = "v0.9.2"
revision = "2fa4bb1fbb4aac4d906b0173a2a408f701439b82"
version = "v0.10.0"
[[projects]]
digest = "1:7a6852b35eb5bbc184561443762d225116ae630c26a7c4d90546619f1e7d2ad2"
@ -142,6 +143,14 @@
revision = "e3702bed27f0d39777b0b37b664b6280e8ef8fbf"
version = "v1.6.2"
[[projects]]
digest = "1:08c58ac78a8c1f61e9a96350066d30fe194b8779799bd932a79932a5166a173f"
name = "github.com/kelseyhightower/envconfig"
packages = ["."]
pruneopts = "NUT"
revision = "0b417c4ec4a8a82eecc22a1459a504aa55163d61"
version = "v1.4.0"
[[projects]]
digest = "1:5985ef4caf91ece5d54817c11ea25f182697534f8ae6521eadcd628c142ac4b6"
name = "github.com/matttproud/golang_protobuf_extensions"
@ -477,6 +486,7 @@
"github.com/google/go-github/github",
"github.com/google/uuid",
"github.com/gorilla/mux",
"github.com/kelseyhightower/envconfig",
"github.com/openzipkin/zipkin-go",
"github.com/openzipkin/zipkin-go/reporter/http",
"github.com/satori/go.uuid",

View File

@ -0,0 +1,6 @@
---
title: "Knative Serving 'Cloud Events' samples"
linkTitle: "Cloud Events apps"
weight: 1
type: "docs"
---

View File

@ -0,0 +1,31 @@
# Use the official Golang image to create a build artifact.
# This is based on Debian and sets the GOPATH to /go.
# https://hub.docker.com/_/golang
FROM golang:1.13 as builder
# Create and change to the app directory.
WORKDIR /app
# Retrieve application dependencies using go modules.
# Allows container builds to reuse downloaded dependencies.
COPY go.* ./
RUN go mod download
# Copy local code to the container image.
COPY . ./
# Build the binary.
# -mod=readonly ensures immutable go.mod and go.sum in container builds.
RUN CGO_ENABLED=0 GOOS=linux go build -mod=readonly -v -o server
# Use the official Alpine image for a lean production container.
# https://hub.docker.com/_/alpine
# https://docs.docker.com/develop/develop-images/multistage-build/#use-multi-stage-builds
FROM alpine:3
RUN apk add --no-cache ca-certificates
# Copy the binary to the production image from the builder stage.
COPY --from=builder /app/server /server
# Run the web service on container startup.
CMD ["/server"]

View File

@ -0,0 +1,120 @@
---
title: "Cloud Events - Go"
linkTitle: "Go"
weight: 1
type: "docs"
---
A simple web app written in Go that can receive and send Cloud Events that you
can use for testing. It supports running in two modes:
1. The default mode has the app reply to your input events with the output
event, which is simplest for demonstrating things working in isolation,
but is also the model for working for the Knative Eventing `Broker` concept.
2. `K_SINK` mode has the app send events to the destination encoded in `$K_SINK`,
which is useful to demonstrate how folks can synthesize events to send to
a Service or Broker when not initiated by a Broker invocation (e.g.
implementing an event source)
The application will use `$K_SINK`-mode whenever the environment variable is specified.
Follow the steps below to create the sample code and then deploy the app to your
cluster. You can also download a working copy of the sample, by running the
following commands:
```shell
git clone -b "{{< branch >}}" https://github.com/knative/docs knative-docs
cd knative-docs/docs/serving/samples/cloudevents/cloudevents-go
```
## Before you begin
- A Kubernetes cluster with Knative installed and DNS configured. Follow the
[installation instructions](../../../../install/README.md) if you need to
create one.
- [Docker](https://www.docker.com) installed and running on your local machine,
and a Docker Hub account configured (we'll use it for a container registry).
## The sample code.
1. If you look in `cloudevents.go`, you will see two key functions for the
different modes of operation:
```go
func (recv *Receiver) ReceiveAndSend(ctx context.Context, event cloudevents.Event) error {
// This is called whenever an event is received if $K_SINK is set, and sends a new event
// to the url in $K_SINK.
}
func (recv *Receiver) ReceiveAndReply(ctx context.Context, event cloudevents.Event, eventResp *cloudevents.EventResponse) error {
// This is called whenever an event is received if $K_SINK is NOT set, and it replies with
// the new event instead.
}
```
1. If you look in `Dockerfile`, you will see a method for pulling in the dependencies and
building a small Go container based on Alpine. You can build and push this to your
registry of choice via:
```shell
docker build -t <image> .
docker push <image>
```
Alternatively you can use [`ko`](https://github.com/google/ko) to build and
push just the image with:
```shell
ko publish github.com/knative/docs/docs/serving/samples/cloudevents/cloudevents-go
```
1. If you look in `service.yaml`, take the `<image>` name above and insert it
into the `image:` field (unless using `ko`).
```shell
kubectl apply -f service.yaml
```
Or if using `ko` to build and push:
```shell
ko apply -f service.yaml
```
## Testing the sample
Get the URL for your Service with:
```shell
$ kubectl get ksvc
NAME URL LATESTCREATED LATESTREADY READY REASON
cloudevents-go http://cloudevents-go.default.1.2.3.4.xip.io cloudevents-go-ss5pj cloudevents-go-ss5pj True
```
Then send a cloud event to it with:
```shell
$ curl -X POST \
-H "content-type: application/json" \
-H "ce-specversion: 1.0" \
-H "ce-source: curl-command" \
-H "ce-type: curl.demo" \
-H "ce-id: 123-abc" \
-d '{"name":"Dave"}' \
http://cloudevents-go.default.1.2.3.4.xip.io
```
You will get back:
```shell
{"message":"Hello, Dave"}
```
## Removing the sample app deployment
To remove the sample app from your cluster, delete the service record:
```shell
kubectl delete --filename service.yaml
```

View File

@ -0,0 +1,105 @@
package main
import (
"context"
"fmt"
"log"
"net/http"
cloudevents "github.com/cloudevents/sdk-go"
"github.com/kelseyhightower/envconfig"
)
type Receiver struct {
client cloudevents.Client
// If the K_SINK environment variable is set, then events are sent there,
// otherwise we simply reply to the inbound request.
Target string `envconfig:"K_SINK"`
}
func main() {
client, err := cloudevents.NewDefaultClient()
if err != nil {
log.Fatal(err.Error())
}
r := Receiver{client: client}
if err := envconfig.Process("", &r); err != nil {
log.Fatal(err.Error())
}
// Depending on whether targetting data has been supplied,
// we will either reply with our response or send it on to
// an event sink.
var receiver interface{} // the SDK reflects on the signature.
if r.Target == "" {
receiver = r.ReceiveAndReply
} else {
receiver = r.ReceiveAndSend
}
if err := client.StartReceiver(context.Background(), receiver); err != nil {
log.Fatal(err)
}
}
// Request is the structure of the event we expect to receive.
type Request struct {
Name string `json:"name"`
}
// Response is the structure of the event we send in response to requests.
type Response struct {
Message string `json:"message,omitempty"`
}
// handle shared the logic for producing the Response event from the Request.
func handle(req Request) (resp Response) {
resp.Message = fmt.Sprintf("Hello, %s", req.Name)
return
}
// ReceiveAndSend is invoked whenever we receive an event.
func (recv *Receiver) ReceiveAndSend(ctx context.Context, event cloudevents.Event) error {
req := Request{}
if err := event.DataAs(&req); err != nil {
return err
}
log.Printf("Got an event from: %q", req.Name)
resp := handle(req)
log.Printf("Sending event: %q", resp.Message)
r := cloudevents.NewEvent(cloudevents.VersionV1)
r.SetType("dev.knative.docs.sample")
r.SetSource("https://github.com/knative/docs/docs/serving/samples/cloudevents/cloudevents-go")
r.SetDataContentType("application/json")
r.SetData(resp)
ctx = cloudevents.ContextWithTarget(ctx, recv.Target)
_, _, err := recv.client.Send(ctx, event)
return err
}
// ReceiveAndReply is invoked whenever we receive an event.
func (recv *Receiver) ReceiveAndReply(ctx context.Context, event cloudevents.Event, eventResp *cloudevents.EventResponse) error {
req := Request{}
if err := event.DataAs(&req); err != nil {
return err
}
log.Printf("Got an event from: %q", req.Name)
resp := handle(req)
log.Printf("Replying with event: %q", resp.Message)
r := cloudevents.NewEvent(cloudevents.VersionV1)
r.SetType("dev.knative.docs.sample")
r.SetSource("https://github.com/knative/docs/docs/serving/samples/cloudevents/cloudevents-go")
r.SetDataContentType("application/json")
r.SetData(resp)
eventResp.RespondWith(http.StatusOK, &r)
return nil
}

View File

@ -0,0 +1,14 @@
apiVersion: serving.knative.dev/v1
kind: Service
metadata:
name: cloudevents-go
namespace: default
spec:
template:
spec:
containers:
- image: github.com/knative/docs/docs/serving/samples/cloudevents/cloudevents-go
# Uncomment this to send events somewhere.
# env:
# - name: K_SINK
# value: http://default-broker.default.svc.cluster.local

View File

@ -26,6 +26,7 @@ type EventResponse = cloudevents.EventResponse
// Context
type EventContext = cloudevents.EventContext
type EventContextV1 = cloudevents.EventContextV1
type EventContextV01 = cloudevents.EventContextV01
type EventContextV02 = cloudevents.EventContextV02
type EventContextV03 = cloudevents.EventContextV03
@ -54,12 +55,16 @@ const (
// Event Versions
VersionV1 = cloudevents.CloudEventsVersionV1
VersionV01 = cloudevents.CloudEventsVersionV01
VersionV02 = cloudevents.CloudEventsVersionV02
VersionV03 = cloudevents.CloudEventsVersionV03
// HTTP Transport Encodings
HTTPBinaryV1 = http.BinaryV1
HTTPStructuredV1 = http.StructuredV1
HTTPBatchedV1 = http.BatchedV1
HTTPBinaryV01 = http.BinaryV01
HTTPStructuredV01 = http.StructuredV01
HTTPBinaryV02 = http.BinaryV02
@ -114,6 +119,8 @@ var (
ParseTimestamp = types.ParseTimestamp
ParseURLRef = types.ParseURLRef
ParseURIRef = types.ParseURIRef
ParseURI = types.ParseURI
// HTTP Transport
@ -133,6 +140,7 @@ var (
WithPath = http.WithPath
WithMiddleware = http.WithMiddleware
WithLongPollTarget = http.WithLongPollTarget
WithListener = http.WithListener
// HTTP Context

View File

@ -5,6 +5,7 @@ import (
"fmt"
"github.com/cloudevents/sdk-go/pkg/cloudevents/datacodec/json"
"github.com/cloudevents/sdk-go/pkg/cloudevents/datacodec/text"
"github.com/cloudevents/sdk-go/pkg/cloudevents/datacodec/xml"
"github.com/cloudevents/sdk-go/pkg/cloudevents/observability"
)
@ -30,12 +31,14 @@ func init() {
AddDecoder("text/json", json.Decode)
AddDecoder("application/xml", xml.Decode)
AddDecoder("text/xml", xml.Decode)
AddDecoder("text/plain", text.Decode)
AddEncoder("", json.Encode)
AddEncoder("application/json", json.Encode)
AddEncoder("text/json", json.Encode)
AddEncoder("application/xml", xml.Encode)
AddEncoder("text/xml", xml.Encode)
AddEncoder("text/plain", text.Encode)
}
// AddDecoder registers a decoder for a given content type. The codecs will use

View File

@ -0,0 +1,33 @@
// Text codec converts []byte or string to string and vice-versa.
package text
import (
"context"
"fmt"
)
func Decode(_ context.Context, in, out interface{}) error {
p, _ := out.(*string)
if p == nil {
return fmt.Errorf("text.Decode out: want *string, got %T", out)
}
switch s := in.(type) {
case string:
*p = s
case []byte:
*p = string(s)
case nil: // treat nil like []byte{}
*p = ""
default:
return fmt.Errorf("text.Decode in: want []byte or string, got %T", in)
}
return nil
}
func Encode(_ context.Context, in interface{}) ([]byte, error) {
s, ok := in.(string)
if !ok {
return nil, fmt.Errorf("text.Encode in: want string, got %T", in)
}
return []byte(s), nil
}

View File

@ -12,6 +12,7 @@ type Event struct {
Context EventContext
Data interface{}
DataEncoded bool
DataBinary bool
}
const (
@ -30,7 +31,17 @@ func New(version ...string) Event {
return *e
}
// ExtensionAs returns Context.ExtensionAs(name, obj)
// DEPRECATED: Access extensions directly via the e.Extensions() map.
// Use functions in the types package to convert extension values.
// For example replace this:
//
// var i int
// err := e.ExtensionAs("foo", &i)
//
// With this:
//
// i, err := types.ToInteger(e.Extensions["foo"])
//
func (e Event) ExtensionAs(name string, obj interface{}) error {
return e.Context.ExtensionAs(name, obj)
}

View File

@ -14,11 +14,43 @@ import (
// SetData implements EventWriter.SetData
func (e *Event) SetData(obj interface{}) error {
if e.SpecVersion() != CloudEventsVersionV1 {
return e.legacySetData(obj)
}
// Version 1.0 and above.
// TODO: we will have to be smarter about how data relates to media type.
// but the issue is we can not just encode data anymore without understanding
// what the encoding will be on the outbound event. Structured will use
// data_base64, binary will not (if the transport supports binary mode).
// TODO: look at content encoding too.
switch obj.(type) {
case []byte:
e.Data = obj
e.DataEncoded = true
e.DataBinary = true
default:
data, err := datacodec.Encode(context.Background(), e.DataMediaType(), obj)
if err != nil {
return err
}
e.Data = data
e.DataEncoded = true
e.DataBinary = false
}
return nil
}
func (e *Event) legacySetData(obj interface{}) error {
data, err := datacodec.Encode(context.Background(), e.DataMediaType(), obj)
if err != nil {
return err
}
if e.DataContentEncoding() == Base64 {
if e.DeprecatedDataContentEncoding() == Base64 {
buf := make([]byte, base64.StdEncoding.EncodedLen(len(data)))
base64.StdEncoding.Encode(buf, data)
e.Data = string(buf)
@ -70,7 +102,7 @@ func (e Event) DataAs(data interface{}) error { // TODO: Clean this function up
// No data.
return nil
}
if e.Context.GetDataContentEncoding() == Base64 {
if e.Context.DeprecatedGetDataContentEncoding() == Base64 {
var bs []byte
// test to see if we need to unquote the data.
if obj[0] == quotes[0] || obj[0] == quotes[1] {
@ -91,9 +123,13 @@ func (e Event) DataAs(data interface{}) error { // TODO: Clean this function up
obj = buf[:n]
}
mediaType, err := e.Context.GetDataMediaType()
if err != nil {
return err
mediaType := ""
if e.Context.GetDataContentType() != "" {
var err error
mediaType, err = e.Context.GetDataMediaType()
if err != nil {
return err
}
}
return datacodec.Decode(context.Background(), mediaType, obj, data)
}

View File

@ -18,26 +18,29 @@ type EventReader interface {
ID() string
// Time returns event.Context.GetTime().
Time() time.Time
// SchemaURL returns event.Context.GetSchemaURL().
SchemaURL() string
// DataSchema returns event.Context.GetDataSchema().
DataSchema() string
// DataContentType returns event.Context.GetDataContentType().
DataContentType() string
// DataMediaType returns event.Context.GetDataMediaType().
DataMediaType() string
// DataContentEncoding returns event.Context.GetDataContentEncoding().
DataContentEncoding() string
// DeprecatedDataContentEncoding returns event.Context.DeprecatedGetDataContentEncoding().
DeprecatedDataContentEncoding() string
// Extension Attributes
// Extensions returns the event.Context.GetExtensions().
// Extensions use the CloudEvents type system, details in package cloudevents/types.
Extensions() map[string]interface{}
// DEPRECATED: see event.Context.ExtensionAs
// ExtensionAs returns event.Context.ExtensionAs(name, obj).
ExtensionAs(string, interface{}) error
// Data Attribute
// ExtensionAs returns event.Context.ExtensionAs(name, obj).
// DataAs attempts to populate the provided data object with the event payload.
// data should be a pointer type.
DataAs(interface{}) error
}
@ -58,11 +61,11 @@ type EventWriter interface {
SetID(string)
// SetTime performs event.Context.SetTime.
SetTime(time.Time)
// SetSchemaURL performs event.Context.SetSchemaURL.
SetSchemaURL(string)
// SetDataSchema performs event.Context.SetDataSchema.
SetDataSchema(string)
// SetDataContentType performs event.Context.SetDataContentType.
SetDataContentType(string)
// SetDataContentEncoding performs event.Context.SetDataContentEncoding.
// DeprecatedSetDataContentEncoding performs event.Context.DeprecatedSetDataContentEncoding.
SetDataContentEncoding(string)
// Extension Attributes

View File

@ -2,9 +2,12 @@ package cloudevents
import (
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"strconv"
"strings"
"github.com/cloudevents/sdk-go/pkg/cloudevents/observability"
)
@ -19,7 +22,17 @@ func (e Event) MarshalJSON() ([]byte, error) {
return nil, err
}
b, err := JsonEncode(e)
var b []byte
var err error
switch e.SpecVersion() {
case CloudEventsVersionV01, CloudEventsVersionV02, CloudEventsVersionV03:
b, err = JsonEncodeLegacy(e)
case CloudEventsVersionV1:
b, err = JsonEncode(e)
default:
return nil, fmt.Errorf("unnknown spec version: %q", e.SpecVersion())
}
// Report the observable
if err != nil {
@ -52,6 +65,8 @@ func (e *Event) UnmarshalJSON(b []byte) error {
err = e.JsonDecodeV02(b, raw)
case CloudEventsVersionV03:
err = e.JsonDecodeV03(b, raw)
case CloudEventsVersionV1:
err = e.JsonDecodeV1(b, raw)
default:
return fmt.Errorf("unnknown spec version: %q", version)
}
@ -89,17 +104,26 @@ func versionFromRawMessage(raw map[string]json.RawMessage) string {
// JsonEncode
func JsonEncode(e Event) ([]byte, error) {
if e.DataContentType() == "" {
e.SetDataContentType(ApplicationJSON)
}
data, err := e.DataBytes()
if err != nil {
return nil, err
}
return jsonEncode(e.Context, data)
return jsonEncode(e.Context, data, e.DataBinary)
}
func jsonEncode(ctx EventContextReader, data []byte) ([]byte, error) {
// JsonEncodeLegacy
func JsonEncodeLegacy(e Event) ([]byte, error) {
var data []byte
isBase64 := e.Context.DeprecatedGetDataContentEncoding() == Base64
var err error
data, err = e.DataBytes()
if err != nil {
return nil, err
}
return jsonEncode(e.Context, data, isBase64)
}
func jsonEncode(ctx EventContextReader, data []byte, isBase64 bool) ([]byte, error) {
var b map[string]json.RawMessage
var err error
@ -122,16 +146,26 @@ func jsonEncode(ctx EventContextReader, data []byte) ([]byte, error) {
if err != nil {
return nil, err
}
isBase64 := ctx.GetDataContentEncoding() == Base64
isJson := mediaType == "" || mediaType == ApplicationJSON || mediaType == TextJSON
// TODO(#60): we do not support json values at the moment, only objects and lists.
if isJson && !isBase64 {
b["data"] = data
} else if data[0] != byte('"') {
b["data"] = []byte(strconv.QuoteToASCII(string(data)))
} else {
// already quoted
b["data"] = data
var dataKey string
if ctx.GetSpecVersion() == CloudEventsVersionV1 {
dataKey = "data_base64"
buf := make([]byte, base64.StdEncoding.EncodedLen(len(data)))
base64.StdEncoding.Encode(buf, data)
data = buf
} else {
dataKey = "data"
}
if data[0] != byte('"') {
b[dataKey] = []byte(strconv.QuoteToASCII(string(data)))
} else {
// already quoted
b[dataKey] = data
}
}
}
@ -189,6 +223,7 @@ func (e *Event) JsonDecodeV02(body []byte, raw map[string]json.RawMessage) error
if len(raw) > 0 {
extensions := make(map[string]interface{}, len(raw))
for k, v := range raw {
k = strings.ToLower(k)
extensions[k] = v
}
ec.Extensions = extensions
@ -229,6 +264,7 @@ func (e *Event) JsonDecodeV03(body []byte, raw map[string]json.RawMessage) error
if len(raw) > 0 {
extensions := make(map[string]interface{}, len(raw))
for k, v := range raw {
k = strings.ToLower(k)
extensions[k] = v
}
ec.Extensions = extensions
@ -241,6 +277,66 @@ func (e *Event) JsonDecodeV03(body []byte, raw map[string]json.RawMessage) error
return nil
}
// JsonDecodeV1 takes in the byte representation of a version 1.0 structured json CloudEvent and returns a
// cloudevent.Event or an error if there are parsing errors.
func (e *Event) JsonDecodeV1(body []byte, raw map[string]json.RawMessage) error {
ec := EventContextV1{}
if err := json.Unmarshal(body, &ec); err != nil {
return err
}
delete(raw, "specversion")
delete(raw, "type")
delete(raw, "source")
delete(raw, "subject")
delete(raw, "id")
delete(raw, "time")
delete(raw, "dataschema")
delete(raw, "datacontenttype")
var data interface{}
if d, ok := raw["data"]; ok {
data = []byte(d)
}
delete(raw, "data")
var dataBase64 []byte
if d, ok := raw["data_base64"]; ok {
var tmp []byte
if err := json.Unmarshal(d, &tmp); err != nil {
return err
}
dataBase64 = tmp
}
delete(raw, "data_base64")
if len(raw) > 0 {
extensions := make(map[string]interface{}, len(raw))
for k, v := range raw {
k = strings.ToLower(k)
var tmp string
if err := json.Unmarshal(v, &tmp); err != nil {
return err
}
extensions[k] = tmp
}
ec.Extensions = extensions
}
e.Context = &ec
if data != nil && dataBase64 != nil {
return errors.New("parsing error: JSON decoder found both 'data', and 'data_base64' in JSON payload")
}
if data != nil {
e.Data = data
} else if dataBase64 != nil {
e.Data = dataBase64
}
e.DataEncoded = data != nil
return nil
}
func marshalEventLegacy(event interface{}) (map[string]json.RawMessage, error) {
b, err := json.Marshal(event)
if err != nil {
@ -267,6 +363,7 @@ func marshalEvent(event interface{}, extensions map[string]interface{}) (map[str
}
for k, v := range extensions {
k = strings.ToLower(k)
vb, err := json.Marshal(v)
if err != nil {
return nil, err

View File

@ -54,10 +54,10 @@ func (e Event) Time() time.Time {
return time.Time{}
}
// SchemaURL implements EventReader.SchemaURL
func (e Event) SchemaURL() string {
// DataSchema implements EventReader.DataSchema
func (e Event) DataSchema() string {
if e.Context != nil {
return e.Context.GetSchemaURL()
return e.Context.GetDataSchema()
}
return ""
}
@ -81,15 +81,15 @@ func (e Event) DataMediaType() string {
return ""
}
// DataContentEncoding implements EventReader.DataContentEncoding
func (e Event) DataContentEncoding() string {
// DeprecatedDataContentEncoding implements EventReader.DeprecatedDataContentEncoding
func (e Event) DeprecatedDataContentEncoding() string {
if e.Context != nil {
return e.Context.GetDataContentEncoding()
return e.Context.DeprecatedGetDataContentEncoding()
}
return ""
}
// DataContentEncoding implements EventReader.DataContentEncoding
// Extensions implements EventReader.Extensions
func (e Event) Extensions() map[string]interface{} {
if e.Context != nil {
return e.Context.GetExtensions()

View File

@ -17,9 +17,11 @@ func (e *Event) SetSpecVersion(v string) {
e.Context = EventContextV02{}.AsV02()
case CloudEventsVersionV03:
e.Context = EventContextV03{}.AsV03()
case CloudEventsVersionV1:
e.Context = EventContextV1{}.AsV1()
default:
panic(fmt.Errorf("a valid spec version is required: [%s, %s, %s]",
CloudEventsVersionV01, CloudEventsVersionV02, CloudEventsVersionV03))
panic(fmt.Errorf("a valid spec version is required: [%s, %s, %s, %s]",
CloudEventsVersionV01, CloudEventsVersionV02, CloudEventsVersionV03, CloudEventsVersionV1))
}
return
}
@ -63,9 +65,9 @@ func (e *Event) SetTime(t time.Time) {
}
}
// SetSchemaURL implements EventWriter.SetSchemaURL
func (e *Event) SetSchemaURL(s string) {
if err := e.Context.SetSchemaURL(s); err != nil {
// SetDataSchema implements EventWriter.SetDataSchema
func (e *Event) SetDataSchema(s string) {
if err := e.Context.SetDataSchema(s); err != nil {
panic(err)
}
}
@ -77,14 +79,14 @@ func (e *Event) SetDataContentType(ct string) {
}
}
// SetDataContentEncoding implements EventWriter.SetDataContentEncoding
// DeprecatedSetDataContentEncoding implements EventWriter.DeprecatedSetDataContentEncoding
func (e *Event) SetDataContentEncoding(enc string) {
if err := e.Context.SetDataContentEncoding(enc); err != nil {
if err := e.Context.DeprecatedSetDataContentEncoding(enc); err != nil {
panic(err)
}
}
// SetDataContentEncoding implements EventWriter.SetDataContentEncoding
// SetExtension implements EventWriter.SetExtension
func (e *Event) SetExtension(name string, obj interface{}) {
if err := e.Context.SetExtension(name, obj); err != nil {
panic(err)

View File

@ -18,27 +18,40 @@ type EventContextReader interface {
GetID() string
// GetTime returns the CloudEvents creation time from the context.
GetTime() time.Time
// GetSchemaURL returns the CloudEvents schema URL (if any) from the
// GetDataSchema returns the CloudEvents schema URL (if any) from the
// context.
GetSchemaURL() string
GetDataSchema() string
// GetDataContentType returns content type on the context.
GetDataContentType() string
// GetDataContentEncoding returns content encoding on the context.
GetDataContentEncoding() string
// DeprecatedGetDataContentEncoding returns content encoding on the context.
DeprecatedGetDataContentEncoding() string
// GetDataMediaType returns the MIME media type for encoded data, which is
// needed by both encoding and decoding. This is a processed form of
// GetDataContentType and it may return an error.
GetDataMediaType() (string, error)
// ExtensionAs populates the given interface with the CloudEvents extension
// of the given name from the extension attributes. It returns an error if
// the extension does not exist, the extension's type does not match the
// provided type, or if the type is not a supported.
// DEPRECATED: Access extensions directly via the GetExtensions()
// For example replace this:
//
// var i int
// err := ec.ExtensionAs("foo", &i)
//
// With this:
//
// i, err := types.ToInteger(ec.GetExtensions["foo"])
//
ExtensionAs(string, interface{}) error
// GetExtensions returns the full extensions map.
//
// Extensions use the CloudEvents type system, details in package cloudevents/types.
GetExtensions() map[string]interface{}
// GetExtension returns the extension associated with with the given key.
// The given key is case insensitive. If the extension can not be found,
// an error will be returned.
GetExtension(string) (interface{}, error)
}
// EventContextWriter are the methods required to be a writer of context
@ -56,18 +69,22 @@ type EventContextWriter interface {
SetID(string) error
// SetTime sets the time of the context.
SetTime(time time.Time) error
// SetSchemaURL sets the schema url of the context.
SetSchemaURL(string) error
// SetDataSchema sets the schema url of the context.
SetDataSchema(string) error
// SetDataContentType sets the data content type of the context.
SetDataContentType(string) error
// SetDataContentEncoding sets the data context encoding of the context.
SetDataContentEncoding(string) error
// DeprecatedSetDataContentEncoding sets the data context encoding of the context.
DeprecatedSetDataContentEncoding(string) error
// SetExtension sets the given interface onto the extension attributes
// determined by the provided name.
//
// Package ./types documents the types that are allowed as extension values.
SetExtension(string, interface{}) error
}
// EventContextConverter are the methods that allow for event version
// conversion.
type EventContextConverter interface {
// AsV01 provides a translation from whatever the "native" encoding of the
// CloudEvent was to the equivalent in v0.1 field names, moving fields to or
@ -83,6 +100,11 @@ type EventContextConverter interface {
// CloudEvent was to the equivalent in v0.3 field names, moving fields to or
// from extensions as necessary.
AsV03() *EventContextV03
// AsV1 provides a translation from whatever the "native" encoding of the
// CloudEvent was to the equivalent in v1.0 field names, moving fields to or
// from extensions as necessary.
AsV1() *EventContextV1
}
// EventContext is conical interface for a CloudEvents Context.

View File

@ -115,8 +115,12 @@ func (ec EventContextV01) AsV02() *EventContextV02 {
// AsV03 implements EventContextConverter.AsV03
func (ec EventContextV01) AsV03() *EventContextV03 {
ecv2 := ec.AsV02()
return ecv2.AsV03()
return ec.AsV02().AsV03()
}
// AsV1 implements EventContextConverter.AsV1
func (ec EventContextV01) AsV1() *EventContextV1 {
return ec.AsV02().AsV03().AsV1()
}
// Validate returns errors based on requirements from the CloudEvents spec.

View File

@ -1,6 +1,7 @@
package cloudevents
import (
"fmt"
"mime"
"time"
)
@ -68,16 +69,16 @@ func (ec EventContextV01) GetTime() time.Time {
return time.Time{}
}
// GetSchemaURL implements EventContextReader.GetSchemaURL
func (ec EventContextV01) GetSchemaURL() string {
// GetDataSchema implements EventContextReader.GetDataSchema
func (ec EventContextV01) GetDataSchema() string {
if ec.SchemaURL != nil {
return ec.SchemaURL.String()
}
return ""
}
// GetDataContentEncoding implements EventContextReader.GetDataContentEncoding
func (ec EventContextV01) GetDataContentEncoding() string {
// DeprecatedGetDataContentEncoding implements EventContextReader.DeprecatedGetDataContentEncoding
func (ec EventContextV01) DeprecatedGetDataContentEncoding() string {
var enc string
if err := ec.ExtensionAs(DataContentEncodingKey, &enc); err != nil {
return ""
@ -85,6 +86,16 @@ func (ec EventContextV01) GetDataContentEncoding() string {
return enc
}
// GetExtensions implements EventContextReader.GetExtensions
func (ec EventContextV01) GetExtensions() map[string]interface{} {
return ec.Extensions
}
// GetExtension implements EventContextReader.GetExtension
func (ec EventContextV01) GetExtension(key string) (interface{}, error) {
v, ok := caseInsensitiveSearch(key, ec.Extensions)
if !ok {
return "", fmt.Errorf("%q not found", key)
}
return v, nil
}

View File

@ -79,8 +79,8 @@ func (ec *EventContextV01) SetTime(t time.Time) error {
return nil
}
// SetSchemaURL implements EventContextWriter.SetSchemaURL
func (ec *EventContextV01) SetSchemaURL(u string) error {
// SetDataSchema implements EventContextWriter.SetDataSchema
func (ec *EventContextV01) SetDataSchema(u string) error {
u = strings.TrimSpace(u)
if u == "" {
ec.SchemaURL = nil
@ -94,8 +94,8 @@ func (ec *EventContextV01) SetSchemaURL(u string) error {
return nil
}
// SetDataContentEncoding implements EventContextWriter.SetDataContentEncoding
func (ec *EventContextV01) SetDataContentEncoding(e string) error {
// DeprecatedSetDataContentEncoding implements EventContextWriter.DeprecatedSetDataContentEncoding
func (ec *EventContextV01) DeprecatedSetDataContentEncoding(e string) error {
e = strings.ToLower(strings.TrimSpace(e))
if e == "" {
return ec.SetExtension(DataContentEncodingKey, nil)

View File

@ -146,7 +146,7 @@ func (ec EventContextV02) AsV03() *EventContextV03 {
}
continue
}
// DataContentEncoding was introduced in 0.3
// DeprecatedDataContentEncoding was introduced in 0.3
if strings.EqualFold(k, DataContentEncodingKey) {
etv, ok := v.(string)
if ok && etv != "" {
@ -163,6 +163,11 @@ func (ec EventContextV02) AsV03() *EventContextV03 {
return &ret
}
// AsV1 implements EventContextConverter.AsV1
func (ec EventContextV02) AsV1() *EventContextV1 {
return ec.AsV03().AsV1()
}
// Validate returns errors based on requirements from the CloudEvents spec.
// For more details, see https://github.com/cloudevents/spec/blob/v0.2/spec.md
func (ec EventContextV02) Validate() error {

View File

@ -1,6 +1,7 @@
package cloudevents
import (
"fmt"
"mime"
"time"
)
@ -48,8 +49,8 @@ func (ec EventContextV02) GetTime() time.Time {
return time.Time{}
}
// GetSchemaURL implements EventContextReader.GetSchemaURL
func (ec EventContextV02) GetSchemaURL() string {
// GetDataSchema implements EventContextReader.GetDataSchema
func (ec EventContextV02) GetDataSchema() string {
if ec.SchemaURL != nil {
return ec.SchemaURL.String()
}
@ -76,8 +77,8 @@ func (ec EventContextV02) GetDataMediaType() (string, error) {
return "", nil
}
// GetDataContentEncoding implements EventContextReader.GetDataContentEncoding
func (ec EventContextV02) GetDataContentEncoding() string {
// DeprecatedGetDataContentEncoding implements EventContextReader.DeprecatedGetDataContentEncoding
func (ec EventContextV02) DeprecatedGetDataContentEncoding() string {
var enc string
if err := ec.ExtensionAs(DataContentEncodingKey, &enc); err != nil {
return ""
@ -85,6 +86,16 @@ func (ec EventContextV02) GetDataContentEncoding() string {
return enc
}
// GetExtensions implements EventContextReader.GetExtensions
func (ec EventContextV02) GetExtensions() map[string]interface{} {
return ec.Extensions
}
// GetExtension implements EventContextReader.GetExtension
func (ec EventContextV02) GetExtension(key string) (interface{}, error) {
v, ok := caseInsensitiveSearch(key, ec.Extensions)
if !ok {
return "", fmt.Errorf("%q not found", key)
}
return v, nil
}

View File

@ -79,8 +79,8 @@ func (ec *EventContextV02) SetTime(t time.Time) error {
return nil
}
// SetSchemaURL implements EventContextWriter.SetSchemaURL
func (ec *EventContextV02) SetSchemaURL(u string) error {
// SetDataSchema implements EventContextWriter.SetDataSchema
func (ec *EventContextV02) SetDataSchema(u string) error {
u = strings.TrimSpace(u)
if u == "" {
ec.SchemaURL = nil
@ -94,8 +94,8 @@ func (ec *EventContextV02) SetSchemaURL(u string) error {
return nil
}
// SetDataContentEncoding implements EventContextWriter.SetDataContentEncoding
func (ec *EventContextV02) SetDataContentEncoding(e string) error {
// DeprecatedSetDataContentEncoding implements EventContextWriter.DeprecatedSetDataContentEncoding
func (ec *EventContextV02) DeprecatedSetDataContentEncoding(e string) error {
e = strings.ToLower(strings.TrimSpace(e))
if e == "" {
return ec.SetExtension(DataContentEncodingKey, nil)

View File

@ -9,8 +9,6 @@ import (
"github.com/cloudevents/sdk-go/pkg/cloudevents/types"
)
// WIP: AS OF FEB 19, 2019
const (
// CloudEventsVersionV03 represents the version 0.3 of the CloudEvents spec.
CloudEventsVersionV03 = "0.3"
@ -32,12 +30,12 @@ type EventContextV03 struct {
ID string `json:"id"`
// Time - A Timestamp when the event happened.
Time *types.Timestamp `json:"time,omitempty"`
// SchemaURL - A link to the schema that the `data` attribute adheres to.
// DataSchema - A link to the schema that the `data` attribute adheres to.
SchemaURL *types.URLRef `json:"schemaurl,omitempty"`
// GetDataMediaType - A MIME (RFC2046) string describing the media type of `data`.
// TODO: Should an empty string assume `application/json`, `application/octet-stream`, or auto-detect the content?
DataContentType *string `json:"datacontenttype,omitempty"`
// DataContentEncoding describes the content encoding for the `data` attribute. Valid: nil, `Base64`.
// DeprecatedDataContentEncoding describes the content encoding for the `data` attribute. Valid: nil, `Base64`.
DataContentEncoding *string `json:"datacontentencoding,omitempty"`
// Extensions - Additional extension metadata beyond the base spec.
Extensions map[string]interface{} `json:"-"`
@ -85,7 +83,11 @@ func (ec *EventContextV03) SetExtension(name string, value interface{}) error {
if value == nil {
delete(ec.Extensions, name)
} else {
ec.Extensions[name] = value
v, err := types.Validate(value)
if err == nil {
ec.Extensions[name] = v
}
return err
}
return nil
}
@ -117,7 +119,7 @@ func (ec EventContextV03) AsV02() *EventContextV02 {
if ec.Subject != nil {
_ = ret.SetExtension(SubjectKey, *ec.Subject)
}
// DataContentEncoding was introduced in 0.3, so put it in an extension for 0.2.
// DeprecatedDataContentEncoding was introduced in 0.3, so put it in an extension for 0.2.
if ec.DataContentEncoding != nil {
_ = ret.SetExtension(DataContentEncodingKey, *ec.DataContentEncoding)
}
@ -138,6 +140,39 @@ func (ec EventContextV03) AsV03() *EventContextV03 {
return &ec
}
// AsV04 implements EventContextConverter.AsV04
func (ec EventContextV03) AsV1() *EventContextV1 {
ret := EventContextV1{
SpecVersion: CloudEventsVersionV1,
ID: ec.ID,
Time: ec.Time,
Type: ec.Type,
DataContentType: ec.DataContentType,
Source: types.URIRef{URL: ec.Source.URL},
Subject: ec.Subject,
Extensions: make(map[string]interface{}),
}
if ec.SchemaURL != nil {
ret.DataSchema = &types.URI{URL: ec.SchemaURL.URL}
}
// DataContentEncoding was removed in 1.0, so put it in an extension for 1.0.
if ec.DataContentEncoding != nil {
_ = ret.SetExtension(DataContentEncodingKey, *ec.DataContentEncoding)
}
if ec.Extensions != nil {
for k, v := range ec.Extensions {
k = strings.ToLower(k)
ret.Extensions[k] = v
}
}
if len(ret.Extensions) == 0 {
ret.Extensions = nil
}
return &ret
}
// Validate returns errors based on requirements from the CloudEvents spec.
// For more details, see https://github.com/cloudevents/spec/blob/master/spec.md
// As of Feb 26, 2019, commit 17c32ea26baf7714ad027d9917d03d2fff79fc7e

View File

@ -1,6 +1,7 @@
package cloudevents
import (
"fmt"
"mime"
"time"
)
@ -64,22 +65,32 @@ func (ec EventContextV03) GetID() string {
return ec.ID
}
// GetSchemaURL implements EventContextReader.GetSchemaURL
func (ec EventContextV03) GetSchemaURL() string {
// GetDataSchema implements EventContextReader.GetDataSchema
func (ec EventContextV03) GetDataSchema() string {
if ec.SchemaURL != nil {
return ec.SchemaURL.String()
}
return ""
}
// GetDataContentEncoding implements EventContextReader.GetDataContentEncoding
func (ec EventContextV03) GetDataContentEncoding() string {
// DeprecatedGetDataContentEncoding implements EventContextReader.DeprecatedGetDataContentEncoding
func (ec EventContextV03) DeprecatedGetDataContentEncoding() string {
if ec.DataContentEncoding != nil {
return *ec.DataContentEncoding
}
return ""
}
// GetExtensions implements EventContextReader.GetExtensions
func (ec EventContextV03) GetExtensions() map[string]interface{} {
return ec.Extensions
}
// GetExtension implements EventContextReader.GetExtension
func (ec EventContextV03) GetExtension(key string) (interface{}, error) {
v, ok := caseInsensitiveSearch(key, ec.Extensions)
if !ok {
return "", fmt.Errorf("%q not found", key)
}
return v, nil
}

View File

@ -81,8 +81,8 @@ func (ec *EventContextV03) SetTime(t time.Time) error {
return nil
}
// SetSchemaURL implements EventContextWriter.SetSchemaURL
func (ec *EventContextV03) SetSchemaURL(u string) error {
// SetDataSchema implements EventContextWriter.SetDataSchema
func (ec *EventContextV03) SetDataSchema(u string) error {
u = strings.TrimSpace(u)
if u == "" {
ec.SchemaURL = nil
@ -96,8 +96,8 @@ func (ec *EventContextV03) SetSchemaURL(u string) error {
return nil
}
// SetDataContentEncoding implements EventContextWriter.SetDataContentEncoding
func (ec *EventContextV03) SetDataContentEncoding(e string) error {
// DeprecatedSetDataContentEncoding implements EventContextWriter.DeprecatedSetDataContentEncoding
func (ec *EventContextV03) DeprecatedSetDataContentEncoding(e string) error {
e = strings.ToLower(strings.TrimSpace(e))
if e == "" {
ec.DataContentEncoding = nil

View File

@ -0,0 +1,300 @@
package cloudevents
import (
"errors"
"fmt"
"mime"
"sort"
"strings"
"github.com/cloudevents/sdk-go/pkg/cloudevents/types"
)
// WIP: AS OF SEP 20, 2019
const (
// CloudEventsVersionV1 represents the version 1.0 of the CloudEvents spec.
CloudEventsVersionV1 = "1.0"
)
// EventContextV1 represents the non-data attributes of a CloudEvents v1.0
// event.
type EventContextV1 struct {
// ID of the event; must be non-empty and unique within the scope of the producer.
// +required
ID string `json:"id"`
// Source - A URI describing the event producer.
// +required
Source types.URIRef `json:"source"`
// SpecVersion - The version of the CloudEvents specification used by the event.
// +required
SpecVersion string `json:"specversion"`
// Type - The type of the occurrence which has happened.
// +required
Type string `json:"type"`
// DataContentType - A MIME (RFC2046) string describing the media type of `data`.
// +optional
DataContentType *string `json:"datacontenttype,omitempty"`
// Subject - The subject of the event in the context of the event producer
// (identified by `source`).
// +optional
Subject *string `json:"subject,omitempty"`
// Time - A Timestamp when the event happened.
// +optional
Time *types.Timestamp `json:"time,omitempty"`
// DataSchema - A link to the schema that the `data` attribute adheres to.
// +optional
DataSchema *types.URI `json:"dataschema,omitempty"`
// Extensions - Additional extension metadata beyond the base spec.
// +optional
Extensions map[string]interface{} `json:"-"`
}
// Adhere to EventContext
var _ EventContext = (*EventContextV1)(nil)
// ExtensionAs implements EventContext.ExtensionAs
func (ec EventContextV1) ExtensionAs(name string, obj interface{}) error {
name = strings.ToLower(name)
value, ok := ec.Extensions[name]
if !ok {
return fmt.Errorf("extension %q does not exist", name)
}
// Only support *string for now.
if v, ok := obj.(*string); ok {
if *v, ok = value.(string); ok {
return nil
}
}
return fmt.Errorf("unknown extension type %T", obj)
}
// SetExtension adds the extension 'name' with value 'value' to the CloudEvents context.
func (ec *EventContextV1) SetExtension(name string, value interface{}) error {
if !IsAlphaNumericLowercaseLetters(name) {
return errors.New("bad key, CloudEvents attribute names MUST consist of lower-case letters ('a' to 'z') or digits ('0' to '9') from the ASCII character set")
}
name = strings.ToLower(name)
if ec.Extensions == nil {
ec.Extensions = make(map[string]interface{})
}
if value == nil {
delete(ec.Extensions, name)
return nil
} else {
v, err := types.Validate(value) // Ensure it's a legal CE attribute value
if err == nil {
ec.Extensions[name] = v
}
return err
}
}
// Clone implements EventContextConverter.Clone
func (ec EventContextV1) Clone() EventContext {
return ec.AsV1()
}
// AsV01 implements EventContextConverter.AsV01
func (ec EventContextV1) AsV01() *EventContextV01 {
ecv2 := ec.AsV02()
return ecv2.AsV01()
}
// AsV02 implements EventContextConverter.AsV02
func (ec EventContextV1) AsV02() *EventContextV02 {
ecv3 := ec.AsV03()
return ecv3.AsV02()
}
// AsV03 implements EventContextConverter.AsV03
func (ec EventContextV1) AsV03() *EventContextV03 {
ret := EventContextV03{
SpecVersion: CloudEventsVersionV03,
ID: ec.ID,
Time: ec.Time,
Type: ec.Type,
DataContentType: ec.DataContentType,
Source: types.URLRef{URL: ec.Source.URL},
Subject: ec.Subject,
Extensions: make(map[string]interface{}),
}
if ec.DataSchema != nil {
ret.SchemaURL = &types.URLRef{URL: ec.DataSchema.URL}
}
// TODO: DeprecatedDataContentEncoding needs to be moved to extensions.
if ec.Extensions != nil {
for k, v := range ec.Extensions {
k = strings.ToLower(k)
// DeprecatedDataContentEncoding was introduced in 0.3, removed in 1.0
if strings.EqualFold(k, DataContentEncodingKey) {
etv, ok := v.(string)
if ok && etv != "" {
ret.DataContentEncoding = &etv
}
continue
}
ret.Extensions[k] = v
}
}
if len(ret.Extensions) == 0 {
ret.Extensions = nil
}
return &ret
}
// AsV04 implements EventContextConverter.AsV04
func (ec EventContextV1) AsV1() *EventContextV1 {
ec.SpecVersion = CloudEventsVersionV1
return &ec
}
// Validate returns errors based on requirements from the CloudEvents spec.
// For more details, see https://github.com/cloudevents/spec/blob/v1.0-rc1/spec.md.
func (ec EventContextV1) Validate() error {
errors := []string(nil)
// id
// Type: String
// Constraints:
// REQUIRED
// MUST be a non-empty string
// MUST be unique within the scope of the producer
id := strings.TrimSpace(ec.ID)
if id == "" {
errors = append(errors, "id: MUST be a non-empty string")
// no way to test "MUST be unique within the scope of the producer"
}
// source
// Type: URI-reference
// Constraints:
// REQUIRED
// MUST be a non-empty URI-reference
// An absolute URI is RECOMMENDED
source := strings.TrimSpace(ec.Source.String())
if source == "" {
errors = append(errors, "source: REQUIRED")
}
// specversion
// Type: String
// Constraints:
// REQUIRED
// MUST be a non-empty string
specVersion := strings.TrimSpace(ec.SpecVersion)
if specVersion == "" {
errors = append(errors, "specversion: MUST be a non-empty string")
}
// type
// Type: String
// Constraints:
// REQUIRED
// MUST be a non-empty string
// SHOULD be prefixed with a reverse-DNS name. The prefixed domain dictates the organization which defines the semantics of this event type.
eventType := strings.TrimSpace(ec.Type)
if eventType == "" {
errors = append(errors, "type: MUST be a non-empty string")
}
// The following attributes are optional but still have validation.
// datacontenttype
// Type: String per RFC 2046
// Constraints:
// OPTIONAL
// If present, MUST adhere to the format specified in RFC 2046
if ec.DataContentType != nil {
dataContentType := strings.TrimSpace(*ec.DataContentType)
if dataContentType == "" {
errors = append(errors, "datacontenttype: if present, MUST adhere to the format specified in RFC 2046")
} else {
_, _, err := mime.ParseMediaType(dataContentType)
if err != nil {
errors = append(errors, fmt.Sprintf("datacontenttype: failed to parse media type, %s", err.Error()))
}
}
}
// dataschema
// Type: URI
// Constraints:
// OPTIONAL
// If present, MUST adhere to the format specified in RFC 3986
if ec.DataSchema != nil {
dataSchema := strings.TrimSpace(ec.DataSchema.String())
// empty string is not RFC 3986 compatible.
if dataSchema == "" {
errors = append(errors, "dataschema: if present, MUST adhere to the format specified in RFC 3986")
}
}
// subject
// Type: String
// Constraints:
// OPTIONAL
// MUST be a non-empty string
if ec.Subject != nil {
subject := strings.TrimSpace(*ec.Subject)
if subject == "" {
errors = append(errors, "subject: if present, MUST be a non-empty string")
}
}
// time
// Type: Timestamp
// Constraints:
// OPTIONAL
// If present, MUST adhere to the format specified in RFC 3339
// --> no need to test this, no way to set the time without it being valid.
if len(errors) > 0 {
return fmt.Errorf(strings.Join(errors, "\n"))
}
return nil
}
// String returns a pretty-printed representation of the EventContext.
func (ec EventContextV1) String() string {
b := strings.Builder{}
b.WriteString("Context Attributes,\n")
b.WriteString(" specversion: " + ec.SpecVersion + "\n")
b.WriteString(" type: " + ec.Type + "\n")
b.WriteString(" source: " + ec.Source.String() + "\n")
if ec.Subject != nil {
b.WriteString(" subject: " + *ec.Subject + "\n")
}
b.WriteString(" id: " + ec.ID + "\n")
if ec.Time != nil {
b.WriteString(" time: " + ec.Time.String() + "\n")
}
if ec.DataSchema != nil {
b.WriteString(" dataschema: " + ec.DataSchema.String() + "\n")
}
if ec.DataContentType != nil {
b.WriteString(" datacontenttype: " + *ec.DataContentType + "\n")
}
if ec.Extensions != nil && len(ec.Extensions) > 0 {
b.WriteString("Extensions,\n")
keys := make([]string, 0, len(ec.Extensions))
for k := range ec.Extensions {
keys = append(keys, k)
}
sort.Strings(keys)
for _, key := range keys {
b.WriteString(fmt.Sprintf(" %s: %v\n", key, ec.Extensions[key]))
}
}
return b.String()
}

View File

@ -0,0 +1,98 @@
package cloudevents
import (
"fmt"
"mime"
"time"
)
// GetSpecVersion implements EventContextReader.GetSpecVersion
func (ec EventContextV1) GetSpecVersion() string {
if ec.SpecVersion != "" {
return ec.SpecVersion
}
return CloudEventsVersionV03
}
// GetDataContentType implements EventContextReader.GetDataContentType
func (ec EventContextV1) GetDataContentType() string {
if ec.DataContentType != nil {
return *ec.DataContentType
}
return ""
}
// GetDataMediaType implements EventContextReader.GetDataMediaType
func (ec EventContextV1) GetDataMediaType() (string, error) {
if ec.DataContentType != nil {
mediaType, _, err := mime.ParseMediaType(*ec.DataContentType)
if err != nil {
return "", err
}
return mediaType, nil
}
return "", nil
}
// GetType implements EventContextReader.GetType
func (ec EventContextV1) GetType() string {
return ec.Type
}
// GetSource implements EventContextReader.GetSource
func (ec EventContextV1) GetSource() string {
return ec.Source.String()
}
// GetSubject implements EventContextReader.GetSubject
func (ec EventContextV1) GetSubject() string {
if ec.Subject != nil {
return *ec.Subject
}
return ""
}
// GetTime implements EventContextReader.GetTime
func (ec EventContextV1) GetTime() time.Time {
if ec.Time != nil {
return ec.Time.Time
}
return time.Time{}
}
// GetID implements EventContextReader.GetID
func (ec EventContextV1) GetID() string {
return ec.ID
}
// GetDataSchema implements EventContextReader.GetDataSchema
func (ec EventContextV1) GetDataSchema() string {
if ec.DataSchema != nil {
return ec.DataSchema.String()
}
return ""
}
// DeprecatedGetDataContentEncoding implements EventContextReader.DeprecatedGetDataContentEncoding
func (ec EventContextV1) DeprecatedGetDataContentEncoding() string {
return ""
}
// GetExtensions implements EventContextReader.GetExtensions
func (ec EventContextV1) GetExtensions() map[string]interface{} {
// For now, convert the extensions of v1.0 to the pre-v1.0 style.
ext := make(map[string]interface{}, len(ec.Extensions))
for k, v := range ec.Extensions {
ext[k] = v
}
return ext
}
// GetExtension implements EventContextReader.GetExtension
func (ec EventContextV1) GetExtension(key string) (interface{}, error) {
v, ok := caseInsensitiveSearch(key, ec.Extensions)
if !ok {
return "", fmt.Errorf("%q not found", key)
}
return v, nil
}

View File

@ -0,0 +1,102 @@
package cloudevents
import (
"errors"
"fmt"
"net/url"
"strings"
"time"
"github.com/cloudevents/sdk-go/pkg/cloudevents/types"
)
// Adhere to EventContextWriter
var _ EventContextWriter = (*EventContextV1)(nil)
// SetSpecVersion implements EventContextWriter.SetSpecVersion
func (ec *EventContextV1) SetSpecVersion(v string) error {
if v != CloudEventsVersionV1 {
return fmt.Errorf("invalid version %q, expecting %q", v, CloudEventsVersionV1)
}
ec.SpecVersion = CloudEventsVersionV1
return nil
}
// SetDataContentType implements EventContextWriter.SetDataContentType
func (ec *EventContextV1) SetDataContentType(ct string) error {
ct = strings.TrimSpace(ct)
if ct == "" {
ec.DataContentType = nil
} else {
ec.DataContentType = &ct
}
return nil
}
// SetType implements EventContextWriter.SetType
func (ec *EventContextV1) SetType(t string) error {
t = strings.TrimSpace(t)
ec.Type = t
return nil
}
// SetSource implements EventContextWriter.SetSource
func (ec *EventContextV1) SetSource(u string) error {
pu, err := url.Parse(u)
if err != nil {
return err
}
ec.Source = types.URIRef{URL: *pu}
return nil
}
// SetSubject implements EventContextWriter.SetSubject
func (ec *EventContextV1) SetSubject(s string) error {
s = strings.TrimSpace(s)
if s == "" {
ec.Subject = nil
} else {
ec.Subject = &s
}
return nil
}
// SetID implements EventContextWriter.SetID
func (ec *EventContextV1) SetID(id string) error {
id = strings.TrimSpace(id)
if id == "" {
return errors.New("id is required to be a non-empty string")
}
ec.ID = id
return nil
}
// SetTime implements EventContextWriter.SetTime
func (ec *EventContextV1) SetTime(t time.Time) error {
if t.IsZero() {
ec.Time = nil
} else {
ec.Time = &types.Timestamp{Time: t}
}
return nil
}
// SetDataSchema implements EventContextWriter.SetDataSchema
func (ec *EventContextV1) SetDataSchema(u string) error {
u = strings.TrimSpace(u)
if u == "" {
ec.DataSchema = nil
return nil
}
pu, err := url.Parse(u)
if err != nil {
return err
}
ec.DataSchema = &types.URI{URL: *pu}
return nil
}
// DeprecatedSetDataContentEncoding implements EventContextWriter.DeprecatedSetDataContentEncoding
func (ec *EventContextV1) DeprecatedSetDataContentEncoding(e string) error {
return errors.New("deprecated: SetDataContentEncoding is not supported in v1.0 of CloudEvents")
}

View File

@ -1,13 +1,30 @@
package cloudevents
import (
"regexp"
"strings"
)
const (
// DataContentEncodingKey is the key to DataContentEncoding for versions that do not support data content encoding
// DataContentEncodingKey is the key to DeprecatedDataContentEncoding for versions that do not support data content encoding
// directly.
DataContentEncodingKey = "datacontentencoding"
// EventTypeVersionKey is the key to EventTypeVersion for versions that do not support event type version directly.
EventTypeVersionKey = "eventTypeVersion"
EventTypeVersionKey = "eventtypeversion"
// SubjectKey is the key to Subject for versions that do not support subject directly.
SubjectKey = "subject"
)
func caseInsensitiveSearch(key string, space map[string]interface{}) (interface{}, bool) {
lkey := strings.ToLower(key)
for k, v := range space {
if strings.EqualFold(lkey, strings.ToLower(k)) {
return v, true
}
}
return nil, false
}
var IsAlphaNumericLowercaseLetters = regexp.MustCompile(`^[a-z0-9]+$`).MatchString

View File

@ -6,15 +6,17 @@ import "fmt"
// message can not be converted.
type ErrTransportMessageConversion struct {
fatal bool
handled bool
transport string
message string
}
// NewErrMessageEncodingUnknown makes a new ErrMessageEncodingUnknown.
func NewErrTransportMessageConversion(transport, message string, fatal bool) *ErrTransportMessageConversion {
func NewErrTransportMessageConversion(transport, message string, handled, fatal bool) *ErrTransportMessageConversion {
return &ErrTransportMessageConversion{
transport: transport,
message: message,
handled: handled,
fatal: fatal,
}
}
@ -24,6 +26,11 @@ func (e *ErrTransportMessageConversion) IsFatal() bool {
return e.fatal
}
// Handled reports if this error should be considered accepted and no further action.
func (e *ErrTransportMessageConversion) Handled() bool {
return e.handled
}
// Error implements error.Error
func (e *ErrTransportMessageConversion) Error() string {
return fmt.Sprintf("transport %s failed to convert message: %s", e.transport, e.message)

View File

@ -2,9 +2,12 @@ package http
import (
"context"
"errors"
"fmt"
"sync"
"github.com/cloudevents/sdk-go/pkg/cloudevents"
cecontext "github.com/cloudevents/sdk-go/pkg/cloudevents/context"
"github.com/cloudevents/sdk-go/pkg/cloudevents/transport"
)
@ -21,152 +24,127 @@ type Codec struct {
v01 *CodecV01
v02 *CodecV02
v03 *CodecV03
v1 *CodecV1
_v01 sync.Once
_v02 sync.Once
_v03 sync.Once
_v1 sync.Once
}
// Adheres to Codec
var _ transport.Codec = (*Codec)(nil)
// Encode encodes the provided event into a transport message.
func (c *Codec) Encode(ctx context.Context, e cloudevents.Event) (transport.Message, error) {
encoding := c.Encoding
if encoding == Default && c.DefaultEncodingSelectionFn != nil {
encoding = c.DefaultEncodingSelectionFn(ctx, e)
}
func (c *Codec) loadCodec(encoding Encoding) (transport.Codec, error) {
switch encoding {
case Default:
fallthrough
case BinaryV01:
fallthrough
case StructuredV01:
if c.v01 == nil {
c.v01 = &CodecV01{Encoding: encoding}
}
return c.v01.Encode(ctx, e)
case BinaryV02:
fallthrough
case StructuredV02:
if c.v02 == nil {
c.v02 = &CodecV02{Encoding: encoding}
}
return c.v02.Encode(ctx, e)
case BinaryV03:
fallthrough
case StructuredV03:
if c.v03 == nil {
c.v03 = &CodecV03{Encoding: encoding}
}
return c.v03.Encode(ctx, e)
default:
return nil, fmt.Errorf("unknown encoding: %s", encoding)
case BinaryV01, StructuredV01:
c._v01.Do(func() {
c.v01 = &CodecV01{DefaultEncoding: c.Encoding}
})
return c.v01, nil
case BinaryV02, StructuredV02:
c._v02.Do(func() {
c.v02 = &CodecV02{DefaultEncoding: c.Encoding}
})
return c.v02, nil
case BinaryV03, StructuredV03, BatchedV03:
c._v03.Do(func() {
c.v03 = &CodecV03{DefaultEncoding: c.Encoding}
})
return c.v03, nil
case BinaryV1, StructuredV1, BatchedV1:
c._v1.Do(func() {
c.v1 = &CodecV1{DefaultEncoding: c.Encoding}
})
return c.v1, nil
}
return nil, fmt.Errorf("unknown encoding: %s", encoding)
}
// Encode encodes the provided event into a transport message.
func (c *Codec) Encode(ctx context.Context, e cloudevents.Event) (transport.Message, error) {
encoding := c.Encoding
if encoding == Default && c.DefaultEncodingSelectionFn != nil {
encoding = c.DefaultEncodingSelectionFn(ctx, e)
}
codec, err := c.loadCodec(encoding)
if err != nil {
return nil, err
}
ctx = cecontext.WithEncoding(ctx, encoding.Name())
return codec.Encode(ctx, e)
}
// Decode converts a provided transport message into an Event, or error.
func (c *Codec) Decode(ctx context.Context, msg transport.Message) (*cloudevents.Event, error) {
switch c.inspectEncoding(ctx, msg) {
case BinaryV01, StructuredV01:
if c.v01 == nil {
c.v01 = &CodecV01{Encoding: c.Encoding}
}
if event, err := c.v01.Decode(ctx, msg); err != nil {
return nil, err
} else {
return c.convertEvent(event), nil
}
case BinaryV02, StructuredV02:
if c.v02 == nil {
c.v02 = &CodecV02{Encoding: c.Encoding}
}
if event, err := c.v02.Decode(ctx, msg); err != nil {
return nil, err
} else {
return c.convertEvent(event), nil
}
case BinaryV03, StructuredV03, BatchedV03:
if c.v03 == nil {
c.v03 = &CodecV03{Encoding: c.Encoding}
}
if event, err := c.v03.Decode(ctx, msg); err != nil {
return nil, err
} else {
return c.convertEvent(event), nil
}
default:
return nil, transport.NewErrMessageEncodingUnknown("wrapper", TransportName)
codec, err := c.loadCodec(c.inspectEncoding(ctx, msg))
if err != nil {
return nil, err
}
event, err := codec.Decode(ctx, msg)
if err != nil {
return nil, err
}
return c.convertEvent(event)
}
// Give the context back as the user expects
func (c *Codec) convertEvent(event *cloudevents.Event) *cloudevents.Event {
func (c *Codec) convertEvent(event *cloudevents.Event) (*cloudevents.Event, error) {
if event == nil {
return nil
return nil, errors.New("event is nil, can not convert")
}
switch c.Encoding {
case Default:
return event
case BinaryV01:
fallthrough
case StructuredV01:
if c.v01 == nil {
c.v01 = &CodecV01{Encoding: c.Encoding}
}
return event, nil
case BinaryV01, StructuredV01:
ca := event.Context.AsV01()
event.Context = ca
return event
case BinaryV02:
fallthrough
case StructuredV02:
if c.v02 == nil {
c.v02 = &CodecV02{Encoding: c.Encoding}
}
return event, nil
case BinaryV02, StructuredV02:
ca := event.Context.AsV02()
event.Context = ca
return event
case BinaryV03:
fallthrough
case StructuredV03:
fallthrough
case BatchedV03:
if c.v03 == nil {
c.v03 = &CodecV03{Encoding: c.Encoding}
}
return event, nil
case BinaryV03, StructuredV03, BatchedV03:
ca := event.Context.AsV03()
event.Context = ca
return event
return event, nil
case BinaryV1, StructuredV1, BatchedV1:
ca := event.Context.AsV03()
event.Context = ca
return event, nil
default:
return nil
return nil, fmt.Errorf("unknown encoding: %s", c.Encoding)
}
}
func (c *Codec) inspectEncoding(ctx context.Context, msg transport.Message) Encoding {
// TODO: there should be a better way to make the version codecs on demand.
if c.v01 == nil {
c.v01 = &CodecV01{Encoding: c.Encoding}
}
// Try v0.1 first.
encoding := c.v01.inspectEncoding(ctx, msg)
// Try v1.0.
_, _ = c.loadCodec(BinaryV1)
encoding := c.v1.inspectEncoding(ctx, msg)
if encoding != Unknown {
return encoding
}
if c.v02 == nil {
c.v02 = &CodecV02{Encoding: c.Encoding}
// Try v0.3.
_, _ = c.loadCodec(BinaryV03)
encoding = c.v03.inspectEncoding(ctx, msg)
if encoding != Unknown {
return encoding
}
// Try v0.2.
_, _ = c.loadCodec(BinaryV02)
encoding = c.v02.inspectEncoding(ctx, msg)
if encoding != Unknown {
return encoding
}
if c.v03 == nil {
c.v03 = &CodecV03{Encoding: c.Encoding}
}
// Try v0.3.
encoding = c.v03.inspectEncoding(ctx, msg)
// Try v0.1 first.
_, _ = c.loadCodec(BinaryV01)
encoding = c.v01.inspectEncoding(ctx, msg)
if encoding != Unknown {
return encoding
}

View File

@ -13,7 +13,7 @@ import (
// CodecStructured represents an structured http transport codec for all versions.
// Intended to be used as a base class.
type CodecStructured struct {
Encoding Encoding
DefaultEncoding Encoding
}
func (v CodecStructured) encodeStructured(ctx context.Context, e cloudevents.Event) (transport.Message, error) {

View File

@ -9,16 +9,17 @@ import (
"strings"
"github.com/cloudevents/sdk-go/pkg/cloudevents"
cecontext "github.com/cloudevents/sdk-go/pkg/cloudevents/context"
"github.com/cloudevents/sdk-go/pkg/cloudevents/observability"
"github.com/cloudevents/sdk-go/pkg/cloudevents/transport"
"github.com/cloudevents/sdk-go/pkg/cloudevents/types"
)
// CodecV01 represents a http transport codec that uses CloudEvents spec v0.3
// CodecV01 represents a http transport codec that uses CloudEvents spec v0.1
type CodecV01 struct {
CodecStructured
Encoding Encoding
DefaultEncoding Encoding
}
// Adheres to Codec
@ -26,9 +27,19 @@ var _ transport.Codec = (*CodecV01)(nil)
// Encode implements Codec.Encode
func (v CodecV01) Encode(ctx context.Context, e cloudevents.Event) (transport.Message, error) {
// TODO: wire context
_, r := observability.NewReporter(context.Background(), CodecObserved{o: reportEncode, c: v.Encoding.Codec()})
m, err := v.obsEncode(ctx, e)
encoding := v.DefaultEncoding
strEnc := cecontext.EncodingFrom(ctx)
if strEnc != "" {
switch strEnc {
case Binary:
encoding = BinaryV01
case Structured:
encoding = StructuredV01
}
}
_, r := observability.NewReporter(context.Background(), CodecObserved{o: reportEncode, c: encoding.Codec()})
m, err := v.obsEncode(ctx, e, encoding)
if err != nil {
r.Error()
} else {
@ -37,8 +48,8 @@ func (v CodecV01) Encode(ctx context.Context, e cloudevents.Event) (transport.Me
return m, err
}
func (v CodecV01) obsEncode(ctx context.Context, e cloudevents.Event) (transport.Message, error) {
switch v.Encoding {
func (v CodecV01) obsEncode(ctx context.Context, e cloudevents.Event, encoding Encoding) (transport.Message, error) {
switch encoding {
case Default:
fallthrough
case BinaryV01:
@ -46,13 +57,12 @@ func (v CodecV01) obsEncode(ctx context.Context, e cloudevents.Event) (transport
case StructuredV01:
return v.encodeStructured(ctx, e)
default:
return nil, fmt.Errorf("unknown encoding: %d", v.Encoding)
return nil, fmt.Errorf("unknown encoding: %d", encoding)
}
}
// Decode implements Codec.Decode
func (v CodecV01) Decode(ctx context.Context, msg transport.Message) (*cloudevents.Event, error) {
// TODO: wire context
_, r := observability.NewReporter(ctx, CodecObserved{o: reportDecode, c: v.inspectEncoding(ctx, msg).Codec()}) // TODO: inspectEncoding is not free.
e, err := v.obsDecode(ctx, msg)
if err != nil {
@ -107,15 +117,10 @@ func (v CodecV01) toHeaders(ec *cloudevents.EventContextV01) (http.Header, error
h["CE-EventTypeVersion"] = []string{*ec.EventTypeVersion}
}
if ec.SchemaURL != nil {
h["CE-SchemaURL"] = []string{ec.SchemaURL.String()}
h["CE-DataSchema"] = []string{ec.SchemaURL.String()}
}
if ec.ContentType != nil {
if ec.ContentType != nil && *ec.ContentType != "" {
h.Set("Content-Type", *ec.ContentType)
} else if v.Encoding == Default || v.Encoding == BinaryV01 {
// in binary v0.1, the Content-Type header is tied to ec.ContentType
// This was later found to be an issue with the spec, but yolo.
// TODO: not sure what the default should be?
h.Set("Content-Type", cloudevents.ApplicationJSON)
}
// Regarding Extensions, v0.1 Spec says the following:
@ -172,17 +177,23 @@ func (v CodecV01) fromHeaders(h http.Header) (cloudevents.EventContextV01, error
if source != nil {
ec.Source = *source
}
ec.EventTime = types.ParseTimestamp(h.Get("CE-EventTime"))
var err error
ec.EventTime, err = types.ParseTimestamp(h.Get("CE-EventTime"))
if err != nil {
return ec, err
}
h.Del("CE-EventTime")
etv := h.Get("CE-EventTypeVersion")
h.Del("CE-EventTypeVersion")
if etv != "" {
ec.EventTypeVersion = &etv
}
ec.SchemaURL = types.ParseURLRef(h.Get("CE-SchemaURL"))
h.Del("CE-SchemaURL")
ec.SchemaURL = types.ParseURLRef(h.Get("CE-DataSchema"))
h.Del("CE-DataSchema")
et := h.Get("Content-Type")
ec.ContentType = &et
if et != "" {
ec.ContentType = &et
}
extensions := make(map[string]interface{})
for k, v := range h {

View File

@ -9,6 +9,7 @@ import (
"strings"
"github.com/cloudevents/sdk-go/pkg/cloudevents"
cecontext "github.com/cloudevents/sdk-go/pkg/cloudevents/context"
"github.com/cloudevents/sdk-go/pkg/cloudevents/observability"
"github.com/cloudevents/sdk-go/pkg/cloudevents/transport"
"github.com/cloudevents/sdk-go/pkg/cloudevents/types"
@ -18,7 +19,7 @@ import (
type CodecV02 struct {
CodecStructured
Encoding Encoding
DefaultEncoding Encoding
}
// Adheres to Codec
@ -26,9 +27,19 @@ var _ transport.Codec = (*CodecV02)(nil)
// Encode implements Codec.Encode
func (v CodecV02) Encode(ctx context.Context, e cloudevents.Event) (transport.Message, error) {
// TODO: wire context
_, r := observability.NewReporter(ctx, CodecObserved{o: reportEncode, c: v.Encoding.Codec()})
m, err := v.obsEncode(ctx, e)
encoding := v.DefaultEncoding
strEnc := cecontext.EncodingFrom(ctx)
if strEnc != "" {
switch strEnc {
case Binary:
encoding = BinaryV02
case Structured:
encoding = StructuredV02
}
}
_, r := observability.NewReporter(ctx, CodecObserved{o: reportEncode, c: encoding.Codec()})
m, err := v.obsEncode(ctx, e, encoding)
if err != nil {
r.Error()
} else {
@ -37,8 +48,8 @@ func (v CodecV02) Encode(ctx context.Context, e cloudevents.Event) (transport.Me
return m, err
}
func (v CodecV02) obsEncode(ctx context.Context, e cloudevents.Event) (transport.Message, error) {
switch v.Encoding {
func (v CodecV02) obsEncode(ctx context.Context, e cloudevents.Event, encoding Encoding) (transport.Message, error) {
switch encoding {
case Default:
fallthrough
case BinaryV02:
@ -46,13 +57,12 @@ func (v CodecV02) obsEncode(ctx context.Context, e cloudevents.Event) (transport
case StructuredV02:
return v.encodeStructured(ctx, e)
default:
return nil, fmt.Errorf("unknown encoding: %d", v.Encoding)
return nil, fmt.Errorf("unknown encoding: %d", encoding)
}
}
// Decode implements Codec.Decode
func (v CodecV02) Decode(ctx context.Context, msg transport.Message) (*cloudevents.Event, error) {
// TODO: wire context
_, r := observability.NewReporter(ctx, CodecObserved{o: reportDecode, c: v.inspectEncoding(ctx, msg).Codec()}) // TODO: inspectEncoding is not free.
e, err := v.obsDecode(ctx, msg)
if err != nil {
@ -104,13 +114,8 @@ func (v CodecV02) toHeaders(ec *cloudevents.EventContextV02) (http.Header, error
if ec.SchemaURL != nil {
h.Set("ce-schemaurl", ec.SchemaURL.String())
}
if ec.ContentType != nil {
if ec.ContentType != nil && *ec.ContentType != "" {
h.Set("Content-Type", *ec.ContentType)
} else if v.Encoding == Default || v.Encoding == BinaryV02 {
// in binary v0.2, the Content-Type header is tied to ec.ContentType
// This was later found to be an issue with the spec, but yolo.
// TODO: not sure what the default should be?
h.Set("Content-Type", cloudevents.ApplicationJSON)
}
for k, v := range ec.Extensions {
// Per spec, map-valued extensions are converted to a list of headers as:
@ -182,7 +187,11 @@ func (v CodecV02) fromHeaders(h http.Header) (cloudevents.EventContextV02, error
}
h.Del("ce-source")
ec.Time = types.ParseTimestamp(h.Get("ce-time"))
var err error
ec.Time, err = types.ParseTimestamp(h.Get("ce-time"))
if err != nil {
return ec, err
}
h.Del("ce-time")
ec.SchemaURL = types.ParseURLRef(h.Get("ce-schemaurl"))

View File

@ -9,6 +9,7 @@ import (
"strings"
"github.com/cloudevents/sdk-go/pkg/cloudevents"
cecontext "github.com/cloudevents/sdk-go/pkg/cloudevents/context"
"github.com/cloudevents/sdk-go/pkg/cloudevents/observability"
"github.com/cloudevents/sdk-go/pkg/cloudevents/transport"
"github.com/cloudevents/sdk-go/pkg/cloudevents/types"
@ -18,7 +19,7 @@ import (
type CodecV03 struct {
CodecStructured
Encoding Encoding
DefaultEncoding Encoding
}
// Adheres to Codec
@ -26,9 +27,19 @@ var _ transport.Codec = (*CodecV03)(nil)
// Encode implements Codec.Encode
func (v CodecV03) Encode(ctx context.Context, e cloudevents.Event) (transport.Message, error) {
// TODO: wire context
_, r := observability.NewReporter(ctx, CodecObserved{o: reportEncode, c: v.Encoding.Codec()})
m, err := v.obsEncode(ctx, e)
encoding := v.DefaultEncoding
strEnc := cecontext.EncodingFrom(ctx)
if strEnc != "" {
switch strEnc {
case Binary:
encoding = BinaryV03
case Structured:
encoding = StructuredV03
}
}
_, r := observability.NewReporter(ctx, CodecObserved{o: reportEncode, c: encoding.Codec()})
m, err := v.obsEncode(ctx, e, encoding)
if err != nil {
r.Error()
} else {
@ -37,8 +48,8 @@ func (v CodecV03) Encode(ctx context.Context, e cloudevents.Event) (transport.Me
return m, err
}
func (v CodecV03) obsEncode(ctx context.Context, e cloudevents.Event) (transport.Message, error) {
switch v.Encoding {
func (v CodecV03) obsEncode(ctx context.Context, e cloudevents.Event, encoding Encoding) (transport.Message, error) {
switch encoding {
case Default:
fallthrough
case BinaryV03:
@ -48,13 +59,12 @@ func (v CodecV03) obsEncode(ctx context.Context, e cloudevents.Event) (transport
case BatchedV03:
return nil, fmt.Errorf("not implemented")
default:
return nil, fmt.Errorf("unknown encoding: %d", v.Encoding)
return nil, fmt.Errorf("unknown encoding: %d", encoding)
}
}
// Decode implements Codec.Decode
func (v CodecV03) Decode(ctx context.Context, msg transport.Message) (*cloudevents.Event, error) {
// TODO: wire context
_, r := observability.NewReporter(ctx, CodecObserved{o: reportDecode, c: v.inspectEncoding(ctx, msg).Codec()}) // TODO: inspectEncoding is not free.
e, err := v.obsDecode(ctx, msg)
if err != nil {
@ -112,19 +122,15 @@ func (v CodecV03) toHeaders(ec *cloudevents.EventContextV03) (http.Header, error
if ec.SchemaURL != nil {
h.Set("ce-schemaurl", ec.SchemaURL.String())
}
if ec.DataContentType != nil {
if ec.DataContentType != nil && *ec.DataContentType != "" {
h.Set("Content-Type", *ec.DataContentType)
} else if v.Encoding == Default || v.Encoding == BinaryV03 {
// in binary v0.2, the Content-Type header is tied to ec.ContentType
// This was later found to be an issue with the spec, but yolo.
// TODO: not sure what the default should be?
h.Set("Content-Type", cloudevents.ApplicationJSON)
}
if ec.DataContentEncoding != nil {
h.Set("ce-datacontentencoding", *ec.DataContentEncoding)
}
for k, v := range ec.Extensions {
k = strings.ToLower(k)
// Per spec, map-valued extensions are converted to a list of headers as:
// CE-attrib-key
switch v.(type) {
@ -212,7 +218,11 @@ func (v CodecV03) fromHeaders(h http.Header) (cloudevents.EventContextV03, error
}
h.Del("ce-subject")
ec.Time = types.ParseTimestamp(h.Get("ce-time"))
var err error
ec.Time, err = types.ParseTimestamp(h.Get("ce-time"))
if err != nil {
return ec, err
}
h.Del("ce-time")
ec.SchemaURL = types.ParseURLRef(h.Get("ce-schemaurl"))
@ -235,6 +245,7 @@ func (v CodecV03) fromHeaders(h http.Header) (cloudevents.EventContextV03, error
extensions := make(map[string]interface{})
for k, v := range h {
k = strings.ToLower(k)
if len(k) > len("ce-") && strings.EqualFold(k[:len("ce-")], "ce-") {
ak := strings.ToLower(k[len("ce-"):])
if i := strings.Index(ak, "-"); i > 0 {

View File

@ -0,0 +1,245 @@
package http
import (
"context"
"fmt"
"net/http"
"net/textproto"
"strings"
"github.com/cloudevents/sdk-go/pkg/cloudevents"
cecontext "github.com/cloudevents/sdk-go/pkg/cloudevents/context"
"github.com/cloudevents/sdk-go/pkg/cloudevents/observability"
"github.com/cloudevents/sdk-go/pkg/cloudevents/transport"
"github.com/cloudevents/sdk-go/pkg/cloudevents/types"
)
// CodecV1 represents a http transport codec that uses CloudEvents spec v1.0
type CodecV1 struct {
CodecStructured
DefaultEncoding Encoding
}
// Adheres to Codec
var _ transport.Codec = (*CodecV1)(nil)
// Encode implements Codec.Encode
func (v CodecV1) Encode(ctx context.Context, e cloudevents.Event) (transport.Message, error) {
encoding := v.DefaultEncoding
strEnc := cecontext.EncodingFrom(ctx)
if strEnc != "" {
switch strEnc {
case Binary:
encoding = BinaryV1
case Structured:
encoding = StructuredV1
}
}
_, r := observability.NewReporter(ctx, CodecObserved{o: reportEncode, c: encoding.Codec()})
m, err := v.obsEncode(ctx, e, encoding)
if err != nil {
r.Error()
} else {
r.OK()
}
return m, err
}
func (v CodecV1) obsEncode(ctx context.Context, e cloudevents.Event, encoding Encoding) (transport.Message, error) {
switch encoding {
case Default:
fallthrough
case BinaryV1:
return v.encodeBinary(ctx, e)
case StructuredV1:
return v.encodeStructured(ctx, e)
case BatchedV1:
return nil, fmt.Errorf("not implemented")
default:
return nil, fmt.Errorf("unknown encoding: %d", encoding)
}
}
// Decode implements Codec.Decode
func (v CodecV1) Decode(ctx context.Context, msg transport.Message) (*cloudevents.Event, error) {
_, r := observability.NewReporter(ctx, CodecObserved{o: reportDecode, c: v.inspectEncoding(ctx, msg).Codec()}) // TODO: inspectEncoding is not free.
e, err := v.obsDecode(ctx, msg)
if err != nil {
r.Error()
} else {
r.OK()
}
return e, err
}
func (v CodecV1) obsDecode(ctx context.Context, msg transport.Message) (*cloudevents.Event, error) {
switch v.inspectEncoding(ctx, msg) {
case BinaryV1:
return v.decodeBinary(ctx, msg)
case StructuredV1:
return v.decodeStructured(ctx, cloudevents.CloudEventsVersionV1, msg)
case BatchedV1:
return nil, fmt.Errorf("not implemented")
default:
return nil, transport.NewErrMessageEncodingUnknown("V1", TransportName)
}
}
func (v CodecV1) encodeBinary(ctx context.Context, e cloudevents.Event) (transport.Message, error) {
header, err := v.toHeaders(e.Context.AsV1())
if err != nil {
return nil, err
}
body, err := e.DataBytes()
if err != nil {
return nil, err
}
msg := &Message{
Header: header,
Body: body,
}
return msg, nil
}
func (v CodecV1) toHeaders(ec *cloudevents.EventContextV1) (http.Header, error) {
h := http.Header{}
h.Set("ce-specversion", ec.SpecVersion)
h.Set("ce-type", ec.Type)
h.Set("ce-source", ec.Source.String())
if ec.Subject != nil {
h.Set("ce-subject", *ec.Subject)
}
h.Set("ce-id", ec.ID)
if ec.Time != nil && !ec.Time.IsZero() {
h.Set("ce-time", ec.Time.String())
}
if ec.DataSchema != nil {
h.Set("ce-dataschema", ec.DataSchema.String())
}
if ec.DataContentType != nil && *ec.DataContentType != "" {
h.Set("Content-Type", *ec.DataContentType)
}
for k, v := range ec.Extensions {
k = strings.ToLower(k)
// Per spec, extensions are strings and converted to a list of headers as:
// ce-key: value
cstr, err := types.Format(v)
if err != nil {
return h, err
}
h.Set("ce-"+k, cstr)
}
return h, nil
}
func (v CodecV1) decodeBinary(ctx context.Context, msg transport.Message) (*cloudevents.Event, error) {
m, ok := msg.(*Message)
if !ok {
return nil, fmt.Errorf("failed to convert transport.Message to http.Message")
}
ca, err := v.fromHeaders(m.Header)
if err != nil {
return nil, err
}
var body interface{}
if len(m.Body) > 0 {
body = m.Body
}
return &cloudevents.Event{
Context: &ca,
Data: body,
DataEncoded: body != nil,
}, nil
}
func (v CodecV1) fromHeaders(h http.Header) (cloudevents.EventContextV1, error) {
// Normalize headers.
for k, v := range h {
ck := textproto.CanonicalMIMEHeaderKey(k)
if k != ck {
delete(h, k)
h[ck] = v
}
}
ec := cloudevents.EventContextV1{}
ec.SpecVersion = h.Get("ce-specversion")
h.Del("ce-specversion")
ec.ID = h.Get("ce-id")
h.Del("ce-id")
ec.Type = h.Get("ce-type")
h.Del("ce-type")
source := types.ParseURIRef(h.Get("ce-source"))
if source != nil {
ec.Source = *source
}
h.Del("ce-source")
subject := h.Get("ce-subject")
if subject != "" {
ec.Subject = &subject
}
h.Del("ce-subject")
var err error
ec.Time, err = types.ParseTimestamp(h.Get("ce-time"))
if err != nil {
return ec, err
}
h.Del("ce-time")
ec.DataSchema = types.ParseURI(h.Get("ce-dataschema"))
h.Del("ce-dataschema")
contentType := h.Get("Content-Type")
if contentType != "" {
ec.DataContentType = &contentType
}
h.Del("Content-Type")
// At this point, we have deleted all the known headers.
// Everything left is assumed to be an extension.
extensions := make(map[string]interface{})
for k := range h {
k = strings.ToLower(k)
if len(k) > len("ce-") && strings.EqualFold(k[:len("ce-")], "ce-") {
ak := strings.ToLower(k[len("ce-"):])
extensions[ak] = h.Get(k)
}
}
if len(extensions) > 0 {
ec.Extensions = extensions
}
return ec, nil
}
func (v CodecV1) inspectEncoding(ctx context.Context, msg transport.Message) Encoding {
version := msg.CloudEventsVersion()
if version != cloudevents.CloudEventsVersionV1 {
return Unknown
}
m, ok := msg.(*Message)
if !ok {
return Unknown
}
contentType := m.Header.Get("Content-Type")
if contentType == cloudevents.ApplicationCloudEventsJSON {
return StructuredV1
}
if contentType == cloudevents.ApplicationCloudEventsBatchJSON {
return BatchedV1
}
return BinaryV1
}

View File

@ -29,6 +29,13 @@ const (
StructuredV03
// BatchedV03 is Batched CloudEvents spec v0.3.
BatchedV03
// BinaryV1 is Binary CloudEvents spec v1.0.
BinaryV1
// StructuredV03 is Structured CloudEvents spec v1.0.
StructuredV1
// BatchedV1 is Batched CloudEvents spec v1.0.
BatchedV1
// Unknown is unknown.
Unknown
@ -39,6 +46,10 @@ const (
// Structured is used for Context Based Encoding Selections to use the
// DefaultStructuredEncodingSelectionStrategy
Structured = "structured"
// Batched is used for Context Based Encoding Selections to use the
// DefaultStructuredEncodingSelectionStrategy
Batched = "batched"
)
func ContextBasedEncodingSelectionStrategy(ctx context.Context, e cloudevents.Event) Encoding {
@ -62,6 +73,8 @@ func DefaultBinaryEncodingSelectionStrategy(ctx context.Context, e cloudevents.E
return BinaryV02
case cloudevents.CloudEventsVersionV03:
return BinaryV03
case cloudevents.CloudEventsVersionV1:
return BinaryV1
}
// Unknown version, return Default.
return Default
@ -77,6 +90,8 @@ func DefaultStructuredEncodingSelectionStrategy(ctx context.Context, e cloudeven
return StructuredV02
case cloudevents.CloudEventsVersionV03:
return StructuredV03
case cloudevents.CloudEventsVersionV1:
return StructuredV1
}
// Unknown version, return Default.
return Default
@ -89,23 +104,15 @@ func (e Encoding) String() string {
return "Default Encoding " + e.Version()
// Binary
case BinaryV01:
fallthrough
case BinaryV02:
fallthrough
case BinaryV03:
case BinaryV01, BinaryV02, BinaryV03, BinaryV1:
return "Binary Encoding " + e.Version()
// Structured
case StructuredV01:
fallthrough
case StructuredV02:
fallthrough
case StructuredV03:
case StructuredV01, StructuredV02, StructuredV03, StructuredV1:
return "Structured Encoding " + e.Version()
// Batched
case BatchedV03:
case BatchedV03, BatchedV1:
return "Batched Encoding " + e.Version()
default:
@ -120,25 +127,21 @@ func (e Encoding) Version() string {
return "Default"
// Version 0.1
case BinaryV01:
fallthrough
case StructuredV01:
case BinaryV01, StructuredV01:
return "v0.1"
// Version 0.2
case BinaryV02:
fallthrough
case StructuredV02:
case BinaryV02, StructuredV02:
return "v0.2"
// Version 0.3
case BinaryV03:
fallthrough
case StructuredV03:
fallthrough
case BatchedV03:
case BinaryV03, StructuredV03, BatchedV03:
return "v0.3"
// Version 1.0
case BinaryV1, StructuredV1, BatchedV1:
return "v1.0"
// Unknown
default:
return "Unknown"
@ -171,8 +174,32 @@ func (e Encoding) Codec() string {
case BatchedV03:
return "batched/v0.3"
// Version 1.0
case BinaryV1:
return "binary/v1.0"
case StructuredV1:
return "structured/v1.0"
case BatchedV1:
return "batched/v1.0"
// Unknown
default:
return "unknown"
}
}
// Name creates a string to represent the the codec name.
func (e Encoding) Name() string {
switch e {
case Default:
return Binary
case BinaryV01, BinaryV02, BinaryV03, BinaryV1:
return Binary
case StructuredV01, StructuredV02, StructuredV03, StructuredV1:
return Structured
case BatchedV03, BatchedV1:
return Batched
default:
return Binary
}
}

View File

@ -178,7 +178,7 @@ func WithPort(port int) Option {
if t == nil {
return fmt.Errorf("http port option can not set nil transport")
}
if port < 0 {
if port < 0 || port > 65535 {
return fmt.Errorf("http port option was given an invalid port: %d", port)
}
if err := checkListen(t, "http port option"); err != nil {

View File

@ -181,6 +181,7 @@ func (t *Transport) obsSend(ctx context.Context, event cloudevents.Event) (conte
req.Method = t.Req.Method
req.URL = t.Req.URL
req.Close = t.Req.Close
req.Host = t.Req.Host
copyHeadersEnsure(t.Req.Header, &req.Header)
}
@ -214,14 +215,21 @@ func (t *Transport) obsSend(ctx context.Context, event cloudevents.Event) (conte
})
if err != nil {
isErr := true
handled := false
if txerr, ok := err.(*transport.ErrTransportMessageConversion); ok {
if !txerr.IsFatal() {
isErr = false
}
if txerr.Handled() {
handled = true
}
}
if isErr {
return rctx, nil, err
}
if handled {
return rctx, nil, nil
}
}
if accepted(resp) {
return rctx, respEvent, nil
@ -240,13 +248,13 @@ func (t *Transport) MessageToEvent(ctx context.Context, msg *Message) (*cloudeve
if msg.CloudEventsVersion() != "" {
// This is likely a cloudevents encoded message, try to decode it.
if ok := t.loadCodec(ctx); !ok {
err = transport.NewErrTransportMessageConversion("http", fmt.Sprintf("unknown encoding set on transport: %d", t.Encoding), true)
err = transport.NewErrTransportMessageConversion("http", fmt.Sprintf("unknown encoding set on transport: %d", t.Encoding), false, true)
logger.Error("failed to load codec", zap.Error(err))
} else {
event, err = t.codec.Decode(ctx, msg)
}
} else {
err = transport.NewErrTransportMessageConversion("http", "cloudevents version unknown", false)
err = transport.NewErrTransportMessageConversion("http", "cloudevents version unknown", false, false)
}
// If codec returns and error, or could not load the correct codec, try
@ -254,12 +262,19 @@ func (t *Transport) MessageToEvent(ctx context.Context, msg *Message) (*cloudeve
if err != nil && t.HasConverter() {
event, err = t.Converter.Convert(ctx, msg, err)
}
// If err is still set, it means that there was no converter, or the
// converter failed to convert.
if err != nil {
logger.Debug("failed to decode message", zap.Error(err))
}
// If event and error are both nil, then there is nothing to do with this event, it was handled.
if err == nil && event == nil {
logger.Debug("convert function returned (nil, nil)")
err = transport.NewErrTransportMessageConversion("http", "convert function handled request", true, false)
}
return event, err
}
@ -548,16 +563,30 @@ func (t *Transport) ServeHTTP(w http.ResponseWriter, req *http.Request) {
})
if err != nil {
isFatal := true
handled := false
if txerr, ok := err.(*transport.ErrTransportMessageConversion); ok {
isFatal = txerr.IsFatal()
handled = txerr.Handled()
}
if isFatal || event == nil {
if isFatal {
logger.Errorw("failed to convert http message to event", zap.Error(err))
w.WriteHeader(http.StatusBadRequest)
_, _ = w.Write([]byte(fmt.Sprintf(`{"error":%q}`, err.Error())))
r.Error()
return
}
// if handled, do not pass to receiver.
if handled {
w.WriteHeader(http.StatusNoContent)
r.OK()
return
}
}
if event == nil {
logger.Error("failed to get non-nil event from MessageToEvent")
w.WriteHeader(http.StatusBadRequest)
r.Error()
return
}
resp, err := t.invokeReceiver(ctx, *event)
@ -623,6 +652,9 @@ func (t *Transport) listen() (net.Addr, error) {
port := 8080
if t.Port != nil {
port = *t.Port
if port < 0 || port > 65535 {
return nil, fmt.Errorf("invalid port %d", port)
}
}
var err error
if t.listener, err = net.Listen("tcp", fmt.Sprintf(":%d", port)); err != nil {

View File

@ -1,4 +1,41 @@
/*
Package types provides custom types to support CloudEvents.
Package types implements the CloudEvents type system.
CloudEvents defines a set of abstract types for event context attributes. Each
type has a corresponding native Go type and a canonical string encoding. The
native Go types used to represent the CloudEvents types are:
bool, int32, string, []byte, *url.URL, time.Time
+----------------+----------------+-----------------------------------+
|CloudEvents Type|Native Type |Convertible From |
+================+================+===================================+
|Bool |bool |bool |
+----------------+----------------+-----------------------------------+
|Integer |int32 |Any numeric type with value in |
| | |range of int32 |
+----------------+----------------+-----------------------------------+
|String |string |string |
+----------------+----------------+-----------------------------------+
|Binary |[]byte |[]byte |
+----------------+----------------+-----------------------------------+
|URI-Reference |*url.URL |url.URL, types.URIRef, types.URI |
+----------------+----------------+-----------------------------------+
|URI |*url.URL |url.URL, types.URIRef, types.URI |
| | |Must be an absolute URI. |
+----------------+----------------+-----------------------------------+
|Timestamp |time.Time |time.Time, types.Timestamp |
+----------------+----------------+-----------------------------------+
Extension attributes may be stored as a native type or a canonical string. The
To<Type> functions will convert to the desired <Type> from any convertible type
or from the canonical string form.
The Parse<Type> and Format<Type> functions convert native types to/from
canonical strings.
Note are no Parse or Format functions for URL or string. For URL use the
standard url.Parse() and url.URL.String(). The canonical string format of a
string is the string itself.
*/
package types

View File

@ -16,15 +16,12 @@ type Timestamp struct {
}
// ParseTimestamp attempts to parse the given time assuming RFC3339 layout
func ParseTimestamp(t string) *Timestamp {
if t == "" {
return nil
func ParseTimestamp(s string) (*Timestamp, error) {
if s == "" {
return nil, nil
}
timestamp, err := time.Parse(time.RFC3339Nano, t)
if err != nil {
return nil
}
return &Timestamp{Time: timestamp}
tt, err := ParseTime(s)
return &Timestamp{Time: tt}, err
}
// MarshalJSON implements a custom json marshal method used when this type is
@ -33,8 +30,7 @@ func (t *Timestamp) MarshalJSON() ([]byte, error) {
if t == nil || t.IsZero() {
return []byte(`""`), nil
}
rfc3339 := fmt.Sprintf("%q", t.UTC().Format(time.RFC3339Nano))
return []byte(rfc3339), nil
return []byte(fmt.Sprintf("%q", t)), nil
}
// UnmarshalJSON implements the json unmarshal method used when this type is
@ -44,10 +40,9 @@ func (t *Timestamp) UnmarshalJSON(b []byte) error {
if err := json.Unmarshal(b, &timestamp); err != nil {
return err
}
if pt := ParseTimestamp(timestamp); pt != nil {
*t = *pt
}
return nil
var err error
t.Time, err = ParseTime(timestamp)
return err
}
// MarshalXML implements a custom xml marshal method used when this type is
@ -56,8 +51,7 @@ func (t *Timestamp) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
if t == nil || t.IsZero() {
return e.EncodeElement(nil, start)
}
v := t.UTC().Format(time.RFC3339Nano)
return e.EncodeElement(v, start)
return e.EncodeElement(t.String(), start)
}
// UnmarshalXML implements the xml unmarshal method used when this type is
@ -67,17 +61,10 @@ func (t *Timestamp) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
if err := d.DecodeElement(&timestamp, &start); err != nil {
return err
}
if pt := ParseTimestamp(timestamp); pt != nil {
*t = *pt
}
return nil
var err error
t.Time, err = ParseTime(timestamp)
return err
}
// String outputs the time using layout RFC3339.
func (t *Timestamp) String() string {
if t == nil {
return time.Time{}.UTC().Format(time.RFC3339Nano)
}
return t.UTC().Format(time.RFC3339Nano)
}
// String outputs the time using RFC3339 format.
func (t Timestamp) String() string { return FormatTime(t.Time) }

View File

@ -0,0 +1,77 @@
package types
import (
"encoding/json"
"encoding/xml"
"fmt"
"net/url"
)
// URI is a wrapper to url.URL. It is intended to enforce compliance with
// the CloudEvents spec for their definition of URI. Custom
// marshal methods are implemented to ensure the outbound URI object
// is a flat string.
type URI struct {
url.URL
}
// ParseURI attempts to parse the given string as a URI.
func ParseURI(u string) *URI {
if u == "" {
return nil
}
pu, err := url.Parse(u)
if err != nil {
return nil
}
return &URI{URL: *pu}
}
// MarshalJSON implements a custom json marshal method used when this type is
// marshaled using json.Marshal.
func (u URI) MarshalJSON() ([]byte, error) {
b := fmt.Sprintf("%q", u.String())
return []byte(b), nil
}
// UnmarshalJSON implements the json unmarshal method used when this type is
// unmarshaled using json.Unmarshal.
func (u *URI) UnmarshalJSON(b []byte) error {
var ref string
if err := json.Unmarshal(b, &ref); err != nil {
return err
}
r := ParseURI(ref)
if r != nil {
*u = *r
}
return nil
}
// MarshalXML implements a custom xml marshal method used when this type is
// marshaled using xml.Marshal.
func (u URI) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
return e.EncodeElement(u.String(), start)
}
// UnmarshalXML implements the xml unmarshal method used when this type is
// unmarshaled using xml.Unmarshal.
func (u *URI) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
var ref string
if err := d.DecodeElement(&ref, &start); err != nil {
return err
}
r := ParseURI(ref)
if r != nil {
*u = *r
}
return nil
}
// String returns the full string representation of the URI-Reference.
func (u *URI) String() string {
if u == nil {
return ""
}
return u.URL.String()
}

View File

@ -0,0 +1,77 @@
package types
import (
"encoding/json"
"encoding/xml"
"fmt"
"net/url"
)
// URIRef is a wrapper to url.URL. It is intended to enforce compliance with
// the CloudEvents spec for their definition of URI-Reference. Custom
// marshal methods are implemented to ensure the outbound URIRef object is
// is a flat string.
type URIRef struct {
url.URL
}
// ParseURIRef attempts to parse the given string as a URI-Reference.
func ParseURIRef(u string) *URIRef {
if u == "" {
return nil
}
pu, err := url.Parse(u)
if err != nil {
return nil
}
return &URIRef{URL: *pu}
}
// MarshalJSON implements a custom json marshal method used when this type is
// marshaled using json.Marshal.
func (u URIRef) MarshalJSON() ([]byte, error) {
b := fmt.Sprintf("%q", u.String())
return []byte(b), nil
}
// UnmarshalJSON implements the json unmarshal method used when this type is
// unmarshaled using json.Unmarshal.
func (u *URIRef) UnmarshalJSON(b []byte) error {
var ref string
if err := json.Unmarshal(b, &ref); err != nil {
return err
}
r := ParseURIRef(ref)
if r != nil {
*u = *r
}
return nil
}
// MarshalXML implements a custom xml marshal method used when this type is
// marshaled using xml.Marshal.
func (u URIRef) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
return e.EncodeElement(u.String(), start)
}
// UnmarshalXML implements the xml unmarshal method used when this type is
// unmarshaled using xml.Unmarshal.
func (u *URIRef) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
var ref string
if err := d.DecodeElement(&ref, &start); err != nil {
return err
}
r := ParseURIRef(ref)
if r != nil {
*u = *r
}
return nil
}
// String returns the full string representation of the URI-Reference.
func (u *URIRef) String() string {
if u == nil {
return ""
}
return u.URL.String()
}

View File

@ -11,6 +11,8 @@ import (
// the CloudEvents spec for their definition of URI-Reference. Custom
// marshal methods are implemented to ensure the outbound URLRef object is
// is a flat string.
//
// deprecated: use URIRef.
type URLRef struct {
url.URL
}

View File

@ -0,0 +1,260 @@
package types
import (
"encoding/base64"
"fmt"
"math"
"net/url"
"reflect"
"strconv"
"time"
)
// FormatBool returns canonical string format: "true" or "false"
func FormatBool(v bool) string { return strconv.FormatBool(v) }
// FormatInteger returns canonical string format: decimal notation.
func FormatInteger(v int32) string { return strconv.Itoa(int(v)) }
// FormatBinary returns canonical string format: standard base64 encoding
func FormatBinary(v []byte) string { return base64.StdEncoding.EncodeToString(v) }
// FormatTime returns canonical string format: RFC3339 with nanoseconds
func FormatTime(v time.Time) string { return v.UTC().Format(time.RFC3339Nano) }
// ParseBool parse canonical string format: "true" or "false"
func ParseBool(v string) (bool, error) { return strconv.ParseBool(v) }
// ParseInteger parse canonical string format: decimal notation.
func ParseInteger(v string) (int32, error) {
// Accept floating-point but truncate to int32 as per CE spec.
f, err := strconv.ParseFloat(v, 64)
if err != nil {
return 0, err
}
if f > math.MaxInt32 || f < math.MinInt32 {
return 0, rangeErr(v)
}
return int32(f), nil
}
// ParseBinary parse canonical string format: standard base64 encoding
func ParseBinary(v string) ([]byte, error) { return base64.StdEncoding.DecodeString(v) }
// ParseTime parse canonical string format: RFC3339 with nanoseconds
func ParseTime(v string) (time.Time, error) {
t, err := time.Parse(time.RFC3339Nano, v)
if err != nil {
err := convertErr(time.Time{}, v)
err.extra = ": not in RFC3339 format"
return time.Time{}, err
}
return t, nil
}
// Format returns the canonical string format of v, where v can be
// any type that is convertible to a CloudEvents type.
func Format(v interface{}) (string, error) {
v, err := Validate(v)
if err != nil {
return "", err
}
switch v := v.(type) {
case bool:
return FormatBool(v), nil
case int32:
return FormatInteger(v), nil
case string:
return v, nil
case []byte:
return FormatBinary(v), nil
case url.URL:
return v.String(), nil
case *url.URL:
// url.URL is often passed by pointer so allow both
return v.String(), nil
case time.Time:
return FormatTime(v), nil
default:
return "", fmt.Errorf("%T is not a CloudEvents type", v)
}
}
// Validate v is a valid CloudEvents attribute value, convert it to one of:
// bool, int32, string, []byte, *url.URL, time.Time
func Validate(v interface{}) (interface{}, error) {
switch v := v.(type) {
case bool, int32, string, []byte, time.Time:
return v, nil // Already a CloudEvents type, no validation needed.
case uint, uintptr, uint8, uint16, uint32, uint64:
u := reflect.ValueOf(v).Uint()
if u > math.MaxInt32 {
return nil, rangeErr(v)
}
return int32(u), nil
case int, int8, int16, int64:
i := reflect.ValueOf(v).Int()
if i > math.MaxInt32 || i < math.MinInt32 {
return nil, rangeErr(v)
}
return int32(i), nil
case float32, float64:
f := reflect.ValueOf(v).Float()
if f > math.MaxInt32 || f < math.MinInt32 {
return nil, rangeErr(v)
}
return int32(f), nil
case *url.URL:
if v == nil {
break
}
return v, nil
case url.URL:
return &v, nil
case URIRef:
return &v.URL, nil
case URI:
return &v.URL, nil
case URLRef:
return &v.URL, nil
case Timestamp:
return v.Time, nil
}
rx := reflect.ValueOf(v)
if rx.Kind() == reflect.Ptr && !rx.IsNil() {
// Allow pointers-to convertible types
return Validate(rx.Elem().Interface())
}
return nil, fmt.Errorf("invalid CloudEvents value: %#v", v)
}
// ToBool accepts a bool value or canonical "true"/"false" string.
func ToBool(v interface{}) (bool, error) {
v, err := Validate(v)
if err != nil {
return false, err
}
switch v := v.(type) {
case bool:
return v, nil
case string:
return ParseBool(v)
default:
return false, convertErr(true, v)
}
}
// ToInteger accepts any numeric value in int32 range, or canonical string.
func ToInteger(v interface{}) (int32, error) {
v, err := Validate(v)
if err != nil {
return 0, err
}
switch v := v.(type) {
case int32:
return v, nil
case string:
return ParseInteger(v)
default:
return 0, convertErr(int32(0), v)
}
}
// ToString returns a string value unaltered.
//
// This function does not perform canonical string encoding, use one of the
// Format functions for that.
func ToString(v interface{}) (string, error) {
v, err := Validate(v)
if err != nil {
return "", err
}
switch v := v.(type) {
case string:
return v, nil
default:
return "", convertErr("", v)
}
}
// ToBinary returns a []byte value, decoding from base64 string if necessary.
func ToBinary(v interface{}) ([]byte, error) {
v, err := Validate(v)
if err != nil {
return nil, err
}
switch v := v.(type) {
case []byte:
return v, nil
case string:
return base64.StdEncoding.DecodeString(v)
default:
return nil, convertErr([]byte(nil), v)
}
}
// ToURL returns a *url.URL value, parsing from string if necessary.
func ToURL(v interface{}) (*url.URL, error) {
v, err := Validate(v)
if err != nil {
return nil, err
}
switch v := v.(type) {
case *url.URL:
return v, nil
case string:
u, err := url.Parse(v)
if err != nil {
return nil, err
}
return u, nil
default:
return nil, convertErr((*url.URL)(nil), v)
}
}
// ToTime returns a time.Time value, parsing from RFC3339 string if necessary.
func ToTime(v interface{}) (time.Time, error) {
v, err := Validate(v)
if err != nil {
return time.Time{}, err
}
switch v := v.(type) {
case time.Time:
return v, nil
case string:
ts, err := time.Parse(time.RFC3339Nano, v)
if err != nil {
return time.Time{}, err
}
return ts, nil
default:
return time.Time{}, convertErr(time.Time{}, v)
}
}
type ConvertErr struct {
// Value being converted
Value interface{}
// Type of attempted conversion
Type reflect.Type
extra string
}
func (e *ConvertErr) Error() string {
return fmt.Sprintf("cannot convert %#v to %s%s", e.Value, e.Type, e.extra)
}
func convertErr(target, v interface{}) *ConvertErr {
return &ConvertErr{Value: v, Type: reflect.TypeOf(target)}
}
func rangeErr(v interface{}) error {
e := convertErr(int32(0), v)
e.extra = ": out of range"
return e
}

19
vendor/github.com/kelseyhightower/envconfig/LICENSE generated vendored Normal file
View File

@ -0,0 +1,19 @@
Copyright (c) 2013 Kelsey Hightower
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

8
vendor/github.com/kelseyhightower/envconfig/doc.go generated vendored Normal file
View File

@ -0,0 +1,8 @@
// Copyright (c) 2013 Kelsey Hightower. All rights reserved.
// Use of this source code is governed by the MIT License that can be found in
// the LICENSE file.
// Package envconfig implements decoding of environment variables based on a user
// defined specification. A typical use is using environment variables for
// configuration settings.
package envconfig

View File

@ -0,0 +1,7 @@
// +build appengine go1.5
package envconfig
import "os"
var lookupEnv = os.LookupEnv

View File

@ -0,0 +1,7 @@
// +build !appengine,!go1.5
package envconfig
import "syscall"
var lookupEnv = syscall.Getenv

View File

@ -0,0 +1,382 @@
// Copyright (c) 2013 Kelsey Hightower. All rights reserved.
// Use of this source code is governed by the MIT License that can be found in
// the LICENSE file.
package envconfig
import (
"encoding"
"errors"
"fmt"
"os"
"reflect"
"regexp"
"strconv"
"strings"
"time"
)
// ErrInvalidSpecification indicates that a specification is of the wrong type.
var ErrInvalidSpecification = errors.New("specification must be a struct pointer")
var gatherRegexp = regexp.MustCompile("([^A-Z]+|[A-Z]+[^A-Z]+|[A-Z]+)")
var acronymRegexp = regexp.MustCompile("([A-Z]+)([A-Z][^A-Z]+)")
// A ParseError occurs when an environment variable cannot be converted to
// the type required by a struct field during assignment.
type ParseError struct {
KeyName string
FieldName string
TypeName string
Value string
Err error
}
// Decoder has the same semantics as Setter, but takes higher precedence.
// It is provided for historical compatibility.
type Decoder interface {
Decode(value string) error
}
// Setter is implemented by types can self-deserialize values.
// Any type that implements flag.Value also implements Setter.
type Setter interface {
Set(value string) error
}
func (e *ParseError) Error() string {
return fmt.Sprintf("envconfig.Process: assigning %[1]s to %[2]s: converting '%[3]s' to type %[4]s. details: %[5]s", e.KeyName, e.FieldName, e.Value, e.TypeName, e.Err)
}
// varInfo maintains information about the configuration variable
type varInfo struct {
Name string
Alt string
Key string
Field reflect.Value
Tags reflect.StructTag
}
// GatherInfo gathers information about the specified struct
func gatherInfo(prefix string, spec interface{}) ([]varInfo, error) {
s := reflect.ValueOf(spec)
if s.Kind() != reflect.Ptr {
return nil, ErrInvalidSpecification
}
s = s.Elem()
if s.Kind() != reflect.Struct {
return nil, ErrInvalidSpecification
}
typeOfSpec := s.Type()
// over allocate an info array, we will extend if needed later
infos := make([]varInfo, 0, s.NumField())
for i := 0; i < s.NumField(); i++ {
f := s.Field(i)
ftype := typeOfSpec.Field(i)
if !f.CanSet() || isTrue(ftype.Tag.Get("ignored")) {
continue
}
for f.Kind() == reflect.Ptr {
if f.IsNil() {
if f.Type().Elem().Kind() != reflect.Struct {
// nil pointer to a non-struct: leave it alone
break
}
// nil pointer to struct: create a zero instance
f.Set(reflect.New(f.Type().Elem()))
}
f = f.Elem()
}
// Capture information about the config variable
info := varInfo{
Name: ftype.Name,
Field: f,
Tags: ftype.Tag,
Alt: strings.ToUpper(ftype.Tag.Get("envconfig")),
}
// Default to the field name as the env var name (will be upcased)
info.Key = info.Name
// Best effort to un-pick camel casing as separate words
if isTrue(ftype.Tag.Get("split_words")) {
words := gatherRegexp.FindAllStringSubmatch(ftype.Name, -1)
if len(words) > 0 {
var name []string
for _, words := range words {
if m := acronymRegexp.FindStringSubmatch(words[0]); len(m) == 3 {
name = append(name, m[1], m[2])
} else {
name = append(name, words[0])
}
}
info.Key = strings.Join(name, "_")
}
}
if info.Alt != "" {
info.Key = info.Alt
}
if prefix != "" {
info.Key = fmt.Sprintf("%s_%s", prefix, info.Key)
}
info.Key = strings.ToUpper(info.Key)
infos = append(infos, info)
if f.Kind() == reflect.Struct {
// honor Decode if present
if decoderFrom(f) == nil && setterFrom(f) == nil && textUnmarshaler(f) == nil && binaryUnmarshaler(f) == nil {
innerPrefix := prefix
if !ftype.Anonymous {
innerPrefix = info.Key
}
embeddedPtr := f.Addr().Interface()
embeddedInfos, err := gatherInfo(innerPrefix, embeddedPtr)
if err != nil {
return nil, err
}
infos = append(infos[:len(infos)-1], embeddedInfos...)
continue
}
}
}
return infos, nil
}
// CheckDisallowed checks that no environment variables with the prefix are set
// that we don't know how or want to parse. This is likely only meaningful with
// a non-empty prefix.
func CheckDisallowed(prefix string, spec interface{}) error {
infos, err := gatherInfo(prefix, spec)
if err != nil {
return err
}
vars := make(map[string]struct{})
for _, info := range infos {
vars[info.Key] = struct{}{}
}
if prefix != "" {
prefix = strings.ToUpper(prefix) + "_"
}
for _, env := range os.Environ() {
if !strings.HasPrefix(env, prefix) {
continue
}
v := strings.SplitN(env, "=", 2)[0]
if _, found := vars[v]; !found {
return fmt.Errorf("unknown environment variable %s", v)
}
}
return nil
}
// Process populates the specified struct based on environment variables
func Process(prefix string, spec interface{}) error {
infos, err := gatherInfo(prefix, spec)
for _, info := range infos {
// `os.Getenv` cannot differentiate between an explicitly set empty value
// and an unset value. `os.LookupEnv` is preferred to `syscall.Getenv`,
// but it is only available in go1.5 or newer. We're using Go build tags
// here to use os.LookupEnv for >=go1.5
value, ok := lookupEnv(info.Key)
if !ok && info.Alt != "" {
value, ok = lookupEnv(info.Alt)
}
def := info.Tags.Get("default")
if def != "" && !ok {
value = def
}
req := info.Tags.Get("required")
if !ok && def == "" {
if isTrue(req) {
key := info.Key
if info.Alt != "" {
key = info.Alt
}
return fmt.Errorf("required key %s missing value", key)
}
continue
}
err = processField(value, info.Field)
if err != nil {
return &ParseError{
KeyName: info.Key,
FieldName: info.Name,
TypeName: info.Field.Type().String(),
Value: value,
Err: err,
}
}
}
return err
}
// MustProcess is the same as Process but panics if an error occurs
func MustProcess(prefix string, spec interface{}) {
if err := Process(prefix, spec); err != nil {
panic(err)
}
}
func processField(value string, field reflect.Value) error {
typ := field.Type()
decoder := decoderFrom(field)
if decoder != nil {
return decoder.Decode(value)
}
// look for Set method if Decode not defined
setter := setterFrom(field)
if setter != nil {
return setter.Set(value)
}
if t := textUnmarshaler(field); t != nil {
return t.UnmarshalText([]byte(value))
}
if b := binaryUnmarshaler(field); b != nil {
return b.UnmarshalBinary([]byte(value))
}
if typ.Kind() == reflect.Ptr {
typ = typ.Elem()
if field.IsNil() {
field.Set(reflect.New(typ))
}
field = field.Elem()
}
switch typ.Kind() {
case reflect.String:
field.SetString(value)
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
var (
val int64
err error
)
if field.Kind() == reflect.Int64 && typ.PkgPath() == "time" && typ.Name() == "Duration" {
var d time.Duration
d, err = time.ParseDuration(value)
val = int64(d)
} else {
val, err = strconv.ParseInt(value, 0, typ.Bits())
}
if err != nil {
return err
}
field.SetInt(val)
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
val, err := strconv.ParseUint(value, 0, typ.Bits())
if err != nil {
return err
}
field.SetUint(val)
case reflect.Bool:
val, err := strconv.ParseBool(value)
if err != nil {
return err
}
field.SetBool(val)
case reflect.Float32, reflect.Float64:
val, err := strconv.ParseFloat(value, typ.Bits())
if err != nil {
return err
}
field.SetFloat(val)
case reflect.Slice:
sl := reflect.MakeSlice(typ, 0, 0)
if typ.Elem().Kind() == reflect.Uint8 {
sl = reflect.ValueOf([]byte(value))
} else if len(strings.TrimSpace(value)) != 0 {
vals := strings.Split(value, ",")
sl = reflect.MakeSlice(typ, len(vals), len(vals))
for i, val := range vals {
err := processField(val, sl.Index(i))
if err != nil {
return err
}
}
}
field.Set(sl)
case reflect.Map:
mp := reflect.MakeMap(typ)
if len(strings.TrimSpace(value)) != 0 {
pairs := strings.Split(value, ",")
for _, pair := range pairs {
kvpair := strings.Split(pair, ":")
if len(kvpair) != 2 {
return fmt.Errorf("invalid map item: %q", pair)
}
k := reflect.New(typ.Key()).Elem()
err := processField(kvpair[0], k)
if err != nil {
return err
}
v := reflect.New(typ.Elem()).Elem()
err = processField(kvpair[1], v)
if err != nil {
return err
}
mp.SetMapIndex(k, v)
}
}
field.Set(mp)
}
return nil
}
func interfaceFrom(field reflect.Value, fn func(interface{}, *bool)) {
// it may be impossible for a struct field to fail this check
if !field.CanInterface() {
return
}
var ok bool
fn(field.Interface(), &ok)
if !ok && field.CanAddr() {
fn(field.Addr().Interface(), &ok)
}
}
func decoderFrom(field reflect.Value) (d Decoder) {
interfaceFrom(field, func(v interface{}, ok *bool) { d, *ok = v.(Decoder) })
return d
}
func setterFrom(field reflect.Value) (s Setter) {
interfaceFrom(field, func(v interface{}, ok *bool) { s, *ok = v.(Setter) })
return s
}
func textUnmarshaler(field reflect.Value) (t encoding.TextUnmarshaler) {
interfaceFrom(field, func(v interface{}, ok *bool) { t, *ok = v.(encoding.TextUnmarshaler) })
return t
}
func binaryUnmarshaler(field reflect.Value) (b encoding.BinaryUnmarshaler) {
interfaceFrom(field, func(v interface{}, ok *bool) { b, *ok = v.(encoding.BinaryUnmarshaler) })
return b
}
func isTrue(s string) bool {
b, _ := strconv.ParseBool(s)
return b
}

164
vendor/github.com/kelseyhightower/envconfig/usage.go generated vendored Normal file
View File

@ -0,0 +1,164 @@
// Copyright (c) 2016 Kelsey Hightower and others. All rights reserved.
// Use of this source code is governed by the MIT License that can be found in
// the LICENSE file.
package envconfig
import (
"encoding"
"fmt"
"io"
"os"
"reflect"
"strconv"
"strings"
"text/tabwriter"
"text/template"
)
const (
// DefaultListFormat constant to use to display usage in a list format
DefaultListFormat = `This application is configured via the environment. The following environment
variables can be used:
{{range .}}
{{usage_key .}}
[description] {{usage_description .}}
[type] {{usage_type .}}
[default] {{usage_default .}}
[required] {{usage_required .}}{{end}}
`
// DefaultTableFormat constant to use to display usage in a tabular format
DefaultTableFormat = `This application is configured via the environment. The following environment
variables can be used:
KEY TYPE DEFAULT REQUIRED DESCRIPTION
{{range .}}{{usage_key .}} {{usage_type .}} {{usage_default .}} {{usage_required .}} {{usage_description .}}
{{end}}`
)
var (
decoderType = reflect.TypeOf((*Decoder)(nil)).Elem()
setterType = reflect.TypeOf((*Setter)(nil)).Elem()
textUnmarshalerType = reflect.TypeOf((*encoding.TextUnmarshaler)(nil)).Elem()
binaryUnmarshalerType = reflect.TypeOf((*encoding.BinaryUnmarshaler)(nil)).Elem()
)
func implementsInterface(t reflect.Type) bool {
return t.Implements(decoderType) ||
reflect.PtrTo(t).Implements(decoderType) ||
t.Implements(setterType) ||
reflect.PtrTo(t).Implements(setterType) ||
t.Implements(textUnmarshalerType) ||
reflect.PtrTo(t).Implements(textUnmarshalerType) ||
t.Implements(binaryUnmarshalerType) ||
reflect.PtrTo(t).Implements(binaryUnmarshalerType)
}
// toTypeDescription converts Go types into a human readable description
func toTypeDescription(t reflect.Type) string {
switch t.Kind() {
case reflect.Array, reflect.Slice:
if t.Elem().Kind() == reflect.Uint8 {
return "String"
}
return fmt.Sprintf("Comma-separated list of %s", toTypeDescription(t.Elem()))
case reflect.Map:
return fmt.Sprintf(
"Comma-separated list of %s:%s pairs",
toTypeDescription(t.Key()),
toTypeDescription(t.Elem()),
)
case reflect.Ptr:
return toTypeDescription(t.Elem())
case reflect.Struct:
if implementsInterface(t) && t.Name() != "" {
return t.Name()
}
return ""
case reflect.String:
name := t.Name()
if name != "" && name != "string" {
return name
}
return "String"
case reflect.Bool:
name := t.Name()
if name != "" && name != "bool" {
return name
}
return "True or False"
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
name := t.Name()
if name != "" && !strings.HasPrefix(name, "int") {
return name
}
return "Integer"
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
name := t.Name()
if name != "" && !strings.HasPrefix(name, "uint") {
return name
}
return "Unsigned Integer"
case reflect.Float32, reflect.Float64:
name := t.Name()
if name != "" && !strings.HasPrefix(name, "float") {
return name
}
return "Float"
}
return fmt.Sprintf("%+v", t)
}
// Usage writes usage information to stdout using the default header and table format
func Usage(prefix string, spec interface{}) error {
// The default is to output the usage information as a table
// Create tabwriter instance to support table output
tabs := tabwriter.NewWriter(os.Stdout, 1, 0, 4, ' ', 0)
err := Usagef(prefix, spec, tabs, DefaultTableFormat)
tabs.Flush()
return err
}
// Usagef writes usage information to the specified io.Writer using the specifed template specification
func Usagef(prefix string, spec interface{}, out io.Writer, format string) error {
// Specify the default usage template functions
functions := template.FuncMap{
"usage_key": func(v varInfo) string { return v.Key },
"usage_description": func(v varInfo) string { return v.Tags.Get("desc") },
"usage_type": func(v varInfo) string { return toTypeDescription(v.Field.Type()) },
"usage_default": func(v varInfo) string { return v.Tags.Get("default") },
"usage_required": func(v varInfo) (string, error) {
req := v.Tags.Get("required")
if req != "" {
reqB, err := strconv.ParseBool(req)
if err != nil {
return "", err
}
if reqB {
req = "true"
}
}
return req, nil
},
}
tmpl, err := template.New("envconfig").Funcs(functions).Parse(format)
if err != nil {
return err
}
return Usaget(prefix, spec, out, tmpl)
}
// Usaget writes usage information to the specified io.Writer using the specified template
func Usaget(prefix string, spec interface{}, out io.Writer, tmpl *template.Template) error {
// gather first
infos, err := gatherInfo(prefix, spec)
if err != nil {
return err
}
return tmpl.Execute(out, infos)
}