-
-
Notifications
You must be signed in to change notification settings - Fork 9.7k
[FrameworkBundle][JsonPath] Add support for custom JsonPath functions #61517
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: 7.4
Are you sure you want to change the base?
[FrameworkBundle][JsonPath] Add support for custom JsonPath functions #61517
Conversation
42a26ef
to
1927fa4
Compare
src/Symfony/Component/JsonPath/Functions/AbstractJsonPathFunction.php
Outdated
Show resolved
Hide resolved
src/Symfony/Component/JsonPath/Functions/JsonPathFunctionArgumentTrait.php
Outdated
Show resolved
Hide resolved
src/Symfony/Component/JsonPath/Functions/JsonPathFunctionInterface.php
Outdated
Show resolved
Hide resolved
src/Symfony/Component/JsonPath/Functions/JsonPathFunctionInterface.php
Outdated
Show resolved
Hide resolved
src/Symfony/Component/JsonPath/Functions/JsonPathFunctionsProvider.php
Outdated
Show resolved
Hide resolved
src/Symfony/Component/JsonPath/Functions/JsonPathFunctionArgumentTrait.php
Outdated
Show resolved
Hide resolved
b5b5819
to
3147335
Compare
Thanks, I addressed all your comments. I introduced a locator approach which should address your concerns in b5b5819 |
question: Instead of introducting a new way to define function can't we use the expression language as has already been done for the components routing and security ? |
This was actually my first idea. However, the RFC defines that you can add custom functions, not necessarily custom expressions. The way it's done currently allows to easily plug on the existing logic. Introducing expression language would bring many challenges (for example, binding JSON data and the path to the expression in some way). I think it's a better choice to not add the dependency and keep the component as standalone as possible (and it's also easier I think). However, nothing would stop one from defining an |
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.
Partial review, I only now realize #61132 should be merged first 😬
src/Symfony/Component/JsonPath/Functions/AsJsonPathFunction.php
Outdated
Show resolved
Hide resolved
src/Symfony/Component/JsonPath/Functions/AsJsonPathFunction.php
Outdated
Show resolved
Hide resolved
323351e
to
27495a8
Compare
public function assertIsQuery(string $arg): void | ||
{ | ||
if (preg_match('/^(true|false|null|\d+(\.\d+)?([eE][+-]?\d+)?|\'[^\']*\'|"[^"]*")$/', $arg)) { | ||
throw new InvalidArgumentException('count() function requires a query argument, not a literal.'); |
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.
count here, length above, this looks specific, not fit for a trait
b66d9a0
to
cbf332a
Compare
} | ||
} | ||
|
||
public function addFunction(JsonPathFunctionInterface $function): void |
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.
I'd rather remove this method, it makes the service mutable
/** | ||
* @param list<string> $args | ||
*/ | ||
public function executeFunction(string $functionName, array $args, mixed $context): mixed |
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.
I'd rather remove this method, it's a needless helper with lots of repetition with getFunction
return $function($args, $context); | ||
} | ||
|
||
public function isComparable(string $functionName): bool |
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.
I'd rather remove this method, it's a needless helper with lots of repetition with getFunction
$value = \is_array($results) ? ($results[0] ?? null) : $results; | ||
|
||
return match (true) { | ||
\is_string($value) => mb_strlen($value), |
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 JSON, it's always utf-8:
\is_string($value) => mb_strlen($value), | |
\is_string($value) => mb_strlen($value, 'UTF-8'), |
* | ||
* @internal | ||
*/ | ||
#[AsJsonPathFunction(name: 'match')] |
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.
I don't think these attributes are needed. They should be removed IMHO
return match (true) { | ||
\is_string($value) && \is_string($pattern) => (bool) @preg_match(\sprintf('/^%s$/u', JsonPathUtils::normalizeRegex($pattern)), $value), | ||
default => false, | ||
}; |
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.
please consider changing to this CS everywhere mach is used:
return match (true) { | |
\is_string($value) && \is_string($pattern) => (bool) @preg_match(\sprintf('/^%s$/u', JsonPathUtils::normalizeRegex($pattern)), $value), | |
default => false, | |
}; | |
return \is_string($value) && \is_string($pattern) && @preg_match(\sprintf('/^%s$/Du', JsonPathUtils::normalizeRegex($pattern)), $value); |
'value' => ValueFunction::class, | ||
] as $name => $function) { | ||
if (!$this->locator?->has($name)) { | ||
$this->addFunction(new $function()); |
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 force instantiating the functions even if ther's not used. I'd rather do the instantiation on demand
@@ -26,9 +25,11 @@ final class JsonPathTokenizer | |||
private const BARE_LITERAL_REGEX = '(true|false|null|\d+(\.\d+)?([eE][+-]?\d+)?|\'[^\']*\'|"[^"]*")'; | |||
|
|||
/** | |||
* @param callable(string $functionName, list<string> $args): void|null $functionValidator |
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.
* @param callable(string $functionName, list<string> $args): void|null $functionValidator | |
* @param callable(string $functionName, list<string> $args): void $functionValidator |
voisd|null
does not make sense
*/ | ||
public function validate(array $args): void; | ||
|
||
public function getName(): string; |
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.
Remove addFunction
in the functions provider also allows removing this method, as that's the only place using that method.
Custom functions are expected to be registered in the service locator using the name as id, which is what would be the source of truth anyway (and which won't be implemented based on this getter in any lazy-loading implementation)
@@ -5,6 +5,8 @@ CHANGELOG | |||
--- | |||
|
|||
* The component is not marked as `@experimental` anymore | |||
* Add `JsonPathFunctionsProviderInterface`, `JsonPathFunctionInterface` and `JsonPathFunctionProviderTrait` to allow registering custom JsonPath functions |
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.
there is no JsonPathFunctionProviderTrait
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.
maybe you meant JsonPathFunctionArgumentTrait
/** | ||
* @return bool Whether the function returns a comparable value or a boolean test result | ||
*/ | ||
public function isComparable(string $functionName): bool; |
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.
the functions provider should only provide functions. Executing them or checking whether the function implement JsonPathComparableFunctionInterface should be implemented in our JsonCrawler consuming that interface
/** | ||
* @author Alexandre Daubois <[email protected]> | ||
*/ | ||
abstract class AbstractJsonPathFunction implements JsonPathFunctionInterface |
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.
Once getName
is dropped, I suggest removing this abstract class. Its only remaining code would be the no-op validate
, and I expect that all functions should at least validate their argument count, making the no-op implementation useless.
try { | ||
return $this->functionsProvider->executeFunction($name, $argList, $context); | ||
} catch (\InvalidArgumentException $e) { | ||
throw new JsonCrawlerException($name, $e->getMessage(), previous: $e); |
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.
using the function name as path being evaluated is wrong.
cbf332a
to
8ea0f8a
Compare
To match the RFC and implement custom functions, |
If we need to expose Nothing as a non-internal API, I suggest reverting the change that replaced the single-case enum with an |
Requires #61132. Only second commit (1927fa4) should be reviewed.
This PR allows to create custom JsonPath functions thanks to the
#[AsJsonPathFunction]
attribute:Then, it's used like this:
All RFC built-in functions are converted to use the new
AbstractJsonPathFunction
to avoid special case handling in the crawler code. This uses the approach detailed here: #60624 (comment). What's also nice is that built-in functions now have their very own unit tests as well.