Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .github/FUNDING.yml

This file was deleted.

2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# httpsfv: Structured Field Values for HTTP in Go

This [Go (golang)](https://golang.org) library implements parsing and serialization for [Structured Field Values for HTTP (RFC 8941)](https://httpwg.org/specs/rfc8941.html).
This [Go (golang)](https://golang.org) library implements parsing and serialization for [Structured Field Values for HTTP (RFC 9651 and 8941)](https://httpwg.org/specs/rfc9651.html).

[![PkgGoDev](https://pkg.go.dev/badge/github.com/dunglas/httpsfv)](https://pkg.go.dev/github.com/dunglas/httpsfv)
![CI](https://github.com/dunglas/httpsfv/workflows/CI/badge.svg)
Expand Down
23 changes: 18 additions & 5 deletions bareitem.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,16 @@ import (
"fmt"
"reflect"
"strings"
"time"
)

// ErrInvalidBareItem is returned when a bare item is invalid.
var ErrInvalidBareItem = errors.New(
"invalid bare item type (allowed types are bool, string, int64, float64, []byte and Token)",
"invalid bare item type (allowed types are bool, string, int64, float64, []byte, time.Time and Token)",
)

// assertBareItem asserts that v is a valid bare item
// according to https://httpwg.org/specs/rfc8941.html#item.
// according to https://httpwg.org/specs/rfc9651.html#item.
//
// v can be either:
//
Expand All @@ -23,6 +24,8 @@ var ErrInvalidBareItem = errors.New(
// * a token (Section 3.3.4.)
// * a byte sequence (Section 3.3.5.)
// * a boolean (Section 3.3.6.)
// * a date (Section 3.3.7.)
// * a display string (Section 3.3.8.)
func assertBareItem(v interface{}) {
switch v.(type) {
case bool,
Expand All @@ -40,15 +43,17 @@ func assertBareItem(v interface{}) {
float32,
float64,
[]byte,
Token:
time.Time,
Token,
DisplayString:
return
default:
panic(fmt.Errorf("%w: got %s", ErrInvalidBareItem, reflect.TypeOf(v)))
}
}

// marshalBareItem serializes as defined in
// https://httpwg.org/specs/rfc8941.html#ser-bare-item.
// https://httpwg.org/specs/rfc9651.html#ser-bare-item.
func marshalBareItem(b *strings.Builder, v interface{}) error {
switch v := v.(type) {
case bool:
Expand All @@ -66,15 +71,19 @@ func marshalBareItem(b *strings.Builder, v interface{}) error {
return marshalDecimal(b, v.(float64))
case []byte:
return marshalBinary(b, v)
case time.Time:
return marshalDate(b, v)
case Token:
return v.marshalSFV(b)
case DisplayString:
return v.marshalSFV(b)
default:
panic(ErrInvalidBareItem)
}
}

// parseBareItem parses as defined in
// https://httpwg.org/specs/rfc8941.html#parse-bare-item.
// https://httpwg.org/specs/rfc9651.html#parse-bare-item.
func parseBareItem(s *scanner) (interface{}, error) {
if s.eof() {
return nil, &UnmarshalError{s.off, ErrUnexpectedEndOfString}
Expand All @@ -92,6 +101,10 @@ func parseBareItem(s *scanner) (interface{}, error) {
return parseBinary(s)
case '-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
return parseNumber(s)
case '@':
return parseDate(s)
case '%':
return parseDisplayString(s)
default:
if isAlpha(c) {
return parseToken(s)
Expand Down
1 change: 1 addition & 0 deletions bareitem_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ func TestParseBareItem(t *testing.T) {
{"abc", Token("abc"), false},
{"*abc", Token("*abc"), false},
{":YWJj:", []byte("abc"), false},
{"@1659578233", time.Unix(1659578233, 0), false},
{"", nil, true},
{"~", nil, true},
}
Expand Down
4 changes: 2 additions & 2 deletions binary.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (
var ErrInvalidBinaryFormat = errors.New("invalid binary format")

// marshalBinary serializes as defined in
// https://httpwg.org/specs/rfc8941.html#ser-binary.
// https://httpwg.org/specs/rfc9651.html#ser-binary.
func marshalBinary(b *strings.Builder, bs []byte) error {
if err := b.WriteByte(':'); err != nil {
return err
Expand All @@ -27,7 +27,7 @@ func marshalBinary(b *strings.Builder, bs []byte) error {
}

// parseBinary parses as defined in
// https://httpwg.org/specs/rfc8941.html#parse-binary.
// https://httpwg.org/specs/rfc9651.html#parse-binary.
func parseBinary(s *scanner) ([]byte, error) {
if s.eof() || s.data[s.off] != ':' {
return nil, &UnmarshalError{s.off, ErrInvalidBinaryFormat}
Expand Down
2 changes: 1 addition & 1 deletion binary_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import (
"testing"
)

func TestBinary(t *testing.T) {
func TestMarshalBinary(t *testing.T) {
t.Parallel()

var bd strings.Builder
Expand Down
4 changes: 2 additions & 2 deletions boolean.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
var ErrInvalidBooleanFormat = errors.New("invalid boolean format")

// marshalBoolean serializes as defined in
// https://httpwg.org/specs/rfc8941.html#ser-boolean.
// https://httpwg.org/specs/rfc9651.html#ser-boolean.
func marshalBoolean(bd io.StringWriter, b bool) error {
if b {
_, err := bd.WriteString("?1")
Expand All @@ -23,7 +23,7 @@ func marshalBoolean(bd io.StringWriter, b bool) error {
}

// parseBoolean parses as defined in
// https://httpwg.org/specs/rfc8941.html#parse-boolean.
// https://httpwg.org/specs/rfc9651.html#parse-boolean.
func parseBoolean(s *scanner) (bool, error) {
if s.eof() || s.data[s.off] != '?' {
return false, &UnmarshalError{s.off, ErrInvalidBooleanFormat}
Expand Down
2 changes: 1 addition & 1 deletion boolean_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import (
"testing"
)

func TestBooleanMarshalSFV(t *testing.T) {
func TestMarshalBoolean(t *testing.T) {
t.Parallel()

var b strings.Builder
Expand Down
41 changes: 41 additions & 0 deletions date.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package httpsfv

import (
"errors"
"io"
"time"
)

var ErrInvalidDateFormat = errors.New("invalid date format")

// marshalDate serializes as defined in
// https://httpwg.org/specs/rfc9651.html#ser-date.
func marshalDate(b io.StringWriter, i time.Time) error {
_, err := b.WriteString("@")
if err != nil {
return err
}

return marshalInteger(b, i.Unix())
}

// parseDate parses as defined in
// https://httpwg.org/specs/rfc9651.html#parse-date.
func parseDate(s *scanner) (time.Time, error) {
if s.eof() || s.data[s.off] != '@' {
return time.Time{}, &UnmarshalError{s.off, ErrInvalidDateFormat}
}
s.off++

n, err := parseNumber(s)
if err != nil {
return time.Time{}, &UnmarshalError{s.off, ErrInvalidDateFormat}
}

i, ok := n.(int64)
if !ok {
return time.Time{}, &UnmarshalError{s.off, ErrInvalidDateFormat}
}

return time.Unix(i, 0), nil
}
63 changes: 63 additions & 0 deletions date_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package httpsfv

import (
"strings"
"testing"
"time"
)

func TestMarshalDate(t *testing.T) {
t.Parallel()

data := []struct {
in time.Time
expected string
valid bool
}{
{time.Unix(1659578233, 0), "@1659578233", true},
{time.Unix(9999999999999999, 0), "@", false},
}

var b strings.Builder

for _, d := range data {
b.Reset()

err := marshalDate(&b, d.in)
if d.valid && err != nil {
t.Errorf("error not expected for %v, got %v", d.in, err)
} else if !d.valid && err == nil {
t.Errorf("error expected for %v, got %v", d.in, err)
}

if b.String() != d.expected {
t.Errorf("got %v; want %v", b.String(), d.expected)
}
}
}

func TestParseDate(t *testing.T) {
t.Parallel()

data := []struct {
in string
out time.Time
err bool
}{
{"@1659578233", time.Unix(1659578233, 0), false},
{"invalid", time.Time{}, true},
}

for _, d := range data {
s := &scanner{data: d.in}

i, err := parseDate(s)
if d.err && err == nil {
t.Errorf("parse%s): error expected", d.in)
}

if !d.err && d.out != i {
t.Errorf("parse%s) = %v, %v; %v, <nil> expected", d.in, i, err, d.out)
}
}
}
2 changes: 1 addition & 1 deletion decimal.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const maxDecDigit = 3
var ErrInvalidDecimal = errors.New("the integer portion is larger than 12 digits: invalid decimal")

// marshalDecimal serializes as defined in
// https://httpwg.org/specs/rfc8941.html#ser-decimal.
// https://httpwg.org/specs/rfc9651.html#ser-decimal.
//
// TODO(dunglas): add support for decimal float type when one will be available
// (https://github.com/golang/go/issues/19787)
Expand Down
2 changes: 1 addition & 1 deletion decimal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import (
"testing"
)

func TestDecimalMarshalSFV(t *testing.T) {
func TestMarshalDecimal(t *testing.T) {
t.Parallel()

data := []struct {
Expand Down
4 changes: 2 additions & 2 deletions dictionary.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import (
)

// Dictionary is an ordered map of name-value pairs.
// See https://httpwg.org/specs/rfc8941.html#dictionary
// See https://httpwg.org/specs/rfc9651.html#dictionary
// Values can be:
// * Item (Section 3.3.)
// * Inner List (Section 3.1.1.)
Expand Down Expand Up @@ -101,7 +101,7 @@ func (d *Dictionary) marshalSFV(b *strings.Builder) error {
}

// UnmarshalDictionary parses a dictionary as defined in
// https://httpwg.org/specs/rfc8941.html#parse-dictionary.
// https://httpwg.org/specs/rfc9651.html#parse-dictionary.
func UnmarshalDictionary(v []string) (*Dictionary, error) {
s := &scanner{
data: strings.Join(v, ","),
Expand Down
2 changes: 1 addition & 1 deletion dictionary_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import (
"testing"
)

func TestDictionnary(t *testing.T) {
func TestMarshalDictionnary(t *testing.T) {
t.Parallel()

dict := NewDictionary()
Expand Down
Loading
Loading