Skip to content

Commit

Permalink
feat(AIP-203): add resource name IDENTIFIER enforcement (#1241)
Browse files Browse the repository at this point in the history
* feat(AIP-203): add resource name IDENTIFIER enforcement

* use resource name field helper
  • Loading branch information
noahdietz committed Aug 30, 2023
1 parent 1022df2 commit 7d9daab
Show file tree
Hide file tree
Showing 4 changed files with 187 additions and 0 deletions.
71 changes: 71 additions & 0 deletions docs/rules/0203/resource-name-identifier.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
---
rule:
aip: 203
name: [core, '0203', resource-name-identifier]
summary: Resource name field must have field behavior IDENTIFIER.
permalink: /203/resource-name-identifier
redirect_from:
- /0203/resource-name-identifier
---

# Resource name: IDENTIFIER

This rule enforces that the field on a resource representing the resource's name
is annotated with with `(google.api.field_behavior) = IDENTIFIER`, as mandated
by [AIP-203][].

## Details

This rule looks at every resource message and complains if the resource name
field is not annotated with `(google.api.field_behavior) = IDENTIFER`.

## Examples

**Incorrect** code for this rule:

```proto
message Book {
option (google.api.resource) = {
type: "library.googleapis.com/Book"
pattern: "books/{book}"
};
// Missing IDENTIFIER field behavior.
string name = 1;
}
```

**Correct** code for this rule:

```proto
// Correct.
message Book {
option (google.api.resource) = {
type: "library.googleapis.com/Book"
pattern: "books/{book}"
};
string name = 1 [(google.api.field_behavior) = IDENTIFIER];
}
```

## 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 {
option (google.api.resource) = {
type: "library.googleapis.com/Book"
pattern: "books/{book}"
};
// (-- api-linter: core::0203::resource-name-identifier=disabled
// aip.dev/not-precedent: We need to do this because reasons. --)
string name = 1;
}
```

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
1 change: 1 addition & 0 deletions rules/aip0203/aip0203.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ func AddRules(r lint.RuleRegistry) error {
203,
fieldBehaviorRequired,
unorderedListRepeated,
resourceNameIdentifier,
resourceIdentifierOnly,
)
}
42 changes: 42 additions & 0 deletions rules/aip0203/resource_name_identifier.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// 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 (
"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"
fpb "google.golang.org/genproto/googleapis/api/annotations"
)

var resourceNameIdentifier = &lint.MessageRule{
Name: lint.NewRuleName(203, "resource-name-identifier"),
OnlyIf: utils.IsResource,
LintMessage: func(m *desc.MessageDescriptor) []lint.Problem {
f := m.FindFieldByName(utils.GetResourceNameField(utils.GetResource(m)))
fb := utils.GetFieldBehavior(f)
if len(fb) == 0 || !fb.Contains(fpb.FieldBehavior_IDENTIFIER.String()) {
return []lint.Problem{{
Message: "resource name field must have field_behavior IDENTIFIER",
Descriptor: f,
Location: locations.FieldOption(f, fpb.E_FieldBehavior),
Suggestion: "(google.api.field_behavior) = IDENTIFIER",
}}
}

return nil
},
}
73 changes: 73 additions & 0 deletions rules/aip0203/resource_name_identifier_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// Copyright 2020 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"
)

func TestResourceNameIdentifier(t *testing.T) {
testCases := []struct {
name string
Field, NameField string
problems testutils.Problems
}{
{
name: "Valid",
Field: "string name = 1 [(google.api.field_behavior) = IDENTIFIER];",
problems: nil,
},
{
name: "ValidNameField",
Field: "string resource_name = 1 [(google.api.field_behavior) = IDENTIFIER];",
NameField: "resource_name",
problems: nil,
},
{
name: "InvalidNoFieldBehavior",
Field: "string name = 1;",
problems: testutils.Problems{{Message: "field_behavior IDENTIFIER", Suggestion: "(google.api.field_behavior) = IDENTIFIER"}},
},
{
name: "InvalidMissingIdentifier",
Field: "string name = 1 [(google.api.field_behavior) = REQUIRED];",
problems: testutils.Problems{{Message: "field_behavior IDENTIFIER", Suggestion: "(google.api.field_behavior) = IDENTIFIER"}},
},
}

for _, test := range testCases {
t.Run(test.name, func(t *testing.T) {
file := testutils.ParseProto3Tmpl(t, `
import "google/api/field_behavior.proto";
import "google/api/resource.proto";
message Book {
option (google.api.resource) = {
type: "library.googleapis.com/Book"
pattern: "books/{book}"
name_field: "{{.NameField}}"
};
{{.Field}}
}`, test)
f := file.GetMessageTypes()[0].GetFields()[0]
problems := resourceNameIdentifier.Lint(file)
if diff := test.problems.SetDescriptor(f).Diff(problems); diff != "" {
t.Error(diff)
}
})
}
}

0 comments on commit 7d9daab

Please sign in to comment.