Repository: googleapis/api-linter
Pull request #278 (2019-11-01 21:52:47)

AIP-233: Batch methods: Create
AIP-233: Batch methods: Create
And some changes to AIP-231

mingzhi: @lukesneeringer please review and approve. Thanks!
lukesneeringer: Once #318 goes in, please make sure this passes the meta-linter. I will review after that.
mingzhi: @lukesneeringer Fixed and passed meta-lint.
lukesneeringer: Approved, but wait to merge until #333 goes in.

Patch:
diff --git a/docs/rules/core/0233-http-body.md b/docs/rules/core/0233-http-body.md
new file mode 100644
index 0000000..14f83e9
--- /dev/null
+++ b/docs/rules/core/0233-http-body.md
@@ -0,0 +1,63 @@
+---
+rule:
+  aip: 233
+  name: [core, '0233', http-body]
+  summary: Batch Create methods should use `*` as the HTTP body.
+---
+
+# Batch Create methods: HTTP body 
+
+This rule enforces that all `BatchCreate` RPCs use `*` as the HTTP `body`, as 
+mandated in [AIP-233][].
+
+## Details
+
+This rule looks at any RPC methods matching beginning with `BatchCreate`, and
+complains if HTTP `body` field is anything other than `*`.
+
+## Examples
+
+**Incorrect** code for this rule:
+
+```proto
+// Incorrect.
+rpc BatchCreateBooks(BatchCreateBooksRequest) returns (BatchCreateBooksResponse) {
+  option (google.api.http) = {
+    post: "/v1/{parent=publishers/*}/books:batchCreate"
+    body: "book"  // The body should be "*".
+  };
+}
+```
+
+**Correct** code for this rule:
+
+```proto
+// Correct.
+rpc BatchCreateBooks(BatchCreateBooksRequest) returns (BatchCreateBooksResponse) {
+  option (google.api.http) = {
+    post: "/v1/{parent=publishers/*}/books:batchCreate"
+    body: "*"
+  };
+}
+```
+
+## 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::0233::http-body=disabled
+//     aip.dev/not-precedent: We need to do this because reasons. --)
+rpc BatchCreateBooks(BatchCreateBooksRequest) returns (BatchCreateBooksResponse) {
+  option (google.api.http) = {
+    post: "/v1/{parent=publishers/*}/books:batchCreate"
+  };
+}
+```
+
+If you need to violate this rule for an entire file, place the comment at the
+top of the file.
+
+[aip-233]: https://aip.dev/233
+[aip.dev/not-precedent]: https://aip.dev/not-precedent
diff --git a/docs/rules/core/0233-http-method.md b/docs/rules/core/0233-http-method.md
new file mode 100644
index 0000000..5cf3b50
--- /dev/null
+++ b/docs/rules/core/0233-http-method.md
@@ -0,0 +1,64 @@
+---
+rule:
+  aip: 233
+  name: [core, '0233', http-method]
+  summary: Batch Create methods must use the POST HTTP verb.
+---
+
+# Batch Create methods: POST HTTP verb
+
+This rule enforces that all `BatchCreate` RPCs use the `POST` HTTP verb, as
+mandated in [AIP-233][].
+
+## Details
+
+This rule looks at any RPCs with the name beginning with `BatchCreate`, and
+complains if the HTTP verb is anything other than `POST`.
+
+## Examples
+
+**Incorrect** code for this rule:
+
+```proto
+// Incorrect.
+rpc BatchCreateBooks(BatchCreateBooksRequest) returns (BatchCreateBooksResponse) {
+  option (google.api.http) = {
+    put: "/v1/{parent=publishers/*}/books:batchCreate"  // Should be `post:`.
+    body: "*"
+  };
+}
+```
+
+**Correct** code for this rule:
+
+```proto
+// Correct.
+rpc BatchCreateBooks(BatchCreateBooksRequest) returns (BatchCreateBooksResponse) {
+  option (google.api.http) = {
+    post: "/v1/{parent=publishers/*}/books:batchCreate"
+    body: "*"
+  };
+}
+```
+
+## 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::0233::http-method=disabled
+//     aip.dev/not-precedent: We need to do this because reasons. --)
+rpc BatchCreateBooks(BatchCreateBooksRequest) returns (BatchCreateBooksResponse) {
+  option (google.api.http) = {
+    get: "/v1/{parent=publishers/*}/books:batchCreate" // Should be `post:`.
+    body: "*"
+  };
+}
+```
+
+If you need to violate this rule for an entire file, place the comment at the
+top of the file.
+
+[aip-233]: https://aip.dev/233
+[aip.dev/not-precedent]: https://aip.dev/not-precedent
diff --git a/docs/rules/core/0233-http-uri-suffix.md b/docs/rules/core/0233-http-uri-suffix.md
new file mode 100644
index 0000000..eb57451
--- /dev/null
+++ b/docs/rules/core/0233-http-uri-suffix.md
@@ -0,0 +1,64 @@
+---
+rule:
+  aip: 233
+  name: [core, '0233', http-uri-suffix]
+  summary: Batch Create methods must have the correct URI suffix
+---
+
+# Batch Create methods: URI suffix
+
+This rule enforces that ` BatchCreate` methods include the `:batchCreate` suffix
+in the REST URI, as mandated in [AIP-233][].
+
+## Details
+
+This rule looks at any method whose name starts with `BatchCreate`, and
+complains if the HTTP URI does not end with `:batchCreate`.
+
+## Examples
+
+**Incorrect** code for this rule:
+
+```proto
+// Incorrect.
+rpc BatchCreateBooks(BatchCreateBooksRequest) returns (BatchCreateBooksResponse) {
+  option (google.api.http) = {
+    post: "/v1/{parent=publishers/*}/books:batch" // Should end with `:batchCreate`.
+    body: "*"
+  };
+}
+```
+
+**Correct** code for this rule:
+
+```proto
+// Correct.
+rpc BatchCreateBooks(BatchCreateBooksRequest) returns (BatchCreateBooksResponse) {
+  option (google.api.http) = {
+    post: "/v1/{parent=publishers/*}/books:batchCreate"
+    body: "*"
+  };
+}
+```
+
+## 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::0233::http-uri-suffix=disabled
+//     aip.dev/not-precedent: We need to do this because reasons. --)
+rpc BatchCreateBooks(BatchCreateBooksRequest) returns (BatchCreateBooksResponse) {
+  option (google.api.http) = {
+    post: "/v1/{parent=publishers/*}/books:batch" // Should end with `:batchCreate`.
+    body: "*"
+  };
+}
+```
+
+If you need to violate this rule for an entire file, place the comment at the
+top of the file.
+
+[aip-233]: https://aip.dev/233
+[aip.dev/not-precedent]: https://aip.dev/not-precedent
diff --git a/docs/rules/core/0233-plural-method-name.md b/docs/rules/core/0233-plural-method-name.md
new file mode 100644
index 0000000..e1fdd75
--- /dev/null
+++ b/docs/rules/core/0233-plural-method-name.md
@@ -0,0 +1,65 @@
+---
+rule:
+  aip: 233
+  name: [core, '0233', plural-method-name]
+  summary: Batch Create methods must have plural names.
+---
+
+# Batch Create methods: Plural method name
+
+This rule enforces that all `BatchCreate` RPCs have a plural resource in the
+method name, as mandated in [AIP-233][].
+
+## Details
+
+This rule looks at any method whose name begins with `BatchCreate`, 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 `BatchCreateBooks`
+rpc BatchCreateBook(BatchCreateBookRequest) returns (BatchCreateBookResponse) {
+  option (google.api.http) = {
+    post: "/v1/{parent=publishers/*}/books:batchCreate"
+    body: "*"
+  };
+}
+```
+
+**Correct** code for this rule:
+
+```proto
+// Correct.
+rpc BatchCreateBooks(BatchCreateBooksRequest) returns (BatchCreateBooksResponse) {
+  option (google.api.http) = {
+    post: "/v1/{parent=publishers/*}/books:batchCreate"
+    body: "*"
+  };
+}
+```
+
+## 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::0233::plural-method-name=disabled
+//     aip.dev/not-precedent: We need to do this because reasons. --)
+rpc BatchCreateBook(BatchCreateBookRequest) returns (BatchCreateBookResponse) {
+  option (google.api.http) = {
+    post: "/v1/{parent=publishers/*}/books:batchCreate"
+    body: "*"
+  };
+}
+```
+
+If you need to violate this rule for an entire file, place the comment at the
+top of the file.
+
+[aip-233]: https://aip.dev/233
+[aip.dev/not-precedent]: https://aip.dev/not-precedent
diff --git a/docs/rules/core/0233-request-message-name.md b/docs/rules/core/0233-request-message-name.md
new file mode 100644
index 0000000..8dd3772
--- /dev/null
+++ b/docs/rules/core/0233-request-message-name.md
@@ -0,0 +1,66 @@
+---
+rule:
+  aip: 233
+  name: [core, '0233', request-message-name]
+  summary: Batch Create methods must have standardized request message names.
+---
+
+# Batch Create methods: Request message
+
+This rule enforces that all `BatchCreate` RPCs have a request message name of
+`BatchCreate*Request`, as mandated in [AIP-233][].
+
+## Details
+
+This rule looks at any RPCs whose name starts with `BatchCreate`, 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 "BatchCreateBooksRequest"
+rpc BatchCreateBooks(CreateBookRequest) returns (BatchCreateBooksResponse) {
+  option (google.api.http) = {
+    post: "/v1/{parent=publishers/*}/books:batchCreate"
+    body: "*"
+  };
+}
+```
+
+**Correct** code for this rule:
+
+```proto
+// Correct.
+rpc BatchCreateBooks(BatchCreateBooksRequest) returns (BatchCreateBooksResponse) {
+  option (google.api.http) = {
+    post: "/v1/{parent=publishers/*}/books:batchCreate"
+    body: "*"
+  };
+}
+```
+
+## 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::0233::request-message-name=disabled
+//     aip.dev/not-precedent: We need to do this because reasons. --)
+rpc BatchCreateBooks(CreateBookRequest) returns (BatchCreateBooksResponse) {
+  option (google.api.http) = {
+    post: "/v1/{parent=publishers/*}/books:batchCreate"
+    body: "*"
+  };
+}
+```
+
+If you need to violate this rule for an entire file, place the comment at the
+top of the file.
+
+[aip-233]: https://aip.dev/233
+[aip.dev/not-precedent]: https://aip.dev/not-precedent
diff --git a/docs/rules/core/0233-request-parent-field.md b/docs/rules/core/0233-request-parent-field.md
new file mode 100644
index 0000000..afa4c26
--- /dev/null
+++ b/docs/rules/core/0233-request-parent-field.md
@@ -0,0 +1,68 @@
+---
+rule:
+  aip: 233
+  name: [core, '0233', request-parent-field]
+  summary: Batch Create RPCs must have a `parent` field in the request.
+---
+
+# Batch Create methods: Parent field
+
+This rule enforces that all `BatchCreate` methods have a `string parent` field
+in the request message, as mandated in [AIP-233][].
+
+## Details
+
+This rule looks at any message matching `BatchCreate*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 BatchCreateBooksRequest {
+  string publisher = 1;  // Field name should be `parent`.
+  repeated CreateBookRequest requests = 2;
+}
+```
+
+```proto
+// Incorrect.
+message BatchCreateBookRequest {
+  bytes parent = 1;  // Field type should be `string`.
+  repeated CreateBookRequest requests = 2;
+}
+```
+
+**Correct** code for this rule:
+
+```proto
+// Correct.
+message BatchCreateBookRequest {
+  string parent = 1;
+  repeated CreateBookRequest requests = 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::0233::request-parent-field=disabled
+//     aip.dev/not-precedent: We need to do this because reasons. --)
+message BatchCreateBooksRequest {
+  string publisher = 1;  // Field name should be `parent`.
+  repeated CreateBookRequest requests = 2;
+}
+```
+
+If you need to violate this rule for an entire file, place the comment at the
+top of the file.
+
+[aip-233]: https://aip.dev/233
+[aip.dev/not-precedent]: https://aip.dev/not-precedent
diff --git a/docs/rules/core/0233-request-requests-field.md b/docs/rules/core/0233-request-requests-field.md
new file mode 100644
index 0000000..37e3d1a
--- /dev/null
+++ b/docs/rules/core/0233-request-requests-field.md
@@ -0,0 +1,70 @@
+---
+rule:
+  aip: 233
+  name: [core, '0233', request-requests-field]
+  summary: Batch Create RPCs should have a `requests` field in the request.
+---
+
+# Batch Create methods: Requests field
+
+This rule enforces that all `BatchCreate` methods have a repeated `requests`
+field, the type of which is the standard create request(like `Create*Request`),
+in the request message, as mandated in [AIP-233][].
+
+## Details
+
+This rule looks at any message matching `BatchCreate*Request` and complains if
+the `requests` field is missing, if it has any type other than `Create*Request`,
+or if it is not `repeated`.
+
+
+## Examples
+
+**Incorrect** code for this rule:
+
+```proto
+// Incorrect.
+message BatchCreateBooksRequest {
+  string parent = 1;
+  repeated CreateBookRequest req = 2;  // Field name should be `requests`.
+}
+```
+
+```proto
+// Incorrect.
+message BatchCreateBooksRequest {
+  string parent = 1;
+  CreateBookRequest requests = 2;  // Field should be repeated.
+}
+```
+
+**Correct** code for this rule:
+
+```proto
+// Correct.
+message BatchCreateBookRequest {
+  string parent = 1;
+  repeated CreateBookRequest requests = 2;
+}
+```
+
+## Disabling
+
+If you need to violate this rule, use a leading comment above the message (if
+the `requests` 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::0233::request-requests-field=disabled
+//     aip.dev/not-precedent: We need to do this because reasons. --)
+message BatchCreateBooksRequest {
+  string parent = 1;
+  repeated string books = 2; // should be "repeated CreateBookRequest requests"
+}
+```
+
+If you need to violate this rule for an entire file, place the comment at the
+top of the file.
+
+[aip-233]: https://aip.dev/233
+[aip.dev/not-precedent]: https://aip.dev/not-precedent
diff --git a/docs/rules/core/0233-response-message-name.md b/docs/rules/core/0233-response-message-name.md
new file mode 100644
index 0000000..32de90d
--- /dev/null
+++ b/docs/rules/core/0233-response-message-name.md
@@ -0,0 +1,72 @@
+---
+rule:
+  aip: 233
+  name: [core, '0233', response-message-name]
+  summary: Batch Create methods must have standardized response message names.
+---
+
+# Batch Create methods: Response message
+
+This rule enforces that all `BatchCreate` RPCs have a response message name of
+`BatchCreate*Response`, as mandated in [AIP-233][].
+
+## Details
+
+This rule looks at any RPCs whose name starts with `BatchCreate`, and
+complains if the name of the corresponding returned message does not match the
+name of the RPC with the suffix `Response` appended.
+
+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 the name of the RPC with the
+suffix `Response` appended, as mandated in [AIP-151][].
+
+## Examples
+
+**Incorrect** code for this rule:
+
+```proto
+// Incorrect.
+// Should be `BatchCreateBooksResponse`.
+rpc BatchCreateBooks(BatchCreateBooksRequest) returns (BooksResponse) {
+  option (google.api.http) = {
+    post: "/v1/{parent=publishers/*}/books:batchCreate"
+    body: "*"
+  };
+}
+```
+
+**Correct** code for this rule:
+
+```proto
+// Correct.
+rpc BatchCreateBooks(BatchCreateBooksRequest) returns (BatchCreateBooksResponse) {
+  option (google.api.http) = {
+    post: "/v1/{parent=publishers/*}/books:batchCreate"
+    body: "*"
+  };
+}
+```
+
+## 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::0233::response-message-name=disabled
+//     aip.dev/not-precedent: We need to do this because reasons. --)
+rpc BatchCreateBooks(BatchCreateBooksRequest) returns (BooksResponse) {
+  option (google.api.http) = {
+    post: "/v1/{parent=publishers/*}/books:batchCreate"
+    body: "*"
+  };
+}
+```
+
+If you need to violate this rule for an entire file, place the comment at the
+top of the file.
+
+[aip-233]: https://aip.dev/233
+[aip-151]: https://aip.dev/151
+[aip.dev/not-precedent]: https://aip.dev/not-precedent
diff --git a/docs/rules/core/0233-response-resource-field.md b/docs/rules/core/0233-response-resource-field.md
new file mode 100644
index 0000000..1bd34cd
--- /dev/null
+++ b/docs/rules/core/0233-response-resource-field.md
@@ -0,0 +1,64 @@
+---
+rule:
+  aip: 233
+  name: [core, '0233', response-resource-field]
+  summary:
+    Batch Create RPCs must have a repeated field for the resource in the response.
+---
+
+# Batch Create methods: Resource field
+
+This rule enforces that all `BatchCreate` methods have a repeated field in the
+response message for the resource itself, as mandated in [AIP-233][].
+
+## Details
+
+This rule looks at any message matching `BatchCreate*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 BatchCreateBooksResponse {
+  // `repeated Book books` is missing.
+}
+```
+
+```proto
+// Incorrect.
+message BatchCreateBooksResponse {
+  Book books = 1;  // Field should be repeated.
+}
+```
+
+**Correct** code for this rule:
+
+```proto
+// Correct.
+message BatchCreateBooksResponse {
+  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::0233::response-resource-field=disabled
+//     aip.dev/not-precedent: We need to do this because reasons. --)
+message BatchCreateBooksResponse {
+  // `repeated Book books` is missing.
+}
+```
+
+If you need to violate this rule for an entire file, place the comment at the
+top of the file.
+
+[aip-233]: https://aip.dev/233
+[aip.dev/not-precedent]: https://aip.dev/not-precedent
diff --git a/rules/aip0231/request_names_field.go b/rules/aip0231/request_names_field.go
index 30eb4c8..d071131 100644
--- a/rules/aip0231/request_names_field.go
+++ b/rules/aip0231/request_names_field.go
@@ -53,7 +53,7 @@ var namesField = &lint.MessageRule{
 		// Rule check: Ensure that the names field is repeated.
 		if names != nil && !names.IsRepeated() {
 			problems = append(problems, lint.Problem{
-				Message:    `The "names"" field should be repeated`,
+				Message:    `The "names" field should be repeated`,
 				Suggestion: "repeated string",
 				Descriptor: names,
 				Location:   locations.FieldType(names),
diff --git a/rules/aip0233/aip0233.go b/rules/aip0233/aip0233.go
new file mode 100644
index 0000000..7c4306e
--- /dev/null
+++ b/rules/aip0233/aip0233.go
@@ -0,0 +1,61 @@
+// 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 aip0233 contains rules defined in https://aip.dev/233.
+package aip0233
+
+import (
+	"regexp"
+
+	"github.com/googleapis/api-linter/lint"
+	"github.com/jhump/protoreflect/desc"
+)
+
+// AddRules accepts a register function and registers each of
+// this AIP's rules to it.
+func AddRules(r lint.RuleRegistry) error {
+	return r.Register(
+		233,
+		pluralMethodName,
+		requestMessageName,
+		responseMessageName,
+		httpBody,
+		httpUriSuffix,
+		httpVerb,
+		requestParentField,
+		requestRequestsField,
+		responseResourceField,
+	)
+}
+
+var batchCreateMethodRegexp = regexp.MustCompile("^BatchCreate(?:[A-Za-z0-9]|$)")
+var batchCreateReqMessageRegexp = regexp.MustCompile("^BatchCreate[A-Za-z0-9]*Request$")
+var batchCreateResMessageRegexp = regexp.MustCompile("^BatchCreate[A-Za-z0-9]*Response$")
+var batchCreateURINameRegexp = regexp.MustCompile("\\{[a-zA-Z=/*]+\\}\\/[A-Za-z0-9-=/*]+:batchCreate$")
+var createReqMessageRegexp = regexp.MustCompile("^Create[A-Za-z0-9]*Request$")
+
+// Returns true if this is a AIP-233 Batch Create method, false otherwise.
+func isBatchCreateMethod(m *desc.MethodDescriptor) bool {
+	return batchCreateMethodRegexp.MatchString(m.GetName())
+}
+
+// Returns true if this is an AIP-233 Batch Create request message, false otherwise.
+func isBatchCreateRequestMessage(m *desc.MessageDescriptor) bool {
+	return batchCreateReqMessageRegexp.MatchString(m.GetName())
+}
+
+// Returns true if this is an AIP-233 Batch Create response message, false otherwise.
+func isBatchCreateResponseMessage(m *desc.MessageDescriptor) bool {
+	return batchCreateResMessageRegexp.MatchString(m.GetName())
+}
diff --git a/rules/aip0233/http_body.go b/rules/aip0233/http_body.go
new file mode 100644
index 0000000..69e1f5b
--- /dev/null
+++ b/rules/aip0233/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 aip0233
+
+import (
+	"github.com/googleapis/api-linter/lint"
+	"github.com/googleapis/api-linter/rules/internal/utils"
+	"github.com/jhump/protoreflect/desc"
+)
+
+// Batch create methods should use "*" as the HTTP body.
+var httpBody = &lint.MethodRule{
+	Name:   lint.NewRuleName(233, "http-body"),
+	OnlyIf: isBatchCreateMethod,
+	LintMethod: func(m *desc.MethodDescriptor) []lint.Problem {
+		// Establish that the RPC has correct HTTP body.
+		for _, httpRule := range utils.GetHTTPRules(m) {
+			if httpRule.Body != "*" {
+				return []lint.Problem{{
+					Message:    `Batch Create methods should use "*" as the HTTP body.`,
+					Descriptor: m,
+				}}
+			}
+		}
+
+		return nil
+	},
+}
diff --git a/rules/aip0233/http_method.go b/rules/aip0233/http_method.go
new file mode 100644
index 0000000..cd80a98
--- /dev/null
+++ b/rules/aip0233/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 aip0233
+
+import (
+	"github.com/googleapis/api-linter/lint"
+	"github.com/googleapis/api-linter/rules/internal/utils"
+	"github.com/jhump/protoreflect/desc"
+)
+
+// Batch Create methods should use the HTTP POST verb.
+var httpVerb = &lint.MethodRule{
+	Name:   lint.NewRuleName(233, "http-method"),
+	OnlyIf: isBatchCreateMethod,
+	LintMethod: func(m *desc.MethodDescriptor) []lint.Problem {
+		// Rule check: Establish that the RPC uses HTTP POST.
+		for _, httpRule := range utils.GetHTTPRules(m) {
+			if httpRule.Method != "POST" {
+				return []lint.Problem{{
+					Message:    "Batch Create methods must use the HTTP POST verb.",
+					Descriptor: m,
+				}}
+			}
+		}
+
+		return nil
+	},
+}
diff --git a/rules/aip0233/http_uri_suffix.go b/rules/aip0233/http_uri_suffix.go
new file mode 100644
index 0000000..39e1096
--- /dev/null
+++ b/rules/aip0233/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 aip0233
+
+import (
+	"github.com/googleapis/api-linter/lint"
+	"github.com/googleapis/api-linter/rules/internal/utils"
+	"github.com/jhump/protoreflect/desc"
+)
+
+// Batch Create methods should have a proper HTTP pattern.
+var httpUriSuffix = &lint.MethodRule{
+	Name:   lint.NewRuleName(233, "http-uri-suffix"),
+	OnlyIf: isBatchCreateMethod,
+	LintMethod: func(m *desc.MethodDescriptor) []lint.Problem {
+		// Establish that the RPC has no HTTP body.
+		for _, httpRule := range utils.GetHTTPRules(m) {
+			if !batchCreateURINameRegexp.MatchString(httpRule.URI) {
+				return []lint.Problem{{
+					Message:    `Batch Create methods URI should be end with ":batchCreate".`,
+					Descriptor: m,
+				}}
+			}
+		}
+
+		return nil
+	},
+}
diff --git a/rules/aip0233/plural_method_name.go b/rules/aip0233/plural_method_name.go
new file mode 100644
index 0000000..2aa97e5
--- /dev/null
+++ b/rules/aip0233/plural_method_name.go
@@ -0,0 +1,52 @@
+// 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 aip0233
+
+import (
+	"fmt"
+	"strings"
+
+	"github.com/gertd/go-pluralize"
+	"github.com/googleapis/api-linter/lint"
+	"github.com/googleapis/api-linter/locations"
+	"github.com/jhump/protoreflect/desc"
+)
+
+var pluralMethodName = &lint.MethodRule{
+	Name:   lint.NewRuleName(233, "plural-method-name"),
+	OnlyIf: isBatchCreateMethod,
+	LintMethod: func(m *desc.MethodDescriptor) []lint.Problem {
+		// Note: Retrieve the resource name from the method name. For example,
+		// "BatchCreateFoos" -> "Foos"
+		pluralMethodResourceName := strings.TrimPrefix(m.GetName(), "BatchCreate")
+
+		pluralize := pluralize.NewClient()
+
+		// Rule check: Establish that for methods such as `BatchCreateFoos`
+		if !pluralize.IsPlural(pluralMethodResourceName) {
+			return []lint.Problem{{
+				Message: fmt.Sprintf(
+					`The resource part in method name %q shouldn't be %q, but should be its plural form %q`,
+					m.GetName(), pluralMethodResourceName, pluralize.Plural(pluralMethodResourceName),
+				),
+				Descriptor: m,
+				Location:   locations.DescriptorName(m),
+				Suggestion: "BatchCreate" + pluralize.Plural(pluralMethodResourceName),
+			}}
+		}
+
+		return nil
+	},
+}
diff --git a/rules/aip0233/request_message_name.go b/rules/aip0233/request_message_name.go
new file mode 100644
index 0000000..8133d3c
--- /dev/null
+++ b/rules/aip0233/request_message_name.go
@@ -0,0 +1,46 @@
+// 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 aip0233
+
+import (
+	"fmt"
+
+	"github.com/googleapis/api-linter/lint"
+	"github.com/googleapis/api-linter/locations"
+	"github.com/jhump/protoreflect/desc"
+)
+
+// Batch Create method should have a properly named Request message.
+var requestMessageName = &lint.MethodRule{
+	Name:   lint.NewRuleName(233, "request-message-name"),
+	OnlyIf: isBatchCreateMethod,
+	LintMethod: func(m *desc.MethodDescriptor) []lint.Problem {
+		// Rule check: Establish that for methods such as `BatchCreateFoos`, the request
+		// message is named `BatchCreateFoosRequest`.
+		if got, want := m.GetInputType().GetName(), m.GetName()+"Request"; got != want {
+			return []lint.Problem{{
+				Message: fmt.Sprintf(
+					"Batch Create RPCs should have a properly named request message %q, but not %q",
+					want, got,
+				),
+				Descriptor: m,
+				Location:   locations.MethodRequestType(m),
+				Suggestion: want,
+			}}
+		}
+
+		return nil
+	},
+}
diff --git a/rules/aip0233/request_parent_field.go b/rules/aip0233/request_parent_field.go
new file mode 100644
index 0000000..d1b33b3
--- /dev/null
+++ b/rules/aip0233/request_parent_field.go
@@ -0,0 +1,52 @@
+// 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 aip0233
+
+import (
+	"fmt"
+
+	"github.com/googleapis/api-linter/lint"
+	"github.com/googleapis/api-linter/locations"
+	"github.com/jhump/protoreflect/desc"
+	"github.com/jhump/protoreflect/desc/builder"
+)
+
+// The Batch Create request message should have parent field.
+var requestParentField = &lint.MessageRule{
+	Name:   lint.NewRuleName(233, "request-parent-field"),
+	OnlyIf: isBatchCreateRequestMessage,
+	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,
+				Location:   locations.FieldType(parentField),
+				Suggestion: "string",
+			}}
+		}
+
+		return nil
+	},
+}
diff --git a/rules/aip0233/request_requests_field.go b/rules/aip0233/request_requests_field.go
new file mode 100644
index 0000000..43cebd2
--- /dev/null
+++ b/rules/aip0233/request_requests_field.go
@@ -0,0 +1,67 @@
+// 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 aip0233
+
+import (
+	"fmt"
+	"strings"
+
+	"github.com/gertd/go-pluralize"
+	"github.com/googleapis/api-linter/lint"
+	"github.com/googleapis/api-linter/locations"
+	"github.com/jhump/protoreflect/desc"
+)
+
+// The Batch Create standard method should have repeated standard create request
+// message field.
+var requestRequestsField = &lint.MessageRule{
+	Name:   lint.NewRuleName(233, "request-requests-field"),
+	OnlyIf: isBatchCreateRequestMessage,
+	LintMessage: func(m *desc.MessageDescriptor) (problems []lint.Problem) {
+		// Rule check: Establish that a "requests" field is present.
+		requests := m.FindFieldByName("requests")
+
+		// Rule check: Ensure that the "requests" field is existed.
+		if requests == nil {
+			return []lint.Problem{{
+				Message:    fmt.Sprintf(`Message %q has no "requests" field`, m.GetName()),
+				Descriptor: m,
+			}}
+		}
+
+		// Rule check: Ensure that the standard create request message field "requests" is repeated.
+		if !requests.IsRepeated() {
+			problems = append(problems, lint.Problem{
+				Message:    `The "requests" field should be repeated`,
+				Descriptor: requests,
+			})
+		}
+
+		// Rule check: Ensure that the standard create request message field is the
+		// correct type. Note: Retrieve the resource name from the the batch create
+		// request, for example: "BatchCreateBooksRequest" -> "Books"
+		rightTypeName := fmt.Sprintf("Create%sRequest",
+			pluralize.NewClient().Singular(strings.TrimPrefix(strings.TrimSuffix(m.GetName(), "Request"), "BatchCreate")))
+		if requests.GetMessageType() == nil || requests.GetMessageType().GetName() != rightTypeName {
+			problems = append(problems, lint.Problem{
+				Message:    fmt.Sprintf(`The "requests" field on Batch Create Request should be a %q type`, rightTypeName),
+				Descriptor: requests,
+				Location:   locations.FieldType(requests),
+				Suggestion: rightTypeName,
+			})
+		}
+		return
+	},
+}
diff --git a/rules/aip0233/response_message_name.go b/rules/aip0233/response_message_name.go
new file mode 100644
index 0000000..64c7916
--- /dev/null
+++ b/rules/aip0233/response_message_name.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 aip0233
+
+import (
+	"fmt"
+
+	"github.com/googleapis/api-linter/lint"
+	"github.com/googleapis/api-linter/locations"
+	"github.com/googleapis/api-linter/rules/internal/utils"
+	"github.com/jhump/protoreflect/desc"
+)
+
+// Batch Create method should have a properly named Response message.
+var responseMessageName = &lint.MethodRule{
+	Name:   lint.NewRuleName(233, "response-message-name"),
+	OnlyIf: isBatchCreateMethod,
+	LintMethod: func(m *desc.MethodDescriptor) []lint.Problem {
+		// Proper response name should be the concatenation of the method name and
+		// "Response"
+		want := m.GetName() + "Response"
+
+		// If this is an LRO, then use the annotated response type instead of
+		// the actual RPC return type.
+		got := m.GetOutputType().GetName()
+		if m.GetOutputType().GetFullyQualifiedName() == "google.longrunning.Operation" {
+			got = utils.GetOperationInfo(m).GetResponseType()
+		}
+
+		// Rule check: Establish that for methods such as `BatchCreateFoos`, the
+		// response message should be named `BatchCreateFoosResponse`
+		//
+		// 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 got != want && got != "" {
+			return []lint.Problem{{
+				Message: fmt.Sprintf(
+					"Batch Create RPCs should have a properly named response message %q, but not %q",
+					want, got,
+				),
+				Descriptor: m,
+				Location:   locations.MethodResponseType(m),
+				Suggestion: want,
+			}}
+		}
+
+		return nil
+	},
+}
diff --git a/rules/aip0233/response_resource_field.go b/rules/aip0233/response_resource_field.go
new file mode 100644
index 0000000..7b7cb9b
--- /dev/null
+++ b/rules/aip0233/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 aip0233
+
+import (
+	"fmt"
+	"strings"
+
+	"github.com/gertd/go-pluralize"
+	"github.com/googleapis/api-linter/lint"
+	"github.com/jhump/protoreflect/desc"
+)
+
+// The Batch Create response message should have resource field.
+var responseResourceField = &lint.MessageRule{
+	Name:   lint.NewRuleName(233, "response-resource-field"),
+	OnlyIf: isBatchCreateResponseMessage,
+	LintMessage: func(m *desc.MessageDescriptor) []lint.Problem {
+		// the singular form the resource name, the first letter is Capitalized.
+		// Note: Retrieve the resource name from the the batch create response,
+		// for example: "BatchCreateBooksResponse" -> "Books"
+		resourceMsgName := pluralize.NewClient().Singular(strings.TrimPrefix(strings.TrimSuffix(m.GetName(), "Response"), "BatchCreate"))
+
+		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 Create 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/rules.go b/rules/rules.go
index dc558fd..0199018 100644
--- a/rules/rules.go
+++ b/rules/rules.go
@@ -69,6 +69,7 @@ import (
 	"github.com/googleapis/api-linter/rules/aip0192"
 	"github.com/googleapis/api-linter/rules/aip0203"
 	"github.com/googleapis/api-linter/rules/aip0231"
+	"github.com/googleapis/api-linter/rules/aip0233"
 )
 
 type addRulesFuncType func(lint.RuleRegistry) error
@@ -92,6 +93,7 @@ var aipAddRulesFuncs = []addRulesFuncType{
 	aip0192.AddRules,
 	aip0203.AddRules,
 	aip0231.AddRules,
+	aip0233.AddRules,
 }
 
 // Add all rules to the given registry.


Test patch:
diff --git a/rules/aip0233/aip0233_test.go b/rules/aip0233/aip0233_test.go
new file mode 100644
index 0000000..29811b3
--- /dev/null
+++ b/rules/aip0233/aip0233_test.go
@@ -0,0 +1,27 @@
+// 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 aip0233
+
+import (
+	"testing"
+
+	"github.com/googleapis/api-linter/lint"
+)
+
+func TestAddRules(t *testing.T) {
+	if err := AddRules(lint.NewRuleRegistry()); err != nil {
+		t.Errorf("AddRules got an error: %v", err)
+	}
+}
diff --git a/rules/aip0233/http_body_test.go b/rules/aip0233/http_body_test.go
new file mode 100644
index 0000000..6cf2d63
--- /dev/null
+++ b/rules/aip0233/http_body_test.go
@@ -0,0 +1,61 @@
+// 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 aip0233
+
+import (
+	"testing"
+
+	"github.com/googleapis/api-linter/rules/internal/testutils"
+)
+
+func TestHttpBody(t *testing.T) {
+	tests := []struct {
+		testName   string
+		Body       string
+		MethodName string
+		problems   testutils.Problems
+	}{
+		{"Valid", "*", "BatchCreateBooks", nil},
+		{"Invalid", "", "BatchCreateBooks", testutils.Problems{{Message: "*"}}},
+		{"Irrelevant", "", "AcquireBook", nil},
+	}
+
+	for _, test := range tests {
+		t.Run(test.testName, func(t *testing.T) {
+			file := testutils.ParseProto3Tmpl(t, `
+				import "google/api/annotations.proto";
+
+				service BookService {
+					rpc {{.MethodName}}({{.MethodName}}Request) returns ({{.MethodName}}Response) {
+						option (google.api.http) = {
+							post: "/v1/{parent=publishers/*}/books:batchCreate"
+							body: "{{.Body}}"
+						};
+					}
+				}
+				
+				message {{.MethodName}}Request {}
+				
+				message {{.MethodName}}Response{}
+				`, test)
+
+			// Run the method, ensure we get what we expect.
+			problems := httpBody.Lint(file)
+			if diff := test.problems.SetDescriptor(file.GetServices()[0].GetMethods()[0]).Diff(problems); diff != "" {
+				t.Errorf(diff)
+			}
+		})
+	}
+}
diff --git a/rules/aip0233/http_method_test.go b/rules/aip0233/http_method_test.go
new file mode 100644
index 0000000..3a92c4f
--- /dev/null
+++ b/rules/aip0233/http_method_test.go
@@ -0,0 +1,61 @@
+// 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 aip0233
+
+import (
+	"testing"
+
+	"github.com/googleapis/api-linter/rules/internal/testutils"
+)
+
+func TestHttpVerb(t *testing.T) {
+	// Set up testing permutations.
+	tests := []struct {
+		testName   string
+		HttpVerb   string
+		MethodName string
+		problems   testutils.Problems
+	}{
+		{"Valid", "post", "BatchCreateBooks", nil},
+		{"Invalid", "get", "BatchCreateBooks", testutils.Problems{{Message: "HTTP POST verb"}}},
+		{"Irrelevant", "get", "AcquireBook", nil},
+	}
+
+	// Run each test.
+	for _, test := range tests {
+		t.Run(test.testName, func(t *testing.T) {
+			file := testutils.ParseProto3Tmpl(t, `
+				import "google/api/annotations.proto";
+
+				service BookService {
+					rpc {{.MethodName}}({{.MethodName}}Request) returns ({{.MethodName}}Response) {
+						option (google.api.http) = {
+							{{.HttpVerb}}: "/v1/{parent=publishers/*}/books:batchCreate"
+							body: "*"
+						};
+					}
+				}
+				message {{.MethodName}}Request{}
+				message {{.MethodName}}Response{}
+				`, test)
+
+			// Run the method, ensure we get what we expect.
+			problems := httpVerb.Lint(file)
+			if diff := test.problems.SetDescriptor(file.GetServices()[0].GetMethods()[0]).Diff(problems); diff != "" {
+				t.Errorf(diff)
+			}
+		})
+	}
+}
diff --git a/rules/aip0233/http_uri_suffix_test.go b/rules/aip0233/http_uri_suffix_test.go
new file mode 100644
index 0000000..8623625
--- /dev/null
+++ b/rules/aip0233/http_uri_suffix_test.go
@@ -0,0 +1,59 @@
+// 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 aip0233
+
+import (
+	"testing"
+
+	"github.com/googleapis/api-linter/rules/internal/testutils"
+)
+
+func TestHttpUriSuffix(t *testing.T) {
+	tests := []struct {
+		testName   string
+		HttpUri    string
+		MethodName string
+		problems   testutils.Problems
+	}{
+		{"Valid", "/v1/{parent=publishers/*}/books:batchCreate", "BatchCreateBooks", nil},
+		{"InvalidVarName", "/v1/{parent=publishers/*}/books", "BatchCreateBooks", testutils.Problems{{Message: ":batchCreate"}}},
+		{"Irrelevant", "/v1/{book=publishers/*/books/*}", "AcquireBook", nil},
+	}
+
+	for _, test := range tests {
+		t.Run(test.testName, func(t *testing.T) {
+			file := testutils.ParseProto3Tmpl(t, `
+				import "google/api/annotations.proto";
+				
+				service BookService {
+					rpc {{.MethodName}}({{.MethodName}}Request) returns ({{.MethodName}}Response) {
+						option (google.api.http) = {
+							post: "{{.HttpUri}}"
+							body: "*"
+						};
+					}
+				}
+				message {{.MethodName}}Request{}
+				message {{.MethodName}}Response{}
+				`, test)
+
+			// Run the method, ensure we get what we expect.
+			problems := httpUriSuffix.Lint(file)
+			if diff := test.problems.SetDescriptor(file.GetServices()[0].GetMethods()[0]).Diff(problems); diff != "" {
+				t.Errorf(diff)
+			}
+		})
+	}
+}
diff --git a/rules/aip0233/plural_method_name_test.go b/rules/aip0233/plural_method_name_test.go
new file mode 100644
index 0000000..be20cae
--- /dev/null
+++ b/rules/aip0233/plural_method_name_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 aip0233
+
+import (
+	"testing"
+
+	"github.com/googleapis/api-linter/rules/internal/testutils"
+)
+
+func TestMethodPluralResourceName(t *testing.T) {
+	// Set up the testing permutations.
+	tests := []struct {
+		testName   string
+		MethodName string
+		UriSuffix  string
+		problems   testutils.Problems
+	}{
+		{
+			testName:   "Valid-BatchCreateBooks",
+			MethodName: "BatchCreateBooks",
+			UriSuffix:  "books:batchCreate",
+			problems:   testutils.Problems{},
+		},
+		{
+			testName:   "Valid-BatchCreateMen",
+			MethodName: "BatchCreateMen",
+			UriSuffix:  "men:batchCreate",
+			problems:   testutils.Problems{},
+		},
+		{
+			testName:   "Invalid-SingularBus",
+			MethodName: "BatchCreateBus",
+			UriSuffix:  "bus:batchCreate",
+			problems: testutils.Problems{{
+				Suggestion: "BatchCreateBuses",
+			}},
+		},
+		{
+			testName:   "Invalid-SingularCorpPerson",
+			MethodName: "BatchCreateCorpPerson",
+			UriSuffix:  "corpPerson:batchCreate",
+			problems: testutils.Problems{{
+				Suggestion: "BatchCreateCorpPeople",
+			}},
+		},
+		{
+			testName:   "Invalid-Irrelevant",
+			MethodName: "AcquireBook",
+			UriSuffix:  "book",
+			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 BookService {
+					rpc {{.MethodName}}({{.MethodName}}Request) returns ({{.MethodName}}Response) {
+						option (google.api.http) = {
+							post: "/v1/{parent=publishers/*}/{{.UriSuffix}}"
+							body: "*"
+						};
+					}
+				}
+				
+				message {{.MethodName}}Request {}
+				
+				message {{.MethodName}}Response{}
+				`, test)
+
+			m := file.GetServices()[0].GetMethods()[0]
+
+			problems := pluralMethodName.Lint(file)
+			if diff := test.problems.SetDescriptor(m).Diff(problems); diff != "" {
+				t.Errorf(diff)
+			}
+		})
+	}
+}
diff --git a/rules/aip0233/request_message_name_test.go b/rules/aip0233/request_message_name_test.go
new file mode 100644
index 0000000..8c535ec
--- /dev/null
+++ b/rules/aip0233/request_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 aip0233
+
+import (
+	"testing"
+
+	"github.com/googleapis/api-linter/rules/internal/testutils"
+)
+
+func TestRequestMessageName(t *testing.T) {
+	// Set up the testing permutations.
+	tests := []struct {
+		testName   string
+		MethodName string
+		Request    string
+		problems   testutils.Problems
+	}{
+		{
+			testName:   "Valid-BatchCreateBooksRequest",
+			MethodName: "BatchCreateBooks",
+			Request:    "BatchCreateBooksRequest",
+			problems:   testutils.Problems{},
+		},
+		{
+			testName:   "Invalid-MissMatchingMethodName",
+			MethodName: "BatchCreateBooks",
+			Request:    "BatchCreateBookRequest",
+			problems: testutils.Problems{{
+				Suggestion: "BatchCreateBooksRequest",
+			}},
+		},
+		{
+			testName:   "Irrelevant",
+			MethodName: "AcquireBook",
+			Request:    "AcquireBookRequest",
+			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 BookService {
+					rpc {{.MethodName}}({{.Request}}) returns ({{.MethodName}}Response) {
+						option (google.api.http) = {
+							post: "/v1/{parent=publishers/*}/"
+							body: "*"
+						};
+					}
+				}
+				
+				message {{.Request}}{}
+				
+				message {{.MethodName}}Response{}
+				`, test)
+
+			m := file.GetServices()[0].GetMethods()[0]
+
+			problems := requestMessageName.Lint(file)
+			if diff := test.problems.SetDescriptor(m).Diff(problems); diff != "" {
+				t.Errorf(diff)
+			}
+		})
+	}
+}
diff --git a/rules/aip0233/request_parent_field_test.go b/rules/aip0233/request_parent_field_test.go
new file mode 100644
index 0000000..0d9c5e6
--- /dev/null
+++ b/rules/aip0233/request_parent_field_test.go
@@ -0,0 +1,90 @@
+// 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 aip0233
+
+import (
+	"testing"
+
+	"github.com/googleapis/api-linter/rules/internal/testutils"
+	"github.com/jhump/protoreflect/desc"
+)
+
+func TestRequestParentField(t *testing.T) {
+	// Set up the testing permutations.
+	tests := []struct {
+		testName    string
+		MessageName string
+		Field       string
+		problems    testutils.Problems
+		problemDesc func(m *desc.MessageDescriptor) desc.Descriptor
+	}{
+		{
+			"Valid",
+			"BatchCreateBooksRequest",
+			"string parent",
+			testutils.Problems{},
+			nil},
+		{
+			"MissingField",
+			"BatchCreateBooksRequest",
+			"string name",
+			testutils.Problems{{Message: "parent"}},
+			nil,
+		},
+		{
+			"InvalidType",
+			"BatchCreateBooksRequest",
+			"int32 parent",
+			testutils.Problems{{
+				Message:    "string",
+				Suggestion: "string",
+			}},
+			func(m *desc.MessageDescriptor) desc.Descriptor {
+				return m.FindFieldByName("parent")
+			},
+		},
+		{
+			"Irrelevant",
+			"EnumerateBooksRequest",
+			"string name",
+			testutils.Problems{},
+			nil,
+		},
+	}
+
+	// Run each test individually.
+	for _, test := range tests {
+		t.Run(test.testName, func(t *testing.T) {
+			file := testutils.ParseProto3Tmpl(t, `
+				message {{.MessageName}} {
+					{{.Field}} = 1;
+				}`, test)
+
+			m := file.GetMessageTypes()[0]
+
+			// Determine the descriptor that a failing test will attach to.
+			var problemDesc desc.Descriptor = m
+			if test.problemDesc != nil {
+				problemDesc = test.problemDesc(m)
+			}
+
+			// Run the lint rule, and establish that it returns the correct problems.
+			problems := requestParentField.Lint(file)
+			if diff := test.problems.SetDescriptor(problemDesc).Diff(problems); diff != "" {
+				t.Errorf(diff)
+			}
+		})
+	}
+}
diff --git a/rules/aip0233/request_requests_field_test.go b/rules/aip0233/request_requests_field_test.go
new file mode 100644
index 0000000..a7a58ea
--- /dev/null
+++ b/rules/aip0233/request_requests_field_test.go
@@ -0,0 +1,99 @@
+// 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 aip0233
+
+import (
+	"testing"
+
+	"github.com/googleapis/api-linter/rules/internal/testutils"
+	"github.com/jhump/protoreflect/desc"
+)
+
+func TestRequestRequestsField(t *testing.T) {
+	// Set up the testing permutations.
+	tests := []struct {
+		testName    string
+		Field       string
+		problems    testutils.Problems
+		problemDesc func(m *desc.MessageDescriptor) desc.Descriptor
+	}{
+		{
+			testName: "Valid",
+			Field:    "repeated CreateBookRequest requests = 1;",
+			problems: testutils.Problems{},
+		},
+		{
+			testName: "Invalid-MissingRequestsField",
+			Field:    "string parent = 1;",
+			problems: testutils.Problems{{Message: `no "requests" field`}},
+		},
+		{
+			testName: "Invalid-RequestsFieldIsNotRepeated",
+			Field:    "CreateBookRequest requests = 1;",
+			problems: testutils.Problems{{Message: "repeated"}},
+			problemDesc: func(m *desc.MessageDescriptor) desc.Descriptor {
+				return m.FindFieldByName("requests")
+			},
+		},
+		{
+			testName: "Invalid-RequestsFieldWrongType",
+			Field:    "repeated int32 requests = 1;",
+			problems: testutils.Problems{{
+				Suggestion: "CreateBookRequest",
+			}},
+			problemDesc: func(m *desc.MessageDescriptor) desc.Descriptor {
+				return m.FindFieldByName("requests")
+			},
+		},
+		{
+			testName: "Invalid-RequestsNotRepeatedWrongType",
+			Field:    "int32 requests = 1;",
+			problems: testutils.Problems{
+				{Message: "repeated"},
+				{
+					Suggestion: "CreateBookRequest",
+				}},
+			problemDesc: func(m *desc.MessageDescriptor) desc.Descriptor {
+				return m.FindFieldByName("requests")
+			},
+		},
+	}
+
+	// Run each test individually.
+	for _, test := range tests {
+		t.Run(test.testName, func(t *testing.T) {
+			file := testutils.ParseProto3Tmpl(t, `
+				message BatchCreateBooksRequest {
+					{{.Field}}
+				}
+				
+				message CreateBookRequest {}
+				`, test)
+
+			m := file.GetMessageTypes()[0]
+
+			// Determine the descriptor that a failing test will attach to.
+			var problemDesc desc.Descriptor = m
+			if test.problemDesc != nil {
+				problemDesc = test.problemDesc(m)
+			}
+
+			problems := requestRequestsField.Lint(file)
+			if diff := test.problems.SetDescriptor(problemDesc).Diff(problems); diff != "" {
+				t.Errorf(diff)
+			}
+		})
+	}
+}
diff --git a/rules/aip0233/response_message_name_test.go b/rules/aip0233/response_message_name_test.go
new file mode 100644
index 0000000..bf89f5a
--- /dev/null
+++ b/rules/aip0233/response_message_name_test.go
@@ -0,0 +1,134 @@
+// 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 aip0233
+
+import (
+	"testing"
+
+	"github.com/googleapis/api-linter/rules/internal/testutils"
+)
+
+func TestResponseMessageName(t *testing.T) {
+	// Set up the testing permutations.
+	tests := []struct {
+		testName   string
+		MethodName string
+		Response   string
+		problems   testutils.Problems
+	}{
+		{
+			testName:   "Valid-BatchCreateBooksResponse",
+			MethodName: "BatchCreateBooks",
+			Response:   "BatchCreateBooksResponse",
+			problems:   testutils.Problems{},
+		},
+		{
+			testName:   "Invalid-MissMatchingMethodName",
+			MethodName: "BatchCreateBooks",
+			Response:   "BatchCreateBookResponse",
+			problems: testutils.Problems{{
+				Suggestion: "BatchCreateBooksResponse",
+			}},
+		},
+		{
+			testName:   "Irrelevant",
+			MethodName: "CreateBook",
+			Response:   "Book",
+			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 BookService {
+					rpc {{.MethodName}}({{.MethodName}}Request) returns ({{.Response}}) {
+						option (google.api.http) = {
+							post: "/v1/{parent=publishers/*}/"
+							body: "*"
+						};
+					}
+				}
+				
+				message {{.MethodName}}Request {}
+				
+				message {{.Response}}{}
+				`, test)
+
+			m := file.GetServices()[0].GetMethods()[0]
+
+			problems := responseMessageName.Lint(file)
+			if diff := test.problems.SetDescriptor(m).Diff(problems); diff != "" {
+				t.Errorf(diff)
+			}
+		})
+	}
+}
+
+// Other cases for long running response will be handled by AIP-0151
+func TestLongRunningResponse(t *testing.T) {
+	// Set up the testing permutations.
+	tests := []struct {
+		testName     string
+		ResponseType string
+		problems     testutils.Problems
+	}{
+		{
+			testName:     "Valid-LongRunning",
+			ResponseType: "BatchCreateBooksResponse",
+			problems:     testutils.Problems{},
+		},
+		{
+			testName: "Valid-LongRunningEmptyResponseType",
+			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";
+				import "google/longrunning/operations.proto";
+				
+				service BookService {
+					rpc BatchCreateBooks(BatchCreateBooksRequest) returns (google.longrunning.Operation) {
+						option (google.api.http) = {
+							post: "/v1/{parent=publishers/*}/books:batchCreate"
+							body: "*"
+						};
+						option (google.longrunning.operation_info) = {
+							response_type: "{{.ResponseType}}"
+						};
+					}
+				}
+				
+				message BatchCreateBooksRequest {}
+				
+				message BatchCreateBooksResponse{}
+				`, test)
+
+			m := file.GetServices()[0].GetMethods()[0]
+
+			problems := responseMessageName.Lint(file)
+			if diff := test.problems.SetDescriptor(m).Diff(problems); diff != "" {
+				t.Errorf(diff)
+			}
+		})
+	}
+}
diff --git a/rules/aip0233/response_resource_field_test.go b/rules/aip0233/response_resource_field_test.go
new file mode 100644
index 0000000..d06ddc1
--- /dev/null
+++ b/rules/aip0233/response_resource_field_test.go
@@ -0,0 +1,76 @@
+// 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 aip0233
+
+import (
+	"testing"
+
+	"github.com/googleapis/api-linter/rules/internal/testutils"
+	"github.com/jhump/protoreflect/desc"
+)
+
+func TestResponseResourceField(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:      `repeated Book books = 1;`,
+			problems: testutils.Problems{},
+		},
+		{
+			testName: "FieldIsNotRepeated",
+			Src:      `Book book = 1;`,
+			problems: testutils.Problems{{Message: "The \"Book\" type field on Batch Create Response message should be repeated"}},
+		},
+		{
+			testName: "MissingField",
+			Src:      `string response = 1;`,
+			problems: testutils.Problems{{Message: "Message \"BatchCreateBooksResponse\" 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) {
+			file := testutils.ParseProto3Tmpl(t, `
+				message BatchCreateBooksResponse {
+					{{.Src}}
+				}
+				message Book {
+				}`, test)
+
+			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 := responseResourceField.Lint(file)
+			if diff := test.problems.SetDescriptor(problemDesc).Diff(problems); diff != "" {
+				t.Errorf(diff)
+			}
+		})
+	}
+}