From f8a6105c9a125e1580b177814c2b22d61886fbd2 Mon Sep 17 00:00:00 2001 From: noahdietz Date: Fri, 13 Oct 2023 10:12:23 -0700 Subject: [PATCH] feat(AIP-148): add uid-format rule --- docs/rules/0148/uid-format.md | 79 ++++++++++++++++++++++++++++++++ docs/rules/0148/use-uid.md | 2 +- rules/aip0148/aip0148.go | 1 + rules/aip0148/uid_format.go | 39 ++++++++++++++++ rules/aip0148/uid_format_test.go | 63 +++++++++++++++++++++++++ 5 files changed, 183 insertions(+), 1 deletion(-) create mode 100644 docs/rules/0148/uid-format.md create mode 100644 rules/aip0148/uid_format.go create mode 100644 rules/aip0148/uid_format_test.go diff --git a/docs/rules/0148/uid-format.md b/docs/rules/0148/uid-format.md new file mode 100644 index 000000000..2d36c75f9 --- /dev/null +++ b/docs/rules/0148/uid-format.md @@ -0,0 +1,79 @@ +--- +rule: + aip: 148 + name: [core, '0148', uid-format] + summary: Annotate uid with UUID4 format. +permalink: /148/uid-format +redirect_from: + - /0148/uid-format +--- + +# `uid` format annotation + +This rule encourages the use of the `UUID4` format annotation on the `uid` +field, as mandated in [AIP-148][]. + +## Details + +This rule looks on for fields named `uid` and complains if it does not have the +`(google.api.field_info).format = UUID4` annotation or has a format other than +`UUID4`. + +## Examples + +**Incorrect** code for this rule: + +```proto +// Incorrect. +message Book { + option (google.api.resource) = { + type: "library.googleapis.com/Book" + pattern: "books/{book}" + }; + + string name = 1; + string uid = 2; // missing (google.api.field_info).format = UUID4 +} +``` + +**Correct** code for this rule: + +```proto +// Correct. +message Book { + option (google.api.resource) = { + type: "library.googleapis.com/Book" + pattern: "books/{book}" + }; + + string name = 1; + string uid = 2 [(google.api.field_info).format = UUID4]; +} +``` + +## Disabling + +If you need to violate this rule, use a leading comment above the field or its +enclosing message. Remember to also include an [aip.dev/not-precedent][] +comment explaining why. + +```proto +// (-- api-linter: core::0148::uid-format=disabled +// aip.dev/not-precedent: We need to do this because reasons. --) +message Book { + option (google.api.resource) = { + type: "library.googleapis.com/Book" + pattern: "books/{book}" + }; + + string name = 1; + + string uid = 2; +} +``` + +If you need to violate this rule for an entire file, place the comment at the +top of the file. + +[aip-148]: https://aip.dev/148 +[aip.dev/not-precedent]: https://aip.dev/not-precedent diff --git a/docs/rules/0148/use-uid.md b/docs/rules/0148/use-uid.md index f19371f86..484e74ae8 100644 --- a/docs/rules/0148/use-uid.md +++ b/docs/rules/0148/use-uid.md @@ -8,7 +8,7 @@ redirect_from: - /0148/use-uid --- -# Human names +# Use `uid` as the resource ID field This rule encourages the use of `uid` instead of `id` for resource messages, as mandated in [AIP-148][]. diff --git a/rules/aip0148/aip0148.go b/rules/aip0148/aip0148.go index 719afda5f..2432382a5 100644 --- a/rules/aip0148/aip0148.go +++ b/rules/aip0148/aip0148.go @@ -27,5 +27,6 @@ func AddRules(r lint.RuleRegistry) error { fieldBehavior, humanNames, useUid, + uidFormat, ) } diff --git a/rules/aip0148/uid_format.go b/rules/aip0148/uid_format.go new file mode 100644 index 000000000..fdee7f326 --- /dev/null +++ b/rules/aip0148/uid_format.go @@ -0,0 +1,39 @@ +// 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 aip0148 + +import ( + "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" +) + +var uidFormat = &lint.FieldRule{ + Name: lint.NewRuleName(148, "uid-format"), + OnlyIf: func(fd *desc.FieldDescriptor) bool { + return fd.GetName() == uidStr + }, + LintField: func(fd *desc.FieldDescriptor) []lint.Problem { + if !utils.HasFormat(fd) || utils.GetFormat(fd) != annotations.FieldInfo_UUID4 { + return []lint.Problem{{ + Message: "The `uid` field must have a `(google.api.field_info).format = UUID4` annotation.", + Descriptor: fd, + }} + } + + return nil + }, +} diff --git a/rules/aip0148/uid_format_test.go b/rules/aip0148/uid_format_test.go new file mode 100644 index 000000000..64433a551 --- /dev/null +++ b/rules/aip0148/uid_format_test.go @@ -0,0 +1,63 @@ +// 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 aip0148 + +import ( + "testing" + + "github.com/googleapis/api-linter/rules/internal/testutils" +) + +func TestUidFormat(t *testing.T) { + for _, test := range []struct { + name, FieldName, Annotation string + problems testutils.Problems + }{ + { + name: "ValidUidFormat", + FieldName: "uid", + Annotation: "[(google.api.field_info).format = UUID4]", + }, + { + name: "SkipNonUid", + FieldName: "other", + }, + { + name: "InvalidMissingFormat", + FieldName: "uid", + problems: testutils.Problems{{Message: "format = UUID4"}}, + }, + { + name: "InvalidWrongFormat", + FieldName: "uid", + Annotation: "[(google.api.field_info).format = IPV4]", + problems: testutils.Problems{{Message: "format = UUID4"}}, + }, + } { + t.Run(test.name, func(t *testing.T) { + f := testutils.ParseProto3Tmpl(t, ` + import "google/api/field_info.proto"; + + message Person { + string {{.FieldName}} = 2 {{.Annotation}}; + } + `, test) + field := f.GetMessageTypes()[0].GetFields()[0] + if diff := test.problems.SetDescriptor(field).Diff(uidFormat.Lint(f)); diff != "" { + t.Errorf(diff) + } + }) + } +}