Skip to content

Commit

Permalink
feat(AIP-203): require field_behavior (#1149)
Browse files Browse the repository at this point in the history
Adding guidance from aip-dev/google.aip.dev#1100

removing optional_consistency, which is now redundant.
  • Loading branch information
toumorokoshi authored May 25, 2023
1 parent 2787792 commit fd373ee
Show file tree
Hide file tree
Showing 9 changed files with 282 additions and 268 deletions.
70 changes: 70 additions & 0 deletions docs/rules/0203/field-behavior-required.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
---
rule:
aip: 203
name: [core, '0203', field-behavior-required]
summary: Field behavior is required, and must have one of OUTPUT_ONLY,
REQUIRED, or OPTIONAL.
permalink: /203/field-behavior-required
redirect_from:
- /0203/field-behavior-required
---

# Field Behavior Required

This rule enforces that each field in a message used in a request has a
`google.api.field_behavior` annotation with valid values, as mandated by
[AIP-203][].

## Details

This rule looks at all fields and ensures they have a
`google.api.field_behavior` annotation. It also verifies that they have at least
one of the options `OUTPUT_ONLY`, `REQUIRED`, or `OPTIONAL`: all fields must
fall into one of these categories.

## Examples

**Incorrect** code for this rule:

```proto
// Incorrect.
message Book {
string name = 1;
// No field behavior
optional string title = 2;
}
```

**Correct** code for this rule:

```proto
// Correct.
message Book {
string name = 1;
string title = 2 [(google.api.field_behavior) = REQUIRED];
}
```

## 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 Book {
string name = 1;
// Required. The title of the book.
// (-- api-linter: core::0203::field-behavior-required=disabled
// aip.dev/not-precedent: We need to do this because reasons. --)
optional string title = 2;
}
```

If you need to violate this rule for an entire file, place the comment at the
top of the file.

[aip-203]: https://aip.dev/203
[aip.dev/not-precedent]: https://aip.dev/not-precedent
94 changes: 0 additions & 94 deletions docs/rules/0203/optional-consistency.md

This file was deleted.

2 changes: 1 addition & 1 deletion docs/rules/0203/required-and-optional.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ message Book {
**Correct** code for this rule:

```proto
// Incorrect.
// Correct.
message Book {
string name = 1;
Expand Down
2 changes: 1 addition & 1 deletion rules/aip0203/aip0203.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,11 @@ var standardFields = stringset.New("etag")
func AddRules(r lint.RuleRegistry) error {
return r.Register(
203,
fieldBehaviorRequired,
inputOnly,
immutable,
optional,
optionalBehaviorConflict,
optionalBehaviorConsistency,
outputOnly,
required,
requiredAndOptional,
Expand Down
68 changes: 68 additions & 0 deletions rules/aip0203/field_behavior_required.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// Copyright 2023 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 aip0203

import (
"fmt"

"bitbucket.org/creachadair/stringset"
"github.com/googleapis/api-linter/lint"
"github.com/googleapis/api-linter/rules/internal/utils"
"github.com/jhump/protoreflect/desc"
)

var minimumRequiredFieldBehavior = stringset.New(
"OPTIONAL", "REQUIRED", "OUTPUT_ONLY", "IMMUTABLE",
)

var fieldBehaviorRequired = &lint.MethodRule{
Name: lint.NewRuleName(203, "field-behavior-required"),
LintMethod: func(m *desc.MethodDescriptor) []lint.Problem {
// we only check requests, as OutputTypes are always
// OUTPUT_ONLY
it := m.GetInputType()
return checkFields(it)
},
}

func checkFields(m *desc.MessageDescriptor) []lint.Problem {
problems := []lint.Problem{}
for _, f := range m.GetFields() {
asMessage := f.GetMessageType()
if asMessage != nil {
problems = append(problems, checkFields(asMessage)...)
}
problems = append(problems, checkFieldBehavior(f)...)
}
return problems
}

func checkFieldBehavior(f *desc.FieldDescriptor) []lint.Problem {
problems := []lint.Problem{}
fieldBehavior := utils.GetFieldBehavior(f)
if len(fieldBehavior) == 0 {
problems = append(problems, lint.Problem{
Message: fmt.Sprintf("google.api.field_behavior annotation must be set, and have one of %v", minimumRequiredFieldBehavior),
Descriptor: f,
})
// check for at least one valid annotation
} else if !minimumRequiredFieldBehavior.Intersects(fieldBehavior) {
problems = append(problems, lint.Problem{
Message: fmt.Sprintf(
"google.api.field_behavior must have at least one of the following behaviors set: %v", minimumRequiredFieldBehavior),
Descriptor: f,
})
}
return problems
}
142 changes: 142 additions & 0 deletions rules/aip0203/field_behavior_required_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
// Copyright 2023 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 aip0203

import (
"testing"

"github.com/googleapis/api-linter/rules/internal/testutils"
)

const ()

func TestFieldBehaviorRequired(t *testing.T) {
for _, test := range []struct {
name string
Fields string
problems testutils.Problems
}{
{
"ValidImmutable",
"int32 page_count = 1 [(google.api.field_behavior) = IMMUTABLE];",
nil,
},
{
"ValidRequired",
"int32 page_count = 1 [(google.api.field_behavior) = REQUIRED];",
nil,
},
{
"ValidOptional",
"int32 page_count = 1 [(google.api.field_behavior) = OPTIONAL];",
nil,
},
{
"ValidOutputOnly",
"int32 page_count = 1 [(google.api.field_behavior) = OUTPUT_ONLY];",
nil,
},
{
"ValidOptionalImmutable",
`int32 page_count = 1 [
(google.api.field_behavior) = OUTPUT_ONLY,
(google.api.field_behavior) = OPTIONAL
];`,
nil,
},
{
"InvalidEmpty",
"int32 page_count = 1;",
testutils.Problems{{Message: "annotation must be set"}},
},
} {
t.Run(test.name, func(t *testing.T) {
f := testutils.ParseProto3Tmpl(t, `
import "google/api/field_behavior.proto";
service Library {
rpc UpdateBook(UpdateBookRequest) returns (UpdateBookResponse) {
}
}
message UpdateBookRequest {
{{.Fields}}
}
message UpdateBookResponse {
// verifies that no error was raised on lack
// of field behavior in the response.
string name = 1;
}
`, test)
field := f.GetMessageTypes()[0].GetFields()[0]
if diff := test.problems.SetDescriptor(field).Diff(fieldBehaviorRequired.Lint(f)); diff != "" {
t.Errorf(diff)
}
})
}
}

func TestFieldBehaviorRequiredNested(t *testing.T) {
for _, test := range []struct {
name string
Fields string
problems testutils.Problems
}{
{
"ValidAnnotatedAndChildAnnotated",
"Annotated annotated = 1 [(google.api.field_behavior) = REQUIRED];",
nil,
},
{
"InvalidChildNotAnnotated",
"NonAnnotated non_annotated = 1 [(google.api.field_behavior) = REQUIRED];",
testutils.Problems{{Message: "must be set"}},
},
} {
t.Run(test.name, func(t *testing.T) {
f := testutils.ParseProto3Tmpl(t, `
import "google/api/field_behavior.proto";
service Library {
rpc UpdateBook(UpdateBookRequest) returns (UpdateBookResponse) {
}
}
message NonAnnotated {
string nested = 1;
}
message Annotated {
string nested = 1 [(google.api.field_behavior) = REQUIRED];
}
message UpdateBookRequest {
{{.Fields}}
}
message UpdateBookResponse {
// verifies that no error was raised on lack
// of field behavior in the response.
string name = 1;
}
`, test)
it := f.GetServices()[0].GetMethods()[0].GetInputType()
nestedField := it.GetFields()[0].GetMessageType().GetFields()[0]
if diff := test.problems.SetDescriptor(nestedField).Diff(fieldBehaviorRequired.Lint(f)); diff != "" {
t.Errorf(diff)
}
})
}
}
Loading

0 comments on commit fd373ee

Please sign in to comment.