Repository: googleapis/api-linter
Pull request #707 (2020-11-02 19:49:34)

feat: Add rules for AIP-152
Part of #698.

lukesneeringer: Just got back from vacation. Will read this soon.

Patch:
diff --git a/docs/rules/0152/http-body.md b/docs/rules/0152/http-body.md
new file mode 100644
index 0000000..0812633
--- /dev/null
+++ b/docs/rules/0152/http-body.md
@@ -0,0 +1,79 @@
+---
+rule:
+  aip: 152
+  name: [core, '0152', http-body]
+  summary: Run methods should use `*` as the HTTP body.
+permalink: /152/http-body
+redirect_from:
+  - /0152/http-body
+---
+
+# Run methods: HTTP body
+
+This rule enforces that all `Run` RPCs use `*` as the HTTP `body`, as
+mandated in [AIP-152][].
+
+## Details
+
+This rule looks at any RPC methods beginning with `Run`, and
+complains if the HTTP `body` field is anything other than `*`.
+
+## Examples
+
+**Incorrect** code for this rule:
+
+```proto
+// Incorrect.
+rpc RunWriteBookJob(RunWriteBookJobRequest) returns (google.longrunning.Operation) {
+  option (google.api.http) = {
+    post: "/v1/{name=publishers/*/writeBookJobs/*}:run"
+    body: ""  // The http body should be "*".
+  };
+  option (google.longrunning.operation_info) = {
+    response_type: "RunWriteBookJobResponse"
+    metadata_type: "RunWriteBookJobMetadata"
+  };
+}
+```
+
+**Correct** code for this rule:
+
+```proto
+// Correct.
+rpc RunWriteBookJob(RunWriteBookJobRequest) returns (google.longrunning.Operation) {
+  option (google.api.http) = {
+    post: "/v1/{name=publishers/*/writeBookJobs/*}:run"
+    body: "*"
+  };
+  option (google.longrunning.operation_info) = {
+    response_type: "RunWriteBookJobResponse"
+    metadata_type: "RunWriteBookJobMetadata"
+  };
+}
+```
+
+## 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::0152::http-body=disabled
+//     aip.dev/not-precedent: We need to do this because reasons. --)
+rpc RunWriteBookJob(RunWriteBookJobRequest) returns (google.longrunning.Operation) {
+  option (google.api.http) = {
+    post: "/v1/{name=publishers/*/writeBookJobs/*}:run"
+    body: ""
+  };
+  option (google.longrunning.operation_info) = {
+    response_type: "RunWriteBookJobResponse"
+    metadata_type: "RunWriteBookJobMetadata"
+  };
+}
+```
+
+If you need to violate this rule for an entire file, place the comment at the
+top of the file.
+
+[aip-152]: https://aip.dev/152
+[aip.dev/not-precedent]: https://aip.dev/not-precedent
diff --git a/docs/rules/0152/http-method.md b/docs/rules/0152/http-method.md
new file mode 100644
index 0000000..63f3a23
--- /dev/null
+++ b/docs/rules/0152/http-method.md
@@ -0,0 +1,79 @@
+---
+rule:
+  aip: 152
+  name: [core, '0152', http-method]
+  summary: Run methods must use the POST HTTP verb.
+permalink: /152/http-method
+redirect_from:
+  - /0152/http-method
+---
+
+# Run methods: POST HTTP verb
+
+This rule enforces that all `Run` use the `POST` HTTP verb, as
+mandated in [AIP-152][].
+
+## Details
+
+This rule looks at any RPCs with the name beginning with `Run`, and
+complains if the HTTP verb is anything other than `POST`.
+
+## Examples
+
+**Incorrect** code for this rule:
+
+```proto
+// Incorrect.
+rpc RunWriteBookJob(RunWriteBookJobRequest) returns (google.longrunning.Operation) {
+  option (google.api.http) = {
+    patch: "/v1/{name=publishers/*/writeBookJobs/*}:run" // Should be `post:`.
+    body: "*"
+  };
+  option (google.longrunning.operation_info) = {
+    response_type: "RunWriteBookJobResponse"
+    metadata_type: "RunWriteBookJobMetadata"
+  };
+}
+```
+
+**Correct** code for this rule:
+
+```proto
+// Correct.
+rpc RunWriteBookJob(RunWriteBookJobRequest) returns (google.longrunning.Operation) {
+  option (google.api.http) = {
+    post: "/v1/{name=publishers/*/writeBookJobs/*}:run"
+    body: "*"
+  };
+  option (google.longrunning.operation_info) = {
+    response_type: "RunWriteBookJobResponse"
+    metadata_type: "RunWriteBookJobMetadata"
+  };
+}
+```
+
+## 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::0152::http-method=disabled
+//     aip.dev/not-precedent: We need to do this because reasons. --)
+rpc RunWriteBookJob(RunWriteBookJobRequest) returns (google.longrunning.Operation) {
+  option (google.api.http) = {
+    patch: "/v1/{name=publishers/*/writeBookJobs/*}:run"
+    body: "*"
+  };
+  option (google.longrunning.operation_info) = {
+    response_type: "RunWriteBookJobResponse"
+    metadata_type: "RunWriteBookJobMetadata"
+  };
+}
+```
+
+If you need to violate this rule for an entire file, place the comment at the
+top of the file.
+
+[aip-152]: https://aip.dev/152
+[aip.dev/not-precedent]: https://aip.dev/not-precedent
diff --git a/docs/rules/0152/http-uri-suffix.md b/docs/rules/0152/http-uri-suffix.md
new file mode 100644
index 0000000..58c977e
--- /dev/null
+++ b/docs/rules/0152/http-uri-suffix.md
@@ -0,0 +1,79 @@
+---
+rule:
+  aip: 152
+  name: [core, '0152', http-uri-suffix]
+  summary: Run methods must have the correct URI suffix
+permalink: /152/http-uri-suffix
+redirect_from:
+  - /0152/http-uri-suffix
+---
+
+# Run methods: URI suffix
+
+This rule enforces that `Run` methods include the `:run` suffix
+in the REST URI, as mandated in [AIP-152][].
+
+## Details
+
+This rule looks at any method whose name starts with `Run`, and
+complains if the HTTP URI does not end with `:run`.
+
+## Examples
+
+**Incorrect** code for this rule:
+
+```proto
+// Incorrect.
+rpc RunWriteBookJob(RunWriteBookJobRequest) returns (google.longrunning.Operation) {
+  option (google.api.http) = {
+    post: "/v1/{name=publishers/*/writeBookJobs/*}:execute" // Should end with `:run`.
+    body: "*"
+  };
+  option (google.longrunning.operation_info) = {
+    response_type: "RunWriteBookJobResponse"
+    metadata_type: "RunWriteBookJobMetadata"
+  };
+}
+```
+
+**Correct** code for this rule:
+
+```proto
+// Correct.
+rpc RunWriteBookJob(RunWriteBookJobRequest) returns (google.longrunning.Operation) {
+  option (google.api.http) = {
+    post: "/v1/{name=publishers/*/writeBookJobs/*}:run"
+    body: "*"
+  };
+  option (google.longrunning.operation_info) = {
+    response_type: "RunWriteBookJobResponse"
+    metadata_type: "RunWriteBookJobMetadata"
+  };
+}
+```
+
+## 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::0152::http-uri-suffix=disabled
+//     aip.dev/not-precedent: We need to do this because reasons. --)
+rpc RunWriteBookJob(RunWriteBookJobRequest) returns (google.longrunning.Operation) {
+  option (google.api.http) = {
+    post: "/v1/{name=publishers/*/writeBookJobs/*}:execute"
+    body: "*"
+  };
+  option (google.longrunning.operation_info) = {
+    response_type: "RunWriteBookJobResponse"
+    metadata_type: "RunWriteBookJobMetadata"
+  };
+}
+```
+
+If you need to violate this rule for an entire file, place the comment at the
+top of the file.
+
+[aip-152]: https://aip.dev/152
+[aip.dev/not-precedent]: https://aip.dev/not-precedent
diff --git a/docs/rules/0152/index.md b/docs/rules/0152/index.md
new file mode 100644
index 0000000..00b3244
--- /dev/null
+++ b/docs/rules/0152/index.md
@@ -0,0 +1,11 @@
+---
+aip_listing: 152
+permalink: /152/
+redirect_from:
+  - /0152/
+prose_title: jobs
+---
+
+# Jobs
+
+{% include linter-aip-listing.md aip=152 %}
diff --git a/docs/rules/0152/request-message-name.md b/docs/rules/0152/request-message-name.md
new file mode 100644
index 0000000..c58ff34
--- /dev/null
+++ b/docs/rules/0152/request-message-name.md
@@ -0,0 +1,81 @@
+---
+rule:
+  aip: 152
+  name: [core, '0152', request-message-name]
+  summary: Run methods must have standardized request message names.
+permalink: /152/request-message-name
+redirect_from:
+  - /0152/request-message-name
+---
+
+# Run methods: Request message
+
+This rule enforces that all `Run` RPCs have a request message name
+of `Run*JobRequest`, as mandated in [AIP-152][].
+
+## Details
+
+This rule looks at any message beginning with `Run`, and complains
+if the name of the corresponding input message does not match the name of the
+RPC with the suffix `Request` appended.
+
+## Examples
+
+**Incorrect** code for this rule:
+
+```proto
+// Incorrect.
+// Should be `RunWriteBookJobRequest`.
+rpc RunWriteBookJob(WriteBookJob) returns (google.longrunning.Operation) {
+  option (google.api.http) = {
+    post: "/v1/{name=publishers/*/writeBookJobs/*}:run"
+    body: "*"
+  };
+  option (google.longrunning.operation_info) = {
+    response_type: "RunWriteBookJobResponse"
+    metadata_type: "RunWriteBookJobMetadata"
+  };
+}
+```
+
+**Correct** code for this rule:
+
+```proto
+// Correct.
+rpc RunWriteBookJob(RunWriteBookJobRequest) returns (google.longrunning.Operation) {
+  option (google.api.http) = {
+    post: "/v1/{name=publishers/*/writeBookJobs/*}:run"
+    body: "*"
+  };
+  option (google.longrunning.operation_info) = {
+    response_type: "RunWriteBookJobResponse"
+    metadata_type: "RunWriteBookJobMetadata"
+  };
+}
+```
+
+## 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::0152::request-message-name=disabled
+//     aip.dev/not-precedent: We need to do this because reasons. --)
+rpc RunWriteBookJob(WriteBookJob) returns (google.longrunning.Operation) {
+  option (google.api.http) = {
+    post: "/v1/{name=publishers/*/writeBookJobs/*}:run"
+    body: "*"
+  };
+  option (google.longrunning.operation_info) = {
+    response_type: "RunWriteBookJobResponse"
+    metadata_type: "RunWriteBookJobMetadata"
+  };
+}
+```
+
+If you need to violate this rule for an entire file, place the comment at the
+top of the file.
+
+[aip-152]: https://aip.dev/152
+[aip.dev/not-precedent]: https://aip.dev/not-precedent
diff --git a/docs/rules/0152/request-name-behavior.md b/docs/rules/0152/request-name-behavior.md
new file mode 100644
index 0000000..cdf171a
--- /dev/null
+++ b/docs/rules/0152/request-name-behavior.md
@@ -0,0 +1,71 @@
+---
+rule:
+  aip: 152
+  name: [core, '0152', request-name-behavior]
+  summary: |
+    Run requests should annotate the `name` field with `google.api.field_behavior`.
+permalink: /152/request-name-behavior
+redirect_from:
+  - /0152/request-name-behavior
+---
+
+# Run requests: Field behavior
+
+This rule enforces that all `Run` requests have
+`google.api.field_behavior` set to `REQUIRED` on their `string name` field, as
+mandated in [AIP-152][].
+
+## Details
+
+This rule looks at any message matching `Run*JobRequest` and complains if the
+`name` field does not have a `google.api.field_behavior` annotation with a
+value of `REQUIRED`.
+
+## Examples
+
+**Incorrect** code for this rule:
+
+```proto
+// Incorrect.
+message RunWriteBookJobRequest {
+  // The `google.api.field_behavior` annotation should also be included.
+  string name = 1 [(google.api.resource_reference) = {
+    type: "library.googleapis.com/WriteBookJob"
+  }];
+}
+```
+
+**Correct** code for this rule:
+
+```proto
+// Correct.
+message RunWriteBookJobRequest {
+  // The `google.api.field_behavior` annotation should also be included.
+  string name = 1 [
+    (google.api.field_behavior) = REQUIRED,
+    (google.api.resource_reference) = {
+      type: "library.googleapis.com/WriteBookJob"
+    }];
+}
+```
+
+## Disabling
+
+If you need to violate this rule, use a leading comment above the field.
+Remember to also include an [aip.dev/not-precedent][] comment explaining why.
+
+```proto
+message RunWriteBookJobRequest {
+  // (-- api-linter: core::0152::request-name-behavior=disabled
+  //     aip.dev/not-precedent: We need to do this because reasons. --)
+  string name = 1 [(google.api.resource_reference) = {
+    type: "library.googleapis.com/WriteBookJob"
+  }];
+}
+```
+
+If you need to violate this rule for an entire file, place the comment at the
+top of the file.
+
+[aip-152]: https://aip.dev/152
+[aip.dev/not-precedent]: https://aip.dev/not-precedent
diff --git a/docs/rules/0152/request-name-field.md b/docs/rules/0152/request-name-field.md
new file mode 100644
index 0000000..fe940a1
--- /dev/null
+++ b/docs/rules/0152/request-name-field.md
@@ -0,0 +1,69 @@
+---
+rule:
+  aip: 152
+  name: [core, '0152', request-name-field]
+  summary: Run RPCs must have a `name` field in the request.
+permalink: /152/request-name-field
+redirect_from:
+  - /0152/request-name-field
+---
+
+# Run requests: Name field
+
+This rule enforces that all `Run` methods have a `string name`
+field in the request message, as mandated in [AIP-152][].
+
+## Details
+
+This rule looks at any message matching `Run*JobRequest` and complains if
+either the `name` field is missing, or if it has any type other than `string`.
+
+## Examples
+
+**Incorrect** code for this rule:
+
+```proto
+// Incorrect.
+message RunWriteBookJobRequest {
+  string write_book_job = 1;  // Field name should be `name`.
+}
+```
+
+```proto
+// Incorrect.
+message RunWriteBookJobRequest {
+  bytes name = 1;  // Field type should be `string`.
+}
+```
+
+**Correct** code for this rule:
+
+```proto
+// Correct.
+message RunWriteBookJobRequest {
+  string name = 1 [
+    (google.api.field_behavior) = REQUIRED,
+    (google.api.resource_reference).type = "library.googleapis.com/WriteBookJob"
+  ];
+}
+```
+
+## Disabling
+
+If you need to violate this rule, use a leading comment above the message (if
+the `name` field is missing) or above the field (if it is the wrong type).
+Remember to also include an [aip.dev/not-precedent][] comment explaining why.
+
+```proto
+// (-- api-linter: core::0152::request-name-field=disabled
+//     aip.dev/not-precedent: We need to do this because reasons. --)
+message RunWriteBookJobRequest {
+  string write_book_job = 1;
+}
+```
+
+If you need to violate this rule for an entire file, place the comment at the
+top of the file.
+
+[aip-152]: https://aip.dev/152
+[aip.dev/not-precedent]: https://aip.dev/not-precedent
diff --git a/docs/rules/0152/request-name-reference.md b/docs/rules/0152/request-name-reference.md
new file mode 100644
index 0000000..8060b04
--- /dev/null
+++ b/docs/rules/0152/request-name-reference.md
@@ -0,0 +1,64 @@
+---
+rule:
+  aip: 152
+  name: [core, '0152', request-name-reference]
+  summary: |
+    Run requests should annotate the `name` field with `google.api.resource_reference`.
+permalink: /152/request-name-reference
+redirect_from:
+  - /0152/request-name-reference
+---
+
+# Run requests: Resource reference
+
+This rule enforces that all `RUn` methods have
+`google.api.resource_reference` on their `string name` field, as mandated in
+[AIP-152][].
+
+## Details
+
+This rule looks at the `name` field of any message matching `Run*JobRequest`
+and complains if it does not have a `google.api.resource_reference` annotation.
+
+## Examples
+
+**Incorrect** code for this rule:
+
+```proto
+// Incorrect.
+message RunWriteBookJobRequest {
+  // The `google.api.resource_reference` annotation should also be included.
+  string name = 1 [(google.api.field_behavior) = REQUIRED];
+}
+```
+
+**Correct** code for this rule:
+
+```proto
+// Correct.
+message RunWriteBookJobRequest {
+  string name = 1 [
+    (google.api.field_behavior) = REQUIRED,
+    (google.api.resource_reference).type = "library.googleapis.com/WriteBookJob"
+  ];
+}
+```
+
+## Disabling
+
+If you need to violate this rule, use a leading comment above the field.
+Remember to also include an [aip.dev/not-precedent][] comment explaining why.
+
+```proto
+message RunWriteBookJobRequest {
+  // (-- api-linter: core::0152::request-name-reference=disabled
+  //     aip.dev/not-precedent: We need to do this because reasons. --)
+  string name = 1 [(google.api.field_behavior) = REQUIRED];
+}
+```
+
+If you need to violate this rule for an entire file, place the comment at the
+top of the file.
+
+[aip-152]: https://aip.dev/152
+[aip.dev/not-precedent]: https://aip.dev/not-precedent
diff --git a/docs/rules/0152/response-message-name.md b/docs/rules/0152/response-message-name.md
new file mode 100644
index 0000000..11b159a
--- /dev/null
+++ b/docs/rules/0152/response-message-name.md
@@ -0,0 +1,90 @@
+---
+rule:
+  aip: 152
+  name: [core, '0152', response-message-name]
+  summary: Run methods must return a long-running operation.
+permalink: /152/response-message-name
+redirect_from:
+  - /0152/response-message-name
+---
+
+# Run methods: Response message
+
+This rule enforces that all `Run` RPCs return a long-running operation that
+resolves to a response with a corresponding name, as mandated in [AIP-152][].
+
+## Details
+
+This rule looks at any message beginning with `Run`, and complains if the
+response is not a long-running operation that resolves to a response matching
+the name of the method with a `Response` suffix.
+
+## Examples
+
+**Incorrect** code for this rule:
+
+```proto
+// Incorrect.
+// Should be `google.longrunning.Operation`.
+rpc RunWriteBookJob(RunWriteBookJobRequest) returns (RunWriteBookJobResponse) {
+  option (google.api.http) = {
+    post: "/v1/{name=publishers/*/writeBookJobs/*}:run"
+    body: "*"
+  };
+}
+```
+
+**Incorrect** code for this rule:
+
+```proto
+// Incorrect.
+rpc RunWriteBookJob(RunWriteBookJobRequest) returns (google.longrunning.Operation) {
+  option (google.api.http) = {
+    post: "/v1/{name=publishers/*/writeBookJobs/*}:run"
+    body: "*"
+  };
+  option (google.longrunning.operation_info) = {
+    // Should be "RunWriteBookJobResponse".
+    response_type: "WriteBookJob"
+    metadata_type: "RunWriteBookJobMetadata"
+  };
+}
+```
+
+**Correct** code for this rule:
+
+```proto
+// Correct.
+rpc RunWriteBookJob(RunWriteBookJobRequest) returns (google.longrunning.Operation) {
+  option (google.api.http) = {
+    post: "/v1/{name=publishers/*/writeBookJobs/*}:run"
+    body: "*"
+  };
+  option (google.longrunning.operation_info) = {
+    response_type: "RunWriteBookJobResponse"
+    metadata_type: "RunWriteBookJobMetadata"
+  };
+}
+```
+
+## 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::0152::response-message-name=disabled
+//     aip.dev/not-precedent: We need to do this because reasons. --)
+rpc RunWriteBookJob(RunWriteBookJobRequest) returns (RunWriteBookJobResponse) {
+  option (google.api.http) = {
+    post: "/v1/{name=publishers/*/writeBookJobs/*}:run"
+    body: "*"
+  };
+}
+```
+
+If you need to violate this rule for an entire file, place the comment at the
+top of the file.
+
+[aip-152]: https://aip.dev/152
+[aip.dev/not-precedent]: https://aip.dev/not-precedent
diff --git a/rules/aip0152/aip0152.go b/rules/aip0152/aip0152.go
new file mode 100644
index 0000000..6e7fe92
--- /dev/null
+++ b/rules/aip0152/aip0152.go
@@ -0,0 +1,53 @@
+// Copyright 2020 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 aip0152 contains rules defined in https://aip.dev/152.
+package aip0152
+
+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(
+		152,
+		httpBody,
+		httpMethod,
+		httpUriSuffix,
+		requestMessageName,
+		requestNameBehavior,
+		requestNameField,
+		requestNameReference,
+		responseMessageName,
+	)
+}
+
+var runMethodRegexp = regexp.MustCompile(`^Run[A-Za-z0-9]+Job$`)
+var runReqMessageRegexp = regexp.MustCompile(`^Run[A-Za-z0-9]+JobRequest$`)
+var runURIRegexp = regexp.MustCompile(`:run$`)
+
+// Returns true if this is an AIP-152 Run method, false otherwise.
+func isRunMethod(m *desc.MethodDescriptor) bool {
+	return runMethodRegexp.MatchString(m.GetName())
+}
+
+// Returns true if this is an AIP-152 Run request message, false otherwise.
+func isRunRequestMessage(m *desc.MessageDescriptor) bool {
+	return runReqMessageRegexp.MatchString(m.GetName())
+}
diff --git a/rules/aip0152/http_body.go b/rules/aip0152/http_body.go
new file mode 100644
index 0000000..063926c
--- /dev/null
+++ b/rules/aip0152/http_body.go
@@ -0,0 +1,27 @@
+// Copyright 2020 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 aip0152
+
+import (
+	"github.com/googleapis/api-linter/lint"
+	"github.com/googleapis/api-linter/rules/internal/utils"
+)
+
+// Run methods should use "*" as the HTTP body.
+var httpBody = &lint.MethodRule{
+	Name:       lint.NewRuleName(152, "http-body"),
+	OnlyIf:     isRunMethod,
+	LintMethod: utils.LintWildcardHTTPBody,
+}
diff --git a/rules/aip0152/http_method.go b/rules/aip0152/http_method.go
new file mode 100644
index 0000000..f5f56c9
--- /dev/null
+++ b/rules/aip0152/http_method.go
@@ -0,0 +1,27 @@
+// Copyright 2020 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 aip0152
+
+import (
+	"github.com/googleapis/api-linter/lint"
+	"github.com/googleapis/api-linter/rules/internal/utils"
+)
+
+// Run methods should use the HTTP POST method.
+var httpMethod = &lint.MethodRule{
+	Name:       lint.NewRuleName(152, "http-method"),
+	OnlyIf:     isRunMethod,
+	LintMethod: utils.LintHTTPMethod("POST"),
+}
diff --git a/rules/aip0152/http_uri_suffix.go b/rules/aip0152/http_uri_suffix.go
new file mode 100644
index 0000000..b8cc57d
--- /dev/null
+++ b/rules/aip0152/http_uri_suffix.go
@@ -0,0 +1,39 @@
+// Copyright 2020 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 aip0152
+
+import (
+	"github.com/googleapis/api-linter/lint"
+	"github.com/googleapis/api-linter/rules/internal/utils"
+	"github.com/jhump/protoreflect/desc"
+)
+
+// Run methods should have a proper HTTP pattern.
+var httpUriSuffix = &lint.MethodRule{
+	Name:   lint.NewRuleName(152, "http-uri-suffix"),
+	OnlyIf: isRunMethod,
+	LintMethod: func(m *desc.MethodDescriptor) []lint.Problem {
+		for _, httpRule := range utils.GetHTTPRules(m) {
+			if !runURIRegexp.MatchString(httpRule.URI) {
+				return []lint.Problem{{
+					Message:    `Run URI should end with ":run".`,
+					Descriptor: m,
+				}}
+			}
+		}
+
+		return nil
+	},
+}
diff --git a/rules/aip0152/request_message_name.go b/rules/aip0152/request_message_name.go
new file mode 100644
index 0000000..4a101b0
--- /dev/null
+++ b/rules/aip0152/request_message_name.go
@@ -0,0 +1,27 @@
+// Copyright 2020 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 aip0152
+
+import (
+	"github.com/googleapis/api-linter/lint"
+	"github.com/googleapis/api-linter/rules/internal/utils"
+)
+
+// Run messages should have a properly named request message.
+var requestMessageName = &lint.MethodRule{
+	Name:       lint.NewRuleName(152, "request-message-name"),
+	OnlyIf:     isRunMethod,
+	LintMethod: utils.LintMethodHasMatchingRequestName,
+}
diff --git a/rules/aip0152/request_name_behavior.go b/rules/aip0152/request_name_behavior.go
new file mode 100644
index 0000000..09663c8
--- /dev/null
+++ b/rules/aip0152/request_name_behavior.go
@@ -0,0 +1,29 @@
+// Copyright 2020 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 aip0152
+
+import (
+	"github.com/googleapis/api-linter/lint"
+	"github.com/googleapis/api-linter/rules/internal/utils"
+	"github.com/jhump/protoreflect/desc"
+)
+
+var requestNameBehavior = &lint.FieldRule{
+	Name: lint.NewRuleName(152, "request-name-behavior"),
+	OnlyIf: func(f *desc.FieldDescriptor) bool {
+		return isRunRequestMessage(f.GetOwner()) && f.GetName() == "name"
+	},
+	LintField: utils.LintRequiredField,
+}
diff --git a/rules/aip0152/request_name_field.go b/rules/aip0152/request_name_field.go
new file mode 100644
index 0000000..b0637e5
--- /dev/null
+++ b/rules/aip0152/request_name_field.go
@@ -0,0 +1,51 @@
+// Copyright 2020 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 aip0152
+
+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"
+)
+
+var requestNameField = &lint.MessageRule{
+	Name:   lint.NewRuleName(152, "request-name-field"),
+	OnlyIf: isRunRequestMessage,
+	LintMessage: func(m *desc.MessageDescriptor) []lint.Problem {
+		// Rule check: Establish that a `name` field is present.
+		nameField := m.FindFieldByName("name")
+		if nameField == nil {
+			return []lint.Problem{{
+				Message:    fmt.Sprintf("Message %q has no `name` field.", m.GetName()),
+				Descriptor: m,
+			}}
+		}
+
+		// Rule check: Establish that the `name` field is a string.
+		if nameField.GetType() != builder.FieldTypeString().GetType() || nameField.IsRepeated() {
+			return []lint.Problem{{
+				Message:    "`name` field on Run request message must be a singular string.",
+				Descriptor: nameField,
+				Location:   locations.FieldType(nameField),
+				Suggestion: "string",
+			}}
+		}
+
+		return nil
+	},
+}
diff --git a/rules/aip0152/request_name_reference.go b/rules/aip0152/request_name_reference.go
new file mode 100644
index 0000000..d707544
--- /dev/null
+++ b/rules/aip0152/request_name_reference.go
@@ -0,0 +1,29 @@
+// Copyright 2020 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 aip0152
+
+import (
+	"github.com/googleapis/api-linter/lint"
+	"github.com/googleapis/api-linter/rules/internal/utils"
+	"github.com/jhump/protoreflect/desc"
+)
+
+var requestNameReference = &lint.FieldRule{
+	Name: lint.NewRuleName(152, "request-name-reference"),
+	OnlyIf: func(f *desc.FieldDescriptor) bool {
+		return isRunRequestMessage(f.GetOwner()) && f.GetName() == "name"
+	},
+	LintField: utils.LintFieldResourceReference,
+}
diff --git a/rules/aip0152/response_message_name.go b/rules/aip0152/response_message_name.go
new file mode 100644
index 0000000..57d5c54
--- /dev/null
+++ b/rules/aip0152/response_message_name.go
@@ -0,0 +1,60 @@
+// Copyright 2020 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 aip0152
+
+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"
+)
+
+var responseMessageName = &lint.MethodRule{
+	Name:   lint.NewRuleName(152, "response-message-name"),
+	OnlyIf: isRunMethod,
+	LintMethod: func(m *desc.MethodDescriptor) []lint.Problem {
+		if m.GetOutputType().GetFullyQualifiedName() != "google.longrunning.Operation" {
+			return []lint.Problem{{
+				Message:    "Run methods should use an LRO.",
+				Descriptor: m,
+				Location:   locations.MethodResponseType(m),
+				Suggestion: "google.longrunning.Operation",
+			}}
+		}
+
+		// Rule check: Establish that for methods such as `RunWriteBookJob`, the
+		// response message should be named `RunWriteBookJobRequest`.
+		//
+		// 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.
+		got := utils.GetOperationInfo(m).GetResponseType()
+		want := m.GetName() + "Response"
+		if got != want && got != "" {
+			return []lint.Problem{{
+				Message: fmt.Sprintf(
+					"Run RPCs should have response message type of `%s`, not %q.",
+					want, got,
+				),
+				Suggestion: want,
+				Descriptor: m,
+				Location:   locations.MethodResponseType(m),
+			}}
+		}
+		return nil
+	},
+}
diff --git a/rules/rules.go b/rules/rules.go
index 0ad781b..14c03bf 100644
--- a/rules/rules.go
+++ b/rules/rules.go
@@ -71,6 +71,7 @@ import (
 	"github.com/googleapis/api-linter/rules/aip0146"
 	"github.com/googleapis/api-linter/rules/aip0148"
 	"github.com/googleapis/api-linter/rules/aip0151"
+	"github.com/googleapis/api-linter/rules/aip0152"
 	"github.com/googleapis/api-linter/rules/aip0154"
 	"github.com/googleapis/api-linter/rules/aip0156"
 	"github.com/googleapis/api-linter/rules/aip0158"
@@ -114,6 +115,7 @@ var aipAddRulesFuncs = []addRulesFuncType{
 	aip0146.AddRules,
 	aip0148.AddRules,
 	aip0151.AddRules,
+	aip0152.AddRules,
 	aip0154.AddRules,
 	aip0156.AddRules,
 	aip0158.AddRules,


Test patch:
diff --git a/rules/aip0152/aip0152_test.go b/rules/aip0152/aip0152_test.go
new file mode 100644
index 0000000..0871148
--- /dev/null
+++ b/rules/aip0152/aip0152_test.go
@@ -0,0 +1,13 @@
+package aip0152
+
+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/aip0152/http_body_test.go b/rules/aip0152/http_body_test.go
new file mode 100644
index 0000000..644d463
--- /dev/null
+++ b/rules/aip0152/http_body_test.go
@@ -0,0 +1,57 @@
+// Copyright 2020 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 aip0152
+
+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", "*", "RunWriteBookJob", nil},
+		{"Invalid", "", "RunWriteBookJob", testutils.Problems{{Message: "HTTP body"}}},
+		{"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 Library {
+					rpc {{.MethodName}}({{.MethodName}}Request) returns ({{.MethodName}}Response) {
+						option (google.api.http) = {
+							post: "/v1/{name=publishers/*/writeBookJobs/*}:run"
+							body: "{{.Body}}"
+						};
+					}
+				}
+				message {{.MethodName}}Request {}
+				message {{.MethodName}}Response {}
+			`, test)
+			method := file.GetServices()[0].GetMethods()[0]
+			problems := httpBody.Lint(file)
+			if diff := test.problems.SetDescriptor(method).Diff(problems); diff != "" {
+				t.Error(diff)
+			}
+		})
+	}
+}
diff --git a/rules/aip0152/http_method_test.go b/rules/aip0152/http_method_test.go
new file mode 100644
index 0000000..05b303e
--- /dev/null
+++ b/rules/aip0152/http_method_test.go
@@ -0,0 +1,58 @@
+// Copyright 2020 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 aip0152
+
+import (
+	"testing"
+
+	"github.com/googleapis/api-linter/rules/internal/testutils"
+)
+
+func TestHttpMethod(t *testing.T) {
+	// Set up testing permutations.
+	tests := []struct {
+		testName   string
+		Method     string
+		MethodName string
+		problems   testutils.Problems
+	}{
+		{"Valid", "post", "RunWriteBookJob", nil},
+		{"Invalid", "get", "RunWriteBookJob", testutils.Problems{{Message: "HTTP POST"}}},
+		{"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 Library {
+					rpc {{.MethodName}}({{.MethodName}}Request) returns ({{.MethodName}}Response) {
+						option (google.api.http) = {
+							{{.Method}}: "/v1/{name=publishers/*/writeBookJobs/*}:run"
+						};
+					}
+				}
+				message {{.MethodName}}Request {}
+				message {{.MethodName}}Response {}
+			`, test)
+			method := file.GetServices()[0].GetMethods()[0]
+			problems := httpMethod.Lint(file)
+			if diff := test.problems.SetDescriptor(method).Diff(problems); diff != "" {
+				t.Error(diff)
+			}
+		})
+	}
+}
diff --git a/rules/aip0152/http_uri_suffix_test.go b/rules/aip0152/http_uri_suffix_test.go
new file mode 100644
index 0000000..fd3200e
--- /dev/null
+++ b/rules/aip0152/http_uri_suffix_test.go
@@ -0,0 +1,57 @@
+// Copyright 2020 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 aip0152
+
+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/{name=publishers/*/writeBookJobs/*}:run", "RunWriteBookJob", nil},
+		{"InvalidSuffix", "/v1/{name=publishers/*/writeBookJobs/*}", "RunWriteBookJob", testutils.Problems{{Message: ":run"}}},
+		{"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)
+			problems := httpUriSuffix.Lint(file)
+			if diff := test.problems.SetDescriptor(file.GetServices()[0].GetMethods()[0]).Diff(problems); diff != "" {
+				t.Errorf(diff)
+			}
+		})
+	}
+}
diff --git a/rules/aip0152/request_message_name_test.go b/rules/aip0152/request_message_name_test.go
new file mode 100644
index 0000000..640722e
--- /dev/null
+++ b/rules/aip0152/request_message_name_test.go
@@ -0,0 +1,52 @@
+// Copyright 2020 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 aip0152
+
+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
+		ReqMessageName string
+		problems       testutils.Problems
+	}{
+		{"Valid", "RunWriteBookJob", "RunWriteBookJobRequest", nil},
+		{"Invalid", "RunWriteBookJob", "Book", testutils.Problems{{Suggestion: "RunWriteBookJobRequest"}}},
+		{"Irrelevant", "AcquireBook", "Book", nil},
+	}
+
+	// Run each test individually.
+	for _, test := range tests {
+		t.Run(test.testName, func(t *testing.T) {
+			f := testutils.ParseProto3Tmpl(t, `
+				import "google/longrunning/operations.proto";
+				service Library {
+					rpc {{.MethodName}}({{.ReqMessageName}}) returns (google.longrunning.Operation) {}
+				}
+				message {{.ReqMessageName}} {}
+			`, test)
+			m := f.GetServices()[0].GetMethods()[0]
+			if diff := test.problems.SetDescriptor(m).Diff(requestMessageName.Lint(f)); diff != "" {
+				t.Errorf(diff)
+			}
+		})
+	}
+}
diff --git a/rules/aip0152/request_name_behavior_test.go b/rules/aip0152/request_name_behavior_test.go
new file mode 100644
index 0000000..b0f3e54
--- /dev/null
+++ b/rules/aip0152/request_name_behavior_test.go
@@ -0,0 +1,49 @@
+// Copyright 2020 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 aip0152
+
+import (
+	"testing"
+
+	"github.com/googleapis/api-linter/rules/internal/testutils"
+)
+
+func TestRequestNameBehavior(t *testing.T) {
+	for _, test := range []struct {
+		name          string
+		MessageName   string
+		FieldName     string
+		FieldBehavior string
+		problems      testutils.Problems
+	}{
+		{"Valid", "RunWriteBookJobRequest", "name", " [(google.api.field_behavior) = REQUIRED]", nil},
+		{"Missing", "RunWriteBookJobRequest", "name", "", testutils.Problems{{Message: "(google.api.field_behavior) = REQUIRED"}}},
+		{"IrrelevantMessage", "AcquireBookRequest", "name", "", nil},
+		{"IrrelevantField", "RunWriteBookJobRequest", "something_else", "", nil},
+	} {
+		t.Run(test.name, func(t *testing.T) {
+			f := testutils.ParseProto3Tmpl(t, `
+				import "google/api/field_behavior.proto";
+				message {{.MessageName}} {
+					string {{.FieldName}} = 1{{.FieldBehavior}};
+				}
+			`, test)
+			field := f.GetMessageTypes()[0].GetFields()[0]
+			if diff := test.problems.SetDescriptor(field).Diff(requestNameBehavior.Lint(f)); diff != "" {
+				t.Errorf(diff)
+			}
+		})
+	}
+}
diff --git a/rules/aip0152/request_name_field_test.go b/rules/aip0152/request_name_field_test.go
new file mode 100644
index 0000000..f93d32c
--- /dev/null
+++ b/rules/aip0152/request_name_field_test.go
@@ -0,0 +1,56 @@
+// Copyright 2020 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 aip0152
+
+import (
+	"testing"
+
+	"github.com/googleapis/api-linter/rules/internal/testutils"
+	"github.com/jhump/protoreflect/desc"
+)
+
+func TestRequestNameField(t *testing.T) {
+	tests := []struct {
+		name        string
+		MessageName string
+		Field       string
+		problems    testutils.Problems
+	}{
+		{"Valid", "RunWriteBookJobRequest", `string name = 1;`, nil},
+		{"InvalidMissing", "RunWriteBookJobRequest", "", testutils.Problems{{Message: "has no"}}},
+		{"InvalidType", "RunWriteBookJobRequest", "bytes name = 1;", testutils.Problems{{Suggestion: "string"}}},
+		{"InvalidRepeated", "RunWriteBookJobRequest", "repeated string name = 1;", testutils.Problems{{Suggestion: "string"}}},
+		{"IrrelevantMessage", "RemoveBookRequest", "", nil},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			f := testutils.ParseProto3Tmpl(t, `
+				message {{.MessageName}} {
+					{{.Field}}
+					bytes other_field = 2;
+				}
+			`, test)
+			var d desc.Descriptor = f.GetMessageTypes()[0]
+			if test.name == "InvalidType" || test.name == "InvalidRepeated" {
+				d = f.GetMessageTypes()[0].GetFields()[0]
+			}
+			problems := requestNameField.Lint(f)
+			if diff := test.problems.SetDescriptor(d).Diff(problems); diff != "" {
+				t.Errorf("Problems did not match: %v", diff)
+			}
+		})
+	}
+}
diff --git a/rules/aip0152/request_name_reference_test.go b/rules/aip0152/request_name_reference_test.go
new file mode 100644
index 0000000..80f1bd0
--- /dev/null
+++ b/rules/aip0152/request_name_reference_test.go
@@ -0,0 +1,60 @@
+// Copyright 2020 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 aip0152
+
+import (
+	"testing"
+
+	"github.com/googleapis/api-linter/rules/internal/testutils"
+)
+
+func TestRequestNameReference(t *testing.T) {
+	t.Run("Present", func(t *testing.T) {
+		f := testutils.ParseProto3String(t, `
+			import "google/api/resource.proto";
+			message RunWriteBookJobRequest {
+				string name = 1 [(google.api.resource_reference) = {
+					type: "library.googleapis.com/Book"
+				}];
+			}
+		`)
+		if diff := (testutils.Problems{}).Diff(requestNameReference.Lint(f)); diff != "" {
+			t.Errorf(diff)
+		}
+	})
+	t.Run("Absent", func(t *testing.T) {
+		for _, test := range []struct {
+			name      string
+			FieldName string
+			problems  testutils.Problems
+		}{
+			{"Error", "name", testutils.Problems{{Message: "google.api.resource_reference"}}},
+			{"Irrelevant", "something_else", nil},
+		} {
+			t.Run(test.name, func(t *testing.T) {
+				f := testutils.ParseProto3Tmpl(t, `
+					import "google/api/resource.proto";
+					message RunWriteBookJobRequest {
+						string {{.FieldName}} = 1;
+					}
+				`, test)
+				field := f.GetMessageTypes()[0].GetFields()[0]
+				if diff := test.problems.SetDescriptor(field).Diff(requestNameReference.Lint(f)); diff != "" {
+					t.Errorf(diff)
+				}
+			})
+		}
+	})
+}
diff --git a/rules/aip0152/response_message_name_test.go b/rules/aip0152/response_message_name_test.go
new file mode 100644
index 0000000..c9e1fde
--- /dev/null
+++ b/rules/aip0152/response_message_name_test.go
@@ -0,0 +1,55 @@
+// Copyright 2020 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 aip0152
+
+import (
+	"testing"
+
+	"github.com/googleapis/api-linter/rules/internal/testutils"
+)
+
+func TestResponseMessageName(t *testing.T) {
+	for _, test := range []struct {
+		name         string
+		Method       string
+		ResponseType string
+		LROType      string
+		problems     testutils.Problems
+	}{
+		{"Valid", "RunWriteBookJob", "google.longrunning.Operation", "RunWriteBookJobResponse", nil},
+		{"InvalidNotLRO", "RunWriteBookJob", "RunWriteBookJobResponse", "", testutils.Problems{{Suggestion: "google.longrunning.Operation"}}},
+		{"InvalidLROType", "RunWriteBookJob", "google.longrunning.Operation", "RunWriteBookJobRequest", testutils.Problems{{Suggestion: "RunWriteBookJobResponse"}}},
+		{"Irrelevant", "AcquireBook", "RunWriteBookJobResponse", "", nil},
+	} {
+		t.Run(test.name, func(t *testing.T) {
+			f := testutils.ParseProto3Tmpl(t, `
+				import "google/longrunning/operations.proto";
+				service Library {
+					rpc {{.Method}}(RunWriteBookJobRequest) returns ({{.ResponseType}}) {
+						option (google.longrunning.operation_info) = {
+							response_type: "{{.LROType}}"
+						};
+					}
+				}
+				message RunWriteBookJobRequest {}
+				message RunWriteBookJobResponse {}
+			`, test)
+			m := f.GetServices()[0].GetMethods()[0]
+			if diff := test.problems.SetDescriptor(m).Diff(responseMessageName.Lint(f)); diff != "" {
+				t.Errorf(diff)
+			}
+		})
+	}
+}