Skip to content

Commit

Permalink
Add bearertoken, datetime, rid, safelong, and uuid packages (#132)
Browse files Browse the repository at this point in the history
  • Loading branch information
bmoylan authored Dec 19, 2018
1 parent 9b18547 commit d0bd127
Show file tree
Hide file tree
Showing 17 changed files with 1,031 additions and 0 deletions.
9 changes: 9 additions & 0 deletions bearertoken/bearertoken.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Copyright (c) 2018 Palantir Technologies. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package bearertoken

// Token represents a bearer token, generally sent by a REST client in a
// Authorization or Cookie header for authentication purposes.
type Token string
49 changes: 49 additions & 0 deletions datetime/datetime.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Copyright (c) 2018 Palantir Technologies. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package datetime

import (
"strings"
"time"
)

// DateTime is an alias for time.Time which implements serialization matching the
// conjure wire specification at https://github.com/palantir/conjure/blob/master/docs/spec/wire.md
type DateTime time.Time

func (d DateTime) String() string {
return time.Time(d).Format(time.RFC3339Nano)
}

// MarshalText implements encoding.TextMarshaler (used by encoding/json and others).
func (d DateTime) MarshalText() ([]byte, error) {
return []byte(d.String()), nil
}

// UnmarshalText implements encoding.TextUnmarshaler (used by encoding/json and others).
func (d *DateTime) UnmarshalText(b []byte) error {
t, err := ParseDateTime(string(b))
if err != nil {
return err
}
*d = t
return nil
}

// ParseDateTime parses a DateTime from a string. Conjure supports DateTime inputs that end with an optional
// zone identifier enclosed in square brackets (for example, "2017-01-02T04:04:05.000000000+01:00[Europe/Berlin]").
func ParseDateTime(s string) (DateTime, error) {
// If the input string ends in a ']' and contains a '[', parse the string up to '['.
if strings.HasSuffix(s, "]") {
if openBracketIdx := strings.LastIndex(s, "["); openBracketIdx != -1 {
s = s[:openBracketIdx]
}
}
timeVal, err := time.Parse(time.RFC3339Nano, s)
if err != nil {
return DateTime(time.Time{}), err
}
return DateTime(timeVal), nil
}
121 changes: 121 additions & 0 deletions datetime/datetime_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
// Copyright (c) 2018 Palantir Technologies. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package datetime_test

import (
"encoding/json"
"testing"
"time"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/palantir/pkg/datetime"
)

var dateTimeJSONs = []struct {
sec int64
zoneOffset int
str string
json string
}{
{
sec: 1483326245,
str: `2017-01-02T03:04:05Z`,
json: `"2017-01-02T03:04:05Z"`,
},
{
sec: 1483326245,
str: `2017-01-02T03:04:05Z`,
json: `"2017-01-02T03:04:05.000Z"`,
},
{
sec: 1483326245,
str: `2017-01-02T03:04:05Z`,
json: `"2017-01-02T03:04:05.000000000Z"`,
},
{
sec: 1483326245,
zoneOffset: 3600,
str: `2017-01-02T04:04:05+01:00`,
json: `"2017-01-02T04:04:05.000000000+01:00"`,
},
{
sec: 1483326245,
zoneOffset: 7200,
str: `2017-01-02T05:04:05+02:00`,
json: `"2017-01-02T05:04:05.000000000+02:00"`,
},
{
sec: 1483326245,
zoneOffset: 3600,
str: `2017-01-02T04:04:05+01:00`,
json: `"2017-01-02T04:04:05.000000000+01:00[Europe/Berlin]"`,
},
}

func TestDateTimeString(t *testing.T) {
for i, currCase := range dateTimeJSONs {
currDateTime := datetime.DateTime(time.Unix(currCase.sec, 0).In(time.FixedZone("", currCase.zoneOffset)))
assert.Equal(t, currCase.str, currDateTime.String(), "Case %d", i)
}
}

func TestDateTimeMarshal(t *testing.T) {
for i, currCase := range dateTimeJSONs {
currDateTime := datetime.DateTime(time.Unix(currCase.sec, 0).In(time.FixedZone("", currCase.zoneOffset)))
bytes, err := json.Marshal(currDateTime)
require.NoError(t, err, "Case %d: marshal %q", i, currDateTime.String())

var unmarshaledFromMarshal datetime.DateTime
err = json.Unmarshal(bytes, &unmarshaledFromMarshal)
require.NoError(t, err, "Case %d: unmarshal %q", i, string(bytes))

var unmarshaledFromCase datetime.DateTime
err = json.Unmarshal([]byte(currCase.json), &unmarshaledFromCase)
require.NoError(t, err, "Case %d: unmarshal %q", i, currCase.json)

assert.Equal(t, unmarshaledFromCase, unmarshaledFromMarshal, "Case %d", i)
}
}

func TestDateTimeUnmarshal(t *testing.T) {
for i, currCase := range dateTimeJSONs {
wantDateTime := time.Unix(currCase.sec, 0).UTC()
if currCase.zoneOffset != 0 {
wantDateTime = wantDateTime.In(time.FixedZone("", currCase.zoneOffset))
}

var gotDateTime datetime.DateTime
err := json.Unmarshal([]byte(currCase.json), &gotDateTime)
require.NoError(t, err, "Case %d", i)

assert.Equal(t, wantDateTime, time.Time(gotDateTime), "Case %d", i)
}
}

func TestDateTimeUnmarshalInvalid(t *testing.T) {
for i, currCase := range []struct {
input string
wantErr string
}{
{
input: `"foo"`,
wantErr: "parsing time \"foo\" as \"2006-01-02T15:04:05.999999999Z07:00\": cannot parse \"foo\" as \"2006\"",
},
{
input: `"2017-01-02T04:04:05.000000000+01:00[Europe/Berlin"`,
wantErr: "parsing time \"2017-01-02T04:04:05.000000000+01:00[Europe/Berlin\": extra text: [Europe/Berlin",
},
{
input: `"2017-01-02T04:04:05.000000000+01:00[[Europe/Berlin]]"`,
wantErr: "parsing time \"2017-01-02T04:04:05.000000000+01:00[\": extra text: [",
},
} {
var gotDateTime *datetime.DateTime
err := json.Unmarshal([]byte(currCase.input), &gotDateTime)
assert.EqualError(t, err, currCase.wantErr, "Case %d", i)
}
}
4 changes: 4 additions & 0 deletions godel/config/license-plugin.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,7 @@ header: |
// Copyright (c) {{YEAR}} Palantir Technologies. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
exclude:
paths:
- uuid/internal/uuid
97 changes: 97 additions & 0 deletions rid/resource_identifier.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// Copyright (c) 2018 Palantir Technologies. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package rid

import (
"errors"
"fmt"
"regexp"
"strings"
)

// A ResourceIdentifier is a four-part identifier string for a resource
// whose format is specified at https://github.com/palantir/resource-identifier.
//
// Resource Identifiers offer a common encoding for wrapping existing unique
// identifiers with some additional context that can be useful when storing
// those identifiers in other applications. Additionally, the context can be
// used to disambiguate application-unique, but not globally-unique,
// identifiers when used in a common space.
type ResourceIdentifier struct {
// Service is a string that represents the service (or application) that namespaces the rest of the identifier.
// Must conform with regex pattern [a-z][a-z0-9\-]*.
Service string
// Instance is an optionally empty string that represents a specific service cluster, to allow disambiguation of artifacts from different service clusters.
// Must conform to regex pattern ([a-z0-9][a-z0-9\-]*)?.
Instance string
// Type is a service-specific resource type to namespace a group of locators.
// Must conform to regex pattern [a-z][a-z0-9\-]*.
Type string
// Locator is a string used to uniquely locate the specific resource.
// Must conform to regex pattern [a-zA-Z0-9\-\._]+.
Locator string
}

func (rid ResourceIdentifier) String() string {
return rid.Service + "." + rid.Instance + "." + rid.Type + "." + rid.Locator
}

// MarshalText implements encoding.TextMarshaler (used by encoding/json and others).
func (rid ResourceIdentifier) MarshalText() (text []byte, err error) {
return []byte(rid.String()), rid.validate()
}

// UnmarshalText implements encoding.TextUnmarshaler (used by encoding/json and others).
func (rid *ResourceIdentifier) UnmarshalText(text []byte) error {
var err error
parsed, err := ParseRID(string(text))
if err != nil {
return err
}
*rid = parsed
return nil
}

// ParseRID parses a string into a 4-part resource identifier.
func ParseRID(s string) (ResourceIdentifier, error) {
segments := strings.SplitN(s, ".", 4)
if len(segments) != 4 {
return ResourceIdentifier{}, errors.New("invalid resource identifier")
}
rid := ResourceIdentifier{
Service: segments[0],
Instance: segments[1],
Type: segments[2],
Locator: segments[3],
}
return rid, rid.validate()
}

var (
servicePattern = regexp.MustCompile(`^[a-z][a-z0-9\-]*$`)
instancePattern = regexp.MustCompile(`^[a-z0-9][a-z0-9\-]*$`)
typePattern = regexp.MustCompile(`^[a-z][a-z0-9\-]*$`)
locatorPattern = regexp.MustCompile(`^[a-zA-Z0-9\-\._]+$`)
)

func (rid ResourceIdentifier) validate() error {
var msgs []string
if !servicePattern.MatchString(rid.Service) {
msgs = append(msgs, fmt.Sprintf("rid first segment (service) does not match %s pattern", servicePattern))
}
if !instancePattern.MatchString(rid.Instance) {
msgs = append(msgs, fmt.Sprintf("rid second segment (instance) does not match %s pattern", instancePattern))
}
if !typePattern.MatchString(rid.Type) {
msgs = append(msgs, fmt.Sprintf("rid third segment (type) does not match %s pattern", typePattern))
}
if !locatorPattern.MatchString(rid.Locator) {
msgs = append(msgs, fmt.Sprintf("rid fourth segment (locator) does not match %s pattern", locatorPattern))
}
if len(msgs) != 0 {
return errors.New(strings.Join(msgs, ": "))
}
return nil
}
69 changes: 69 additions & 0 deletions rid/resource_identifier_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// Copyright (c) 2018 Palantir Technologies. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package rid_test

import (
"encoding/json"
"fmt"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/palantir/pkg/rid"
)

func TestResourceIdentifier(t *testing.T) {
for _, test := range []struct {
Name string
Input rid.ResourceIdentifier
Expected string
ExpectedErr string
}{
{
Name: "basic RID",
Input: rid.ResourceIdentifier{
Service: "my-service",
Instance: "my-instance",
Type: "my-type",
Locator: "my.locator.with.dots",
},
Expected: "my-service.my-instance.my-type.my.locator.with.dots",
},
{
Name: "invalid casing",
Input: rid.ResourceIdentifier{
Service: "myService",
Instance: "myInstance",
Type: "myType",
Locator: "my.locator.with.dots",
},
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`,
},
} {
t.Run(test.Name, func(t *testing.T) {
type ridContainer struct {
RID rid.ResourceIdentifier `json:"rid"`
}

// Test Marshal
jsonBytes, err := json.Marshal(ridContainer{RID: test.Input})
if test.ExpectedErr != "" {
require.Error(t, err)
require.Contains(t, err.Error(), test.ExpectedErr)
return
}
require.NoError(t, err)
require.Equal(t, fmt.Sprintf(`{"rid":%q}`, test.Expected), string(jsonBytes))

// Test Unmarshal
var unmarshaled ridContainer
err = json.Unmarshal(jsonBytes, &unmarshaled)
require.NoError(t, err, "failed to unmarshal json: %s", string(jsonBytes))
assert.Equal(t, test.Expected, unmarshaled.RID.String())
assert.Equal(t, test.Input, unmarshaled.RID)
})
}
}
Loading

0 comments on commit d0bd127

Please sign in to comment.