diff --git a/go.mod b/go.mod index f04c12811c..7527ca9bb0 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.15 require ( github.com/golang/protobuf v1.4.3 // indirect + github.com/google/uuid v1.1.2 google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d google.golang.org/protobuf v1.25.0 gotest.tools/v3 v3.0.3 diff --git a/go.sum b/go.sum index 843462291f..92ca00d611 100644 --- a/go.sum +++ b/go.sum @@ -23,6 +23,8 @@ github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= diff --git a/resourceid/systemgenerated.go b/resourceid/systemgenerated.go new file mode 100644 index 0000000000..206dafdda5 --- /dev/null +++ b/resourceid/systemgenerated.go @@ -0,0 +1,8 @@ +package resourceid + +import "github.com/google/uuid" + +// NewSystemGenerated returns a new system-generated resource ID. +func NewSystemGenerated() string { + return uuid.New().String() +} diff --git a/resourceid/systemgenerated_test.go b/resourceid/systemgenerated_test.go new file mode 100644 index 0000000000..94907893ef --- /dev/null +++ b/resourceid/systemgenerated_test.go @@ -0,0 +1,15 @@ +package resourceid + +import ( + "regexp" + "testing" + + "gotest.tools/v3/assert" + "gotest.tools/v3/assert/cmp" +) + +func TestNewSystemGenerated(t *testing.T) { + t.Parallel() + const uuidV4Regexp = `^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$` + assert.Assert(t, cmp.Regexp(regexp.MustCompile(uuidV4Regexp), NewSystemGenerated())) +} diff --git a/resourceid/usersettable.go b/resourceid/usersettable.go new file mode 100644 index 0000000000..9949db968d --- /dev/null +++ b/resourceid/usersettable.go @@ -0,0 +1,48 @@ +package resourceid + +import ( + "fmt" + + "github.com/google/uuid" +) + +// ValidateUserSettable validates a user-settable resource ID. +// +// From https://google.aip.dev/122#resource-id-segments: +// +// User-settable resource IDs should conform to RFC-1034,which restricts to letters, numbers, and hyphen, with a 63 +// character maximum. Additionally, user-settable resource IDs should restrict letters to lower-case. +// +// User-settable IDs should not be permitted to be a UUID (or any value that syntactically appears to be a UUID). +// +// See also: https://google.aip.dev/133#user-specified-ids +func ValidateUserSettable(id string) error { + if len(id) < 4 || 63 < len(id) { + return fmt.Errorf("user-settable ID must be between 4 and 63 characters") + } + if id[0] == '-' { + return fmt.Errorf("user-settable ID must not start with a hyphen") + } + if _, err := uuid.Parse(id); err == nil { + return fmt.Errorf("user-settable ID must not be a valid UUIDv4") + } + for position, character := range id { + switch character { + case + // numbers + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', + // hyphen + '-', + // lower-case + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', + 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z': + default: + return fmt.Errorf( + "user-settable ID must only contain lowercase, numbers and hyphens (got: '%c' in position %d)", + character, + position, + ) + } + } + return nil +} diff --git a/resourceid/usersettable_test.go b/resourceid/usersettable_test.go new file mode 100644 index 0000000000..70459d5b5c --- /dev/null +++ b/resourceid/usersettable_test.go @@ -0,0 +1,33 @@ +package resourceid + +import ( + "testing" + + "gotest.tools/v3/assert" +) + +func TestValidateUserSettable(t *testing.T) { + t.Parallel() + for _, tt := range []struct { + id string + errorContains string + }{ + {id: "abcd"}, + {id: "abcd-efgh-1234"}, + {id: "abc", errorContains: "must be between 4 and 63 characters"}, + {id: "-abc", errorContains: "must not start with a hyphen"}, + {id: "daf1cb3e-f33b-43f1-81cc-e65fda51efa5", errorContains: "must not be a valid UUIDv4"}, + {id: "abcd/efgh", errorContains: "must only contain lowercase, numbers and hyphens"}, + } { + tt := tt + t.Run(tt.id, func(t *testing.T) { + t.Parallel() + err := ValidateUserSettable(tt.id) + if tt.errorContains != "" { + assert.ErrorContains(t, err, tt.errorContains) + } else { + assert.NilError(t, err) + } + }) + } +}