Repository: apigee/registry
Pull request #793 (2022-10-20 20:52:42)

registry apply YAML import of deployments, versions, specs, and non-project artifacts
Addresses #771. See the description there for desired behavior. Notably, this adds a new `parent` field to `metadata` sections to specifies the parent resource of resources described in YAML. For versions this is `api/{api_id}`. Note that the containing project and location are omitted from `metadata.parent` values. This allows YAML exports to be portable across projects.

In the future, we might want to support other variations in the `parent` field, such as containing the full project-rooted resource name (although I personally think this should be discouraged) or having abbreviated forms like `{api_id}/{version_id}` instead of `apis/{api_id}/versions/{version_id}` (I think this also has problems: ambiguity with deployments and increased inconsistency with the Registry API.)

It could be argued that omitting `project/{project_id}/location/{location_id}` from `metadata.parent` values is also inconsistent with the API, but I think the benefits of portable resource YAMLs justifies that.

This allows bulk application of directories of YAML files and to ensure that all parents are created before their children, resources are all read before application and are applied in a specific order: first APIs, then versions, specs, deployments, and finally artifacts.

Two new tests upload the same project that is described in the `samples` directory but as "unbundled" files that are stored in two different directory organizations: one flat, one hierarchical.

codecov[bot]: # [Codecov](https://codecov.io/gh/apigee/registry/pull/793?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apigee) Report
> Merging [#793](https://codecov.io/gh/apigee/registry/pull/793?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apigee) (11d904f) into [main](https://codecov.io/gh/apigee/registry/commit/400e49ee25f42d7900f5938b652c7a91fc8b09ed?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apigee) (400e49e) will **not change** coverage.
> The diff coverage is `n/a`.

```diff
@@           Coverage Diff           @@
##             main     #793   +/-   ##
=======================================
  Coverage   52.09%   52.09%           
=======================================
  Files          97       97           
  Lines        8537     8537           
=======================================
  Hits         4447     4447           
  Misses       3659     3659           
  Partials      431      431           
```



:mega: We’re building smart automated test selection to slash your CI/CD build times. [Learn more](https://about.codecov.io/iterative-testing/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apigee)


Patch:
diff --git a/cmd/registry/cmd/apply/apply.go b/cmd/registry/cmd/apply/apply.go
index 7d46846..1d3c7f0 100644
--- a/cmd/registry/cmd/apply/apply.go
+++ b/cmd/registry/cmd/apply/apply.go
@@ -18,7 +18,6 @@ import (
 	"errors"
 	"io/fs"
 
-	"github.com/apigee/registry/cmd/registry/core"
 	"github.com/apigee/registry/cmd/registry/patch"
 	"github.com/apigee/registry/log"
 	"github.com/apigee/registry/pkg/connection"
@@ -50,9 +49,7 @@ func Command() *cobra.Command {
 			if err != nil {
 				log.FromContext(ctx).WithError(err).Fatal("Failed to get client")
 			}
-			taskQueue, wait := core.WorkerPool(ctx, jobs)
-			defer wait()
-			err = patch.Apply(ctx, client, fileName, parent, recursive, taskQueue)
+			err = patch.Apply(ctx, client, fileName, parent, recursive, jobs)
 			if errors.Is(err, fs.ErrNotExist) {
 				log.FromContext(ctx).WithError(err).Fatalf("File %q doesn't exist", fileName)
 			} else if err != nil {
diff --git a/cmd/registry/patch/api.go b/cmd/registry/patch/api.go
index e1fe94f..1906a54 100644
--- a/cmd/registry/patch/api.go
+++ b/cmd/registry/patch/api.go
@@ -185,7 +185,7 @@ func optionalDeploymentName(apiName names.Api, deploymentID string) string {
 	return apiName.Deployment(deploymentID).String()
 }
 
-func applyApiPatch(ctx context.Context, client connection.RegistryClient, bytes []byte, parent string) error {
+func applyApiPatchBytes(ctx context.Context, client connection.RegistryClient, bytes []byte, parent string) error {
 	var api models.Api
 	err := yaml.Unmarshal(bytes, &api)
 	if err != nil {
diff --git a/cmd/registry/patch/apply.go b/cmd/registry/patch/apply.go
index 2735e39..4d85780 100644
--- a/cmd/registry/patch/apply.go
+++ b/cmd/registry/patch/apply.go
@@ -26,29 +26,81 @@ import (
 	"github.com/apigee/registry/pkg/connection"
 )
 
-func Apply(ctx context.Context, client connection.RegistryClient, path, parent string, recursive bool, taskQueue chan<- core.Task) error {
-	return filepath.WalkDir(path,
-		func(p string, entry fs.DirEntry, err error) error {
+func Apply(ctx context.Context, client connection.RegistryClient, path, parent string, recursive bool, jobs int) error {
+	patches := &patchGroup{}
+	err := filepath.WalkDir(path,
+		func(fileName string, entry fs.DirEntry, err error) error {
 			if err != nil {
 				return err
-			} else if entry.IsDir() && p != path && !recursive {
+			} else if entry.IsDir() && fileName != path && !recursive {
 				return filepath.SkipDir // Skip the directory and contents.
 			} else if entry.IsDir() {
 				return nil // Do nothing for the directory, but still walk its contents.
+			} else if !strings.HasSuffix(fileName, ".yaml") {
+				return nil // Skip everything that's not a YAML file.
 			}
-			return applyFile(ctx, client, p, parent, taskQueue)
+			return patches.add(&applyFileTask{
+				client: client,
+				path:   fileName,
+				parent: parent,
+			})
 		})
+	if err != nil {
+		return err
+	}
+	return patches.run(ctx, jobs)
 }
 
-func applyFile(ctx context.Context, client connection.RegistryClient, fileName, parent string, taskQueue chan<- core.Task) error {
-	if !strings.HasSuffix(fileName, ".yaml") {
-		return nil
+type patchGroup struct {
+	apiTasks        []core.Task
+	versionTasks    []core.Task
+	specTasks       []core.Task
+	deploymentTasks []core.Task
+	artifactTasks   []core.Task
+}
+
+func (p *patchGroup) add(task *applyFileTask) error {
+	bytes, err := os.ReadFile(task.path)
+	if err != nil {
+		return err
+	}
+	header, err := readHeader(bytes)
+	if err != nil {
+		return err
 	}
-	// Create an upload job for each API.
-	taskQueue <- &applyFileTask{
-		client: client,
-		path:   fileName,
-		parent: parent,
+	task.kind = header.Kind
+	if header.Metadata.Parent != "" {
+		task.parent = task.parent + "/" + header.Metadata.Parent
+	}
+	switch task.kind {
+	case "API":
+		p.apiTasks = append(p.apiTasks, task)
+	case "Version":
+		p.versionTasks = append(p.versionTasks, task)
+	case "Spec":
+		p.specTasks = append(p.specTasks, task)
+	case "Deployment":
+		p.deploymentTasks = append(p.deploymentTasks, task)
+	default: // for everything else, try an artifact type
+		p.artifactTasks = append(p.artifactTasks, task)
+	}
+	return nil
+}
+
+func (p *patchGroup) run(ctx context.Context, jobs int) error {
+	// Apply each resource type independently in order of ownership (parents first).
+	for _, tasks := range [][]core.Task{
+		p.apiTasks,
+		p.versionTasks,
+		p.specTasks,
+		p.deploymentTasks,
+		p.artifactTasks,
+	} {
+		taskQueue, wait := core.WorkerPool(ctx, jobs)
+		for _, task := range tasks {
+			taskQueue <- task
+		}
+		wait()
 	}
 	return nil
 }
@@ -57,6 +109,7 @@ type applyFileTask struct {
 	client connection.RegistryClient
 	path   string
 	parent string
+	kind   string
 }
 
 func (task *applyFileTask) String() string {
@@ -69,13 +122,15 @@ func (task *applyFileTask) Run(ctx context.Context) error {
 	if err != nil {
 		return err
 	}
-	header, err := readHeader(bytes)
-	if err != nil {
-		return err
-	}
-	switch header.Kind {
+	switch task.kind {
 	case "API":
-		return applyApiPatch(ctx, task.client, bytes, task.parent)
+		return applyApiPatchBytes(ctx, task.client, bytes, task.parent)
+	case "Version":
+		return applyApiVersionPatchBytes(ctx, task.client, bytes, task.parent)
+	case "Spec":
+		return applyApiSpecPatchBytes(ctx, task.client, bytes, task.parent)
+	case "Deployment":
+		return applyApiDeploymentPatchBytes(ctx, task.client, bytes, task.parent)
 	default: // for everything else, try an artifact type
 		return applyArtifactPatchBytes(ctx, task.client, bytes, task.parent)
 	}
diff --git a/cmd/registry/patch/artifact.go b/cmd/registry/patch/artifact.go
index 54b8d17..63a30f2 100644
--- a/cmd/registry/patch/artifact.go
+++ b/cmd/registry/patch/artifact.go
@@ -161,6 +161,10 @@ func applyArtifactPatchBytes(ctx context.Context, client connection.RegistryClie
 	return applyArtifactPatch(ctx, client, &artifact, parent)
 }
 
+func artifactName(parent, artifactID string) (names.Artifact, error) {
+	return names.ParseArtifact(parent + "/artifacts/" + artifactID)
+}
+
 func applyArtifactPatch(ctx context.Context, client connection.RegistryClient, content *models.Artifact, parent string) error {
 	// Restyle the YAML representation so that yaml.Marshal will marshal it as JSON.
 	styleForJSON(&content.Data)
@@ -189,14 +193,18 @@ func applyArtifactPatch(ctx context.Context, client connection.RegistryClient, c
 	if err != nil {
 		return err
 	}
+	name, err := artifactName(parent, content.Header.Metadata.Name)
+	if err != nil {
+		return err
+	}
 	artifact := &rpc.Artifact{
-		Name:     fmt.Sprintf("%s/artifacts/%s", parent, content.Header.Metadata.Name),
+		Name:     name.String(),
 		MimeType: MimeTypeForKind(content.Kind),
 		Contents: bytes,
 	}
 	req := &rpc.CreateArtifactRequest{
-		Parent:     parent,
-		ArtifactId: content.Header.Metadata.Name,
+		Parent:     name.Parent(),
+		ArtifactId: name.ArtifactID(),
 		Artifact:   artifact,
 	}
 	_, err = client.CreateArtifact(ctx, req)
diff --git a/cmd/registry/patch/deployment.go b/cmd/registry/patch/deployment.go
index 64c2cc7..603d1e9 100644
--- a/cmd/registry/patch/deployment.go
+++ b/cmd/registry/patch/deployment.go
@@ -22,6 +22,7 @@ import (
 	"github.com/apigee/registry/pkg/models"
 	"github.com/apigee/registry/rpc"
 	"github.com/apigee/registry/server/registry/names"
+	"gopkg.in/yaml.v3"
 )
 
 // relativeSpecRevisionName returns the versionid+specid if the spec is within the specified API
@@ -71,19 +72,35 @@ func newApiDeployment(message *rpc.ApiDeployment) (*models.ApiDeployment, error)
 	}, nil
 }
 
+func applyApiDeploymentPatchBytes(ctx context.Context, client connection.RegistryClient, bytes []byte, parent string) error {
+	var deployment models.ApiDeployment
+	err := yaml.Unmarshal(bytes, &deployment)
+	if err != nil {
+		return err
+	}
+	return applyApiDeploymentPatch(ctx, client, &deployment, parent)
+}
+
+func deploymentName(parent, deploymentID string) (names.Deployment, error) {
+	api, err := names.ParseApi(parent)
+	if err != nil {
+		return names.Deployment{}, err
+	}
+	return api.Deployment(deploymentID), nil
+}
+
 func applyApiDeploymentPatch(
 	ctx context.Context,
 	client connection.RegistryClient,
 	deployment *models.ApiDeployment,
 	parent string) error {
-	apiName, err := names.ParseApi(parent)
+	name, err := deploymentName(parent, deployment.Metadata.Name)
 	if err != nil {
 		return err
 	}
-	deploymentName := apiName.Deployment(deployment.Metadata.Name)
 	req := &rpc.UpdateApiDeploymentRequest{
 		ApiDeployment: &rpc.ApiDeployment{
-			Name:               deploymentName.String(),
+			Name:               name.String(),
 			DisplayName:        deployment.Data.DisplayName,
 			Description:        deployment.Data.Description,
 			EndpointUri:        deployment.Data.EndpointURI,
@@ -95,7 +112,7 @@ func applyApiDeploymentPatch(
 		},
 		AllowMissing: true,
 	}
-	req.ApiDeployment.ApiSpecRevision = optionalSpecRevisionName(deploymentName, deployment.Data.ApiSpecRevision)
+	req.ApiDeployment.ApiSpecRevision = optionalSpecRevisionName(name, deployment.Data.ApiSpecRevision)
 	if err != nil {
 		return err
 	}
diff --git a/cmd/registry/patch/spec.go b/cmd/registry/patch/spec.go
index 20d1d24..808efe5 100644
--- a/cmd/registry/patch/spec.go
+++ b/cmd/registry/patch/spec.go
@@ -16,7 +16,6 @@ package patch
 
 import (
 	"context"
-	"fmt"
 	"io"
 	"net/http"
 	"net/url"
@@ -29,6 +28,7 @@ import (
 	"github.com/apigee/registry/pkg/models"
 	"github.com/apigee/registry/rpc"
 	"github.com/apigee/registry/server/registry/names"
+	"gopkg.in/yaml.v3"
 )
 
 func newApiSpec(message *rpc.ApiSpec) (*models.ApiSpec, error) {
@@ -55,15 +55,39 @@ func newApiSpec(message *rpc.ApiSpec) (*models.ApiSpec, error) {
 	}, nil
 }
 
+func applyApiSpecPatchBytes(
+	ctx context.Context,
+	client connection.RegistryClient,
+	bytes []byte,
+	parent string) error {
+	var spec models.ApiSpec
+	err := yaml.Unmarshal(bytes, &spec)
+	if err != nil {
+		return err
+	}
+	return applyApiSpecPatch(ctx, client, &spec, parent)
+}
+
+func specName(parent, specID string) (names.Spec, error) {
+	version, err := names.ParseVersion(parent)
+	if err != nil {
+		return names.Spec{}, err
+	}
+	return version.Spec(specID), nil
+}
+
 func applyApiSpecPatch(
 	ctx context.Context,
 	client connection.RegistryClient,
 	spec *models.ApiSpec,
 	parent string) error {
-	name := fmt.Sprintf("%s/specs/%s", parent, spec.Metadata.Name)
+	name, err := specName(parent, spec.Metadata.Name)
+	if err != nil {
+		return err
+	}
 	req := &rpc.UpdateApiSpecRequest{
 		ApiSpec: &rpc.ApiSpec{
-			Name:        name,
+			Name:        name.String(),
 			Filename:    spec.Data.FileName,
 			Description: spec.Data.Description,
 			MimeType:    spec.Data.MimeType,
@@ -134,6 +158,6 @@ func applyApiSpecPatch(
 			}
 		}
 	}
-	_, err := client.UpdateApiSpec(ctx, req)
+	_, err = client.UpdateApiSpec(ctx, req)
 	return err
 }
diff --git a/cmd/registry/patch/version.go b/cmd/registry/patch/version.go
index 804ad3f..97dd8bf 100644
--- a/cmd/registry/patch/version.go
+++ b/cmd/registry/patch/version.go
@@ -16,7 +16,6 @@ package patch
 
 import (
 	"context"
-	"fmt"
 
 	"github.com/apigee/registry/cmd/registry/core"
 	"github.com/apigee/registry/gapic"
@@ -24,6 +23,7 @@ import (
 	"github.com/apigee/registry/pkg/models"
 	"github.com/apigee/registry/rpc"
 	"github.com/apigee/registry/server/registry/names"
+	"gopkg.in/yaml.v3"
 )
 
 func newApiVersion(ctx context.Context, client *gapic.RegistryClient, message *rpc.ApiVersion) (*models.ApiVersion, error) {
@@ -66,15 +66,39 @@ func newApiVersion(ctx context.Context, client *gapic.RegistryClient, message *r
 	}, nil
 }
 
+func applyApiVersionPatchBytes(
+	ctx context.Context,
+	client connection.RegistryClient,
+	bytes []byte,
+	parent string) error {
+	var version models.ApiVersion
+	err := yaml.Unmarshal(bytes, &version)
+	if err != nil {
+		return err
+	}
+	return applyApiVersionPatch(ctx, client, &version, parent)
+}
+
+func versionName(parent, versionID string) (names.Version, error) {
+	api, err := names.ParseApi(parent)
+	if err != nil {
+		return names.Version{}, err
+	}
+	return api.Version(versionID), nil
+}
+
 func applyApiVersionPatch(
 	ctx context.Context,
 	client connection.RegistryClient,
 	version *models.ApiVersion,
 	parent string) error {
-	name := fmt.Sprintf("%s/versions/%s", parent, version.Metadata.Name)
+	name, err := versionName(parent, version.Metadata.Name)
+	if err != nil {
+		return err
+	}
 	req := &rpc.UpdateApiVersionRequest{
 		ApiVersion: &rpc.ApiVersion{
-			Name:        name,
+			Name:        name.String(),
 			DisplayName: version.Data.DisplayName,
 			Description: version.Data.Description,
 			State:       version.Data.State,
@@ -83,12 +107,12 @@ func applyApiVersionPatch(
 		},
 		AllowMissing: true,
 	}
-	_, err := client.UpdateApiVersion(ctx, req)
+	_, err = client.UpdateApiVersion(ctx, req)
 	if err != nil {
 		return err
 	}
 	for _, specPatch := range version.Data.ApiSpecs {
-		err := applyApiSpecPatch(ctx, client, specPatch, name)
+		err := applyApiSpecPatch(ctx, client, specPatch, name.String())
 		if err != nil {
 			return err
 		}
diff --git a/pkg/models/header.go b/pkg/models/header.go
index 8bcbdcf..a683b8c 100644
--- a/pkg/models/header.go
+++ b/pkg/models/header.go
@@ -22,6 +22,7 @@ type Header struct {
 
 type Metadata struct {
 	Name        string            `yaml:"name"`
+	Parent      string            `yaml:"parent,omitempty"`
 	Labels      map[string]string `yaml:"labels,omitempty"`
 	Annotations map[string]string `yaml:"annotations,omitempty"`
 }


Test patch:
diff --git a/cmd/registry/cmd/apply/apply_test.go b/cmd/registry/cmd/apply/apply_test.go
index 0247c7a..7394376 100644
--- a/cmd/registry/cmd/apply/apply_test.go
+++ b/cmd/registry/cmd/apply/apply_test.go
@@ -151,101 +151,123 @@ func TestApply(t *testing.T) {
 }
 
 func TestApplyProject(t *testing.T) {
-	ctx := context.Background()
-	adminClient, err := connection.NewAdminClient(ctx)
-	if err != nil {
-		t.Fatalf("Setup: failed to create client: %+v", err)
-	}
-	defer adminClient.Close()
-
-	project := names.Project{ProjectID: "apply-project-test"}
-	if err = adminClient.DeleteProject(ctx, &rpc.DeleteProjectRequest{
-		Name:  project.String(),
-		Force: true,
-	}); err != nil && status.Code(err) != codes.NotFound {
-		t.Errorf("Setup: failed to delete test project: %s", err)
-	}
-
-	if _, err := adminClient.CreateProject(ctx, &rpc.CreateProjectRequest{
-		ProjectId: project.ProjectID,
-		Project:   &rpc.Project{},
-	}); err != nil {
-		t.Fatalf("Setup: Failed to create test project: %s", err)
-	}
-
-	// set the configured registry.project to the test project
-	config, err := connection.ActiveConfig()
-	if err != nil {
-		t.Fatalf("Setup: Failed to get registry configuration: %s", err)
-	}
-	config.Project = project.ProjectID
-	connection.SetConfig(config)
-
-	registryClient, err := connection.NewRegistryClient(ctx)
-	if err != nil {
-		t.Fatalf("Setup: Failed to create registry client: %s", err)
-	}
-	defer registryClient.Close()
-
-	cmd := Command()
-	cmd.SetArgs([]string{"-f", sampleDir, "-R"})
-	if err := cmd.Execute(); err != nil {
-		t.Fatalf("Execute() with args %+v returned error: %s", cmd.Args, err)
-	}
-
-	filename := sampleDir + "/apis/registry.yaml"
-	expected, err := os.ReadFile(filename)
-	if err != nil {
-		t.Fatalf("Failed to read API YAML: %s", err)
-	}
-
-	got, err := registryClient.GetApi(ctx, &rpc.GetApiRequest{
-		Name: project.Api("registry").String(),
-	})
-	if err != nil {
-		t.Fatalf("failed to get api: %s", err)
+	// Each of these three imports should import an identical project that can be exported
+	// into the structures in "sampleDir".
+	tests := []struct {
+		desc string
+		root string
+	}{
+		{
+			desc: "bundled",
+			root: sampleDir,
+		},
+		{
+			desc: "unbundled-hierarchical",
+			root: "testdata/unbundled-hierarchical",
+		},
+		{
+			desc: "unbundled-flat",
+			root: "testdata/unbundled-flat",
+		},
 	}
-
-	actual, _, err := patch.ExportAPI(ctx, registryClient, got)
-	if err != nil {
-		t.Fatalf("ExportApi(%+v) returned an error: %s", got, err)
-	}
-
-	if diff := cmp.Diff(expected, actual); diff != "" {
-		t.Errorf("GetApi(%q) returned unexpected diff: (-want +got):\n%s", got, diff)
-	}
-
-	artifacts := []string{"lifecycle", "manifest", "taxonomies"}
-	for _, a := range artifacts {
-		filename := fmt.Sprintf("%s/artifacts/%s.yaml", sampleDir, a)
-		expected, err := os.ReadFile(filename)
-		if err != nil {
-			t.Fatalf("Failed to read artifact YAML %s", err)
-		}
-
-		message, err := registryClient.GetArtifact(ctx, &rpc.GetArtifactRequest{
-			Name: project.Artifact(a).String(),
+	for _, test := range tests {
+		t.Run(test.desc, func(t *testing.T) {
+			ctx := context.Background()
+			adminClient, err := connection.NewAdminClient(ctx)
+			if err != nil {
+				t.Fatalf("Setup: failed to create client: %+v", err)
+			}
+			defer adminClient.Close()
+			project := names.Project{ProjectID: "apply-project-test"}
+			if err = adminClient.DeleteProject(ctx, &rpc.DeleteProjectRequest{
+				Name:  project.String(),
+				Force: true,
+			}); err != nil && status.Code(err) != codes.NotFound {
+				t.Errorf("Setup: failed to delete test project: %s", err)
+			}
+
+			if _, err := adminClient.CreateProject(ctx, &rpc.CreateProjectRequest{
+				ProjectId: project.ProjectID,
+				Project:   &rpc.Project{},
+			}); err != nil {
+				t.Fatalf("Setup: Failed to create test project: %s", err)
+			}
+
+			// set the configured registry.project to the test project
+			config, err := connection.ActiveConfig()
+			if err != nil {
+				t.Fatalf("Setup: Failed to get registry configuration: %s", err)
+			}
+			config.Project = project.ProjectID
+			connection.SetConfig(config)
+
+			registryClient, err := connection.NewRegistryClient(ctx)
+			if err != nil {
+				t.Fatalf("Setup: Failed to create registry client: %s", err)
+			}
+			defer registryClient.Close()
+
+			cmd := Command()
+			cmd.SetArgs([]string{"-f", test.root, "-R"})
+			if err := cmd.Execute(); err != nil {
+				t.Fatalf("Execute() with args %+v returned error: %s", cmd.Args, err)
+			}
+
+			filename := sampleDir + "/apis/registry.yaml"
+			expected, err := os.ReadFile(filename)
+			if err != nil {
+				t.Fatalf("Failed to read API YAML: %s", err)
+			}
+
+			got, err := registryClient.GetApi(ctx, &rpc.GetApiRequest{
+				Name: project.Api("registry").String(),
+			})
+			if err != nil {
+				t.Fatalf("Failed to get API: %s", err)
+			}
+
+			actual, _, err := patch.ExportAPI(ctx, registryClient, got)
+			if err != nil {
+				t.Fatalf("ExportApi(%+v) returned an error: %s", got, err)
+			}
+
+			if diff := cmp.Diff(expected, actual); diff != "" {
+				t.Errorf("GetApi(%q) returned unexpected diff: (-want +got):\n%s", got, diff)
+			}
+
+			artifacts := []string{"lifecycle", "manifest", "taxonomies"}
+			for _, a := range artifacts {
+				filename := fmt.Sprintf("%s/artifacts/%s.yaml", sampleDir, a)
+				expected, err := os.ReadFile(filename)
+				if err != nil {
+					t.Fatalf("Failed to read artifact YAML %s", err)
+				}
+
+				message, err := registryClient.GetArtifact(ctx, &rpc.GetArtifactRequest{
+					Name: project.Artifact(a).String(),
+				})
+				if status.Code(err) == codes.NotFound {
+					t.Fatalf("Expected artifact doesn't exist: %s", err)
+				} else if err != nil {
+					t.Fatalf("Failed to verify artifact existence: %s", err)
+				}
+
+				actual, _, err := patch.ExportArtifact(ctx, registryClient, message)
+				if err != nil {
+					t.Fatalf("ExportArtifact(%+v) returned an error: %s", message, err)
+				}
+
+				if diff := cmp.Diff(expected, actual); diff != "" {
+					t.Errorf("GetArtifact(%q) returned unexpected diff: (-want +got):\n%s", message, diff)
+				}
+			}
+
+			if err := adminClient.DeleteProject(ctx, &rpc.DeleteProjectRequest{
+				Name:  project.String(),
+				Force: true,
+			}); err != nil {
+				t.Logf("Cleanup: Failed to delete test project: %s", err)
+			}
 		})
-		if status.Code(err) == codes.NotFound {
-			t.Fatalf("Expected artifact doesn't exist: %s", err)
-		} else if err != nil {
-			t.Fatalf("Failed to verify artifact existence: %s", err)
-		}
-
-		actual, _, err := patch.ExportArtifact(ctx, registryClient, message)
-		if err != nil {
-			t.Fatalf("ExportArtifact(%+v) returned an error: %s", message, err)
-		}
-
-		if diff := cmp.Diff(expected, actual); diff != "" {
-			t.Errorf("GetArtifact(%q) returned unexpected diff: (-want +got):\n%s", message, diff)
-		}
-	}
-
-	if err := adminClient.DeleteProject(ctx, &rpc.DeleteProjectRequest{
-		Name:  project.String(),
-		Force: true,
-	}); err != nil {
-		t.Logf("Cleanup: Failed to delete test project: %s", err)
 	}
 }
diff --git a/cmd/registry/cmd/apply/testdata/unbundled-flat/apis-registry-artifacts-apihub-related.yaml b/cmd/registry/cmd/apply/testdata/unbundled-flat/apis-registry-artifacts-apihub-related.yaml
new file mode 100644
index 0000000..756222d
--- /dev/null
+++ b/cmd/registry/cmd/apply/testdata/unbundled-flat/apis-registry-artifacts-apihub-related.yaml
@@ -0,0 +1,19 @@
+apiVersion: apigeeregistry/v1
+kind: ReferenceList
+metadata:
+  name: apihub-related
+  parent: apis/registry
+data:
+  displayName: ""
+  description: Defines a list of related resources
+  references:
+    - id: github
+      displayName: GitHub Repo
+      category: apihub-source-code
+      resource: ""
+      uri: https://github.com/apigee/registry
+    - id: docs
+      displayName: GitHub Documentation
+      category: apihub-other
+      resource: ""
+      uri: https://apigee.github.io/registry/
\ No newline at end of file
diff --git a/cmd/registry/cmd/apply/testdata/unbundled-flat/apis-registry-deployments-prod.yaml b/cmd/registry/cmd/apply/testdata/unbundled-flat/apis-registry-deployments-prod.yaml
new file mode 100644
index 0000000..8fed69c
--- /dev/null
+++ b/cmd/registry/cmd/apply/testdata/unbundled-flat/apis-registry-deployments-prod.yaml
@@ -0,0 +1,18 @@
+apiVersion: apigeeregistry/v1
+kind: Deployment
+metadata:
+  name: prod
+  parent: apis/registry
+  labels:
+    platform: google
+  annotations:
+    region: us-central1
+data:
+  displayName: Production
+  description: The hosted deployment of the Registry API
+  apiSpecRevision: v1/specs/openapi.yaml@latest
+  endpointURI: https://apigeeregistry.googleapis.com
+  externalChannelURI: https://apigee.github.io/registry/
+  intendedAudience: Public
+  accessGuidance: See https://github.com/apigee/registry for tools and usage information.
+  
\ No newline at end of file
diff --git a/cmd/registry/cmd/apply/testdata/unbundled-flat/apis-registry-versions-v1-specs-openapi.yaml b/cmd/registry/cmd/apply/testdata/unbundled-flat/apis-registry-versions-v1-specs-openapi.yaml
new file mode 100644
index 0000000..c578806
--- /dev/null
+++ b/cmd/registry/cmd/apply/testdata/unbundled-flat/apis-registry-versions-v1-specs-openapi.yaml
@@ -0,0 +1,14 @@
+apiVersion: apigeeregistry/v1
+kind: Spec
+metadata:
+  name: openapi.yaml
+  parent: apis/registry/versions/v1
+  labels:
+    openapi-verified: "true"
+  annotations:
+    linters: spectral,gnostic
+data:
+  filename: openapi.yaml
+  description: OpenAPI description of the Registry API
+  mimeType: application/x.openapi+gzip;version=3
+  sourceURI: https://raw.githubusercontent.com/apigee/registry/main/openapi.yaml
\ No newline at end of file
diff --git a/cmd/registry/cmd/apply/testdata/unbundled-flat/apis-registry-versions-v1.yaml b/cmd/registry/cmd/apply/testdata/unbundled-flat/apis-registry-versions-v1.yaml
new file mode 100644
index 0000000..37e7d8d
--- /dev/null
+++ b/cmd/registry/cmd/apply/testdata/unbundled-flat/apis-registry-versions-v1.yaml
@@ -0,0 +1,14 @@
+apiVersion: apigeeregistry/v1
+kind: Version
+metadata:
+  name: v1
+  parent: apis/registry
+  labels:
+    apihub-team: apigee
+  annotations:
+    release-date: "2021-12-15"
+data:
+  displayName: v1
+  description: New in 2022
+  state: Staging
+        
\ No newline at end of file
diff --git a/cmd/registry/cmd/apply/testdata/unbundled-flat/apis-registry.yaml b/cmd/registry/cmd/apply/testdata/unbundled-flat/apis-registry.yaml
new file mode 100644
index 0000000..09b594f
--- /dev/null
+++ b/cmd/registry/cmd/apply/testdata/unbundled-flat/apis-registry.yaml
@@ -0,0 +1,14 @@
+apiVersion: apigeeregistry/v1
+kind: API
+metadata:
+  name: registry
+  labels:
+    apihub-owner: google
+  annotations:
+    apihub-score: "99"
+data:
+  displayName: Apigee Registry API
+  description: The Registry API allows teams to track and manage machine-readable descriptions of APIs.
+  availability: Preview
+  recommendedVersion: v1
+  recommendedDeployment: prod
\ No newline at end of file
diff --git a/cmd/registry/cmd/apply/testdata/unbundled-flat/artifacts-lifecycle.yaml b/cmd/registry/cmd/apply/testdata/unbundled-flat/artifacts-lifecycle.yaml
new file mode 100644
index 0000000..a3414af
--- /dev/null
+++ b/cmd/registry/cmd/apply/testdata/unbundled-flat/artifacts-lifecycle.yaml
@@ -0,0 +1,43 @@
+apiVersion: apigeeregistry/v1
+kind: Lifecycle
+metadata:
+  name: lifecycle
+data:
+  displayName: Lifecycle
+  description: A series of stages that an API typically moves through in its lifetime
+  stages:
+    - id: concept
+      displayName: Concept
+      description: Description of the business case and user needs for why an API should exist
+      url: ""
+      displayOrder: 0
+    - id: design
+      displayName: Design
+      description: Definition of the interface details and proposal of the API contract
+      url: ""
+      displayOrder: 1
+    - id: develop
+      displayName: Develop
+      description: Implementation of the service and its API
+      url: ""
+      displayOrder: 2
+    - id: preview
+      displayName: Preview
+      description: Staging of implementations in the pre-production phase
+      url: ""
+      displayOrder: 3
+    - id: production
+      displayName: Production
+      description: API available for production workloads
+      url: ""
+      displayOrder: 4
+    - id: deprecated
+      displayName: Deprecated
+      description: API not recommended for new consumers
+      url: ""
+      displayOrder: 5
+    - id: retired
+      displayName: Retired
+      description: API no longer available for use
+      url: ""
+      displayOrder: 6
diff --git a/cmd/registry/cmd/apply/testdata/unbundled-flat/artifacts-manifest.yaml b/cmd/registry/cmd/apply/testdata/unbundled-flat/artifacts-manifest.yaml
new file mode 100644
index 0000000..e515149
--- /dev/null
+++ b/cmd/registry/cmd/apply/testdata/unbundled-flat/artifacts-manifest.yaml
@@ -0,0 +1,42 @@
+apiVersion: apigeeregistry/v1
+kind: Manifest
+metadata:
+  name: manifest
+data:
+  displayName: ""
+  description: ""
+  generatedResources:
+    - pattern: apis/-/versions/-/specs/-/artifacts/lint-spectral
+      filter: ""
+      receipt: false
+      dependencies:
+        - pattern: $resource.spec
+          filter: mime_type.contains('openapi')
+      action: registry compute lint $resource.spec --linter spectral
+      refresh: null
+    - pattern: apis/-/versions/-/specs/-/artifacts/lintstats-spectral
+      filter: ""
+      receipt: false
+      dependencies:
+        - pattern: $resource.spec/artifacts/lint-spectral
+          filter: ""
+        - pattern: $resource.spec/artifacts/complexity
+          filter: ""
+      action: registry compute lintstats $resource.spec --linter spectral
+      refresh: null
+    - pattern: apis/-/versions/-/specs/-/artifacts/vocabulary
+      filter: ""
+      receipt: false
+      dependencies:
+        - pattern: $resource.spec
+          filter: ""
+      action: registry compute vocabulary $resource.spec
+      refresh: null
+    - pattern: apis/-/versions/-/specs/-/artifacts/complexity
+      filter: ""
+      receipt: false
+      dependencies:
+        - pattern: $resource.spec
+          filter: ""
+      action: registry compute complexity $resource.spec
+      refresh: null
diff --git a/cmd/registry/cmd/apply/testdata/unbundled-flat/artifacts-styleguide.yaml b/cmd/registry/cmd/apply/testdata/unbundled-flat/artifacts-styleguide.yaml
new file mode 100644
index 0000000..8d8e5b9
--- /dev/null
+++ b/cmd/registry/cmd/apply/testdata/unbundled-flat/artifacts-styleguide.yaml
@@ -0,0 +1,24 @@
+apiVersion: apigeeregistry/v1
+kind: StyleGuide
+metadata:
+  name: styleguide
+data:
+  displayName: StyleGuide
+  mimeTypes:
+    - application/x.openapi+gzip;version=2
+  guidelines:
+    - id: refproperties
+      displayName: Govern Ref Properties
+      description: This guideline governs properties for ref fields on specs.
+      rules:
+        - id: norefsiblings
+          displayName: No Ref Siblings
+          description: An object exposing a $ref property cannot be further extended with additional properties.
+          linter: spectral
+          linterRulename: no-$ref-siblings
+          severity: ERROR
+          docUri: https://meta.stoplight.io/docs/spectral/4dec24461f3af-open-api-rules#no-ref-siblings
+      state: ACTIVE
+  linters:
+    - name: spectral
+      uri: https://github.com/stoplightio/spectral
diff --git a/cmd/registry/cmd/apply/testdata/unbundled-flat/artifacts-taxonomies.yaml b/cmd/registry/cmd/apply/testdata/unbundled-flat/artifacts-taxonomies.yaml
new file mode 100644
index 0000000..da36304
--- /dev/null
+++ b/cmd/registry/cmd/apply/testdata/unbundled-flat/artifacts-taxonomies.yaml
@@ -0,0 +1,95 @@
+apiVersion: apigeeregistry/v1
+kind: TaxonomyList
+metadata:
+  name: taxonomies
+data:
+  displayName: TaxonomyList
+  description: A list of taxonomies that can be used to classify resources in the registry
+  taxonomies:
+    - id: target-users
+      displayName: Target users
+      description: The intended users (consumers) of an API
+      adminApplied: false
+      singleSelection: false
+      searchExcluded: false
+      systemManaged: true
+      displayOrder: 0
+      elements:
+        - id: team
+          displayName: Team
+          description: Intended for exclusive use by the producing team
+        - id: internal
+          displayName: Internal
+          description: Available to internal teams
+        - id: partner
+          displayName: Partner
+          description: Available to select partners
+        - id: public
+          displayName: Public
+          description: Published for discovery by the general public
+    - id: style
+      displayName: Style (primary)
+      description: The primary architectural style of the API
+      adminApplied: false
+      singleSelection: true
+      searchExcluded: false
+      systemManaged: true
+      displayOrder: 1
+      elements:
+        - id: openapi
+          displayName: OpenAPI
+          description: https://spec.openapis.org/oas/latest.html
+        - id: grpc
+          displayName: gRPC
+          description: https://grpc.io
+        - id: graphql
+          displayName: GraphQL
+          description: https://graphql.org
+        - id: asyncapi
+          displayName: AsyncAPI
+          description: https://www.asyncapi.com
+        - id: soap
+          displayName: SOAP
+          description: https://en.wikipedia.org/wiki/Web_Services_Description_Language
+    - id: team
+      displayName: Team
+      description: The producing team responsible for an API
+      adminApplied: false
+      singleSelection: true
+      searchExcluded: false
+      systemManaged: true
+      displayOrder: 2
+      elements:
+        - id: example
+          displayName: Example Team Name
+          description: Example description
+    - id: business-unit
+      displayName: Business unit
+      description: A business unit responsible for an API
+      adminApplied: false
+      singleSelection: true
+      searchExcluded: false
+      systemManaged: true
+      displayOrder: 3
+      elements:
+        - id: example
+          displayName: Example Business Unit Name
+          description: Example description
+    - id: gateway
+      displayName: Gateway
+      description: ""
+      adminApplied: false
+      singleSelection: true
+      searchExcluded: true
+      systemManaged: true
+      displayOrder: 4
+      elements:
+        - id: google-cloud-apigee
+          displayName: Google Cloud - Apigee
+          description: ""
+        - id: google-cloud-api-gateway
+          displayName: Google Cloud - API Gateway
+          description: ""
+        - id: unmanaged
+          displayName: Unmanaged
+          description: ""
diff --git a/cmd/registry/cmd/apply/testdata/unbundled-hierarchical/apis/registry.yaml b/cmd/registry/cmd/apply/testdata/unbundled-hierarchical/apis/registry.yaml
new file mode 100644
index 0000000..09b594f
--- /dev/null
+++ b/cmd/registry/cmd/apply/testdata/unbundled-hierarchical/apis/registry.yaml
@@ -0,0 +1,14 @@
+apiVersion: apigeeregistry/v1
+kind: API
+metadata:
+  name: registry
+  labels:
+    apihub-owner: google
+  annotations:
+    apihub-score: "99"
+data:
+  displayName: Apigee Registry API
+  description: The Registry API allows teams to track and manage machine-readable descriptions of APIs.
+  availability: Preview
+  recommendedVersion: v1
+  recommendedDeployment: prod
\ No newline at end of file
diff --git a/cmd/registry/cmd/apply/testdata/unbundled-hierarchical/apis/registry/artifacts/apihub-related.yaml b/cmd/registry/cmd/apply/testdata/unbundled-hierarchical/apis/registry/artifacts/apihub-related.yaml
new file mode 100644
index 0000000..756222d
--- /dev/null
+++ b/cmd/registry/cmd/apply/testdata/unbundled-hierarchical/apis/registry/artifacts/apihub-related.yaml
@@ -0,0 +1,19 @@
+apiVersion: apigeeregistry/v1
+kind: ReferenceList
+metadata:
+  name: apihub-related
+  parent: apis/registry
+data:
+  displayName: ""
+  description: Defines a list of related resources
+  references:
+    - id: github
+      displayName: GitHub Repo
+      category: apihub-source-code
+      resource: ""
+      uri: https://github.com/apigee/registry
+    - id: docs
+      displayName: GitHub Documentation
+      category: apihub-other
+      resource: ""
+      uri: https://apigee.github.io/registry/
\ No newline at end of file
diff --git a/cmd/registry/cmd/apply/testdata/unbundled-hierarchical/apis/registry/deployments/prod.yaml b/cmd/registry/cmd/apply/testdata/unbundled-hierarchical/apis/registry/deployments/prod.yaml
new file mode 100644
index 0000000..8fed69c
--- /dev/null
+++ b/cmd/registry/cmd/apply/testdata/unbundled-hierarchical/apis/registry/deployments/prod.yaml
@@ -0,0 +1,18 @@
+apiVersion: apigeeregistry/v1
+kind: Deployment
+metadata:
+  name: prod
+  parent: apis/registry
+  labels:
+    platform: google
+  annotations:
+    region: us-central1
+data:
+  displayName: Production
+  description: The hosted deployment of the Registry API
+  apiSpecRevision: v1/specs/openapi.yaml@latest
+  endpointURI: https://apigeeregistry.googleapis.com
+  externalChannelURI: https://apigee.github.io/registry/
+  intendedAudience: Public
+  accessGuidance: See https://github.com/apigee/registry for tools and usage information.
+  
\ No newline at end of file
diff --git a/cmd/registry/cmd/apply/testdata/unbundled-hierarchical/apis/registry/versions/v1.yaml b/cmd/registry/cmd/apply/testdata/unbundled-hierarchical/apis/registry/versions/v1.yaml
new file mode 100644
index 0000000..37e7d8d
--- /dev/null
+++ b/cmd/registry/cmd/apply/testdata/unbundled-hierarchical/apis/registry/versions/v1.yaml
@@ -0,0 +1,14 @@
+apiVersion: apigeeregistry/v1
+kind: Version
+metadata:
+  name: v1
+  parent: apis/registry
+  labels:
+    apihub-team: apigee
+  annotations:
+    release-date: "2021-12-15"
+data:
+  displayName: v1
+  description: New in 2022
+  state: Staging
+        
\ No newline at end of file
diff --git a/cmd/registry/cmd/apply/testdata/unbundled-hierarchical/apis/registry/versions/v1/specs/openapi.yaml b/cmd/registry/cmd/apply/testdata/unbundled-hierarchical/apis/registry/versions/v1/specs/openapi.yaml
new file mode 100644
index 0000000..c578806
--- /dev/null
+++ b/cmd/registry/cmd/apply/testdata/unbundled-hierarchical/apis/registry/versions/v1/specs/openapi.yaml
@@ -0,0 +1,14 @@
+apiVersion: apigeeregistry/v1
+kind: Spec
+metadata:
+  name: openapi.yaml
+  parent: apis/registry/versions/v1
+  labels:
+    openapi-verified: "true"
+  annotations:
+    linters: spectral,gnostic
+data:
+  filename: openapi.yaml
+  description: OpenAPI description of the Registry API
+  mimeType: application/x.openapi+gzip;version=3
+  sourceURI: https://raw.githubusercontent.com/apigee/registry/main/openapi.yaml
\ No newline at end of file
diff --git a/cmd/registry/cmd/apply/testdata/unbundled-hierarchical/artifacts/lifecycle.yaml b/cmd/registry/cmd/apply/testdata/unbundled-hierarchical/artifacts/lifecycle.yaml
new file mode 100644
index 0000000..a3414af
--- /dev/null
+++ b/cmd/registry/cmd/apply/testdata/unbundled-hierarchical/artifacts/lifecycle.yaml
@@ -0,0 +1,43 @@
+apiVersion: apigeeregistry/v1
+kind: Lifecycle
+metadata:
+  name: lifecycle
+data:
+  displayName: Lifecycle
+  description: A series of stages that an API typically moves through in its lifetime
+  stages:
+    - id: concept
+      displayName: Concept
+      description: Description of the business case and user needs for why an API should exist
+      url: ""
+      displayOrder: 0
+    - id: design
+      displayName: Design
+      description: Definition of the interface details and proposal of the API contract
+      url: ""
+      displayOrder: 1
+    - id: develop
+      displayName: Develop
+      description: Implementation of the service and its API
+      url: ""
+      displayOrder: 2
+    - id: preview
+      displayName: Preview
+      description: Staging of implementations in the pre-production phase
+      url: ""
+      displayOrder: 3
+    - id: production
+      displayName: Production
+      description: API available for production workloads
+      url: ""
+      displayOrder: 4
+    - id: deprecated
+      displayName: Deprecated
+      description: API not recommended for new consumers
+      url: ""
+      displayOrder: 5
+    - id: retired
+      displayName: Retired
+      description: API no longer available for use
+      url: ""
+      displayOrder: 6
diff --git a/cmd/registry/cmd/apply/testdata/unbundled-hierarchical/artifacts/manifest.yaml b/cmd/registry/cmd/apply/testdata/unbundled-hierarchical/artifacts/manifest.yaml
new file mode 100644
index 0000000..e515149
--- /dev/null
+++ b/cmd/registry/cmd/apply/testdata/unbundled-hierarchical/artifacts/manifest.yaml
@@ -0,0 +1,42 @@
+apiVersion: apigeeregistry/v1
+kind: Manifest
+metadata:
+  name: manifest
+data:
+  displayName: ""
+  description: ""
+  generatedResources:
+    - pattern: apis/-/versions/-/specs/-/artifacts/lint-spectral
+      filter: ""
+      receipt: false
+      dependencies:
+        - pattern: $resource.spec
+          filter: mime_type.contains('openapi')
+      action: registry compute lint $resource.spec --linter spectral
+      refresh: null
+    - pattern: apis/-/versions/-/specs/-/artifacts/lintstats-spectral
+      filter: ""
+      receipt: false
+      dependencies:
+        - pattern: $resource.spec/artifacts/lint-spectral
+          filter: ""
+        - pattern: $resource.spec/artifacts/complexity
+          filter: ""
+      action: registry compute lintstats $resource.spec --linter spectral
+      refresh: null
+    - pattern: apis/-/versions/-/specs/-/artifacts/vocabulary
+      filter: ""
+      receipt: false
+      dependencies:
+        - pattern: $resource.spec
+          filter: ""
+      action: registry compute vocabulary $resource.spec
+      refresh: null
+    - pattern: apis/-/versions/-/specs/-/artifacts/complexity
+      filter: ""
+      receipt: false
+      dependencies:
+        - pattern: $resource.spec
+          filter: ""
+      action: registry compute complexity $resource.spec
+      refresh: null
diff --git a/cmd/registry/cmd/apply/testdata/unbundled-hierarchical/artifacts/styleguide.yaml b/cmd/registry/cmd/apply/testdata/unbundled-hierarchical/artifacts/styleguide.yaml
new file mode 100644
index 0000000..8d8e5b9
--- /dev/null
+++ b/cmd/registry/cmd/apply/testdata/unbundled-hierarchical/artifacts/styleguide.yaml
@@ -0,0 +1,24 @@
+apiVersion: apigeeregistry/v1
+kind: StyleGuide
+metadata:
+  name: styleguide
+data:
+  displayName: StyleGuide
+  mimeTypes:
+    - application/x.openapi+gzip;version=2
+  guidelines:
+    - id: refproperties
+      displayName: Govern Ref Properties
+      description: This guideline governs properties for ref fields on specs.
+      rules:
+        - id: norefsiblings
+          displayName: No Ref Siblings
+          description: An object exposing a $ref property cannot be further extended with additional properties.
+          linter: spectral
+          linterRulename: no-$ref-siblings
+          severity: ERROR
+          docUri: https://meta.stoplight.io/docs/spectral/4dec24461f3af-open-api-rules#no-ref-siblings
+      state: ACTIVE
+  linters:
+    - name: spectral
+      uri: https://github.com/stoplightio/spectral
diff --git a/cmd/registry/cmd/apply/testdata/unbundled-hierarchical/artifacts/taxonomies.yaml b/cmd/registry/cmd/apply/testdata/unbundled-hierarchical/artifacts/taxonomies.yaml
new file mode 100644
index 0000000..da36304
--- /dev/null
+++ b/cmd/registry/cmd/apply/testdata/unbundled-hierarchical/artifacts/taxonomies.yaml
@@ -0,0 +1,95 @@
+apiVersion: apigeeregistry/v1
+kind: TaxonomyList
+metadata:
+  name: taxonomies
+data:
+  displayName: TaxonomyList
+  description: A list of taxonomies that can be used to classify resources in the registry
+  taxonomies:
+    - id: target-users
+      displayName: Target users
+      description: The intended users (consumers) of an API
+      adminApplied: false
+      singleSelection: false
+      searchExcluded: false
+      systemManaged: true
+      displayOrder: 0
+      elements:
+        - id: team
+          displayName: Team
+          description: Intended for exclusive use by the producing team
+        - id: internal
+          displayName: Internal
+          description: Available to internal teams
+        - id: partner
+          displayName: Partner
+          description: Available to select partners
+        - id: public
+          displayName: Public
+          description: Published for discovery by the general public
+    - id: style
+      displayName: Style (primary)
+      description: The primary architectural style of the API
+      adminApplied: false
+      singleSelection: true
+      searchExcluded: false
+      systemManaged: true
+      displayOrder: 1
+      elements:
+        - id: openapi
+          displayName: OpenAPI
+          description: https://spec.openapis.org/oas/latest.html
+        - id: grpc
+          displayName: gRPC
+          description: https://grpc.io
+        - id: graphql
+          displayName: GraphQL
+          description: https://graphql.org
+        - id: asyncapi
+          displayName: AsyncAPI
+          description: https://www.asyncapi.com
+        - id: soap
+          displayName: SOAP
+          description: https://en.wikipedia.org/wiki/Web_Services_Description_Language
+    - id: team
+      displayName: Team
+      description: The producing team responsible for an API
+      adminApplied: false
+      singleSelection: true
+      searchExcluded: false
+      systemManaged: true
+      displayOrder: 2
+      elements:
+        - id: example
+          displayName: Example Team Name
+          description: Example description
+    - id: business-unit
+      displayName: Business unit
+      description: A business unit responsible for an API
+      adminApplied: false
+      singleSelection: true
+      searchExcluded: false
+      systemManaged: true
+      displayOrder: 3
+      elements:
+        - id: example
+          displayName: Example Business Unit Name
+          description: Example description
+    - id: gateway
+      displayName: Gateway
+      description: ""
+      adminApplied: false
+      singleSelection: true
+      searchExcluded: true
+      systemManaged: true
+      displayOrder: 4
+      elements:
+        - id: google-cloud-apigee
+          displayName: Google Cloud - Apigee
+          description: ""
+        - id: google-cloud-api-gateway
+          displayName: Google Cloud - API Gateway
+          description: ""
+        - id: unmanaged
+          displayName: Unmanaged
+          description: ""