Skip to content

Commit d0bd127

Browse files
authored
Add bearertoken, datetime, rid, safelong, and uuid packages (#132)
1 parent 9b18547 commit d0bd127

17 files changed

Lines changed: 1031 additions & 0 deletions

bearertoken/bearertoken.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// Copyright (c) 2018 Palantir Technologies. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package bearertoken
6+
7+
// Token represents a bearer token, generally sent by a REST client in a
8+
// Authorization or Cookie header for authentication purposes.
9+
type Token string

datetime/datetime.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
// Copyright (c) 2018 Palantir Technologies. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package datetime
6+
7+
import (
8+
"strings"
9+
"time"
10+
)
11+
12+
// DateTime is an alias for time.Time which implements serialization matching the
13+
// conjure wire specification at https://github.com/palantir/conjure/blob/master/docs/spec/wire.md
14+
type DateTime time.Time
15+
16+
func (d DateTime) String() string {
17+
return time.Time(d).Format(time.RFC3339Nano)
18+
}
19+
20+
// MarshalText implements encoding.TextMarshaler (used by encoding/json and others).
21+
func (d DateTime) MarshalText() ([]byte, error) {
22+
return []byte(d.String()), nil
23+
}
24+
25+
// UnmarshalText implements encoding.TextUnmarshaler (used by encoding/json and others).
26+
func (d *DateTime) UnmarshalText(b []byte) error {
27+
t, err := ParseDateTime(string(b))
28+
if err != nil {
29+
return err
30+
}
31+
*d = t
32+
return nil
33+
}
34+
35+
// ParseDateTime parses a DateTime from a string. Conjure supports DateTime inputs that end with an optional
36+
// zone identifier enclosed in square brackets (for example, "2017-01-02T04:04:05.000000000+01:00[Europe/Berlin]").
37+
func ParseDateTime(s string) (DateTime, error) {
38+
// If the input string ends in a ']' and contains a '[', parse the string up to '['.
39+
if strings.HasSuffix(s, "]") {
40+
if openBracketIdx := strings.LastIndex(s, "["); openBracketIdx != -1 {
41+
s = s[:openBracketIdx]
42+
}
43+
}
44+
timeVal, err := time.Parse(time.RFC3339Nano, s)
45+
if err != nil {
46+
return DateTime(time.Time{}), err
47+
}
48+
return DateTime(timeVal), nil
49+
}

datetime/datetime_test.go

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
// Copyright (c) 2018 Palantir Technologies. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package datetime_test
6+
7+
import (
8+
"encoding/json"
9+
"testing"
10+
"time"
11+
12+
"github.com/stretchr/testify/assert"
13+
"github.com/stretchr/testify/require"
14+
15+
"github.com/palantir/pkg/datetime"
16+
)
17+
18+
var dateTimeJSONs = []struct {
19+
sec int64
20+
zoneOffset int
21+
str string
22+
json string
23+
}{
24+
{
25+
sec: 1483326245,
26+
str: `2017-01-02T03:04:05Z`,
27+
json: `"2017-01-02T03:04:05Z"`,
28+
},
29+
{
30+
sec: 1483326245,
31+
str: `2017-01-02T03:04:05Z`,
32+
json: `"2017-01-02T03:04:05.000Z"`,
33+
},
34+
{
35+
sec: 1483326245,
36+
str: `2017-01-02T03:04:05Z`,
37+
json: `"2017-01-02T03:04:05.000000000Z"`,
38+
},
39+
{
40+
sec: 1483326245,
41+
zoneOffset: 3600,
42+
str: `2017-01-02T04:04:05+01:00`,
43+
json: `"2017-01-02T04:04:05.000000000+01:00"`,
44+
},
45+
{
46+
sec: 1483326245,
47+
zoneOffset: 7200,
48+
str: `2017-01-02T05:04:05+02:00`,
49+
json: `"2017-01-02T05:04:05.000000000+02:00"`,
50+
},
51+
{
52+
sec: 1483326245,
53+
zoneOffset: 3600,
54+
str: `2017-01-02T04:04:05+01:00`,
55+
json: `"2017-01-02T04:04:05.000000000+01:00[Europe/Berlin]"`,
56+
},
57+
}
58+
59+
func TestDateTimeString(t *testing.T) {
60+
for i, currCase := range dateTimeJSONs {
61+
currDateTime := datetime.DateTime(time.Unix(currCase.sec, 0).In(time.FixedZone("", currCase.zoneOffset)))
62+
assert.Equal(t, currCase.str, currDateTime.String(), "Case %d", i)
63+
}
64+
}
65+
66+
func TestDateTimeMarshal(t *testing.T) {
67+
for i, currCase := range dateTimeJSONs {
68+
currDateTime := datetime.DateTime(time.Unix(currCase.sec, 0).In(time.FixedZone("", currCase.zoneOffset)))
69+
bytes, err := json.Marshal(currDateTime)
70+
require.NoError(t, err, "Case %d: marshal %q", i, currDateTime.String())
71+
72+
var unmarshaledFromMarshal datetime.DateTime
73+
err = json.Unmarshal(bytes, &unmarshaledFromMarshal)
74+
require.NoError(t, err, "Case %d: unmarshal %q", i, string(bytes))
75+
76+
var unmarshaledFromCase datetime.DateTime
77+
err = json.Unmarshal([]byte(currCase.json), &unmarshaledFromCase)
78+
require.NoError(t, err, "Case %d: unmarshal %q", i, currCase.json)
79+
80+
assert.Equal(t, unmarshaledFromCase, unmarshaledFromMarshal, "Case %d", i)
81+
}
82+
}
83+
84+
func TestDateTimeUnmarshal(t *testing.T) {
85+
for i, currCase := range dateTimeJSONs {
86+
wantDateTime := time.Unix(currCase.sec, 0).UTC()
87+
if currCase.zoneOffset != 0 {
88+
wantDateTime = wantDateTime.In(time.FixedZone("", currCase.zoneOffset))
89+
}
90+
91+
var gotDateTime datetime.DateTime
92+
err := json.Unmarshal([]byte(currCase.json), &gotDateTime)
93+
require.NoError(t, err, "Case %d", i)
94+
95+
assert.Equal(t, wantDateTime, time.Time(gotDateTime), "Case %d", i)
96+
}
97+
}
98+
99+
func TestDateTimeUnmarshalInvalid(t *testing.T) {
100+
for i, currCase := range []struct {
101+
input string
102+
wantErr string
103+
}{
104+
{
105+
input: `"foo"`,
106+
wantErr: "parsing time \"foo\" as \"2006-01-02T15:04:05.999999999Z07:00\": cannot parse \"foo\" as \"2006\"",
107+
},
108+
{
109+
input: `"2017-01-02T04:04:05.000000000+01:00[Europe/Berlin"`,
110+
wantErr: "parsing time \"2017-01-02T04:04:05.000000000+01:00[Europe/Berlin\": extra text: [Europe/Berlin",
111+
},
112+
{
113+
input: `"2017-01-02T04:04:05.000000000+01:00[[Europe/Berlin]]"`,
114+
wantErr: "parsing time \"2017-01-02T04:04:05.000000000+01:00[\": extra text: [",
115+
},
116+
} {
117+
var gotDateTime *datetime.DateTime
118+
err := json.Unmarshal([]byte(currCase.input), &gotDateTime)
119+
assert.EqualError(t, err, currCase.wantErr, "Case %d", i)
120+
}
121+
}

godel/config/license-plugin.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,7 @@ header: |
22
// Copyright (c) {{YEAR}} Palantir Technologies. All rights reserved.
33
// Use of this source code is governed by a BSD-style
44
// license that can be found in the LICENSE file.
5+
6+
exclude:
7+
paths:
8+
- uuid/internal/uuid

rid/resource_identifier.go

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
// Copyright (c) 2018 Palantir Technologies. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package rid
6+
7+
import (
8+
"errors"
9+
"fmt"
10+
"regexp"
11+
"strings"
12+
)
13+
14+
// A ResourceIdentifier is a four-part identifier string for a resource
15+
// whose format is specified at https://github.com/palantir/resource-identifier.
16+
//
17+
// Resource Identifiers offer a common encoding for wrapping existing unique
18+
// identifiers with some additional context that can be useful when storing
19+
// those identifiers in other applications. Additionally, the context can be
20+
// used to disambiguate application-unique, but not globally-unique,
21+
// identifiers when used in a common space.
22+
type ResourceIdentifier struct {
23+
// Service is a string that represents the service (or application) that namespaces the rest of the identifier.
24+
// Must conform with regex pattern [a-z][a-z0-9\-]*.
25+
Service string
26+
// Instance is an optionally empty string that represents a specific service cluster, to allow disambiguation of artifacts from different service clusters.
27+
// Must conform to regex pattern ([a-z0-9][a-z0-9\-]*)?.
28+
Instance string
29+
// Type is a service-specific resource type to namespace a group of locators.
30+
// Must conform to regex pattern [a-z][a-z0-9\-]*.
31+
Type string
32+
// Locator is a string used to uniquely locate the specific resource.
33+
// Must conform to regex pattern [a-zA-Z0-9\-\._]+.
34+
Locator string
35+
}
36+
37+
func (rid ResourceIdentifier) String() string {
38+
return rid.Service + "." + rid.Instance + "." + rid.Type + "." + rid.Locator
39+
}
40+
41+
// MarshalText implements encoding.TextMarshaler (used by encoding/json and others).
42+
func (rid ResourceIdentifier) MarshalText() (text []byte, err error) {
43+
return []byte(rid.String()), rid.validate()
44+
}
45+
46+
// UnmarshalText implements encoding.TextUnmarshaler (used by encoding/json and others).
47+
func (rid *ResourceIdentifier) UnmarshalText(text []byte) error {
48+
var err error
49+
parsed, err := ParseRID(string(text))
50+
if err != nil {
51+
return err
52+
}
53+
*rid = parsed
54+
return nil
55+
}
56+
57+
// ParseRID parses a string into a 4-part resource identifier.
58+
func ParseRID(s string) (ResourceIdentifier, error) {
59+
segments := strings.SplitN(s, ".", 4)
60+
if len(segments) != 4 {
61+
return ResourceIdentifier{}, errors.New("invalid resource identifier")
62+
}
63+
rid := ResourceIdentifier{
64+
Service: segments[0],
65+
Instance: segments[1],
66+
Type: segments[2],
67+
Locator: segments[3],
68+
}
69+
return rid, rid.validate()
70+
}
71+
72+
var (
73+
servicePattern = regexp.MustCompile(`^[a-z][a-z0-9\-]*$`)
74+
instancePattern = regexp.MustCompile(`^[a-z0-9][a-z0-9\-]*$`)
75+
typePattern = regexp.MustCompile(`^[a-z][a-z0-9\-]*$`)
76+
locatorPattern = regexp.MustCompile(`^[a-zA-Z0-9\-\._]+$`)
77+
)
78+
79+
func (rid ResourceIdentifier) validate() error {
80+
var msgs []string
81+
if !servicePattern.MatchString(rid.Service) {
82+
msgs = append(msgs, fmt.Sprintf("rid first segment (service) does not match %s pattern", servicePattern))
83+
}
84+
if !instancePattern.MatchString(rid.Instance) {
85+
msgs = append(msgs, fmt.Sprintf("rid second segment (instance) does not match %s pattern", instancePattern))
86+
}
87+
if !typePattern.MatchString(rid.Type) {
88+
msgs = append(msgs, fmt.Sprintf("rid third segment (type) does not match %s pattern", typePattern))
89+
}
90+
if !locatorPattern.MatchString(rid.Locator) {
91+
msgs = append(msgs, fmt.Sprintf("rid fourth segment (locator) does not match %s pattern", locatorPattern))
92+
}
93+
if len(msgs) != 0 {
94+
return errors.New(strings.Join(msgs, ": "))
95+
}
96+
return nil
97+
}

rid/resource_identifier_test.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
// Copyright (c) 2018 Palantir Technologies. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package rid_test
6+
7+
import (
8+
"encoding/json"
9+
"fmt"
10+
"testing"
11+
12+
"github.com/stretchr/testify/assert"
13+
"github.com/stretchr/testify/require"
14+
15+
"github.com/palantir/pkg/rid"
16+
)
17+
18+
func TestResourceIdentifier(t *testing.T) {
19+
for _, test := range []struct {
20+
Name string
21+
Input rid.ResourceIdentifier
22+
Expected string
23+
ExpectedErr string
24+
}{
25+
{
26+
Name: "basic RID",
27+
Input: rid.ResourceIdentifier{
28+
Service: "my-service",
29+
Instance: "my-instance",
30+
Type: "my-type",
31+
Locator: "my.locator.with.dots",
32+
},
33+
Expected: "my-service.my-instance.my-type.my.locator.with.dots",
34+
},
35+
{
36+
Name: "invalid casing",
37+
Input: rid.ResourceIdentifier{
38+
Service: "myService",
39+
Instance: "myInstance",
40+
Type: "myType",
41+
Locator: "my.locator.with.dots",
42+
},
43+
ExpectedErr: `rid first segment (service) does not match ^[a-z][a-z0-9\-]*$ pattern: rid second segment (instance) does not match ^[a-z0-9][a-z0-9\-]*$ pattern: rid third segment (type) does not match ^[a-z][a-z0-9\-]*$ pattern`,
44+
},
45+
} {
46+
t.Run(test.Name, func(t *testing.T) {
47+
type ridContainer struct {
48+
RID rid.ResourceIdentifier `json:"rid"`
49+
}
50+
51+
// Test Marshal
52+
jsonBytes, err := json.Marshal(ridContainer{RID: test.Input})
53+
if test.ExpectedErr != "" {
54+
require.Error(t, err)
55+
require.Contains(t, err.Error(), test.ExpectedErr)
56+
return
57+
}
58+
require.NoError(t, err)
59+
require.Equal(t, fmt.Sprintf(`{"rid":%q}`, test.Expected), string(jsonBytes))
60+
61+
// Test Unmarshal
62+
var unmarshaled ridContainer
63+
err = json.Unmarshal(jsonBytes, &unmarshaled)
64+
require.NoError(t, err, "failed to unmarshal json: %s", string(jsonBytes))
65+
assert.Equal(t, test.Expected, unmarshaled.RID.String())
66+
assert.Equal(t, test.Input, unmarshaled.RID)
67+
})
68+
}
69+
}

0 commit comments

Comments
 (0)