Skip to content

fix: Respect google.api.resource singular annotation in batch method rules#1629

Open
stevenwarejones wants to merge 6 commits into
googleapis:mainfrom
stevenwarejones:fix/batch-resource-singular-annotation
Open

fix: Respect google.api.resource singular annotation in batch method rules#1629
stevenwarejones wants to merge 6 commits into
googleapis:mainfrom
stevenwarejones:fix/batch-resource-singular-annotation

Conversation

@stevenwarejones

@stevenwarejones stevenwarejones commented May 13, 2026

Copy link
Copy Markdown

Summary

Batch method rules (AIP-0231, 0233, 0234, 0235) for request-requests-field, request-names-field, and response-resource-field used go-pluralize to singularize the resource name extracted from batch message names. This produced incorrect results for words like "Metadata", which go-pluralize singularizes to "Metadatum" using Latin grammar rules.

This PR adds a DeriveResourceSingular() helper that first checks for a google.api.resource annotation with a singular field on sibling or (transitively) imported resource messages before falling back to go-pluralize. This is analogous to the fix in #1573 for the plural-method-name rule.

When no google.api.resource annotation is found, the function falls back to go-pluralize, preserving existing behavior for all other resources.

Affected Rules

  • core::0231::request-names-field
  • core::0231::response-resource-field
  • core::0233::request-requests-field
  • core::0233::response-resource-field
  • core::0234::request-requests-field
  • core::0234::response-resource-field
  • core::0235::request-names-field
  • core::0235::response-resource-field

Changes

  • rules/internal/utils/string_pluralize.go: Add DeriveResourceSingular(), which searches messages in the current file and its transitive imports (via the existing utils.GetAllDependencies helper) for a google.api.resource annotation with a matching singular value, converting it to UpperCamelCase. Falls back to go-pluralize if no annotation is found.
  • 8 rule files across AIP-0231, 0233, 0234, 0235: Replace pluralize.NewClient().Singular() calls with utils.DeriveResourceSingular().
  • Rule tests across AIP-0231, 0233, 0234, 0235: Add ...ResourceAnnotation test cases for ImpressionMetadata with a google.api.resource annotation, verifying both the request-*-field and response-resource-field rules.

Test plan

  • Added resource annotation tests for all 8 affected rules across AIP-0231, 0233, 0234, and 0235
  • Added unit tests for DeriveResourceSingular() covering: annotation in same file, annotation in imported file, fallback to go-pluralize, uncountable nouns, message name matching, and no-annotation Latin words
  • Added multi-file import tests (TestDeriveResourceSingularImportedFile for a directly imported file, and TestDeriveResourceSingularTransitiveImport for a 3-file service.proto -> common.proto -> resource.proto chain) exercising the transitive import search path
  • go test -race ./... passes (0 failures)
  • golangci-lint run ./... reports 0 issues
  • Verified against cross-media-measurement protos: removing all 6 Metadatum disable comments produces zero false positives with the fixed linter

Issue: #1628

Batch method rules (AIP-0231, 0233, 0234, 0235) for request-requests-field,
request-names-field, and response-resource-field used go-pluralize to
singularize the resource name extracted from batch message names. This
produced incorrect results for words like "Metadata", which go-pluralize
singularizes to "Metadatum" using Latin grammar rules.

Add ResourceSingular() helper that first checks for a google.api.resource
annotation with a singular field on sibling or imported resource messages
before falling back to go-pluralize. This is analogous to the fix in googleapis#1573
for the plural-method-name rule.

Issue: googleapis#1566
@stevenwarejones
stevenwarejones requested a review from a team as a code owner May 13, 2026 23:03
@google-cla

google-cla Bot commented May 13, 2026

Copy link
Copy Markdown

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request replaces the go-pluralize library with a custom ResourceSingular utility across AIP rules 0231, 0233, 0234, and 0235 to correctly handle resource singularization by checking google.api.resource annotations. This change prevents incorrect singularization of words like "Metadata" and includes new test cases for verification. Feedback includes a suggestion to use consistent string trimming methods across the packages and a recommendation to implement recursive import searching to ensure resource annotations are found in complex proto hierarchies.

// The singular form the resource message name; the first letter capitalized.
plural := strings.TrimSuffix(strings.TrimPrefix(string(m.Name()), "BatchGet"), "Response")
resourceMsgName := pluralize.NewClient().Singular(plural)
pluralName := strings.TrimSuffix(strings.TrimPrefix(string(m.Name()), "BatchGet"), "Response")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

For consistency with other rules in this PR (including the request rule in this package), consider using TrimPrefix on the result of TrimSuffix.

Suggested change
pluralName := strings.TrimSuffix(strings.TrimPrefix(string(m.Name()), "BatchGet"), "Response")
pluralName := strings.TrimPrefix(strings.TrimSuffix(string(m.Name()), "Response"), "BatchGet")

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 9c9e3a5 — reordered to TrimPrefix(TrimSuffix(...)) to match all other rules.

Comment on lines +60 to +65
imports := f.Imports()
for i := 0; i < imports.Len(); i++ {
if s := findResourceSingularInFile(pluralName, imports.Get(i).FileDescriptor); s != "" {
return s
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The current implementation only searches direct imports for the resource annotation. In complex proto structures, the resource might be defined in a file that is imported transitively. Consider implementing a recursive search to ensure the resource annotation is found even if it's not in a directly imported file.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good callout. Intentionally keeping this as direct imports only — in standard proto layouts the resource message is in a sibling file directly imported by the service file, so single-depth is sufficient. Recursive search would add complexity for a case we haven't seen in practice. Updated the comment to make this intentional, and added a multi-file test (TestResourceSingularImportedFile) that exercises the import search path. See 9c9e3a5.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On reflection, agreed — implemented recursive search with cycle detection via a visited set in 027891b. Also added that exercises a 3-file chain (service.proto → common.proto → resource.proto) to verify the annotation is found through transitive imports.

@stevenwarejones stevenwarejones changed the title fix: Use google.api.resource singular for batch method field type checks fix: Respect google.api.resource singular annotation in batch method rules May 14, 2026
… lookup

Add test cases across all 8 modified batch rules (AIP-0231, 0233, 0234,
0235) and the ResourceSingular utility to verify that google.api.resource
singular annotations are respected. Uses ImpressionMetadata as the test
resource since go-pluralize incorrectly singularizes it to Metadatum.

Also fix copyright year in string_pluralize.go to 2026 for new code.

Issue: googleapis#1628
… docs

- Make TrimPrefix/TrimSuffix ordering consistent in aip0231
  response_resource_field.go (was TrimSuffix(TrimPrefix(...)), now
  TrimPrefix(TrimSuffix(...)) like all other rules)
- Clarify that single-depth import search is intentional and sufficient
  for standard proto layouts
- Add TestResourceSingularImportedFile exercising the cross-file import
  search path using ParseProtoStrings with separate resource.proto and
  service.proto files

Issue: googleapis#1628

@noahdietz noahdietz left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi there, thanks for the extensive improvement. Couple of comments.

//
// This avoids incorrect singularization of words like "Metadata" (which
// go-pluralize converts to "Metadatum" using Latin grammar rules).
func ResourceSingular(pluralName string, m protoreflect.MessageDescriptor) string {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's use a different function name as I think this is a bit misleading alongside other utilities like GetResourceSingular (which arguably should've been named ResourceSingular).

This might be something like DeriveResourceSingular as it tries to find a resource from a derived name and then get the singular.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Renamed to DeriveResourceSingular.

Comment on lines +219 to +223
m := serviceFile.Messages().Get(0)
got := ResourceSingular("ImpressionMetadata", m)
if got != "ImpressionMetadata" {
t.Errorf("ResourceSingular(\"ImpressionMetadata\") = %q, want \"ImpressionMetadata\"", got)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please make "ImpressionMetadata" a variable want, then use that throughout and change the failure message from using \"ImpressionMetadata\" to %q

Here and everywhere similar

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Hoisted the literal into want := "ImpressionMetadata" and switched the failure messages to %q in both standalone import tests.

Address review feedback:

- Rename ResourceSingular -> DeriveResourceSingular to disambiguate from
  GetResourceSingular; the function derives a resource from a derived
  plural name, then returns its singular. Updates all call sites and tests.
- Replace the bespoke recursive import walk with the existing
  utils.GetAllDependencies helper for transitive-import traversal.
- Hoist the repeated "ImpressionMetadata" literal into a `want` variable
  and use %q in the import-test failure messages.
- Restore the original 2019 copyright year on string_pluralize.go.
@stevenwarejones

Copy link
Copy Markdown
Author

@noahdietz - updated the PR description as well. lmk if you have any other things you'd like done. thx

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants