Repository: apigee/registry
Pull request #376 (2021-10-29 03:48:13)

Support for deployment resources in the names package
This also updates the names tests to use Parse functions instead of regular expressions (now fully hiding the regular expressions inside the names package)


Patch:
diff --git a/server/registry/names/api.go b/server/registry/names/api.go
index 4ad29d5..876c1c1 100644
--- a/server/registry/names/api.go
+++ b/server/registry/names/api.go
@@ -52,6 +52,15 @@ func (a Api) Version(id string) Version {
 	}
 }
 
+// Deployment returns an API deployment with the provided ID and this resource as its parent.
+func (a Api) Deployment(id string) Deployment {
+	return Deployment{
+		ProjectID:    a.ProjectID,
+		ApiID:        a.ApiID,
+		DeploymentID: id,
+	}
+}
+
 // Artifact returns an artifact with the provided ID and this resource as its parent.
 func (a Api) Artifact(id string) Artifact {
 	return Artifact{
diff --git a/server/registry/names/artifact.go b/server/registry/names/artifact.go
index 02cd170..a5cc5d2 100644
--- a/server/registry/names/artifact.go
+++ b/server/registry/names/artifact.go
@@ -20,15 +20,17 @@ import (
 )
 
 var (
-	projectArtifactCollectionRegexp = regexp.MustCompile(fmt.Sprintf("^projects/%s/locations/%s/artifacts$", identifier, Location))
-	apiArtifactCollectionRegexp     = regexp.MustCompile(fmt.Sprintf("^projects/%s/locations/%s/apis/%s/artifacts$", identifier, Location, identifier))
-	versionArtifactCollectionRegexp = regexp.MustCompile(fmt.Sprintf("^projects/%s/locations/%s/apis/%s/versions/%s/artifacts$", identifier, Location, identifier, identifier))
-	specArtifactCollectionRegexp    = regexp.MustCompile(fmt.Sprintf("^projects/%s/locations/%s/apis/%s/versions/%s/specs/%s/artifacts$", identifier, Location, identifier, identifier, identifier))
-
-	projectArtifactRegexp = regexp.MustCompile(fmt.Sprintf("^projects/%s/locations/%s/artifacts/%s$", identifier, Location, identifier))
-	apiArtifactRegexp     = regexp.MustCompile(fmt.Sprintf("^projects/%s/locations/%s/apis/%s/artifacts/%s$", identifier, Location, identifier, identifier))
-	versionArtifactRegexp = regexp.MustCompile(fmt.Sprintf("^projects/%s/locations/%s/apis/%s/versions/%s/artifacts/%s$", identifier, Location, identifier, identifier, identifier))
-	specArtifactRegexp    = regexp.MustCompile(fmt.Sprintf("^projects/%s/locations/%s/apis/%s/versions/%s/specs/%s/artifacts/%s$", identifier, Location, identifier, identifier, identifier, identifier))
+	projectArtifactCollectionRegexp    = regexp.MustCompile(fmt.Sprintf("^projects/%s/locations/%s/artifacts$", identifier, Location))
+	apiArtifactCollectionRegexp        = regexp.MustCompile(fmt.Sprintf("^projects/%s/locations/%s/apis/%s/artifacts$", identifier, Location, identifier))
+	versionArtifactCollectionRegexp    = regexp.MustCompile(fmt.Sprintf("^projects/%s/locations/%s/apis/%s/versions/%s/artifacts$", identifier, Location, identifier, identifier))
+	specArtifactCollectionRegexp       = regexp.MustCompile(fmt.Sprintf("^projects/%s/locations/%s/apis/%s/versions/%s/specs/%s/artifacts$", identifier, Location, identifier, identifier, identifier))
+	deploymentArtifactCollectionRegexp = regexp.MustCompile(fmt.Sprintf("^projects/%s/locations/%s/apis/%s/deployments/%s/artifacts$", identifier, Location, identifier, identifier))
+
+	projectArtifactRegexp    = regexp.MustCompile(fmt.Sprintf("^projects/%s/locations/%s/artifacts/%s$", identifier, Location, identifier))
+	apiArtifactRegexp        = regexp.MustCompile(fmt.Sprintf("^projects/%s/locations/%s/apis/%s/artifacts/%s$", identifier, Location, identifier, identifier))
+	versionArtifactRegexp    = regexp.MustCompile(fmt.Sprintf("^projects/%s/locations/%s/apis/%s/versions/%s/artifacts/%s$", identifier, Location, identifier, identifier, identifier))
+	specArtifactRegexp       = regexp.MustCompile(fmt.Sprintf("^projects/%s/locations/%s/apis/%s/versions/%s/specs/%s/artifacts/%s$", identifier, Location, identifier, identifier, identifier, identifier))
+	deploymentArtifactRegexp = regexp.MustCompile(fmt.Sprintf("^projects/%s/locations/%s/apis/%s/deployments/%s/artifacts/%s$", identifier, Location, identifier, identifier, identifier))
 )
 
 // Artifact represents a resource name for an artifact.
@@ -50,6 +52,8 @@ func (a Artifact) ProjectID() string {
 		return name.ProjectID
 	case specArtifact:
 		return name.ProjectID
+	case deploymentArtifact:
+		return name.ProjectID
 	default:
 		return ""
 	}
@@ -64,6 +68,8 @@ func (a Artifact) ApiID() string {
 		return name.ApiID
 	case specArtifact:
 		return name.ApiID
+	case deploymentArtifact:
+		return name.ApiID
 	default:
 		return ""
 	}
@@ -91,6 +97,16 @@ func (a Artifact) SpecID() string {
 	}
 }
 
+// DeploymentID returns the artifact's deployment ID, or empty string if it doesn't have one.
+func (a Artifact) DeploymentID() string {
+	switch name := a.name.(type) {
+	case deploymentArtifact:
+		return name.DeploymentID
+	default:
+		return ""
+	}
+}
+
 // ArtifactID returns the artifact's ID.
 func (a Artifact) ArtifactID() string {
 	switch name := a.name.(type) {
@@ -102,6 +118,8 @@ func (a Artifact) ArtifactID() string {
 		return name.ArtifactID
 	case specArtifact:
 		return name.ArtifactID
+	case deploymentArtifact:
+		return name.ArtifactID
 	default:
 		return ""
 	}
@@ -124,6 +142,8 @@ func (a Artifact) Parent() string {
 		return name.Parent()
 	case specArtifact:
 		return name.Parent()
+	case deploymentArtifact:
+		return name.Parent()
 	default:
 		return ""
 	}
@@ -143,6 +163,8 @@ func ParseArtifact(name string) (Artifact, error) {
 		return Artifact{name: n}, nil
 	} else if n, err := parseProjectArtifact(name); err == nil {
 		return Artifact{name: n}, nil
+	} else if n, err := parseDeploymentArtifact(name); err == nil {
+		return Artifact{name: n}, nil
 	}
 
 	return Artifact{}, fmt.Errorf("invalid artifact name %q, must match one of: %v", name, []string{
@@ -150,19 +172,22 @@ func ParseArtifact(name string) (Artifact, error) {
 		apiArtifactRegexp.String(),
 		versionArtifactRegexp.String(),
 		specArtifactRegexp.String(),
+		deploymentArtifactRegexp.String(),
 	})
 }
 
 // ParseArtifactCollection parses the name of an artifact collection.
 func ParseArtifactCollection(name string) (Artifact, error) {
-	if spec, err := parseSpecArtifactCollection(name); err == nil {
-		return Artifact{name: spec}, nil
-	} else if version, err := parseVersionArtifactCollection(name); err == nil {
-		return Artifact{name: version}, nil
-	} else if api, err := parseApiArtifactCollection(name); err == nil {
-		return Artifact{name: api}, nil
-	} else if project, err := parseProjectArtifactCollection(name); err == nil {
-		return Artifact{name: project}, nil
+	if n, err := parseSpecArtifactCollection(name); err == nil {
+		return Artifact{name: n}, nil
+	} else if n, err := parseVersionArtifactCollection(name); err == nil {
+		return Artifact{name: n}, nil
+	} else if n, err := parseApiArtifactCollection(name); err == nil {
+		return Artifact{name: n}, nil
+	} else if n, err := parseProjectArtifactCollection(name); err == nil {
+		return Artifact{name: n}, nil
+	} else if n, err := parseDeploymentArtifactCollection(name); err == nil {
+		return Artifact{name: n}, nil
 	}
 
 	return Artifact{}, fmt.Errorf("invalid artifact collection name %q, must match one of: %v", name, []string{
@@ -170,6 +195,7 @@ func ParseArtifactCollection(name string) (Artifact, error) {
 		apiArtifactCollectionRegexp.String(),
 		versionArtifactCollectionRegexp.String(),
 		specArtifactCollectionRegexp.String(),
+		deploymentArtifactCollectionRegexp.String(),
 	})
 }
 
@@ -403,18 +429,62 @@ func parseSpecArtifactCollection(name string) (specArtifact, error) {
 	return artifact, nil
 }
 
-// artifactCollectionRegexp returns a regular expression that matches collection of artifacts.
-// TODO: Delete
-func artifactCollectionRegexp() *regexp.Regexp {
-	return regexp.MustCompile(
-		fmt.Sprintf("^projects/%s/locations/%s(/apis/%s(/versions/%s(/specs/%s)?)?)?/artifacts$",
-			identifier, Location, identifier, identifier, identifier))
+type deploymentArtifact struct {
+	ProjectID    string
+	ApiID        string
+	DeploymentID string
+	ArtifactID   string
+}
+
+func (a deploymentArtifact) Validate() error {
+	if name := a.String(); !deploymentArtifactRegexp.MatchString(name) {
+		return fmt.Errorf("invalid version artifact name %q: must match %q", name, deploymentArtifactRegexp)
+	}
+
+	return validateID(a.ArtifactID)
+}
+
+func (a deploymentArtifact) Parent() string {
+	return Deployment{
+		ProjectID:    a.ProjectID,
+		ApiID:        a.ApiID,
+		DeploymentID: a.DeploymentID,
+	}.String()
+}
+
+func (a deploymentArtifact) String() string {
+	return normalize(fmt.Sprintf("projects/%s/locations/%s/apis/%s/deployments/%s/artifacts/%s",
+		a.ProjectID, Location, a.ApiID, a.DeploymentID, a.ArtifactID))
 }
 
-// artifactRegexp returns a regular expression that matches an artifact resource name.
-// TODO: Delete
-func artifactRegexp() *regexp.Regexp {
-	return regexp.MustCompile(
-		fmt.Sprintf("^projects/%s/locations/%s(/apis/%s(/versions/%s(/specs/%s)?)?)?/artifacts/%s$",
-			identifier, Location, identifier, identifier, identifier, identifier))
+func parseDeploymentArtifact(name string) (deploymentArtifact, error) {
+	if !deploymentArtifactRegexp.MatchString(name) {
+		return deploymentArtifact{}, fmt.Errorf("invalid deployment artifact name %q: must match %q", name, deploymentArtifactRegexp)
+	}
+
+	m := deploymentArtifactRegexp.FindStringSubmatch(name)
+	artifact := deploymentArtifact{
+		ProjectID:    m[1],
+		ApiID:        m[2],
+		DeploymentID: m[3],
+		ArtifactID:   m[4],
+	}
+
+	return artifact, nil
+}
+
+func parseDeploymentArtifactCollection(name string) (deploymentArtifact, error) {
+	if !deploymentArtifactCollectionRegexp.MatchString(name) {
+		return deploymentArtifact{}, fmt.Errorf("invalid deployment artifact name %q: must match %q", name, deploymentArtifactCollectionRegexp)
+	}
+
+	m := deploymentArtifactCollectionRegexp.FindStringSubmatch(name)
+	artifact := deploymentArtifact{
+		ProjectID:    m[1],
+		ApiID:        m[2],
+		DeploymentID: m[3],
+		ArtifactID:   "",
+	}
+
+	return artifact, nil
 }
diff --git a/server/registry/names/deployment.go b/server/registry/names/deployment.go
new file mode 100644
index 0000000..61e3961
--- /dev/null
+++ b/server/registry/names/deployment.go
@@ -0,0 +1,133 @@
+// Copyright 2020 Google LLC. All Rights Reserved.
+//
+// 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 names
+
+import (
+	"fmt"
+	"regexp"
+)
+
+// Deployment represents a resource name for an API deployment.
+type Deployment struct {
+	ProjectID    string
+	ApiID        string
+	DeploymentID string
+}
+
+// Validate returns an error if the resource name is invalid.
+// For backward compatibility, names should only be validated at creation time.
+func (d Deployment) Validate() error {
+	r := deploymentRegexp()
+	if name := d.String(); !r.MatchString(name) {
+		return fmt.Errorf("invalid deployment name %q: must match %q", name, r)
+	}
+
+	return validateID(d.DeploymentID)
+}
+
+// Project returns the parent project for this resource.
+func (d Deployment) Project() Project {
+	return d.Api().Project()
+}
+
+// Api returns the parent API for this resource.
+func (d Deployment) Api() Api {
+	return Api{
+		ProjectID: d.ProjectID,
+		ApiID:     d.ApiID,
+	}
+}
+
+// Revision returns an API deployment revision with the provided ID and this resource as its parent.
+func (d Deployment) Revision(id string) DeploymentRevision {
+	return DeploymentRevision{
+		ProjectID:    d.ProjectID,
+		ApiID:        d.ApiID,
+		DeploymentID: d.DeploymentID,
+		RevisionID:   id,
+	}
+}
+
+// Artifact returns an artifact with the provided ID and this resource as its parent.
+func (d Deployment) Artifact(id string) Artifact {
+	return Artifact{
+		name: deploymentArtifact{
+			ProjectID:    d.ProjectID,
+			ApiID:        d.ApiID,
+			DeploymentID: d.DeploymentID,
+			ArtifactID:   id,
+		},
+	}
+}
+
+// Normal returns the resource name with normalized identifiers.
+func (d Deployment) Normal() Deployment {
+	return Deployment{
+		ProjectID:    normalize(d.ProjectID),
+		ApiID:        normalize(d.ApiID),
+		DeploymentID: normalize(d.DeploymentID),
+	}
+}
+
+// Parent returns this resource's parent API resource name.
+func (d Deployment) Parent() string {
+	return d.Api().String()
+}
+
+func (d Deployment) String() string {
+	return normalize(fmt.Sprintf("projects/%s/locations/%s/apis/%s/deployments/%s",
+		d.ProjectID, Location, d.ApiID, d.DeploymentID))
+}
+
+// deploymentCollectionRegexp returns a regular expression that matches a collection of deployments.
+func deploymentCollectionRegexp() *regexp.Regexp {
+	return regexp.MustCompile(fmt.Sprintf("^projects/%s/locations/%s/apis/%s/deployments$",
+		identifier, Location, identifier))
+}
+
+// deploymentRegexp returns a regular expression that matches a deployment resource name.
+func deploymentRegexp() *regexp.Regexp {
+	return regexp.MustCompile(fmt.Sprintf("^projects/%s/locations/%s/apis/%s/deployments/%s$",
+		identifier, Location, identifier, identifier))
+}
+
+// ParseDeployment parses the name of a deployment.
+func ParseDeployment(name string) (Deployment, error) {
+	r := deploymentRegexp()
+	if !r.MatchString(name) {
+		return Deployment{}, fmt.Errorf("invalid deployment name %q: must match %q", name, r)
+	}
+
+	m := r.FindStringSubmatch(name)
+	return Deployment{
+		ProjectID:    m[1],
+		ApiID:        m[2],
+		DeploymentID: m[3],
+	}, nil
+}
+
+func ParseDeploymentCollection(name string) (Deployment, error) {
+	r := deploymentCollectionRegexp()
+	if !r.MatchString(name) {
+		return Deployment{}, fmt.Errorf("invalid deployment collection name %q: must match %q", name, r)
+	}
+
+	m := r.FindStringSubmatch(name)
+	return Deployment{
+		ProjectID:    m[1],
+		ApiID:        m[2],
+		DeploymentID: "",
+	}, nil
+}
diff --git a/server/registry/names/deployment_revision.go b/server/registry/names/deployment_revision.go
new file mode 100644
index 0000000..11720b8
--- /dev/null
+++ b/server/registry/names/deployment_revision.go
@@ -0,0 +1,61 @@
+// Copyright 2020 Google LLC. All Rights Reserved.
+//
+// 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 names
+
+import (
+	"fmt"
+	"regexp"
+)
+
+var deploymentRevisionRegexp = regexp.MustCompile(fmt.Sprintf("^projects/%s/locations/%s/apis/%s/deployments/%s@%s$", identifier, Location, identifier, identifier, revisionTag))
+
+// DeploymentRevision represents a resource name for an API deployment revision.
+type DeploymentRevision struct {
+	ProjectID    string
+	ApiID        string
+	DeploymentID string
+	RevisionID   string
+}
+
+// Deployment returns the parent deployment for this resource.
+func (s DeploymentRevision) Deployment() Deployment {
+	return Deployment{
+		ProjectID:    s.ProjectID,
+		ApiID:        s.ApiID,
+		DeploymentID: s.DeploymentID,
+	}
+}
+
+func (s DeploymentRevision) String() string {
+	return normalize(fmt.Sprintf("projects/%s/locations/%s/apis/%s/deployments/%s@%s",
+		s.ProjectID, Location, s.ApiID, s.DeploymentID, s.RevisionID))
+}
+
+// ParseDeploymentRevision parses the name of a deployment.
+func ParseDeploymentRevision(name string) (DeploymentRevision, error) {
+	if !deploymentRevisionRegexp.MatchString(name) {
+		return DeploymentRevision{}, fmt.Errorf("invalid deployment revision name %q: must match %q", name, deploymentRevisionRegexp)
+	}
+
+	m := deploymentRevisionRegexp.FindStringSubmatch(name)
+	revision := DeploymentRevision{
+		ProjectID:    m[1],
+		ApiID:        m[2],
+		DeploymentID: m[3],
+		RevisionID:   m[4],
+	}
+
+	return revision, nil
+}


Test patch:
diff --git a/server/registry/names/names_test.go b/server/registry/names/names_test.go
index 99a3fa3..7b52314 100644
--- a/server/registry/names/names_test.go
+++ b/server/registry/names/names_test.go
@@ -15,20 +15,22 @@
 package names
 
 import (
-	"regexp"
 	"testing"
 )
 
 func TestResourceNames(t *testing.T) {
 	groups := []struct {
-		name   string
-		regexp *regexp.Regexp
-		pass   []string
-		fail   []string
+		name  string
+		check func(name string) bool
+		pass  []string
+		fail  []string
 	}{
 		{
-			name:   "projects",
-			regexp: projectCollectionRegexp(),
+			name: "project collections",
+			check: func(name string) bool {
+				_, err := ParseProjectCollection(name)
+				return err == nil
+			},
 			pass: []string{
 				"projects",
 			},
@@ -40,8 +42,11 @@ func TestResourceNames(t *testing.T) {
 			},
 		},
 		{
-			name:   "project",
-			regexp: projectRegexp(),
+			name: "project",
+			check: func(name string) bool {
+				_, err := ParseProject(name)
+				return err == nil
+			},
 			pass: []string{
 				"projects/google",
 				"projects/-",
@@ -51,8 +56,11 @@ func TestResourceNames(t *testing.T) {
 			},
 		},
 		{
-			name:   "apis",
-			regexp: apiCollectionRegexp(),
+			name: "api collections",
+			check: func(name string) bool {
+				_, err := ParseApiCollection(name)
+				return err == nil
+			},
 			pass: []string{
 				"projects/google/locations/global/apis",
 				"projects/-/locations/global/apis",
@@ -62,8 +70,11 @@ func TestResourceNames(t *testing.T) {
 			},
 		},
 		{
-			name:   "api",
-			regexp: apiRegexp(),
+			name: "api",
+			check: func(name string) bool {
+				_, err := ParseApi(name)
+				return err == nil
+			},
 			pass: []string{
 				"projects/google/locations/global/apis/sample",
 				"projects/-/locations/global/apis/-",
@@ -80,8 +91,11 @@ func TestResourceNames(t *testing.T) {
 			},
 		},
 		{
-			name:   "versions",
-			regexp: versionCollectionRegexp(),
+			name: "version collections",
+			check: func(name string) bool {
+				_, err := ParseVersionCollection(name)
+				return err == nil
+			},
 			pass: []string{
 				"projects/google/locations/global/apis/sample/versions",
 				"projects/-/locations/global/apis/-/versions",
@@ -91,8 +105,11 @@ func TestResourceNames(t *testing.T) {
 			},
 		},
 		{
-			name:   "version",
-			regexp: versionRegexp(),
+			name: "version",
+			check: func(name string) bool {
+				_, err := ParseVersion(name)
+				return err == nil
+			},
 			pass: []string{
 				"projects/google/locations/global/apis/sample/versions/v1",
 				"projects/-/locations/global/apis/-/versions/-",
@@ -109,8 +126,11 @@ func TestResourceNames(t *testing.T) {
 			},
 		},
 		{
-			name:   "specs",
-			regexp: specCollectionRegexp(),
+			name: "spec collections",
+			check: func(name string) bool {
+				_, err := ParseSpecCollection(name)
+				return err == nil
+			},
 			pass: []string{
 				"projects/google/locations/global/apis/sample/versions/v1/specs",
 				"projects/-/locations/global/apis/-/versions/-/specs",
@@ -120,15 +140,74 @@ func TestResourceNames(t *testing.T) {
 			},
 		},
 		{
-			name:   "spec",
-			regexp: specRegexp(),
+			name: "spec",
+			check: func(name string) bool {
+				_, err := ParseSpec(name)
+				return err == nil
+			},
 			pass: []string{
+				"projects/google/locations/global/apis/sample/versions/v1/specs/openapi.yaml",
+				"projects/-/locations/global/apis/-/versions/-/specs/-",
+				"projects/123/locations/global/apis/abc/versions/123/specs/abc",
+				"projects/1-2-3/locations/global/apis/abc/versions/123/specs/abc",
+			},
+			fail: []string{
+				"-",
+				"invalid",
+				"projects//locations/global/apis/123",
+				"projects/123/locations/global/apis/",
+				"projects/123/locations/global/invalid/123",
+				"projects/123/locations/global/apis/ 123",
 				"projects/google/locations/global/apis/sample/versions/v1/specs/openapi.yaml@1234567890abcdef",
+			},
+		},
+		{
+			name: "spec revision",
+			check: func(name string) bool {
+				_, err := ParseSpecRevision(name)
+				return err == nil
+			},
+			pass: []string{
+				"projects/google/locations/global/apis/sample/versions/v1/specs/openapi.yaml@1234567890abcdef",
+			},
+			fail: []string{
+				"-",
+				"invalid",
+				"projects//locations/global/apis/123",
+				"projects/123/locations/global/apis/",
+				"projects/123/locations/global/invalid/123",
+				"projects/123/locations/global/apis/ 123",
 				"projects/google/locations/global/apis/sample/versions/v1/specs/openapi.yaml",
 				"projects/-/locations/global/apis/-/versions/-/specs/-",
 				"projects/123/locations/global/apis/abc/versions/123/specs/abc",
 				"projects/1-2-3/locations/global/apis/abc/versions/123/specs/abc",
 			},
+		},
+		{
+			name: "deployment collections",
+			check: func(name string) bool {
+				_, err := ParseDeploymentCollection(name)
+				return err == nil
+			},
+			pass: []string{
+				"projects/google/locations/global/apis/sample/deployments",
+				"projects/-/locations/global/apis/-/deployments",
+			},
+			fail: []string{
+				"-",
+			},
+		},
+		{
+			name: "deployment",
+			check: func(name string) bool {
+				_, err := ParseDeployment(name)
+				return err == nil
+			}, pass: []string{
+				"projects/google/locations/global/apis/sample/deployments/v1",
+				"projects/-/locations/global/apis/-/deployments/-",
+				"projects/123/locations/global/apis/abc/deployments/123",
+				"projects/1-2-3/locations/global/apis/abc/deployments/123",
+			},
 			fail: []string{
 				"-",
 				"invalid",
@@ -136,29 +215,59 @@ func TestResourceNames(t *testing.T) {
 				"projects/123/locations/global/apis/",
 				"projects/123/locations/global/invalid/123",
 				"projects/123/locations/global/apis/ 123",
+				"projects/google/locations/global/apis/sample/deployments/v1@1234567890abcdef",
 			},
 		},
 		{
-			name:   "artifacts",
-			regexp: artifactCollectionRegexp(),
+			name: "deployment revision",
+			check: func(name string) bool {
+				_, err := ParseDeploymentRevision(name)
+				return err == nil
+			}, pass: []string{
+				"projects/google/locations/global/apis/sample/deployments/v1@1234567890abcdef",
+			},
+			fail: []string{
+				"-",
+				"invalid",
+				"projects//locations/global/apis/123",
+				"projects/123/locations/global/apis/",
+				"projects/123/locations/global/invalid/123",
+				"projects/123/locations/global/apis/ 123",
+				"projects/google/locations/global/apis/sample/deployments/v1",
+				"projects/-/locations/global/apis/-/deployments/-",
+				"projects/123/locations/global/apis/abc/deployments/123",
+				"projects/1-2-3/locations/global/apis/abc/deployments/123",
+			},
+		},
+		{
+			name: "artifact collections",
+			check: func(name string) bool {
+				_, err := ParseArtifactCollection(name)
+				return err == nil
+			},
 			pass: []string{
-				"projects/google/locations/global/apis/sample/versions/v1/specs/openapi.yaml/artifacts",
-				"projects/google/locations/global/apis/sample/versions/v1/artifacts",
-				"projects/google/locations/global/apis/sample/artifacts",
 				"projects/google/locations/global/artifacts",
+				"projects/google/locations/global/apis/sample/artifacts",
+				"projects/google/locations/global/apis/sample/versions/v1/artifacts",
+				"projects/google/locations/global/apis/sample/versions/v1/specs/openapi.yaml/artifacts",
+				"projects/google/locations/global/apis/sample/deployments/prod/artifacts",
 			},
 			fail: []string{
 				"-",
 			},
 		},
 		{
-			name:   "artifact",
-			regexp: artifactRegexp(),
+			name: "artifact",
+			check: func(name string) bool {
+				_, err := ParseArtifact(name)
+				return err == nil
+			},
 			pass: []string{
-				"projects/google/locations/global/apis/sample/versions/v1/specs/openapi.yaml/artifacts/test-artifact",
-				"projects/google/locations/global/apis/sample/versions/v1/artifacts/test-artifact",
-				"projects/google/locations/global/apis/sample/artifacts/test-artifact",
 				"projects/google/locations/global/artifacts/test-artifact",
+				"projects/google/locations/global/apis/sample/artifacts/test-artifact",
+				"projects/google/locations/global/apis/sample/versions/v1/artifacts/test-artifact",
+				"projects/google/locations/global/apis/sample/versions/v1/specs/openapi.yaml/artifacts/test-artifact",
+				"projects/google/locations/global/apis/sample/deployments/prod/artifacts/test-artifact",
 			},
 			fail: []string{
 				"-",
@@ -167,14 +276,12 @@ func TestResourceNames(t *testing.T) {
 	}
 	for _, g := range groups {
 		for _, path := range g.pass {
-			m := g.regexp.FindStringSubmatch(path)
-			if m == nil {
+			if !g.check(path) {
 				t.Errorf("failed to match %s: %s", g.name, path)
 			}
 		}
 		for _, path := range g.fail {
-			m := g.regexp.FindStringSubmatch(path)
-			if m != nil {
+			if g.check(path) {
 				t.Errorf("false match %s: %s", g.name, path)
 			}
 		}