-
Notifications
You must be signed in to change notification settings - Fork 0
feat: ✨ support union operator in RequiredCheck
#201
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
base: main
Are you sure you want to change the base?
Conversation
| @dataclass(frozen=True) | ||
| class TargetJsonPath: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we allow JSON path unions, one JSON path string can encode multiple completely different JSON paths. I added TargetJsonPath to keep track of these different JSON paths in the context of a RequiredCheck.
As before, I'm keeping track of the beginning of the path and the last field name separately, because I need access to them separately in apply. Otherwise, the general logic hasn't changed, but now we're running it on all JSON paths, not just the one.
|
|
||
| def _jsonpath_to_targets(jsonpath: JSONPath) -> list[TargetJsonPath]: | ||
| """Create a list of `TargetJsonPath`s from a `JSONPath`.""" | ||
| if not jsonpath.segments: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Segments are path parts, e.g., resources, *, name for $.resources[*].name
| if isinstance(last_segment, JSONPathRecursiveDescentSegment): | ||
| raise ValueError( | ||
| f"Cannot use `RequiredCheck` for the JSON path `{full_path}`" | ||
| " because it ends in the recursive descent (`..`) operator." | ||
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Non-final recursive descents are fine, see the tests for an example
| " because it ends in the recursive descent (`..`) operator." | ||
| ) | ||
|
|
||
| selectors = last_segment.selectors |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Selectors are things like field names, array indices, wildcards
| parent = "".join(_map(jsonpath.segments[:-1], lambda segment: str(segment))) | ||
| name_selectors = cast(tuple[NameSelector], selectors) | ||
| return _map( | ||
| name_selectors, | ||
| lambda selector: TargetJsonPath( | ||
| parent=str(compile(parent)), field=selector.name | ||
| ), | ||
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
An easy way of constructing a string JSON path to the parent of the field is to take all segments except the last one (this is the field name), join them, and then compile them. Just joining them into a string without compiling them also creates a valid JSON path but it is in a slightly different notation.
| else: | ||
| first_path = cast(JSONPath, jsonpath.path) | ||
| paths = [first_path] + _map(jsonpath.paths, lambda path: path[1]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is where we handle CompoundJSONPaths, i.e., JSON paths with unions (or intersections).
The structure of CompoundJSONPaths is that they have the first member of the union as path and the rest in paths.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For intersections see #202
| properties = example_package_properties() | ||
| properties["resources"][0]["licenses"] = [{"name": "odc-pddl"}] | ||
| required_check = RequiredCheck( | ||
| jsonpath="$..licenses[*].title", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Match all license titles in the object, no matter how nested the license is
| ] | ||
|
|
||
|
|
||
| def test_required_check_root(): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
More of a sanity check
| "jsonpath", | ||
| [ | ||
| "<><>bad.path", | ||
| "$", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Whether we allow the root node to be required or not is not really important. If there is no root node, something else will surely fail. Refactoring the Pydantic check made it easier to allow this.
| "<><>bad.path", | ||
| "$", | ||
| "..*", | ||
| "created", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is just a shorthand for $.created. After the refactor this is easy to allow.
Description
This PR adds support for the union JSON path operator for
RequiredChecks.To be able to do this more intelligently than a monster regex, I looked into how
jsonpathrepresents a JSON path internally and used their terms/classification. This also allowed me to be more precise about what JSON paths we are not allowing forRequiredChecks.Closes #193
Needs an in-depth review.
Checklist
just run-all