Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add bearertoken, datetime, rid, safelong, and uuid packages #132

Merged
merged 2 commits into from
Dec 19, 2018
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
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