Repository: apigee/registry
Pull request #935 (2023-01-12 23:55:20)

add registry rules for Api
WIP toward #933

Note: Check framework needs ability to do name lookup (#936) before the issue can be completed. However, this PR works standalone, so if we can get it in, it would be helpful from a dependency standpoint.

codecov[bot]: # [Codecov](https://codecov.io/gh/apigee/registry/pull/935?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apigee) Report
> Merging [#935](https://codecov.io/gh/apigee/registry/pull/935?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apigee) (38d5886) into [main](https://codecov.io/gh/apigee/registry/commit/aeaeb368f1c3c0df1c0069cdb6c73b8c93441107?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apigee) (aeaeb36) will **increase** coverage by `0.23%`.
> The diff coverage is `96.66%`.

```diff
@@            Coverage Diff             @@
##             main     #935      +/-   ##
==========================================
+ Coverage   63.77%   64.01%   +0.23%     
==========================================
  Files         111      113       +2     
  Lines       10035    10109      +74     
==========================================
+ Hits         6400     6471      +71     
- Misses       3015     3018       +3     
  Partials      620      620              
```


| [Impacted Files](https://codecov.io/gh/apigee/registry/pull/935?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apigee) | Coverage Δ | |
|---|---|---|
| [cmd/registry/cmd/check/lint/response.go](https://codecov.io/gh/apigee/registry/pull/935?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apigee#diff-Y21kL3JlZ2lzdHJ5L2NtZC9jaGVjay9saW50L3Jlc3BvbnNlLmdv) | `100.00% <ø> (ø)` | |
| [cmd/registry/cmd/check/lint/problem.go](https://codecov.io/gh/apigee/registry/pull/935?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apigee#diff-Y21kL3JlZ2lzdHJ5L2NtZC9jaGVjay9saW50L3Byb2JsZW0uZ28=) | `90.62% <72.72%> (-9.38%)` | :arrow_down: |
| [cmd/registry/cmd/check/lint/rule\_groups.go](https://codecov.io/gh/apigee/registry/pull/935?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apigee#diff-Y21kL3JlZ2lzdHJ5L2NtZC9jaGVjay9saW50L3J1bGVfZ3JvdXBzLmdv) | `95.23% <100.00%> (-1.54%)` | :arrow_down: |
| [cmd/registry/cmd/check/rules/rule100/rule100.go](https://codecov.io/gh/apigee/registry/pull/935?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apigee#diff-Y21kL3JlZ2lzdHJ5L2NtZC9jaGVjay9ydWxlcy9ydWxlMTAwL3J1bGUxMDAuZ28=) | `100.00% <100.00%> (ø)` | |
| [cmd/registry/cmd/check/rules/rule101/rule101.go](https://codecov.io/gh/apigee/registry/pull/935?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apigee#diff-Y21kL3JlZ2lzdHJ5L2NtZC9jaGVjay9ydWxlcy9ydWxlMTAxL3J1bGUxMDEuZ28=) | `100.00% <100.00%> (ø)` | |
| [cmd/registry/cmd/check/rules/rule102/rule102.go](https://codecov.io/gh/apigee/registry/pull/935?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apigee#diff-Y21kL3JlZ2lzdHJ5L2NtZC9jaGVjay9ydWxlcy9ydWxlMTAyL3J1bGUxMDIuZ28=) | `100.00% <100.00%> (ø)` | |
| [cmd/registry/cmd/check/rules/rule110/rule110.go](https://codecov.io/gh/apigee/registry/pull/935?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apigee#diff-Y21kL3JlZ2lzdHJ5L2NtZC9jaGVjay9ydWxlcy9ydWxlMTEwL3J1bGUxMTAuZ28=) | `100.00% <100.00%> (ø)` | |

: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/check/lint/problem.go b/cmd/registry/cmd/check/lint/problem.go
index f89e871..1cc4747 100644
--- a/cmd/registry/cmd/check/lint/problem.go
+++ b/cmd/registry/cmd/check/lint/problem.go
@@ -26,6 +26,18 @@ const (
 	ERROR
 )
 
+func (s Severity) String() string {
+	switch s {
+	case INFO:
+		return "INFO"
+	case WARNING:
+		return "WARNING"
+	case ERROR:
+		return "ERROR"
+	}
+	return "UNKNOWN"
+}
+
 // Problem contains information about a result produced by an API Linter.
 //
 // All rules return []Problem. Most lint rules return 0 or 1 problems, but
@@ -71,12 +83,14 @@ func (p Problem) marshal() interface{} {
 		Location   string   `json:"location" yaml:"location"`
 		RuleID     RuleName `json:"rule_id" yaml:"rule_id"`
 		RuleDocURI string   `json:"rule_doc_uri" yaml:"rule_doc_uri"`
+		Severity   string   `json:"severity" yaml:"severity"`
 	}{
 		p.Message,
 		p.Suggestion,
 		p.Location,
 		p.RuleID,
 		p.GetRuleURI(),
+		p.Severity.String(),
 	}
 }
 
diff --git a/cmd/registry/cmd/check/lint/response.go b/cmd/registry/cmd/check/lint/response.go
index 432b037..b5c92ea 100644
--- a/cmd/registry/cmd/check/lint/response.go
+++ b/cmd/registry/cmd/check/lint/response.go
@@ -21,10 +21,10 @@ import (
 
 // Response collects the results of running the Rules.
 type Response struct {
-	sync.Mutex
-	RunTime  time.Time `json:"time" yaml:"time"`
-	Problems []Problem `json:"problems" yaml:"problems"`
-	Error    error     `json:"error,omitempty" yaml:"error,omitempty"` // populated if panic
+	sync.Mutex `json:"-" yaml:"-"`
+	RunTime    time.Time `json:"time" yaml:"time"`
+	Problems   []Problem `json:"problems" yaml:"problems"`
+	Error      error     `json:"error,omitempty" yaml:"error,omitempty"` // populated if panic
 }
 
 func (r *Response) append(res Resource, probs []Problem, err error) {
diff --git a/cmd/registry/cmd/check/lint/rule_groups.go b/cmd/registry/cmd/check/lint/rule_groups.go
index 0e1a50a..764c1ae 100644
--- a/cmd/registry/cmd/check/lint/rule_groups.go
+++ b/cmd/registry/cmd/check/lint/rule_groups.go
@@ -9,9 +9,7 @@ import "fmt"
 var ruleGroup = []func(int) string{
 	registryGroup,
 	hubGroup,
-	controllerGroup,
 	scoreGroup,
-	styleGroup,
 }
 
 func registryGroup(ruleNum int) string {
@@ -28,25 +26,12 @@ func hubGroup(ruleNum int) string {
 	return ""
 }
 
-func controllerGroup(ruleNum int) string {
-	if ruleNum >= 1100 && ruleNum < 1200 {
-		return "controller"
-	}
-	return ""
-}
-
 func scoreGroup(ruleNum int) string {
-	if ruleNum >= 1200 && ruleNum < 1300 {
+	if ruleNum >= 1100 && ruleNum < 1200 {
 		return "score"
 	}
 	return ""
 }
-func styleGroup(ruleNum int) string {
-	if ruleNum >= 1300 && ruleNum < 1400 {
-		return "style"
-	}
-	return ""
-}
 
 // getRuleGroup takes an rule number and returns the appropriate group.
 // It panics if no group is found.
diff --git a/cmd/registry/cmd/check/rules/rule0001/rule0001.go b/cmd/registry/cmd/check/rules/rule0001/rule0001.go
deleted file mode 100644
index 4a12613..0000000
--- a/cmd/registry/cmd/check/rules/rule0001/rule0001.go
+++ /dev/null
@@ -1,26 +0,0 @@
-// Copyright 2022 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 rule0001
-
-import "github.com/apigee/registry/cmd/registry/cmd/check/lint"
-
-// AddRules accepts a register function and registers each of
-// this rules' checks to the RuleRegistry.
-func AddRules(r lint.RuleRegistry) error {
-	return r.Register(
-		0001,
-		mimeTypeContents,
-	)
-}
diff --git a/cmd/registry/cmd/check/rules/rule100/rule100.go b/cmd/registry/cmd/check/rules/rule100/rule100.go
new file mode 100644
index 0000000..0f202df
--- /dev/null
+++ b/cmd/registry/cmd/check/rules/rule100/rule100.go
@@ -0,0 +1,53 @@
+// Copyright 2023 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.
+
+// Api availability field is free-form, but we expect single words that describe availability.
+// https://github.com/apigee/registry/blob/main/google/cloud/apigeeregistry/v1/registry_models.proto#L51
+package rule100
+
+import (
+	"strings"
+
+	"github.com/apigee/registry/cmd/registry/cmd/check/lint"
+	"github.com/apigee/registry/rpc"
+)
+
+var ruleNum = 100
+var ruleName = lint.NewRuleName(ruleNum, "api-availability-single-word")
+
+// AddRules accepts a register function and registers each of
+// this rules' checks to the RuleRegistry.
+func AddRules(r lint.RuleRegistry) error {
+	return r.Register(
+		ruleNum,
+		availabilitySingleWord,
+	)
+}
+
+var availabilitySingleWord = &lint.ApiRule{
+	Name: ruleName,
+	OnlyIf: func(a *rpc.Api) bool {
+		return true
+	},
+	ApplyToApi: func(a *rpc.Api) []lint.Problem {
+		if arr := strings.SplitN(a.Availability, " ", 2); len(arr) > 1 {
+			return []lint.Problem{{
+				Severity:   lint.INFO,
+				Message:    `Availability is free-form, but we expect single words that describe availability.`,
+				Suggestion: `Use single words like: "NONE", "TESTING", "PREVIEW", "GENERAL", "DEPRECATED", "SHUTDOWN"`,
+			}}
+		}
+		return nil
+	},
+}
diff --git a/cmd/registry/cmd/check/rules/rule101/rule101.go b/cmd/registry/cmd/check/rules/rule101/rule101.go
new file mode 100644
index 0000000..fc58085
--- /dev/null
+++ b/cmd/registry/cmd/check/rules/rule101/rule101.go
@@ -0,0 +1,66 @@
+// Copyright 2023 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.
+
+// Api recommended_version must be an ApiVersion that is a child of this Api.
+package rule101
+
+import (
+	"fmt"
+	"strings"
+
+	"github.com/apigee/registry/cmd/registry/cmd/check/lint"
+	"github.com/apigee/registry/rpc"
+	"github.com/apigee/registry/server/registry/names"
+)
+
+var ruleNum = 101
+var ruleName = lint.NewRuleName(ruleNum, "api-recommended-version-ref")
+
+// AddRules accepts a register function and registers each of
+// this rules' checks to the RuleRegistry.
+func AddRules(r lint.RuleRegistry) error {
+	return r.Register(
+		ruleNum,
+		recommendedVersionRef,
+	)
+}
+
+var recommendedVersionRef = &lint.ApiRule{
+	Name: ruleName,
+	OnlyIf: func(a *rpc.Api) bool {
+		return strings.TrimSpace(a.RecommendedVersion) != ""
+	},
+	ApplyToApi: func(a *rpc.Api) []lint.Problem {
+		versionName, err := names.ParseVersion(a.RecommendedVersion)
+		if err != nil {
+			return []lint.Problem{{
+				Severity:   lint.ERROR,
+				Message:    fmt.Sprintf(`recommended_version %q is not a valid ApiVersion name.`, a.RecommendedVersion),
+				Suggestion: fmt.Sprintf(`Parse error: %s`, err),
+			}}
+		}
+
+		apiName, _ := names.ParseApi(a.Name) // name assumed to be valid
+		if versionName.Api() != apiName {
+			return []lint.Problem{{
+				Severity:   lint.ERROR,
+				Message:    fmt.Sprintf(`recommended_version %q is not a child of this Api.`, a.RecommendedVersion),
+				Suggestion: `Correct the recommended_version.`,
+			}}
+		}
+
+		// TODO: check DB for existence
+		return nil
+	},
+}
diff --git a/cmd/registry/cmd/check/rules/rule102/rule102.go b/cmd/registry/cmd/check/rules/rule102/rule102.go
new file mode 100644
index 0000000..3768f6b
--- /dev/null
+++ b/cmd/registry/cmd/check/rules/rule102/rule102.go
@@ -0,0 +1,66 @@
+// Copyright 2023 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.
+
+// Api recommended_version must be an ApiVersion that is a child of this Api.
+package rule102
+
+import (
+	"fmt"
+	"strings"
+
+	"github.com/apigee/registry/cmd/registry/cmd/check/lint"
+	"github.com/apigee/registry/rpc"
+	"github.com/apigee/registry/server/registry/names"
+)
+
+var ruleNum = 102
+var ruleName = lint.NewRuleName(ruleNum, "api-recommended-deployment-ref")
+
+// AddRules accepts a register function and registers each of
+// this rules' checks to the RuleRegistry.
+func AddRules(r lint.RuleRegistry) error {
+	return r.Register(
+		ruleNum,
+		recommendedDeploymentRef,
+	)
+}
+
+var recommendedDeploymentRef = &lint.ApiRule{
+	Name: ruleName,
+	OnlyIf: func(a *rpc.Api) bool {
+		return strings.TrimSpace(a.RecommendedDeployment) != ""
+	},
+	ApplyToApi: func(a *rpc.Api) []lint.Problem {
+		deploymentName, err := names.ParseDeployment(a.RecommendedDeployment)
+		if err != nil {
+			return []lint.Problem{{
+				Severity:   lint.ERROR,
+				Message:    fmt.Sprintf(`recommended_deployment %q is not a valid ApiDeployment name.`, a.RecommendedDeployment),
+				Suggestion: fmt.Sprintf(`Parse error: %s`, err),
+			}}
+		}
+
+		apiName, _ := names.ParseApi(a.Name) // name assumed to be valid
+		if deploymentName.Api() != apiName {
+			return []lint.Problem{{
+				Severity:   lint.ERROR,
+				Message:    fmt.Sprintf(`recommended_deployment %q is not a child of this Api.`, a.RecommendedDeployment),
+				Suggestion: `Correct the recommended_deployment.`,
+			}}
+		}
+
+		// TODO: check DB for existence
+		return nil
+	},
+}
diff --git a/cmd/registry/cmd/check/rules/rule0001/mime_type_contents.go b/cmd/registry/cmd/check/rules/rule110/rule110.go
similarity index 75%
rename from cmd/registry/cmd/check/rules/rule0001/mime_type_contents.go
rename to cmd/registry/cmd/check/rules/rule110/rule110.go
index e82f342..6931447 100644
--- a/cmd/registry/cmd/check/rules/rule0001/mime_type_contents.go
+++ b/cmd/registry/cmd/check/rules/rule110/rule110.go
@@ -1,10 +1,10 @@
-// Copyright 2022 Google LLC
+// Copyright 2023 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
 //
-//     https://www.apache.org/licenses/LICENSE-2.0
+//    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,
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package rule0001
+package rule110
 
 import (
 	"fmt"
@@ -24,8 +24,27 @@ import (
 	"github.com/apigee/registry/rpc"
 )
 
+var ruleNum = 110
+var ruleName = lint.NewRuleName(ruleNum, "mime-type-detected-contents")
+
+// AddRules accepts a register function and registers each of
+// this rules' checks to the RuleRegistry.
+func AddRules(r lint.RuleRegistry) error {
+	return r.Register(
+		ruleNum,
+		mimeTypeContents,
+	)
+}
+
+var allowPrefixes = []string{
+	"application/octet-stream",
+	"application/x.",
+	"application/x-",
+	"application/vnd",
+}
+
 var mimeTypeContents = &lint.FieldRule{
-	Name: lint.NewRuleName(1, "mime-type-contents"),
+	Name: ruleName,
 	OnlyIf: func(resource lint.Resource, field string) bool {
 		return field == "MimeType"
 	},
@@ -43,6 +62,12 @@ var mimeTypeContents = &lint.FieldRule{
 		if len(contents) == 0 {
 			return nil
 		}
+		for _, p := range allowPrefixes {
+			if strings.HasPrefix(declared, p) {
+				return nil
+			}
+		}
+
 		detected := http.DetectContentType(contents)
 
 		if strings.TrimSpace(declared) == "" {
diff --git a/cmd/registry/cmd/check/rules/rules.go b/cmd/registry/cmd/check/rules/rules.go
index 1b5ac2d..5e8010c 100644
--- a/cmd/registry/cmd/check/rules/rules.go
+++ b/cmd/registry/cmd/check/rules/rules.go
@@ -33,13 +33,17 @@ package rules
 
 import (
 	"github.com/apigee/registry/cmd/registry/cmd/check/lint"
-	"github.com/apigee/registry/cmd/registry/cmd/check/rules/rule0001"
+	"github.com/apigee/registry/cmd/registry/cmd/check/rules/rule100"
+	"github.com/apigee/registry/cmd/registry/cmd/check/rules/rule101"
+	"github.com/apigee/registry/cmd/registry/cmd/check/rules/rule110"
 )
 
 type addRulesFuncType func(lint.RuleRegistry) error
 
 var addRulesFuncs = []addRulesFuncType{
-	rule0001.AddRules,
+	rule100.AddRules,
+	rule101.AddRules,
+	rule110.AddRules,
 }
 
 // Add all rules to the given registry.


Test patch:
diff --git a/cmd/registry/cmd/check/lint/problem_test.go b/cmd/registry/cmd/check/lint/problem_test.go
index 783da13..4ede6d6 100644
--- a/cmd/registry/cmd/check/lint/problem_test.go
+++ b/cmd/registry/cmd/check/lint/problem_test.go
@@ -54,6 +54,7 @@ func TestProblemYAML(t *testing.T) {
 		Message:  "foo bar",
 		Location: "test/location",
 		RuleID:   "core::0131",
+		Severity: ERROR,
 	}
 	serialized, err := yaml.Marshal(problem)
 	if err != nil {
@@ -66,6 +67,7 @@ func TestProblemYAML(t *testing.T) {
 		{"Message", `message: foo bar`},
 		{"Location", `location: test/location`},
 		{"RuleID", `rule_id: core::0131`},
+		{"Severity", `ERROR`},
 	}
 	for _, test := range tests {
 		t.Run(test.testName, func(t *testing.T) {
diff --git a/cmd/registry/cmd/check/lint/rule_groups_test.go b/cmd/registry/cmd/check/lint/rule_groups_test.go
index c021287..bff5bae 100644
--- a/cmd/registry/cmd/check/lint/rule_groups_test.go
+++ b/cmd/registry/cmd/check/lint/rule_groups_test.go
@@ -14,12 +14,8 @@ func TestGroups(t *testing.T) {
 		{"registry", 999, "registry"},
 		{"hub", 1000, "hub"},
 		{"hub", 1099, "hub"},
-		{"controller", 1100, "controller"},
-		{"controller", 1199, "controller"},
-		{"score", 1200, "score"},
-		{"score", 1299, "score"},
-		{"style", 1300, "style"},
-		{"style", 1399, "style"},
+		{"score", 1100, "score"},
+		{"score", 1199, "score"},
 	}
 	for _, test := range tests {
 		t.Run(test.name, func(t *testing.T) {
diff --git a/cmd/registry/cmd/check/rules/rule0001/rule0001_test.go b/cmd/registry/cmd/check/rules/rule0001/rule0001_test.go
deleted file mode 100644
index 8fd9e80..0000000
--- a/cmd/registry/cmd/check/rules/rule0001/rule0001_test.go
+++ /dev/null
@@ -1,27 +0,0 @@
-// Copyright 2022 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 rule0001
-
-import (
-	"testing"
-
-	"github.com/apigee/registry/cmd/registry/cmd/check/lint"
-)
-
-func TestAddRules(t *testing.T) {
-	if err := AddRules(lint.NewRuleRegistry()); err != nil {
-		t.Errorf("AddRules got an error: %v", err)
-	}
-}
diff --git a/cmd/registry/cmd/check/rules/rule100/rule100_test.go b/cmd/registry/cmd/check/rules/rule100/rule100_test.go
new file mode 100644
index 0000000..35cfc27
--- /dev/null
+++ b/cmd/registry/cmd/check/rules/rule100/rule100_test.go
@@ -0,0 +1,56 @@
+// Copyright 2023 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 rule100
+
+import (
+	"testing"
+
+	"github.com/apigee/registry/cmd/registry/cmd/check/lint"
+	"github.com/apigee/registry/rpc"
+	"github.com/google/go-cmp/cmp"
+)
+
+func TestAddRules(t *testing.T) {
+	if err := AddRules(lint.NewRuleRegistry()); err != nil {
+		t.Errorf("AddRules got an error: %v", err)
+	}
+}
+
+func Test_availabilitySingleWord(t *testing.T) {
+	prob := []lint.Problem{{
+		Severity:   lint.INFO,
+		Message:    `Availability is free-form, but we expect single words that describe availability.`,
+		Suggestion: `Use single words like: "NONE", "TESTING", "PREVIEW", "GENERAL", "DEPRECATED", "SHUTDOWN"`,
+	}}
+
+	for _, tt := range []struct {
+		in       string
+		expected []lint.Problem
+	}{
+		{"", nil},
+		{"x", nil},
+		{"x x", prob},
+	} {
+		api := &rpc.Api{
+			Availability: tt.in,
+		}
+		if availabilitySingleWord.OnlyIf(api) {
+			got := availabilitySingleWord.ApplyToApi(api)
+			if diff := cmp.Diff(got, tt.expected); diff != "" {
+				t.Errorf("unexpected diff: (-want +got):\n%s", diff)
+			}
+		}
+	}
+}
diff --git a/cmd/registry/cmd/check/rules/rule101/rule101_test.go b/cmd/registry/cmd/check/rules/rule101/rule101_test.go
new file mode 100644
index 0000000..a86f622
--- /dev/null
+++ b/cmd/registry/cmd/check/rules/rule101/rule101_test.go
@@ -0,0 +1,64 @@
+// Copyright 2023 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 rule101
+
+import (
+	"testing"
+
+	"github.com/apigee/registry/cmd/registry/cmd/check/lint"
+	"github.com/apigee/registry/rpc"
+	"github.com/google/go-cmp/cmp"
+)
+
+func TestAddRules(t *testing.T) {
+	if err := AddRules(lint.NewRuleRegistry()); err != nil {
+		t.Errorf("AddRules got an error: %v", err)
+	}
+}
+
+func Test_recommendedVersionRef(t *testing.T) {
+	for _, tt := range []struct {
+		desc     string
+		in       string
+		expected []lint.Problem
+	}{
+		{"empty", "", nil},
+		{"unable to parse", "bad", []lint.Problem{{
+			Severity:   lint.ERROR,
+			Message:    `recommended_version "bad" is not a valid ApiVersion name.`,
+			Suggestion: `Parse error: invalid version name "bad": must match "^projects/([A-Za-z0-9-.]+)/locations/global/apis/([A-Za-z0-9-.]+)/versions/([A-Za-z0-9-.]+)$"`,
+		}}},
+		{"not a child", "projects/myproject/locations/global/apis/bad/versions/bad", []lint.Problem{{
+			Severity:   lint.ERROR,
+			Message:    `recommended_version "projects/myproject/locations/global/apis/bad/versions/bad" is not a child of this Api.`,
+			Suggestion: `Correct the recommended_version.`,
+		}}},
+		{"good", "projects/myproject/locations/global/apis/myapi/versions/good", nil},
+	} {
+		t.Run(tt.desc, func(t *testing.T) {
+			a := &rpc.Api{
+				Name:               "projects/myproject/locations/global/apis/myapi",
+				RecommendedVersion: tt.in,
+			}
+
+			if recommendedVersionRef.OnlyIf(a) {
+				got := recommendedVersionRef.ApplyToApi(a)
+				if diff := cmp.Diff(got, tt.expected); diff != "" {
+					t.Errorf("unexpected diff: (-want +got):\n%s", diff)
+				}
+			}
+		})
+	}
+}
diff --git a/cmd/registry/cmd/check/rules/rule102/rule102_test.go b/cmd/registry/cmd/check/rules/rule102/rule102_test.go
new file mode 100644
index 0000000..0c53e35
--- /dev/null
+++ b/cmd/registry/cmd/check/rules/rule102/rule102_test.go
@@ -0,0 +1,64 @@
+// Copyright 2023 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 rule102
+
+import (
+	"testing"
+
+	"github.com/apigee/registry/cmd/registry/cmd/check/lint"
+	"github.com/apigee/registry/rpc"
+	"github.com/google/go-cmp/cmp"
+)
+
+func TestAddRules(t *testing.T) {
+	if err := AddRules(lint.NewRuleRegistry()); err != nil {
+		t.Errorf("AddRules got an error: %v", err)
+	}
+}
+
+func Test_recommendedDeploymentRef(t *testing.T) {
+	for _, tt := range []struct {
+		desc     string
+		in       string
+		expected []lint.Problem
+	}{
+		{"empty", "", nil},
+		{"unable to parse", "bad", []lint.Problem{{
+			Severity:   lint.ERROR,
+			Message:    `recommended_deployment "bad" is not a valid ApiDeployment name.`,
+			Suggestion: `Parse error: invalid deployment name "bad": must match "^projects/([A-Za-z0-9-.]+)/locations/global/apis/([A-Za-z0-9-.]+)/deployments/([A-Za-z0-9-.]+)$"`,
+		}}},
+		{"not a child", "projects/myproject/locations/global/apis/bad/deployments/bad", []lint.Problem{{
+			Severity:   lint.ERROR,
+			Message:    `recommended_deployment "projects/myproject/locations/global/apis/bad/deployments/bad" is not a child of this Api.`,
+			Suggestion: `Correct the recommended_deployment.`,
+		}}},
+		{"good", "projects/myproject/locations/global/apis/myapi/deployments/good", nil},
+	} {
+		t.Run(tt.desc, func(t *testing.T) {
+			a := &rpc.Api{
+				Name:                  "projects/myproject/locations/global/apis/myapi",
+				RecommendedDeployment: tt.in,
+			}
+
+			if recommendedDeploymentRef.OnlyIf(a) {
+				got := recommendedDeploymentRef.ApplyToApi(a)
+				if diff := cmp.Diff(got, tt.expected); diff != "" {
+					t.Errorf("unexpected diff: (-want +got):\n%s", diff)
+				}
+			}
+		})
+	}
+}
diff --git a/cmd/registry/cmd/check/rules/rule0001/mime_type_contents_test.go b/cmd/registry/cmd/check/rules/rule110/rule110_test.go
similarity index 90%
rename from cmd/registry/cmd/check/rules/rule0001/mime_type_contents_test.go
rename to cmd/registry/cmd/check/rules/rule110/rule110_test.go
index e927352..fac275d 100644
--- a/cmd/registry/cmd/check/rules/rule0001/mime_type_contents_test.go
+++ b/cmd/registry/cmd/check/rules/rule110/rule110_test.go
@@ -1,10 +1,10 @@
-// Copyright 2022 Google LLC
+// Copyright 2023 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
 //
-//     https://www.apache.org/licenses/LICENSE-2.0
+//    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,
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package rule0001
+package rule110
 
 import (
 	"testing"
@@ -24,7 +24,13 @@ import (
 	"google.golang.org/protobuf/proto"
 )
 
-func TestContentsMimeType(t *testing.T) {
+func TestAddRules(t *testing.T) {
+	if err := AddRules(lint.NewRuleRegistry()); err != nil {
+		t.Errorf("AddRules got an error: %v", err)
+	}
+}
+
+func Test_mimeTypeContents(t *testing.T) {
 	tests := []struct {
 		name     string
 		mimeType string