Skip to content

Commit

Permalink
Add initial infratographer urn lib (#18)
Browse files Browse the repository at this point in the history
Initial library that allows for building and parsing URNs in a format
that we expect to use within infratographer

---------

Signed-off-by: Tyler Auerbeck <[email protected]>
Signed-off-by: Tyler Auerbeck <[email protected]>
Co-authored-by: Tyler Auerbeck <[email protected]>
Co-authored-by: E Camden Fisher <[email protected]>
  • Loading branch information
3 people authored Feb 24, 2023
1 parent 1cb6750 commit a3f7084
Show file tree
Hide file tree
Showing 10 changed files with 433 additions and 0 deletions.
6 changes: 6 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ require (
go.uber.org/zap v1.24.0
)

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
)

require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/cenkalti/backoff/v4 v4.2.0 // indirect
Expand Down Expand Up @@ -69,6 +74,7 @@ require (
github.com/spf13/afero v1.9.3 // indirect
github.com/spf13/cast v1.5.0 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/stretchr/testify v1.8.1
github.com/subosito/gotenv v1.4.1 // indirect
github.com/ugorji/go/codec v1.2.7 // indirect
go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.11.2 // indirect
Expand Down
34 changes: 34 additions & 0 deletions urnx/build.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package urnx

import (
"fmt"
"strings"

"github.com/google/uuid"
)

// Build create a new URN with the specified fields
func Build(namespace string, resourceType string, resourceID uuid.UUID) (*URN, error) {
ns := validateNamespace(namespace)
if !ns {
return nil, ErrInvalidURNNamespace
}

rt := validateResourceType(resourceType)
if !rt {
return nil, ErrInvalidURNResourceType
}

u := &URN{
Namespace: strings.ToLower(namespace),
ResourceType: strings.ToLower(resourceType),
ResourceID: resourceID,
}

return u, nil
}

// String returns the string representation of the URN
func (u *URN) String() string {
return fmt.Sprintf("%s:%s:%s:%s", prefix, u.Namespace, u.ResourceType, u.ResourceID)
}
75 changes: 75 additions & 0 deletions urnx/build_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package urnx

import (
"fmt"
"testing"

"github.com/google/uuid"
"github.com/stretchr/testify/assert"
)

type bcase struct {
name string
namespace string
resourceType string
resourceID uuid.UUID
expectError bool
expectedErrors []error
}

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

bc := []bcase{
{
name: "valid-build",
namespace: "namespace",
resourceType: "resource-type",
resourceID: uuid.New(),
expectError: false,
expectedErrors: []error{},
},
{
name: "invalid-namespace",
namespace: "invalid-namespace!",
resourceType: "resource-type",
resourceID: uuid.New(),
expectError: true,
expectedErrors: []error{
ErrInvalidURNNamespace,
},
},
{
name: "invalid-resource-type",
namespace: "namespace",
resourceType: "invalid-resource-type!",
resourceID: uuid.New(),
expectError: true,
expectedErrors: []error{
ErrInvalidURNResourceType,
},
},
}

for _, c := range bc {
t.Run(c.name, func(t *testing.T) {
urn, err := Build(c.namespace, c.resourceType, c.resourceID)

if c.expectError {
assert.Error(t, err)
assert.EqualError(t, err, c.expectedErrors[0].Error())
} else {
assert.NoError(t, err)
expectedURN := &URN{Namespace: c.namespace, ResourceType: c.resourceType, ResourceID: c.resourceID}
assert.Equal(t, expectedURN, urn)
}
})
}
}

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

urn := &URN{Namespace: "namespace", ResourceType: "resource-type", ResourceID: uuid.New()}
assert.Equal(t, fmt.Sprintf("%s:%s:%s:%s", prefix, urn.Namespace, urn.ResourceType, urn.ResourceID), urn.String())
}
18 changes: 18 additions & 0 deletions urnx/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Copyright 2022 The Infratographer Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Package urnx is a package for creating and parsing Infratographer based URNs
// in the format of urn:<namespace>:<resource type>:<resource id> which are used
// to identify resources in the Infratographer ecosystem.
package urnx
20 changes: 20 additions & 0 deletions urnx/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package urnx

import "errors"

// ErrInvalidURNPrefix is returned when the URN prefix is invalid
var ErrInvalidURNPrefix = errors.New("invalid urn prefix: expected '" + prefix + "'")

// ErrInvalidURN is returned when the URN is invalid
var ErrInvalidURN = errors.New("invalid urn: expected 'urn:<namespace>:<resource type>:<resource id>")

// ErrInvalidURNNamespace is returned when the URN namespace is invalid and does not match
// the regex [A-za-z0-9-]{1,30}
var ErrInvalidURNNamespace = errors.New("invalid urn namespace: expected string consisting of [A-za-z0-9-]{1,30}")

// ErrInvalidURNResourceType is returned when the URN resource type is invalid and does not match
// the regex [A-za-z0-9-]{1,}
var ErrInvalidURNResourceType = errors.New("invalid urn resource type: expected string consisting of [A-za-z0-9-]{1,255}")

// ErrInvalidURNResourceID is returned when the URN resource ID is invalid and not a valid UUID
var ErrInvalidURNResourceID = errors.New("invalid urn resource id: expected valid uuid")
43 changes: 43 additions & 0 deletions urnx/parse.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package urnx

import (
"strings"

"github.com/google/uuid"
)

// Parse parses a string into a URN object
func Parse(urn string) (*URN, error) {
conv := strings.Split(urn, ":")

if len(conv) != urnLength {
return nil, ErrInvalidURN
}

if conv[0] != prefix {
return nil, ErrInvalidURNPrefix
}

ns := validateNamespace(conv[1])
if !ns {
return nil, ErrInvalidURNNamespace
}

rt := validateResourceType(conv[2])
if !rt {
return nil, ErrInvalidURNResourceType
}

id, err := uuid.Parse(conv[3])
if err != nil {
return nil, ErrInvalidURNResourceID
}

u := &URN{
Namespace: conv[1],
ResourceType: conv[2],
ResourceID: id,
}

return u, nil
}
97 changes: 97 additions & 0 deletions urnx/parse_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package urnx

import (
"testing"

"github.com/google/uuid"
"github.com/stretchr/testify/assert"
)

type pcase struct {
name string
urn string
expectError bool
expectedErrors []error
}

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

pc := []pcase{
{
name: "valid-parse",
urn: "urn:namespace:resource-type:" + uuid.New().String(),
expectError: false,
expectedErrors: []error{},
},
{
name: "invalid-uuid",
urn: "urn:namespace:resource-type:invalid-uuid",
expectError: true,
expectedErrors: []error{
ErrInvalidURNResourceID,
},
},
{
name: "invalid-prefix",
urn: "invalid-prefix:namespace:resource-type:" + uuid.New().String(),
expectError: true,
expectedErrors: []error{
ErrInvalidURNPrefix,
},
},
{
name: "too-many-fields",
urn: "urn:namespace:resource-type:" + uuid.New().String() + ":extra-field",
expectError: true,
expectedErrors: []error{
ErrInvalidURN,
},
},
{
name: "too-few-fields",
urn: "urn:namespace:resource-type",
expectError: true,
expectedErrors: []error{
ErrInvalidURN,
},
},
{
name: "invalid-separator",
urn: "urn-namespace-resource-type:" + uuid.New().String() + "-extra-field",
expectError: true,
expectedErrors: []error{
ErrInvalidURN,
},
},
{
name: "invalid-namespace",
urn: "urn:invalid-namespace!:resource-type:" + uuid.New().String(),
expectError: true,
expectedErrors: []error{
ErrInvalidURNNamespace,
},
},
{
name: "invalid-resource-type",
urn: "urn:namespace:invalid-resource-type!:" + uuid.New().String(),
expectError: true,
expectedErrors: []error{
ErrInvalidURNResourceType,
},
},
}

for _, c := range pc {
t.Run(c.name, func(t *testing.T) {
_, err := Parse(c.urn)

if c.expectError {
assert.Error(t, err)
assert.EqualError(t, err, c.expectedErrors[0].Error())
} else {
assert.NoError(t, err)
}
})
}
}
15 changes: 15 additions & 0 deletions urnx/types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package urnx

import "github.com/google/uuid"

const (
prefix = "urn"
urnLength = 4
)

// URN is an infratographer based URN consisting of a namespace, resource type and resource ID
type URN struct {
Namespace string
ResourceType string
ResourceID uuid.UUID
}
27 changes: 27 additions & 0 deletions urnx/validate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package urnx

import (
"regexp"
)

var (
nsrx *regexp.Regexp
rtrx *regexp.Regexp
)

func init() {
nsrx = regexp.MustCompile("^[A-Za-z0-9-]{1,30}$")
rtrx = regexp.MustCompile("^[A-Za-z0-9-]{1,255}$")
}

func validateNamespace(namespace string) bool {
rx := nsrx.MatchString(namespace)

return rx
}

func validateResourceType(name string) bool {
rx := rtrx.MatchString(name)

return rx
}
Loading

0 comments on commit a3f7084

Please sign in to comment.