Skip to content

Commit d65fd05

Browse files
committed
feat: support for RFC 9651
1 parent 0bbc76e commit d65fd05

35 files changed

+402
-99
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# httpsfv: Structured Field Values for HTTP in Go
22

3-
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).
3+
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).
44

55
[![PkgGoDev](https://pkg.go.dev/badge/github.com/dunglas/httpsfv)](https://pkg.go.dev/github.com/dunglas/httpsfv)
66
![CI](https://github.com/dunglas/httpsfv/workflows/CI/badge.svg)

bareitem.go

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,16 @@ import (
55
"fmt"
66
"reflect"
77
"strings"
8+
"time"
89
)
910

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

1516
// assertBareItem asserts that v is a valid bare item
16-
// according to https://httpwg.org/specs/rfc8941.html#item.
17+
// according to https://httpwg.org/specs/rfc9651.html#item.
1718
//
1819
// v can be either:
1920
//
@@ -23,6 +24,7 @@ var ErrInvalidBareItem = errors.New(
2324
// * a token (Section 3.3.4.)
2425
// * a byte sequence (Section 3.3.5.)
2526
// * a boolean (Section 3.3.6.)
27+
// * a date (Section 3.3.7.)
2628
func assertBareItem(v interface{}) {
2729
switch v.(type) {
2830
case bool,
@@ -40,15 +42,17 @@ func assertBareItem(v interface{}) {
4042
float32,
4143
float64,
4244
[]byte,
43-
Token:
45+
time.Time,
46+
Token,
47+
DisplayString:
4448
return
4549
default:
4650
panic(fmt.Errorf("%w: got %s", ErrInvalidBareItem, reflect.TypeOf(v)))
4751
}
4852
}
4953

5054
// marshalBareItem serializes as defined in
51-
// https://httpwg.org/specs/rfc8941.html#ser-bare-item.
55+
// https://httpwg.org/specs/rfc9651.html#ser-bare-item.
5256
func marshalBareItem(b *strings.Builder, v interface{}) error {
5357
switch v := v.(type) {
5458
case bool:
@@ -66,15 +70,19 @@ func marshalBareItem(b *strings.Builder, v interface{}) error {
6670
return marshalDecimal(b, v.(float64))
6771
case []byte:
6872
return marshalBinary(b, v)
73+
case time.Time:
74+
return marshalDate(b, v)
6975
case Token:
7076
return v.marshalSFV(b)
77+
case DisplayString:
78+
return v.marshalSFV(b)
7179
default:
7280
panic(ErrInvalidBareItem)
7381
}
7482
}
7583

7684
// parseBareItem parses as defined in
77-
// https://httpwg.org/specs/rfc8941.html#parse-bare-item.
85+
// https://httpwg.org/specs/rfc9651.html#parse-bare-item.
7886
func parseBareItem(s *scanner) (interface{}, error) {
7987
if s.eof() {
8088
return nil, &UnmarshalError{s.off, ErrUnexpectedEndOfString}
@@ -92,6 +100,10 @@ func parseBareItem(s *scanner) (interface{}, error) {
92100
return parseBinary(s)
93101
case '-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
94102
return parseNumber(s)
103+
case '@':
104+
return parseDate(s)
105+
case '%':
106+
return parseDisplayString(s)
95107
default:
96108
if isAlpha(c) {
97109
return parseToken(s)

bareitem_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ func TestParseBareItem(t *testing.T) {
2323
{"abc", Token("abc"), false},
2424
{"*abc", Token("*abc"), false},
2525
{":YWJj:", []byte("abc"), false},
26+
{"@1659578233", time.Unix(1659578233, 0), false},
2627
{"", nil, true},
2728
{"~", nil, true},
2829
}

binary.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import (
1010
var ErrInvalidBinaryFormat = errors.New("invalid binary format")
1111

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

2929
// parseBinary parses as defined in
30-
// https://httpwg.org/specs/rfc8941.html#parse-binary.
30+
// https://httpwg.org/specs/rfc9651.html#parse-binary.
3131
func parseBinary(s *scanner) ([]byte, error) {
3232
if s.eof() || s.data[s.off] != ':' {
3333
return nil, &UnmarshalError{s.off, ErrInvalidBinaryFormat}

binary_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import (
66
"testing"
77
)
88

9-
func TestBinary(t *testing.T) {
9+
func TestMarshalBinary(t *testing.T) {
1010
t.Parallel()
1111

1212
var bd strings.Builder

boolean.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import (
99
var ErrInvalidBooleanFormat = errors.New("invalid boolean format")
1010

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

2525
// parseBoolean parses as defined in
26-
// https://httpwg.org/specs/rfc8941.html#parse-boolean.
26+
// https://httpwg.org/specs/rfc9651.html#parse-boolean.
2727
func parseBoolean(s *scanner) (bool, error) {
2828
if s.eof() || s.data[s.off] != '?' {
2929
return false, &UnmarshalError{s.off, ErrInvalidBooleanFormat}

boolean_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import (
55
"testing"
66
)
77

8-
func TestBooleanMarshalSFV(t *testing.T) {
8+
func TestMarshalBoolean(t *testing.T) {
99
t.Parallel()
1010

1111
var b strings.Builder

date.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package httpsfv
2+
3+
import (
4+
"errors"
5+
"io"
6+
"time"
7+
)
8+
9+
var ErrInvalidDateFormat = errors.New("invalid date format")
10+
11+
// marshalDate serializes as defined in
12+
// https://httpwg.org/specs/rfc9651.html#ser-date.
13+
func marshalDate(b io.StringWriter, i time.Time) error {
14+
_, err := b.WriteString("@")
15+
if err != nil {
16+
return err
17+
}
18+
19+
return marshalInteger(b, i.Unix())
20+
}
21+
22+
// parseDate parses as defined in
23+
// https://httpwg.org/specs/rfc9651.html#parse-date.
24+
func parseDate(s *scanner) (time.Time, error) {
25+
if s.eof() || s.data[s.off] != '@' {
26+
return time.Time{}, &UnmarshalError{s.off, ErrInvalidDateFormat}
27+
}
28+
s.off++
29+
30+
n, err := parseNumber(s)
31+
if err != nil {
32+
return time.Time{}, &UnmarshalError{s.off, ErrInvalidDateFormat}
33+
}
34+
35+
i, ok := n.(int64)
36+
if !ok {
37+
return time.Time{}, &UnmarshalError{s.off, ErrInvalidDateFormat}
38+
}
39+
40+
return time.Unix(i, 0), nil
41+
}

date_test.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package httpsfv
2+
3+
import (
4+
"strings"
5+
"testing"
6+
"time"
7+
)
8+
9+
func TestMarshalDate(t *testing.T) {
10+
t.Parallel()
11+
12+
data := []struct {
13+
in time.Time
14+
expected string
15+
valid bool
16+
}{
17+
{time.Unix(1659578233, 0), "@1659578233", true},
18+
{time.Unix(9999999999999999, 0), "@", false},
19+
}
20+
21+
var b strings.Builder
22+
23+
for _, d := range data {
24+
b.Reset()
25+
26+
err := marshalDate(&b, d.in)
27+
if d.valid && err != nil {
28+
t.Errorf("error not expected for %v, got %v", d.in, err)
29+
} else if !d.valid && err == nil {
30+
t.Errorf("error expected for %v, got %v", d.in, err)
31+
}
32+
33+
if b.String() != d.expected {
34+
t.Errorf("got %v; want %v", b.String(), d.expected)
35+
}
36+
}
37+
}
38+
39+
func TestParseDate(t *testing.T) {
40+
t.Parallel()
41+
42+
data := []struct {
43+
in string
44+
out time.Time
45+
err bool
46+
}{
47+
{"@1659578233", time.Unix(1659578233, 0), false},
48+
{"invalid", time.Time{}, true},
49+
}
50+
51+
for _, d := range data {
52+
s := &scanner{data: d.in}
53+
54+
i, err := parseDate(s)
55+
if d.err && err == nil {
56+
t.Errorf("parse%s): error expected", d.in)
57+
}
58+
59+
if !d.err && d.out != i {
60+
t.Errorf("parse%s) = %v, %v; %v, <nil> expected", d.in, i, err, d.out)
61+
}
62+
}
63+
}

decimal.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ const maxDecDigit = 3
1414
var ErrInvalidDecimal = errors.New("the integer portion is larger than 12 digits: invalid decimal")
1515

1616
// marshalDecimal serializes as defined in
17-
// https://httpwg.org/specs/rfc8941.html#ser-decimal.
17+
// https://httpwg.org/specs/rfc9651.html#ser-decimal.
1818
//
1919
// TODO(dunglas): add support for decimal float type when one will be available
2020
// (https://github.com/golang/go/issues/19787)

0 commit comments

Comments
 (0)