Skip to content

Commit

Permalink
feat: enforce all required fields in method signatures (#894)
Browse files Browse the repository at this point in the history
* feat: enforce all required fields in method signatures
  • Loading branch information
noahdietz authored Oct 25, 2021
1 parent b850a4b commit 1d1a79a
Show file tree
Hide file tree
Showing 4 changed files with 187 additions and 0 deletions.
72 changes: 72 additions & 0 deletions docs/rules/4232/required-fields.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
---
rule:
aip: 4232
name: [client-libraries, '4232', required-fields]
summary: Method Signatures must contain all fields annotated as required.
permalink: /4232/required-fields
---

# Method Signature: Required fields

This rule enforces that all `google.api.method_signature` annotations contain
all top-level request message fields that are annotated with `REQUIRED`, as
mandated in [AIP-4232][].

## Details

This rule looks at any RPC methods with a `google.api.method_signature`
annotation, and complains if any request field annotated with `REQUIRED` is
missing.

## Examples

**Incorrect** code for this rule:

```proto
// Incorrect.
rpc BatchDeleteBooks(BatchDeleteBooksRequest) returns (google.protobuf.Empty) {
option (google.api.http) = {
post: "/v1/{parent=publishers/*}/books:batchDelete"
body: "*"
};
// The field "names" is annotated with REQUIRED and is missing from the
// method_signature.
option (google.api.method_signature) = "parent";
}
```

**Correct** code for this rule:

```proto
// Correct.
rpc BatchDeleteBooks(BatchDeleteBooksRequest) returns (google.protobuf.Empty) {
option (google.api.http) = {
post: "/v1/{parent=publishers/*}/books:batchDelete"
body: "*"
};
option (google.api.method_signature) = "parent,names";
}
```

## Disabling

If you need to violate this rule, use a leading comment above the method.
Remember to also include an [aip.dev/not-precedent][] comment explaining why.

```proto
// (-- api-linter: client-libraries::4232::required-fields=disabled
// aip.dev/not-precedent: We need to do this because reasons. --)
rpc BatchDeleteBooks(BatchDeleteBooksRequest) returns (google.protobuf.Empty) {
option (google.api.http) = {
post: "/v1/{parent=publishers/*}/books:batchDelete"
body: "*"
};
option (google.api.method_signature) = "parent";
}
```

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

[aip-4232]: https://aip.dev/4232
[aip.dev/not-precedent]: https://aip.dev/not-precedent
1 change: 1 addition & 0 deletions rules/aip4232/aip4232.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ func AddRules(r lint.RuleRegistry) error {
return r.Register(
4232,
repeatedFields,
requiredFields,
)
}

Expand Down
56 changes: 56 additions & 0 deletions rules/aip4232/required_fields.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// Copyright 2021 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 aip4232

import (
"fmt"
"strings"

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

// All fields annotated as REQUIRED must be in every method_signature.
var requiredFields = &lint.MethodRule{
Name: lint.NewRuleName(4232, "required-fields"),
OnlyIf: hasMethodSignatures,
LintMethod: func(m *desc.MethodDescriptor) []lint.Problem {
var problems []lint.Problem
sigs := utils.GetMethodSignatures(m)
in := m.GetInputType()

requiredFields := []string{}
for _, f := range in.GetFields() {
if utils.GetFieldBehavior(f).Contains("REQUIRED") {
requiredFields = append(requiredFields, f.GetName())
}
}
for i, sig := range sigs {
sigset := stringset.New(sig...)
if !sigset.Contains(requiredFields...) {
problems = append(problems, lint.Problem{
Message: fmt.Sprintf("Method signature %q missing at least one of the required fields: %q", strings.Join(sig, ","), strings.Join(requiredFields, ",")),
Descriptor: m,
Location: locations.MethodSignature(m, i),
})
}
}

return problems
},
}
58 changes: 58 additions & 0 deletions rules/aip4232/required_fields_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// Copyright 2021 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 aip4232

import (
"testing"

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

func TestRequiredFields(t *testing.T) {
tests := []struct {
testName string
Signature string
FieldBehavior string
problems testutils.Problems
}{
{"Valid", "name,paperback_only", "REQUIRED", nil},
{"ValidNotRequired", "paperback_only", "OPTIONAL", nil},
{"Invalid", "paperback_only", "REQUIRED", testutils.Problems{{Message: "missing at least one"}}},
}

for _, test := range tests {
t.Run(test.testName, func(t *testing.T) {
f := testutils.ParseProto3Tmpl(t, `
import "google/api/client.proto";
import "google/api/field_behavior.proto";
service Library {
rpc ArchiveBook(ArchiveBookRequest) returns (ArchiveBookResponse) {
option (google.api.method_signature) = "{{.Signature}}";
}
}
message ArchiveBookRequest {
string name = 1 [(google.api.field_behavior) = {{.FieldBehavior}}];
bool paperback_only = 2;
}
message ArchiveBookResponse {}
`, test)
method := f.GetServices()[0].GetMethods()[0]
if diff := test.problems.SetDescriptor(method).Diff(requiredFields.Lint(f)); diff != "" {
t.Error(diff)
}
})
}
}

0 comments on commit 1d1a79a

Please sign in to comment.