Repository: googleapis/api-linter
Pull request #228 (2019-10-11 18:57:46)

AIP-203: Field behavior / required format
AIP-203: Field behavior / required format


Patch:
diff --git a/rules/aip0203/aip0203.go b/rules/aip0203/aip0203.go
index ded7723..b0ced03 100644
--- a/rules/aip0203/aip0203.go
+++ b/rules/aip0203/aip0203.go
@@ -29,6 +29,7 @@ func AddRules(r lint.RuleRegistry) {
 		inputOnly,
 		immutable,
 		outputOnly,
+		required,
 	)
 }
 
diff --git a/rules/aip0203/required.go b/rules/aip0203/required.go
new file mode 100644
index 0000000..8512967
--- /dev/null
+++ b/rules/aip0203/required.go
@@ -0,0 +1,57 @@
+package aip0203
+
+import (
+	"regexp"
+
+	"github.com/googleapis/api-linter/lint"
+	"github.com/googleapis/api-linter/rules/internal/utils"
+	"github.com/jhump/protoreflect/desc"
+	"google.golang.org/genproto/googleapis/api/annotations"
+)
+
+// This rule inspects the leading comments of each field
+// and if anything looks similar to "Required.", it throws
+// a problem.
+//
+// Examples:
+// Incorrect code for this rule:
+//
+//	message Book {
+//	// The title of the book.
+//	// @Required
+//	string title = 1;
+//	}
+//
+// or
+//
+//	message Book {
+//	// Required. The title of the book.
+//	string title = 1;
+//	}
+//
+//
+// Correct code for this rule:
+//
+//	message Book {
+//		// The title of the book.
+//		string title = 1 [(google.api.field_behavior) = REQUIRED];
+//	}
+var required = &lint.FieldRule{
+	Name:   lint.NewRuleName("core", "0203", "required"),
+	URI:    "http://api.dev/203#guidance",
+	OnlyIf: withoutRequiredRegexpFieldBehavior,
+	LintField: func(f *desc.FieldDescriptor) []lint.Problem {
+		return checkLeadingComments(f, requiredRegexp, "REQUIRED")
+	},
+}
+
+var requiredRegexp = regexp.MustCompile("(?i).*required.*")
+
+func withoutRequiredRegexpFieldBehavior(f *desc.FieldDescriptor) bool {
+	for _, v := range utils.GetFieldBehavior(f) {
+		if v == annotations.FieldBehavior_REQUIRED {
+			return false
+		}
+	}
+	return true
+}


Test patch:
diff --git a/rules/aip0203/required_test.go b/rules/aip0203/required_test.go
new file mode 100644
index 0000000..e5ebba0
--- /dev/null
+++ b/rules/aip0203/required_test.go
@@ -0,0 +1,118 @@
+package aip0203
+
+import (
+	"testing"
+
+	"github.com/googleapis/api-linter/rules/internal/testutils"
+)
+
+var title = "string title = 1;"
+var titleWithRequiredBehavior = "string title = 1 [(google.api.field_behavior) = REQUIRED];"
+
+func TestRequired(t *testing.T) {
+	testCases := []struct {
+		name     string
+		comment  string
+		field    string
+		problems testutils.Problems
+	}{
+		{
+			name:     "Valid",
+			comment:  "Required",
+			field:    titleWithRequiredBehavior,
+			problems: nil,
+		},
+		{
+			name:     "Valid",
+			comment:  "@required",
+			field:    titleWithRequiredBehavior,
+			problems: nil,
+		},
+		{
+			name:    "Invalid-required",
+			comment: "required",
+			field:   title,
+			problems: testutils.Problems{{
+				Message: "google.api.field_behavior",
+			}},
+		},
+		{
+			name:    "Invalid-Required",
+			comment: "Required",
+			field:   title,
+			problems: testutils.Problems{{
+				Message: "google.api.field_behavior",
+			}},
+		},
+		{
+			name:    "Invalid-@required",
+			comment: "@required",
+			field:   title,
+			problems: testutils.Problems{{
+				Message: "google.api.field_behavior",
+			}},
+		},
+		{
+			name:    "Invalid-@Required",
+			comment: "@Required",
+			field:   title,
+			problems: testutils.Problems{{
+				Message: "google.api.field_behavior",
+			}},
+		},
+		{
+			name:    "Invalid-REQUIRED",
+			comment: "REQUIRED",
+			field:   title,
+			problems: testutils.Problems{{
+				Message: "google.api.field_behavior",
+			}},
+		},
+		{
+			name:    "Invalid-@REQUIRED",
+			comment: "@REQUIRED",
+			field:   title,
+			problems: testutils.Problems{{
+				Message: "google.api.field_behavior",
+			}},
+		},
+		{
+			name:    "Invalid-required_free_text",
+			comment: "This field is required",
+			field:   title,
+			problems: testutils.Problems{{
+				Message: "google.api.field_behavior",
+			}},
+		},
+		{
+			name:    "Invalid-!required",
+			comment: "!required",
+			field:   title,
+			problems: testutils.Problems{{
+				Message: "google.api.field_behavior",
+			}},
+		},
+	}
+
+	for _, test := range testCases {
+		t.Run(test.name, func(t *testing.T) {
+			template := `
+import "google/api/field_behavior.proto";
+message Book {
+	// Title of the book
+	// {{.Comment}}
+	{{.Field}}
+}`
+			file := testutils.ParseProto3Tmpl(t, template,
+				struct {
+					Comment string
+					Field   string
+				}{test.comment, test.field})
+			f := file.GetMessageTypes()[0].GetFields()[0]
+			problems := required.Lint(file)
+			if diff := test.problems.SetDescriptor(f).Diff(problems); diff != "" {
+				t.Errorf(diff)
+			}
+		})
+	}
+}