-
-
Notifications
You must be signed in to change notification settings - Fork 74
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
feat: improve union type narrowing during mapping #493
Conversation
9331f1a
to
210b33e
Compare
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.
LGTM, but please consider rebasing on top of #487 to preserve research/history
$this->arguments = $this->delegate->describeArguments(); | ||
} | ||
|
||
public static function from(mixed $source, ObjectBuilder ...$builders): ObjectBuilder |
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.
s/from/mix, perhaps? You seem to aggregate multiple builders?
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.
It actually filters builders depending on the input, so that only one remains at the end. I added this static constructor to check if there is only one builder, in which case it can be returned as is and does not need the whole filtering process.
return ! $type instanceof CompositeTraversableType | ||
&& ! $type instanceof ShapedArrayType |
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.
Excluded on purpose? 🤔
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.
From a reader's perspective, it's not 100% clear why these checks are in place: shouldn't CompositeTraversableType
and ShapedArrayType
also have accepts()
?
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 a tricky one, and I'm still not convinced this class is at the correct place.
The goal was to save resources by checking if the input is accepted by the current node's type, before trying casting. It led to issues during development of this PR, so I adapted it, but I think I need to find a better way to check whether the input is valid or not. I wrote it in my internal notes, I'll try to improve it in the future.
]; | ||
|
||
$this->body = TypeHelper::containsObject($unionType) | ||
? 'Invalid value {source_value}, it matches at least two types from union.' |
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.
Nice! We should probably add "couldn't take a decision" or such?
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.
What sentence would you have in mind?
FYI the TypeHelper::containsObject
is there because right now the system cannot display a meaningful representation of an object constructor. This is because an object might have several constructors, for instance when taking the Color
class from the docs, its representation should be non-empty-string|array{red: int<0, 255>, green: int<0, 255>, blue: int<0, 255>}
. I have plans to improve it, but this will be in a separate PR.
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.
-'Invalid value {source_value}, it matches at least two types from union.'
-'Invalid value {source_value}, it matches two or more types in an union: cannot take a decision.'
This would already be a good improvement :)
The algorithm used by the mapper to narrow a union type has been greatly improved, and should cover more edge-cases that would previously prevent the mapper from performing well. If an interface, a class or a shaped array is matched by the input, it will take precedence over arrays or scalars. ```php (new \CuyZ\Valinor\MapperBuilder()) ->mapper() ->map( signature: 'array<int>|' . Color::class, source: [ 'red' => 255, 'green' => 128, 'blue' => 64, ], ); // Returns an instance of `Color` ``` When superfluous keys are allowed, if the input matches several interfaces, classes or shaped array, the one with the most children node will be prioritized, as it is considered the most specific type: ```php (new \CuyZ\Valinor\MapperBuilder()) ->allowSuperfluousKeys() ->mapper() ->map( // Even if the first shaped array matches the input, the second one is // used because it's more specific. signature: 'array{foo: int}|array{foo: int, bar: int}', source: [ 'foo' => 42, 'bar' => 1337, ], ); ``` If the input matches several types within the union, a collision will occur and cause the mapper to fail: ```php (new \CuyZ\Valinor\MapperBuilder()) ->mapper() ->map( // Even if the first shaped array matches the input, the second one is // used because it's more specific. signature: 'array{red: int, green: int, blue: int}|' . Color::class, source: [ 'red' => 255, 'green' => 128, 'blue' => 64, ], ); //⚠️ Invalid value array{red: 255, green: 128, blue: 64}, it matches at // least two types from union. ```
efd6267
to
59520c1
Compare
@@ -94,6 +94,18 @@ public static function union_mapping_works_properly_data_provider(): iterable | |||
'assertion' => fn (mixed $result) => self::assertSame(['key' => [42, 1337]], $result), | |||
]; | |||
|
|||
yield 'shaped array representing http response with status 200' => [ | |||
'type' => "array{status: 200, data: array{text: string}} | array{status: 400, error: string}", | |||
'source' => ['status' => 200, 'data' => ['text' => 'foo']], |
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.
Note: The test case in my issue was specifically about allowing superfluous fields, because I don't care about extra fields in the response, i.e. this:
'source' => ['status' => 200, 'data' => ['text' => 'foo', 'extra' => 'bar']],
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.
Yeah, I edited my message already. Just pushed a new test case that should 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.
Awesome, thank you!
There we go! 🎉 |
The algorithm used by the mapper to narrow a union type has been greatly improved, and should cover more edge-cases that would previously prevent the mapper from performing well.
If an interface, a class or a shaped array is matched by the input, it will take precedence over arrays or scalars.
When superfluous keys are allowed, if the input matches several interfaces, classes or shaped array, the one with the most children node will be prioritized, as it is considered the most specific type:
If the input matches several types within the union, a collision will occur and cause the mapper to fail:
Fixes #395
Fixes #356
Fixes #385
Closes #487
Closes #495