Skip to content

Commit 9c6635b

Browse files
authored
feat(AIP-190): UpperCamelCase service, method and message names. (#1623)
* feat(AIP-190): UpperCamelCase service, method and message names. * allowlist some terms where the single letter word is not first
1 parent d857f46 commit 9c6635b

12 files changed

Lines changed: 485 additions & 0 deletions

docs/rules/0190/message-case.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
---
2+
rule:
3+
aip: 190
4+
name: [core, '0190', message-case]
5+
summary: Message names must use UpperCamelCase.
6+
permalink: /190/message-case
7+
redirect_from:
8+
- /0190/message-case
9+
---
10+
11+
# Message name case
12+
13+
This rule enforces that all message names use UpperCamelCase, as mandated in
14+
[AIP-190][].
15+
16+
## Details
17+
18+
This rule checks all message names and complains if they are not UpperCamelCase,
19+
or if they contain consecutive uppercase letters not present in the allowlist.
20+
21+
## Caveats
22+
23+
This rule is an imperfect heuristic. The root problem is that AIP-190's definition of camel case starts with the prose form of the name as input, for example "XML HTTP Request". But this prose need not actually be written down anywhere – it may only exist inside the API author's head! This linter certainly doesn't have access to it. What we actually have is just the author’s _proposed_ capitalization. While some case violations are obvious on their face, like `snake_case` or `lowerCamelCase`, the rest require guessing.
24+
25+
Without knowing the actual boundaries of words, we can’t detect a violation like `XmlhttpRequest` where the letter H is incorrectly not capitalized. And even with access to a dictionary, we couldn’t tell whether a `CarpetService` should be actually capitalized as `CarPetService`! Finally, some terms can be written as a single word or multiple, like "nonempty" vs "non-empty". An API or even whole suite of APIs should probably be consistent about using one or the other but this rule isn't smart enough to know which of `CheckNotEmpty` or `CheckNonempty` is a violation. Fortunately, this undercapitalization type of error is less common and so we focus on the problem of excessive capitalization.
26+
27+
As a heuristic, we flag consecutive capital letters in the proposed name, for example the `XML` part of `XMLHttpRequest`. But there are legitimate words with just a single letter, like the “x” in x-ray or the “p” in p-value. We use an allowlist of exceptions to ignore them.
28+
29+
## Disabling
30+
31+
Users should not hesitate to disable this rule if it makes a mistake. To do so, include the “prose form” of your name, as defined by the Google Java Style Guide, in a comment above the proto element. For example:
32+
33+
```proto
34+
// (-- api-linter: core::0190::message-case=disabled
35+
// Prose message name: "Print T Shirt Request" --)
36+
message PrintTShirtRequest {}
37+
```
38+
39+
[aip-190]: https://aip.dev/190

docs/rules/0190/method-case.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
---
2+
rule:
3+
aip: 190
4+
name: [core, '0190', method-case]
5+
summary: Method names must use UpperCamelCase.
6+
permalink: /190/method-case
7+
redirect_from:
8+
- /0190/method-case
9+
---
10+
11+
# Method name case
12+
13+
This rule enforces that all method names use UpperCamelCase, as mandated in
14+
[AIP-190][].
15+
16+
## Details
17+
18+
This rule checks all method names and complains if they are not UpperCamelCase,
19+
or if they contain consecutive uppercase letters not present in the allowlist.
20+
21+
## Caveats
22+
23+
This rule is an imperfect heuristic. The root problem is that AIP-190's definition of camel case starts with the prose form of the name as input, for example "XML HTTP Request". But this prose need not actually be written down anywhere – it may only exist inside the API author's head! This linter certainly doesn't have access to it. What we actually have is just the author’s _proposed_ capitalization. While some case violations are obvious on their face, like `snake_case` or `lowerCamelCase`, the rest require guessing.
24+
25+
Without knowing the actual boundaries of words, we can’t detect a violation like `XmlhttpRequest` where the letter H is incorrectly not capitalized. And even with access to a dictionary, we couldn’t tell whether a `CarpetService` should be actually capitalized as `CarPetService`! Finally, some terms can be written as a single word or multiple, like "nonempty" vs "non-empty". An API or even whole suite of APIs should probably be consistent about using one or the other but this rule isn't smart enough to know which of `CheckNotEmpty` or `CheckNonempty` is a violation. Fortunately, this undercapitalization type of error is less common and so we focus on the problem of excessive capitalization.
26+
27+
As a heuristic, we flag consecutive capital letters in the proposed name, for example the `XML` part of `XMLHttpRequest`. But there are legitimate words with just a single letter, like the “x” in x-ray or the “p” in p-value. We use an allowlist of exceptions to ignore them.
28+
29+
## Disabling
30+
31+
Users should not hesitate to disable this rule if it makes a mistake. To do so, include the “prose form” of your name, as defined by the Google Java Style Guide, in a comment above the proto element. For example:
32+
33+
```proto
34+
service TShirtService {
35+
// (-- api-linter: core::0190::method-case=disabled
36+
// Prose method name: "Print T Shirt" --)
37+
rpc PrintTShirt(PrintTShirtRequest) returns (PrintTShirtResponse) {}
38+
}
39+
```
40+
41+
[aip-190]: https://aip.dev/190

docs/rules/0190/service-case.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
---
2+
rule:
3+
aip: 190
4+
name: [core, '0190', service-case]
5+
summary: Service names must use UpperCamelCase.
6+
permalink: /190/service-case
7+
redirect_from:
8+
- /0190/service-case
9+
---
10+
11+
# Service name case
12+
13+
This rule enforces that all service names use UpperCamelCase, as mandated in
14+
[AIP-190][].
15+
16+
## Details
17+
18+
This rule checks all service names and complains if they are not UpperCamelCase,
19+
or if they contain consecutive uppercase letters not present in the allowlist.
20+
21+
## Caveats
22+
23+
This rule is an imperfect heuristic. The root problem is that AIP-190's definition of camel case starts with the prose form of the name as input, for example "XML HTTP Request". But this prose need not actually be written down anywhere – it may only exist inside the API author's head! This linter certainly doesn't have access to it. What we actually have is just the author’s _proposed_ capitalization. While some case violations are obvious on their face, like `snake_case` or `lowerCamelCase`, the rest require guessing.
24+
25+
Without knowing the actual boundaries of words, we can’t detect a violation like `XmlhttpRequest` where the letter H is incorrectly not capitalized. And even with access to a dictionary, we couldn’t tell whether a `CarpetService` should be actually capitalized as `CarPetService`! Finally, some terms can be written as a single word or multiple, like "nonempty" vs "non-empty". An API or even whole suite of APIs should probably be consistent about using one or the other but this rule isn't smart enough to know which of `CheckNotEmpty` or `CheckNonempty` is a violation. Fortunately, this undercapitalization type of error is less common and so we focus on the problem of excessive capitalization.
26+
27+
As a heuristic, we flag consecutive capital letters in the proposed name, for example the `XML` part of `XMLHttpRequest`. But there are legitimate words with just a single letter, like the “x” in x-ray or the “p” in p-value. We use an allowlist of exceptions to ignore them.
28+
29+
## Disabling
30+
31+
Users should not hesitate to disable this rule if it makes a mistake. To do so, include the “prose form” of your name, as defined by the Google Java Style Guide, in a comment above the proto element. For example:
32+
33+
```proto
34+
// (-- api-linter: core::0190::service-case=disabled
35+
// Prose service name: "T Shirt Service" --)
36+
service TShirtService { ... }
37+
```
38+
39+
[aip-190]: https://aip.dev/190

rules/aip0190/aip0190.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
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 aip0190
16+
17+
import (
18+
"regexp"
19+
"strings"
20+
21+
"github.com/googleapis/api-linter/v2/lint"
22+
)
23+
24+
// AddRules accepts a register function and registers each of
25+
// this AIP's rules to it.
26+
func AddRules(r lint.RuleRegistry) error {
27+
return r.Register(
28+
190,
29+
messageCase,
30+
methodCase,
31+
serviceCase,
32+
)
33+
}
34+
35+
var termsWithSingleLetterWords = []string{
36+
"AStar", "BTree", "RTree", "MTree", "QTree",
37+
"NGram", "NAry", "NWay", "KWay", "QLearning",
38+
"XAxis", "YAxis", "ZAxis", "KMeans", "PlanA",
39+
"PlanB", "PValue", "TTest", "TValue",
40+
"TypeIError", "TypeIIError", "ZScore",
41+
"FTest", "FScore", "HIndex", "INode", "VNode",
42+
"LValue", "RValue", "PCode", "SExpression", "ETag",
43+
"IFrame", "VCard", "CName", "QName", "OAuth",
44+
"ABTest", "DPad", "ZIndex", "ZOrder", "ZBuffer",
45+
"XRay",
46+
}
47+
48+
// validNameRegex validates that a string is a valid UpperCamelCase name
49+
// without illegal consecutive capital letters, unless they match allowlisted terms.
50+
var validNameRegex = regexp.MustCompile(`^(?:` +
51+
// A capitalized word from the prose form of the name.
52+
`[A-Z][a-z0-9]+|` +
53+
// Or an allowlisted term containing single letter "words".
54+
`(?:` + strings.Join(termsWithSingleLetterWords, "|") + `)[a-z0-9]*` +
55+
`)+$`)
56+
57+
// isValidCamelCase checks if the given name follows AIP-190 case requirements.
58+
func isValidCamelCase(name string) bool {
59+
return validNameRegex.MatchString(name)
60+
}

rules/aip0190/aip0190_test.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// Copyright 2026 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 aip0190
16+
17+
import "testing"
18+
19+
func TestIsValidCamelCase(t *testing.T) {
20+
tests := []struct {
21+
name string
22+
input string
23+
want bool
24+
}{
25+
{"ValidNormal", "CustomerId", true},
26+
{"ValidXmlHttpRequest", "XmlHttpRequest", true},
27+
{"ValidIpv6OnIosOptions", "Ipv6OnIosOptions", true},
28+
{"ValidABTestingService", "ABTestingService", true},
29+
{"ValidHttpABTestingService", "HttpABTestingService", true},
30+
// Multiple allowlist entries with trailing lowercase letters.
31+
{"ValidABTestETaggingRequest", "ABTestETaggingRequest", true},
32+
// Single letter words can also appear at the end of an allowlisted term.
33+
{"ValidPlanB", "ExecutePlanB", true},
34+
// We sometimes require leading and trailing context for single letters .
35+
{"ValidTypeIIError", "TypeIIErrorResponse", true},
36+
{"ValidXRay", "XRay", true},
37+
{"ValidOAuth", "OAuth", true},
38+
{"ValidABTest", "ABTest", true},
39+
{"InvalidTShirtService", "TShirtService", false},
40+
{"InvalidSnakeCase", "snake_case", false},
41+
{"InvalidLowerCamelCase", "lowerCamelCase", false},
42+
{"InvalidLowercase", "lowercase", false},
43+
{"InvalidCustomerID", "CustomerID", false},
44+
{"InvalidXMLHTTPRequest", "XMLHTTPRequest", false},
45+
{"InvalidIPv6OnIOSOptions", "IPv6OnIOSOptions", false},
46+
{"InvalidHTTPABTestingService", "HTTPABTestingService", false},
47+
{"InvalidEMail", "EMail", false},
48+
{"InvalidEBook", "EBook", false},
49+
{"InvalidECommerce", "ECommerce", false},
50+
}
51+
52+
for _, test := range tests {
53+
t.Run(test.name, func(t *testing.T) {
54+
got := isValidCamelCase(test.input)
55+
if got != test.want {
56+
t.Errorf("isValidCamelCase(%q) = %t; want %t", test.input, got, test.want)
57+
}
58+
})
59+
}
60+
}

rules/aip0190/message_case.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// Copyright 2026 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 aip0190
16+
17+
import (
18+
"fmt"
19+
20+
"github.com/googleapis/api-linter/v2/lint"
21+
"google.golang.org/protobuf/reflect/protoreflect"
22+
)
23+
24+
var messageCase = &lint.MessageRule{
25+
Name: lint.NewRuleName(190, "message-case"),
26+
LintMessage: func(m protoreflect.MessageDescriptor) []lint.Problem {
27+
name := string(m.Name())
28+
if !isValidCamelCase(name) {
29+
return []lint.Problem{{
30+
Message: fmt.Sprintf("Message name %q must use UpperCamelCase.", name),
31+
Descriptor: m,
32+
}}
33+
}
34+
return nil
35+
},
36+
}

rules/aip0190/message_case_test.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// Copyright 2026 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 aip0190
16+
17+
import (
18+
"testing"
19+
20+
"github.com/googleapis/api-linter/v2/rules/internal/testutils"
21+
)
22+
23+
func TestMessageCase(t *testing.T) {
24+
for _, test := range []struct {
25+
name string
26+
MessageName string
27+
problems testutils.Problems
28+
}{
29+
{"Valid", "Book", testutils.Problems{}},
30+
{"InvalidCustomerID", "CustomerID", testutils.Problems{{Message: `Message name "CustomerID" must use UpperCamelCase.`}}},
31+
} {
32+
t.Run(test.name, func(t *testing.T) {
33+
f := testutils.ParseProto3Tmpl(t, `
34+
message {{ .MessageName }} {
35+
string name = 1;
36+
}
37+
`, test)
38+
m := f.Messages().Get(0)
39+
if diff := test.problems.SetDescriptor(m).Diff(messageCase.Lint(f)); diff != "" {
40+
t.Error(diff)
41+
}
42+
})
43+
}
44+
}

rules/aip0190/method_case.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// Copyright 2026 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 aip0190
16+
17+
import (
18+
"fmt"
19+
20+
"github.com/googleapis/api-linter/v2/lint"
21+
"google.golang.org/protobuf/reflect/protoreflect"
22+
)
23+
24+
var methodCase = &lint.MethodRule{
25+
Name: lint.NewRuleName(190, "method-case"),
26+
LintMethod: func(m protoreflect.MethodDescriptor) []lint.Problem {
27+
name := string(m.Name())
28+
if !isValidCamelCase(name) {
29+
return []lint.Problem{{
30+
Message: fmt.Sprintf("Method name %q must use UpperCamelCase.", name),
31+
Descriptor: m,
32+
}}
33+
}
34+
return nil
35+
},
36+
}

0 commit comments

Comments
 (0)