Repository: googleapis/api-linter
Pull request #289 (2019-11-03 21:46:40)

[AIP-135] Document all AIP-135 rules.


Patch:
diff --git a/docs/rules/core/0131-http-body.md b/docs/rules/core/0131-http-body.md
index da60bdf..fc262ee 100644
--- a/docs/rules/core/0131-http-body.md
+++ b/docs/rules/core/0131-http-body.md
@@ -5,7 +5,7 @@ rule:
   summary: Get methods must not have an HTTP body.
 ---
 
-# Get methods: Request message
+# Get methods: No HTTP body
 
 This rule enforces that all `Get` RPCs omit the HTTP `body`, as mandated in
 [AIP-131][].
diff --git a/docs/rules/core/0131-http-uri-name.md b/docs/rules/core/0131-http-uri-name.md
index 73044d1..c88c95f 100644
--- a/docs/rules/core/0131-http-uri-name.md
+++ b/docs/rules/core/0131-http-uri-name.md
@@ -5,7 +5,7 @@ rule:
   summary: Get methods must map the name field to the URI.
 ---
 
-# Get methods: Request message
+# Get methods: HTTP URI name field
 
 This rule enforces that all `Get` RPCs map the `name` field to the HTTP URI, as
 mandated in [AIP-131][].
diff --git a/docs/rules/core/0131-request-name-field.md b/docs/rules/core/0131-request-name-field.md
index 600eed6..f10f2b8 100644
--- a/docs/rules/core/0131-request-name-field.md
+++ b/docs/rules/core/0131-request-name-field.md
@@ -8,7 +8,7 @@ rule:
 # Get methods: Name field
 
 This rule enforces that all `Get` standard methods have a `string name` field
-in the request message, as mandated in [AIP-131](http://aip.dev/131).
+in the request message, as mandated in [AIP-131][].
 
 ## Details
 
@@ -59,4 +59,5 @@ message GetBookRequest {
 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/core/0131-request-unknown-fields.md b/docs/rules/core/0131-request-unknown-fields.md
index 50dc906..13c05e6 100644
--- a/docs/rules/core/0131-request-unknown-fields.md
+++ b/docs/rules/core/0131-request-unknown-fields.md
@@ -26,7 +26,7 @@ across any fields other than:
 ```proto
 // Incorrect.
 message GetBookRequest {
-  string book = 1;
+  string name = 1;
   string library_id = 2;  // Non-standard field.
 }
 ```
diff --git a/docs/rules/core/0135-http-body.md b/docs/rules/core/0135-http-body.md
new file mode 100644
index 0000000..0edd2fd
--- /dev/null
+++ b/docs/rules/core/0135-http-body.md
@@ -0,0 +1,63 @@
+---
+rule:
+  aip: 135
+  name: [core, '0135', http-body]
+  summary: Delete methods must not have an HTTP body.
+---
+
+# Delete methods: No HTTP body
+
+This rule enforces that all `Delete` RPCs omit the HTTP `body`, as mandated in
+[AIP-135][].
+
+## Details
+
+This rule looks at any message matching beginning with `Delete`, and complains
+if the HTTP `body` field is set.
+
+## Examples
+
+**Incorrect** code for this rule:
+
+```proto
+// Incorrect.
+rpc DeleteBook(DeleteBookRequest) returns (google.protobuf.Empty) {
+  option (google.api.http) = {
+    delete: "/v1/{name=publishers/*/books/*}"
+    body: "*"  // This should be absent.
+  };
+}
+```
+
+**Correct** code for this rule:
+
+```proto
+// Correct.
+rpc DeleteBook(DeleteBookRequest) returns (google.protobuf.Empty) {
+  option (google.api.http) = {
+    delete: "/v1/{name=publishers/*/books/*}"
+  };
+}
+```
+
+## Disabling
+
+If you need to violate this rule, use a leading comment above the method.
+Remember to also include an [aip.dev/not-precedent][] comment explaining why.
+
+```proto
+// (-- api-linter: core::0135::http-body=disabled
+//     aip.dev/not-precedent: We need to do this because reasons. --)
+rpc DeleteBook(DeleteBookRequest) returns (google.protobuf.Empty) {
+  option (google.api.http) = {
+    delete: "/v1/{name=publishers/*/books/*}"
+    body: "*"
+  };
+}
+```
+
+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/docs/rules/core/0135-http-method.md b/docs/rules/core/0135-http-method.md
new file mode 100644
index 0000000..d7ffe42
--- /dev/null
+++ b/docs/rules/core/0135-http-method.md
@@ -0,0 +1,62 @@
+---
+rule:
+  aip: 135
+  name: [core, '0135', http-method]
+  summary: Delete methods must use the DELETE HTTP verb.
+---
+
+# Delete methods: DELETE HTTP verb
+
+This rule enforces that all `Delete` RPCs use the `DELETE` HTTP verb, as
+mandated in [AIP-135][].
+
+## Details
+
+This rule looks at any message matching beginning with `Delete`, and complains
+if the HTTP verb is anything other than `DELETE`. It _does_ check additional
+bindings if they are present.
+
+## Examples
+
+**Incorrect** code for this rule:
+
+```proto
+// Incorrect.
+rpc DeleteBook(DeleteBookRequest) returns (google.protobuf.Empty) {
+  option (google.api.http) = {
+    post: "/v1/{name=publishers/*/books/*}"  // Should be `delete:`.
+  };
+}
+```
+
+**Correct** code for this rule:
+
+```proto
+// Correct.
+rpc DeleteBook(DeleteBookRequest) returns (google.protobuf.Empty) {
+  option (google.api.http) = {
+    delete: "/v1/{name=publishers/*/books/*}"
+  };
+}
+```
+
+## Disabling
+
+If you need to violate this rule, use a leading comment above the method.
+Remember to also include an [aip.dev/not-precedent][] comment explaining why.
+
+```proto
+// (-- api-linter: core::0135::http-method=disabled
+//     aip.dev/not-precedent: We need to do this because reasons. --)
+rpc DeleteBook(DeleteBookRequest) returns (google.protobuf.Empty) {
+  option (google.api.http) = {
+    post: "/v1/{name=publishers/*/books/*}"
+  };
+}
+```
+
+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/docs/rules/core/0135-http-uri-name.md b/docs/rules/core/0135-http-uri-name.md
new file mode 100644
index 0000000..55a8023
--- /dev/null
+++ b/docs/rules/core/0135-http-uri-name.md
@@ -0,0 +1,62 @@
+---
+rule:
+  aip: 135
+  name: [core, '0135', http-uri-name]
+  summary: Delete methods must map the name field to the URI.
+---
+
+# Delete methods: HTTP URI name field
+
+This rule enforces that all `Delete` RPCs map the `name` field to the HTTP URI,
+as mandated in [AIP-135][].
+
+## Details
+
+This rule looks at any message matching beginning with `Delete`, and complains
+if the `name` variable is not included in the URI. It _does_ check additional
+bindings if they are present.
+
+## Examples
+
+**Incorrect** code for this rule:
+
+```proto
+// Incorrect.
+rpc DeleteBook(DeleteBookRequest) returns (google.protobuf.Empty) {
+  option (google.api.http) = {
+    delete: "/v1/publishers/*/books/*"  // The `name` field should be extracted.
+  };
+}
+```
+
+**Correct** code for this rule:
+
+```proto
+// Correct.
+rpc DeleteBook(DeleteBookRequest) returns (google.protobuf.Empty) {
+  option (google.api.http) = {
+    delete: "/v1/{name=publishers/*/books/*}"
+  };
+}
+```
+
+## Disabling
+
+If you need to violate this rule, use a leading comment above the method.
+Remember to also include an [aip.dev/not-precedent][] comment explaining why.
+
+```proto
+// (-- api-linter: core::0135::http-uri-name=disabled
+//     aip.dev/not-precedent: We need to do this because reasons. --)
+rpc DeleteBook(DeleteBookRequest) returns (google.protobuf.Empty) {
+  option (google.api.http) = {
+    delete: "/v1/publishers/*/books/*"
+  };
+}
+```
+
+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/docs/rules/core/0135-request-message-name.md b/docs/rules/core/0135-request-message-name.md
new file mode 100644
index 0000000..aec794d
--- /dev/null
+++ b/docs/rules/core/0135-request-message-name.md
@@ -0,0 +1,63 @@
+---
+rule:
+  aip: 135
+  name: [core, '0135', request-message-name]
+  summary: Delete methods must have standardized request message names.
+---
+
+# Delete methods: Request message
+
+This rule enforces that all `Delete` RPCs have a request message name of
+`Delete*Request`, as mandated in [AIP-135][].
+
+## Details
+
+This rule looks at any message matching beginning with `Delete`, and complains
+if the name of the corresponding input message does not match the name of the
+RPC with the suffix `Request` appended.
+
+## Examples
+
+**Incorrect** code for this rule:
+
+```proto
+// Incorrect.
+// Should be `DeleteBookRequest`.
+rpc DeleteBook(Book) returns (google.protobuf.Empty) {
+  option (google.api.http) = {
+    delete: "/v1/{name=publishers/*/books/*}"
+  };
+}
+```
+
+**Correct** code for this rule:
+
+```proto
+// Correct.
+rpc DeleteBook(DeleteBookRequest) returns (Book) {
+  option (google.api.http) = {
+    delete: "/v1/{name=publishers/*/books/*}"
+  };
+}
+```
+
+## Disabling
+
+If you need to violate this rule, use a leading comment above the method.
+Remember to also include an [aip.dev/not-precedent][] comment explaining why.
+
+```proto
+// (-- api-linter: core::0135::request-message-name=disabled
+//     aip.dev/not-precedent: We need to do this because reasons. --)
+rpc DeleteBook(Book) returns (google.protobuf.Empty) {
+  option (google.api.http) = {
+    get: "/v1/{name=publishers/*/books/*}"
+  };
+}
+```
+
+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/docs/rules/core/0135-request-name-field.md b/docs/rules/core/0135-request-name-field.md
new file mode 100644
index 0000000..811feac
--- /dev/null
+++ b/docs/rules/core/0135-request-name-field.md
@@ -0,0 +1,63 @@
+---
+rule:
+  aip: 135
+  name: [core, '0135', request-name-field]
+  summary: Delete RPCs must have a `name` field in the request.
+---
+
+# Delete methods: Name field
+
+This rule enforces that all `Delete` standard methods have a `string name`
+field in the request message, as mandated in [AIP-135][].
+
+## Details
+
+This rule looks at any message matching `Delete*Request` and complains if
+either the `name` field is missing, or if it has any type other than `string`.
+
+## Examples
+
+**Incorrect** code for this rule:
+
+```proto
+// Incorrect.
+message DeleteBookRequest {
+  string book = 1;  // Field name should be `name`.
+}
+```
+
+```proto
+// Incorrect.
+message DeleteBookRequest {
+  bytes name = 1;  // Field type should be `string`.
+}
+```
+
+**Correct** code for this rule:
+
+```proto
+// Correct.
+message DeleteBookRequest {
+  string name = 1;
+}
+```
+
+## Disabling
+
+If you need to violate this rule, use a leading comment above the message (if
+the `name` field is missing) or above the field (if it is the wrong type).
+Remember to also include an [aip.dev/not-precedent][] comment explaining why.
+
+```proto
+// (-- api-linter: core::0135::request-name-field=disabled
+//     aip.dev/not-precedent: We need to do this because reasons. --)
+message DeleteBookRequest {
+  string book = 1;
+}
+```
+
+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/docs/rules/core/0135-request-unknown-fields.md b/docs/rules/core/0135-request-unknown-fields.md
new file mode 100644
index 0000000..1478cef
--- /dev/null
+++ b/docs/rules/core/0135-request-unknown-fields.md
@@ -0,0 +1,65 @@
+---
+rule:
+  aip: 135
+  name: [core, '0135', request-unknown-fields]
+  summary: Delete RPCs should not have unexpected fields in the request.
+---
+
+# Delete methods: Unknown fields
+
+This rule enforces that all `Delete` standard methods do not have unexpected
+fields, as mandated in [AIP-135][].
+
+## Details
+
+This rule looks at any message matching `Delete*Request` and complains if it
+comes across any fields other than:
+
+- `string name` ([AIP-135][])
+- `bool force` ([AIP-135][])
+- `string etag` ([AIP-154][])
+- `string request_id` ([AIP-155][])
+
+## Examples
+
+**Incorrect** code for this rule:
+
+```proto
+// Incorrect.
+message DeleteBookRequest {
+  string name = 1;
+  string library_id = 2;  // Non-standard field.
+}
+```
+
+**Correct** code for this rule:
+
+```proto
+// Correct.
+message DeleteBookRequest {
+  string name = 1;
+}
+```
+
+## 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;
+
+  // (-- api-linter: core::0135::request-unknown-fields=disabled
+  //     aip.dev/not-precedent: We really need this field because reaosns. --)
+  string library_id = 2;
+}
+```
+
+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-154]: https://aip.dev/154
+[aip-155]: https://aip.dev/155
+[aip.dev/not-precedent]: https://aip.dev/not-precedent
diff --git a/docs/rules/core/0135-response-message-name.md b/docs/rules/core/0135-response-message-name.md
new file mode 100644
index 0000000..5d29899
--- /dev/null
+++ b/docs/rules/core/0135-response-message-name.md
@@ -0,0 +1,127 @@
+---
+rule:
+  aip: 135
+  name: [core, '0135', response-message-name]
+  summary: Delete methods must return Empty or the resource.
+---
+
+# Delete methods: Empty response message
+
+This rule enforces that all `Delete` RPCs have a response message of
+`google.protobuf.Empty` or the resource, as mandated in [AIP-135][].
+
+## Details
+
+This rule looks at any message matching beginning with `Delete`, and complains
+if the name of the corresponding output message is not one of:
+
+- `google.protobuf.Empty`
+- The name of the RPC with the prefix `Delete` removed.
+
+It also permits a response of `google.longrunning.Operation`; in this case, it
+checks the `response_type` in the `google.longrunning.operation_info`
+annotation and ensures that _it_ corresponds to either `google.protobuf.Empty`
+or the name of the RPC with the prefix `Delete` removed.
+
+## Examples
+
+### Standard
+
+**Incorrect** code for this rule:
+
+```proto
+// Incorrect.
+// Should be `google.protobuf.Empty` or the resource.
+rpc DeleteBook(DeleteBookRequest) returns (DeleteBookResponse) {
+  option (google.api.http) = {
+    delete: "/v1/{name=publishers/*/books/*}"
+  };
+}
+```
+
+**Correct** code for this rule:
+
+```proto
+// Correct.
+rpc DeleteBook(DeleteBookRequest) returns (google.protobuf.Empty) {
+  option (google.api.http) = {
+    delete: "/v1/{name=publishers/*/books/*}"
+  };
+}
+```
+
+```proto
+// Correct.
+rpc DeleteBook(DeleteBookRequest) returns (Book) {
+  option (google.api.http) = {
+    delete: "/v1/{name=publishers/*/books/*}"
+  };
+}
+```
+
+### Long-running operation
+
+**Incorrect** code for this rule:
+
+```proto
+// Incorrect.
+rpc DeleteBook(DeleteBookRequest) returns (google.longrunning.Operation) {
+  option (google.api.http) = {
+    delete: "/v1/{name=publishers/*/books/*}"
+  };
+  option (google.longrunning.operation_info) = {
+    // Should be "google.protobuf.Empty" or "Book".
+    response_type: "DeleteBookResponse"
+    metadata_type: "DeleteBookMetadata"
+  }
+}
+```
+
+**Correct** code for this rule:
+
+```proto
+// Correct.
+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: "DeleteBookMetadata"
+  }
+}
+```
+
+```proto
+// Correct.
+rpc DeleteBook(DeleteBookRequest) returns (google.longrunning.Operation) {
+  option (google.api.http) = {
+    delete: "/v1/{name=publishers/*/books/*}"
+  };
+  option (google.longrunning.operation_info) = {
+    response_type: "Book"
+    metadata_type: "DeleteBookMetadata"
+  }
+}
+```
+
+## Disabling
+
+If you need to violate this rule, use a leading comment above the method.
+Remember to also include an [aip.dev/not-precedent][] comment explaining why.
+
+```proto
+// (-- api-linter: core::0135::response-message-name=disabled
+//     aip.dev/not-precedent: We need to do this because reasons. --)
+rpc DeleteBook(DeleteBookRequest) returns (DeleteBookResponse) {
+  option (google.api.http) = {
+    delete: "/v1/{book.name=publishers/*/books/*}"
+  };
+}
+```
+
+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/aip0135/http_body.go b/rules/aip0135/http_body.go
new file mode 100644
index 0000000..5d85090
--- /dev/null
+++ b/rules/aip0135/http_body.go
@@ -0,0 +1,41 @@
+// 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 (
+	"github.com/googleapis/api-linter/lint"
+	"github.com/googleapis/api-linter/rules/internal/utils"
+	"github.com/jhump/protoreflect/desc"
+)
+
+// 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.Body != "" {
+				return []lint.Problem{{
+					Message:    "Delete methods should not have an HTTP body.",
+					Descriptor: m,
+				}}
+			}
+		}
+
+		return nil
+	},
+}
diff --git a/rules/aip0135/http_method.go b/rules/aip0135/http_method.go
new file mode 100644
index 0000000..b870d0f
--- /dev/null
+++ b/rules/aip0135/http_method.go
@@ -0,0 +1,41 @@
+// 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 (
+	"github.com/googleapis/api-linter/lint"
+	"github.com/googleapis/api-linter/rules/internal/utils"
+	"github.com/jhump/protoreflect/desc"
+)
+
+// Delete methods should use the HTTP DELETE method.
+var httpMethod = &lint.MethodRule{
+	Name:   lint.NewRuleName("core", "0135", "http-method"),
+	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.Method != "DELETE" {
+				return []lint.Problem{{
+					Message:    "Delete methods must use the HTTP DELETE verb.",
+					Descriptor: m,
+				}}
+			}
+		}
+
+		return nil
+	},
+}
diff --git a/rules/aip0135/http_uri_name.go b/rules/aip0135/http_uri_name.go
new file mode 100644
index 0000000..b96aca5
--- /dev/null
+++ b/rules/aip0135/http_uri_name.go
@@ -0,0 +1,41 @@
+// 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 (
+	"github.com/googleapis/api-linter/lint"
+	"github.com/googleapis/api-linter/rules/internal/utils"
+	"github.com/jhump/protoreflect/desc"
+)
+
+// Delete methods should have a proper HTTP pattern.
+var httpNameField = &lint.MethodRule{
+	Name:   lint.NewRuleName("core", "0135", "http-uri-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 !deleteURINameRegexp.MatchString(httpRule.URI) {
+				return []lint.Problem{{
+					Message:    "Delete methods should include the `name` field in the URI.",
+					Descriptor: m,
+				}}
+			}
+		}
+
+		return nil
+	},
+}
diff --git a/rules/aip0135/method_rules.go b/rules/aip0135/method_rules.go
deleted file mode 100644
index 0eead83..0000000
--- a/rules/aip0135/method_rules.go
+++ /dev/null
@@ -1,150 +0,0 @@
-// 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"
-	"strings"
-
-	"bitbucket.org/creachadair/stringset"
-	"github.com/googleapis/api-linter/lint"
-	"github.com/googleapis/api-linter/rules/internal/utils"
-	"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` or `Foo`.
-		got := m.GetOutputType().GetName()
-		if stringset.New("Empty", "Operation").Contains(got) {
-			got = m.GetOutputType().GetFullyQualifiedName()
-		}
-		want := stringset.New(
-			"google.protobuf.Empty",
-			strings.Replace(m.GetName(), "Delete", "", 1),
-		)
-
-		// If the return type is an Operation, use the annotated response type.
-		if got == "google.longrunning.Operation" {
-			got = utils.GetOperationInfo(m).GetResponseType()
-		}
-
-		// If we did not get a permitted value, return a problem.
-		//
-		// Note: If `got` is empty string, this is an unannotated LRO.
-		// The AIP-151 rule will whine about that, and this rule should not as it
-		// would be confusing.
-		if !want.Contains(got) && got != "" {
-			return []lint.Problem{{
-				Message: fmt.Sprintf(
-					"Delete RPCs should have response message type of Empty or the resource, not %q.",
-					got,
-				),
-				Suggestion: "google.protobuf.Empty",
-				Descriptor: m,
-			}}
-		}
-
-		return nil
-	},
-}
-
-// Delete methods should use the HTTP DELETE method.
-var httpMethod = &lint.MethodRule{
-	Name:   lint.NewRuleName("core", "0135", "http-method"),
-	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.Method != "DELETE" {
-				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 !deleteURINameRegexp.MatchString(httpRule.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.Body != "" {
-				return []lint.Problem{{
-					Message:    "Delete methods should not have an HTTP body.",
-					Descriptor: m,
-				}}
-			}
-		}
-
-		return nil
-	},
-}
diff --git a/rules/aip0135/request_message_name.go b/rules/aip0135/request_message_name.go
new file mode 100644
index 0000000..de9c8e5
--- /dev/null
+++ b/rules/aip0135/request_message_name.go
@@ -0,0 +1,45 @@
+// 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"
+)
+
+// 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
+	},
+}
diff --git a/rules/aip0135/message_rules.go b/rules/aip0135/request_name_field.go
similarity index 60%
rename from rules/aip0135/message_rules.go
rename to rules/aip0135/request_name_field.go
index a338928..ec9d70f 100644
--- a/rules/aip0135/message_rules.go
+++ b/rules/aip0135/request_name_field.go
@@ -24,7 +24,7 @@ import (
 
 // The Delete standard method should only have expected fields.
 var standardFields = &lint.MessageRule{
-	Name:   lint.NewRuleName("core", "0135", "request-message", "name-field"),
+	Name:   lint.NewRuleName("core", "0135", "request-name-field"),
 	URI:    "https://aip.dev/135#request-message",
 	OnlyIf: isDeleteRequestMessage,
 	LintMessage: func(m *desc.MessageDescriptor) []lint.Problem {
@@ -48,31 +48,3 @@ var standardFields = &lint.MessageRule{
 		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/request_unknown_fields.go b/rules/aip0135/request_unknown_fields.go
new file mode 100644
index 0000000..c628780
--- /dev/null
+++ b/rules/aip0135/request_unknown_fields.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
+
+import (
+	"fmt"
+
+	"github.com/googleapis/api-linter/lint"
+	"github.com/jhump/protoreflect/desc"
+)
+
+// Delete methods should not have unrecognized fields.
+var unknownFields = &lint.MessageRule{
+	Name:   lint.NewRuleName("core", "0135", "request-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
+			"request_id": {}, // AIP-155
+		}
+		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/response_message_name.go b/rules/aip0135/response_message_name.go
new file mode 100644
index 0000000..c1d6f47
--- /dev/null
+++ b/rules/aip0135/response_message_name.go
@@ -0,0 +1,69 @@
+// 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"
+	"strings"
+
+	"bitbucket.org/creachadair/stringset"
+	"github.com/googleapis/api-linter/lint"
+	"github.com/googleapis/api-linter/rules/internal/utils"
+	"github.com/jhump/protoreflect/desc"
+)
+
+// 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` or `Foo`.
+		got := m.GetOutputType().GetName()
+		if stringset.New("Empty", "Operation").Contains(got) {
+			got = m.GetOutputType().GetFullyQualifiedName()
+		}
+		want := stringset.New(
+			"google.protobuf.Empty",
+			strings.Replace(m.GetName(), "Delete", "", 1),
+		)
+
+		// If the return type is an Operation, use the annotated response type.
+		if got == "google.longrunning.Operation" {
+			got = utils.GetOperationInfo(m).GetResponseType()
+		}
+
+		// If we did not get a permitted value, return a problem.
+		//
+		// Note: If `got` is empty string, this is an unannotated LRO.
+		// The AIP-151 rule will whine about that, and this rule should not as it
+		// would be confusing.
+		if !want.Contains(got) && got != "" {
+			return []lint.Problem{{
+				Message: fmt.Sprintf(
+					"Delete RPCs should have response message type of Empty or the resource, not %q.",
+					got,
+				),
+				Suggestion: "google.protobuf.Empty",
+				Descriptor: m,
+			}}
+		}
+
+		return nil
+	},
+}


Test patch:
diff --git a/rules/aip0135/http_body_test.go b/rules/aip0135/http_body_test.go
new file mode 100644
index 0000000..6ef711c
--- /dev/null
+++ b/rules/aip0135/http_body_test.go
@@ -0,0 +1,72 @@
+// 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/jhump/protoreflect/desc/builder"
+	"google.golang.org/genproto/googleapis/api/annotations"
+)
+
+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)
+			}
+		})
+	}
+}
diff --git a/rules/aip0135/http_method_test.go b/rules/aip0135/http_method_test.go
new file mode 100644
index 0000000..2972e84
--- /dev/null
+++ b/rules/aip0135/http_method_test.go
@@ -0,0 +1,80 @@
+// 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/jhump/protoreflect/desc/builder"
+	"google.golang.org/genproto/googleapis/api/annotations"
+)
+
+func TestHttpMethod(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, "DeleteBook", ""},
+		{"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 := httpMethod.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)
+			}
+		})
+	}
+}
diff --git a/rules/aip0135/http_uri_name_test.go b/rules/aip0135/http_uri_name_test.go
new file mode 100644
index 0000000..d01a24d
--- /dev/null
+++ b/rules/aip0135/http_uri_name_test.go
@@ -0,0 +1,74 @@
+// 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/jhump/protoreflect/desc/builder"
+	"google.golang.org/genproto/googleapis/api/annotations"
+)
+
+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)
+			}
+		})
+	}
+}
diff --git a/rules/aip0135/method_rules_test.go b/rules/aip0135/method_rules_test.go
deleted file mode 100644
index 818748e..0000000
--- a/rules/aip0135/method_rules_test.go
+++ /dev/null
@@ -1,272 +0,0 @@
-// 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"
-	"github.com/jhump/protoreflect/desc/builder"
-	"google.golang.org/genproto/googleapis/api/annotations"
-)
-
-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) {
-	tmpl := map[string]string{
-		"sync": `
-			package test;
-			import "google/protobuf/empty.proto";
-			service Library {
-				rpc {{.MethodName}}({{.MethodName}}Request) returns ({{.RespTypeName}});
-			}
-			message {{.MethodName}}Request {}
-			{{ if (ne .RespTypeName "google.protobuf.Empty") }}message {{.RespTypeName}} {}{{ end }}
-		`,
-		"lro": `
-			package test;
-			import "google/longrunning/operations.proto";
-			service Library {
-				rpc {{.MethodName}}({{.MethodName}}Request)
-				    returns (google.longrunning.Operation) {
-					option (google.longrunning.operation_info) = {
-						response_type: "{{.RespTypeName}}"
-						metadata_type: "{{.MethodName}}Metadata"
-					};
-				}
-			}
-			message {{.MethodName}}Request {}
-		`,
-	}
-
-	// Set up the testing permutations.
-	tests := []struct {
-		testName     string
-		tmpl         string
-		MethodName   string
-		RespTypeName string
-		problems     testutils.Problems
-	}{
-		{"ValidEmpty", tmpl["sync"], "DeleteBook", "google.protobuf.Empty", testutils.Problems{}},
-		{"ValidResource", tmpl["sync"], "DeleteBook", "Book", testutils.Problems{}},
-		{"ValidLROEmpty", tmpl["lro"], "DeleteBook", "google.protobuf.Empty", testutils.Problems{}},
-		{"ValidLROResource", tmpl["lro"], "DeleteBook", "Book", testutils.Problems{}},
-		{"Invalid", tmpl["sync"], "DeleteBook", "DeleteBookResponse", testutils.Problems{{Suggestion: "google.protobuf.Empty"}}},
-		{"InvalidLRO", tmpl["lro"], "DeleteBook", "DeleteBookResponse", testutils.Problems{{Suggestion: "google.protobuf.Empty"}}},
-		{"Irrelevant", tmpl["sync"], "DestroyBook", "DestroyBookResponse", 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
-			file := testutils.ParseProto3Tmpl(t, test.tmpl, test)
-
-			// Run the lint rule, and establish that it returns the expected problems.
-			method := file.GetServices()[0].GetMethods()[0]
-			problems := responseMessageName.Lint(file)
-			if diff := test.problems.SetDescriptor(method).Diff(problems); diff != "" {
-				t.Errorf(diff)
-			}
-		})
-	}
-}
-
-func TestHttpMethod(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, "DeleteBook", ""},
-		{"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 := httpMethod.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)
-			}
-		})
-	}
-}
diff --git a/rules/aip0135/request_message_name_test.go b/rules/aip0135/request_message_name_test.go
new file mode 100644
index 0000000..8c2fb28
--- /dev/null
+++ b/rules/aip0135/request_message_name_test.go
@@ -0,0 +1,57 @@
+// 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 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)
+			}
+		})
+	}
+}
diff --git a/rules/aip0135/message_rules_test.go b/rules/aip0135/request_name_field_test.go
similarity index 63%
rename from rules/aip0135/message_rules_test.go
rename to rules/aip0135/request_name_field_test.go
index cebcf2a..205ee83 100644
--- a/rules/aip0135/message_rules_test.go
+++ b/rules/aip0135/request_name_field_test.go
@@ -74,43 +74,3 @@ func TestStandardFieldsInvalidType(t *testing.T) {
 		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/request_unknown_fields_test.go b/rules/aip0135/request_unknown_fields_test.go
new file mode 100644
index 0000000..29597a3
--- /dev/null
+++ b/rules/aip0135/request_unknown_fields_test.go
@@ -0,0 +1,63 @@
+// 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 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{}},
+		{"RequestId", "DeleteBookRequest", "request_id", 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/response_message_name_test.go b/rules/aip0135/response_message_name_test.go
new file mode 100644
index 0000000..73e8020
--- /dev/null
+++ b/rules/aip0135/response_message_name_test.go
@@ -0,0 +1,81 @@
+// 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"
+)
+
+func TestResponseMessageName(t *testing.T) {
+	tmpl := map[string]string{
+		"sync": `
+			package test;
+			import "google/protobuf/empty.proto";
+			service Library {
+				rpc {{.MethodName}}({{.MethodName}}Request) returns ({{.RespTypeName}});
+			}
+			message {{.MethodName}}Request {}
+			{{ if (ne .RespTypeName "google.protobuf.Empty") }}message {{.RespTypeName}} {}{{ end }}
+		`,
+		"lro": `
+			package test;
+			import "google/longrunning/operations.proto";
+			service Library {
+				rpc {{.MethodName}}({{.MethodName}}Request)
+				    returns (google.longrunning.Operation) {
+					option (google.longrunning.operation_info) = {
+						response_type: "{{.RespTypeName}}"
+						metadata_type: "{{.MethodName}}Metadata"
+					};
+				}
+			}
+			message {{.MethodName}}Request {}
+		`,
+	}
+
+	// Set up the testing permutations.
+	tests := []struct {
+		testName     string
+		tmpl         string
+		MethodName   string
+		RespTypeName string
+		problems     testutils.Problems
+	}{
+		{"ValidEmpty", tmpl["sync"], "DeleteBook", "google.protobuf.Empty", testutils.Problems{}},
+		{"ValidResource", tmpl["sync"], "DeleteBook", "Book", testutils.Problems{}},
+		{"ValidLROEmpty", tmpl["lro"], "DeleteBook", "google.protobuf.Empty", testutils.Problems{}},
+		{"ValidLROResource", tmpl["lro"], "DeleteBook", "Book", testutils.Problems{}},
+		{"Invalid", tmpl["sync"], "DeleteBook", "DeleteBookResponse", testutils.Problems{{Suggestion: "google.protobuf.Empty"}}},
+		{"InvalidLRO", tmpl["lro"], "DeleteBook", "DeleteBookResponse", testutils.Problems{{Suggestion: "google.protobuf.Empty"}}},
+		{"Irrelevant", tmpl["sync"], "DestroyBook", "DestroyBookResponse", 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
+			file := testutils.ParseProto3Tmpl(t, test.tmpl, test)
+
+			// Run the lint rule, and establish that it returns the expected problems.
+			method := file.GetServices()[0].GetMethods()[0]
+			problems := responseMessageName.Lint(file)
+			if diff := test.problems.SetDescriptor(method).Diff(problems); diff != "" {
+				t.Errorf(diff)
+			}
+		})
+	}
+}