Skip to content

Commit 8f3e2ac

Browse files
authored
feat(AIP-123): resource type name matches message (#1452)
1 parent 34d1046 commit 8f3e2ac

File tree

4 files changed

+176
-0
lines changed

4 files changed

+176
-0
lines changed
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
---
2+
rule:
3+
aip: 123
4+
name: [core, '0123', resource-type-message]
5+
summary: Resource type names must match containing message name.
6+
permalink: /123/resource-type-message
7+
redirect_from:
8+
- /0123/resource-type-message
9+
---
10+
11+
# Resource type name
12+
13+
This rule enforces that messages that have a `google.api.resource` annotation,
14+
have a `type` aligned with the schema, as described in [AIP-123][].
15+
16+
## Details
17+
18+
This rule scans messages with a `google.api.resource` annotation, and validates
19+
that the `{Type}` portion of the `{Service Name}/{Type}` `type` field matches
20+
the containing message name.
21+
22+
## Examples
23+
24+
**Incorrect** code for this rule:
25+
26+
```proto
27+
// Incorrect.
28+
message Book {
29+
option (google.api.resource) = {
30+
// Should match containing message name 'Book'.
31+
type: "library.googleapis.com/Literature"
32+
pattern: "publishers/{publisher}/books/{book}"
33+
};
34+
35+
string name = 1;
36+
}
37+
```
38+
39+
**Correct** code for this rule:
40+
41+
```proto
42+
// Correct.
43+
message Book {
44+
option (google.api.resource) = {
45+
type: "library.googleapis.com/Book"
46+
pattern: "publishers/{publisher}/books/{book}"
47+
};
48+
49+
string name = 1;
50+
}
51+
```
52+
53+
## Disabling
54+
55+
If you need to violate this rule, use a leading comment above the message.
56+
57+
```proto
58+
// (-- api-linter: core::0123::resource-type-message=disabled
59+
// aip.dev/not-precedent: We need to do this because reasons. --)
60+
message Book {
61+
option (google.api.resource) = {
62+
type: "library.googleapis.com/Literature"
63+
pattern: "publishers/{publisher}/books/{book}"
64+
};
65+
66+
string name = 1;
67+
}
68+
```
69+
70+
If you need to violate this rule for an entire file, place the comment at the
71+
top of the file.
72+
73+
[aip-123]: http://aip.dev/123
74+
[aip.dev/not-precedent]: https://aip.dev/not-precedent

rules/aip0123/aip0123.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ func AddRules(r lint.RuleRegistry) error {
4040
resourcePlural,
4141
resourceReferenceType,
4242
resourceSingular,
43+
resourceTypeMessage,
4344
resourceTypeName,
4445
resourceVariables,
4546
resourceDefinitionVariables,
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// Copyright 2024 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package aip0123
16+
17+
import (
18+
"fmt"
19+
"strings"
20+
21+
"github.com/googleapis/api-linter/lint"
22+
"github.com/googleapis/api-linter/locations"
23+
"github.com/googleapis/api-linter/rules/internal/utils"
24+
"github.com/jhump/protoreflect/desc"
25+
)
26+
27+
var resourceTypeMessage = &lint.MessageRule{
28+
Name: lint.NewRuleName(123, "resource-type-message"),
29+
OnlyIf: utils.IsResource,
30+
LintMessage: func(m *desc.MessageDescriptor) []lint.Problem {
31+
n := m.GetName()
32+
typ := utils.GetResource(m).GetType()
33+
typ = typ[strings.LastIndex(typ, "/")+1:]
34+
35+
if typ != n {
36+
return []lint.Problem{{
37+
Message: fmt.Sprintf("Resource type name %q should match containing message name %q", typ, n),
38+
Descriptor: m,
39+
Location: locations.MessageResource(m),
40+
}}
41+
}
42+
43+
return nil
44+
},
45+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
// Copyright 2024 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package aip0123
16+
17+
import (
18+
"testing"
19+
20+
"github.com/googleapis/api-linter/rules/internal/testutils"
21+
)
22+
23+
func TestResourceTypeMessage(t *testing.T) {
24+
for _, test := range []struct {
25+
name string
26+
TypeName string
27+
problems testutils.Problems
28+
}{
29+
{
30+
name: "Valid",
31+
TypeName: "Book",
32+
},
33+
{
34+
name: "Invalid",
35+
TypeName: "Shelf",
36+
problems: testutils.Problems{{Message: "should match containing message name"}},
37+
},
38+
} {
39+
t.Run(test.name, func(t *testing.T) {
40+
f := testutils.ParseProto3Tmpl(t, `
41+
import "google/api/resource.proto";
42+
message Book {
43+
option (google.api.resource) = {
44+
type: "library.googleapis.com/{{ .TypeName }}"
45+
pattern: "publishers/{publisher}/books/{book}"
46+
};
47+
string name = 1;
48+
}
49+
`, test)
50+
m := f.GetMessageTypes()[0]
51+
if diff := test.problems.SetDescriptor(m).Diff(resourceTypeMessage.Lint(f)); diff != "" {
52+
t.Error(diff)
53+
}
54+
})
55+
}
56+
}

0 commit comments

Comments
 (0)