Repository: googleapis/api-linter
Pull request #199 (2019-10-02 16:33:05)

[AIP-135] Add Delete Method rules
Also some small other fixes.

This whole thing is basically cribbed from AIP 131, so please be on the lookout for places where I accidentally reference "GET" instead of "DELETE".

googlebot: We found a Contributor License Agreement for you (the sender of this pull request), but were unable to find agreements for all the commit author(s) or Co-authors.  If you authored these, maybe you used a different email address in the git commits than was used to sign the CLA ([login here](https://cla.developers.google.com/) to double check)?  If these were authored by someone else, then they will need to sign a CLA as well, and confirm that they're okay with these being contributed to Google.
In order to pass this check, please resolve this problem and then comment `@googlebot I fixed it.`. If the bot doesn't comment, it means it doesn't think anything has changed.

ℹ️ **Googlers: [Go here](https://goto.google.com/prinfo/https%3A%2F%2Fgithub.com%2Fgoogleapis%2Fapi-linter%2Fpull%2F199) for more info**.

<!-- need_author_cla -->
googlebot: We found a Contributor License Agreement for you (the sender of this pull request), but were unable to find agreements for all the commit author(s) or Co-authors.  If you authored these, maybe you used a different email address in the git commits than was used to sign the CLA ([login here](https://cla.developers.google.com/) to double check)?  If these were authored by someone else, then they will need to sign a CLA as well, and confirm that they're okay with these being contributed to Google.
In order to pass this check, please resolve this problem and then comment `@googlebot I fixed it.`. If the bot doesn't comment, it means it doesn't think anything has changed.

ℹ️ **Googlers: [Go here](https://goto.google.com/prinfo/https%3A%2F%2Fgithub.com%2Fgoogleapis%2Fapi-linter%2Fpull%2F199) for more info**.

<!-- need_author_cla -->
lukesneeringer: @googlebot I fixed it.
googlebot: CLAs look good, thanks!

ℹ️ **Googlers: [Go here](https://goto.google.com/prinfo/https%3A%2F%2Fgithub.com%2Fgoogleapis%2Fapi-linter%2Fpull%2F199) for more info**.

<!-- ok -->
michaelbausor: PTAL

Patch:
diff --git a/rules/aip0131/method_rules.go b/rules/aip0131/method_rules.go
index eb28b7a..308e3b9 100644
--- a/rules/aip0131/method_rules.go
+++ b/rules/aip0131/method_rules.go
@@ -90,7 +90,7 @@ var httpVerb = &lint.MethodRule{
 
 // Get methods should have a proper HTTP pattern.
 var httpNameField = &lint.MethodRule{
-	Name:   lint.NewRuleName("core", "0131", "http-body"),
+	Name:   lint.NewRuleName("core", "0131", "http-name"),
 	URI:    "https://aip.dev/131#guidance",
 	OnlyIf: isGetMethod,
 	LintMethod: func(m *desc.MethodDescriptor) []lint.Problem {
@@ -112,7 +112,7 @@ var httpNameField = &lint.MethodRule{
 
 // Get methods should not have an HTTP body.
 var httpBody = &lint.MethodRule{
-	Name:   lint.NewRuleName("core", "0131", "http-name"),
+	Name:   lint.NewRuleName("core", "0131", "http-body"),
 	URI:    "https://aip.dev/131#guidance",
 	OnlyIf: isGetMethod,
 	LintMethod: func(m *desc.MethodDescriptor) []lint.Problem {
diff --git a/rules/aip0135/aip0135.go b/rules/aip0135/aip0135.go
new file mode 100644
index 0000000..56afb3e
--- /dev/null
+++ b/rules/aip0135/aip0135.go
@@ -0,0 +1,51 @@
+// Copyright 2019 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 contains rules defined in https://aip.dev/135.
+package aip0135
+
+import (
+	"regexp"
+
+	"github.com/googleapis/api-linter/lint"
+	"github.com/jhump/protoreflect/desc"
+)
+
+// AddRules accepts a register function and registers each of
+// this AIP's rules to it.
+func AddRules(r lint.RuleRegistry) {
+	r.Register(
+		httpBody,
+		httpNameField,
+		httpVerb,
+		responseMessageName,
+		requestMessageName,
+		standardFields,
+		unknownFields,
+	)
+}
+
+var deleteMethodRegexp = regexp.MustCompile("^Delete(?:[A-Z]|$)")
+var deleteReqMessageRegexp = regexp.MustCompile("^Delete[A-Za-z0-9]*Request$")
+var deleteURINameRegexp = regexp.MustCompile("\\{name=[a-zA-Z/*]+\\}$")
+
+// Returns true if this is a AIP-135 Delete method, false otherwise.
+func isDeleteMethod(m *desc.MethodDescriptor) bool {
+	return deleteMethodRegexp.MatchString(m.GetName())
+}
+
+// Returns true if this is an AIP-135 Delete request message, false otherwise.
+func isDeleteRequestMessage(m *desc.MessageDescriptor) bool {
+	return deleteReqMessageRegexp.MatchString(m.GetName())
+}
diff --git a/rules/aip0135/message_rules.go b/rules/aip0135/message_rules.go
new file mode 100644
index 0000000..a338928
--- /dev/null
+++ b/rules/aip0135/message_rules.go
@@ -0,0 +1,78 @@
+// Copyright 2019 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"
+
+	"github.com/googleapis/api-linter/lint"
+	"github.com/jhump/protoreflect/desc"
+	"github.com/jhump/protoreflect/desc/builder"
+)
+
+// The Delete standard method should only have expected fields.
+var standardFields = &lint.MessageRule{
+	Name:   lint.NewRuleName("core", "0135", "request-message", "name-field"),
+	URI:    "https://aip.dev/135#request-message",
+	OnlyIf: isDeleteRequestMessage,
+	LintMessage: func(m *desc.MessageDescriptor) []lint.Problem {
+		// Rule check: Establish that a name field is present.
+		name := m.FindFieldByName("name")
+		if name == nil {
+			return []lint.Problem{{
+				Message:    fmt.Sprintf("Method %q has no `name` field", m.GetName()),
+				Descriptor: m,
+			}}
+		}
+
+		// Rule check: Ensure that the name field is the correct type.
+		if name.GetType() != builder.FieldTypeString().GetType() {
+			return []lint.Problem{{
+				Message:    "`name` field on Get RPCs should be a string",
+				Descriptor: name,
+			}}
+		}
+
+		return nil
+	},
+}
+
+// Delete methods should not have unrecognized fields.
+var unknownFields = &lint.MessageRule{
+	Name:   lint.NewRuleName("core", "0135", "request-message", "unknown-fields"),
+	URI:    "https://aip.dev/135#request-message",
+	OnlyIf: isDeleteRequestMessage,
+	LintMessage: func(m *desc.MessageDescriptor) (problems []lint.Problem) {
+		// Rule check: Establish that there are no unexpected fields.
+		allowedFields := map[string]struct{}{
+			"name":      {}, // AIP-135
+			"force":     {}, // AIP-135
+			"etag":      {}, // AIP-154
+		}
+		for _, field := range m.GetFields() {
+			if _, ok := allowedFields[string(field.GetName())]; !ok {
+				problems = append(problems, lint.Problem{
+					Message: fmt.Sprintf(
+						"Unexpected field: Delete RPCs must only contain fields explicitly described in AIPs, not %q.",
+						string(field.GetName()),
+					),
+					Descriptor: field,
+				})
+			}
+		}
+
+		return
+	},
+}
diff --git a/rules/aip0135/method_rules.go b/rules/aip0135/method_rules.go
new file mode 100644
index 0000000..931775a
--- /dev/null
+++ b/rules/aip0135/method_rules.go
@@ -0,0 +1,139 @@
+// Copyright 2019 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"
+
+	"github.com/googleapis/api-linter/rules/internal/utils"
+	"github.com/googleapis/api-linter/lint"
+	"github.com/jhump/protoreflect/desc"
+)
+
+// Delete messages should have a properly named Request message.
+var requestMessageName = &lint.MethodRule{
+	Name:   lint.NewRuleName("core", "0135", "request-message", "name"),
+	URI:    "https://aip.dev/135#guidance",
+	OnlyIf: isDeleteMethod,
+	LintMethod: func(m *desc.MethodDescriptor) []lint.Problem {
+		// Rule check: Establish that for methods such as `DeleteFoo`, the request
+		// message is named `DeleteFooRequest`.
+		if got, want := m.GetInputType().GetName(), m.GetName()+"Request"; got != want {
+			return []lint.Problem{{
+				Message: fmt.Sprintf(
+					"Delete RPCs should have a request message named after the RPC, such as %q.",
+					want,
+				),
+				Suggestion: want,
+				Descriptor: m,
+			}}
+		}
+
+		return nil
+	},
+}
+
+// Delete messages should use google.protobuf.Empty, google.longrunning.Operation or the resource itself as the response message
+var responseMessageName = &lint.MethodRule{
+	Name:   lint.NewRuleName("core", "0135", "response-message", "name"),
+	URI:    "https://aip.dev/135#guidance",
+	OnlyIf: isDeleteMethod,
+	LintMethod: func(m *desc.MethodDescriptor) []lint.Problem {
+		// Rule check: Establish that for methods such as `DeleteFoo`, the response
+		// message is `google.protobuf.Empty`, `google.longrunning.Operation`, or `Foo`.
+		got := m.GetOutputType().GetFullyQualifiedName()
+		want := []string{
+			"google.protobuf.Empty",
+			"google.longrunning.Operation",
+			m.GetName()[6:],
+		}
+		for _, v := range want {
+			if got == v {
+				return nil
+			}
+		}
+		return []lint.Problem{{
+			Message: fmt.Sprintf(
+				"Delete RPCs should have response message type in %q, not %q.",
+				want,
+				got,
+			),
+			Suggestion: want[0],
+			Descriptor: m,
+		}}
+	},
+}
+
+// Delete methods should use the HTTP DELETE verb.
+var httpVerb = &lint.MethodRule{
+	Name:   lint.NewRuleName("core", "0135", "http-verb"),
+	URI:    "https://aip.dev/135#guidance",
+	OnlyIf: isDeleteMethod,
+	LintMethod: func(m *desc.MethodDescriptor) []lint.Problem {
+		// Rule check: Establish that the RPC uses HTTP DELETE.
+		for _, httpRule := range utils.GetHTTPRules(m) {
+			if httpRule.GetDelete() == "" {
+				return []lint.Problem{{
+					Message:    "Delete methods must use the HTTP DELETE verb.",
+					Descriptor: m,
+				}}
+			}
+		}
+
+		return nil
+	},
+}
+
+// Delete methods should have a proper HTTP pattern.
+var httpNameField = &lint.MethodRule{
+	Name:   lint.NewRuleName("core", "0135", "http-name"),
+	URI:    "https://aip.dev/135#guidance",
+	OnlyIf: isDeleteMethod,
+	LintMethod: func(m *desc.MethodDescriptor) []lint.Problem {
+		// Establish that the RPC has no HTTP body.
+		for _, httpRule := range utils.GetHTTPRules(m) {
+			if uri := httpRule.GetDelete(); uri != "" {
+				if !deleteURINameRegexp.MatchString(uri) {
+					return []lint.Problem{{
+						Message:    "Delete methods should include the `name` field in the URI.",
+						Descriptor: m,
+					}}
+				}
+			}
+		}
+
+		return nil
+	},
+}
+
+// Delete methods should not have an HTTP body.
+var httpBody = &lint.MethodRule{
+	Name:   lint.NewRuleName("core", "0135", "http-body"),
+	URI:    "https://aip.dev/131#guidance",
+	OnlyIf: isDeleteMethod,
+	LintMethod: func(m *desc.MethodDescriptor) []lint.Problem {
+		// Establish that the RPC has no HTTP body.
+		for _, httpRule := range utils.GetHTTPRules(m) {
+			if httpRule.GetBody() != "" {
+				return []lint.Problem{{
+					Message:    "Delete methods should not have an HTTP body.",
+					Descriptor: m,
+				}}
+			}
+		}
+
+		return nil
+	},
+}
diff --git a/rules/aip0158/aip0158.go b/rules/aip0158/aip0158.go
index 1e94cb1..72a04d4 100644
--- a/rules/aip0158/aip0158.go
+++ b/rules/aip0158/aip0158.go
@@ -39,7 +39,7 @@ func isListRequestMessage(m *desc.MessageDescriptor) bool {
 	return listReqMessageRegexp.MatchString(m.GetName())
 }
 
-// Return true if this is an AIP-158 List request message, false otherwise.
+// Return true if this is an AIP-158 List response message, false otherwise.
 func isListResponseMessage(m *desc.MessageDescriptor) bool {
 	return listResMessageRegexp.MatchString(m.GetName())
 }


Test patch:
diff --git a/rules/aip0135/aip0135_test.go b/rules/aip0135/aip0135_test.go
new file mode 100644
index 0000000..3136705
--- /dev/null
+++ b/rules/aip0135/aip0135_test.go
@@ -0,0 +1,32 @@
+// Copyright 2019 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 (
+	"strings"
+	"testing"
+
+	"github.com/googleapis/api-linter/lint"
+)
+
+func TestAddRules(t *testing.T) {
+	rules := make(lint.RuleRegistry)
+	AddRules(rules)
+	for ruleName := range rules {
+		if !strings.HasPrefix(string(ruleName), "core::0135") {
+			t.Errorf("Rule %s is not namespaced to core::0135.", ruleName)
+		}
+	}
+}
diff --git a/rules/aip0135/message_rules_test.go b/rules/aip0135/message_rules_test.go
new file mode 100644
index 0000000..cebcf2a
--- /dev/null
+++ b/rules/aip0135/message_rules_test.go
@@ -0,0 +1,116 @@
+// Copyright 2019 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/builder"
+)
+
+func TestStandardFields(t *testing.T) {
+	// Set up the testing permutations.
+	tests := []struct {
+		testName      string
+		messageName   string
+		nameFieldName string
+		problems      testutils.Problems
+	}{
+		{"Valid", "DeleteBookRequest", "name", testutils.Problems{}},
+		{"InvalidName", "DeleteBookRequest", "id", testutils.Problems{{Message: "name"}}},
+		{"Irrelevant", "RemoveBookRequest", "id", testutils.Problems{}},
+	}
+
+	// Run each test individually.
+	for _, test := range tests {
+		t.Run(test.testName, func(t *testing.T) {
+			// Create an appropriate message descriptor.
+			message, err := builder.NewMessage(test.messageName).AddField(
+				builder.NewField(test.nameFieldName, builder.FieldTypeString()),
+			).Build()
+			if err != nil {
+				t.Fatalf("Could not build %s message.", test.messageName)
+			}
+
+			// Run the lint rule, and establish that it returns the correct problems.
+			problems := standardFields.Lint(message.GetFile())
+			if diff := test.problems.SetDescriptor(message).Diff(problems); diff != "" {
+				t.Errorf("Problems did not match: %v", diff)
+			}
+		})
+	}
+}
+
+func TestStandardFieldsInvalidType(t *testing.T) {
+	// Create an appropriate message descriptor.
+	message, err := builder.NewMessage("DeleteBookRequest").AddField(
+		builder.NewField("name", builder.FieldTypeBytes()),
+	).Build()
+	if err != nil {
+		t.Fatalf("Could not build descriptor.")
+	}
+
+	// Run the lint rule, and establish that it returns the correct
+	// number of problems.
+	wantProblems := testutils.Problems{{
+		Descriptor: message.GetFields()[0],
+		Message:    "string",
+	}}
+	gotProblems := standardFields.Lint(message.GetFile())
+	if diff := wantProblems.Diff(gotProblems); diff != "" {
+		t.Errorf(diff)
+	}
+}
+
+func TestUnknownFields(t *testing.T) {
+	// Set up the testing permutations.
+	tests := []struct {
+		testName    string
+		messageName string
+		fieldName   string
+		fieldType   *builder.FieldType
+		problems    testutils.Problems
+	}{
+		{"Force", "DeleteBookRequest", "force", builder.FieldTypeBool(), testutils.Problems{}},
+		{"Etag", "DeleteBookRequest", "etag", builder.FieldTypeString(), testutils.Problems{}},
+		{"Invalid", "DeleteBookRequest", "application_id", builder.FieldTypeString(), testutils.Problems{{
+			Message: "Unexpected field",
+		}}},
+		{"Irrelevant", "RemoveBookRequest", "application_id", builder.FieldTypeString(), testutils.Problems{}},
+	}
+
+	// Run each test individually.
+	for _, test := range tests {
+		t.Run(test.testName, func(t *testing.T) {
+			// Create an appropriate message descriptor.
+			message, err := builder.NewMessage(test.messageName).AddField(
+				builder.NewField("name", builder.FieldTypeString()),
+			).AddField(
+				builder.NewField(test.fieldName, test.fieldType),
+			).Build()
+			if err != nil {
+				t.Fatalf("Could not build DeleteBookRequest message.")
+			}
+
+			// Run the lint rule, and establish that it returns the correct problems.
+			wantProblems := test.problems.SetDescriptor(message.FindFieldByName(test.fieldName))
+			gotProblems := unknownFields.Lint(message.GetFile())
+			if diff := wantProblems.Diff(gotProblems); diff != "" {
+				t.Errorf(diff)
+			}
+		})
+	}
+}
diff --git a/rules/aip0135/method_rules_test.go b/rules/aip0135/method_rules_test.go
new file mode 100644
index 0000000..04deb9d
--- /dev/null
+++ b/rules/aip0135/method_rules_test.go
@@ -0,0 +1,270 @@
+// Copyright 2019 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 (
+	"strings"
+	"testing"
+
+	"github.com/golang/protobuf/proto"
+	dpb "github.com/golang/protobuf/protoc-gen-go/descriptor"
+	"github.com/googleapis/api-linter/rules/internal/testutils"
+	epb "github.com/golang/protobuf/ptypes/empty"
+	"github.com/jhump/protoreflect/desc"
+	"github.com/jhump/protoreflect/desc/builder"
+	"google.golang.org/genproto/googleapis/api/annotations"
+	lro "google.golang.org/genproto/googleapis/longrunning"
+)
+
+func TestRequestMessageName(t *testing.T) {
+	// Set up the testing permutations.
+	tests := []struct {
+		testName       string
+		methodName     string
+		reqMessageName string
+		problems       testutils.Problems
+	}{
+		{"Valid", "DeleteBook", "DeleteBookRequest", testutils.Problems{}},
+		{"Invalid", "DeleteBook", "Book", testutils.Problems{{Suggestion: "DeleteBookRequest"}}},
+		{"Irrelevant", "AcquireBook", "Book", testutils.Problems{}},
+	}
+
+	// Run each test individually.
+	for _, test := range tests {
+		t.Run(test.testName, func(t *testing.T) {
+			// Create a minimal service with a AIP-135 Delete method
+			// (or with a different method, in the "Irrelevant" case).
+			service, err := builder.NewService("Library").AddMethod(builder.NewMethod(test.methodName,
+				builder.RpcTypeMessage(builder.NewMessage(test.reqMessageName), false),
+				builder.RpcTypeMessage(builder.NewMessage("Book"), false),
+			)).Build()
+			if err != nil {
+				t.Fatalf("Could not build %s method.", test.methodName)
+			}
+
+			// Run the lint rule, and establish that it returns the expected problems.
+			problems := requestMessageName.Lint(service.GetFile())
+			if diff := test.problems.SetDescriptor(service.GetMethods()[0]).Diff(problems); diff != "" {
+				t.Errorf(diff)
+			}
+		})
+	}
+}
+
+func TestResponseMessageName(t *testing.T) {
+	// Get the correct message type for google.protobuf.Empty
+	empty, err := desc.LoadMessageDescriptorForMessage(&epb.Empty{})
+	if err != nil {
+		t.Fatalf("Unable to load the empty message.")
+	}
+	emptybldr, err := builder.FromMessage(empty)
+	if err != nil {
+		t.Fatalf("Unable to construct builder from empty desc.")
+	}
+	op, err := desc.LoadMessageDescriptorForMessage(&lro.Operation{})
+	if err != nil {
+		t.Fatalf("Unable to load the Operation message.")
+	}
+	opbldr, err := builder.FromMessage(op)
+	if err != nil {
+		t.Fatalf("Unable to construct builder from op desc.")
+	}
+
+	// Set up the testing permutations.
+	tests := []struct {
+		testName     string
+		methodName   string
+		respMessage  *builder.MessageBuilder
+		problems     testutils.Problems
+	}{
+		{"ValidEmpty", "DeleteBook", emptybldr, testutils.Problems{}},
+		{"ValidLRO", "DeleteBook", opbldr, testutils.Problems{}},
+		{"ValidResource", "DeleteBook", builder.NewMessage("Book"), testutils.Problems{}},
+		{"Invalid", "DeleteBook", builder.NewMessage("DeleteBookResponse"), testutils.Problems{{Suggestion: "google.protobuf.Empty"}}},
+		{"Irrelevant", "AcquireBook", builder.NewMessage("AcquireBookResponse"), testutils.Problems{}},
+	}
+
+	// Run each test individually.
+	for _, test := range tests {
+		t.Run(test.testName, func(t *testing.T) {
+			// Create a minimal service with a AIP-135 Delete method
+			// (or with a different method, in the "Irrelevant" case).
+			service, err := builder.NewService("Library").AddMethod(builder.NewMethod(test.methodName,
+				builder.RpcTypeMessage(builder.NewMessage("DeleteBookRequest"), false),
+				builder.RpcTypeMessage(test.respMessage, false),
+			)).Build()
+			if err != nil {
+				t.Fatalf("Could not build %s method.", test.methodName)
+			}
+
+			// Run the lint rule, and establish that it returns the expected problems.
+			problems := responseMessageName.Lint(service.GetFile())
+			if diff := test.problems.SetDescriptor(service.GetMethods()[0]).Diff(problems); diff != "" {
+				t.Errorf(diff)
+			}
+		})
+	}
+}
+
+func TestHttpVerb(t *testing.T) {
+	// Set up GET and DELETE HTTP annotations.
+	httpGet := &annotations.HttpRule{
+		Pattern: &annotations.HttpRule_Get{
+			Get: "/v1/{name=publishers/*/books/*}",
+		},
+	}
+	httpDelete := &annotations.HttpRule{
+		Pattern: &annotations.HttpRule_Delete{
+			Delete: "/v1/{name=publishers/*/books/*}",
+		},
+	}
+
+	// Set up testing permutations.
+	tests := []struct {
+		testName   string
+		httpRule   *annotations.HttpRule
+		methodName string
+		msg        string
+	}{
+		{"Valid", httpDelete, "GeDeleteBook", ""},
+		{"Invalid", httpGet, "DeleteBook", "HTTP DELETE"},
+		{"Irrelevant", httpGet, "AcquireBook", ""},
+	}
+
+	// Run each test.
+	for _, test := range tests {
+		t.Run(test.testName, func(t *testing.T) {
+			// Create a MethodOptions with the annotation set.
+			opts := &dpb.MethodOptions{}
+			if err := proto.SetExtension(opts, annotations.E_Http, test.httpRule); err != nil {
+				t.Fatalf("Failed to set google.api.http annotation.")
+			}
+
+			// Create a minimal service with a AIP-135 Delete method
+			// (or with a different method, in the "Irrelevant" case).
+			service, err := builder.NewService("Library").AddMethod(builder.NewMethod(test.methodName,
+				builder.RpcTypeMessage(builder.NewMessage("DeleteBookRequest"), false),
+				builder.RpcTypeMessage(builder.NewMessage("Book"), false),
+			).SetOptions(opts)).Build()
+			if err != nil {
+				t.Fatalf("Could not build %s method.", test.methodName)
+			}
+
+			// Run the method, ensure we get what we expect.
+			problems := httpVerb.Lint(service.GetFile())
+			if test.msg == "" && len(problems) > 0 {
+				t.Errorf("Got %v, expected no problems.", problems)
+			} else if test.msg != "" && !strings.Contains(problems[0].Message, test.msg) {
+				t.Errorf("Got %q, expected message containing %q", problems[0].Message, test.msg)
+			}
+		})
+	}
+}
+
+func TestHttpBody(t *testing.T) {
+	tests := []struct {
+		testName   string
+		body       string
+		methodName string
+		msg        string
+	}{
+		{"Valid", "", "DeleteBook", ""},
+		{"Invalid", "*", "DeleteBook", "HTTP body"},
+		{"Irrelevant", "*", "AcquireBook", ""},
+	}
+
+	for _, test := range tests {
+		t.Run(test.testName, func(t *testing.T) {
+			// Create a MethodOptions with the annotation set.
+			opts := &dpb.MethodOptions{}
+			httpRule := &annotations.HttpRule{
+				Pattern: &annotations.HttpRule_Get{
+					Get: "/v1/{name=publishers/*/books/*}",
+				},
+				Body: test.body,
+			}
+			if err := proto.SetExtension(opts, annotations.E_Http, httpRule); err != nil {
+				t.Fatalf("Failed to set google.api.http annotation.")
+			}
+
+			// Create a minimal service with a AIP-135 Get method
+			// (or with a different method, in the "Irrelevant" case).
+			service, err := builder.NewService("Library").AddMethod(builder.NewMethod(test.methodName,
+				builder.RpcTypeMessage(builder.NewMessage("DeleteBookRequest"), false),
+				builder.RpcTypeMessage(builder.NewMessage("Book"), false),
+			).SetOptions(opts)).Build()
+			if err != nil {
+				t.Fatalf("Could not build %s method.", test.methodName)
+			}
+
+			// Run the method, ensure we get what we expect.
+			problems := httpBody.Lint(service.GetFile())
+			if test.msg == "" && len(problems) > 0 {
+				t.Errorf("Got %v, expected no problems.", problems)
+			} else if test.msg != "" && !strings.Contains(problems[0].Message, test.msg) {
+				t.Errorf("Got %q, expected message containing %q", problems[0].Message, test.msg)
+			}
+		})
+	}
+}
+
+func TestHttpNameField(t *testing.T) {
+	tests := []struct {
+		testName   string
+		uri        string
+		methodName string
+		msg        string
+	}{
+		{"Valid", "/v1/{name=publishers/*/books/*}", "DeleteBook", ""},
+		{"InvalidVarName", "/v1/{book=publishers/*/books/*}", "DeleteBook", "`name` field"},
+		{"NoVarName", "/v1/publishers/*/books/*", "DeleteBook", "`name` field"},
+		{"Irrelevant", "/v1/{book=publishers/*/books/*}", "AcquireBook", ""},
+	}
+
+	for _, test := range tests {
+		t.Run(test.testName, func(t *testing.T) {
+			// Create a MethodOptions with the annotation set.
+			opts := &dpb.MethodOptions{}
+			httpRule := &annotations.HttpRule{
+				Pattern: &annotations.HttpRule_Delete{
+					Delete: test.uri,
+				},
+			}
+			if err := proto.SetExtension(opts, annotations.E_Http, httpRule); err != nil {
+				t.Fatalf("Failed to set google.api.http annotation.")
+			}
+
+			// Create a minimal service with a AIP-135 Delete method
+			// (or with a different method, in the "Irrelevant" case).
+			service, err := builder.NewService("Library").AddMethod(builder.NewMethod(test.methodName,
+				builder.RpcTypeMessage(builder.NewMessage("DeleteBookRequest"), false),
+				builder.RpcTypeMessage(builder.NewMessage("Book"), false),
+			).SetOptions(opts)).Build()
+			if err != nil {
+				t.Fatalf("Could not build %s method.", test.methodName)
+			}
+
+			// Run the method, ensure we get what we expect.
+			problems := httpNameField.Lint(service.GetFile())
+			if test.msg == "" && len(problems) > 0 {
+				t.Errorf("Got %v, expected no problems.", problems)
+			} else if test.msg != "" && len(problems) == 0 {
+				t.Errorf("Got no problems, expected 1.")
+			} else if test.msg != "" && !strings.Contains(problems[0].Message, test.msg) {
+				t.Errorf("Got %q, expected message containing %q", problems[0].Message, test.msg)
+			}
+		})
+	}
+}