diff --git a/docs/rules/0203/resource-name-identifier.md b/docs/rules/0203/resource-name-identifier.md new file mode 100644 index 000000000..22fba30ab --- /dev/null +++ b/docs/rules/0203/resource-name-identifier.md @@ -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 diff --git a/rules/aip0203/aip0203.go b/rules/aip0203/aip0203.go index 5ffa3d7e3..12d4ca521 100644 --- a/rules/aip0203/aip0203.go +++ b/rules/aip0203/aip0203.go @@ -25,6 +25,7 @@ func AddRules(r lint.RuleRegistry) error { 203, fieldBehaviorRequired, unorderedListRepeated, + resourceNameIdentifier, resourceIdentifierOnly, ) } diff --git a/rules/aip0203/resource_name_identifier.go b/rules/aip0203/resource_name_identifier.go new file mode 100644 index 000000000..3f42b0f27 --- /dev/null +++ b/rules/aip0203/resource_name_identifier.go @@ -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 + }, +} diff --git a/rules/aip0203/resource_name_identifier_test.go b/rules/aip0203/resource_name_identifier_test.go new file mode 100644 index 000000000..436562fc0 --- /dev/null +++ b/rules/aip0203/resource_name_identifier_test.go @@ -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) + } + }) + } +}