Repository: googleapis/api-linter
Pull request #705 (2020-10-29 18:55:16)

feat: Add rules for AIP-165
Part of #704.


Patch:
diff --git a/docs/rules/0165/http-body.md b/docs/rules/0165/http-body.md
new file mode 100644
index 0000000..245d54b
--- /dev/null
+++ b/docs/rules/0165/http-body.md
@@ -0,0 +1,78 @@
+---
+rule:
+  aip: 165
+  name: [core, '0165', http-body]
+  summary: Purge methods should use `*` as the HTTP body.
+permalink: /165/http-body
+redirect_from:
+  - /0165/http-body
+---
+
+# Purge methods: HTTP body
+
+This rule enforces that all `Purge` RPCs use `*` as the HTTP `body`, as mandated in
+[AIP-165][].
+
+## Details
+
+This rule looks at any message beginning with `Purge`, and complains
+if the HTTP `body` field is anything other than `*`.
+
+## Examples
+
+**Incorrect** code for this rule:
+
+```proto
+// Incorrect.
+rpc PurgeBooks(PurgeBooksRequest) returns (google.longrunning.Operation) {
+  option (google.api.http) = {
+    post: "/v1/{parent=publishers/*}/books:purge"
+    // body: "*" should be set.
+  };
+  option (google.longrunning.operation_info) = {
+    response_type: "PurgeBooksResponse"
+    metadata_type: "PurgeBooksMetadata"
+  };
+}
+```
+
+**Correct** code for this rule:
+
+```proto
+// Correct.
+rpc PurgeBooks(PurgeBooksRequest) returns (google.longrunning.Operation) {
+  option (google.api.http) = {
+    post: "/v1/{parent=publishers/*}/books:purge"
+    body: "*"
+  };
+  option (google.longrunning.operation_info) = {
+    response_type: "PurgeBooksResponse"
+    metadata_type: "PurgeBooksMetadata"
+  };
+}
+```
+
+## 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::0165::http-body=disabled
+//     aip.dev/not-precedent: We need to do this because reasons. --)
+rpc PurgeBooks(PurgeBooksRequest) returns (google.longrunning.Operation) {
+  option (google.api.http) = {
+    post: "/v1/{parent=publishers/*}/books:purge"
+  };
+  option (google.longrunning.operation_info) = {
+    response_type: "PurgeBooksResponse"
+    metadata_type: "PurgeBooksMetadata"
+  };
+}
+```
+
+If you need to violate this rule for an entire file, place the comment at the
+top of the file.
+
+[aip-165]: https://aip.dev/165
+[aip.dev/not-precedent]: https://aip.dev/not-precedent
diff --git a/docs/rules/0165/http-method.md b/docs/rules/0165/http-method.md
new file mode 100644
index 0000000..a3499cc
--- /dev/null
+++ b/docs/rules/0165/http-method.md
@@ -0,0 +1,78 @@
+---
+rule:
+  aip: 165
+  name: [core, '0165', http-method]
+  summary: Purge methods must use the POST HTTP verb.
+permalink: /165/http-method
+redirect_from:
+  - /0165/http-method
+---
+
+# Purge methods: POST HTTP verb
+
+This rule enforces that all `Purge` RPCs use the `POST` HTTP verb, as
+mandated in [AIP-165][].
+
+## Details
+
+This rule looks at any message beginning with `Purge`, and complains
+if the HTTP verb is anything other than `POST`. It _does_ check additional
+bindings if they are present.
+
+## Examples
+
+**Incorrect** code for this rule:
+
+```proto
+// Incorrect.
+rpc PurgeBooks(PurgeBooksRequest) returns (google.longrunning.Operation) {
+  option (google.api.http) = {
+    get: "/v1/{parent=publishers/*}/books:purge"  // Should be `post:`.
+  };
+  option (google.longrunning.operation_info) = {
+    response_type: "PurgeBooksResponse"
+    metadata_type: "PurgeBooksMetadata"
+  };
+}
+```
+
+**Correct** code for this rule:
+
+```proto
+// Correct.
+rpc PurgeBooks(PurgeBooksRequest) returns (google.longrunning.Operation) {
+  option (google.api.http) = {
+    post: "/v1/{parent=publishers/*}/books:purge"
+    body: "*"
+  };
+  option (google.longrunning.operation_info) = {
+    response_type: "PurgeBooksResponse"
+    metadata_type: "PurgeBooksMetadata"
+  };
+}
+```
+
+## 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::0165::http-method=disabled
+//     aip.dev/not-precedent: We need to do this because reasons. --)
+rpc PurgeBooks(PurgeBooksRequest) returns (google.longrunning.Operation) {
+  option (google.api.http) = {
+    get: "/v1/{parent=publishers/*}/books:purge"
+  };
+  option (google.longrunning.operation_info) = {
+    response_type: "PurgeBooksResponse"
+    metadata_type: "PurgeBooksMetadata"
+  };
+}
+```
+
+If you need to violate this rule for an entire file, place the comment at the
+top of the file.
+
+[aip-165]: https://aip.dev/165
+[aip.dev/not-precedent]: https://aip.dev/not-precedent
diff --git a/docs/rules/0165/http-uri-suffix.md b/docs/rules/0165/http-uri-suffix.md
new file mode 100644
index 0000000..4d43ab6
--- /dev/null
+++ b/docs/rules/0165/http-uri-suffix.md
@@ -0,0 +1,79 @@
+---
+rule:
+  aip: 165
+  name: [core, '0165', http-uri-suffix]
+  summary: Purge methods must have the correct URI suffix
+permalink: /165/http-uri-suffix
+redirect_from:
+  - /0165/http-uri-suffix
+---
+
+# Purge methods: URI suffix
+
+This rule enforces that `Purge` methods include the `:purge` suffix
+in the REST URI, as mandated in [AIP-165][].
+
+## Details
+
+This rule looks at any method whose name starts with `Purge`, and
+complains if the HTTP URI does not end with `:purge`.
+
+## Examples
+
+**Incorrect** code for this rule:
+
+```proto
+// Incorrect.
+rpc PurgeBooks(PurgeBooksRequest) returns (google.longrunning.Operation) {
+  option (google.api.http) = {
+    post: "/v1/{parent=publishers/*}/books:delete"  // Should end with `:purge`
+    body: "*"
+  };
+  option (google.longrunning.operation_info) = {
+    response_type: "PurgeBooksResponse"
+    metadata_type: "PurgeBooksMetadata"
+  };
+}
+```
+
+**Correct** code for this rule:
+
+```proto
+// Correct.
+rpc PurgeBooks(PurgeBooksRequest) returns (google.longrunning.Operation) {
+  option (google.api.http) = {
+    post: "/v1/{parent=publishers/*}/books:purge"
+    body: "*"
+  };
+  option (google.longrunning.operation_info) = {
+    response_type: "PurgeBooksResponse"
+    metadata_type: "PurgeBooksMetadata"
+  };
+}
+```
+
+## 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::0165::http-uri-suffix=disabled
+//     aip.dev/not-precedent: We need to do this because reasons. --)
+rpc PurgeBooks(PurgeBooksRequest) returns (google.longrunning.Operation) {
+  option (google.api.http) = {
+    post: "/v1/{parent=publishers/*}/books:delete"
+    body: "*"
+  };
+  option (google.longrunning.operation_info) = {
+    response_type: "PurgeBooksResponse"
+    metadata_type: "PurgeBooksMetadata"
+  };
+}
+```
+
+If you need to violate this rule for an entire file, place the comment at the
+top of the file.
+
+[aip-165]: https://aip.dev/165
+[aip.dev/not-precedent]: https://aip.dev/not-precedent
diff --git a/docs/rules/0165/index.md b/docs/rules/0165/index.md
new file mode 100644
index 0000000..52fa625
--- /dev/null
+++ b/docs/rules/0165/index.md
@@ -0,0 +1,10 @@
+---
+aip_listing: 165
+permalink: /165/
+redirect_from:
+  - /0165/
+---
+
+# Criteria-based delete
+
+{% include linter-aip-listing.md aip=165 %}
diff --git a/docs/rules/0165/request-filter-behavior.md b/docs/rules/0165/request-filter-behavior.md
new file mode 100644
index 0000000..829d86a
--- /dev/null
+++ b/docs/rules/0165/request-filter-behavior.md
@@ -0,0 +1,83 @@
+---
+rule:
+  aip: 165
+  name: [core, '0165', request-filter-behavior]
+  summary: |
+    Purge requests should annotate the `filter` field with `google.api.field_behavior`.
+permalink: /165/request-filter-behavior
+redirect_from:
+  - /0165/request-filter-behavior
+---
+
+# Purge requests: Filter field behavior
+
+This rule enforces that all `Purge` requests have
+`google.api.field_behavior` set to `REQUIRED` on their `string filter` field, as
+mandated in [AIP-165][].
+
+## Details
+
+This rule looks at any message matching `Purge*Request` and complains if the
+`filter` field does not have a `google.api.field_behavior` annotation with a
+value of `REQUIRED`.
+
+## Examples
+
+**Incorrect** code for this rule:
+
+```proto
+// Incorrect.
+message PurgeBooksRequest {
+  string parent = 1 [
+    (google.api.field_behavior) = REQUIRED,
+    (google.api.resource_reference).child_type = "library.googleapis.com/Book"
+  ];
+
+  // The `google.api.field_behavior` annotation should be included.
+  string filter = 2;
+
+  bool force = 3;
+}
+```
+
+**Correct** code for this rule:
+
+```proto
+// Correct.
+message PurgeBooksRequest {
+  string parent = 1 [
+    (google.api.field_behavior) = REQUIRED,
+    (google.api.resource_reference).child_type = "library.googleapis.com/Book"
+  ];
+
+  string filter = 2 [(google.api.field_behavior) = REQUIRED];
+
+  bool force = 3;
+}
+```
+
+## 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 PurgeBooksRequest {
+  string parent = 1 [
+    (google.api.field_behavior) = REQUIRED,
+    (google.api.resource_reference).child_type = "library.googleapis.com/Book"
+  ];
+
+  // (-- api-linter: core::0165::request-filter-behavior=disabled
+  //     aip.dev/not-precedent: We need to do this because reasons. --)
+  string filter = 2;
+
+  bool force = 3;
+}
+```
+
+If you need to violate this rule for an entire file, place the comment at the
+top of the file.
+
+[aip-165]: https://aip.dev/165
+[aip.dev/not-precedent]: https://aip.dev/not-precedent
diff --git a/docs/rules/0165/request-filter-field.md b/docs/rules/0165/request-filter-field.md
new file mode 100644
index 0000000..42777dc
--- /dev/null
+++ b/docs/rules/0165/request-filter-field.md
@@ -0,0 +1,94 @@
+---
+rule:
+  aip: 165
+  name: [core, '0165', request-filter-field]
+  summary: Purge RPCs must have a `filter` field in the request.
+permalink: /165/request-filter-field
+redirect_from:
+  - /0165/request-filter-field
+---
+
+# Purge requests: Filter field
+
+This rule enforces that all `Purge` methods have a `string filter`
+field in the request message, as mandated in [AIP-165][].
+
+## Details
+
+This rule looks at any message matching `Purge*Request` and complains if
+either the `filter` field is missing, or if it has any type other than `string`.
+
+## Examples
+
+**Incorrect** code for this rule:
+
+```proto
+// Incorrect.
+// Should include a `string filter` field.
+message PurgeBooksRequest {
+  string parent = 1 [
+    (google.api.field_behavior) = REQUIRED,
+    (google.api.resource_reference).child_type = "library.googleapis.com/Book"
+  ];
+
+  bool force = 3;
+}
+```
+
+```proto
+// Incorrect.
+message PurgeBooksRequest {
+  string parent = 1 [
+    (google.api.field_behavior) = REQUIRED,
+    (google.api.resource_reference).child_type = "library.googleapis.com/Book"
+  ];
+
+  // Field type should be `string`.
+  bytes filter = 2 [(google.api.field_behavior) = REQUIRED];
+
+  bool force = 3;
+}
+```
+
+**Correct** code for this rule:
+
+```proto
+// Correct.
+message PurgeBooksRequest {
+  string parent = 1 [
+    (google.api.field_behavior) = REQUIRED,
+    (google.api.resource_reference).child_type = "library.googleapis.com/Book"
+  ];
+
+  string filter = 2 [(google.api.field_behavior) = REQUIRED];
+
+  bool force = 3;
+}
+```
+
+## Disabling
+
+If you need to violate this rule, use a leading comment above the message (if
+the `filter` 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
+message PurgeBooksRequest {
+  string parent = 1 [
+    (google.api.field_behavior) = REQUIRED,
+    (google.api.resource_reference).child_type = "library.googleapis.com/Book"
+  ];
+
+  // (-- api-linter: core::0165::request-filter-field=disabled
+  //     aip.dev/not-precedent: We need to do this because reasons. --)
+  bytes filter = 2 [(google.api.field_behavior) = REQUIRED];
+
+  bool force = 3;
+}
+```
+
+If you need to violate this rule for an entire file, place the comment at the
+top of the file.
+
+[aip-165]: https://aip.dev/165
+[aip.dev/not-precedent]: https://aip.dev/not-precedent
diff --git a/docs/rules/0165/request-force-field.md b/docs/rules/0165/request-force-field.md
new file mode 100644
index 0000000..79fcc68
--- /dev/null
+++ b/docs/rules/0165/request-force-field.md
@@ -0,0 +1,94 @@
+---
+rule:
+  aip: 165
+  name: [core, '0165', request-force-field]
+  summary: Purge RPCs must have a `force` field in the request.
+permalink: /165/request-force-field
+redirect_from:
+  - /0165/request-force-field
+---
+
+# Purge requests: Force field
+
+This rule enforces that all `Purge` methods have a `bool force`
+field in the request message, as mandated in [AIP-165][].
+
+## Details
+
+This rule looks at any message matching `Purge*Request` and complains if
+either the `force` field is missing, or if it has any type other than `bool`.
+
+## Examples
+
+**Incorrect** code for this rule:
+
+```proto
+// Incorrect.
+// Should include a `bool force` field.
+message PurgeBooksRequest {
+  string parent = 1 [
+    (google.api.field_behavior) = REQUIRED,
+    (google.api.resource_reference).child_type = "library.googleapis.com/Book"
+  ];
+
+  string filter = 2 [(google.api.field_behavior) = REQUIRED];
+}
+```
+
+```proto
+// Incorrect.
+message PurgeBooksRequest {
+  string parent = 1 [
+    (google.api.field_behavior) = REQUIRED,
+    (google.api.resource_reference).child_type = "library.googleapis.com/Book"
+  ];
+
+  string filter = 2 [(google.api.field_behavior) = REQUIRED];
+
+  // Field type should be `bool`.
+  int32 force = 3;
+}
+```
+
+**Correct** code for this rule:
+
+```proto
+// Correct.
+message PurgeBooksRequest {
+  string parent = 1 [
+    (google.api.field_behavior) = REQUIRED,
+    (google.api.resource_reference).child_type = "library.googleapis.com/Book"
+  ];
+
+  string filter = 2 [(google.api.field_behavior) = REQUIRED];
+
+  bool force = 3;
+}
+```
+
+## Disabling
+
+If you need to violate this rule, use a leading comment above the message (if
+the `force` 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
+message PurgeBooksRequest {
+  string parent = 1 [
+    (google.api.field_behavior) = REQUIRED,
+    (google.api.resource_reference).child_type = "library.googleapis.com/Book"
+  ];
+
+  string filter = 2 [(google.api.field_behavior) = REQUIRED];
+
+  // (-- api-linter: core::0165::request-force-field=disabled
+  //     aip.dev/not-precedent: We need to do this because reasons. --)
+  bool force = 3;
+}
+```
+
+If you need to violate this rule for an entire file, place the comment at the
+top of the file.
+
+[aip-165]: https://aip.dev/165
+[aip.dev/not-precedent]: https://aip.dev/not-precedent
diff --git a/docs/rules/0165/request-message-name.md b/docs/rules/0165/request-message-name.md
new file mode 100644
index 0000000..65d47ed
--- /dev/null
+++ b/docs/rules/0165/request-message-name.md
@@ -0,0 +1,81 @@
+---
+rule:
+  aip: 165
+  name: [core, '0165', request-message-name]
+  summary: Purge methods must have standardized request message names.
+permalink: /165/request-message-name
+redirect_from:
+  - /0165/request-message-name
+---
+
+# Purge methods: Request message
+
+This rule enforces that all `Purge` RPCs have a request message name of
+`Purge*Request`, as mandated in [AIP-165][].
+
+## Details
+
+This rule looks at any message beginning with `Purge`, 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 `PurgeBooksRequest`.
+rpc PurgeBooks(DeleteBooksRequest) returns (google.longrunning.Operation) {
+  option (google.api.http) = {
+    post: "/v1/{parent=publishers/*}/books:purge"
+    body: "*"
+  };
+  option (google.longrunning.operation_info) = {
+    response_type: "PurgeBooksResponse"
+    metadata_type: "PurgeBooksMetadata"
+  };
+}
+```
+
+**Correct** code for this rule:
+
+```proto
+// Correct.
+rpc PurgeBooks(PurgeBooksRequest) returns (google.longrunning.Operation) {
+  option (google.api.http) = {
+    post: "/v1/{parent=publishers/*}/books:purge"
+    body: "*"
+  };
+  option (google.longrunning.operation_info) = {
+    response_type: "PurgeBooksResponse"
+    metadata_type: "PurgeBooksMetadata"
+  };
+}
+```
+
+## 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::0165::request-message-name=disabled
+//     aip.dev/not-precedent: We need to do this because reasons. --)
+rpc PurgeBooks(DeleteBooksRequest) returns (google.longrunning.Operation) {
+  option (google.api.http) = {
+    post: "/v1/{parent=publishers/*}/books:purge"
+    body: "*"
+  };
+  option (google.longrunning.operation_info) = {
+    response_type: "PurgeBooksResponse"
+    metadata_type: "PurgeBooksMetadata"
+  };
+}
+```
+
+If you need to violate this rule for an entire file, place the comment at the
+top of the file.
+
+[aip-165]: https://aip.dev/165
+[aip.dev/not-precedent]: https://aip.dev/not-precedent
diff --git a/docs/rules/0165/request-parent-behavior.md b/docs/rules/0165/request-parent-behavior.md
new file mode 100644
index 0000000..ac6343e
--- /dev/null
+++ b/docs/rules/0165/request-parent-behavior.md
@@ -0,0 +1,81 @@
+---
+rule:
+  aip: 165
+  name: [core, '0165', request-parent-behavior]
+  summary: |
+    Purge requests should annotate the `parent` field with `google.api.field_behavior`.
+permalink: /165/request-parent-behavior
+redirect_from:
+  - /0165/request-parent-behavior
+---
+
+# Purge requests: Parent field behavior
+
+This rule enforces that all `Purge` requests have
+`google.api.field_behavior` set to `REQUIRED` on their `string parent` field, as
+mandated in [AIP-165][].
+
+## Details
+
+This rule looks at any message matching `Purge*Request` and complains if the
+`parent` field does not have a `google.api.field_behavior` annotation with a
+value of `REQUIRED`.
+
+## Examples
+
+**Incorrect** code for this rule:
+
+```proto
+// Incorrect.
+message PurgeBooksRequest {
+  // The `google.api.field_behavior` annotation should also be included.
+  string parent = 1 [
+    (google.api.resource_reference).child_type = "library.googleapis.com/Book"
+  ];
+
+  string filter = 2 [(google.api.field_behavior) = REQUIRED];
+
+  bool force = 3;
+}
+```
+
+**Correct** code for this rule:
+
+```proto
+// Correct.
+message PurgeBooksRequest {
+  string parent = 1 [
+    (google.api.field_behavior) = REQUIRED,
+    (google.api.resource_reference).child_type = "library.googleapis.com/Book"
+  ];
+
+  string filter = 2 [(google.api.field_behavior) = REQUIRED];
+
+  bool force = 3;
+}
+```
+
+## 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 PurgeBooksRequest {
+  // (-- api-linter: core::0165::request-parent-behavior=disabled
+  //     aip.dev/not-precedent: We need to do this because reasons. --)
+  string parent = 1 [
+    (google.api.resource_reference).child_type = "library.googleapis.com/Book"
+  ];
+
+  string filter = 2 [(google.api.field_behavior) = REQUIRED];
+
+  bool force = 3;
+}
+```
+
+If you need to violate this rule for an entire file, place the comment at the
+top of the file.
+
+[aip-165]: https://aip.dev/165
+[aip.dev/not-precedent]: https://aip.dev/not-precedent
diff --git a/docs/rules/0165/request-parent-field.md b/docs/rules/0165/request-parent-field.md
new file mode 100644
index 0000000..83bb15b
--- /dev/null
+++ b/docs/rules/0165/request-parent-field.md
@@ -0,0 +1,94 @@
+---
+rule:
+  aip: 165
+  name: [core, '0165', request-parent-field]
+  summary: Purge RPCs must have a `parent` field in the request.
+permalink: /165/request-parent-field
+redirect_from:
+  - /0165/request-parent-field
+---
+
+# Purge requests: Parent field
+
+This rule enforces that all `Purge` methods have a `string parent`
+field in the request message, as mandated in [AIP-165][], unless the resource is
+top-level.
+
+## Details
+
+This rule looks at any message matching `Purge*Request` and complains if
+either the `parent` field is missing, or if it has any type other than `string`.
+
+This rule is skipped for top-level resources, since they have no parent.
+
+## Examples
+
+**Incorrect** code for this rule:
+
+```proto
+// Incorrect.
+// Should include a `string parent` field.
+message PurgeBooksRequest {
+  string filter = 2 [(google.api.field_behavior) = REQUIRED];
+
+  bool force = 3;
+}
+```
+
+```proto
+// Incorrect.
+message PurgeBooksRequest {
+  // Field type should be `string`.
+  bytes parent = 1 [
+    (google.api.field_behavior) = REQUIRED,
+    (google.api.resource_reference).child_type = "library.googleapis.com/Book"
+  ];
+
+  string filter = 2 [(google.api.field_behavior) = REQUIRED];
+
+  bool force = 3;
+}
+```
+
+**Correct** code for this rule:
+
+```proto
+// Correct.
+message PurgeBooksRequest {
+  string parent = 1 [
+    (google.api.field_behavior) = REQUIRED,
+    (google.api.resource_reference).child_type = "library.googleapis.com/Book"
+  ];
+
+  string filter = 2 [(google.api.field_behavior) = REQUIRED];
+
+  bool force = 3;
+}
+```
+
+## 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
+message PurgeBooksRequest {
+  // (-- api-linter: core::0165::request-parent-field=disabled
+  //     aip.dev/not-precedent: We need to do this because reasons. --)
+  bytes parent = 1 [
+    (google.api.field_behavior) = REQUIRED,
+    (google.api.resource_reference).child_type = "library.googleapis.com/Book"
+  ];
+
+  string filter = 2 [(google.api.field_behavior) = REQUIRED];
+
+  bool force = 3;
+}
+```
+
+If you need to violate this rule for an entire file, place the comment at the
+top of the file.
+
+[aip-165]: https://aip.dev/165
+[aip.dev/not-precedent]: https://aip.dev/not-precedent
diff --git a/docs/rules/0165/request-parent-reference.md b/docs/rules/0165/request-parent-reference.md
new file mode 100644
index 0000000..6771309
--- /dev/null
+++ b/docs/rules/0165/request-parent-reference.md
@@ -0,0 +1,76 @@
+---
+rule:
+  aip: 165
+  name: [core, '0165', request-parent-reference]
+  summary: |
+    Purge requests should annotate the `parent` field with `google.api.resource_reference`.
+permalink: /165/request-parent-reference
+redirect_from:
+  - /0165/request-parent-reference
+---
+
+# Purge requests: Parent resource reference
+
+This rule enforces that all `Purge` requests have
+`google.api.resource_reference` on their `string parent` field, as mandated in
+[AIP-165][].
+
+## Details
+
+This rule looks at the `parent` field of any message matching `Purge*Request`
+and complains if it does not have a `google.api.resource_reference` annotation.
+
+## Examples
+
+**Incorrect** code for this rule:
+
+```proto
+// Incorrect.
+message PurgeBooksRequest {
+  // The `google.api.resource_reference` annotation should also be included.
+  string parent = 1 [(google.api.field_behavior) = REQUIRED];
+
+  string filter = 2 [(google.api.field_behavior) = REQUIRED];
+
+  bool force = 3;
+}
+```
+
+**Correct** code for this rule:
+
+```proto
+// Correct.
+message PurgeBooksRequest {
+  string parent = 1 [
+    (google.api.field_behavior) = REQUIRED,
+    (google.api.resource_reference).child_type = "library.googleapis.com/Book"
+  ];
+
+  string filter = 2 [(google.api.field_behavior) = REQUIRED];
+
+  bool force = 3;
+}
+```
+
+## 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 PurgeBooksRequest {
+  // (-- api-linter: core::0165::request-parent-reference=disabled
+  //     aip.dev/not-precedent: We need to do this because reasons. --)
+  string parent = 1 [(google.api.field_behavior) = REQUIRED];
+
+  string filter = 2 [(google.api.field_behavior) = REQUIRED];
+
+  bool force = 3;
+}
+```
+
+If you need to violate this rule for an entire file, place the comment at the
+top of the file.
+
+[aip-165]: https://aip.dev/165
+[aip.dev/not-precedent]: https://aip.dev/not-precedent
diff --git a/docs/rules/0165/response-purge-count-field.md b/docs/rules/0165/response-purge-count-field.md
new file mode 100644
index 0000000..6b6d836
--- /dev/null
+++ b/docs/rules/0165/response-purge-count-field.md
@@ -0,0 +1,84 @@
+---
+rule:
+  aip: 165
+  name: [core, '0165', response-purge-count-field]
+  summary: Purge RPCs must have a `purge_count` field in the response.
+permalink: /165/response-purge-count-field
+redirect_from:
+  - /0165/response-purge-count-field
+---
+
+# Purge responses: Purge count field
+
+This rule enforces that all `Purge` methods have an `int32 purge_count`
+field in the response message, as mandated in [AIP-165][].
+
+## Details
+
+This rule looks at any message matching `Purge*Response` and complains if
+either the `purge_count` field is missing, or if it has any type other than
+`int32`.
+
+## Examples
+
+**Incorrect** code for this rule:
+
+```proto
+// Incorrect.
+// Should include an `int32 purge_count` field.
+message PurgeBooksResponse {
+  repeated string purge_sample = 2 [
+    (google.api.resource_reference).type = "library.googleapis.com/Book"
+  ];
+}
+```
+
+```proto
+// Incorrect.
+message PurgeBooksResponse {
+  // Field type should be `int32`.
+  int64 purge_count = 1;
+
+  repeated string purge_sample = 2 [
+    (google.api.resource_reference).type = "library.googleapis.com/Book"
+  ];
+}
+```
+
+**Correct** code for this rule:
+
+```proto
+// Correct.
+message PurgeBooksResponse {
+  int32 purge_count = 1;
+
+  repeated string purge_sample = 2 [
+    (google.api.resource_reference).type = "library.googleapis.com/Book"
+  ];
+}
+```
+
+## Disabling
+
+If you need to violate this rule, use a leading comment above the message (if
+the `purge_count` 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
+message PurgeBooksResponse {
+  // (-- api-linter: core::0165::response-purge-count-field=disabled
+  //     aip.dev/not-precedent: We need to do this because reasons. --)
+  int64 purge_count = 1;
+
+  repeated string purge_sample = 2 [
+    (google.api.resource_reference).type = "library.googleapis.com/Book"
+  ];
+}
+```
+
+If you need to violate this rule for an entire file, place the comment at the
+top of the file.
+
+[aip-165]: https://aip.dev/165
+[aip.dev/not-precedent]: https://aip.dev/not-precedent
diff --git a/docs/rules/0165/response-purge-sample-field.md b/docs/rules/0165/response-purge-sample-field.md
new file mode 100644
index 0000000..71402a4
--- /dev/null
+++ b/docs/rules/0165/response-purge-sample-field.md
@@ -0,0 +1,82 @@
+---
+rule:
+  aip: 165
+  name: [core, '0165', response-purge-sample-field]
+  summary: Purge RPCs must have a `purge_sample` field in the response.
+permalink: /165/response-purge-sample-field
+redirect_from:
+  - /0165/response-purge-sample-field
+---
+
+# Purge responses: Purge sample field
+
+This rule enforces that all `Purge` methods have a `repeated string
+purge_sample` field in the response message, as mandated in [AIP-165][].
+
+## Details
+
+This rule looks at any message matching `Purge*Response` and complains if
+either the `purge_sample` field is missing, or if it has any type other than
+`repeated string`.
+
+## Examples
+
+**Incorrect** code for this rule:
+
+```proto
+// Incorrect.
+// Should include a `repeated string purge_sample` field.
+message PurgeBooksResponse {
+  int32 purge_count = 1;
+}
+```
+
+```proto
+// Incorrect.
+message PurgeBooksResponse {
+  int64 purge_count = 1;
+
+  // Field type should be `repeated string`.
+  repeated bytes purge_sample = 2 [
+    (google.api.resource_reference).type = "library.googleapis.com/Book"
+  ];
+}
+```
+
+**Correct** code for this rule:
+
+```proto
+// Correct.
+message PurgeBooksResponse {
+  int32 purge_count = 1;
+
+  repeated string purge_sample = 2 [
+    (google.api.resource_reference).type = "library.googleapis.com/Book"
+  ];
+}
+```
+
+## Disabling
+
+If you need to violate this rule, use a leading comment above the message (if
+the `purge_sample` 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
+message PurgeBooksResponse {
+  int32 purge_count = 1;
+
+  // (-- api-linter: core::0165::response-purge-sample-field=disabled
+  //     aip.dev/not-precedent: We need to do this because reasons. --)
+  repeated bytes purge_sample = 2 [
+    (google.api.resource_reference).type = "library.googleapis.com/Book"
+  ];
+}
+```
+
+If you need to violate this rule for an entire file, place the comment at the
+top of the file.
+
+[aip-165]: https://aip.dev/165
+[aip.dev/not-precedent]: https://aip.dev/not-precedent
diff --git a/docs/rules/0165/response-purge-sample-reference.md b/docs/rules/0165/response-purge-sample-reference.md
new file mode 100644
index 0000000..ccaf04d
--- /dev/null
+++ b/docs/rules/0165/response-purge-sample-reference.md
@@ -0,0 +1,70 @@
+---
+rule:
+  aip: 165
+  name: [core, '0165', response-purge-sample-reference]
+  summary: |
+    Purge responses should annotate the `purge_sample` field with `google.api.resource_reference`.
+permalink: /165/response-purge-sample-reference
+redirect_from:
+  - /0165/response-purge-sample-reference
+---
+
+# Purge responses: Purge sample resource reference
+
+This rule enforces that all `Purge` responses have
+`google.api.resource_reference` on their `repeated string purge_sample` field,
+as mandated in [AIP-165][].
+
+## Details
+
+This rule looks at the `purge_sample` field of any message matching
+`Purge*Response` and complains if it does not have a
+`google.api.resource_reference` annotation.
+
+## Examples
+
+**Incorrect** code for this rule:
+
+```proto
+// Incorrect.
+message PurgeBooksResponse {
+  int32 purge_count = 1;
+
+  // The `google.api.resource_reference` annotation should be included.
+  repeated string purge_sample = 2;
+}
+```
+
+**Correct** code for this rule:
+
+```proto
+// Correct.
+message PurgeBooksResponse {
+  int32 purge_count = 1;
+
+  repeated string purge_sample = 2 [
+    (google.api.resource_reference).type = "library.googleapis.com/Book"
+  ];
+}
+```
+
+## 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 PurgeBooksResponse {
+  int32 purge_count = 1;
+
+  // (-- api-linter: core::0165::response-purge-sample-reference=disabled
+  //     aip.dev/not-precedent: We need to do this because reasons. --)
+  repeated string purge_sample = 2;
+}
+```
+
+If you need to violate this rule for an entire file, place the comment at the
+top of the file.
+
+[aip-165]: https://aip.dev/165
+[aip.dev/not-precedent]: https://aip.dev/not-precedent
diff --git a/rules/aip0165/aip0165.go b/rules/aip0165/aip0165.go
new file mode 100644
index 0000000..656060c
--- /dev/null
+++ b/rules/aip0165/aip0165.go
@@ -0,0 +1,63 @@
+// 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 aip0165 contains rules defined in https://aip.dev/165.
+package aip0165
+
+import (
+	"regexp"
+
+	"github.com/googleapis/api-linter/lint"
+	"github.com/jhump/protoreflect/desc"
+)
+
+// AddRules adds all of the AIP-165 rules to the provided registry.
+func AddRules(r lint.RuleRegistry) error {
+	return r.Register(
+		165,
+		httpBody,
+		httpMethod,
+		httpURISuffix,
+		requestFilterBehavior,
+		requestFilterField,
+		requestForceField,
+		requestMessageName,
+		requestParentBehavior,
+		requestParentField,
+		requestParentReference,
+		responsePurgeCountField,
+		responsePurgeSampleField,
+		responsePurgeSampleReference,
+	)
+}
+
+var purgeMethodRegexp = regexp.MustCompile("^Purge(?:[A-Z]|$)")
+var purgeReqMessageRegexp = regexp.MustCompile("^Purge[A-Za-z0-9]*Request$")
+var purgeRespMessageRegexp = regexp.MustCompile("^Purge[A-Za-z0-9]*Response$")
+var purgeURINameRegexp = regexp.MustCompile(`:purge$`)
+
+// Returns true if this is a AIP-165 Purge method, false otherwise.
+func isPurgeMethod(m *desc.MethodDescriptor) bool {
+	return purgeMethodRegexp.MatchString(m.GetName())
+}
+
+// Returns true if this is an AIP-165 Purge request message, false otherwise.
+func isPurgeRequestMessage(m *desc.MessageDescriptor) bool {
+	return purgeReqMessageRegexp.MatchString(m.GetName())
+}
+
+// Returns true if this is an AIP-165 Purge response message, false otherwise.
+func isPurgeResponseMessage(m *desc.MessageDescriptor) bool {
+	return purgeRespMessageRegexp.MatchString(m.GetName())
+}
diff --git a/rules/aip0165/http_body.go b/rules/aip0165/http_body.go
new file mode 100644
index 0000000..bed1db3
--- /dev/null
+++ b/rules/aip0165/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 aip0165
+
+import (
+	"github.com/googleapis/api-linter/lint"
+	"github.com/googleapis/api-linter/rules/internal/utils"
+)
+
+// Purge methods should have "*" as the HTTP body.
+var httpBody = &lint.MethodRule{
+	Name:       lint.NewRuleName(165, "http-body"),
+	OnlyIf:     isPurgeMethod,
+	LintMethod: utils.LintWildcardHTTPBody,
+}
diff --git a/rules/aip0165/http_method.go b/rules/aip0165/http_method.go
new file mode 100644
index 0000000..e6b88ba
--- /dev/null
+++ b/rules/aip0165/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 aip0165
+
+import (
+	"github.com/googleapis/api-linter/lint"
+	"github.com/googleapis/api-linter/rules/internal/utils"
+)
+
+// Purge methods should use the HTTP POST method.
+var httpMethod = &lint.MethodRule{
+	Name:       lint.NewRuleName(165, "http-method"),
+	OnlyIf:     isPurgeMethod,
+	LintMethod: utils.LintHTTPMethod("POST"),
+}
diff --git a/rules/aip0165/http_uri_suffix.go b/rules/aip0165/http_uri_suffix.go
new file mode 100644
index 0000000..c4d3391
--- /dev/null
+++ b/rules/aip0165/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 aip0165
+
+import (
+	"github.com/googleapis/api-linter/lint"
+	"github.com/googleapis/api-linter/rules/internal/utils"
+	"github.com/jhump/protoreflect/desc"
+)
+
+// Purge methods should have a proper HTTP pattern.
+var httpURISuffix = &lint.MethodRule{
+	Name:   lint.NewRuleName(165, "http-uri-suffix"),
+	OnlyIf: isPurgeMethod,
+	LintMethod: func(m *desc.MethodDescriptor) []lint.Problem {
+		for _, httpRule := range utils.GetHTTPRules(m) {
+			if !purgeURINameRegexp.MatchString(httpRule.URI) {
+				return []lint.Problem{{
+					Message:    `Purge URI should end with ":purge".`,
+					Descriptor: m,
+				}}
+			}
+		}
+
+		return nil
+	},
+}
diff --git a/rules/aip0165/request_filter_behavior.go b/rules/aip0165/request_filter_behavior.go
new file mode 100644
index 0000000..868a282
--- /dev/null
+++ b/rules/aip0165/request_filter_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 aip0165
+
+import (
+	"github.com/googleapis/api-linter/lint"
+	"github.com/googleapis/api-linter/rules/internal/utils"
+	"github.com/jhump/protoreflect/desc"
+)
+
+var requestFilterBehavior = &lint.FieldRule{
+	Name: lint.NewRuleName(165, "request-filter-behavior"),
+	OnlyIf: func(f *desc.FieldDescriptor) bool {
+		return isPurgeRequestMessage(f.GetOwner()) && f.GetName() == "filter"
+	},
+	LintField: utils.LintRequiredField,
+}
diff --git a/rules/aip0165/request_filter_field.go b/rules/aip0165/request_filter_field.go
new file mode 100644
index 0000000..59f688b
--- /dev/null
+++ b/rules/aip0165/request_filter_field.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 aip0165
+
+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 Purge request message should have filter field.
+var requestFilterField = &lint.MessageRule{
+	Name:   lint.NewRuleName(165, "request-filter-field"),
+	OnlyIf: isPurgeRequestMessage,
+	LintMessage: func(m *desc.MessageDescriptor) []lint.Problem {
+		// Rule check: Establish that a `filter` field is present.
+		filterField := m.FindFieldByName("filter")
+		if filterField == nil {
+			return []lint.Problem{{
+				Message:    fmt.Sprintf("Message %q has no `filter` field.", m.GetName()),
+				Descriptor: m,
+			}}
+		}
+
+		// Rule check: Establish that the filter field is a string.
+		if filterField.GetType() != builder.FieldTypeString().GetType() || filterField.IsRepeated() {
+			return []lint.Problem{{
+				Message:    "`filter` field on Purge request message must be a singular string.",
+				Descriptor: filterField,
+				Location:   locations.FieldType(filterField),
+				Suggestion: "string",
+			}}
+		}
+
+		return nil
+	},
+}
diff --git a/rules/aip0165/request_force_field.go b/rules/aip0165/request_force_field.go
new file mode 100644
index 0000000..fa4c68e
--- /dev/null
+++ b/rules/aip0165/request_force_field.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 aip0165
+
+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 Purge request message should have force field.
+var requestForceField = &lint.MessageRule{
+	Name:   lint.NewRuleName(165, "request-force-field"),
+	OnlyIf: isPurgeRequestMessage,
+	LintMessage: func(m *desc.MessageDescriptor) []lint.Problem {
+		// Rule check: Establish that a `force` field is present.
+		forceField := m.FindFieldByName("force")
+		if forceField == nil {
+			return []lint.Problem{{
+				Message:    fmt.Sprintf("Message %q has no `force` field.", m.GetName()),
+				Descriptor: m,
+			}}
+		}
+
+		// Rule check: Establish that the force field is a bool.
+		if forceField.GetType() != builder.FieldTypeBool().GetType() || forceField.IsRepeated() {
+			return []lint.Problem{{
+				Message:    "`force` field on Purge request message must be a singular bool.",
+				Descriptor: forceField,
+				Location:   locations.FieldType(forceField),
+				Suggestion: "bool",
+			}}
+		}
+
+		return nil
+	},
+}
diff --git a/rules/aip0165/request_message_name.go b/rules/aip0165/request_message_name.go
new file mode 100644
index 0000000..b2e5a42
--- /dev/null
+++ b/rules/aip0165/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 aip0165
+
+import (
+	"github.com/googleapis/api-linter/lint"
+	"github.com/googleapis/api-linter/rules/internal/utils"
+)
+
+// Purge messages should have a properly named request message.
+var requestMessageName = &lint.MethodRule{
+	Name:       lint.NewRuleName(165, "request-message-name"),
+	OnlyIf:     isPurgeMethod,
+	LintMethod: utils.LintMethodHasMatchingRequestName,
+}
diff --git a/rules/aip0165/request_parent_behavior.go b/rules/aip0165/request_parent_behavior.go
new file mode 100644
index 0000000..61baa80
--- /dev/null
+++ b/rules/aip0165/request_parent_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 aip0165
+
+import (
+	"github.com/googleapis/api-linter/lint"
+	"github.com/googleapis/api-linter/rules/internal/utils"
+	"github.com/jhump/protoreflect/desc"
+)
+
+var requestParentBehavior = &lint.FieldRule{
+	Name: lint.NewRuleName(165, "request-parent-behavior"),
+	OnlyIf: func(f *desc.FieldDescriptor) bool {
+		return isPurgeRequestMessage(f.GetOwner()) && f.GetName() == "parent"
+	},
+	LintField: utils.LintRequiredField,
+}
diff --git a/rules/aip0165/request_parent_field.go b/rules/aip0165/request_parent_field.go
new file mode 100644
index 0000000..9c9d380
--- /dev/null
+++ b/rules/aip0165/request_parent_field.go
@@ -0,0 +1,76 @@
+// 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 aip0165
+
+import (
+	"fmt"
+	"strings"
+
+	"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"
+	"github.com/jhump/protoreflect/desc/builder"
+	"github.com/stoewer/go-strcase"
+)
+
+// The Purge request message should have parent field.
+var requestParentField = &lint.MessageRule{
+	Name: lint.NewRuleName(165, "request-parent-field"),
+	OnlyIf: func(m *desc.MessageDescriptor) bool {
+		// Sanity check: If the resource has a pattern, and that pattern
+		// contains no variables, then a parent field is not expected.
+		//
+		// In order to parse out the pattern, we get the resource message
+		// from the response, then get the resource annotation from that,
+		// and then inspect the pattern there (oy!).
+		plural := strings.TrimPrefix(strings.TrimSuffix(m.GetName(), "Request"), "Purge")
+		if resp := utils.FindMessage(m.GetFile(), fmt.Sprintf("Purge%sResponse", plural)); resp != nil {
+			if paged := resp.FindFieldByName(strcase.SnakeCase(plural)); paged != nil {
+				if resource := utils.GetResource(paged.GetMessageType()); resource != nil {
+					for _, pattern := range resource.GetPattern() {
+						if strings.Count(pattern, "{") == 0 {
+							return false
+						}
+					}
+				}
+			}
+		}
+
+		return isPurgeRequestMessage(m)
+	},
+	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() || parentField.IsRepeated() {
+			return []lint.Problem{{
+				Message:    "`parent` field on Purge request message must be a singular string.",
+				Descriptor: parentField,
+				Location:   locations.FieldType(parentField),
+				Suggestion: "string",
+			}}
+		}
+
+		return nil
+	},
+}
diff --git a/rules/aip0165/request_parent_reference.go b/rules/aip0165/request_parent_reference.go
new file mode 100644
index 0000000..28df802
--- /dev/null
+++ b/rules/aip0165/request_parent_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 aip0165
+
+import (
+	"github.com/googleapis/api-linter/lint"
+	"github.com/googleapis/api-linter/rules/internal/utils"
+	"github.com/jhump/protoreflect/desc"
+)
+
+var requestParentReference = &lint.FieldRule{
+	Name: lint.NewRuleName(165, "request-parent-reference"),
+	OnlyIf: func(f *desc.FieldDescriptor) bool {
+		return isPurgeRequestMessage(f.GetOwner()) && f.GetName() == "parent"
+	},
+	LintField: utils.LintFieldResourceReference,
+}
diff --git a/rules/aip0165/response_purge_count_field.go b/rules/aip0165/response_purge_count_field.go
new file mode 100644
index 0000000..bec01e8
--- /dev/null
+++ b/rules/aip0165/response_purge_count_field.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 aip0165
+
+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 Purge response message should have purge_count field.
+var responsePurgeCountField = &lint.MessageRule{
+	Name:   lint.NewRuleName(165, "response-purge-count-field"),
+	OnlyIf: isPurgeResponseMessage,
+	LintMessage: func(m *desc.MessageDescriptor) []lint.Problem {
+		// Rule check: Establish that a `purge_count` field is present.
+		field := m.FindFieldByName("purge_count")
+		if field == nil {
+			return []lint.Problem{{
+				Message:    fmt.Sprintf("Message %q has no `purge_count` field.", m.GetName()),
+				Descriptor: m,
+			}}
+		}
+
+		// Rule check: Establish that the purge_count field is a singular int32.
+		if field.GetType() != builder.FieldTypeInt32().GetType() || field.IsRepeated() {
+			return []lint.Problem{{
+				Message:    "`purge_count` field on Purge response message must be a singular int32.",
+				Descriptor: field,
+				Location:   locations.FieldType(field),
+				Suggestion: "int32",
+			}}
+		}
+
+		return nil
+	},
+}
diff --git a/rules/aip0165/response_purge_sample_field.go b/rules/aip0165/response_purge_sample_field.go
new file mode 100644
index 0000000..8709ae2
--- /dev/null
+++ b/rules/aip0165/response_purge_sample_field.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 aip0165
+
+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 Purge response message should have purge_sample field.
+var responsePurgeSampleField = &lint.MessageRule{
+	Name:   lint.NewRuleName(165, "response-purge-sample-field"),
+	OnlyIf: isPurgeResponseMessage,
+	LintMessage: func(m *desc.MessageDescriptor) []lint.Problem {
+		// Rule check: Establish that a `purge_sample` field is present.
+		field := m.FindFieldByName("purge_sample")
+		if field == nil {
+			return []lint.Problem{{
+				Message:    fmt.Sprintf("Message %q has no `purge_sample` field.", m.GetName()),
+				Descriptor: m,
+			}}
+		}
+
+		// Rule check: Establish that the purge_sample field is a repeated string.
+		if field.GetType() != builder.FieldTypeString().GetType() || !field.IsRepeated() {
+			return []lint.Problem{{
+				Message:    "`purge_sample` field on Purge response message must be a repeated string.",
+				Descriptor: field,
+				Location:   locations.FieldType(field),
+				Suggestion: "repeated string",
+			}}
+		}
+
+		return nil
+	},
+}
diff --git a/rules/aip0165/response_purge_sample_reference.go b/rules/aip0165/response_purge_sample_reference.go
new file mode 100644
index 0000000..a18a41c
--- /dev/null
+++ b/rules/aip0165/response_purge_sample_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 aip0165
+
+import (
+	"github.com/googleapis/api-linter/lint"
+	"github.com/googleapis/api-linter/rules/internal/utils"
+	"github.com/jhump/protoreflect/desc"
+)
+
+var responsePurgeSampleReference = &lint.FieldRule{
+	Name: lint.NewRuleName(165, "response-purge-sample-reference"),
+	OnlyIf: func(f *desc.FieldDescriptor) bool {
+		return isPurgeResponseMessage(f.GetOwner()) && f.GetName() == "purge_sample"
+	},
+	LintField: utils.LintFieldResourceReference,
+}
diff --git a/rules/rules.go b/rules/rules.go
index 9af2ad8..0ad781b 100644
--- a/rules/rules.go
+++ b/rules/rules.go
@@ -77,6 +77,7 @@ import (
 	"github.com/googleapis/api-linter/rules/aip0159"
 	"github.com/googleapis/api-linter/rules/aip0163"
 	"github.com/googleapis/api-linter/rules/aip0164"
+	"github.com/googleapis/api-linter/rules/aip0165"
 	"github.com/googleapis/api-linter/rules/aip0191"
 	"github.com/googleapis/api-linter/rules/aip0192"
 	"github.com/googleapis/api-linter/rules/aip0203"
@@ -119,6 +120,7 @@ var aipAddRulesFuncs = []addRulesFuncType{
 	aip0159.AddRules,
 	aip0163.AddRules,
 	aip0164.AddRules,
+	aip0165.AddRules,
 	aip0191.AddRules,
 	aip0192.AddRules,
 	aip0203.AddRules,


Test patch:
diff --git a/rules/aip0165/aip0165_test.go b/rules/aip0165/aip0165_test.go
new file mode 100644
index 0000000..f2a6cac
--- /dev/null
+++ b/rules/aip0165/aip0165_test.go
@@ -0,0 +1,13 @@
+package aip0165
+
+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/aip0165/http_body_test.go b/rules/aip0165/http_body_test.go
new file mode 100644
index 0000000..bb022f8
--- /dev/null
+++ b/rules/aip0165/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 aip0165
+
+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", "*", "PurgeBooks", nil},
+		{"Invalid", "", "PurgeBooks", 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/{parent=publishers/*}/books:purge"
+							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/aip0165/http_method_test.go b/rules/aip0165/http_method_test.go
new file mode 100644
index 0000000..33b38ea
--- /dev/null
+++ b/rules/aip0165/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 aip0165
+
+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", "PurgeBooks", nil},
+		{"Invalid", "delete", "PurgeBooks", testutils.Problems{{Message: "HTTP POST"}}},
+		{"Irrelevant", "delete", "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/*}/books:purge"
+						};
+					}
+				}
+				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/aip0165/http_uri_suffix_test.go b/rules/aip0165/http_uri_suffix_test.go
new file mode 100644
index 0000000..23a718b
--- /dev/null
+++ b/rules/aip0165/http_uri_suffix_test.go
@@ -0,0 +1,59 @@
+// 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 aip0165
+
+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/*}/books:purge", "PurgeBooks", nil},
+		{"InvalidSuffix", "/v1/{name=publishers/*}:delete", "PurgeBooks", testutils.Problems{{Message: ":purge"}}},
+		{"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/aip0165/request_filter_behavior_test.go b/rules/aip0165/request_filter_behavior_test.go
new file mode 100644
index 0000000..fa42e60
--- /dev/null
+++ b/rules/aip0165/request_filter_behavior_test.go
@@ -0,0 +1,47 @@
+// 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 aip0165
+
+import (
+	"testing"
+
+	"github.com/googleapis/api-linter/rules/internal/testutils"
+)
+
+func TestRequestFilterBehavior(t *testing.T) {
+	for _, test := range []struct {
+		name          string
+		FieldName     string
+		FieldBehavior string
+		problems      testutils.Problems
+	}{
+		{"Valid", "filter", " [(google.api.field_behavior) = REQUIRED]", nil},
+		{"Missing", "filter", "", testutils.Problems{{Message: "(google.api.field_behavior) = REQUIRED"}}},
+		{"Irrelevant", "something_else", "", nil},
+	} {
+		t.Run(test.name, func(t *testing.T) {
+			f := testutils.ParseProto3Tmpl(t, `
+				import "google/api/field_behavior.proto";
+				message PurgeBooksRequest {
+					string {{.FieldName}} = 1{{.FieldBehavior}};
+				}
+			`, test)
+			field := f.GetMessageTypes()[0].GetFields()[0]
+			if diff := test.problems.SetDescriptor(field).Diff(requestFilterBehavior.Lint(f)); diff != "" {
+				t.Errorf(diff)
+			}
+		})
+	}
+}
diff --git a/rules/aip0165/request_filter_field_test.go b/rules/aip0165/request_filter_field_test.go
new file mode 100644
index 0000000..272a413
--- /dev/null
+++ b/rules/aip0165/request_filter_field_test.go
@@ -0,0 +1,54 @@
+// 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 aip0165
+
+import (
+	"strings"
+	"testing"
+
+	"github.com/googleapis/api-linter/rules/internal/testutils"
+	"github.com/jhump/protoreflect/desc"
+)
+
+func TestFilterField(t *testing.T) {
+	for _, test := range []struct {
+		name     string
+		Message  string
+		Field    string
+		problems testutils.Problems
+	}{
+		{"Valid", "PurgeBooksRequest", "string filter = 1;", nil},
+		{"Missing", "PurgeBooksRequest", "", testutils.Problems{{Message: "no `filter`"}}},
+		{"InvalidType", "PurgeBooksRequest", "int32 filter = 1;", testutils.Problems{{Suggestion: "string"}}},
+		{"InvalidTypeRepeated", "PurgeBooksRequest", "repeated string filter = 1;", testutils.Problems{{Suggestion: "string"}}},
+		{"IrrelevantMessage", "EnumerateBooksRequest", "", nil},
+	} {
+		t.Run(test.name, func(t *testing.T) {
+			f := testutils.ParseProto3Tmpl(t, `
+				message {{.Message}} {
+					{{.Field}}
+					repeated string names = 2;
+				}
+			`, test)
+			var d desc.Descriptor = f.GetMessageTypes()[0]
+			if strings.HasPrefix(test.name, "InvalidType") {
+				d = f.GetMessageTypes()[0].GetFields()[0]
+			}
+			if diff := test.problems.SetDescriptor(d).Diff(requestFilterField.Lint(f)); diff != "" {
+				t.Errorf(diff)
+			}
+		})
+	}
+}
diff --git a/rules/aip0165/request_force_field_test.go b/rules/aip0165/request_force_field_test.go
new file mode 100644
index 0000000..35d03ad
--- /dev/null
+++ b/rules/aip0165/request_force_field_test.go
@@ -0,0 +1,54 @@
+// 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 aip0165
+
+import (
+	"strings"
+	"testing"
+
+	"github.com/googleapis/api-linter/rules/internal/testutils"
+	"github.com/jhump/protoreflect/desc"
+)
+
+func TestForceField(t *testing.T) {
+	for _, test := range []struct {
+		name     string
+		Message  string
+		Field    string
+		problems testutils.Problems
+	}{
+		{"Valid", "PurgeBooksRequest", "bool force = 1;", nil},
+		{"Missing", "PurgeBooksRequest", "", testutils.Problems{{Message: "no `force`"}}},
+		{"InvalidType", "PurgeBooksRequest", "int32 force = 1;", testutils.Problems{{Suggestion: "bool"}}},
+		{"InvalidTypeRepeated", "PurgeBooksRequest", "repeated bool force = 1;", testutils.Problems{{Suggestion: "bool"}}},
+		{"IrrelevantMessage", "EnumerateBooksRequest", "", nil},
+	} {
+		t.Run(test.name, func(t *testing.T) {
+			f := testutils.ParseProto3Tmpl(t, `
+				message {{.Message}} {
+					{{.Field}}
+					repeated string names = 2;
+				}
+			`, test)
+			var d desc.Descriptor = f.GetMessageTypes()[0]
+			if strings.HasPrefix(test.name, "InvalidType") {
+				d = f.GetMessageTypes()[0].GetFields()[0]
+			}
+			if diff := test.problems.SetDescriptor(d).Diff(requestForceField.Lint(f)); diff != "" {
+				t.Errorf(diff)
+			}
+		})
+	}
+}
diff --git a/rules/aip0165/request_message_name_test.go b/rules/aip0165/request_message_name_test.go
new file mode 100644
index 0000000..3c43bef
--- /dev/null
+++ b/rules/aip0165/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 aip0165
+
+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", "PurgeBooks", "PurgeBooksRequest", nil},
+		{"Invalid", "PurgeBooks", "PurgeBookRequest", testutils.Problems{{Suggestion: "PurgeBooksRequest"}}},
+		{"Irrelevant", "AcquireBook", "Book", nil},
+	}
+
+	// Run each test individually.
+	for _, test := range tests {
+		t.Run(test.testName, func(t *testing.T) {
+			f := testutils.ParseProto3Tmpl(t, `
+				service Library {
+					rpc {{.MethodName}}({{.ReqMessageName}}) returns ({{.MethodName}}Response) {}
+				}
+				message {{.ReqMessageName}} {}
+				message {{.MethodName}}Response {}
+			`, 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/aip0165/request_parent_behavior_test.go b/rules/aip0165/request_parent_behavior_test.go
new file mode 100644
index 0000000..509b9fa
--- /dev/null
+++ b/rules/aip0165/request_parent_behavior_test.go
@@ -0,0 +1,47 @@
+// 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 aip0165
+
+import (
+	"testing"
+
+	"github.com/googleapis/api-linter/rules/internal/testutils"
+)
+
+func TestRequestParentBehavior(t *testing.T) {
+	for _, test := range []struct {
+		name          string
+		FieldName     string
+		FieldBehavior string
+		problems      testutils.Problems
+	}{
+		{"Valid", "parent", " [(google.api.field_behavior) = REQUIRED]", nil},
+		{"Missing", "parent", "", testutils.Problems{{Message: "(google.api.field_behavior) = REQUIRED"}}},
+		{"Irrelevant", "something_else", "", nil},
+	} {
+		t.Run(test.name, func(t *testing.T) {
+			f := testutils.ParseProto3Tmpl(t, `
+				import "google/api/field_behavior.proto";
+				message PurgeBooksRequest {
+					string {{.FieldName}} = 1{{.FieldBehavior}};
+				}
+			`, test)
+			field := f.GetMessageTypes()[0].GetFields()[0]
+			if diff := test.problems.SetDescriptor(field).Diff(requestParentBehavior.Lint(f)); diff != "" {
+				t.Errorf(diff)
+			}
+		})
+	}
+}
diff --git a/rules/aip0165/request_parent_field_test.go b/rules/aip0165/request_parent_field_test.go
new file mode 100644
index 0000000..7e66828
--- /dev/null
+++ b/rules/aip0165/request_parent_field_test.go
@@ -0,0 +1,79 @@
+// 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 aip0165
+
+import (
+	"testing"
+
+	"github.com/googleapis/api-linter/rules/internal/testutils"
+	"github.com/jhump/protoreflect/desc"
+)
+
+func TestParentField(t *testing.T) {
+	for _, test := range []struct {
+		name     string
+		Package  string
+		RPC      string
+		Field    string
+		Pattern  string
+		problems testutils.Problems
+	}{
+		{"Valid", "", "PurgeBooks", "string parent = 1;", "publishers/{p}/books", nil},
+		{"Missing", "", "PurgeBooks", "", "publishers/{p}/books", testutils.Problems{{Message: "no `parent`"}}},
+		{"InvalidType", "", "PurgeBooks", "int32 parent = 1;", "publishers/{p}/books", testutils.Problems{{Suggestion: "string"}}},
+		{"IrrelevantRPCName", "", "EnumerateBooks", "", "publishers/{p}/books", nil},
+		{"IrrelevantNoParent", "", "PurgeBooks", "", "books", nil},
+
+		{"PackageValid", "package foo;", "PurgeBooks", "string parent = 1;", "publishers/{p}/books", nil},
+		{"PackageMissing", "package foo;", "PurgeBooks", "", "publishers/{p}/books", testutils.Problems{{Message: "no `parent`"}}},
+		{"PackageInvalidType", "package foo;", "PurgeBooks", "int32 parent = 1;", "publishers/{p}/books", testutils.Problems{{Suggestion: "string"}}},
+		{"PackageIrrelevantRPCName", "package foo;", "EnumerateBooks", "", "publishers/{p}/books", nil},
+		{"PackageIrrelevantNoParent", "package foo;", "PurgeBooks", "", "books", nil},
+	} {
+		t.Run(test.name, func(t *testing.T) {
+			f := testutils.ParseProto3Tmpl(t, `
+				{{.Package}}
+				import "google/api/resource.proto";
+
+				service Library {
+					rpc {{.RPC}}({{.RPC}}Request) returns ({{.RPC}}Response);
+				}
+
+				message {{.RPC}}Request {
+					{{.Field}}
+					repeated string names = 2;
+				}
+
+				message {{.RPC}}Response {
+					repeated Book books = 1;
+				}
+
+				message Book {
+					option (google.api.resource) = {
+						pattern: "{{.Pattern}}";
+					};
+					string name = 1;
+				}
+			`, test)
+			var d desc.Descriptor = f.GetMessageTypes()[0]
+			if test.name == "InvalidType" || test.name == "PackageInvalidType" {
+				d = f.GetMessageTypes()[0].GetFields()[0]
+			}
+			if diff := test.problems.SetDescriptor(d).Diff(requestParentField.Lint(f)); diff != "" {
+				t.Errorf(diff)
+			}
+		})
+	}
+}
diff --git a/rules/aip0165/request_parent_reference_test.go b/rules/aip0165/request_parent_reference_test.go
new file mode 100644
index 0000000..d09a01c
--- /dev/null
+++ b/rules/aip0165/request_parent_reference_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 aip0165
+
+import (
+	"testing"
+
+	"github.com/googleapis/api-linter/rules/internal/testutils"
+)
+
+func TestRequestParentReference(t *testing.T) {
+	for _, test := range []struct {
+		name        string
+		MessageName string
+		FieldName   string
+		FieldOpts   string
+		problems    testutils.Problems
+	}{
+		{"Valid", "PurgeBooks", "parent", ` [(google.api.resource_reference) = { type: "foo" }]`, nil},
+		{"Missing", "PurgeBooks", "parent", "", testutils.Problems{{Message: "google.api.resource_reference"}}},
+		{"IrrelevantMessage", "DeleteBooks", "parent", "", nil},
+		{"IrrelevantField", "PurgeBooks", "names", "", nil},
+	} {
+		t.Run(test.name, func(t *testing.T) {
+			f := testutils.ParseProto3Tmpl(t, `
+				import "google/api/resource.proto";
+				message {{.MessageName}}Request {
+					repeated string {{.FieldName}} = 1{{.FieldOpts}};
+				}
+			`, test)
+			field := f.GetMessageTypes()[0].GetFields()[0]
+			if diff := test.problems.SetDescriptor(field).Diff(requestParentReference.Lint(f)); diff != "" {
+				t.Errorf(diff)
+			}
+		})
+	}
+}
diff --git a/rules/aip0165/response_purge_count_field_test.go b/rules/aip0165/response_purge_count_field_test.go
new file mode 100644
index 0000000..6de43f7
--- /dev/null
+++ b/rules/aip0165/response_purge_count_field_test.go
@@ -0,0 +1,54 @@
+// 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 aip0165
+
+import (
+	"strings"
+	"testing"
+
+	"github.com/googleapis/api-linter/rules/internal/testutils"
+	"github.com/jhump/protoreflect/desc"
+)
+
+func TestResponsePurgeCountField(t *testing.T) {
+	for _, test := range []struct {
+		name     string
+		RPC      string
+		Field    string
+		problems testutils.Problems
+	}{
+		{"Valid", "PurgeBooks", "int32 purge_count = 1;", nil},
+		{"Missing", "PurgeBooks", "", testutils.Problems{{Message: "no `purge_count`"}}},
+		{"InvalidType", "PurgeBooks", "int64 purge_count = 1;", testutils.Problems{{Suggestion: "int32"}}},
+		{"InvalidTypeRepeated", "PurgeBooks", "repeated int32 purge_count = 1;", testutils.Problems{{Suggestion: "int32"}}},
+		{"IrrelevantMessage", "EnumerateBooks", "", nil},
+	} {
+		t.Run(test.name, func(t *testing.T) {
+			f := testutils.ParseProto3Tmpl(t, `
+				message {{.RPC}}Response {
+					{{.Field}}
+					repeated string names = 2;
+				}
+			`, test)
+			var d desc.Descriptor = f.GetMessageTypes()[0]
+			if strings.HasPrefix(test.name, "InvalidType") {
+				d = f.GetMessageTypes()[0].GetFields()[0]
+			}
+			if diff := test.problems.SetDescriptor(d).Diff(responsePurgeCountField.Lint(f)); diff != "" {
+				t.Errorf(diff)
+			}
+		})
+	}
+}
diff --git a/rules/aip0165/response_purge_sample_field_test.go b/rules/aip0165/response_purge_sample_field_test.go
new file mode 100644
index 0000000..2291fbb
--- /dev/null
+++ b/rules/aip0165/response_purge_sample_field_test.go
@@ -0,0 +1,54 @@
+// 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 aip0165
+
+import (
+	"strings"
+	"testing"
+
+	"github.com/googleapis/api-linter/rules/internal/testutils"
+	"github.com/jhump/protoreflect/desc"
+)
+
+func TestResponsePurgeSampleField(t *testing.T) {
+	for _, test := range []struct {
+		name     string
+		RPC      string
+		Field    string
+		problems testutils.Problems
+	}{
+		{"Valid", "PurgeBooks", "repeated string purge_sample = 1;", nil},
+		{"Missing", "PurgeBooks", "", testutils.Problems{{Message: "no `purge_sample`"}}},
+		{"InvalidType", "PurgeBooks", "repeated int64 purge_sample = 1;", testutils.Problems{{Suggestion: "repeated string"}}},
+		{"InvalidTypeNotRepeated", "PurgeBooks", "string purge_sample = 1;", testutils.Problems{{Suggestion: "repeated string"}}},
+		{"IrrelevantMessage", "EnumerateBooks", "", nil},
+	} {
+		t.Run(test.name, func(t *testing.T) {
+			f := testutils.ParseProto3Tmpl(t, `
+				message {{.RPC}}Response {
+					{{.Field}}
+					int32 purge_count = 2;
+				}
+			`, test)
+			var d desc.Descriptor = f.GetMessageTypes()[0]
+			if strings.HasPrefix(test.name, "InvalidType") {
+				d = f.GetMessageTypes()[0].GetFields()[0]
+			}
+			if diff := test.problems.SetDescriptor(d).Diff(responsePurgeSampleField.Lint(f)); diff != "" {
+				t.Errorf(diff)
+			}
+		})
+	}
+}
diff --git a/rules/aip0165/response_purge_sample_reference_test.go b/rules/aip0165/response_purge_sample_reference_test.go
new file mode 100644
index 0000000..c44d6b5
--- /dev/null
+++ b/rules/aip0165/response_purge_sample_reference_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 aip0165
+
+import (
+	"testing"
+
+	"github.com/googleapis/api-linter/rules/internal/testutils"
+)
+
+func TestResponsePurgeSampleReference(t *testing.T) {
+	for _, test := range []struct {
+		name        string
+		MessageName string
+		FieldName   string
+		FieldOpts   string
+		problems    testutils.Problems
+	}{
+		{"Valid", "PurgeBooks", "purge_sample", ` [(google.api.resource_reference) = { type: "foo" }]`, nil},
+		{"Missing", "PurgeBooks", "purge_sample", "", testutils.Problems{{Message: "google.api.resource_reference"}}},
+		{"IrrelevantMessage", "DeleteBooks", "purge_sample", "", nil},
+		{"IrrelevantField", "PurgeBooks", "names", "", nil},
+	} {
+		t.Run(test.name, func(t *testing.T) {
+			f := testutils.ParseProto3Tmpl(t, `
+				import "google/api/resource.proto";
+				message {{.MessageName}}Response {
+					repeated string {{.FieldName}} = 1{{.FieldOpts}};
+				}
+			`, test)
+			field := f.GetMessageTypes()[0].GetFields()[0]
+			if diff := test.problems.SetDescriptor(field).Diff(responsePurgeSampleReference.Lint(f)); diff != "" {
+				t.Errorf(diff)
+			}
+		})
+	}
+}