Skip to content

Commit

Permalink
feat(AIP-155): enforce format uuid4 guidance (#1272)
Browse files Browse the repository at this point in the history
Enforces new AIP-155 guidance that says `request_id` should have `(google.api.field_info).format = UUID4`.
  • Loading branch information
noahdietz committed Oct 18, 2023
1 parent fd8e75c commit 2f2e34b
Show file tree
Hide file tree
Showing 7 changed files with 252 additions and 0 deletions.
10 changes: 10 additions & 0 deletions docs/rules/0155/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
aip_listing: 155
permalink: /155/
redirect_from:
- /0155/
---

# Singleton resources

{% include linter-aip-listing.md aip=155 %}
72 changes: 72 additions & 0 deletions docs/rules/0155/request-id-format.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
---
rule:
aip: 155
name: [core, '0155', request-id-format]
summary: Annotate request_id with UUID4 format.
permalink: /155/request-id-format
redirect_from:
- /0155/request-id-format
---

# `request_id` format annotation

This rule encourages the use of the `UUID4` format annotation on the
`request_id` field, as mandated in [AIP-155][].

## Details

This rule looks on for fields named `request_id` 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 CreateBookRequest {
string parent = 1;
Book book = 2;
string request_id = 3; // missing (google.api.field_info).format = UUID4
}
```

**Correct** code for this rule:

```proto
// Correct.
message CreateBookRequest {
string parent = 1;
Book book = 2;
string request_id = 3 [(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
message CreateBookRequest {
string parent = 1;
Book book = 2;
// (-- api-linter: core::0155::request-id-format=disabled
// aip.dev/not-precedent: We need to do this because reasons. --)
string request_id = 3;
}
```

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

[aip-155]: https://aip.dev/155
[aip.dev/not-precedent]: https://aip.dev/not-precedent
28 changes: 28 additions & 0 deletions rules/aip0155/aip0155.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// 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 aip0155 contains rules defined in https://aip.dev/155.
package aip0155

import (
"github.com/googleapis/api-linter/lint"
)

// AddRules adds all of the AIP-155 rules to the provided registry.
func AddRules(r lint.RuleRegistry) error {
return r.Register(
155,
requestIdFormat,
)
}
27 changes: 27 additions & 0 deletions rules/aip0155/aip0155_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// 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 aip0155

import (
"testing"

"github.com/googleapis/api-linter/lint"
)

func TestAddRules(t *testing.T) {
if err := AddRules(lint.NewRuleRegistry()); err != nil {
t.Errorf("AddRules got an error: %v", err)
}
}
41 changes: 41 additions & 0 deletions rules/aip0155/request_id_format.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// 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 aip0155

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"
"google.golang.org/protobuf/types/descriptorpb"
)

var requestIdFormat = &lint.FieldRule{
Name: lint.NewRuleName(155, "request-id-format"),
OnlyIf: func(fd *desc.FieldDescriptor) bool {
return fd.GetType() == descriptorpb.FieldDescriptorProto_TYPE_STRING &&
fd.GetName() == "request_id"
},
LintField: func(fd *desc.FieldDescriptor) []lint.Problem {
if !utils.HasFormat(fd) || utils.GetFormat(fd) != annotations.FieldInfo_UUID4 {
return []lint.Problem{{
Message: "The `request_id` field should have a `(google.api.field_info).format = UUID4` annotation.",
Descriptor: fd,
}}
}

return nil
},
}
72 changes: 72 additions & 0 deletions rules/aip0155/request_id_format_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// 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 aip0155

import (
"testing"

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

func TestRequestIdFormat(t *testing.T) {
for _, test := range []struct {
name, Annotation, FieldName, Type string
problems testutils.Problems
}{
{
name: "ValidRequestIdFormat",
FieldName: "request_id",
Annotation: "[(google.api.field_info).format = UUID4]",
Type: "string",
},
{
name: "SkipNonRequestId",
FieldName: "other",
Type: "string",
},
{
name: "SkipNonStringRequestId",
FieldName: "request_id",
Type: "Foo",
},
{
name: "InvalidMissingFormat",
FieldName: "request_id",
Type: "string",
problems: testutils.Problems{{Message: "format = UUID4"}},
},
{
name: "InvalidWrongFormat",
FieldName: "request_id",
Annotation: "[(google.api.field_info).format = IPV4]",
Type: "string",
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 {
{{.Type}} {{.FieldName}} = 2 {{.Annotation}};
}
message Foo {}
`, test)
field := f.GetMessageTypes()[0].GetFields()[0]
if diff := test.problems.SetDescriptor(field).Diff(requestIdFormat.Lint(f)); diff != "" {
t.Errorf(diff)
}
})
}
}
2 changes: 2 additions & 0 deletions rules/rules.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ import (
"github.com/googleapis/api-linter/rules/aip0151"
"github.com/googleapis/api-linter/rules/aip0152"
"github.com/googleapis/api-linter/rules/aip0154"
"github.com/googleapis/api-linter/rules/aip0155"
"github.com/googleapis/api-linter/rules/aip0156"
"github.com/googleapis/api-linter/rules/aip0157"
"github.com/googleapis/api-linter/rules/aip0158"
Expand Down Expand Up @@ -123,6 +124,7 @@ var aipAddRulesFuncs = []addRulesFuncType{
aip0151.AddRules,
aip0152.AddRules,
aip0154.AddRules,
aip0155.AddRules,
aip0156.AddRules,
aip0157.AddRules,
aip0158.AddRules,
Expand Down

0 comments on commit 2f2e34b

Please sign in to comment.