Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement predefined field constraints #246

Merged
merged 25 commits into from
Sep 17, 2024

Conversation

jchadwick-buf
Copy link
Member

@jchadwick-buf jchadwick-buf commented Sep 4, 2024

  • Renames buf.validate.priv.FieldConstraints to buf.validate.PredefinedConstraint
  • Renames buf.validate.priv.field to buf.validate.predefined
  • Removes package buf.validate.priv to buf.validate
  • Merges buf/validate/expression.proto to buf/validate/validate.proto
  • Removes //proto/protovalidate/buf/validate:expression_proto
  • Switches buf/validate/validate.proto from proto3 syntax to proto2 syntax to enable usage of extensions
  • Adds extension 1000 to max to each ...Rules message
  • Adds test cases for valid and invalid predefined constraints of every possible rule type for both proto2 and protobuf edition 2023
  • Added the CEL constant rule that can be used by predefined constraints to refer to themselves specifically.

A predefined constraint in this schema looks like this:

edition = "2023";

package example.v1;

import "buf/validate/validate.proto";

extend buf.validate.StringRules {
  bool valid_path = 1162 [
    (buf.validate.predefined).cel = {
      id: "string.valid_path"
      expression: "!rule && !this.matches('^([^/.][^/]?|[^/][^/.]|[^/]{3,})(/([^/.][^/]?|[^/][^/.]|[^/]{3,}))*$') ? 'not a valid path: `%s`'.format([this]) : ''"
    }
  ];
}

message File {
  string path = 1 [(buf.validate.field).string.(valid_path) = true];
}

This is a breaking change.

@jchadwick-buf jchadwick-buf changed the title Implement shared field rules Implement shared field constraints Sep 4, 2024
@rodaine rodaine added Feature New feature or request Breaking Change Describes a breaking change to the protovalidate API labels Sep 4, 2024
@rodaine rodaine linked an issue Sep 4, 2024 that may be closed by this pull request
@jchadwick-buf
Copy link
Member Author

After merging main back in, it looks like there is a new lint error:

Error: Enum option "allow_alias" on enum "Ignore" must be false.

After some inspection it appears this may have to do with Buf v1.40.0, as the same code seems to have been passing in Buf v1.39. Further inspection may be needed. (In any case, it doesn't have anything to do with the changes in this PR as best as I can ascertain.)

rodaine
rodaine previously approved these changes Sep 6, 2024
docs/shared-constraints.md Outdated Show resolved Hide resolved
@rodaine rodaine dismissed their stale review September 6, 2024 16:05

see comments

@oliversun9
Copy link
Contributor

After merging main back in, it looks like there is a new lint error:

Error: Enum option "allow_alias" on enum "Ignore" must be false.

After some inspection it appears this may have to do with Buf v1.40.0, as the same code seems to have been passing in Buf v1.39. Further inspection may be needed. (In any case, it doesn't have anything to do with the changes in this PR as best as I can ascertain.)

I think this is a CLI bug, I will take a look.

@rodaine
Copy link
Member

rodaine commented Sep 6, 2024

@oliversun9 @jchadwick-buf once the CLI issue is patched (and released) let's get that in here (or a separate pr that's merged in) as well as fixing the deprecation warnings. I'll merge bypass once the only remaining issue is the breaking change detection.

@oliversun9
Copy link
Contributor

@oliversun9 @jchadwick-buf once the CLI issue is patched (and released) let's get that in here (or a separate pr that's merged in) as well as fixing the deprecation warnings. I'll merge bypass once the only remaining issue is the breaking change detection.

I have a CLI PR up for the fix, but one thing you could do to avoid waiting on the CLI fix to land is to break the ignore comment into two lines, i.e.

// buf:lint:ignore ENUM_NO_ALLOW_ALIAS
// allowance for deprecations. TODO: remove pre-v1.

@jchadwick-buf
Copy link
Member Author

jchadwick-buf commented Sep 9, 2024

@oliversun9 Thanks for the quick fix! It worked, so no need to workaround this. (I did contemplate doing the workaround but it's probably for the best to not merge a PR this big on a Friday afternoon anyways.) I'll try to get all of the ducks in a row here and if there are no objections I'll get this merged and get the runtime PRs under review.

There's definitely more stuff that people will want (rules on arbitrary types, shared message constraints, recursively applying constraints) but I think this is a good opportunity to launch-and-iterate.

Thanks for all of the help, feedback, reviews, etc.!

@jchadwick-buf
Copy link
Member Author

Why do we need the shared package at all?

No particular reason, if anything it's really just a matter of coming up with a good naming scheme for them to be merged into one package. We could just prefix the shared constraint identifiers with Shared/shared_. I'll ask a couple other folks and see if anyone has any particularly nice ideas.

@bufdev
Copy link
Member

bufdev commented Sep 10, 2024

There's only two types in the shared package and neither look like they overlap with anything in the main package, why does anything need prefixing?

@jchadwick-buf
Copy link
Member Author

jchadwick-buf commented Sep 10, 2024

FieldConstraints and field are both present in both packages.

I just pushed a commit that does what you asked (merges everything into validate.proto and converts it all to proto2), but buf.validate.shared.FieldConstraints becomes buf.validate.SharedFieldConstraints and buf.validate.shared.field becomes buf.validate.shared_field. I'm not too concerned about undoing it later because the vast majority of the work here was converting the Go code to use the proto2 interface for Violation which I assume we want to do either way if we want it to all be proto2.

@@ -55,6 +53,81 @@ extend google.protobuf.FieldOptions {
// Rules specify the validations to be performed on this field. By default,
// no validation is performed against a field.
optional FieldConstraints field = 1159;

// Specifies a shared field constraint rule. This option can be specified on
Copy link
Member

Choose a reason for hiding this comment

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

I don't understand the docs here - the types are the same, why can I not share something specified in field? Why is this named shared_field? I'm sure there are answers to all of this, but it's not clear to me how to use these two extensions from the documentation here.

Copy link
Member Author

Choose a reason for hiding this comment

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

I did propose that, and even implemented it in protovalidate-go before the current implementation. As a result of many discussions, I wound up backtracking to something very close to @rodaine's original proposal instead. Personally I still like the idea of unifying it all, but I don't think I really convinced anyone else of it.

The main points are:

  • There is concern that using the same extension for shared rules and custom rules would be confusing.

  • It's confusing to understand when shared rules would apply. For example, consider this:

    extend buf.validate.FloatRules {
      bool is_zero [(buf.validate.field).float.const = 0]
    }
    
    message MyMessage {
      // `is_zero` applies, since it is set.
      float number_1 = 1 [(buf.validate.field).float.(is_zero) = true];
      // `is_zero` still applies, since it is set. Extensions always have field presence.
      float number_2 = 2 [(buf.validate.field).float.(is_zero) = false];
    }

    This isn't a huge problem for the current version because you can do this:

    extend buf.validate.FloatRules {
      bool is_zero [(buf.validate.shared_field).cel = "rule ? this == 0 : true"]
    }
  • Moving the standard rules to use the same mechanism as custom constraints will require making changes to how custom constraints work in each runtime to allow rule values to be plumbed through.

If you would prefer a version that eliminates the need for a SharedFieldConstraints type and allows using standard constraints recursively, I do have some work already done on that front and can switch gears.

I discussed a design that would work this way here: #246 (comment) - but it would need to change at least somewhat, since we can't just blindly proto.Merge everything if we want to be able to e.g. bind rules/rule differently per CEL expression based on where it is in the chain of options.

Copy link
Member

Choose a reason for hiding this comment

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

@rodaine I'd love to sync on this for 5 minutes pre-merge

@jchadwick-buf my main comment here is that regardless of design (and even assuming that this existing design stays), the comments/documentation are not clear to me as a user. I'm reading the comments and am not sure how to use protovalidate - this is a critical thing to tackle

Copy link
Member Author

Choose a reason for hiding this comment

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

I understand what you mean, I'll try to improve the comments at least. I do worry that the reason it's hard to explain is because the design is actually a bit confusing, so it would probably be best if we really did make sure that this is what we want to go with.

Copy link
Member Author

Choose a reason for hiding this comment

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

I just noticed a probable huge part of the confusion, which is that I have had a typo here. I need to switch the second one to SharedFieldConstraints.

Copy link
Member Author

Choose a reason for hiding this comment

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

Now it is PredefinedConstraints and predefined. I'm leaving this comment unresolved just in case I've screwed up any details from the meeting.

>
> Extension numbers may be from 1000 to 536870911, inclusive. Values from 1000
> to 49999 are reserved for [Protobuf Global Extension Registry][1] entries, and
> values from 50000 to 536870911 are reserved for randomly-generated integers.
Copy link
Member

Choose a reason for hiding this comment

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

This isn't actually true. 50000-99999 are reserved for internal use for organizations. The random number thing is actually a somewhat-uncomfortable recommendation for me - I get the birthday paradox, but I'm not thrilled that this is what we're recommending people do. I would prefer saying "choose a number that won't conflict" and leave this up to the user.

Copy link
Member Author

Choose a reason for hiding this comment

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

Thanks, I was mistaken on the ranges somehow, that's what I get for not actually checking.

I agree we shouldn't recommend it, so I've adjusted the documentation altogether:

  • Fixed the ranges.
  • No longer explicitly recommends randomly-generated integers
  • Discourages use of 100000....536870911 for publicly-consumed schemas at all, due to risk of conflicts
  • But keeping a suggestion from Miguel, I kept a note that suggests using a high-quality random source if the user decides to use a randomly generated integer anyway, under the belief that it's better if they do that versus merely choose arbitrary numbers.

WDYT?

Copy link
Member

Choose a reason for hiding this comment

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

  • Remove random number generation references
  • Coalesce extension ranges

Copy link
Member Author

Choose a reason for hiding this comment

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

Done.

`buf.validate.FloatRules`, as follows:

```proto
import "buf/validate/shared/constraints.proto";
Copy link
Member

Choose a reason for hiding this comment

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

Note that these docs no longer match impl

Copy link
Member Author

Choose a reason for hiding this comment

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

Documentation has been updated.

// `Violations` is a collection of `Violation` messages. This message type is returned by
// protovalidate when a proto message fails to meet the requirements set by the `Constraint` validation rules.
// Each individual violation is represented by a `Violation` message.
message Violations {
Copy link
Member

Choose a reason for hiding this comment

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

I wouldn't expect these to be intermixed in order with the *Constraints definitions. These should likely be at the bottom of the file

Copy link
Member Author

Choose a reason for hiding this comment

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

Moved Violations and Violation to end of file.

// constraints that can then/ be set on field options of other messages to
// apply reusable field constraints.
//
// When using randomly generated numbers, please use a high-quality source of
Copy link
Member

Choose a reason for hiding this comment

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

I still don't understand why we are referencing random number generation in the docs - I've read the comments, but this doesn't seem like a good idea.

Copy link
Member Author

Choose a reason for hiding this comment

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

OK, references to random number generation are just removed.

// messages to apply reusable field constraints.
//
// [1]: https://github.com/protocolbuffers/protobuf/blob/main/docs/options.md
extensions 1000 to 99999;
Copy link
Member

Choose a reason for hiding this comment

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

I don't understand why we're splitting up the extension ranges into two sections - I've never seen that before. In the absence of a strong reason, coalesce.

Copy link
Member Author

Choose a reason for hiding this comment

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

Was suggested here and seemed reasonable enough to me. Either way, it's coalesced again now.

@rodaine
Copy link
Member

rodaine commented Sep 17, 2024

:shipit: Thanks @jchadwick-buf! Herculean effort.

@jchadwick-buf jchadwick-buf merged commit 81eafa5 into bufbuild:main Sep 17, 2024
3 of 4 checks passed
@jchadwick-buf jchadwick-buf deleted the shared-field-rules branch September 17, 2024 15:24
jchadwick-buf added a commit to bufbuild/protovalidate-cc that referenced this pull request Sep 23, 2024
Like protovalidate-go and protovalidate-java, we need to adjust the code
to handle dynamic descriptor sets more robustly, since we need to jump
between resolving the protovalidate standard rules and the predefined
rule extensions. This necessitates adding a couple of additions to the
API surface, namely `ValidatorFactory::SetMessageFactory` and
`ValidatorFactory::SetAllowUnknownFields`, which controls instantiation
of unknown dynamic types and whether or not to ignore unresolved rules,
respectively. Like other protovalidate runtimes, we will default to
failing compilation when unknown predefined rules are encountered. This
should not break existing users but will prevent silent incorrect
behavior.

TODO:
- [x] Skip reparse when there are no empty fields—this way we can
avoid pessimizing the common case
- [x] Add an option to fail when unknown rule fields are unable to be
resolved.
- [x] Update for protobuf changes in
bufbuild/protovalidate#246.

This will depend on bufbuild/protovalidate#246.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Breaking Change Describes a breaking change to the protovalidate API Feature New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[BREAKING CHANGE] Reusing Custom Validation Rules
4 participants