Repository: googleapis/api-linter
Pull request #210 (2019-10-03 21:59:44)

[AIP-133] Add rules for Create methods.
AIP-0133 create


Patch:
diff --git a/rules/aip0133/aip0133.go b/rules/aip0133/aip0133.go
new file mode 100644
index 0000000..a2c06b5
--- /dev/null
+++ b/rules/aip0133/aip0133.go
@@ -0,0 +1,84 @@
+// 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 aip0133 contains rules defined in https://aip.dev/133.
+package aip0133
+
+import (
+	"regexp"
+	"strings"
+
+	"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) {
+	r.Register(
+		httpUriField,
+		httpVerb,
+		httpBody,
+		inputName,
+		outputName,
+		resourceField,
+		parentField,
+		unknownFields,
+	)
+}
+
+var createMethodRegexp = regexp.MustCompile("^Create(?:[A-Z]|$)")
+var createReqMessageRegexp = regexp.MustCompile("^Create[A-Za-z0-9]*Request$")
+var createURINameRegexp = regexp.MustCompile("\\{parent=[a-zA-Z/*]+\\}$")
+
+// Returns true if this is a AIP-133 Create method, false otherwise.
+func isCreateMethod(m *desc.MethodDescriptor) bool {
+	return createMethodRegexp.MatchString(m.GetName())
+}
+
+// Returns true if this is an AIP-133 Get request message, false otherwise.
+func isCreateRequestMessage(m *desc.MessageDescriptor) bool {
+	return createReqMessageRegexp.MatchString(m.GetName())
+}
+
+// get resource message type name from method
+func getResourceMsgName(m *desc.MethodDescriptor) string {
+	// Usually the response message will be the resource message, and its name will
+	// be part of method name (make a double check here to avoid the issue when
+	// method or output naming doesn't follow the right principles)
+	if strings.Contains(m.GetName()[6:], m.GetOutputType().GetName()) {
+		return m.GetOutputType().GetName()
+	} else {
+		return m.GetName()[6:]
+	}
+}
+
+// get resource message type name from request message
+func getResourceMsgNameFromReq(m *desc.MessageDescriptor) string {
+	// retrieve the string between the prefix "Create" and suffix "Request" from
+	// the name "Create<XXX>Request", and this part will usually be the resource
+	// message name(if its naming follows the right principle)
+	resourceMsgName := m.GetName()[6 : len(m.GetName())-7]
+
+	// Get the resource field of the request message if it exist, this part will
+	// be exactly the resource message name (make a double check here to avoid the
+	// issues when request message naming doesn't follow the right principles)
+	for _, fieldDesc := range m.GetFields() {
+		if msgDesc := fieldDesc.GetMessageType(); msgDesc != nil && strings.Contains(resourceMsgName, msgDesc.GetName()) {
+			resourceMsgName = msgDesc.GetName()
+		}
+	}
+
+	return resourceMsgName
+}
diff --git a/rules/aip0133/message_rules.go b/rules/aip0133/message_rules.go
new file mode 100644
index 0000000..f8d913e
--- /dev/null
+++ b/rules/aip0133/message_rules.go
@@ -0,0 +1,112 @@
+// 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 aip0133
+
+import (
+	"fmt"
+	"strings"
+
+	"github.com/googleapis/api-linter/lint"
+	"github.com/jhump/protoreflect/desc"
+	"github.com/jhump/protoreflect/desc/builder"
+	"github.com/stoewer/go-strcase"
+)
+
+// The create request message should have parent field.
+var parentField = &lint.MessageRule{
+	Name:   lint.NewRuleName("core", "0133", "request-message", "parent-field"),
+	URI:    "https://aip.dev/133#request-message",
+	OnlyIf: isCreateRequestMessage,
+	LintMessage: func(m *desc.MessageDescriptor) []lint.Problem {
+		// Rule check: Establish that a `parent` field is present.
+		parentField := m.FindFieldByName("parent")
+		if parentField == nil {
+			return []lint.Problem{{
+				Message:    fmt.Sprintf("Message %q has no `parent` field", m.GetName()),
+				Descriptor: m,
+			}}
+		}
+
+		// Rule check: Establish that the parent field is a string.
+		if parentField.GetType() != builder.FieldTypeString().GetType() {
+			return []lint.Problem{{
+				Message:    "`parent` field on create request message must be a string",
+				Descriptor: parentField,
+			}}
+		}
+
+		return nil
+	},
+}
+
+// The create request message should have resource field.
+var resourceField = &lint.MessageRule{
+	Name:   lint.NewRuleName("core", "0133", "request-message", "resource-field"),
+	URI:    "https://aip.dev/133#request-message",
+	OnlyIf: isCreateRequestMessage,
+	LintMessage: func(m *desc.MessageDescriptor) []lint.Problem {
+		resourceMsgName := getResourceMsgNameFromReq(m)
+
+		// The rule (resource field name must map to the POST body) is checked by AIP-0133 ("core::0133::http-body")
+		for _, fieldDesc := range m.GetFields() {
+			if msgDesc := fieldDesc.GetMessageType(); msgDesc != nil && msgDesc.GetName() == resourceMsgName {
+				return nil
+			}
+		}
+
+		// Rule check: Establish that a resource field must be included.
+		return []lint.Problem{{
+			Message:    fmt.Sprintf("Message %q has no %q type field", m.GetName(), resourceMsgName),
+			Descriptor: m,
+		}}
+	},
+}
+
+// The create request message should not have unrecognized fields.
+var unknownFields = &lint.MessageRule{
+	Name:   lint.NewRuleName("core", "0133", "request-message", "unknown-fields"),
+	URI:    "https://aip.dev/133#request-message",
+	OnlyIf: isCreateRequestMessage,
+	LintMessage: func(m *desc.MessageDescriptor) (problems []lint.Problem) {
+		resourceMsgName := getResourceMsgNameFromReq(m)
+
+		// Rule check: Establish that there are no unexpected fields.
+		allowedFields := map[string]*builder.FieldType{
+			"parent":     nil, // AIP-133
+			"request_id": nil, // AIP-155
+			fmt.Sprintf("%s_id", strings.ToLower(strcase.SnakeCase(resourceMsgName))): nil,
+		}
+
+		for _, fieldDesc := range m.GetFields() {
+			if msgDesc := fieldDesc.GetMessageType(); msgDesc != nil && msgDesc.GetName() == resourceMsgName {
+				allowedFields[fieldDesc.GetName()] = nil // AIP-133
+			}
+		}
+
+		for _, field := range m.GetFields() {
+			if _, ok := allowedFields[string(field.GetName())]; !ok {
+				problems = append(problems, lint.Problem{
+					Message: fmt.Sprintf(
+						"Create RPCs must only contain fields explicitly described in AIPs, not %q.",
+						field.GetName(),
+					),
+					Descriptor: field,
+				})
+			}
+		}
+
+		return
+	},
+}
diff --git a/rules/aip0133/method_rules.go b/rules/aip0133/method_rules.go
new file mode 100644
index 0000000..36feb5c
--- /dev/null
+++ b/rules/aip0133/method_rules.go
@@ -0,0 +1,171 @@
+// 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 aip0133
+
+import (
+	"fmt"
+	"strings"
+
+	"github.com/googleapis/api-linter/lint"
+	"github.com/googleapis/api-linter/rules/internal/utils"
+	"github.com/jhump/protoreflect/desc"
+)
+
+// Create methods should use the HTTP POST verb.
+var httpVerb = &lint.MethodRule{
+	Name:   lint.NewRuleName("core", "0133", "http-verb"),
+	URI:    "https://aip.dev/133#guidance",
+	OnlyIf: isCreateMethod,
+	LintMethod: func(m *desc.MethodDescriptor) []lint.Problem {
+		// Rule check: Establish that the RPC uses HTTP POST.
+		for _, httpRule := range utils.GetHTTPRules(m) {
+			if httpRule.GetPost() == "" {
+				return []lint.Problem{{
+					Message:    "Create methods must use the HTTP POST verb.",
+					Descriptor: m,
+				}}
+			}
+		}
+
+		return nil
+	},
+}
+
+// Create methods should have a proper HTTP pattern.
+var httpUriField = &lint.MethodRule{
+	Name:   lint.NewRuleName("core", "0133", "http-uri"),
+	URI:    "https://aip.dev/133#guidance",
+	OnlyIf: isCreateMethod,
+	LintMethod: func(m *desc.MethodDescriptor) []lint.Problem {
+		// Establish that the RPC uri should include the `parent` field.
+		for _, httpRule := range utils.GetHTTPRules(m) {
+			if uri := httpRule.GetPost(); uri != "" {
+				if !createURINameRegexp.MatchString(uri) {
+					return []lint.Problem{{
+						Message:    "Post methods should include the `parent` field in the URI.",
+						Descriptor: m,
+					}}
+				}
+			}
+		}
+
+		return nil
+	},
+}
+
+// Create methods should have an HTTP body, and the body value should be resource.
+var httpBody = &lint.MethodRule{
+	Name:   lint.NewRuleName("core", "0133", "http-body"),
+	URI:    "https://aip.dev/133#guidance",
+	OnlyIf: isCreateMethod,
+	LintMethod: func(m *desc.MethodDescriptor) []lint.Problem {
+		// We only care about Create methods for the purpose of this rule;
+		// ignore everything else.
+		if !isCreateMethod(m) {
+			return nil
+		}
+
+		resourceMsgName := getResourceMsgName(m)
+		resourceFieldName := strings.ToLower(resourceMsgName)
+		for _, fieldDesc := range m.GetInputType().GetFields() {
+			// when msgDesc is nil, the resource field in the request message is
+			// missing. A lint warning for the rule `resourceField` will be generated.
+			// For here, we will use the lower case resource message name as default
+			if msgDesc := fieldDesc.GetMessageType(); msgDesc != nil && msgDesc.GetName() == resourceMsgName {
+				resourceFieldName = fieldDesc.GetName()
+			}
+		}
+
+		// Establish that HTTP body the RPC should map the resource field name in the request message.
+		for _, httpRule := range utils.GetHTTPRules(m) {
+			if body := httpRule.GetBody(); body == "" {
+				// Establish that the RPC should have HTTP body
+				return []lint.Problem{{
+					Message:    "Post methods should have an HTTP body.",
+					Descriptor: m,
+				}}
+				// When resource field is not set in the request message, the problem
+				// will not be triggered by the rule"core::0133::http-body". It will be
+				// triggered by another rule
+				// "core::0133::request-message::resource-field"
+			} else if resourceFieldName != "" && body != resourceFieldName {
+				return []lint.Problem{{
+					Message:    fmt.Sprintf("The content of body %q must map to the resource field %q in the request message", body, resourceFieldName),
+					Descriptor: m,
+				}}
+			}
+		}
+		return nil
+	},
+}
+
+// Create method should have a properly named input message.
+var inputName = &lint.MethodRule{
+	Name:   lint.NewRuleName("core", "0133", "request-message", "name"),
+	URI:    "https://aip.dev/133#guidance",
+	OnlyIf: isCreateMethod,
+	LintMethod: func(m *desc.MethodDescriptor) []lint.Problem {
+		resourceMsgName := getResourceMsgName(m)
+
+		// Rule check: Establish that for methods such as `CreateFoo`, the request
+		// message is named `CreateFooRequest`.
+		if got, want := m.GetInputType().GetName(), fmt.Sprintf("Create%sRequest", resourceMsgName); got != want {
+			return []lint.Problem{{
+				Message: fmt.Sprintf(
+					"Post RPCs should have a properly named request message %q, but not %q",
+					want, got,
+				),
+				// TODO: suggestion will be set after the location is set properly
+				// Suggestion: want,
+				Descriptor: m,
+			}}
+		}
+
+		return nil
+	},
+}
+
+// Create method should use the resource as the output message
+var outputName = &lint.MethodRule{
+	Name:   lint.NewRuleName("core", "0133", "response-message", "name"),
+	URI:    "https://aip.dev/133#guidance",
+	OnlyIf: isCreateMethod,
+	LintMethod: func(m *desc.MethodDescriptor) []lint.Problem {
+		output := m.GetOutputType()
+
+		// AIP-0151
+		if output.GetFullyQualifiedName() == "google.longrunning.Operation" {
+			return nil
+		}
+
+		resourceMsgName := getResourceMsgName(m)
+
+		// Rule check: Establish that for methods such as `CreateFoo`, the response
+		// message should be named `Foo`
+		if output.GetName() != resourceMsgName {
+			return []lint.Problem{{
+				Message: fmt.Sprintf(
+					"Create RPCs should have the corresponding resource as the response message, such as %q.",
+					resourceMsgName,
+				),
+				// TODO: suggestion will be set after the location is set properly
+				// Suggestion: want,
+				Descriptor: m,
+			}}
+		}
+
+		return nil
+	},
+}


Test patch:
diff --git a/rules/aip0133/aip0133_test.go b/rules/aip0133/aip0133_test.go
new file mode 100644
index 0000000..a976567
--- /dev/null
+++ b/rules/aip0133/aip0133_test.go
@@ -0,0 +1,32 @@
+// 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 aip0133
+
+import (
+	"strings"
+	"testing"
+
+	"github.com/googleapis/api-linter/lint"
+)
+
+func TestAddRules(t *testing.T) {
+	rules := make(lint.RuleRegistry)
+	AddRules(rules)
+	for ruleName := range rules {
+		if !strings.HasPrefix(string(ruleName), "core::0133") {
+			t.Errorf("Rule %s is not namespaced to core::0133.", ruleName)
+		}
+	}
+}
diff --git a/rules/aip0133/message_rules_test.go b/rules/aip0133/message_rules_test.go
new file mode 100644
index 0000000..d32a5c6
--- /dev/null
+++ b/rules/aip0133/message_rules_test.go
@@ -0,0 +1,237 @@
+// 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 aip0133
+
+import (
+	"testing"
+
+	"github.com/googleapis/api-linter/rules/internal/testutils"
+	"github.com/jhump/protoreflect/desc"
+	"github.com/jhump/protoreflect/desc/builder"
+)
+
+type field struct {
+	fieldName string
+	fieldType *builder.FieldType
+}
+
+func TestParentField(t *testing.T) {
+	// Set up the testing permutations.
+	tests := []struct {
+		testName     string
+		messageName  string
+		messageField *field
+		problems     testutils.Problems
+		problemDesc  func(m *desc.MessageDescriptor) desc.Descriptor
+	}{
+		{
+			"Valid",
+			"CreateBookRequest",
+			&field{"parent", builder.FieldTypeString()},
+			testutils.Problems{},
+			nil},
+		{
+			"MissingField",
+			"CreateBookRequest",
+			nil,
+			testutils.Problems{{Message: "parent"}},
+			nil,
+		},
+		{
+			"InvalidType",
+			"CreateBookRequest",
+			&field{"parent", builder.FieldTypeDouble()},
+			testutils.Problems{{Message: "string"}},
+			func(m *desc.MessageDescriptor) desc.Descriptor {
+				return m.FindFieldByName("parent")
+			},
+		},
+	}
+
+	// Run each test individually.
+	for _, test := range tests {
+		t.Run(test.testName, func(t *testing.T) {
+			// Create an appropriate message descriptor.
+			messageBuilder := builder.NewMessage(test.messageName)
+
+			if test.messageField != nil {
+				messageBuilder.AddField(
+					builder.NewField(test.messageField.fieldName, test.messageField.fieldType),
+				)
+			}
+
+			message, err := messageBuilder.Build()
+			if err != nil {
+				t.Fatalf("Could not build %s message.", test.messageName)
+			}
+
+			// Determine the descriptor that a failing test will attach to.
+			var problemDesc desc.Descriptor = message
+			if test.problemDesc != nil {
+				problemDesc = test.problemDesc(message)
+			}
+
+			// Run the lint rule, and establish that it returns the correct problems.
+			problems := parentField.Lint(message.GetFile())
+			if diff := test.problems.SetDescriptor(problemDesc).Diff(problems); diff != "" {
+				t.Errorf(diff)
+			}
+		})
+	}
+}
+
+func TestResourceField(t *testing.T) {
+	// Set up the testing permutations.
+	tests := []struct {
+		testName     string
+		messageName  string
+		messageField *field
+		problems     testutils.Problems
+	}{
+		{
+			"Valid",
+			"CreateBookRequest",
+			&field{"book", builder.FieldTypeMessage(builder.NewMessage("Book"))},
+			testutils.Problems{},
+		},
+		{
+			"MissingField",
+			"CreateBookRequest",
+			nil,
+			testutils.Problems{{Message: "Message \"CreateBookRequest\" has no \"Book\" type field"}},
+		},
+	}
+
+	// Run each test individually.
+	for _, test := range tests {
+		t.Run(test.testName, func(t *testing.T) {
+			// Create an appropriate message descriptor.
+			messageBuilder := builder.NewMessage(test.messageName)
+
+			if test.messageField != nil {
+				messageBuilder.AddField(
+					builder.NewField(test.messageField.fieldName, test.messageField.fieldType),
+				)
+			}
+
+			message, err := messageBuilder.Build()
+			if err != nil {
+				t.Fatalf("Could not build %s message.", test.messageName)
+			}
+
+			// Run the lint rule, and establish that it returns the correct problems.
+			problems := resourceField.Lint(message.GetFile())
+			if diff := test.problems.SetDescriptor(message).Diff(problems); diff != "" {
+				t.Errorf(diff)
+			}
+		})
+	}
+}
+
+func TestUnknownFields(t *testing.T) {
+	// Set up the testing permutations.
+	tests := []struct {
+		testName      string
+		messageName   string
+		messageFields []field
+		problems      testutils.Problems
+		problemDesc   func(m *desc.MessageDescriptor) desc.Descriptor
+	}{
+		{
+			"Parent",
+			"CreateBookRequest",
+			[]field{{"parent", builder.FieldTypeString()}},
+			testutils.Problems{},
+			nil,
+		},
+		{
+			"ResourceRelatedField",
+			"CreateBookRequest",
+			[]field{
+				{"book", builder.FieldTypeMessage(builder.NewMessage("Book"))},
+				{"book_id", builder.FieldTypeString()}},
+			testutils.Problems{},
+			nil,
+		},
+		{
+			"ResourceRelatedField",
+			"CreateBookStoreRequest",
+			[]field{{"book_store_id", builder.FieldTypeString()}},
+			testutils.Problems{},
+			nil,
+		},
+		{
+			"RequestIdField",
+			"CreateBookRequest",
+			[]field{{"request_id", builder.FieldTypeString()}},
+			testutils.Problems{},
+			nil,
+		},
+		{
+			"Invalid",
+			"CreateBookRequest",
+			[]field{{"name", builder.FieldTypeString()}},
+			testutils.Problems{{Message: "Create RPCs must only contain fields explicitly described in AIPs, not \"name\"."}},
+			func(m *desc.MessageDescriptor) desc.Descriptor {
+				return m.FindFieldByName("name")
+			},
+		},
+		{
+			"InvalidResourceRelatedField",
+			"CreateBookStoreRequest",
+			[]field{{"book_id", builder.FieldTypeString()}},
+			testutils.Problems{{Message: "Create RPCs must only contain fields explicitly described in AIPs, not \"book_id\"."}},
+			func(m *desc.MessageDescriptor) desc.Descriptor {
+				return m.FindFieldByName("book_id")
+			},
+		},
+		{
+			"Irrelevant",
+			"GetBookRequest",
+			[]field{{"name", builder.FieldTypeString()}},
+			testutils.Problems{},
+			nil,
+		},
+	}
+
+	// Run each test individually.
+	for _, test := range tests {
+		t.Run(test.testName, func(t *testing.T) {
+			// Create an appropriate message descriptor.
+			msgBuilder := builder.NewMessage(test.messageName)
+			for _, messageField := range test.messageFields {
+				msgBuilder.AddField(
+					builder.NewField(messageField.fieldName, messageField.fieldType),
+				)
+			}
+			message, err := msgBuilder.Build()
+			if err != nil {
+				t.Fatalf("Could not build GetBookRequest message.")
+			}
+
+			// Determine the descriptor that a failing test will attach to.
+			var problemDesc desc.Descriptor = message
+			if test.problemDesc != nil {
+				problemDesc = test.problemDesc(message)
+			}
+
+			// Run the lint rule, and establish that it returns the correct problems.
+			problems := unknownFields.Lint(message.GetFile())
+			if diff := test.problems.SetDescriptor(problemDesc).Diff(problems); diff != "" {
+				t.Errorf(diff)
+			}
+		})
+	}
+}
diff --git a/rules/aip0133/method_rules_test.go b/rules/aip0133/method_rules_test.go
new file mode 100644
index 0000000..45d67b4
--- /dev/null
+++ b/rules/aip0133/method_rules_test.go
@@ -0,0 +1,265 @@
+// 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 aip0133
+
+import (
+	"strings"
+	"testing"
+
+	"github.com/golang/protobuf/proto"
+	dpb "github.com/golang/protobuf/protoc-gen-go/descriptor"
+	"github.com/jhump/protoreflect/desc/builder"
+	"google.golang.org/genproto/googleapis/api/annotations"
+)
+
+func TestHttpVerb(t *testing.T) {
+	// Set up GET and POST HTTP annotations.
+	httpGet := &annotations.HttpRule{
+		Pattern: &annotations.HttpRule_Get{
+			Get: "/v1/{name=publishers/*/books/*}",
+		},
+	}
+	httpPost := &annotations.HttpRule{
+		Pattern: &annotations.HttpRule_Post{
+			Post: "/v1/{=publishers/*/books/*}",
+		},
+	}
+
+	// Set up testing permutations.
+	tests := []struct {
+		testName   string
+		httpRule   *annotations.HttpRule
+		methodName string
+		msg        string
+	}{
+		{"Valid", httpPost, "CreateBook", ""},
+		{"Invalid", httpGet, "CreateBook", "HTTP POST"},
+		{"Irrelevant", httpPost, "AcquireBook", ""},
+	}
+
+	// Run each test.
+	for _, test := range tests {
+		t.Run(test.testName, func(t *testing.T) {
+			// Create a MethodOptions with the annotation set.
+			opts := &dpb.MethodOptions{}
+			if err := proto.SetExtension(opts, annotations.E_Http, test.httpRule); err != nil {
+				t.Fatalf("Failed to set google.api.http annotation.")
+			}
+
+			// Create a minimal service with a AIP-133 Create method
+			// (or with a different method, in the "Irrelevant" case).
+			service, err := builder.NewService("Library").AddMethod(builder.NewMethod(test.methodName,
+				builder.RpcTypeMessage(builder.NewMessage("CreateBookRequest"), false),
+				builder.RpcTypeMessage(builder.NewMessage("Book"), false),
+			).SetOptions(opts)).Build()
+			if err != nil {
+				t.Fatalf("Could not build %s method.", test.methodName)
+			}
+
+			// Run the method, ensure we get what we expect.
+			problems := httpVerb.Lint(service.GetFile())
+			if test.msg == "" && len(problems) > 0 {
+				t.Errorf("Got %v, expected no problems.", problems)
+			} else if test.msg != "" && !strings.Contains(problems[0].Message, test.msg) {
+				t.Errorf("Got %q, expected message containing %q", problems[0].Message, test.msg)
+			}
+		})
+	}
+}
+
+func TestHttpUriField(t *testing.T) {
+	tests := []struct {
+		testName   string
+		uri        string
+		methodName string
+		msg        string
+	}{
+		{"Valid", "/v1/{parent=publishers/*/books/*}", "CreateBook", ""},
+		{"InvalidVarParent", "/v1/{book=publishers/*/books/*}", "CreateBook", "`parent` field"},
+		{"NoVarParent", "/v1/publishers/*/books/*", "CreateBook", "`parent` field"},
+		{"Irrelevant", "/v1/{book=publishers/*/books/*}", "BuildBook", ""},
+	}
+
+	for _, test := range tests {
+		t.Run(test.testName, func(t *testing.T) {
+			// Create a MethodOptions with the annotation set.
+			opts := &dpb.MethodOptions{}
+			httpRule := &annotations.HttpRule{
+				Pattern: &annotations.HttpRule_Post{
+					Post: test.uri,
+				},
+			}
+			if err := proto.SetExtension(opts, annotations.E_Http, httpRule); err != nil {
+				t.Fatalf("Failed to set google.api.http annotation.")
+			}
+
+			// Create a minimal service with a AIP-133 Create method
+			// (or with a different method, in the "Irrelevant" case).
+			service, err := builder.NewService("Library").AddMethod(builder.NewMethod(test.methodName,
+				builder.RpcTypeMessage(builder.NewMessage("CreateBookRequest"), false),
+				builder.RpcTypeMessage(builder.NewMessage("Book"), false),
+			).SetOptions(opts)).Build()
+			if err != nil {
+				t.Fatalf("Could not build %s method.", test.methodName)
+			}
+
+			// Run the method, ensure we get what we expect.
+			problems := httpUriField.Lint(service.GetFile())
+			if test.msg == "" && len(problems) > 0 {
+				t.Errorf("Got %v, expected no problems.", problems)
+			} else if test.msg != "" && len(problems) == 0 {
+				t.Errorf("Got no problems, expected 1.")
+			} else if test.msg != "" && !strings.Contains(problems[0].Message, test.msg) {
+				t.Errorf("Got %q, expected message containing %q", problems[0].Message, test.msg)
+			}
+		})
+	}
+}
+
+func TestHttpBody(t *testing.T) {
+	tests := []struct {
+		testName      string
+		resourceField string
+		body          string
+		methodName    string
+		msg           string
+	}{
+		{"Valid", "book", "book", "CreateBook", ""},
+		{"Valid", "textbook", "textbook", "CreateBook", ""},
+		{"Valid", "", "book", "CreateBook", ""}, // valid for http body rule check, but it will fail under resource fail rule check
+		{"Invalid_BodyMissing", "book", "", "CreateBook", "Post methods should have an HTTP body"},
+		{"Invalid_BodyMismatch", "book", "abook", "CreateBook", "The content of body \"abook\" must map to the resource field \"book\" in the request message"},
+		{"Irrelevant", "book", "book", "CreateBook", ""},
+	}
+
+	for _, test := range tests {
+		t.Run(test.testName, func(t *testing.T) {
+			// Create a MethodOptions with the annotation set.
+			opts := &dpb.MethodOptions{}
+			httpRule := &annotations.HttpRule{
+				Pattern: &annotations.HttpRule_Post{
+					Post: "/v1/{parent=publishers/*/books/*}",
+				},
+				Body: test.body,
+			}
+			if err := proto.SetExtension(opts, annotations.E_Http, httpRule); err != nil {
+				t.Fatalf("Failed to set google.api.http annotation.")
+			}
+
+			book, err := builder.NewMessage("Book").Build()
+
+			if err != nil {
+				t.Fatalf("Failed to create resource 'Book' message for test.")
+			}
+
+			// Create a minimal service with a AIP-133 Create method
+			// (or with a different method, in the "Irrelevant" case).
+			service, err := builder.NewService("Library").AddMethod(builder.NewMethod(test.methodName,
+				builder.RpcTypeMessage(builder.NewMessage("CreateBookRequest").AddField(builder.NewField(test.resourceField, builder.FieldTypeImportedMessage(book))), false),
+				builder.RpcTypeMessage(builder.NewMessage("Book"), false),
+			).SetOptions(opts)).Build()
+			if err != nil {
+				t.Fatalf("Could not build %s method.", test.methodName)
+			}
+
+			// Run the method, ensure we get what we expect.
+			problems := httpBody.Lint(service.GetFile())
+			if test.msg == "" && len(problems) > 0 {
+				t.Errorf("Got %v, expected no problems.", problems)
+			} else if test.msg != "" && !strings.Contains(problems[0].Message, test.msg) {
+				t.Errorf("Got %q, expected message containing %q", problems[0].Message, test.msg)
+			}
+		})
+	}
+}
+
+func TestInputName(t *testing.T) {
+	// Set up the testing permutations.
+	tests := []struct {
+		testName   string
+		methodName string
+		inputName  string
+		msg        string
+	}{
+		{"Valid", "CreateBook", "CreateBookRequest", ""},
+		{"Invalid", "CreateBook", "Book",
+			"Post RPCs should have a properly named request message \"CreateBookRequest\", but not \"Book\""},
+		{"Irrelevant_OutputWrong", "CreateIamPolicy", "CreateIamPolicyRequest", ""},
+		{"Irrelevant_NotCreate", "BuildBook", "Book", ""},
+	}
+
+	// Run each test individually.
+	for _, test := range tests {
+		t.Run(test.testName, func(t *testing.T) {
+
+			// Create a minimal service with a AIP-133 Get method
+			// (or with a different method, in the "Irrelevant" case).
+			service, err := builder.NewService("Library").AddMethod(builder.NewMethod(test.methodName,
+				builder.RpcTypeMessage(builder.NewMessage(test.inputName), false),
+				builder.RpcTypeMessage(builder.NewMessage("Book"), false),
+			)).Build()
+			if err != nil {
+				t.Fatalf("Could not build %s method.", test.methodName)
+			}
+
+			// Run the lint rule, and establish that it returns the expected problems.
+			problems := inputName.Lint(service.GetFile())
+			if test.msg == "" && len(problems) > 0 {
+				t.Errorf("Got %v, expected no problems.", problems)
+			} else if test.msg != "" && !strings.Contains(problems[0].Message, test.msg) {
+				t.Errorf("Got %q, expected message containing %q", problems[0].Message, test.msg)
+			}
+		})
+	}
+}
+
+// Test does not cover the case when Output is "google.longrunning.Operation"
+func TestOutputMessageName(t *testing.T) {
+	// Set up the testing permutations.
+	tests := []struct {
+		testName   string
+		methodName string
+		outputName string
+		msg        string
+	}{
+		{"Valid", "CreateBook", "Book", ""},
+		{"Invalid", "CreateBook", "CreateBookResponse",
+			"Create RPCs should have the corresponding resource as the response message, such as \"Book\"."},
+		{"Irrelevant", "BuildBook", "BuildBookResponse", ""},
+	}
+
+	// Run each test individually.
+	for _, test := range tests {
+		t.Run(test.testName, func(t *testing.T) {
+			// Create a minimal service with a AIP-133 Get method
+			// (or with a different method, in the "Irrelevant" case).
+			service, err := builder.NewService("Library").AddMethod(builder.NewMethod(test.methodName,
+				builder.RpcTypeMessage(builder.NewMessage("GetBookRequest"), false),
+				builder.RpcTypeMessage(builder.NewMessage(test.outputName), false),
+			)).Build()
+			if err != nil {
+				t.Fatalf("Could not build %s method.", test.methodName)
+			}
+
+			// Run the lint rule, and establish that it returns the expected problems.
+			problems := outputName.Lint(service.GetFile())
+			if test.msg == "" && len(problems) > 0 {
+				t.Errorf("Got %v, expected no problems.", problems)
+			} else if test.msg != "" && !strings.Contains(problems[0].Message, test.msg) {
+				t.Errorf("Got %q, expected message containing %q", problems[0].Message, test.msg)
+			}
+		})
+	}
+}