Repository: googleapis/api-linter
Pull request #304 (2019-11-05 00:22:39)

[AIP-231] Document all AIP-231 rules.
Also changes a rule name in AIP-136 to better be consistent.


Patch:
diff --git a/docs/rules/core/0136-uri-suffix.md b/docs/rules/core/0136-http-uri-suffix.md
similarity index 95%
rename from docs/rules/core/0136-uri-suffix.md
rename to docs/rules/core/0136-http-uri-suffix.md
index d8f4c3c..3757004 100644
--- a/docs/rules/core/0136-uri-suffix.md
+++ b/docs/rules/core/0136-http-uri-suffix.md
@@ -1,7 +1,7 @@
 ---
 rule:
   aip: 136
-  name: [core, '0136', uri-suffix]
+  name: [core, '0136', http-uri-suffix]
   summary: Custom methods should have a correct URI suffix.
 ---
 
@@ -81,7 +81,7 @@ 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::0136::uri-suffix=disabled
+// (-- api-linter: core::0136::http-uri-suffix=disabled
 //     aip.dev/not-precedent: We need to do this because reasons. --)
 rpc CheckoutBook(CheckoutBookRequest) returns (CheckoutBookResponse) {
   option (google.api.http) = {
diff --git a/docs/rules/core/0231-http-body.md b/docs/rules/core/0231-http-body.md
new file mode 100644
index 0000000..73ff6e1
--- /dev/null
+++ b/docs/rules/core/0231-http-body.md
@@ -0,0 +1,68 @@
+---
+rule:
+  aip: 231
+  name: [core, '0231', http-body]
+  summary: Batch Get methods must not have an HTTP body.
+---
+
+# Batch Get methods: No HTTP body
+
+This rule enforces that all `Get` RPCs omit the HTTP `body`, as mandated in
+[AIP-231][].
+
+## Details
+
+This rule looks at any message matching beginning with `BatchGet`, and
+complains if the HTTP `body` field is set.
+
+## Examples
+
+**Incorrect** code for this rule:
+
+```proto
+// Incorrect.
+rpc BatchGetBooks(BatchGetBooksRequest) returns (BatchGetBooksResponse) {
+  option (google.api.http) = {
+    post: "/v1/{parent=publishers/*}/books:batchGet"
+    body: "*"  // This should be absent.
+  };
+}
+```
+
+**Correct** code for this rule:
+
+```proto
+// Correct.
+rpc BatchGetBooks(BatchGetBooksRequest) returns (BatchGetBooksResponse) {
+  option (google.api.http) = {
+    get: "/v1/{parent=publishers/*}/books:batchGet"
+  };
+}
+```
+
+## 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::0231::http-body=disabled
+//     api-linter: core::0231::http-method=disabled
+//     aip.dev/not-precedent: We need to do this because reasons. --)
+rpc BatchGetBooks(BatchGetBooksRequest) returns (BatchGetBooksResponse) {
+  option (google.api.http) = {
+    post: "/v1/{parent=publishers/*}/books:batchGet"
+    body: "*"  // This should be absent.
+  };
+}
+```
+
+**Important:** HTTP `GET` requests are unable to have an HTTP body, due to the
+nature of the protocol. The only valid way to include a body is to also use a
+different HTTP method (as depicted above).
+
+If you need to violate this rule for an entire file, place the comment at the
+top of the file.
+
+[aip-231]: https://aip.dev/231
+[aip.dev/not-precedent]: https://aip.dev/not-precedent
diff --git a/docs/rules/core/0231-http-method.md b/docs/rules/core/0231-http-method.md
new file mode 100644
index 0000000..d7162d0
--- /dev/null
+++ b/docs/rules/core/0231-http-method.md
@@ -0,0 +1,62 @@
+---
+rule:
+  aip: 231
+  name: [core, '0231', http-method]
+  summary: Batch Get methods must use the GET HTTP verb.
+---
+
+# Get methods: GET HTTP verb
+
+This rule enforces that all `BatchGet` RPCs use the `GET` HTTP verb, as
+mandated in [AIP-231][].
+
+## Details
+
+This rule looks at any message matching beginning with `BatchGet`, and
+complains if the HTTP verb is anything other than `GET`. It _does_ check
+additional bindings if they are present.
+
+## Examples
+
+**Incorrect** code for this rule:
+
+```proto
+// Incorrect.
+rpc BatchGetBooks(BatchGetBooksRequest) returns (BatchGetBooksResponse) {
+  option (google.api.http) = {
+    post: "/v1/{name=publishers/*}/books:batchGet"  // Should be `get:`.
+  };
+}
+```
+
+**Correct** code for this rule:
+
+```proto
+// Correct.
+rpc BatchGetBooks(BatchGetBooksRequest) returns (BatchGetBooksResponse) {
+  option (google.api.http) = {
+    get: "/v1/{name=publishers/*}/books:batchGet"
+  };
+}
+```
+
+## 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::0231::http-method=disabled
+//     aip.dev/not-precedent: We need to do this because reasons. --)
+rpc BatchGetBooks(BatchGetBooksRequest) returns (BatchGetBooksResponse) {
+  option (google.api.http) = {
+    post: "/v1/{name=publishers/*}/books:batchGet"  // Should be `get:`.
+  };
+}
+```
+
+If you need to violate this rule for an entire file, place the comment at the
+top of the file.
+
+[aip-231]: https://aip.dev/231
+[aip.dev/not-precedent]: https://aip.dev/not-precedent
diff --git a/docs/rules/core/0231-http-uri-suffix.md b/docs/rules/core/0231-http-uri-suffix.md
new file mode 100644
index 0000000..7fc771e
--- /dev/null
+++ b/docs/rules/core/0231-http-uri-suffix.md
@@ -0,0 +1,65 @@
+---
+rule:
+  aip: 231
+  name: [core, '0231', http-uri-suffix]
+  summary: Batch Get methods must have the correct URI suffix
+---
+
+# Batch Get methods: URI suffix
+
+This rule enforces that ` BatchGet` methods include the `:batchGet` suffix in
+the REST URI, as mandated in [AIP-231][].
+
+## Details
+
+This rule looks at any method whose name starts with `BatchGet`, and complains
+if the HTTP URI does not end with `:batchGet`.
+
+## Examples
+
+**Incorrect** code for this rule:
+
+```proto
+// Incorrect.
+rpc BatchGetBooks(BatchGetBooksRequest) returns (BatchGetBooksResponse) {
+  option (google.api.http) = {
+    get: "/v1/{parent=publishers/*}/books:batch"  // Should end with `:batchGet`.
+  };
+}
+```
+
+**Correct** code for this rule:
+
+```proto
+// Correct.
+rpc BatchGetBooks(BatchGetBooksRequest) returns (BatchGetBooksResponse) {
+  option (google.api.http) = {
+    get: "/v1/{parent=publishers/*}/books:batchGet"
+  };
+}
+```
+
+## 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::0231::http-uri-suffix=disabled
+//     aip.dev/not-precedent: We need to do this because reasons. --)
+rpc BatchGetBooks(BatchGetBooksRequest) returns (BatchGetBooksResponse) {
+  option (google.api.http) = {
+    get: "/v1/{parent=publishers/*}/books:batch"  // Should end with `:batchGet`.
+  };
+}
+```
+
+If you need to violate this rule for an entire file, place the comment at the
+top of the file.
+
+[aip-231]: https://aip.dev/231
+[aip.dev/not-precedent]: https://aip.dev/not-precedent
+
+```
+
+```
diff --git a/docs/rules/core/0231-plural-method-name.md b/docs/rules/core/0231-plural-method-name.md
new file mode 100644
index 0000000..4d8d0bb
--- /dev/null
+++ b/docs/rules/core/0231-plural-method-name.md
@@ -0,0 +1,62 @@
+---
+rule:
+  aip: 231
+  name: [core, '0231', plural-method-name]
+  summary: Batch Get methods must have plural names.
+---
+
+# Batch Get methods: Plural method name
+
+This rule enforces that all `BatchGet` RPCs have a plural resource in the
+method name, as mandated in [AIP-231][].
+
+## Details
+
+This rule looks at any method whose name begins with `BatchGet`, and complains
+if the name of the resource in the method name is singular.
+
+## Examples
+
+**Incorrect** code for this rule:
+
+```proto
+// Incorrect.
+// Method name should be `BatchGetBooks`
+rpc BatchGetBook(BatchGetBookRequest) returns (BatchGetBookRespose) {
+  option (google.api.http) = {
+    get: "/v1/{parent=publishers/*}/books:batchGet"
+  };
+}
+```
+
+**Correct** code for this rule:
+
+```proto
+// Correct.
+rpc BatchGetBooks(BatchGetBooksRequest) returns (BatchGetBooksRespose) {
+  option (google.api.http) = {
+    get: "/v1/{parent=publishers/*}/books:batchGet"
+  };
+}
+```
+
+## 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::0231::plural-method-name=disabled
+//     aip.dev/not-precedent: We need to do this because reasons. --)
+rpc BatchGetBook(BatchGetBookRequest) returns (BatchGetBookRespose) {
+  option (google.api.http) = {
+    get: "/v1/{parent=publishers/*}/books:batchGet"
+  };
+}
+```
+
+If you need to violate this rule for an entire file, place the comment at the
+top of the file.
+
+[aip-231]: https://aip.dev/231
+[aip.dev/not-precedent]: https://aip.dev/not-precedent
diff --git a/docs/rules/core/0231-request-message-name.md b/docs/rules/core/0231-request-message-name.md
new file mode 100644
index 0000000..21633de
--- /dev/null
+++ b/docs/rules/core/0231-request-message-name.md
@@ -0,0 +1,63 @@
+---
+rule:
+  aip: 231
+  name: [core, '0231', request-message-name]
+  summary: Batch Get methods must have standardized request message names.
+---
+
+# Batch Get methods: Request message
+
+This rule enforces that all `BatchGet` RPCs have a request message name of
+`BatchGet*Request`, as mandated in [AIP-231][].
+
+## Details
+
+This rule looks at any message matching beginning with `BatchGet`, 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.
+// Method input should be "BatchGetBooksRequest"
+rpc BatchGetBooks(GetBookRequest) returns (BatchGetBooksResponse) {
+  option (google.api.http) = {
+    get: "/v1/{parent=publishers/*}/books:batchGet"
+  };
+}
+```
+
+**Correct** code for this rule:
+
+```proto
+// Correct.
+rpc BatchGetBooks(BatchGetBooksRequest) returns (BatchGetBooksResponse) {
+  option (google.api.http) = {
+    get: "/v1/{parent=publishers/*}/books:batchGet"
+  };
+}
+```
+
+## 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::0231::request-message-name=disabled
+//     aip.dev/not-precedent: We need to do this because reasons. --)
+rpc BatchGetBooks(GetBookRequest) returns (BatchGetBooksResponse) {
+  option (google.api.http) = {
+    get: "/v1/{parent=publishers/*}/books:batchGet"
+  };
+}
+```
+
+If you need to violate this rule for an entire file, place the comment at the
+top of the file.
+
+[aip-231]: https://aip.dev/231
+[aip.dev/not-precedent]: https://aip.dev/not-precedent
diff --git a/docs/rules/core/0231-request-names-field.md b/docs/rules/core/0231-request-names-field.md
new file mode 100644
index 0000000..374570e
--- /dev/null
+++ b/docs/rules/core/0231-request-names-field.md
@@ -0,0 +1,71 @@
+---
+rule:
+  aip: 231
+  name: [core, '0231', request-names-field]
+  summary: Batch Get RPCs should have a `names` field in the request.
+---
+
+# Batch Get methods: Names field
+
+This rule enforces that all `BatchGet` methods have a `repeated string names`
+field in the request message, as mandated in [AIP-231][].
+
+## Details
+
+This rule looks at any message matching `BatchGet*Request` and complains if
+either the `names` field is missing, if it has any type other than `string`, or
+if it is not `repeated`.
+
+Alternatively, if there is a `repeated GetBookRequest requests` field, this is
+accepted in its place.
+
+## Examples
+
+**Incorrect** code for this rule:
+
+```proto
+// Incorrect.
+message BatchGetBooksRequest {
+  string parent = 1;
+  repeated string books = 2;  // Field name should be `names`.
+}
+```
+
+```proto
+// Incorrect.
+message BatchGetBooksRequest {
+  string parent = 1;
+  string books = 2;  // Field should be repeated.
+}
+```
+
+**Correct** code for this rule:
+
+```proto
+// Correct.
+message BatchGetBooksRequest {
+  string parent = 1;
+  repeated string names = 2;
+}
+```
+
+## Disabling
+
+If you need to violate this rule, use a leading comment above the message (if
+the `names` 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::0231::request-names-field=disabled
+//     aip.dev/not-precedent: We need to do this because reasons. --)
+message BatchGetBooksRequest {
+  string parent = 1;
+  repeated string books = 2;
+}
+```
+
+If you need to violate this rule for an entire file, place the comment at the
+top of the file.
+
+[aip-231]: https://aip.dev/231
+[aip.dev/not-precedent]: https://aip.dev/not-precedent
diff --git a/docs/rules/core/0231-request-parent-field.md b/docs/rules/core/0231-request-parent-field.md
new file mode 100644
index 0000000..e91b0c0
--- /dev/null
+++ b/docs/rules/core/0231-request-parent-field.md
@@ -0,0 +1,68 @@
+---
+rule:
+  aip: 231
+  name: [core, '0231', request-parent-field]
+  summary: Batch Get RPCs must have a `parent` field in the request.
+---
+
+# Batch Get methods: Parent field
+
+This rule enforces that all `BatchGet` methods have a `string parent` field in
+the request message, as mandated in [AIP-231][].
+
+## Details
+
+This rule looks at any message matching `BatchGet*Request` and complains if
+either the `parent` field is missing, or if it has any type other than
+`string`.
+
+## Examples
+
+**Incorrect** code for this rule:
+
+```proto
+// Incorrect.
+message BatchGetBooksRequest {
+  string publisher = 1;  // Field name should be `parent`.
+  repeated string names = 2;
+}
+```
+
+```proto
+// Incorrect.
+message BatchGetBookRequest {
+  bytes parent = 1;  // Field type should be `string`.
+  repeated string names = 2;
+}
+```
+
+**Correct** code for this rule:
+
+```proto
+// Correct.
+message CreateBookRequest {
+  string parent = 1;
+  repeated string names = 2;
+}
+```
+
+## Disabling
+
+If you need to violate this rule, use a leading comment above the message (if
+the `parent` 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::0231::request-parent-field=disabled
+//     aip.dev/not-precedent: We need to do this because reasons. --)
+message BatchGetBooksRequest {
+  string publisher = 1;  // Field name should be `parent`.
+  repeated string names = 2;
+}
+```
+
+If you need to violate this rule for an entire file, place the comment at the
+top of the file.
+
+[aip-231]: https://aip.dev/231
+[aip.dev/not-precedent]: https://aip.dev/not-precedent
diff --git a/docs/rules/core/0231-response-message-name.md b/docs/rules/core/0231-response-message-name.md
new file mode 100644
index 0000000..860f576
--- /dev/null
+++ b/docs/rules/core/0231-response-message-name.md
@@ -0,0 +1,63 @@
+---
+rule:
+  aip: 231
+  name: [core, '0231', response-message-name]
+  summary: Batch Get methods must have standardized response message names.
+---
+
+# Batch Get methods: Response message
+
+This rule enforces that all `BatchGet` RPCs have a response message name of
+`BatchGet*Response`, as mandated in [AIP-231][].
+
+## Details
+
+This rule looks at any message matching beginning with `BatchGet`, and
+complains if the name of the corresponding returned message does not match the
+name of the RPC with the suffix `Response` appended.
+
+## Examples
+
+**Incorrect** code for this rule:
+
+```proto
+// Incorrect.
+// Should be `BatchGetBooksResponse`.
+rpc BatchGetBooks(BatchGetBooksRequest) returns (Books) {
+  option (google.api.http) = {
+    get: "/v1/{parent=publishers/*}/books:batchGet"
+  };
+}
+```
+
+**Correct** code for this rule:
+
+```proto
+// Correct.
+rpc BatchGetBooks(BatchGetBooksRequest) returns (BatchGetBooksResponse) {
+  option (google.api.http) = {
+    get: "/v1/{parent=publishers/*}/books:batchGet"
+  };
+}
+```
+
+## 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::0231::response-message-name=disabled
+//     aip.dev/not-precedent: We need to do this because reasons. --)
+rpc BatchGetBooks(BatchGetBooksRequest) returns (Books) {
+  option (google.api.http) = {
+    get: "/v1/{parent=publishers/*}/books:batchGet"
+  };
+}
+```
+
+If you need to violate this rule for an entire file, place the comment at the
+top of the file.
+
+[aip-231]: https://aip.dev/231
+[aip.dev/not-precedent]: https://aip.dev/not-precedent
diff --git a/docs/rules/core/0231-response-resource-field.md b/docs/rules/core/0231-response-resource-field.md
new file mode 100644
index 0000000..5b31f99
--- /dev/null
+++ b/docs/rules/core/0231-response-resource-field.md
@@ -0,0 +1,64 @@
+---
+rule:
+  aip: 231
+  name: [core, '0231', response-resource-field]
+  summary:
+    Batch Get RPCs must have a repeated field for the resource in the response.
+---
+
+# Batch Get methods: Resource field
+
+This rule enforces that all `BatchGet` methods have a repeated field in the
+response message for the resource itself, as mandated in [AIP-231][].
+
+## Details
+
+This rule looks at any message matching `BatchGet*Response` and complains if
+there is no repeated field of the resource's type with the expected field name.
+
+## Examples
+
+**Incorrect** code for this rule:
+
+```proto
+// Incorrect.
+message BatchGetBooksResponse {
+  // `repeated Book books` is missing.
+}
+```
+
+```proto
+// Incorrect.
+message BatchGetBooksResponse {
+  Book books = 1;  // Field should be repeated.
+}
+```
+
+**Correct** code for this rule:
+
+```proto
+// Correct.
+message BatchGetBooksResponse {
+  repeated Book books = 1;
+}
+```
+
+## Disabling
+
+If you need to violate this rule, use a leading comment above the message (if
+the resource field is missing) or above the field (if it is improperly named).
+Remember to also include an [aip.dev/not-precedent][] comment explaining why.
+
+```proto
+// (-- api-linter: core::0231::response-resource-field=disabled
+//     aip.dev/not-precedent: We need to do this because reasons. --)
+message BatchGetBooksResponse {
+  Book books = 1;
+}
+```
+
+If you need to violate this rule for an entire file, place the comment at the
+top of the file.
+
+[aip-231]: https://aip.dev/231
+[aip.dev/not-precedent]: https://aip.dev/not-precedent
diff --git a/rules/aip0136/uri_suffix.go b/rules/aip0136/http_uri_suffix.go
similarity index 97%
rename from rules/aip0136/uri_suffix.go
rename to rules/aip0136/http_uri_suffix.go
index cc1753d..509028f 100644
--- a/rules/aip0136/uri_suffix.go
+++ b/rules/aip0136/http_uri_suffix.go
@@ -25,7 +25,7 @@ import (
 )
 
 var uriSuffix = &lint.MethodRule{
-	Name:   lint.NewRuleName("core", "0136", "uri-suffix"),
+	Name:   lint.NewRuleName("core", "0136", "http-uri-suffix"),
 	OnlyIf: isCustomMethod,
 	LintMethod: func(m *desc.MethodDescriptor) []lint.Problem {
 		for _, httpRule := range utils.GetHTTPRules(m) {
diff --git a/rules/aip0136/uri_suffix_test.go b/rules/aip0136/http_uri_suffix_test.go
similarity index 100%
rename from rules/aip0136/uri_suffix_test.go
rename to rules/aip0136/http_uri_suffix_test.go
diff --git a/rules/aip0231/aip0231.go b/rules/aip0231/aip0231.go
index f8cc400..b7bb28c 100644
--- a/rules/aip0231/aip0231.go
+++ b/rules/aip0231/aip0231.go
@@ -30,11 +30,11 @@ func AddRules(r lint.RuleRegistry) {
 		inputName,
 		outputName,
 		httpBody,
-		httpUrl,
 		httpVerb,
 		parentField,
 		namesField,
 		resourceField,
+		uriSuffix,
 	)
 }
 
diff --git a/rules/aip0231/http_body.go b/rules/aip0231/http_body.go
new file mode 100644
index 0000000..a321325
--- /dev/null
+++ b/rules/aip0231/http_body.go
@@ -0,0 +1,40 @@
+// 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 aip0231
+
+import (
+	"github.com/googleapis/api-linter/lint"
+	"github.com/googleapis/api-linter/rules/internal/utils"
+	"github.com/jhump/protoreflect/desc"
+)
+
+// Get methods should not have an HTTP body.
+var httpBody = &lint.MethodRule{
+	Name:   lint.NewRuleName("core", "0231", "http-body"),
+	OnlyIf: isBatchGetMethod,
+	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:    "Batch Get methods should not have an HTTP body.",
+					Descriptor: m,
+				}}
+			}
+		}
+
+		return nil
+	},
+}
diff --git a/rules/aip0231/http_method.go b/rules/aip0231/http_method.go
new file mode 100644
index 0000000..54f9306
--- /dev/null
+++ b/rules/aip0231/http_method.go
@@ -0,0 +1,40 @@
+// 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 aip0231
+
+import (
+	"github.com/googleapis/api-linter/lint"
+	"github.com/googleapis/api-linter/rules/internal/utils"
+	"github.com/jhump/protoreflect/desc"
+)
+
+// Batch Get methods should use the HTTP GET verb.
+var httpVerb = &lint.MethodRule{
+	Name:   lint.NewRuleName("core", "0231", "http-method"),
+	OnlyIf: isBatchGetMethod,
+	LintMethod: func(m *desc.MethodDescriptor) []lint.Problem {
+		// Rule check: Establish that the RPC uses HTTP GET.
+		for _, httpRule := range utils.GetHTTPRules(m) {
+			if httpRule.Method != "GET" {
+				return []lint.Problem{{
+					Message:    "Batch Get methods must use the HTTP GET verb.",
+					Descriptor: m,
+				}}
+			}
+		}
+
+		return nil
+	},
+}
diff --git a/rules/aip0231/http_uri_suffix.go b/rules/aip0231/http_uri_suffix.go
new file mode 100644
index 0000000..290f057
--- /dev/null
+++ b/rules/aip0231/http_uri_suffix.go
@@ -0,0 +1,40 @@
+// 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 aip0231
+
+import (
+	"github.com/googleapis/api-linter/lint"
+	"github.com/googleapis/api-linter/rules/internal/utils"
+	"github.com/jhump/protoreflect/desc"
+)
+
+// Batch Get methods should have a proper HTTP pattern.
+var uriSuffix = &lint.MethodRule{
+	Name:   lint.NewRuleName("core", "0231", "http-name"),
+	OnlyIf: isBatchGetMethod,
+	LintMethod: func(m *desc.MethodDescriptor) []lint.Problem {
+		// Establish that the RPC has no HTTP body.
+		for _, httpRule := range utils.GetHTTPRules(m) {
+			if !batchGetURINameRegexp.MatchString(httpRule.URI) {
+				return []lint.Problem{{
+					Message:    `Get methods URI should be end with ":batchGet".`,
+					Descriptor: m,
+				}}
+			}
+		}
+
+		return nil
+	},
+}
diff --git a/rules/aip0231/method_rules.go b/rules/aip0231/method_rules.go
deleted file mode 100644
index 19c874d..0000000
--- a/rules/aip0231/method_rules.go
+++ /dev/null
@@ -1,152 +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 aip0231
-
-import (
-	"fmt"
-
-	"github.com/gertd/go-pluralize"
-	"github.com/googleapis/api-linter/lint"
-	"github.com/googleapis/api-linter/rules/internal/utils"
-	"github.com/jhump/protoreflect/desc"
-)
-
-var pluralMethodResourceName = &lint.MethodRule{
-	Name:   lint.NewRuleName("core", "0231", "method-name", "name"),
-	OnlyIf: isBatchGetMethod,
-	LintMethod: func(m *desc.MethodDescriptor) []lint.Problem {
-		// Note: m.GetName()[8:] is used to retrieve the resource name from the
-		// method name. For example, "BatchGetFoos" -> "Foos"
-		pluralMethodResourceName := m.GetName()[8:]
-
-		pluralize := pluralize.NewClient()
-
-		// Rule check: Establish that for methods such as `BatchGetFoos`
-		if !pluralize.IsPlural(pluralMethodResourceName) {
-			return []lint.Problem{{
-				Message: fmt.Sprintf(
-					`The resource part in method name %q shouldn't %q, but should be its plural form %q`,
-					m.GetName(), pluralMethodResourceName, pluralize.Plural(pluralMethodResourceName),
-				),
-				Descriptor: m,
-			}}
-		}
-
-		return nil
-	},
-}
-
-// Batch Get method should have a properly named Request message.
-var inputName = &lint.MethodRule{
-	Name:   lint.NewRuleName("core", "0231", "input-message", "name"),
-	OnlyIf: isBatchGetMethod,
-	LintMethod: func(m *desc.MethodDescriptor) []lint.Problem {
-		pluralInputResourceName := pluralize.NewClient().Plural(m.GetName()[8:])
-
-		// Rule check: Establish that for methods such as `BatchGetFoos`, the request
-		// message is named `BatchGetFoosRequest`.
-		if got, want := m.GetInputType().GetName(), fmt.Sprintf("BatchGet%sRequest", pluralInputResourceName); got != want {
-			return []lint.Problem{{
-				Message: fmt.Sprintf(
-					"Batch Get RPCs should have a properly named request message %q, but not %q",
-					want, got,
-				),
-				Descriptor: m,
-			}}
-		}
-
-		return nil
-	},
-}
-
-// Batch Get method should have a properly named Response message.
-var outputName = &lint.MethodRule{
-	Name:   lint.NewRuleName("core", "0231", "output-message", "name"),
-	OnlyIf: isBatchGetMethod,
-	LintMethod: func(m *desc.MethodDescriptor) []lint.Problem {
-		pluralInputResourceName := pluralize.NewClient().Plural(m.GetName()[8:])
-
-		// Rule check: Establish that for methods such as `BatchGetFoos`, the request
-		// message is named `BatchGetFoosResponse`.
-		if got, want := m.GetOutputType().GetName(), fmt.Sprintf("BatchGet%sResponse", pluralInputResourceName); got != want {
-			return []lint.Problem{{
-				Message: fmt.Sprintf(
-					"Batch Get RPCs should have a properly named response message %q, but not %q",
-					want, got,
-				),
-				Descriptor: m,
-			}}
-		}
-
-		return nil
-	},
-}
-
-// Batch Get methods should use the HTTP GET verb.
-var httpVerb = &lint.MethodRule{
-	Name:   lint.NewRuleName("core", "0231", "http-verb"),
-	OnlyIf: isBatchGetMethod,
-	LintMethod: func(m *desc.MethodDescriptor) []lint.Problem {
-		// Rule check: Establish that the RPC uses HTTP GET.
-		for _, httpRule := range utils.GetHTTPRules(m) {
-			if httpRule.Method != "GET" {
-				return []lint.Problem{{
-					Message:    "Batch Get methods must use the HTTP GET verb.",
-					Descriptor: m,
-				}}
-			}
-		}
-
-		return nil
-	},
-}
-
-// Batch Get methods should have a proper HTTP pattern.
-var httpUrl = &lint.MethodRule{
-	Name:   lint.NewRuleName("core", "0231", "http-name"),
-	OnlyIf: isBatchGetMethod,
-	LintMethod: func(m *desc.MethodDescriptor) []lint.Problem {
-		// Establish that the RPC has no HTTP body.
-		for _, httpRule := range utils.GetHTTPRules(m) {
-			if !batchGetURINameRegexp.MatchString(httpRule.URI) {
-				return []lint.Problem{{
-					Message:    `Get methods URI should be end with ":batchGet".`,
-					Descriptor: m,
-				}}
-			}
-		}
-
-		return nil
-	},
-}
-
-// Get methods should not have an HTTP body.
-var httpBody = &lint.MethodRule{
-	Name:   lint.NewRuleName("core", "0231", "http-body"),
-	OnlyIf: isBatchGetMethod,
-	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:    "Batch Get methods should not have an HTTP body.",
-					Descriptor: m,
-				}}
-			}
-		}
-
-		return nil
-	},
-}
diff --git a/rules/aip0231/plural_method_name.go b/rules/aip0231/plural_method_name.go
new file mode 100644
index 0000000..f544d75
--- /dev/null
+++ b/rules/aip0231/plural_method_name.go
@@ -0,0 +1,48 @@
+// 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 aip0231
+
+import (
+	"fmt"
+
+	"github.com/gertd/go-pluralize"
+	"github.com/googleapis/api-linter/lint"
+	"github.com/jhump/protoreflect/desc"
+)
+
+var pluralMethodResourceName = &lint.MethodRule{
+	Name:   lint.NewRuleName("core", "0231", "plural-method-name"),
+	OnlyIf: isBatchGetMethod,
+	LintMethod: func(m *desc.MethodDescriptor) []lint.Problem {
+		// Note: m.GetName()[8:] is used to retrieve the resource name from the
+		// method name. For example, "BatchGetFoos" -> "Foos"
+		pluralMethodResourceName := m.GetName()[8:]
+
+		pluralize := pluralize.NewClient()
+
+		// Rule check: Establish that for methods such as `BatchGetFoos`
+		if !pluralize.IsPlural(pluralMethodResourceName) {
+			return []lint.Problem{{
+				Message: fmt.Sprintf(
+					`The resource part in method %q should not be %q, but should be its plural form %q`,
+					m.GetName(), pluralMethodResourceName, pluralize.Plural(pluralMethodResourceName),
+				),
+				Descriptor: m,
+			}}
+		}
+
+		return nil
+	},
+}
diff --git a/rules/aip0231/request_message_name.go b/rules/aip0231/request_message_name.go
new file mode 100644
index 0000000..9358e04
--- /dev/null
+++ b/rules/aip0231/request_message_name.go
@@ -0,0 +1,43 @@
+// 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 aip0231
+
+import (
+	"fmt"
+
+	"github.com/googleapis/api-linter/lint"
+	"github.com/jhump/protoreflect/desc"
+)
+
+// Batch Get method should have a properly named Request message.
+var inputName = &lint.MethodRule{
+	Name:   lint.NewRuleName("core", "0231", "request-message-name"),
+	OnlyIf: isBatchGetMethod,
+	LintMethod: func(m *desc.MethodDescriptor) []lint.Problem {
+		// Rule check: Establish that for methods such as `BatchGetFoos`, the request
+		// message is named `BatchGetFoosRequest`.
+		if got, want := m.GetInputType().GetName(), m.GetName()+"Request"; got != want {
+			return []lint.Problem{{
+				Message: fmt.Sprintf(
+					"Batch Get RPCs should have a properly named request message %q, but not %q",
+					want, got,
+				),
+				Descriptor: m,
+			}}
+		}
+
+		return nil
+	},
+}
diff --git a/rules/aip0231/message_rules.go b/rules/aip0231/request_names_field.go
similarity index 60%
rename from rules/aip0231/message_rules.go
rename to rules/aip0231/request_names_field.go
index 6dc692e..b44cf58 100644
--- a/rules/aip0231/message_rules.go
+++ b/rules/aip0231/request_names_field.go
@@ -23,36 +23,10 @@ import (
 	"github.com/jhump/protoreflect/desc/builder"
 )
 
-// The Batch Get request message should have parent field.
-var parentField = &lint.MessageRule{
-	Name:   lint.NewRuleName("core", "0231", "request-message", "parent-field"),
-	OnlyIf: isBatchGetRequestMessage,
-	LintMessage: func(m *desc.MessageDescriptor) []lint.Problem {
-		// Rule check: Establish that a `parent` field is present.
-		parentField := m.FindFieldByName("parent")
-		if parentField == nil {
-			return []lint.Problem{{
-				Message:    fmt.Sprintf("Message %q has no `parent` field", m.GetName()),
-				Descriptor: m,
-			}}
-		}
-
-		// Rule check: Establish that the parent field is a string.
-		if parentField.GetType() != builder.FieldTypeString().GetType() {
-			return []lint.Problem{{
-				Message:    "`parent` field on create request message must be a string",
-				Descriptor: parentField,
-			}}
-		}
-
-		return nil
-	},
-}
-
 // The Batch Get standard method should have repeated name field or repeated
 // standard get request message field, but the latter one is not suggested.
 var namesField = &lint.MessageRule{
-	Name:   lint.NewRuleName("core", "0231", "request-message", "name-field"),
+	Name:   lint.NewRuleName("core", "0231", "request-names-field"),
 	OnlyIf: isBatchGetRequestMessage,
 	LintMessage: func(m *desc.MessageDescriptor) (problems []lint.Problem) {
 		// Rule check: Establish that a name field is present.
@@ -113,35 +87,3 @@ var namesField = &lint.MessageRule{
 		return
 	},
 }
-
-// The Batch Get response message should have resource field.
-var resourceField = &lint.MessageRule{
-	Name:   lint.NewRuleName("core", "0231", "response-message", "resource-field"),
-	OnlyIf: isBatchGetResponseMessage,
-	LintMessage: func(m *desc.MessageDescriptor) []lint.Problem {
-		// the singular form the resource name, the first letter is Capitalized.
-		// Note: Use m.GetName()[8 : len(m.GetName())-9] to retrieve the resource
-		// name from the the batch get response, for example:
-		// "BatchGetBooksResponse" -> "Books"
-		resourceMsgName := pluralize.NewClient().Singular(m.GetName()[8 : len(m.GetName())-9])
-
-		for _, fieldDesc := range m.GetFields() {
-			if msgDesc := fieldDesc.GetMessageType(); msgDesc != nil && msgDesc.GetName() == resourceMsgName {
-				if !fieldDesc.IsRepeated() {
-					return []lint.Problem{{
-						Message:    fmt.Sprintf("The %q type field on Batch Get Response message should be repeated", msgDesc.GetName()),
-						Descriptor: fieldDesc,
-					}}
-				}
-
-				return nil
-			}
-		}
-
-		// Rule check: Establish that a resource field must be included.
-		return []lint.Problem{{
-			Message:    fmt.Sprintf("Message %q has no %q type field", m.GetName(), resourceMsgName),
-			Descriptor: m,
-		}}
-	},
-}
diff --git a/rules/aip0231/request_parent_field.go b/rules/aip0231/request_parent_field.go
new file mode 100644
index 0000000..88d8524
--- /dev/null
+++ b/rules/aip0231/request_parent_field.go
@@ -0,0 +1,49 @@
+// 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 aip0231
+
+import (
+	"fmt"
+
+	"github.com/googleapis/api-linter/lint"
+	"github.com/jhump/protoreflect/desc"
+	"github.com/jhump/protoreflect/desc/builder"
+)
+
+// The Batch Get request message should have parent field.
+var parentField = &lint.MessageRule{
+	Name:   lint.NewRuleName("core", "0231", "request-parent-field"),
+	OnlyIf: isBatchGetRequestMessage,
+	LintMessage: func(m *desc.MessageDescriptor) []lint.Problem {
+		// Rule check: Establish that a `parent` field is present.
+		parentField := m.FindFieldByName("parent")
+		if parentField == nil {
+			return []lint.Problem{{
+				Message:    fmt.Sprintf("Message %q has no `parent` field", m.GetName()),
+				Descriptor: m,
+			}}
+		}
+
+		// Rule check: Establish that the parent field is a string.
+		if parentField.GetType() != builder.FieldTypeString().GetType() {
+			return []lint.Problem{{
+				Message:    "`parent` field on create request message must be a string",
+				Descriptor: parentField,
+			}}
+		}
+
+		return nil
+	},
+}
diff --git a/rules/aip0231/response_message_name.go b/rules/aip0231/response_message_name.go
new file mode 100644
index 0000000..6131200
--- /dev/null
+++ b/rules/aip0231/response_message_name.go
@@ -0,0 +1,43 @@
+// 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 aip0231
+
+import (
+	"fmt"
+
+	"github.com/googleapis/api-linter/lint"
+	"github.com/jhump/protoreflect/desc"
+)
+
+// Batch Get method should have a properly named Response message.
+var outputName = &lint.MethodRule{
+	Name:   lint.NewRuleName("core", "0231", "output-message", "name"),
+	OnlyIf: isBatchGetMethod,
+	LintMethod: func(m *desc.MethodDescriptor) []lint.Problem {
+		// Rule check: Establish that for methods such as `BatchGetFoos`, the request
+		// message is named `BatchGetFoosResponse`.
+		if got, want := m.GetOutputType().GetName(), m.GetName()+"Response"; got != want {
+			return []lint.Problem{{
+				Message: fmt.Sprintf(
+					"Batch Get RPCs should have a properly named response message %q, but not %q",
+					want, got,
+				),
+				Descriptor: m,
+			}}
+		}
+
+		return nil
+	},
+}
diff --git a/rules/aip0231/response_resource_field.go b/rules/aip0231/response_resource_field.go
new file mode 100644
index 0000000..3c716eb
--- /dev/null
+++ b/rules/aip0231/response_resource_field.go
@@ -0,0 +1,55 @@
+// 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 aip0231
+
+import (
+	"fmt"
+
+	"github.com/gertd/go-pluralize"
+	"github.com/googleapis/api-linter/lint"
+	"github.com/jhump/protoreflect/desc"
+)
+
+// The Batch Get response message should have resource field.
+var resourceField = &lint.MessageRule{
+	Name:   lint.NewRuleName("core", "0231", "response-resource-field"),
+	OnlyIf: isBatchGetResponseMessage,
+	LintMessage: func(m *desc.MessageDescriptor) []lint.Problem {
+		// the singular form the resource name, the first letter is Capitalized.
+		// Note: Use m.GetName()[8 : len(m.GetName())-9] to retrieve the resource
+		// name from the the batch get response, for example:
+		// "BatchGetBooksResponse" -> "Books"
+		resourceMsgName := pluralize.NewClient().Singular(m.GetName()[8 : len(m.GetName())-9])
+
+		for _, fieldDesc := range m.GetFields() {
+			if msgDesc := fieldDesc.GetMessageType(); msgDesc != nil && msgDesc.GetName() == resourceMsgName {
+				if !fieldDesc.IsRepeated() {
+					return []lint.Problem{{
+						Message:    fmt.Sprintf("The %q type field on Batch Get Response message should be repeated", msgDesc.GetName()),
+						Descriptor: fieldDesc,
+					}}
+				}
+
+				return nil
+			}
+		}
+
+		// Rule check: Establish that a resource field must be included.
+		return []lint.Problem{{
+			Message:    fmt.Sprintf("Message %q has no %q type field", m.GetName(), resourceMsgName),
+			Descriptor: m,
+		}}
+	},
+}


Test patch:
diff --git a/rules/aip0231/aip0231_test.go b/rules/aip0231/aip0231_test.go
index e2b37b9..5071041 100644
--- a/rules/aip0231/aip0231_test.go
+++ b/rules/aip0231/aip0231_test.go
@@ -19,6 +19,7 @@ import (
 	"testing"
 
 	"github.com/googleapis/api-linter/lint"
+	"github.com/jhump/protoreflect/desc/builder"
 )
 
 func TestAddRules(t *testing.T) {
@@ -30,3 +31,8 @@ func TestAddRules(t *testing.T) {
 		}
 	}
 }
+
+type field struct {
+	fieldName string
+	fieldType *builder.FieldType
+}
diff --git a/rules/aip0231/http_body_test.go b/rules/aip0231/http_body_test.go
new file mode 100644
index 0000000..923bbbb
--- /dev/null
+++ b/rules/aip0231/http_body_test.go
@@ -0,0 +1,70 @@
+// 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 aip0231
+
+import (
+	"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 TestHttpBody(t *testing.T) {
+	tests := []struct {
+		testName   string
+		body       string
+		methodName string
+		problems   testutils.Problems
+	}{
+		{"Valid", "", "BatchGetBooks", nil},
+		{"Invalid", "*", "BatchGetBooks", testutils.Problems{{Message: "Batch Get methods should not have an HTTP body."}}},
+		{"Irrelevant", "*", "AcquireBook", nil},
+	}
+
+	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-231 Get method
+			// (or with a different method, in the "Irrelevant" case).
+			service, err := builder.NewService("BookService").AddMethod(builder.NewMethod(test.methodName,
+				builder.RpcTypeMessage(builder.NewMessage("BatchGetBooksRequest"), false),
+				builder.RpcTypeMessage(builder.NewMessage("BatchGetBooksResponse"), 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 diff := test.problems.SetDescriptor(service.GetMethods()[0]).Diff(problems); diff != "" {
+				t.Errorf(diff)
+			}
+		})
+	}
+}
diff --git a/rules/aip0231/http_method_test.go b/rules/aip0231/http_method_test.go
new file mode 100644
index 0000000..bf2e242
--- /dev/null
+++ b/rules/aip0231/http_method_test.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 aip0231
+
+import (
+	"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 TestHttpVerb(t *testing.T) {
+	// Set up GET and POST HTTP annotations.
+	httpGet := &annotations.HttpRule{
+		Pattern: &annotations.HttpRule_Get{
+			Get: "/v1/{parent=publishers/*}/books:batchGet",
+		},
+	}
+	httpPost := &annotations.HttpRule{
+		Pattern: &annotations.HttpRule_Post{
+			Post: "/v1/{name=publishers/*/books/*}",
+		},
+	}
+
+	// Set up testing permutations.
+	tests := []struct {
+		testName   string
+		httpRule   *annotations.HttpRule
+		methodName string
+		problems   testutils.Problems
+	}{
+		{"Valid", httpGet, "BatchGetBooks", nil},
+		{"Invalid", httpPost, "BatchGetBooks", testutils.Problems{{Message: "Batch Get methods must use the HTTP GET verb."}}},
+		{"Irrelevant", httpPost, "AcquireBook", nil},
+	}
+
+	// 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-231 Get method
+			// (or with a different method, in the "Irrelevant" case).
+			service, err := builder.NewService("BookService").AddMethod(builder.NewMethod(test.methodName,
+				builder.RpcTypeMessage(builder.NewMessage("BatchGetBooksRequest"), false),
+				builder.RpcTypeMessage(builder.NewMessage("BatchGetBooksResponse"), 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 diff := test.problems.SetDescriptor(service.GetMethods()[0]).Diff(problems); diff != "" {
+				t.Errorf(diff)
+			}
+		})
+	}
+}
diff --git a/rules/aip0231/http_uri_suffix_test.go b/rules/aip0231/http_uri_suffix_test.go
new file mode 100644
index 0000000..7532613
--- /dev/null
+++ b/rules/aip0231/http_uri_suffix_test.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 aip0231
+
+import (
+	"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 TestHttpUrl(t *testing.T) {
+	tests := []struct {
+		testName   string
+		uri        string
+		methodName string
+		problems   testutils.Problems
+	}{
+		{"Valid", "/v1/{parent=publishers/*}/books:batchGet", "BatchGetBooks", nil},
+		{"InvalidVarName", "/v1/{parent=publishers/*}/books", "BatchGetBooks", testutils.Problems{{Message: `Get methods URI should be end with ":batchGet".`}}},
+		{"Irrelevant", "/v1/{book=publishers/*/books/*}", "AcquireBook", nil},
+	}
+
+	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: 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-231 Get method
+			// (or with a different method, in the "Irrelevant" case).
+			service, err := builder.NewService("BookService").AddMethod(builder.NewMethod(test.methodName,
+				builder.RpcTypeMessage(builder.NewMessage("BatchGetBooksRequest"), false),
+				builder.RpcTypeMessage(builder.NewMessage("BatchGetBooksResponse"), 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 := uriSuffix.Lint(service.GetFile())
+			if diff := test.problems.SetDescriptor(service.GetMethods()[0]).Diff(problems); diff != "" {
+				t.Errorf(diff)
+			}
+		})
+	}
+}
diff --git a/rules/aip0231/method_rules_test.go b/rules/aip0231/method_rules_test.go
deleted file mode 100644
index 8f397a5..0000000
--- a/rules/aip0231/method_rules_test.go
+++ /dev/null
@@ -1,509 +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 aip0231
-
-import (
-	"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 TestPluralMethodResourceName(t *testing.T) {
-	// Set up the testing permutations.
-	tests := []struct {
-		testName string
-		src      string
-		problems testutils.Problems
-	}{
-		{
-			testName: "Valid-BatchGetBooks",
-			src: `import "google/api/annotations.proto";
-
-service BookService {
-	rpc BatchGetBooks(BatchGetBooksRequest) returns (BatchGetBooksResponse) {
-		option (google.api.http) = {
-			get: "/v1/{parent=publishers/*}/books:batchGet"
-		};
-	}
-}
-
-message BatchGetBooksRequest {}
-
-message BatchGetBooksResponse{}
-`,
-			problems: testutils.Problems{},
-		},
-		{
-			testName: "Valid-BatchGetMen",
-			src: `import "google/api/annotations.proto";
-
-service ManService {
-	rpc BatchGetMen(BatchGetMenRequest) returns (BatchGetMenResponse) {
-		option (google.api.http) = {
-			get: "/v1/{parent=publishers/*}/men:batchGet"
-		};
-	}
-}
-
-message BatchGetMenRequest {}
-
-message BatchGetMenResponse{}
-`,
-			problems: testutils.Problems{},
-		},
-		{
-			testName: "Invalid-SingularBus",
-			src: `import "google/api/annotations.proto";
-
-service BusService {
-	rpc BatchGetBus(BatchGetBusRequest) returns (BatchGetBusResponse) {
-		option (google.api.http) = {
-			get: "/v1/{parent=publishers/*}/buses:batchGet"
-		};
-	}
-}
-
-message BatchGetBusRequest {}
-
-message BatchGetBusResponse{}
-`,
-			problems: testutils.Problems{{Message: `The resource part in method name "BatchGetBus" shouldn't "Bus", but should be its plural form "Buses"`}},
-		},
-		{
-			testName: "Invalid-SingularCorpPerson",
-			src: `import "google/api/annotations.proto";
-
-service CorpPersonService {
-	rpc BatchGetCorpPerson(BatchGetCorpPersonRequest) returns (BatchGetCorpPersonResponse) {
-		option (google.api.http) = {
-			get: "/v1/{parent=publishers/*}/corpPerson:batchGet"
-		};
-	}
-}
-
-message BatchGetCorpPersonRequest {}
-
-message BatchGetCorpPersonResponse{}
-`,
-			problems: testutils.Problems{{Message: `The resource part in method name "BatchGetCorpPerson" shouldn't "CorpPerson", but should be its plural form "CorpPeople"`}},
-		},
-		{
-			testName: "Invalid-Irrelevant",
-			src: `import "google/api/annotations.proto";
-
-service BookService {
-	rpc GetBook(GetBookRequest) returns (Book) {
-		option (google.api.http) = {
-			get: "/v1/{name=publishers/*/books/*}"
-		};
-	}
-}
-
-message GetBookRequest {}
-
-message Book{}
-`,
-			problems: testutils.Problems{},
-		},
-	}
-
-	// Run each test individually.
-	for _, test := range tests {
-		t.Run(test.testName, func(t *testing.T) {
-			file := testutils.ParseProto3String(t, test.src)
-
-			m := file.GetServices()[0].GetMethods()[0]
-
-			problems := pluralMethodResourceName.Lint(file)
-			if diff := test.problems.SetDescriptor(m).Diff(problems); diff != "" {
-				t.Errorf(diff)
-			}
-		})
-	}
-}
-
-func TestInputName(t *testing.T) {
-	// Set up the testing permutations.
-	tests := []struct {
-		testName string
-		src      string
-		problems testutils.Problems
-	}{
-		{
-			testName: "Valid-BatchGetBooksRequest",
-			src: `import "google/api/annotations.proto";
-
-service BookService {
-	rpc BatchGetBooks(BatchGetBooksRequest) returns (BatchGetBooksResponse) {
-		option (google.api.http) = {
-			get: "/v1/{parent=publishers/*}/books:batchGet"
-		};
-	}
-}
-
-message BatchGetBooksRequest {}
-
-message BatchGetBooksResponse{}
-`,
-			problems: testutils.Problems{},
-		},
-		{
-			testName: "Valid-BatchGetMenRequest",
-			src: `import "google/api/annotations.proto";
-
-service ManService {
-	rpc BatchGetMen(BatchGetMenRequest) returns (BatchGetMenResponse) {
-		option (google.api.http) = {
-			get: "/v1/{parent=publishers/*}/men:batchGet"
-		};
-	}
-}
-
-message BatchGetMenRequest {}
-
-message BatchGetMenResponse{}
-`,
-			problems: testutils.Problems{},
-		},
-		{
-			testName: "Invalid-SingularBus",
-			src: `import "google/api/annotations.proto";
-
-service BusService {
-	rpc BatchGetBuses(BatchGetBusRequest) returns (BatchGetBusResponse) {
-		option (google.api.http) = {
-			get: "/v1/{parent=publishers/*}/buses:batchGet"
-		};
-	}
-}
-
-message BatchGetBusRequest {}
-
-message BatchGetBusResponse{}
-`,
-			problems: testutils.Problems{{Message: `Batch Get RPCs should have a properly named request message "BatchGetBusesRequest", but not "BatchGetBusRequest"`}},
-		},
-		{
-			testName: "Invalid-SingularCorpPerson",
-			src: `import "google/api/annotations.proto";
-
-service CorpPersonService {
-	rpc BatchGetCorpPerson(BatchGetCorpPersonRequest) returns (BatchGetCorpPersonResponse) {
-		option (google.api.http) = {
-			get: "/v1/{parent=publishers/*}/corpPerson:batchGet"
-		};
-	}
-}
-
-message BatchGetCorpPersonRequest {}
-
-message BatchGetCorpPersonResponse{}
-`,
-			problems: testutils.Problems{{Message: `Batch Get RPCs should have a properly named request message "BatchGetCorpPeopleRequest", but not "BatchGetCorpPersonRequest"`}},
-		},
-		{
-			testName: "Irrelevant",
-			src: `import "google/api/annotations.proto";
-
-service BookService {
-	rpc GetBook(GetBookRequest) returns (Book) {
-		option (google.api.http) = {
-			get: "/v1/{name=publishers/*/books/*}"
-		};
-	}
-}
-
-message GetBookRequest {}
-
-message Book{}
-`,
-			problems: testutils.Problems{},
-		},
-	}
-
-	// Run each test individually.
-	for _, test := range tests {
-		t.Run(test.testName, func(t *testing.T) {
-			file := testutils.ParseProto3String(t, test.src)
-
-			m := file.GetServices()[0].GetMethods()[0]
-
-			problems := inputName.Lint(file)
-			if diff := test.problems.SetDescriptor(m).Diff(problems); diff != "" {
-				t.Errorf(diff)
-			}
-		})
-	}
-}
-
-func TestOutputName(t *testing.T) {
-	// Set up the testing permutations.
-	tests := []struct {
-		testName string
-		src      string
-		problems testutils.Problems
-	}{
-		{
-			testName: "Valid-BatchGetBooksResponse",
-			src: `import "google/api/annotations.proto";
-
-service BookService {
-	rpc BatchGetBooks(BatchGetBooksRequest) returns (BatchGetBooksResponse) {
-		option (google.api.http) = {
-			get: "/v1/{parent=publishers/*}/books:batchGet"
-		};
-	}
-}
-
-message BatchGetBooksRequest {}
-
-message BatchGetBooksResponse{}
-`,
-			problems: testutils.Problems{},
-		},
-		{
-			testName: "Valid-BatchGetBooksResponse",
-			src: `import "google/api/annotations.proto";
-
-service ManService {
-	rpc BatchGetMen(BatchGetMenRequest) returns (BatchGetMenResponse) {
-		option (google.api.http) = {
-			get: "/v1/{parent=publishers/*}/men:batchGet"
-		};
-	}
-}
-
-message BatchGetMenRequest {}
-
-message BatchGetMenResponse{}
-`,
-			problems: testutils.Problems{},
-		},
-		{
-			testName: "Invalid-SingularBus",
-			src: `import "google/api/annotations.proto";
-
-service BusService {
-	rpc BatchGetBuses(BatchGetBusRequest) returns (BatchGetBusResponse) {
-		option (google.api.http) = {
-			get: "/v1/{parent=publishers/*}/buses:batchGet"
-		};
-	}
-}
-
-message BatchGetBusRequest {}
-
-message BatchGetBusResponse{}
-`,
-			problems: testutils.Problems{{Message: `Batch Get RPCs should have a properly named response message "BatchGetBusesResponse", but not "BatchGetBusResponse"`}},
-		},
-		{
-			testName: "Invalid-SingularCorpPerson",
-			src: `import "google/api/annotations.proto";
-
-service CorpPersonService {
-	rpc BatchGetCorpPerson(BatchGetCorpPersonRequest) returns (BatchGetCorpPersonResponse) {
-		option (google.api.http) = {
-			get: "/v1/{parent=publishers/*}/corpPerson:batchGet"
-		};
-	}
-}
-
-message BatchGetCorpPersonRequest {}
-
-message BatchGetCorpPersonResponse{}
-`,
-			problems: testutils.Problems{{Message: `Batch Get RPCs should have a properly named response message "BatchGetCorpPeopleResponse", but not "BatchGetCorpPersonResponse"`}},
-		},
-		{
-			testName: "Irrelevant",
-			src: `import "google/api/annotations.proto";
-
-service BookService {
-	rpc GetBook(GetBookRequest) returns (Book) {
-		option (google.api.http) = {
-			get: "/v1/{name=publishers/*/books/*}"
-		};
-	}
-}
-
-message GetBookRequest {}
-
-message Book{}
-`,
-			problems: testutils.Problems{},
-		},
-	}
-
-	// Run each test individually.
-	for _, test := range tests {
-		t.Run(test.testName, func(t *testing.T) {
-			file := testutils.ParseProto3String(t, test.src)
-
-			m := file.GetServices()[0].GetMethods()[0]
-
-			problems := outputName.Lint(file)
-			if diff := test.problems.SetDescriptor(m).Diff(problems); diff != "" {
-				t.Errorf(diff)
-			}
-		})
-	}
-}
-
-func TestHttpVerb(t *testing.T) {
-	// Set up GET and POST HTTP annotations.
-	httpGet := &annotations.HttpRule{
-		Pattern: &annotations.HttpRule_Get{
-			Get: "/v1/{parent=publishers/*}/books:batchGet",
-		},
-	}
-	httpPost := &annotations.HttpRule{
-		Pattern: &annotations.HttpRule_Post{
-			Post: "/v1/{name=publishers/*/books/*}",
-		},
-	}
-
-	// Set up testing permutations.
-	tests := []struct {
-		testName   string
-		httpRule   *annotations.HttpRule
-		methodName string
-		problems   testutils.Problems
-	}{
-		{"Valid", httpGet, "BatchGetBooks", nil},
-		{"Invalid", httpPost, "BatchGetBooks", testutils.Problems{{Message: "Batch Get methods must use the HTTP GET verb."}}},
-		{"Irrelevant", httpPost, "AcquireBook", nil},
-	}
-
-	// 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-231 Get method
-			// (or with a different method, in the "Irrelevant" case).
-			service, err := builder.NewService("BookService").AddMethod(builder.NewMethod(test.methodName,
-				builder.RpcTypeMessage(builder.NewMessage("BatchGetBooksRequest"), false),
-				builder.RpcTypeMessage(builder.NewMessage("BatchGetBooksResponse"), 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 diff := test.problems.SetDescriptor(service.GetMethods()[0]).Diff(problems); diff != "" {
-				t.Errorf(diff)
-			}
-		})
-	}
-}
-
-func TestHttpUrl(t *testing.T) {
-	tests := []struct {
-		testName   string
-		uri        string
-		methodName string
-		problems   testutils.Problems
-	}{
-		{"Valid", "/v1/{parent=publishers/*}/books:batchGet", "BatchGetBooks", nil},
-		{"InvalidVarName", "/v1/{parent=publishers/*}/books", "BatchGetBooks", testutils.Problems{{Message: `Get methods URI should be end with ":batchGet".`}}},
-		{"Irrelevant", "/v1/{book=publishers/*/books/*}", "AcquireBook", nil},
-	}
-
-	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: 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-231 Get method
-			// (or with a different method, in the "Irrelevant" case).
-			service, err := builder.NewService("BookService").AddMethod(builder.NewMethod(test.methodName,
-				builder.RpcTypeMessage(builder.NewMessage("BatchGetBooksRequest"), false),
-				builder.RpcTypeMessage(builder.NewMessage("BatchGetBooksResponse"), 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 := httpUrl.Lint(service.GetFile())
-			if diff := test.problems.SetDescriptor(service.GetMethods()[0]).Diff(problems); diff != "" {
-				t.Errorf(diff)
-			}
-		})
-	}
-}
-
-func TestHttpBody(t *testing.T) {
-	tests := []struct {
-		testName   string
-		body       string
-		methodName string
-		problems   testutils.Problems
-	}{
-		{"Valid", "", "BatchGetBooks", nil},
-		{"Invalid", "*", "BatchGetBooks", testutils.Problems{{Message: "Batch Get methods should not have an HTTP body."}}},
-		{"Irrelevant", "*", "AcquireBook", nil},
-	}
-
-	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-231 Get method
-			// (or with a different method, in the "Irrelevant" case).
-			service, err := builder.NewService("BookService").AddMethod(builder.NewMethod(test.methodName,
-				builder.RpcTypeMessage(builder.NewMessage("BatchGetBooksRequest"), false),
-				builder.RpcTypeMessage(builder.NewMessage("BatchGetBooksResponse"), 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 diff := test.problems.SetDescriptor(service.GetMethods()[0]).Diff(problems); diff != "" {
-				t.Errorf(diff)
-			}
-		})
-	}
-}
diff --git a/rules/aip0231/plural_method_name_test.go b/rules/aip0231/plural_method_name_test.go
new file mode 100644
index 0000000..4a74202
--- /dev/null
+++ b/rules/aip0231/plural_method_name_test.go
@@ -0,0 +1,91 @@
+// 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 aip0231
+
+import (
+	"testing"
+
+	"github.com/googleapis/api-linter/rules/internal/testutils"
+)
+
+func TestPluralMethodResourceName(t *testing.T) {
+	// Set up the testing permutations.
+	tests := []struct {
+		testName       string
+		src            string
+		MethodName     string
+		CollectionName string
+		problems       testutils.Problems
+	}{
+		{
+			testName:       "ValidBatchGetBooks",
+			MethodName:     "BatchGetBooks",
+			CollectionName: "books",
+			problems:       testutils.Problems{},
+		},
+		{
+			testName:       "ValidBatchGetMen",
+			MethodName:     "BatchGetMen",
+			CollectionName: "men",
+			problems:       testutils.Problems{},
+		},
+		{
+			testName:       "InvalidSingularBus",
+			MethodName:     "BatchGetBus",
+			CollectionName: "buses",
+			problems:       testutils.Problems{{Message: "Buses"}},
+		},
+		{
+			testName:       "Invalid-SingularCorpPerson",
+			MethodName:     "BatchGetCorpPerson",
+			CollectionName: "corpPerson",
+			problems:       testutils.Problems{{Message: "CorpPeople"}},
+		},
+		{
+			testName:       "Invalid-Irrelevant",
+			MethodName:     "GetBook",
+			CollectionName: "books",
+			problems:       testutils.Problems{},
+		},
+	}
+
+	// Run each test individually.
+	for _, test := range tests {
+		t.Run(test.testName, func(t *testing.T) {
+			file := testutils.ParseProto3Tmpl(t, `
+				import "google/api/annotations.proto";
+
+				service Test {
+					rpc {{.MethodName}}({{.MethodName}}Request) returns ({{.MethodName}}Response) {
+						option (google.api.http) = {
+							get: "/v1/{parent=publishers/*}/{{.CollectionName}}:batchGet"
+						};
+					}
+				}
+
+				message {{.MethodName}}Request {}
+
+				message {{.MethodName}}Response {}
+			`, test)
+
+			m := file.GetServices()[0].GetMethods()[0]
+
+			problems := pluralMethodResourceName.Lint(file)
+			if diff := test.problems.SetDescriptor(m).Diff(problems); diff != "" {
+				t.Errorf(diff)
+			}
+		})
+	}
+}
diff --git a/rules/aip0231/request_message_name_test.go b/rules/aip0231/request_message_name_test.go
new file mode 100644
index 0000000..aedc447
--- /dev/null
+++ b/rules/aip0231/request_message_name_test.go
@@ -0,0 +1,62 @@
+// 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 aip0231
+
+import (
+	"testing"
+
+	"github.com/googleapis/api-linter/rules/internal/testutils"
+)
+
+func TestInputName(t *testing.T) {
+	// Set up the testing permutations.
+	tests := []struct {
+		testName   string
+		MethodName string
+		InputName  string
+		problems   testutils.Problems
+	}{
+		{"Valid", "BatchGetBooks", "BatchGetBooksRequest", testutils.Problems{}},
+		{"Invalid", "BatchGetBooks", "GetBookRequest", testutils.Problems{{Message: "BatchGetBooksRequest"}}},
+		{"Irrelevant", "GetBookRequest", "Foo", testutils.Problems{}},
+	}
+
+	// Run each test individually.
+	for _, test := range tests {
+		t.Run(test.testName, func(t *testing.T) {
+			file := testutils.ParseProto3Tmpl(t, `
+				import "google/api/annotations.proto";
+
+				service Library {
+					rpc {{.MethodName}}({{.InputName}}) returns ({{.MethodName}}Response) {
+						option (google.api.http) = {
+							get: "/v1/{parent=publishers/*}/books:batchGet"
+						};
+					}
+				}
+
+				message {{.InputName}} {}
+				message {{.MethodName}}Response {}
+			`, test)
+
+			m := file.GetServices()[0].GetMethods()[0]
+
+			problems := inputName.Lint(file)
+			if diff := test.problems.SetDescriptor(m).Diff(problems); diff != "" {
+				t.Errorf(diff)
+			}
+		})
+	}
+}
diff --git a/rules/aip0231/message_rules_test.go b/rules/aip0231/request_names_field_test.go
similarity index 52%
rename from rules/aip0231/message_rules_test.go
rename to rules/aip0231/request_names_field_test.go
index ce8a1d7..ebb4b89 100644
--- a/rules/aip0231/message_rules_test.go
+++ b/rules/aip0231/request_names_field_test.go
@@ -19,150 +19,8 @@ import (
 
 	"github.com/googleapis/api-linter/rules/internal/testutils"
 	"github.com/jhump/protoreflect/desc"
-	"github.com/jhump/protoreflect/desc/builder"
 )
 
-type field struct {
-	fieldName string
-	fieldType *builder.FieldType
-}
-
-func TestParentField(t *testing.T) {
-	// Set up the testing permutations.
-	tests := []struct {
-		testName     string
-		messageName  string
-		messageField *field
-		problems     testutils.Problems
-		problemDesc  func(m *desc.MessageDescriptor) desc.Descriptor
-	}{
-		{
-			"Valid",
-			"BatchGetBooksRequest",
-			&field{"parent", builder.FieldTypeString()},
-			testutils.Problems{},
-			nil},
-		{
-			"MissingField",
-			"BatchGetBooksRequest",
-			nil,
-			testutils.Problems{{Message: "parent"}},
-			nil,
-		},
-		{
-			"InvalidType",
-			"BatchGetBooksRequest",
-			&field{"parent", builder.FieldTypeDouble()},
-			testutils.Problems{{Message: "string"}},
-			func(m *desc.MessageDescriptor) desc.Descriptor {
-				return m.FindFieldByName("parent")
-			},
-		},
-		{
-			"Irrelevant",
-			"EnumerateBooksRequest",
-			&field{"id", builder.FieldTypeString()},
-			testutils.Problems{},
-			nil,
-		},
-	}
-
-	// Run each test individually.
-	for _, test := range tests {
-		t.Run(test.testName, func(t *testing.T) {
-			// Create an appropriate message descriptor.
-			messageBuilder := builder.NewMessage(test.messageName)
-
-			if test.messageField != nil {
-				messageBuilder.AddField(
-					builder.NewField(test.messageField.fieldName, test.messageField.fieldType),
-				)
-			}
-
-			message, err := messageBuilder.Build()
-			if err != nil {
-				t.Fatalf("Could not build %s message.", test.messageName)
-			}
-
-			// Determine the descriptor that a failing test will attach to.
-			var problemDesc desc.Descriptor = message
-			if test.problemDesc != nil {
-				problemDesc = test.problemDesc(message)
-			}
-
-			// Run the lint rule, and establish that it returns the correct problems.
-			problems := parentField.Lint(message.GetFile())
-			if diff := test.problems.SetDescriptor(problemDesc).Diff(problems); diff != "" {
-				t.Errorf(diff)
-			}
-		})
-	}
-}
-
-func TestResourceField(t *testing.T) {
-	// Set up the testing permutations.
-	tests := []struct {
-		testName    string
-		src         string
-		problems    testutils.Problems
-		problemDesc func(m *desc.MessageDescriptor) desc.Descriptor
-	}{
-		{
-			testName: "Valid",
-			src: `
-message BatchGetBooksResponse {
-  // Books requested.
-  repeated Book books = 1;
-}`,
-			problems: testutils.Problems{},
-		},
-		{
-			testName: "FieldIsNotRepeated",
-			src: `
-message BatchGetBooksResponse {
-  // Book requested.
-  Book book = 1;
-}`,
-			problems: testutils.Problems{{Message: "The \"Book\" type field on Batch Get Response message should be repeated"}},
-		},
-		{
-			testName: "MissingField",
-			src: `
-message BatchGetBooksResponse {
-  string response = 1;
-}`,
-			problems: testutils.Problems{{Message: "Message \"BatchGetBooksResponse\" has no \"Book\" type field"}},
-			problemDesc: func(m *desc.MessageDescriptor) desc.Descriptor {
-				return m
-			},
-		},
-	}
-
-	// Run each test individually.
-	for _, test := range tests {
-		t.Run(test.testName, func(t *testing.T) {
-			template := `
-{{.Src}}
-message Book {
-}`
-			file := testutils.ParseProto3Tmpl(t, template, struct{ Src string }{test.src})
-
-			m := file.GetMessageTypes()[0]
-
-			// Determine the descriptor that a failing test will attach to.
-			var problemDesc desc.Descriptor = m.GetFields()[0]
-			if test.problemDesc != nil {
-				problemDesc = test.problemDesc(m)
-			}
-
-			problems := resourceField.Lint(file)
-			if diff := test.problems.SetDescriptor(problemDesc).Diff(problems); diff != "" {
-				t.Errorf(diff)
-			}
-		})
-	}
-}
-
 func TestNamesField(t *testing.T) {
 	// Set up the testing permutations.
 	tests := []struct {
diff --git a/rules/aip0231/request_parent_field_test.go b/rules/aip0231/request_parent_field_test.go
new file mode 100644
index 0000000..ba394da
--- /dev/null
+++ b/rules/aip0231/request_parent_field_test.go
@@ -0,0 +1,95 @@
+// 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 aip0231
+
+import (
+	"testing"
+
+	"github.com/googleapis/api-linter/rules/internal/testutils"
+	"github.com/jhump/protoreflect/desc"
+	"github.com/jhump/protoreflect/desc/builder"
+)
+
+func TestParentField(t *testing.T) {
+	// Set up the testing permutations.
+	tests := []struct {
+		testName     string
+		messageName  string
+		messageField *field
+		problems     testutils.Problems
+		problemDesc  func(m *desc.MessageDescriptor) desc.Descriptor
+	}{
+		{
+			"Valid",
+			"BatchGetBooksRequest",
+			&field{"parent", builder.FieldTypeString()},
+			testutils.Problems{},
+			nil},
+		{
+			"MissingField",
+			"BatchGetBooksRequest",
+			nil,
+			testutils.Problems{{Message: "parent"}},
+			nil,
+		},
+		{
+			"InvalidType",
+			"BatchGetBooksRequest",
+			&field{"parent", builder.FieldTypeDouble()},
+			testutils.Problems{{Message: "string"}},
+			func(m *desc.MessageDescriptor) desc.Descriptor {
+				return m.FindFieldByName("parent")
+			},
+		},
+		{
+			"Irrelevant",
+			"EnumerateBooksRequest",
+			&field{"id", builder.FieldTypeString()},
+			testutils.Problems{},
+			nil,
+		},
+	}
+
+	// Run each test individually.
+	for _, test := range tests {
+		t.Run(test.testName, func(t *testing.T) {
+			// Create an appropriate message descriptor.
+			messageBuilder := builder.NewMessage(test.messageName)
+
+			if test.messageField != nil {
+				messageBuilder.AddField(
+					builder.NewField(test.messageField.fieldName, test.messageField.fieldType),
+				)
+			}
+
+			message, err := messageBuilder.Build()
+			if err != nil {
+				t.Fatalf("Could not build %s message.", test.messageName)
+			}
+
+			// Determine the descriptor that a failing test will attach to.
+			var problemDesc desc.Descriptor = message
+			if test.problemDesc != nil {
+				problemDesc = test.problemDesc(message)
+			}
+
+			// Run the lint rule, and establish that it returns the correct problems.
+			problems := parentField.Lint(message.GetFile())
+			if diff := test.problems.SetDescriptor(problemDesc).Diff(problems); diff != "" {
+				t.Errorf(diff)
+			}
+		})
+	}
+}
diff --git a/rules/aip0231/response_message_name_test.go b/rules/aip0231/response_message_name_test.go
new file mode 100644
index 0000000..1232dc3
--- /dev/null
+++ b/rules/aip0231/response_message_name_test.go
@@ -0,0 +1,62 @@
+// 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 aip0231
+
+import (
+	"testing"
+
+	"github.com/googleapis/api-linter/rules/internal/testutils"
+)
+
+func TestOutputName(t *testing.T) {
+	// Set up the testing permutations.
+	tests := []struct {
+		testName   string
+		MethodName string
+		OutputName string
+		problems   testutils.Problems
+	}{
+		{"Valid", "BatchGetBooks", "BatchGetBooksResponse", testutils.Problems{}},
+		{"Invalid", "BatchGetBooks", "Book", testutils.Problems{{Message: "BatchGetBooksResponse"}}},
+		{"Irrelevant", "GetBookRequest", "Book", testutils.Problems{}},
+	}
+
+	// Run each test individually.
+	for _, test := range tests {
+		t.Run(test.testName, func(t *testing.T) {
+			file := testutils.ParseProto3Tmpl(t, `
+				import "google/api/annotations.proto";
+
+				service Library {
+					rpc {{.MethodName}}({{.MethodName}}Request) returns ({{.OutputName}}) {
+						option (google.api.http) = {
+							get: "/v1/{parent=publishers/*}/books:batchGet"
+						};
+					}
+				}
+
+				message {{.MethodName}}Request {}
+				message {{.OutputName}} {}
+			`, test)
+
+			m := file.GetServices()[0].GetMethods()[0]
+
+			problems := outputName.Lint(file)
+			if diff := test.problems.SetDescriptor(m).Diff(problems); diff != "" {
+				t.Errorf(diff)
+			}
+		})
+	}
+}
diff --git a/rules/aip0231/response_resource_field_test.go b/rules/aip0231/response_resource_field_test.go
new file mode 100644
index 0000000..f569c2e
--- /dev/null
+++ b/rules/aip0231/response_resource_field_test.go
@@ -0,0 +1,86 @@
+// 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 aip0231
+
+import (
+	"testing"
+
+	"github.com/googleapis/api-linter/rules/internal/testutils"
+	"github.com/jhump/protoreflect/desc"
+)
+
+func TestResourceField(t *testing.T) {
+	// Set up the testing permutations.
+	tests := []struct {
+		testName    string
+		src         string
+		problems    testutils.Problems
+		problemDesc func(m *desc.MessageDescriptor) desc.Descriptor
+	}{
+		{
+			testName: "Valid",
+			src: `
+message BatchGetBooksResponse {
+  // Books requested.
+  repeated Book books = 1;
+}`,
+			problems: testutils.Problems{},
+		},
+		{
+			testName: "FieldIsNotRepeated",
+			src: `
+message BatchGetBooksResponse {
+  // Book requested.
+  Book book = 1;
+}`,
+			problems: testutils.Problems{{Message: "The \"Book\" type field on Batch Get Response message should be repeated"}},
+		},
+		{
+			testName: "MissingField",
+			src: `
+message BatchGetBooksResponse {
+  string response = 1;
+}`,
+			problems: testutils.Problems{{Message: "Message \"BatchGetBooksResponse\" has no \"Book\" type field"}},
+			problemDesc: func(m *desc.MessageDescriptor) desc.Descriptor {
+				return m
+			},
+		},
+	}
+
+	// Run each test individually.
+	for _, test := range tests {
+		t.Run(test.testName, func(t *testing.T) {
+			template := `
+{{.Src}}
+message Book {
+}`
+			file := testutils.ParseProto3Tmpl(t, template, struct{ Src string }{test.src})
+
+			m := file.GetMessageTypes()[0]
+
+			// Determine the descriptor that a failing test will attach to.
+			var problemDesc desc.Descriptor = m.GetFields()[0]
+			if test.problemDesc != nil {
+				problemDesc = test.problemDesc(m)
+			}
+
+			problems := resourceField.Lint(file)
+			if diff := test.problems.SetDescriptor(problemDesc).Diff(problems); diff != "" {
+				t.Errorf(diff)
+			}
+		})
+	}
+}