Repository: googleapis/api-linter
Pull request #192 (2019-09-25 21:20:20)

Corp rules - aip0158
add the aip0158 pagination from corp to the github


Patch:
diff --git a/rules/aip0158/aip0158.go b/rules/aip0158/aip0158.go
new file mode 100644
index 0000000..1e94cb1
--- /dev/null
+++ b/rules/aip0158/aip0158.go
@@ -0,0 +1,45 @@
+// 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 aip0158 contains rules defined in https://aip.dev/158.
+package aip0158
+
+import (
+	"regexp"
+
+	"github.com/googleapis/api-linter/lint"
+	"github.com/jhump/protoreflect/desc"
+)
+
+// AddRules adds all of the AIP-158 rules to the provided registry.
+func AddRules(r lint.RuleRegistry) {
+	r.Register(
+		requestPaginationPageSize,
+		requestPaginationPageToken,
+		responsePaginationNextPageToken,
+	)
+}
+
+var listReqMessageRegexp = regexp.MustCompile("^List[A-Za-z0-9]*Request$")
+var listResMessageRegexp = regexp.MustCompile("^List[A-Za-z0-9]*Response$")
+
+// Return true if this is an AIP-158 List request message, false otherwise.
+func isListRequestMessage(m *desc.MessageDescriptor) bool {
+	return listReqMessageRegexp.MatchString(m.GetName())
+}
+
+// Return true if this is an AIP-158 List request message, false otherwise.
+func isListResponseMessage(m *desc.MessageDescriptor) bool {
+	return listResMessageRegexp.MatchString(m.GetName())
+}
diff --git a/rules/aip0158/pagination.go b/rules/aip0158/pagination.go
new file mode 100644
index 0000000..a45dd48
--- /dev/null
+++ b/rules/aip0158/pagination.go
@@ -0,0 +1,101 @@
+// 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 aip0158
+
+import (
+	"fmt"
+
+	"github.com/googleapis/api-linter/lint"
+	"github.com/jhump/protoreflect/desc"
+	"github.com/jhump/protoreflect/desc/builder"
+)
+
+var requestPaginationPageSize = &lint.MessageRule{
+	Name:   lint.NewRuleName("core", "0158", "request-message", "page-size-field"),
+	URI:    "https://aip.dev/158",
+	OnlyIf: isListRequestMessage,
+	LintMessage: func(m *desc.MessageDescriptor) (problems []lint.Problem) {
+		// Rule check: Establish that a page_size field is present.
+		pageSize := m.FindFieldByName("page_size")
+		if pageSize == nil {
+			return []lint.Problem{{
+				Message:    fmt.Sprintf("Message %q has no `page_size` field.", m.GetName()),
+				Descriptor: m,
+			}}
+		}
+
+		// Rule check: Ensure that the name page_size is the correct type.
+		if pageSize.GetType() != builder.FieldTypeInt32().GetType() {
+			return []lint.Problem{{
+				Message:    "`page_size` field on List RPCs should be a int32",
+				Descriptor: pageSize,
+			}}
+		}
+
+		return nil
+	},
+}
+
+var requestPaginationPageToken = &lint.MessageRule{
+	Name:   lint.NewRuleName("core", "0158", "request-message", "page-token-field"),
+	URI:    "https://aip.dev/158",
+	OnlyIf: isListRequestMessage,
+	LintMessage: func(m *desc.MessageDescriptor) (problems []lint.Problem) {
+		// Rule check: Establish that a page_size field is present.
+		pageToken := m.FindFieldByName("page_token")
+		if pageToken == nil {
+			return []lint.Problem{{
+				Message:    fmt.Sprintf("Message %q has no `page_token` field.", m.GetName()),
+				Descriptor: m,
+			}}
+		}
+
+		// Rule check: Ensure that the name page_size is the correct type.
+		if pageToken.GetType() != builder.FieldTypeString().GetType() {
+			return []lint.Problem{{
+				Message:    "`page_token` field on List RPCs should be a string",
+				Descriptor: pageToken,
+			}}
+		}
+
+		return nil
+	},
+}
+
+var responsePaginationNextPageToken = &lint.MessageRule{
+	Name:   lint.NewRuleName("core", "0158", "response-message", "next-page-token-field"),
+	URI:    "https://aip.dev/158",
+	OnlyIf: isListResponseMessage,
+	LintMessage: func(m *desc.MessageDescriptor) []lint.Problem {
+		// Rule check: Establish that a next_page_token field is present.
+		nextPageToken := m.FindFieldByName("next_page_token")
+		if nextPageToken == nil {
+			return []lint.Problem{{
+				Message:    fmt.Sprintf("Message %q has no `next_page_token` field.", m.GetName()),
+				Descriptor: m,
+			}}
+		}
+
+		// Rule check: Ensure that the name next_page_token is the correct type.
+		if nextPageToken.GetType() != builder.FieldTypeString().GetType() {
+			return []lint.Problem{{
+				Message:    "`next_page_token` field on List RPCs should be a string",
+				Descriptor: nextPageToken,
+			}}
+		}
+
+		return nil
+	},
+}


Test patch:
diff --git a/rules/aip0158/aip0158_test.go b/rules/aip0158/aip0158_test.go
new file mode 100644
index 0000000..620abbd
--- /dev/null
+++ b/rules/aip0158/aip0158_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 aip0158
+
+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::0158") {
+			t.Errorf("Rule %s is not namespaced to core::0158.", ruleName)
+		}
+	}
+}
diff --git a/rules/aip0158/pagination_test.go b/rules/aip0158/pagination_test.go
new file mode 100644
index 0000000..ea8aae8
--- /dev/null
+++ b/rules/aip0158/pagination_test.go
@@ -0,0 +1,146 @@
+// 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 aip0158
+
+import (
+	"testing"
+
+	"github.com/jhump/protoreflect/desc/builder"
+)
+
+type field struct {
+	fieldName string
+	fieldType *builder.FieldType
+}
+
+func TestRequestPaginationPageSize(t *testing.T) {
+	// Set up the testing permutations.
+	tests := []struct {
+		testName      string
+		messageName   string
+		messageFields []field
+		problemCount  int
+		errPrefix     string
+	}{
+		{"Valid", "ListFooRequest", []field{{"page_size", builder.FieldTypeInt32()}, {"page_token", builder.FieldTypeString()}}, 0, "False positive"},
+		{"MissingField", "ListFooRequest", []field{{"page_token", builder.FieldTypeString()}}, 1, "False positive"},
+		{"InvalidType", "ListFooRequest", []field{{"page_size", builder.FieldTypeDouble()}}, 1, "False positive"},
+	}
+
+	// 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)
+
+			for _, f := range test.messageFields {
+				messageBuilder.AddField(
+					builder.NewField(f.fieldName, f.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
+			// number of problems.
+			if problems := requestPaginationPageSize.LintMessage(message); len(problems) != test.problemCount {
+				t.Errorf("%s on rule %s: %#v", test.errPrefix, requestPaginationPageSize.Name, problems)
+			}
+		})
+	}
+}
+
+func TestRequestPaginationPageToken(t *testing.T) {
+	// Set up the testing permutations.
+	tests := []struct {
+		testName      string
+		messageName   string
+		messageFields []field
+		problemCount  int
+		errPrefix     string
+	}{
+		{"Valid", "ListFooRequest", []field{{"page_token", builder.FieldTypeString()}}, 0, "False positive"},
+		{"MissingField", "ListFooRequest", []field{{"name", builder.FieldTypeString()}}, 1, "False positive"},
+		{"InvalidType", "ListFooRequest", []field{{"page_token", builder.FieldTypeDouble()}}, 1, "False positive"},
+	}
+
+	// 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)
+
+			for _, f := range test.messageFields {
+				messageBuilder.AddField(
+					builder.NewField(f.fieldName, f.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
+			// number of problems.
+			if problems := requestPaginationPageToken.LintMessage(message); len(problems) != test.problemCount {
+				t.Errorf("%s on rule %s: %#v", test.errPrefix, requestPaginationPageToken.Name, problems)
+			}
+		})
+	}
+}
+
+func TestResponsePaginationNextPageToken(t *testing.T) {
+	// Set up the testing permutations.
+	tests := []struct {
+		testName      string
+		messageName   string
+		messageFields []field
+		problemCount  int
+		errPrefix     string
+	}{
+		{"Valid", "ListFooResponse", []field{{"name", builder.FieldTypeString()}, {"next_page_token", builder.FieldTypeString()}}, 0, "False positive"},
+		{"MissingField", "ListFooResponse", []field{{"name", builder.FieldTypeString()}}, 1, "False positive"},
+		{"InvalidType", "ListFooResponse", []field{{"name", builder.FieldTypeString()}, {"next_page_token", builder.FieldTypeDouble()}}, 1, "False positive"},
+	}
+
+	// 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)
+
+			for _, f := range test.messageFields {
+				messageBuilder.AddField(
+					builder.NewField(f.fieldName, f.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
+			// number of problems.
+			if problems := responsePaginationNextPageToken.LintMessage(message); len(problems) != test.problemCount {
+				t.Errorf("%s on rule %s: %#v", test.errPrefix, responsePaginationNextPageToken.Name, problems)
+			}
+		})
+	}
+}