Repository: googleapis/api-linter
Pull request #1156 (2023-05-24 05:42:32)

feat: add request-required-fields for std methods
As a follow-up to #1142, adding check for required fields for other standard methods. Each AIP has guidance on which fields are required vs not required.


Patch:
diff --git a/docs/rules/0131/request-required-fields.md b/docs/rules/0131/request-required-fields.md
new file mode 100644
index 0000000..7588203
--- /dev/null
+++ b/docs/rules/0131/request-required-fields.md
@@ -0,0 +1,86 @@
+---
+rule:
+  aip: 131
+  name: [core, '0131', request-unknown-fields]
+  summary: Get RPCs must not have unexpected required fields in the request.
+permalink: /131/request-required-fields
+redirect_from:
+  - /0131/request-required-fields
+---
+
+# Get methods: Required fields
+
+This rule enforces that all `Get` standard methods do not have unexpected
+required fields, as mandated in [AIP-131][].
+
+## Details
+
+This rule looks at any message matching `Get*Request` and complains if it
+comes across any required fields other than:
+
+- `string name` ([AIP-131][])
+
+## Examples
+
+**Incorrect** code for this rule:
+
+```proto
+// Incorrect.
+message GetBookRequest {
+  // The name of the book to retrieve.
+  // Format: publishers/{publisher}/books/{book}
+  string name = 1 [
+    (google.api.field_behavior) = REQUIRED,
+    (google.api.resource_reference) = {
+      type: "library.googleapis.com/Book"
+  }];
+
+  // Non-standard required field.
+   google.protobuf.FieldMask read_mask = 2 [(google.api.field_behavior) = REQUIRED];
+}
+```
+
+**Correct** code for this rule:
+
+```proto
+// Correct.
+message GetBookRequest {
+  // The name of the book to retrieve.
+  // Format: publishers/{publisher}/books/{book}
+  string name = 1 [
+    (google.api.field_behavior) = REQUIRED,
+    (google.api.resource_reference) = {
+      type: "library.googleapis.com/Book"
+  }];
+
+  google.protobuf.FieldMask read_mask = 2 [(google.api.field_behavior) = OPTIONAL];
+}
+```
+
+## Disabling
+
+If you need to violate this rule, use a leading comment above the field.
+Remember to also include an [aip.dev/not-precedent][] comment explaining why.
+
+```proto
+message GetBookRequest {
+  // The name of the book to retrieve.
+  // Format: publishers/{publisher}/books/{book}
+  string name = 1 [
+    (google.api.field_behavior) = REQUIRED,
+    (google.api.resource_reference) = {
+      type: "library.googleapis.com/Book"
+  }];
+
+  // (-- api-linter: core::0131::request-required-fields=disabled
+  //     aip.dev/not-precedent: We really need this field to be required because
+  //     reasons. --)
+   google.protobuf.FieldMask read_mask = 2 [(google.api.field_behavior) = REQUIRED];
+}
+```
+
+If you need to violate this rule for an entire file, place the comment at the
+top of the file.
+
+[aip-131]: https://aip.dev/131
+[aip.dev/not-precedent]: https://aip.dev/not-precedent
diff --git a/docs/rules/0132/request-required-fields.md b/docs/rules/0132/request-required-fields.md
new file mode 100644
index 0000000..c7890ee
--- /dev/null
+++ b/docs/rules/0132/request-required-fields.md
@@ -0,0 +1,86 @@
+---
+rule:
+  aip: 132
+  name: [core, '0132', request-unknown-fields]
+  summary: List RPCs must not have unexpected required fields in the request.
+permalink: /132/request-required-fields
+redirect_from:
+  - /0132/request-required-fields
+---
+
+# List methods: Required fields
+
+This rule enforces that all `List` standard methods do not have unexpected
+required fields, as mandated in [AIP-132][].
+
+## Details
+
+This rule looks at any message matching `List*Request` and complains if it
+comes across any required fields other than:
+
+- `string parent` ([AIP-132][])
+
+## Examples
+
+**Incorrect** code for this rule:
+
+```proto
+// Incorrect.
+message ListBooksRequest {
+	// The parent, which owns this collection of books.
+	// Format: publishers/{publisher}
+	string parent = 1 [
+	    (google.api.field_behavior) = REQUIRED,
+	    (google.api.resource_reference) = {
+	  		child_type: "library.googleapis.com/Book"
+	    }];
+
+  // Non-standard required field.
+  int32 page_size = 2 [(google.api.field_behavior) = REQUIRED]
+}
+```
+
+**Correct** code for this rule:
+
+```proto
+// Correct.
+message ListBooksRequest {
+	// The parent, which owns this collection of books.
+	// Format: publishers/{publisher}
+	string parent = 1 [
+	    (google.api.field_behavior) = REQUIRED,
+	    (google.api.resource_reference) = {
+	  		child_type: "library.googleapis.com/Book"
+	    }];
+
+  int32 page_size = 2 [(google.api.field_behavior) = OPTIONAL]
+}
+```
+
+## Disabling
+
+If you need to violate this rule, use a leading comment above the field.
+Remember to also include an [aip.dev/not-precedent][] comment explaining why.
+
+```proto
+message ListBooksRequest {
+	// The parent, which owns this collection of books.
+	// Format: publishers/{publisher}
+	string parent = 1 [
+	    (google.api.field_behavior) = REQUIRED,
+	    (google.api.resource_reference) = {
+	  		child_type: "library.googleapis.com/Book"
+	    }];
+
+  // (-- api-linter: core::0132::request-required-fields=disabled
+  //     aip.dev/not-precedent: We really need this field to be required because
+  // reasons. --)
+  int32 page_size = 2 [(google.api.field_behavior) = REQUIRED]
+}
+```
+
+If you need to violate this rule for an entire file, place the comment at the
+top of the file.
+
+[aip-132]: https://aip.dev/132
+[aip.dev/not-precedent]: https://aip.dev/not-precedent
diff --git a/docs/rules/0133/request-required-fields.md b/docs/rules/0133/request-required-fields.md
index 894bd99..050e1ff 100644
--- a/docs/rules/0133/request-required-fields.md
+++ b/docs/rules/0133/request-required-fields.md
@@ -71,5 +71,4 @@ If you need to violate this rule for an entire file, place the comment at the
 top of the file.
 
 [aip-133]: https://aip.dev/133
-[aip-155]: https://aip.dev/155
 [aip.dev/not-precedent]: https://aip.dev/not-precedent
diff --git a/docs/rules/0134/request-required-fields.md b/docs/rules/0134/request-required-fields.md
new file mode 100644
index 0000000..e1c8e75
--- /dev/null
+++ b/docs/rules/0134/request-required-fields.md
@@ -0,0 +1,65 @@
+---
+rule:
+  aip: 134
+  name: [core, '0134', request-unknown-fields]
+  summary: Update RPCs must not have unexpected required fields in the request.
+permalink: /134/request-required-fields
+redirect_from:
+  - /0134/request-required-fields
+---
+
+# Update methods: Required fields
+
+This rule enforces that all `Update` standard methods do not have unexpected
+required fields, as mandated in [AIP-134][].
+
+## Details
+
+This rule looks at any message matching `Update*Request` and complains if it
+comes across any required fields other than:
+
+- `{Resource} {resource}` ([AIP-134][])
+
+## Examples
+
+**Incorrect** code for this rule:
+
+```proto
+// Incorrect.
+message UpdateBookRequest {
+  Book book = 1 [(google.api.field_behavior) = REQUIRED];
+  // Non-standard required field.
+  bool allow_missing = 2 [(google.api.field_behavior) = REQUIRED];
+}
+```
+
+**Correct** code for this rule:
+
+```proto
+// Correct.
+message UpdateBookRequest {
+  Book book = 1 [(google.api.field_behavior) = REQUIRED];
+  bool allow_missing = 2 [(google.api.field_behavior) = OPTIONAL];
+}
+```
+
+## Disabling
+
+If you need to violate this rule, use a leading comment above the field.
+Remember to also include an [aip.dev/not-precedent][] comment explaining why.
+
+```proto
+message UpdateBookRequest {
+  Book book = 1 [(google.api.field_behavior) = REQUIRED];
+  // (-- api-linter: core::0134::request-required-fields=disabled
+  //     aip.dev/not-precedent: We really need this field to be required because
+  // reasons. --)
+  bool allow_missing = 2 [(google.api.field_behavior) = REQUIRED];
+}
+```
+
+If you need to violate this rule for an entire file, place the comment at the
+top of the file.
+
+[aip-134]: https://aip.dev/134
+[aip.dev/not-precedent]: https://aip.dev/not-precedent
diff --git a/docs/rules/0135/request-required-fields.md b/docs/rules/0135/request-required-fields.md
new file mode 100644
index 0000000..b5c0439
--- /dev/null
+++ b/docs/rules/0135/request-required-fields.md
@@ -0,0 +1,65 @@
+---
+rule:
+  aip: 135
+  name: [core, '0135', request-unknown-fields]
+  summary: Delete RPCs must not have unexpected required fields in the request.
+permalink: /135/request-required-fields
+redirect_from:
+  - /0135/request-required-fields
+---
+
+# Delete methods: Required fields
+
+This rule enforces that all `Delete` standard methods do not have unexpected
+required fields, as mandated in [AIP-135][].
+
+## Details
+
+This rule looks at any message matching `Delete*Request` and complains if it
+comes across any required fields other than:
+
+- `string name` ([AIP-135][])
+
+## Examples
+
+**Incorrect** code for this rule:
+
+```proto
+// Incorrect.
+message DeleteBookRequest {
+  string name = 1 [(google.api.field_behavior) = REQUIRED];
+  // Non-standard required field.
+  bool allow_missing = 4 [(google.api.field_behavior) = REQUIRED];
+}
+```
+
+**Correct** code for this rule:
+
+```proto
+// Correct.
+message DeleteBookRequest {
+  string name = 1 [(google.api.field_behavior) = REQUIRED];
+  bool allow_missing = 4 [(google.api.field_behavior) = OPTIONAL];
+}
+```
+
+## Disabling
+
+If you need to violate this rule, use a leading comment above the field.
+Remember to also include an [aip.dev/not-precedent][] comment explaining why.
+
+```proto
+message DeleteBookRequest {
+  string name = 1 [(google.api.field_behavior) = REQUIRED];
+  // (-- api-linter: core::0135::request-required-fields=disabled
+  //     aip.dev/not-precedent: We really need this field to be required because
+  // reasons. --)
+  bool allow_missing = 4 [(google.api.field_behavior) = REQUIRED];
+}
+```
+
+If you need to violate this rule for an entire file, place the comment at the
+top of the file.
+
+[aip-135]: https://aip.dev/135
+[aip.dev/not-precedent]: https://aip.dev/not-precedent
diff --git a/rules/aip0131/aip0131.go b/rules/aip0131/aip0131.go
index 3a674f2..262e3dc 100644
--- a/rules/aip0131/aip0131.go
+++ b/rules/aip0131/aip0131.go
@@ -38,6 +38,7 @@ func AddRules(r lint.RuleRegistry) error {
 		requestNameReference,
 		requestNameReferenceType,
 		requestNameRequired,
+		requestRequiredFields,
 		synonyms,
 		unknownFields,
 	)
diff --git a/rules/aip0131/request_required_fields.go b/rules/aip0131/request_required_fields.go
new file mode 100644
index 0000000..9458a79
--- /dev/null
+++ b/rules/aip0131/request_required_fields.go
@@ -0,0 +1,49 @@
+// Copyright 2023 Google LLC
+//
+// 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
+//
+// 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 aip0131
+
+import (
+	"fmt"
+
+	"bitbucket.org/creachadair/stringset"
+	"github.com/googleapis/api-linter/lint"
+	"github.com/googleapis/api-linter/rules/internal/utils"
+	"github.com/jhump/protoreflect/desc"
+)
+
+// The get request message should not have unrecognized fields.
+var requestRequiredFields = &lint.MessageRule{
+	Name:   lint.NewRuleName(131, "request-required-fields"),
+	OnlyIf: isGetRequestMessage,
+	LintMessage: func(m *desc.MessageDescriptor) (problems []lint.Problem) {
+		// Rule check: Establish that there are no unexpected fields.
+		allowedRequiredFields := stringset.New("name")
+
+		for _, f := range m.GetFields() {
+			if !utils.GetFieldBehavior(f).Contains("REQUIRED") {
+				continue
+			}
+			// add a problem.
+			if !allowedRequiredFields.Contains(string(f.GetName())) {
+				problems = append(problems, lint.Problem{
+					Message:    fmt.Sprintf("Get RPCs must only require fields explicitly described in AIPs, not %q.", f.GetName()),
+					Descriptor: f,
+				})
+			}
+		}
+
+		return problems
+	},
+}
diff --git a/rules/aip0132/aip0132.go b/rules/aip0132/aip0132.go
index 3070378..ab0172a 100644
--- a/rules/aip0132/aip0132.go
+++ b/rules/aip0132/aip0132.go
@@ -38,6 +38,7 @@ func AddRules(r lint.RuleRegistry) error {
 		requestParentReference,
 		requestParentValidReference,
 		requestParentRequired,
+		requestRequiredFields,
 		requestShowDeletedRequired,
 		resourceReferenceType,
 		responseMessageName,
diff --git a/rules/aip0132/request_required_fields.go b/rules/aip0132/request_required_fields.go
new file mode 100644
index 0000000..a6ab465
--- /dev/null
+++ b/rules/aip0132/request_required_fields.go
@@ -0,0 +1,49 @@
+// Copyright 2023 Google LLC
+//
+// 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
+//
+// 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 aip0132
+
+import (
+	"fmt"
+
+	"bitbucket.org/creachadair/stringset"
+	"github.com/googleapis/api-linter/lint"
+	"github.com/googleapis/api-linter/rules/internal/utils"
+	"github.com/jhump/protoreflect/desc"
+)
+
+// The list request message should not have unrecognized fields.
+var requestRequiredFields = &lint.MessageRule{
+	Name:   lint.NewRuleName(132, "request-required-fields"),
+	OnlyIf: isListRequestMessage,
+	LintMessage: func(m *desc.MessageDescriptor) (problems []lint.Problem) {
+		// Rule check: Establish that there are no unexpected fields.
+		allowedRequiredFields := stringset.New("parent")
+
+		for _, f := range m.GetFields() {
+			if !utils.GetFieldBehavior(f).Contains("REQUIRED") {
+				continue
+			}
+			// add a problem.
+			if !allowedRequiredFields.Contains(string(f.GetName())) {
+				problems = append(problems, lint.Problem{
+					Message:    fmt.Sprintf("List RPCs must only require fields explicitly described in AIPs, not %q.", f.GetName()),
+					Descriptor: f,
+				})
+			}
+		}
+
+		return problems
+	},
+}
diff --git a/rules/aip0133/request_required_fields.go b/rules/aip0133/request_required_fields.go
index 00ec361..c244736 100644
--- a/rules/aip0133/request_required_fields.go
+++ b/rules/aip0133/request_required_fields.go
@@ -18,27 +18,30 @@ import (
 	"fmt"
 	"strings"
 
+	"bitbucket.org/creachadair/stringset"
 	"github.com/googleapis/api-linter/lint"
 	"github.com/googleapis/api-linter/rules/internal/utils"
 	"github.com/jhump/protoreflect/desc"
-	"github.com/jhump/protoreflect/desc/builder"
 	"github.com/stoewer/go-strcase"
 )
 
 // The create request message should not have unrecognized fields.
-var requestRequiredFields = &lint.MessageRule{
+var requestRequiredFields = &lint.MethodRule{
 	Name:   lint.NewRuleName(133, "request-required-fields"),
-	OnlyIf: isCreateRequestMessage,
-	LintMessage: func(m *desc.MessageDescriptor) (problems []lint.Problem) {
-		resourceMsgName := getResourceMsgNameFromReq(m)
+	OnlyIf: utils.IsCreateMethod,
+	LintMethod: func(m *desc.MethodDescriptor) []lint.Problem {
+		ot := utils.GetResponseType(m)
+		r := utils.GetResource(ot)
+		resourceMsgName := utils.GetResourceSingular(r)
 
 		// Rule check: Establish that there are no unexpected fields.
-		allowedRequiredFields := map[string]*builder.FieldType{
-			"parent": nil, // AIP-133
-			fmt.Sprintf("%s_id", strings.ToLower(strcase.SnakeCase(resourceMsgName))): nil,
-		}
+		allowedRequiredFields := stringset.New(
+			"parent",
+			fmt.Sprintf("%s_id", strings.ToLower(strcase.SnakeCase(resourceMsgName))),
+		)
 
-		for _, f := range m.GetFields() {
+		problems := []lint.Problem{}
+		for _, f := range m.GetInputType().GetFields() {
 			if !utils.GetFieldBehavior(f).Contains("REQUIRED") {
 				continue
 			}
@@ -48,14 +51,13 @@ var requestRequiredFields = &lint.MessageRule{
 			}
 			// Iterate remaining fields. If they're not in the allowed list,
 			// add a problem.
-			if _, ok := allowedRequiredFields[string(f.GetName())]; !ok {
+			if !allowedRequiredFields.Contains(string(f.GetName())) {
 				problems = append(problems, lint.Problem{
 					Message:    fmt.Sprintf("Create RPCs must only require fields explicitly described in AIPs, not %q.", f.GetName()),
 					Descriptor: f,
 				})
 			}
 		}
-
 		return problems
 	},
 }
diff --git a/rules/aip0134/aip0134.go b/rules/aip0134/aip0134.go
index f93ad75..690d4ce 100644
--- a/rules/aip0134/aip0134.go
+++ b/rules/aip0134/aip0134.go
@@ -38,6 +38,7 @@ func AddRules(r lint.RuleRegistry) error {
 		requestMaskField,
 		requestMaskRequired,
 		requestMessageName,
+		requestRequiredFields,
 		requestResourceField,
 		requestResourceRequired,
 		responseLRO,
diff --git a/rules/aip0134/request_required_fields.go b/rules/aip0134/request_required_fields.go
new file mode 100644
index 0000000..3479910
--- /dev/null
+++ b/rules/aip0134/request_required_fields.go
@@ -0,0 +1,54 @@
+// Copyright 2023 Google LLC
+//
+// 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
+//
+// 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 aip0134
+
+import (
+	"fmt"
+
+	"bitbucket.org/creachadair/stringset"
+	"github.com/googleapis/api-linter/lint"
+	"github.com/googleapis/api-linter/rules/internal/utils"
+	"github.com/jhump/protoreflect/desc"
+)
+
+// The update request message should not have unrecognized fields.
+var requestRequiredFields = &lint.MessageRule{
+	Name:   lint.NewRuleName(134, "request-required-fields"),
+	OnlyIf: isUpdateRequestMessage,
+	LintMessage: func(m *desc.MessageDescriptor) (problems []lint.Problem) {
+		resourceMsgName := extractResource(m.GetName())
+
+		allowedRequiredFields := stringset.New("update_mask")
+
+		for _, f := range m.GetFields() {
+			if !utils.GetFieldBehavior(f).Contains("REQUIRED") {
+				continue
+			}
+			// Skip the check with the field that is the body.
+			if t := f.GetMessageType(); t != nil && t.GetName() == resourceMsgName {
+				continue
+			}
+			// add a problem.
+			if !allowedRequiredFields.Contains(string(f.GetName())) {
+				problems = append(problems, lint.Problem{
+					Message:    fmt.Sprintf("Update RPCs must only require fields explicitly described in AIPs, not %q.", f.GetName()),
+					Descriptor: f,
+				})
+			}
+		}
+
+		return problems
+	},
+}
diff --git a/rules/aip0135/aip0135.go b/rules/aip0135/aip0135.go
index 2a35c17..4b50147 100644
--- a/rules/aip0135/aip0135.go
+++ b/rules/aip0135/aip0135.go
@@ -38,6 +38,7 @@ func AddRules(r lint.RuleRegistry) error {
 		requestNameField,
 		requestNameReference,
 		requestNameRequired,
+		requestRequiredFields,
 		responseLRO,
 		unknownFields,
 	)
diff --git a/rules/aip0135/request_required_fields.go b/rules/aip0135/request_required_fields.go
new file mode 100644
index 0000000..e59d122
--- /dev/null
+++ b/rules/aip0135/request_required_fields.go
@@ -0,0 +1,50 @@
+// Copyright 2023 Google LLC
+//
+// 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
+//
+// 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 aip0135
+
+import (
+	"fmt"
+
+	"bitbucket.org/creachadair/stringset"
+	"github.com/googleapis/api-linter/lint"
+	"github.com/googleapis/api-linter/rules/internal/utils"
+	"github.com/jhump/protoreflect/desc"
+)
+
+// The delete request message should not have unrecognized fields.
+var requestRequiredFields = &lint.MessageRule{
+	Name:   lint.NewRuleName(135, "request-required-fields"),
+	OnlyIf: isDeleteRequestMessage,
+	LintMessage: func(m *desc.MessageDescriptor) (problems []lint.Problem) {
+		// Rule check: Establish that there are no unexpected fields.
+		allowedRequiredFields := stringset.New("name")
+
+		for _, f := range m.GetFields() {
+			if !utils.GetFieldBehavior(f).Contains("REQUIRED") {
+				continue
+			}
+			// Iterate remaining fields. If they're not in the allowed list,
+			// add a problem.
+			if !allowedRequiredFields.Contains(string(f.GetName())) {
+				problems = append(problems, lint.Problem{
+					Message:    fmt.Sprintf("Delete RPCs must only require fields explicitly described in AIPs, not %q.", f.GetName()),
+					Descriptor: f,
+				})
+			}
+		}
+
+		return problems
+	},
+}
diff --git a/rules/internal/utils/resource.go b/rules/internal/utils/resource.go
new file mode 100644
index 0000000..72191cf
--- /dev/null
+++ b/rules/internal/utils/resource.go
@@ -0,0 +1,39 @@
+// Copyright 2023 Google LLC
+//
+// 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
+//
+// 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 utils
+
+import apb "google.golang.org/genproto/googleapis/api/annotations"
+
+// GetResourceSingular returns the resource singular. The
+// empty string is returned if the singular cannot be found.
+// Since the singular is not always annotated, it extracts
+// it from multiple different locations including:
+// 1. the singular annotation
+// 2. the type definition
+func GetResourceSingular(r *apb.ResourceDescriptor) string {
+	if r == nil {
+		return ""
+	}
+	if r.Singular != "" {
+		return r.Singular
+	}
+	if r.Type != "" {
+		_, tn, ok := SplitResourceTypeName(r.Type)
+		if ok {
+			return tn
+		}
+	}
+	return ""
+}


Test patch:
diff --git a/rules/aip0131/request_required_fields_test.go b/rules/aip0131/request_required_fields_test.go
new file mode 100644
index 0000000..c606d1e
--- /dev/null
+++ b/rules/aip0131/request_required_fields_test.go
@@ -0,0 +1,104 @@
+// Copyright 2023 Google LLC
+//
+// 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
+//
+// 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 aip0131
+
+import (
+	"testing"
+
+	"github.com/googleapis/api-linter/rules/internal/testutils"
+	"github.com/jhump/protoreflect/desc"
+)
+
+func TestRequiredFieldTests(t *testing.T) {
+	for _, test := range []struct {
+		name                 string
+		Fields               string
+		problematicFieldName string
+		problems             testutils.Problems
+	}{
+		{
+			"ValidNoExtraFields",
+			"",
+			"",
+			nil,
+		},
+		{
+			"ValidOptionalReadMask",
+			"google.protobuf.FieldMask read_mask = 2 [(google.api.field_behavior) = OPTIONAL];",
+			"read_mask",
+			nil,
+		},
+		{
+			"InvalidRequiredReadMask",
+			"google.protobuf.FieldMask read_mask = 2 [(google.api.field_behavior) = REQUIRED];",
+			"read_mask",
+			testutils.Problems{
+				{Message: `Get RPCs must only require fields explicitly described in AIPs, not "read_mask"`},
+			},
+		},
+		{
+			"InvalidRequiredUnknownField",
+			"bool create_iam = 3 [(google.api.field_behavior) = REQUIRED];",
+			"create_iam",
+			testutils.Problems{
+				{Message: `Get RPCs must only require fields explicitly described in AIPs, not "create_iam"`},
+			},
+		},
+	} {
+		t.Run(test.name, func(t *testing.T) {
+			f := testutils.ParseProto3Tmpl(t, `
+				import "google/api/annotations.proto";
+				import "google/api/field_behavior.proto";
+				import "google/api/resource.proto";
+				import "google/protobuf/field_mask.proto";
+
+				service Library {
+					rpc GetBook(GetBookRequest) returns (Book) {
+						option (google.api.http) = {
+							get: "/v1/{name=publishers/*/books/*}"
+						};
+					}
+				}
+
+				message GetBookRequest {
+					// The name of the book to retrieve.
+					// Format: publishers/{publisher}/books/{book}
+					string name = 1 [
+					    (google.api.field_behavior) = REQUIRED,
+						(google.api.resource_reference) = {
+							type: "library.googleapis.com/Book"
+						}
+					];
+					{{.Fields}}
+				}
+
+				message Book {
+					option (google.api.resource) = {
+						type: "library.googleapis.com/Book"
+						pattern: "publishers/{publisher}/books/{book}"
+					};
+					string name = 1;
+				}
+			`, test)
+			var dbr desc.Descriptor = f.FindMessage("GetBookRequest")
+			if test.problematicFieldName != "" {
+				dbr = f.FindMessage("GetBookRequest").FindFieldByName(test.problematicFieldName)
+			}
+			if diff := test.problems.SetDescriptor(dbr).Diff(requestRequiredFields.Lint(f)); diff != "" {
+				t.Errorf(diff)
+			}
+		})
+	}
+}
diff --git a/rules/aip0132/request_required_fields_test.go b/rules/aip0132/request_required_fields_test.go
new file mode 100644
index 0000000..db52d13
--- /dev/null
+++ b/rules/aip0132/request_required_fields_test.go
@@ -0,0 +1,108 @@
+// Copyright 2023 Google LLC
+//
+// 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
+//
+// 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 aip0132
+
+import (
+	"testing"
+
+	"github.com/googleapis/api-linter/rules/internal/testutils"
+	"github.com/jhump/protoreflect/desc"
+)
+
+func TestRequiredFieldTests(t *testing.T) {
+	for _, test := range []struct {
+		name                 string
+		Fields               string
+		problematicFieldName string
+		problems             testutils.Problems
+	}{
+		{
+			"ValidNoExtraFields",
+			"",
+			"",
+			nil,
+		},
+		{
+			"ValidOptionalPageSize",
+			"int32 page_size = 2 [(google.api.field_behavior) = OPTIONAL];",
+			"page_size",
+			nil,
+		},
+		{
+			"InvalidRequiredPageSize",
+			"int32 page_size = 2 [(google.api.field_behavior) = REQUIRED];",
+			"page_size",
+			testutils.Problems{
+				{Message: `List RPCs must only require fields explicitly described in AIPs, not "page_size"`},
+			},
+		},
+		{
+			"InvalidRequiredUnknownField",
+			"bool create_iam = 3 [(google.api.field_behavior) = REQUIRED];",
+			"create_iam",
+			testutils.Problems{
+				{Message: `List RPCs must only require fields explicitly described in AIPs, not "create_iam"`},
+			},
+		},
+	} {
+		t.Run(test.name, func(t *testing.T) {
+			f := testutils.ParseProto3Tmpl(t, `
+				import "google/api/annotations.proto";
+				import "google/api/field_behavior.proto";
+				import "google/api/resource.proto";
+
+				service Library {
+					rpc ListBooks(ListBooksRequest) returns (ListBooksResponse) {
+						option (google.api.http) = {
+							get: "/v1/{parent=publishers/*}/books"
+						};
+					}
+				}
+
+				message ListBooksRequest {
+					// The parent, which owns this collection of books.
+					// Format: publishers/{publisher}
+					string parent = 1 [
+					    (google.api.field_behavior) = REQUIRED,
+					    (google.api.resource_reference) = {
+					  		child_type: "library.googleapis.com/Book"
+					    }];
+
+					{{.Fields}}
+				}
+
+				message ListBooksResponse {
+					repeated Book books = 1;
+					string next_page_token = 2;
+				}
+
+				message Book {
+					option (google.api.resource) = {
+						type: "library.googleapis.com/Book"
+						pattern: "publishers/{publisher}/books/{book}"
+					};
+					string name = 1;
+				}
+			`, test)
+			var dbr desc.Descriptor = f.FindMessage("ListBooksRequest")
+			if test.problematicFieldName != "" {
+				dbr = f.FindMessage("ListBooksRequest").FindFieldByName(test.problematicFieldName)
+			}
+			if diff := test.problems.SetDescriptor(dbr).Diff(requestRequiredFields.Lint(f)); diff != "" {
+				t.Errorf(diff)
+			}
+		})
+	}
+}
diff --git a/rules/aip0133/request_required_fields_test.go b/rules/aip0133/request_required_fields_test.go
index a16e370..c847638 100644
--- a/rules/aip0133/request_required_fields_test.go
+++ b/rules/aip0133/request_required_fields_test.go
@@ -64,34 +64,34 @@ func TestRequiredFieldTests(t *testing.T) {
 				import "google/api/resource.proto";
 
 				service Library {
-					rpc CreateBook(CreateBookRequest) returns (Book) {
+					rpc CreateBookShelf(CreateBookShelfRequest) returns (BookShelf) {
 						option (google.api.http) = {
-							delete: "/v1/{name=publishers/*/books/*}"
+							delete: "/v1/{name=publishers/*/bookShelves/*}"
 						};
 					}
 				}
 
-				message Book {
+				message BookShelf {
 					option (google.api.resource) = {
-						type: "library.googleapis.com/Book"
-						pattern: "publishers/{publisher}/books/{book}"
+						type: "library.googleapis.com/BookShelf"
+						pattern: "publishers/{publisher}/bookShelves/{book_shelf}"
 					};
 					string name = 1;
 				}
 
-				message CreateBookRequest {
+				message CreateBookShelfRequest {
 					string parent = 1 [
 						(google.api.field_behavior) = REQUIRED
 					];
-					Book book = 2 [
+					BookShelf book_shelf = 2 [
 						(google.api.field_behavior) = REQUIRED
 					];
 					{{.Fields}}
 				}
 			`, test)
-			var dbr desc.Descriptor = f.FindMessage("CreateBookRequest")
+			var dbr desc.Descriptor = f.FindMessage("CreateBookShelfRequest")
 			if test.problematicFieldName != "" {
-				dbr = f.FindMessage("CreateBookRequest").FindFieldByName(test.problematicFieldName)
+				dbr = f.FindMessage("CreateBookShelfRequest").FindFieldByName(test.problematicFieldName)
 			}
 			if diff := test.problems.SetDescriptor(dbr).Diff(requestRequiredFields.Lint(f)); diff != "" {
 				t.Errorf(diff)
diff --git a/rules/aip0134/request_required_fields_test.go b/rules/aip0134/request_required_fields_test.go
new file mode 100644
index 0000000..5b0e68b
--- /dev/null
+++ b/rules/aip0134/request_required_fields_test.go
@@ -0,0 +1,112 @@
+// Copyright 2023 Google LLC
+//
+// 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
+//
+// 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 aip0134
+
+import (
+	"testing"
+
+	"github.com/googleapis/api-linter/rules/internal/testutils"
+	"github.com/jhump/protoreflect/desc"
+)
+
+func TestRequiredFieldTests(t *testing.T) {
+	for _, test := range []struct {
+		name                 string
+		Fields               string
+		problematicFieldName string
+		problems             testutils.Problems
+	}{
+		{
+			"ValidNoExtraFields",
+			"",
+			"",
+			nil,
+		},
+		{
+			"ValidOptionalUpdateMask",
+			"google.protobuf.FieldMask update_mask = 2 [(google.api.field_behavior) = OPTIONAL];",
+			"update_mask",
+			nil,
+		},
+		{
+			"ValidRequiredUpdateMask",
+			"google.protobuf.FieldMask update_mask = 2 [(google.api.field_behavior) = REQUIRED];",
+			"update_mask",
+			nil,
+		},
+		{
+			"ValidOptionalValidateOnly",
+			"bool validate_only = 3 [(google.api.field_behavior) = OPTIONAL];",
+			"validate_only",
+			nil,
+		},
+		{
+			"InvalidRequiredValidateOnly",
+			"bool validate_only = 3 [(google.api.field_behavior) = REQUIRED];",
+			"validate_only",
+			testutils.Problems{
+				{Message: `Update RPCs must only require fields explicitly described in AIPs, not "validate_only"`},
+			},
+		},
+		{
+			"InvalidRequiredUnknownField",
+			"bool create_iam = 3 [(google.api.field_behavior) = REQUIRED];",
+			"create_iam",
+			testutils.Problems{
+				{Message: `Update RPCs must only require fields explicitly described in AIPs, not "create_iam"`},
+			},
+		},
+	} {
+		t.Run(test.name, func(t *testing.T) {
+			f := testutils.ParseProto3Tmpl(t, `
+				import "google/api/annotations.proto";
+				import "google/api/field_behavior.proto";
+				import "google/api/resource.proto";
+				import "google/protobuf/field_mask.proto";
+
+				service Library {
+					rpc UpdateBookShelf(UpdateBookShelfRequest) returns (BookShelf) {
+						option (google.api.http) = {
+							patch: "/v1/{name=publishers/*/bookShelves/*}"
+							body: "book"
+						};
+					}
+				}
+
+				message BookShelf {
+					option (google.api.resource) = {
+						type: "library.googleapis.com/BookShelf"
+						pattern: "publishers/{publisher}/bookShelves/{book_shelf}"
+					};
+					string name = 1;
+				}
+
+				message UpdateBookShelfRequest {
+					BookShelf book_shelf = 1 [
+						(google.api.field_behavior) = REQUIRED
+					];
+					{{.Fields}}
+				}
+			`, test)
+			var dbr desc.Descriptor = f.FindMessage("UpdateBookShelfRequest")
+			if test.problematicFieldName != "" {
+				dbr = f.FindMessage("UpdateBookShelfRequest").FindFieldByName(test.problematicFieldName)
+			}
+			if diff := test.problems.SetDescriptor(dbr).Diff(requestRequiredFields.Lint(f)); diff != "" {
+				t.Errorf(diff)
+			}
+		})
+	}
+}
diff --git a/rules/aip0135/request_required_fields_test.go b/rules/aip0135/request_required_fields_test.go
new file mode 100644
index 0000000..f1a4aa8
--- /dev/null
+++ b/rules/aip0135/request_required_fields_test.go
@@ -0,0 +1,95 @@
+// Copyright 2023 Google LLC
+//
+// 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
+//
+// 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 aip0135
+
+import (
+	"testing"
+
+	"github.com/googleapis/api-linter/rules/internal/testutils"
+	"github.com/jhump/protoreflect/desc"
+)
+
+func TestRequiredFieldTests(t *testing.T) {
+	for _, test := range []struct {
+		name                 string
+		Fields               string
+		problematicFieldName string
+		problems             testutils.Problems
+	}{
+		{
+			"ValidNoExtraFields",
+			"",
+			"",
+			nil,
+		},
+		{
+			"ValidOptionalAllowMissing",
+			"bool allow_missing = 2 [(google.api.field_behavior) = OPTIONAL];",
+			"allow_missing",
+			nil,
+		},
+		{
+			"InvalidRequiredAllowMissing",
+			"bool allow_missing = 2 [(google.api.field_behavior) = REQUIRED];",
+			"allow_missing",
+			testutils.Problems{
+				{Message: `Delete RPCs must only require fields explicitly described in AIPs, not "allow_missing"`},
+			},
+		},
+		{
+			"InvalidRequiredUnknownField",
+			"bool create_iam = 2 [(google.api.field_behavior) = REQUIRED];",
+			"create_iam",
+			testutils.Problems{
+				{Message: `Delete RPCs must only require fields explicitly described in AIPs, not "create_iam"`},
+			},
+		},
+	} {
+		t.Run(test.name, func(t *testing.T) {
+			f := testutils.ParseProto3Tmpl(t, `
+				import "google/api/annotations.proto";
+				import "google/api/field_behavior.proto";
+				import "google/api/resource.proto";
+			    import "google/longrunning/operations.proto";
+
+				service Library {
+					rpc DeleteBook(DeleteBookRequest) returns (google.longrunning.Operation) {
+						option (google.api.http) = {
+						    delete: "/v1/{name=publishers/*/books/*}"
+						};
+						option (google.longrunning.operation_info) = {
+							response_type: "google.protobuf.Empty"
+							metadata_type: "OperationMetadata"
+						};
+					}
+				}
+
+				message DeleteBookRequest {
+					string name = 1 [
+						(google.api.field_behavior) = REQUIRED
+					];
+					{{.Fields}}
+				}
+			`, test)
+			var dbr desc.Descriptor = f.FindMessage("DeleteBookRequest")
+			if test.problematicFieldName != "" {
+				dbr = f.FindMessage("DeleteBookRequest").FindFieldByName(test.problematicFieldName)
+			}
+			if diff := test.problems.SetDescriptor(dbr).Diff(requestRequiredFields.Lint(f)); diff != "" {
+				t.Errorf(diff)
+			}
+		})
+	}
+}
diff --git a/rules/internal/utils/resource_test.go b/rules/internal/utils/resource_test.go
new file mode 100644
index 0000000..5f27673
--- /dev/null
+++ b/rules/internal/utils/resource_test.go
@@ -0,0 +1,71 @@
+// Copyright 2023 Google LLC
+//
+// 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
+//
+// 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 utils
+
+import (
+	"testing"
+
+	apb "google.golang.org/genproto/googleapis/api/annotations"
+)
+
+func TestMatches(t *testing.T) {
+	for _, test := range []struct {
+		name     string
+		resource *apb.ResourceDescriptor
+		want     string
+	}{
+		{
+			name: "SingularSpecified",
+			resource: &apb.ResourceDescriptor{
+				Singular: "bookShelf",
+			},
+			want: "bookShelf",
+		},
+		{
+			name: "SingularAndTypeSpecified",
+			resource: &apb.ResourceDescriptor{
+				Singular: "bookShelf",
+				// NOTE: this is not a correct resource annotation.
+				// it must match singular.
+				Type: "library.googleapis.com/book",
+			},
+			want: "bookShelf",
+		},
+		{
+			name: "TypeSpecified",
+			resource: &apb.ResourceDescriptor{
+				Type: "library.googleapis.com/bookShelf",
+			},
+			want: "bookShelf",
+		},
+		{
+			name:     "NothingSpecified",
+			resource: &apb.ResourceDescriptor{},
+			want:     "",
+		},
+		{
+			name:     "Nil",
+			resource: nil,
+			want:     "",
+		},
+	} {
+		t.Run(test.name, func(t *testing.T) {
+			got := GetResourceSingular(test.resource)
+			if got != test.want {
+				t.Errorf("GetResourceSingular: expected %v, got %v", test.want, got)
+			}
+		})
+	}
+}