Skip to content

Conversation

@parthea
Copy link
Contributor

@parthea parthea commented Mar 13, 2024

Adds support for reading Method settings in the service configuration YAML

https://github.com/googleapis/googleapis/blob/7dab3de7ec79098bb367b6b2ac3815512a49dd56/google/api/client.proto#L325

Towards b/322910372

@product-auto-label product-auto-label bot added the size: m Pull request size is medium. label Mar 13, 2024
@parthea parthea force-pushed the add-support-for-method-settings branch 2 times, most recently from c6124db to 77eaeb2 Compare March 13, 2024 14:41
@product-auto-label product-auto-label bot added size: l Pull request size is large. and removed size: m Pull request size is medium. labels Mar 13, 2024
@parthea parthea force-pushed the add-support-for-method-settings branch from 08eb8e6 to 160f578 Compare March 13, 2024 15:26
@parthea parthea marked this pull request as ready for review March 13, 2024 15:26
@parthea parthea requested a review from a team as a code owner March 13, 2024 15:26
@parthea parthea force-pushed the add-support-for-method-settings branch from 160f578 to ecf0f97 Compare March 13, 2024 15:59
Copy link
Contributor

@vchudnov-g vchudnov-g left a comment

Choose a reason for hiding this comment

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

Some initial comments, all minor (nothing blocking).

I haven't yet looked at the test cases, Will do that next.

@parthea parthea requested review from ohmayr and vchudnov-g March 14, 2024 13:18
- The RPC must be a unary RPC (i.e. streaming RPCs are not supported)
- The field must not be annotated with google.api.field_behavior = REQUIRED.
- The field must be annotated with google.api.field_info.format = UUID4.
- The field presence requirements in AIP-4235 are checked at run time.
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: this item doesn't read right under a list introduced as
'All conditions below must be true". I say take take this out of the list (outdent it)

Suggested change
- The field presence requirements in AIP-4235 are checked at run time.
Note that the field presence requirements in AIP-4235 are checked at run time.

Copy link
Contributor Author

@parthea parthea Mar 18, 2024

Choose a reason for hiding this comment

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

Fixed in 5a2d33b

Checks each `google.api.client.MethodSettings` for validity. If
`google.api.client.MethodSettings.auto_populated_fields`
is set, verify each field against the criteria of AIP-4235
(https://google.aip.dev/client-libraries/4235). All the configurations
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
(https://google.aip.dev/client-libraries/4235). All the configurations
(https://google.aip.dev/client-libraries/4235). All the conditions

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 5a2d33b


if all_errors:
raise ValueError(
f"Fields cannot be automatically populated in the top level "
Copy link
Contributor

Choose a reason for hiding this comment

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

This error header text should be more general. For example, maybe the only error condition is the not method_descriptor above, and nothing in the config even intended to have auto-populated fields.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 5a2d33b

invalid_selectors.append(method_settings.selector)

if all_errors:
raise ValueError(
Copy link
Contributor

Choose a reason for hiding this comment

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

Consider defining a sub-type of ValueError and raising that. Maybe MethodSettingsError or ConfigurationError?

Not strictly necessary; up to you. It would make it easier for us and users to test for the error condition without brittle examination of the error text.

And the custom type gives us the option to internally keep the various individual errors in a list, so in our tests we could check each element of the list contains key parts (eg method selector and "UUID4"). In fact, you could have invalid_selectors be a list of pairs, where the second element of the pair is the list of error strings for that selector. I.e. by doing invalid_selectors.append((method_settings.selector, errors_for_this_method)) the structure could be (in loose pseudo-yaml form)

invalid_selectors:
  - google.example.v1beta1.SomeExample.DoFoo"
     - "Field `auto_fill_me` is a required field"
     - "Field `auto_fill_me` is not a UUID4 field"
  - google.example.v1beta1.SomeExample.DoBar
     - "Field `request_id` is not of type string"
     - "Field `request_id` is a required field"

(To be clear, I do NOT think we should define custom error types for each of these conditions. That seems like too much pedantic overhead at this point. Nor should we use error chaining. But the single custom error type with some internal structure seems reasonable to me.)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 5a2d33b

)
api_schema = api.API.build(fd, "google.example.v1beta1", opts=opts)

exception_message = """Fields cannot be automatically populated in the top level \
Copy link
Contributor

Choose a reason for hiding this comment

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

We shouldn't compare exact text of error messages. We don't want users to rely on that and we shouldn't either. I suggest checking the custom error type (if you go with my other suggestion) and then checking that the error message contains the relevant strings (eg selector strings defined in this test).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 5a2d33b

if method_settings.auto_populated_fields:
# The method must not be a streaming method.
if method_descriptor.client_streaming or method_descriptor.server_streaming:
all_errors += f"Selector {method_settings.selector} is a streaming method. `auto_populated_fields` are only supported for unary methods. "
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
all_errors += f"Selector {method_settings.selector} is a streaming method. `auto_populated_fields` are only supported for unary methods. "
all_errors += f"Selector {method_settings.selector} is a streaming method. Only unary methods support the `auto_populated_fields` setting. "

(Just so we can capitalize the first word of the second sentence ;-) )

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 5a2d33b

for method_settings in service_method_settings:
method_descriptor = self.all_methods.get(method_settings.selector)
if not method_descriptor:
all_errors += f"Selector {method_settings.selector} is not a valid method in this API. "
Copy link
Contributor

Choose a reason for hiding this comment

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

These += result in run-on text. At the very least, insert "\n".

I suggest having all_errors be a list you .append() to, and in the end the exception message can "\n".join(all_errors). This will work nicely with my suggestion for a custom exception and testing that exception.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 5a2d33b

clam = make_field_pb2(
name="clam", type="TYPE_STRING", options=field_options, number=3
)
# Also include a field which is not of type string
Copy link
Contributor

Choose a reason for hiding this comment

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

This tests both non-string and a non-UUID4. I suggest separate test cases.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 5a2d33b

'google.example.v1beta1.SomeExample.Example3'\]. Selector \
google.example.v1beta1.DoesNotExist.Example1 is not a valid method in this API. \
Field `doesnotexist` is not in the top level request message. Field `octopus` is not \
of type string. Field `octopus` is a required field. Field `octopus` is not a UUID4 \
Copy link
Contributor

Choose a reason for hiding this comment

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

Field octopus is testing several failing conditions at once. I think we we should test fields with one failing condition at a time. I think a better, accepted practice is to test simple cases and assume if those work then combinations ought to work too, rather than test a combination case and assume that if it works, its constituent parts will then work by themselves.

(The full coverage solution is to test all combinations, but I think that's overkill here.)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 5a2d33b

for method_key, method_value in service_value.methods.items()
}

def check_method_settings_validity(
Copy link
Contributor

@vchudnov-g vchudnov-g Mar 14, 2024

Choose a reason for hiding this comment

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

nit nit suggestion: def enforce_valid_method_settings

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 5a2d33b

@parthea parthea assigned parthea and unassigned vchudnov-g, dizcology and ohmayr Mar 15, 2024
@parthea parthea assigned vchudnov-g and unassigned parthea Mar 18, 2024
@parthea
Copy link
Contributor Author

parthea commented Mar 18, 2024

@vchudnov-g Please could you take another look?

Copy link
Contributor

@vchudnov-g vchudnov-g left a comment

Choose a reason for hiding this comment

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

Looks good!

I have one suggestion about using YAML rather than JSON in error text, and some more minor, non-blocking suggestions about possible enhancements.

for method_settings in service_method_settings:
# Check if this selector is defind more than once
if method_settings.selector in selectors_seen:
all_errors[method_settings.selector] = ["Duplicate selector"]
Copy link
Contributor

Choose a reason for hiding this comment

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

So this will overwrite any errors found the first time through the selector. It does seem like this is the more important error, but we might as well present all the errors found at once, which would mean appending (or maybe in this case prepending) to the previous list value in the map.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If something wrong with the selector (it is a duplicate, doesn't map to a method or maps to a streaming method), I decided not to continue checking the validity of the auto-populated fields. The issue with the selector should be fixed and providing additional detail in the error message on the auto-populated fields would take away from the more important error with the selector.

f"request message for selectors: {invalid_selectors}. "
f"{all_errors.strip()}"
)
raise MethodSettingsError(json.dumps(all_errors))
Copy link
Contributor

Choose a reason for hiding this comment

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

Why JSON? IMO, YAML would be more concise and hence better for someone getting the exception on their console.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 4161702 and e1c8785

assert api_schema.has_iam_mixin


def get_file_descriptor_proto_for_method_settings_tests(
Copy link
Contributor

@vchudnov-g vchudnov-g Mar 18, 2024

Choose a reason for hiding this comment

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

Thanks for factoring this out. It's much easier to follow!

Minor, non-blocking: I thought I left the following comment when I reviewed last week, but I don't see it on GitHub so it must not have gotten saved. I'll put it here just for your consideration. It's less pressing now that you've done this refactoring, but it would make it nicer when defining the various fields, etc.

If it's trivial to read from a proto "file" (string) in tests, it might be simpler to simply encode the protofile inline rather than calling make_file_pb2 and its ilk... something like

proto_descriptor = make_protodescriptor("""
package xx;

message Foo { 
  string foo = 1;
}
""")

(This is similar in spirit to what you do with the YAML file below!)

This would make it easier to keep the proto concepts being tested next to the code without a lot of boilerplate. If it's not trivial to do now, let's defer it. (But I personally would love to eventually have most of our tests use this declarative style rather than the harder-to-follow procedural set-up of they test cases.)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I created #1986 as a follow up issue to explore whether this is feasible

),
services=(
descriptor_pb2.ServiceDescriptorProto(
name="SomeExample",
Copy link
Contributor

Choose a reason for hiding this comment

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

nit, non-blocking: consider more descriptive names, like ServiceOne and ServiceTwo for theservices, and DoFoo for the methods. Just to make it easier for humans to track which names are used where. (ExampleRequest and ExampleResponse do that, which is great.)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 04fa73e

api_schema = api.API.build(fd, "google.example.v1beta1")
methodsettings = [
client_pb2.MethodSettings(
selector="google.example.v1beta1.DoesNotExist.Example1",
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: Do you want to test a non-existent method in a valid service as well?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed in eac51b6

`client_pb2.MethodSettings.auto_populated_fields` is not found in the top-level
request message of the selector.
"""
fd = get_file_descriptor_proto_for_method_settings_tests()
Copy link
Contributor

Choose a reason for hiding this comment

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

Consider populating some other field of the message.

Also, is it simple to test when the field in auto_populated_fields is nested one level in ? (I.e., it still exists but not at the top-level, so we still get the exception)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done in 1ab06ea

parthea and others added 3 commits March 18, 2024 16:34
@parthea parthea enabled auto-merge (squash) March 19, 2024 02:06
@parthea parthea merged commit 24a23a1 into main Mar 19, 2024
@parthea parthea deleted the add-support-for-method-settings branch March 19, 2024 02:10
@parthea parthea restored the add-support-for-method-settings branch March 19, 2024 02:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size: l Pull request size is large.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants