diff --git a/v2/binding/spec/match_exact_version.go b/v2/binding/spec/match_exact_version.go new file mode 100644 index 000000000..5976faf12 --- /dev/null +++ b/v2/binding/spec/match_exact_version.go @@ -0,0 +1,76 @@ +package spec + +import ( + "github.com/cloudevents/sdk-go/v2/event" +) + +type matchExactVersion struct { + version +} + +func (v *matchExactVersion) Attribute(name string) Attribute { return v.attrMap[name] } + +var _ Version = (*matchExactVersion)(nil) + +func newMatchExactVersionVersion( + prefix string, + attributeNameMatchMapper func(string) string, + context event.EventContext, + convert func(event.EventContextConverter) event.EventContext, + attrs ...*attribute, +) *matchExactVersion { + v := &matchExactVersion{ + version: version{ + prefix: prefix, + context: context, + convert: convert, + attrMap: map[string]Attribute{}, + attrs: make([]Attribute, len(attrs)), + }, + } + for i, a := range attrs { + a.version = v + v.attrs[i] = a + v.attrMap[attributeNameMatchMapper(a.name)] = a + } + return v +} + +// WithPrefixMatchExact returns a set of versions with prefix added to all attribute names. +func WithPrefixMatchExact(attributeNameMatchMapper func(string) string, prefix string) *Versions { + attr := func(name string, kind Kind) *attribute { + return &attribute{accessor: acc[kind], name: name} + } + vs := &Versions{ + m: map[string]Version{}, + prefix: prefix, + all: []Version{ + newMatchExactVersionVersion(prefix, attributeNameMatchMapper, event.EventContextV1{}.AsV1(), + func(c event.EventContextConverter) event.EventContext { return c.AsV1() }, + attr("id", ID), + attr("source", Source), + attr("specversion", SpecVersion), + attr("type", Type), + attr("datacontenttype", DataContentType), + attr("dataschema", DataSchema), + attr("subject", Subject), + attr("time", Time), + ), + newMatchExactVersionVersion(prefix, attributeNameMatchMapper, event.EventContextV03{}.AsV03(), + func(c event.EventContextConverter) event.EventContext { return c.AsV03() }, + attr("specversion", SpecVersion), + attr("type", Type), + attr("source", Source), + attr("schemaurl", DataSchema), + attr("subject", Subject), + attr("id", ID), + attr("time", Time), + attr("datacontenttype", DataContentType), + ), + }, + } + for _, v := range vs.all { + vs.m[v.String()] = v + } + return vs +} diff --git a/v2/binding/spec/match_exact_version_test.go b/v2/binding/spec/match_exact_version_test.go new file mode 100644 index 000000000..3cd8dc282 --- /dev/null +++ b/v2/binding/spec/match_exact_version_test.go @@ -0,0 +1,36 @@ +package spec_test + +import ( + "net/textproto" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/cloudevents/sdk-go/v2/binding/spec" + "github.com/cloudevents/sdk-go/v2/binding/test" + "github.com/cloudevents/sdk-go/v2/event" +) + +func TestMatchExactVersion(t *testing.T) { + test.EachEvent(t, test.AllVersions([]event.Event{test.FullEvent()}), func(t *testing.T, e event.Event) { + e = e.Clone() + s := spec.WithPrefixMatchExact( + func(s string) string { + if s == "datacontenttype" { + return "Content-Type" + } else { + return textproto.CanonicalMIMEHeaderKey("Ce-" + s) + } + }, + "Ce-", + ) + sv := s.Version(e.SpecVersion()) + require.NotNil(t, sv) + + require.Equal(t, e.ID(), sv.Attribute("Ce-Id").Get(e.Context)) + require.Equal(t, "id", sv.Attribute("Ce-Id").Name()) + + require.Equal(t, e.DataContentType(), sv.Attribute("Content-Type").Get(e.Context)) + require.Equal(t, "datacontenttype", sv.Attribute("Content-Type").Name()) + }) +} diff --git a/v2/protocol/http/headers.go b/v2/protocol/http/headers.go new file mode 100644 index 000000000..7479290e9 --- /dev/null +++ b/v2/protocol/http/headers.go @@ -0,0 +1,22 @@ +package http + +import ( + "net/textproto" + + "github.com/cloudevents/sdk-go/v2/binding/spec" +) + +var attributeHeadersMapping map[string]string + +func init() { + attributeHeadersMapping = make(map[string]string) + for _, v := range specs.Versions() { + for _, a := range v.Attributes() { + if a.Kind() == spec.DataContentType { + attributeHeadersMapping[a.Name()] = ContentType + } else { + attributeHeadersMapping[a.Name()] = textproto.CanonicalMIMEHeaderKey(prefix + a.Name()) + } + } + } +} diff --git a/v2/protocol/http/message.go b/v2/protocol/http/message.go index ecf57a64a..46d0712c5 100644 --- a/v2/protocol/http/message.go +++ b/v2/protocol/http/message.go @@ -4,7 +4,9 @@ import ( "context" "io" nethttp "net/http" + "net/textproto" "strings" + "unicode" "github.com/cloudevents/sdk-go/v2/binding" "github.com/cloudevents/sdk-go/v2/binding/format" @@ -13,7 +15,16 @@ import ( const prefix = "Ce-" -var specs = spec.WithPrefix(prefix) +var specs = spec.WithPrefixMatchExact( + func(s string) string { + if s == "datacontenttype" { + return "Content-Type" + } else { + return textproto.CanonicalMIMEHeaderKey("Ce-" + s) + } + }, + "Ce-", +) const ContentType = "Content-Type" const ContentLength = "Content-Length" @@ -94,15 +105,16 @@ func (m *Message) ReadBinary(ctx context.Context, encoder binding.BinaryWriter) } for k, v := range m.Header { - if strings.HasPrefix(k, prefix) { - attr := m.version.Attribute(k) - if attr != nil { - err = encoder.SetAttribute(attr, v[0]) - } else { - err = encoder.SetExtension(strings.ToLower(strings.TrimPrefix(k, prefix)), v[0]) - } - } else if k == ContentType { - err = encoder.SetAttribute(m.version.AttributeFromKind(spec.DataContentType), v[0]) + attr := m.version.Attribute(k) + if attr != nil { + err = encoder.SetAttribute(attr, v[0]) + } else if strings.HasPrefix(k, prefix) { + // Trim Prefix + To lower + var b strings.Builder + b.Grow(len(k) - len(prefix)) + b.WriteRune(unicode.ToLower(rune(k[len(prefix)]))) + b.WriteString(k[len(prefix)+1:]) + err = encoder.SetExtension(b.String(), v[0]) } if err != nil { return err diff --git a/v2/protocol/http/write_request.go b/v2/protocol/http/write_request.go index 692180791..50ffdfb6d 100644 --- a/v2/protocol/http/write_request.go +++ b/v2/protocol/http/write_request.go @@ -109,11 +109,8 @@ func (b *httpRequestWriter) SetAttribute(attribute spec.Attribute, value interfa return err } - if attribute.Kind() == spec.DataContentType { - b.Header.Add(ContentType, s) - } else { - b.Header.Add(prefix+attribute.Name(), s) - } + mapping := attributeHeadersMapping[attribute.Name()] + b.Header[mapping] = append(b.Header[mapping], s) return nil } diff --git a/v2/protocol/http/write_responsewriter.go b/v2/protocol/http/write_responsewriter.go index 0427a8544..830ff0fa5 100644 --- a/v2/protocol/http/write_responsewriter.go +++ b/v2/protocol/http/write_responsewriter.go @@ -55,11 +55,8 @@ func (b *httpResponseWriter) SetAttribute(attribute spec.Attribute, value interf return err } - if attribute.Kind() == spec.DataContentType { - b.rw.Header().Add(ContentType, s) - } else { - b.rw.Header().Add(prefix+attribute.Name(), s) - } + mapping := attributeHeadersMapping[attribute.Name()] + b.rw.Header()[mapping] = append(b.rw.Header()[mapping], s) return nil }