Repository: googleapis/api-linter
Pull request #223 (2019-10-10 19:10:58)

[AIP-203] Field behavior / output only format
AIP-203: Field behavior / output only format


Patch:
diff --git a/rules/aip0203/aip0203.go b/rules/aip0203/aip0203.go
index 8df7281..ded7723 100644
--- a/rules/aip0203/aip0203.go
+++ b/rules/aip0203/aip0203.go
@@ -28,6 +28,7 @@ func AddRules(r lint.RuleRegistry) {
 	r.Register(
 		inputOnly,
 		immutable,
+		outputOnly,
 	)
 }
 
diff --git a/rules/aip0203/output_only.go b/rules/aip0203/output_only.go
new file mode 100644
index 0000000..7f82e65
--- /dev/null
+++ b/rules/aip0203/output_only.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 "Output Only", it throws
+// a problem.
+//
+// Examples:
+// Incorrect code for this rule:
+//
+//	message Book {
+//	// A generated URI for this book.
+//	// @OutputOnly
+//	string generated_uri = 1;
+//	}
+//
+// or
+//
+//	message Book {
+//	// Output only. A generated URI for this book.
+//	string generated_uri = 1;
+//	}
+//
+//
+// Correct code for this rule:
+//
+//	message Book {
+//	// A generated URI for this book.
+//	string generated_uri = 1 [(google.api.field_behavior) = OUTPUT_ONLY];
+//	}
+var outputOnly = &lint.FieldRule{
+	Name:   lint.NewRuleName("core", "0203", "output-only"),
+	URI:    "http://api.dev/203#guidance",
+	OnlyIf: withoutOutputOnlyFieldBehavior,
+	LintField: func(f *desc.FieldDescriptor) []lint.Problem {
+		return checkLeadingComments(f, outputOnlyRegexp, "OUTPUT_ONLY")
+	},
+}
+
+var outputOnlyRegexp = regexp.MustCompile("(?i).*output.?only.*")
+
+func withoutOutputOnlyFieldBehavior(f *desc.FieldDescriptor) bool {
+	for _, v := range utils.GetFieldBehavior(f) {
+		if v == annotations.FieldBehavior_OUTPUT_ONLY {
+			return false
+		}
+	}
+	return true
+}


Test patch:
diff --git a/rules/aip0203/output_only_test.go b/rules/aip0203/output_only_test.go
new file mode 100644
index 0000000..147868d
--- /dev/null
+++ b/rules/aip0203/output_only_test.go
@@ -0,0 +1,142 @@
+package aip0203
+
+import (
+	"testing"
+
+	"github.com/googleapis/api-linter/rules/internal/testutils"
+)
+
+var generatedUri = "string generated_uri = 1;"
+var generatedUriWithOutputOnlyBehavior = "string generated_uri = 1 [(google.api.field_behavior) = OUTPUT_ONLY];"
+
+func TestOutput(t *testing.T) {
+	testCases := []struct {
+		name     string
+		comment  string
+		field    string
+		problems testutils.Problems
+	}{
+		{
+			name:     "Valid",
+			comment:  "Output Only",
+			field:    generatedUriWithOutputOnlyBehavior,
+			problems: nil,
+		},
+		{
+			name:     "Valid",
+			comment:  "@OutputOnly",
+			field:    generatedUriWithOutputOnlyBehavior,
+			problems: nil,
+		},
+		{
+			name:    "Invalid-Output Only",
+			comment: "Output Only",
+			field:   generatedUri,
+			problems: testutils.Problems{{
+				Message: "google.api.field_behavior",
+			}},
+		},
+		{
+			name:    "Invalid-OutputOnly",
+			comment: "OutputOnly",
+			field:   generatedUri,
+			problems: testutils.Problems{{
+				Message: "google.api.field_behavior",
+			}},
+		},
+		{
+			name:    "Invalid-@OutputOnly",
+			comment: "@OutputOnly",
+			field:   generatedUri,
+			problems: testutils.Problems{{
+				Message: "google.api.field_behavior",
+			}},
+		},
+		{
+			name:    "Invalid-output_only",
+			comment: "output_only",
+			field:   generatedUri,
+			problems: testutils.Problems{{
+				Message: "google.api.field_behavior",
+			}},
+		},
+		{
+			name:    "Invalid-@output_only",
+			comment: "@output_only",
+			field:   generatedUri,
+			problems: testutils.Problems{{
+				Message: "google.api.field_behavior",
+			}},
+		},
+		{
+			name:    "Invalid-OUTPUT_ONLY",
+			comment: "OUTPUT_ONLY",
+			field:   generatedUri,
+			problems: testutils.Problems{{
+				Message: "google.api.field_behavior",
+			}},
+		},
+		{
+			name:    "Invalid-outputOnly",
+			comment: "outputOnly",
+			field:   generatedUri,
+			problems: testutils.Problems{{
+				Message: "google.api.field_behavior",
+			}},
+		},
+		{
+			name:    "Invalid-@outputOnly",
+			comment: "@outputonly",
+			field:   generatedUri,
+			problems: testutils.Problems{{
+				Message: "google.api.field_behavior",
+			}},
+		},
+		{
+			name:    "Invalid-@OUTPUT_ONLY",
+			comment: "@OUTPUT_ONLY",
+			field:   generatedUri,
+			problems: testutils.Problems{{
+				Message: "google.api.field_behavior",
+			}},
+		},
+		{
+			name:    "Invalid-output_only_free_text",
+			comment: "This field is output only",
+			field:   generatedUri,
+			problems: testutils.Problems{{
+				Message: "google.api.field_behavior",
+			}},
+		},
+		{
+			name:    "Invalid-!outputOnly",
+			comment: "!outputOnly",
+			field:   generatedUri,
+			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 {
+	// Secret to be stored in 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 := outputOnly.Lint(file)
+			if diff := test.problems.SetDescriptor(f).Diff(problems); diff != "" {
+				t.Errorf(diff)
+			}
+		})
+	}
+}