diff --git a/src/Bridge/NextrasOrm/EntityFromIdRule.php b/src/Bridge/NextrasOrm/EntityFromIdRule.php index 70b8b38d..015f19cd 100644 --- a/src/Bridge/NextrasOrm/EntityFromIdRule.php +++ b/src/Bridge/NextrasOrm/EntityFromIdRule.php @@ -13,6 +13,8 @@ use Orisai\ObjectMapper\Exception\InvalidData; use Orisai\ObjectMapper\Exception\ValueDoesNotMatch; use Orisai\ObjectMapper\Meta\Compile\RuleCompileMeta; +use Orisai\ObjectMapper\PhpTypes\Node; +use Orisai\ObjectMapper\PhpTypes\SimpleNode; use Orisai\ObjectMapper\Rules\Rule; use Orisai\ObjectMapper\Types\SimpleValueType; use Orisai\ObjectMapper\Types\Value; @@ -129,4 +131,24 @@ public function createType(Args $args, TypeContext $context): SimpleValueType return new SimpleValueType($args->name); } + /** + * @param EntityFromIdArgs $args + */ + public function getExpectedInputType(Args $args, TypeContext $context): Node + { + $ruleMeta = $args->idRule; + $rule = $context->getRule($ruleMeta->getType()); + $ruleArgs = $ruleMeta->getArgs(); + + return $rule->getExpectedInputType($ruleArgs, $context); + } + + /** + * @param EntityFromIdArgs $args + */ + public function getReturnType(Args $args, TypeContext $context): Node + { + return new SimpleNode($args->entity); + } + } diff --git a/src/PhpTypes/ClassReferenceNode.php b/src/PhpTypes/ClassReferenceNode.php new file mode 100644 index 00000000..5aa5e189 --- /dev/null +++ b/src/PhpTypes/ClassReferenceNode.php @@ -0,0 +1,49 @@ + */ + private array $structure; + + /** + * @param class-string $class + * @param array $structure + */ + public function __construct(string $class, array $structure) + { + $this->class = $class; + $this->structure = $structure; + } + + public function getArrayShape(): string + { + $inline = ''; + $lastKey = array_key_last($this->structure); + foreach ($this->structure as $field => $node) { + $inline .= + $field + . ': ' + . ((string) $node); + + if ($field !== $lastKey) { + $inline .= ', '; + } + } + + return "array{{$inline}}"; + } + + public function __toString(): string + { + return $this->class; + } + +} diff --git a/src/PhpTypes/CompoundNode.php b/src/PhpTypes/CompoundNode.php new file mode 100644 index 00000000..20604cd6 --- /dev/null +++ b/src/PhpTypes/CompoundNode.php @@ -0,0 +1,55 @@ + */ + private array $nodes; + + private string $operator; + + /** + * @param array $nodes + */ + private function __construct(array $nodes, string $operator) + { + $this->nodes = $nodes; + $this->operator = $operator; + } + + /** + * @param array $nodes + */ + public static function createAndType(array $nodes): self + { + return new self($nodes, '&'); + } + + /** + * @param array $nodes + */ + public static function createOrType(array $nodes): self + { + return new self($nodes, '|'); + } + + public function __toString(): string + { + $string = ''; + $lastKey = array_key_last($this->nodes); + foreach ($this->nodes as $key => $node) { + $string .= $node; + + if ($key !== $lastKey) { + $string .= $this->operator; + } + } + + return "($string)"; + } + +} diff --git a/src/PhpTypes/LiteralNode.php b/src/PhpTypes/LiteralNode.php new file mode 100644 index 00000000..2895a96b --- /dev/null +++ b/src/PhpTypes/LiteralNode.php @@ -0,0 +1,41 @@ +value = $value; + } + + public function __toString(): string + { + if (is_bool($this->value)) { + return $this->value ? 'true' : 'false'; + } + + if ($this->value === null) { + return 'null'; + } + + if (is_int($this->value) || is_float($this->value)) { + return var_export($this->value, true); + } + + return "'{$this->value}'"; + } + +} diff --git a/src/PhpTypes/MultiValueNode.php b/src/PhpTypes/MultiValueNode.php new file mode 100644 index 00000000..22903e3c --- /dev/null +++ b/src/PhpTypes/MultiValueNode.php @@ -0,0 +1,30 @@ +name = $name; + $this->key = $key; + $this->item = $item; + } + + public function __toString(): string + { + return $this->name + . '<' + . ($this->key !== null ? "$this->key, " : '') + . ((string) $this->item) + . '>'; + } + +} diff --git a/src/PhpTypes/Node.php b/src/PhpTypes/Node.php new file mode 100644 index 00000000..6bf99afb --- /dev/null +++ b/src/PhpTypes/Node.php @@ -0,0 +1,10 @@ +value = $value; + } + + public function __toString(): string + { + return $this->value; + } + +} diff --git a/src/Rules/AllOfRule.php b/src/Rules/AllOfRule.php index f4c6b7f4..63002c5b 100644 --- a/src/Rules/AllOfRule.php +++ b/src/Rules/AllOfRule.php @@ -4,8 +4,11 @@ use Orisai\ObjectMapper\Args\Args; use Orisai\ObjectMapper\Context\FieldContext; +use Orisai\ObjectMapper\Context\TypeContext; use Orisai\ObjectMapper\Exception\InvalidData; use Orisai\ObjectMapper\Exception\ValueDoesNotMatch; +use Orisai\ObjectMapper\PhpTypes\CompoundNode; +use Orisai\ObjectMapper\PhpTypes\Node; use Orisai\ObjectMapper\Types\CompoundType; use Orisai\ObjectMapper\Types\Value; @@ -59,4 +62,24 @@ protected function createCompoundType(): CompoundType return CompoundType::createAndType(); } + /** + * @param CompoundRuleArgs $args + */ + public function getExpectedInputType(Args $args, TypeContext $context): Node + { + return CompoundNode::createAndType( + $this->getExpectedInputTypeNodes($args, $context), + ); + } + + /** + * @param CompoundRuleArgs $args + */ + public function getReturnType(Args $args, TypeContext $context): Node + { + return CompoundNode::createAndType( + $this->getReturnTypeNodes($args, $context), + ); + } + } diff --git a/src/Rules/AnyOfRule.php b/src/Rules/AnyOfRule.php index 352a0c2a..3263553a 100644 --- a/src/Rules/AnyOfRule.php +++ b/src/Rules/AnyOfRule.php @@ -4,8 +4,11 @@ use Orisai\ObjectMapper\Args\Args; use Orisai\ObjectMapper\Context\FieldContext; +use Orisai\ObjectMapper\Context\TypeContext; use Orisai\ObjectMapper\Exception\InvalidData; use Orisai\ObjectMapper\Exception\ValueDoesNotMatch; +use Orisai\ObjectMapper\PhpTypes\CompoundNode; +use Orisai\ObjectMapper\PhpTypes\Node; use Orisai\ObjectMapper\Types\CompoundType; use Orisai\ObjectMapper\Types\Value; @@ -58,4 +61,24 @@ protected function createCompoundType(): CompoundType return CompoundType::createOrType(); } + /** + * @param CompoundRuleArgs $args + */ + public function getExpectedInputType(Args $args, TypeContext $context): Node + { + return CompoundNode::createOrType( + $this->getExpectedInputTypeNodes($args, $context), + ); + } + + /** + * @param CompoundRuleArgs $args + */ + public function getReturnType(Args $args, TypeContext $context): Node + { + return CompoundNode::createOrType( + $this->getReturnTypeNodes($args, $context), + ); + } + } diff --git a/src/Rules/ArrayEnumRule.php b/src/Rules/ArrayEnumRule.php index 3b2b1dad..9c88a715 100644 --- a/src/Rules/ArrayEnumRule.php +++ b/src/Rules/ArrayEnumRule.php @@ -9,6 +9,9 @@ use Orisai\ObjectMapper\Context\RuleArgsContext; use Orisai\ObjectMapper\Context\TypeContext; use Orisai\ObjectMapper\Exception\ValueDoesNotMatch; +use Orisai\ObjectMapper\PhpTypes\CompoundNode; +use Orisai\ObjectMapper\PhpTypes\LiteralNode; +use Orisai\ObjectMapper\PhpTypes\Node; use Orisai\ObjectMapper\Types\EnumType; use Orisai\ObjectMapper\Types\Value; use function array_keys; @@ -95,4 +98,25 @@ private function getEnumValues(ArrayEnumArgs $args): array : array_values($args->values); } + /** + * @param ArrayEnumArgs $args + */ + public function getExpectedInputType(Args $args, TypeContext $context): Node + { + $types = []; + foreach ($this->getEnumValues($args) as $value) { + $types[] = new LiteralNode($value); + } + + return CompoundNode::createOrType($types); + } + + /** + * @param ArrayEnumArgs $args + */ + public function getReturnType(Args $args, TypeContext $context): Node + { + return $this->getExpectedInputType($args, $context); + } + } diff --git a/src/Rules/ArrayOfRule.php b/src/Rules/ArrayOfRule.php index 375c75cd..8edeb356 100644 --- a/src/Rules/ArrayOfRule.php +++ b/src/Rules/ArrayOfRule.php @@ -10,6 +10,8 @@ use Orisai\ObjectMapper\Exception\InvalidData; use Orisai\ObjectMapper\Exception\ValueDoesNotMatch; use Orisai\ObjectMapper\Meta\Compile\RuleCompileMeta; +use Orisai\ObjectMapper\PhpTypes\MultiValueNode; +use Orisai\ObjectMapper\PhpTypes\Node; use Orisai\ObjectMapper\Types\ArrayType; use Orisai\ObjectMapper\Types\Value; use Orisai\Utils\Arrays\ArrayMerger; @@ -77,7 +79,7 @@ public function getArgsType(): string } /** - * @param mixed $value + * @param mixed $value * @param ArrayOfArgs $args * @return array * @throws ValueDoesNotMatch @@ -163,14 +165,10 @@ public function processValue($value, Args $args, FieldContext $context): array */ public function createType(Args $args, TypeContext $context): ArrayType { - $itemMeta = $args->itemRuleMeta; - $itemRule = $context->getRule($itemMeta->getType()); - $itemArgs = $itemMeta->getArgs(); + [$keyRule, $keyArgs] = $this->getKeyRuleArgs($args, $context); + [$itemRule, $itemArgs] = $this->getItemRuleArgs($args, $context); - $keyMeta = $args->keyRuleMeta; - if ($keyMeta !== null) { - $keyRule = $context->getRule($keyMeta->getType()); - $keyArgs = $keyMeta->getArgs(); + if ($keyRule !== null && $keyArgs !== null) { $keyType = $keyRule->createType($keyArgs, $context); } @@ -190,4 +188,64 @@ public function createType(Args $args, TypeContext $context): ArrayType return $type; } + /** + * @param ArrayOfArgs $args + */ + public function getExpectedInputType(Args $args, TypeContext $context): Node + { + [$keyRule, $keyArgs] = $this->getKeyRuleArgs($args, $context); + [$itemRule, $itemArgs] = $this->getItemRuleArgs($args, $context); + + if ($keyRule !== null && $keyArgs !== null) { + $keyNode = $keyRule->getExpectedInputType($keyArgs, $context); + } + + return new MultiValueNode( + $this->getNodeName($args), + $keyNode ?? null, + $itemRule->getExpectedInputType($itemArgs, $context), + ); + } + + /** + * @param ArrayOfArgs $args + */ + public function getReturnType(Args $args, TypeContext $context): Node + { + [$keyRule, $keyArgs] = $this->getKeyRuleArgs($args, $context); + [$itemRule, $itemArgs] = $this->getItemRuleArgs($args, $context); + + if ($keyRule !== null && $keyArgs !== null) { + $keyNode = $keyRule->getReturnType($keyArgs, $context); + } + + return new MultiValueNode( + $this->getNodeName($args), + $keyNode ?? null, + $itemRule->getReturnType($itemArgs, $context), + ); + } + + private function getNodeName(ArrayOfArgs $args): string + { + return ($args->minItems ?? 0) > 0 ? 'non-empty-array' : 'array'; + } + + /** + * @return array{Rule, Args}|array{null, null} + */ + private function getKeyRuleArgs(ArrayOfArgs $args, TypeContext $context): array + { + $keyRuleMeta = $args->keyRuleMeta; + + if ($keyRuleMeta === null) { + return [null, null]; + } + + $keyRule = $context->getRule($keyRuleMeta->getType()); + $keyArgs = $keyRuleMeta->getArgs(); + + return [$keyRule, $keyArgs]; + } + } diff --git a/src/Rules/BackedEnumRule.php b/src/Rules/BackedEnumRule.php index a522c98f..4b809016 100644 --- a/src/Rules/BackedEnumRule.php +++ b/src/Rules/BackedEnumRule.php @@ -11,6 +11,10 @@ use Orisai\ObjectMapper\Context\RuleArgsContext; use Orisai\ObjectMapper\Context\TypeContext; use Orisai\ObjectMapper\Exception\ValueDoesNotMatch; +use Orisai\ObjectMapper\PhpTypes\CompoundNode; +use Orisai\ObjectMapper\PhpTypes\LiteralNode; +use Orisai\ObjectMapper\PhpTypes\Node; +use Orisai\ObjectMapper\PhpTypes\SimpleNode; use Orisai\ObjectMapper\Types\EnumType; use Orisai\ObjectMapper\Types\Value; use TypeError; @@ -115,4 +119,44 @@ private function getEnumValues(BackedEnumArgs $args): array return $values; } + /** + * @param BackedEnumArgs $args + */ + public function getExpectedInputType(Args $args, TypeContext $context): Node + { + return CompoundNode::createOrType($this->createNodes($args)); + } + + /** + * @param BackedEnumArgs $args + */ + public function getReturnType(Args $args, TypeContext $context): Node + { + $return = new SimpleNode($args->class); + + if ($args->allowUnknown) { + return CompoundNode::createOrType([ + $return, + new SimpleNode('null'), + ]); + } + + return $return; + } + + /** + * @return array + */ + private function createNodes(BackedEnumArgs $args): array + { + $class = $args->class; + + $nodes = []; + foreach ($class::cases() as $case) { + $nodes[] = new LiteralNode($case->value); + } + + return $nodes; + } + } diff --git a/src/Rules/BoolRule.php b/src/Rules/BoolRule.php index 29f46e3d..f0a25cae 100644 --- a/src/Rules/BoolRule.php +++ b/src/Rules/BoolRule.php @@ -8,6 +8,10 @@ use Orisai\ObjectMapper\Context\RuleArgsContext; use Orisai\ObjectMapper\Context\TypeContext; use Orisai\ObjectMapper\Exception\ValueDoesNotMatch; +use Orisai\ObjectMapper\PhpTypes\CompoundNode; +use Orisai\ObjectMapper\PhpTypes\LiteralNode; +use Orisai\ObjectMapper\PhpTypes\Node; +use Orisai\ObjectMapper\PhpTypes\SimpleNode; use Orisai\ObjectMapper\Types\SimpleValueType; use Orisai\ObjectMapper\Types\Value; use function is_bool; @@ -104,4 +108,31 @@ private function tryConvert($value, BoolArgs $args) return $value; } + /** + * @param BoolArgs $args + */ + public function getExpectedInputType(Args $args, TypeContext $context): Node + { + if (!$args->castBoolLike) { + return new SimpleNode('bool'); + } + + $nodes = []; + $nodes[] = new SimpleNode('bool'); + + foreach (self::CastMap as $input => $output) { + $nodes[] = new LiteralNode($input); + } + + return CompoundNode::createOrType($nodes); + } + + /** + * @param BoolArgs $args + */ + public function getReturnType(Args $args, TypeContext $context): Node + { + return new SimpleNode('bool'); + } + } diff --git a/src/Rules/CompoundRule.php b/src/Rules/CompoundRule.php index d1d55180..11e322e0 100644 --- a/src/Rules/CompoundRule.php +++ b/src/Rules/CompoundRule.php @@ -8,6 +8,8 @@ use Orisai\ObjectMapper\Context\RuleArgsContext; use Orisai\ObjectMapper\Context\TypeContext; use Orisai\ObjectMapper\Meta\Compile\RuleCompileMeta; +use Orisai\ObjectMapper\Meta\Runtime\RuleRuntimeMeta; +use Orisai\ObjectMapper\PhpTypes\Node; use Orisai\ObjectMapper\Types\CompoundType; use function count; use function sprintf; @@ -64,9 +66,8 @@ public function createType(Args $args, TypeContext $context): CompoundType $type = $this->createCompoundType(); foreach ($args->rules as $key => $nestedRuleMeta) { - $nestedRule = $context->getRule($nestedRuleMeta->getType()); - $nestedRuleArgs = $nestedRuleMeta->getArgs(); - $type->addSubtype($key, $nestedRule->createType($nestedRuleArgs, $context)); + [$subNodeRule, $subNodeArgs] = $this->getSubNodeRuleArgs($nestedRuleMeta, $context); + $type->addSubtype($key, $subNodeRule->createType($subNodeArgs, $context)); } return $type; @@ -74,4 +75,44 @@ public function createType(Args $args, TypeContext $context): CompoundType abstract protected function createCompoundType(): CompoundType; + /** + * @param RuleRuntimeMeta $meta + * @return array{Rule, Args} + */ + private function getSubNodeRuleArgs(RuleRuntimeMeta $meta, TypeContext $context): array + { + $rule = $context->getRule($meta->getType()); + $args = $meta->getArgs(); + + return [$rule, $args]; + } + + /** + * @return array + */ + protected function getExpectedInputTypeNodes(CompoundRuleArgs $args, TypeContext $context): array + { + $nodes = []; + foreach ($args->rules as $ruleMeta) { + [$subNodeRule, $subNodeArgs] = $this->getSubNodeRuleArgs($ruleMeta, $context); + $nodes[] = $subNodeRule->getExpectedInputType($subNodeArgs, $context); + } + + return $nodes; + } + + /** + * @return array + */ + protected function getReturnTypeNodes(CompoundRuleArgs $args, TypeContext $context): array + { + $nodes = []; + foreach ($args->rules as $ruleMeta) { + [$subNodeRule, $subNodeArgs] = $this->getSubNodeRuleArgs($ruleMeta, $context); + $nodes[] = $subNodeRule->getReturnType($subNodeArgs, $context); + } + + return $nodes; + } + } diff --git a/src/Rules/DateTimeRule.php b/src/Rules/DateTimeRule.php index f45a13ea..8e349924 100644 --- a/src/Rules/DateTimeRule.php +++ b/src/Rules/DateTimeRule.php @@ -13,6 +13,9 @@ use Orisai\ObjectMapper\Context\RuleArgsContext; use Orisai\ObjectMapper\Context\TypeContext; use Orisai\ObjectMapper\Exception\ValueDoesNotMatch; +use Orisai\ObjectMapper\PhpTypes\CompoundNode; +use Orisai\ObjectMapper\PhpTypes\Node; +use Orisai\ObjectMapper\PhpTypes\SimpleNode; use Orisai\ObjectMapper\Types\SimpleValueType; use Orisai\ObjectMapper\Types\Value; use ReflectionClass; @@ -172,4 +175,31 @@ public function createType(Args $args, TypeContext $context): SimpleValueType return $type; } + /** + * @param DateTimeArgs $args + */ + public function getExpectedInputType(Args $args, TypeContext $context): Node + { + if ($args->format === self::FormatTimestamp) { + return new SimpleNode('int'); + } + + if ($args->format === self::FormatAny) { + return CompoundNode::createOrType([ + new SimpleNode('int'), + new SimpleNode('string'), + ]); + } + + return new SimpleNode('string'); + } + + /** + * @param DateTimeArgs $args + */ + public function getReturnType(Args $args, TypeContext $context): Node + { + return new SimpleNode($args->type); + } + } diff --git a/src/Rules/FloatRule.php b/src/Rules/FloatRule.php index 87d9d085..0e32af1d 100644 --- a/src/Rules/FloatRule.php +++ b/src/Rules/FloatRule.php @@ -9,6 +9,9 @@ use Orisai\ObjectMapper\Context\RuleArgsContext; use Orisai\ObjectMapper\Context\TypeContext; use Orisai\ObjectMapper\Exception\ValueDoesNotMatch; +use Orisai\ObjectMapper\PhpTypes\CompoundNode; +use Orisai\ObjectMapper\PhpTypes\Node; +use Orisai\ObjectMapper\PhpTypes\SimpleNode; use Orisai\ObjectMapper\Types\SimpleValueType; use Orisai\ObjectMapper\Types\Value; use function is_float; @@ -185,4 +188,53 @@ private function tryConvert(string $value) return $value; } + /** + * @param FloatArgs $args + */ + public function getExpectedInputType(Args $args, TypeContext $context): Node + { + $intNode = $this->createFloatNode($args); + + if (!$args->castNumericString) { + return $intNode; + } + + return CompoundNode::createOrType([ + $intNode, + new SimpleNode('numeric-string'), + ]); + } + + /** + * @param FloatArgs $args + */ + public function getReturnType(Args $args, TypeContext $context): Node + { + return $this->createFloatNode($args); + } + + private function createFloatNode(FloatArgs $args): Node + { + $min = $args->min; + if ($args->unsigned && ($min ?? 0.0) <= 0.0) { + $min = 0.0; + } + + $max = $args->max; + + if ($min !== null && $max !== null) { + return new SimpleNode("float<$min, $max>"); + } + + if ($min !== null) { + return new SimpleNode("float<$min, max>"); + } + + if ($max !== null) { + return new SimpleNode("float"); + } + + return new SimpleNode('float'); + } + } diff --git a/src/Rules/InstanceRule.php b/src/Rules/InstanceRule.php index 87c83ec0..c45cf69f 100644 --- a/src/Rules/InstanceRule.php +++ b/src/Rules/InstanceRule.php @@ -9,6 +9,8 @@ use Orisai\ObjectMapper\Context\RuleArgsContext; use Orisai\ObjectMapper\Context\TypeContext; use Orisai\ObjectMapper\Exception\ValueDoesNotMatch; +use Orisai\ObjectMapper\PhpTypes\Node; +use Orisai\ObjectMapper\PhpTypes\SimpleNode; use Orisai\ObjectMapper\Types\SimpleValueType; use Orisai\ObjectMapper\Types\Value; use function class_exists; @@ -70,4 +72,20 @@ public function createType(Args $args, TypeContext $context): SimpleValueType return new SimpleValueType($args->type); } + /** + * @param InstanceArgs $args + */ + public function getExpectedInputType(Args $args, TypeContext $context): Node + { + return new SimpleNode($args->type); + } + + /** + * @param InstanceArgs $args + */ + public function getReturnType(Args $args, TypeContext $context): Node + { + return $this->getExpectedInputType($args, $context); + } + } diff --git a/src/Rules/IntRule.php b/src/Rules/IntRule.php index 0ec5621b..8844b97e 100644 --- a/src/Rules/IntRule.php +++ b/src/Rules/IntRule.php @@ -9,6 +9,9 @@ use Orisai\ObjectMapper\Context\RuleArgsContext; use Orisai\ObjectMapper\Context\TypeContext; use Orisai\ObjectMapper\Exception\ValueDoesNotMatch; +use Orisai\ObjectMapper\PhpTypes\CompoundNode; +use Orisai\ObjectMapper\PhpTypes\Node; +use Orisai\ObjectMapper\PhpTypes\SimpleNode; use Orisai\ObjectMapper\Types\SimpleValueType; use Orisai\ObjectMapper\Types\Value; use function is_int; @@ -181,4 +184,53 @@ private function tryConvert(string $value) return $value; } + /** + * @param IntArgs $args + */ + public function getExpectedInputType(Args $args, TypeContext $context): Node + { + $intNode = $this->createIntNode($args); + + if (!$args->castNumericString) { + return $intNode; + } + + return CompoundNode::createOrType([ + $intNode, + new SimpleNode('numeric-string'), + ]); + } + + /** + * @param IntArgs $args + */ + public function getReturnType(Args $args, TypeContext $context): Node + { + return $this->createIntNode($args); + } + + private function createIntNode(IntArgs $args): Node + { + $min = $args->min; + if ($args->unsigned && ($min ?? 0) <= 0) { + $min = 0; + } + + $max = $args->max; + + if ($min !== null && $max !== null) { + return new SimpleNode("int<$min, $max>"); + } + + if ($min !== null) { + return new SimpleNode("int<$min, max>"); + } + + if ($max !== null) { + return new SimpleNode("int"); + } + + return new SimpleNode('int'); + } + } diff --git a/src/Rules/ListOfRule.php b/src/Rules/ListOfRule.php index fc957749..155fadfa 100644 --- a/src/Rules/ListOfRule.php +++ b/src/Rules/ListOfRule.php @@ -10,6 +10,8 @@ use Orisai\ObjectMapper\Exception\InvalidData; use Orisai\ObjectMapper\Exception\ValueDoesNotMatch; use Orisai\ObjectMapper\Meta\Compile\RuleCompileMeta; +use Orisai\ObjectMapper\PhpTypes\MultiValueNode; +use Orisai\ObjectMapper\PhpTypes\Node; use Orisai\ObjectMapper\Types\ArrayType; use Orisai\ObjectMapper\Types\SimpleValueType; use Orisai\ObjectMapper\Types\Value; @@ -144,9 +146,7 @@ public function processValue($value, Args $args, FieldContext $context): array */ public function createType(Args $args, TypeContext $context): ArrayType { - $itemMeta = $args->itemRuleMeta; - $itemRule = $context->getRule($itemMeta->getType()); - $itemArgs = $itemMeta->getArgs(); + [$itemRule, $itemArgs] = $this->getItemRuleArgs($args, $context); $type = ArrayType::forList( $this->createKeyType(), @@ -172,4 +172,37 @@ private function createKeyType(): SimpleValueType return $type; } + /** + * @param MultiValueArgs $args + */ + public function getExpectedInputType(Args $args, TypeContext $context): Node + { + [$itemRule, $itemArgs] = $this->getItemRuleArgs($args, $context); + + return new MultiValueNode( + $this->getNodeName($args), + null, + $itemRule->getExpectedInputType($itemArgs, $context), + ); + } + + /** + * @param MultiValueArgs $args + */ + public function getReturnType(Args $args, TypeContext $context): Node + { + [$itemRule, $itemArgs] = $this->getItemRuleArgs($args, $context); + + return new MultiValueNode( + $this->getNodeName($args), + null, + $itemRule->getReturnType($itemArgs, $context), + ); + } + + private function getNodeName(MultiValueArgs $args): string + { + return ($args->minItems ?? 0) > 0 ? 'non-empty-list' : 'list'; + } + } diff --git a/src/Rules/MappedObjectRule.php b/src/Rules/MappedObjectRule.php index 07bff135..14debe7e 100644 --- a/src/Rules/MappedObjectRule.php +++ b/src/Rules/MappedObjectRule.php @@ -9,7 +9,12 @@ use Orisai\ObjectMapper\Context\TypeContext; use Orisai\ObjectMapper\Exception\InvalidData; use Orisai\ObjectMapper\MappedObject; +use Orisai\ObjectMapper\Meta\Runtime\PropertyRuntimeMeta; +use Orisai\ObjectMapper\Meta\Runtime\RuleRuntimeMeta; use Orisai\ObjectMapper\Modifiers\FieldNameModifier; +use Orisai\ObjectMapper\PhpTypes\ClassReferenceNode; +use Orisai\ObjectMapper\PhpTypes\Node; +use Orisai\ObjectMapper\PhpTypes\SimpleNode; use Orisai\ObjectMapper\Types\MappedObjectType; use function array_keys; use function assert; @@ -72,12 +77,9 @@ public function createType(Args $args, TypeContext $context): MappedObjectType foreach ($propertyNames as $propertyName) { $propertyMeta = $propertiesMeta[$propertyName]; - $propertyRuleMeta = $propertyMeta->getRule(); - $propertyRule = $context->getRule($propertyRuleMeta->getType()); - $propertyArgs = $propertyRuleMeta->getArgs(); + [$propertyRule, $propertyArgs] = $this->getPropertyRuleArgs($propertyMeta->getRule(), $context); - $fieldNameMeta = $propertyMeta->getModifier(FieldNameModifier::class); - $fieldName = $fieldNameMeta !== null ? $fieldNameMeta->getArgs()->name : $propertyName; + $fieldName = $this->getFieldName($propertyMeta, $propertyName); $type->addField( $fieldName, @@ -88,4 +90,57 @@ public function createType(Args $args, TypeContext $context): MappedObjectType return $type; } + /** + * @param MappedObjectArgs $args + */ + public function getExpectedInputType(Args $args, TypeContext $context): ClassReferenceNode + { + $propertiesMeta = $context->getMeta($args->type)->getProperties(); + $propertyNames = array_keys($propertiesMeta); + + $structure = []; + foreach ($propertyNames as $propertyName) { + $propertyMeta = $propertiesMeta[$propertyName]; + [$propertyRule, $propertyArgs] = $this->getPropertyRuleArgs($propertyMeta->getRule(), $context); + + $fieldName = $this->getFieldName($propertyMeta, $propertyName); + + $structure[$fieldName] = $propertyRule->getExpectedInputType($propertyArgs, $context); + } + + return new ClassReferenceNode($args->type, $structure); + } + + /** + * @param MappedObjectArgs $args + */ + public function getReturnType(Args $args, TypeContext $context): Node + { + return new SimpleNode($args->type); + } + + /** + * @param RuleRuntimeMeta $meta + * @return array{Rule, Args} + */ + private function getPropertyRuleArgs(RuleRuntimeMeta $meta, TypeContext $context): array + { + $rule = $context->getRule($meta->getType()); + $args = $meta->getArgs(); + + return [$rule, $args]; + } + + /** + * @return int|string + */ + private function getFieldName(PropertyRuntimeMeta $propertyMeta, string $propertyName) + { + $fieldNameMeta = $propertyMeta->getModifier(FieldNameModifier::class); + + return $fieldNameMeta !== null + ? $fieldNameMeta->getArgs()->name + : $propertyName; + } + } diff --git a/src/Rules/MixedRule.php b/src/Rules/MixedRule.php index 132b4833..fd15e113 100644 --- a/src/Rules/MixedRule.php +++ b/src/Rules/MixedRule.php @@ -6,6 +6,8 @@ use Orisai\ObjectMapper\Args\EmptyArgs; use Orisai\ObjectMapper\Context\FieldContext; use Orisai\ObjectMapper\Context\TypeContext; +use Orisai\ObjectMapper\PhpTypes\Node; +use Orisai\ObjectMapper\PhpTypes\SimpleNode; use Orisai\ObjectMapper\Types\SimpleValueType; /** @@ -34,4 +36,20 @@ public function createType(Args $args, TypeContext $context): SimpleValueType return new SimpleValueType('mixed'); } + /** + * @param EmptyArgs $args + */ + public function getExpectedInputType(Args $args, TypeContext $context): Node + { + return new SimpleNode('mixed'); + } + + /** + * @param EmptyArgs $args + */ + public function getReturnType(Args $args, TypeContext $context): Node + { + return $this->getExpectedInputType($args, $context); + } + } diff --git a/src/Rules/MultiValueRule.php b/src/Rules/MultiValueRule.php index 89ac28d8..b7c942ba 100644 --- a/src/Rules/MultiValueRule.php +++ b/src/Rules/MultiValueRule.php @@ -2,6 +2,9 @@ namespace Orisai\ObjectMapper\Rules; +use Orisai\ObjectMapper\Args\Args; +use Orisai\ObjectMapper\Context\TypeContext; + /** * @phpstan-template T of MultiValueArgs * @phpstan-implements Rule @@ -16,4 +19,17 @@ abstract class MultiValueRule implements Rule MaxItems = 'maxItems', MergeDefaults = 'mergeDefaults'; + /** + * @return array{Rule, Args} + */ + protected function getItemRuleArgs(MultiValueArgs $args, TypeContext $context): array + { + $itemRuleMeta = $args->itemRuleMeta; + + $itemRule = $context->getRule($itemRuleMeta->getType()); + $itemArgs = $itemRuleMeta->getArgs(); + + return [$itemRule, $itemArgs]; + } + } diff --git a/src/Rules/NullRule.php b/src/Rules/NullRule.php index 507d0c93..0ea952bd 100644 --- a/src/Rules/NullRule.php +++ b/src/Rules/NullRule.php @@ -8,6 +8,10 @@ use Orisai\ObjectMapper\Context\RuleArgsContext; use Orisai\ObjectMapper\Context\TypeContext; use Orisai\ObjectMapper\Exception\ValueDoesNotMatch; +use Orisai\ObjectMapper\PhpTypes\CompoundNode; +use Orisai\ObjectMapper\PhpTypes\LiteralNode; +use Orisai\ObjectMapper\PhpTypes\Node; +use Orisai\ObjectMapper\PhpTypes\SimpleNode; use Orisai\ObjectMapper\Types\SimpleValueType; use Orisai\ObjectMapper\Types\Value; use function is_string; @@ -40,7 +44,7 @@ public function getArgsType(): string } /** - * @param mixed $value + * @param mixed $value * @param NullArgs $args * @return null * @throws ValueDoesNotMatch @@ -85,4 +89,27 @@ private function tryConvert($value, NullArgs $args) return $value; } + /** + * @param NullArgs $args + */ + public function getExpectedInputType(Args $args, TypeContext $context): Node + { + if ($args->castEmptyString) { + return CompoundNode::createOrType([ + new SimpleNode('null'), + new LiteralNode(''), + ]); + } + + return new SimpleNode('null'); + } + + /** + * @param NullArgs $args + */ + public function getReturnType(Args $args, TypeContext $context): Node + { + return new SimpleNode('null'); + } + } diff --git a/src/Rules/ObjectRule.php b/src/Rules/ObjectRule.php index 2dcb3196..71315551 100644 --- a/src/Rules/ObjectRule.php +++ b/src/Rules/ObjectRule.php @@ -7,6 +7,8 @@ use Orisai\ObjectMapper\Context\FieldContext; use Orisai\ObjectMapper\Context\TypeContext; use Orisai\ObjectMapper\Exception\ValueDoesNotMatch; +use Orisai\ObjectMapper\PhpTypes\Node; +use Orisai\ObjectMapper\PhpTypes\SimpleNode; use Orisai\ObjectMapper\Types\SimpleValueType; use Orisai\ObjectMapper\Types\Value; use function is_object; @@ -41,4 +43,20 @@ public function createType(Args $args, TypeContext $context): SimpleValueType return new SimpleValueType('object'); } + /** + * @param EmptyArgs $args + */ + public function getExpectedInputType(Args $args, TypeContext $context): Node + { + return new SimpleNode('object'); + } + + /** + * @param EmptyArgs $args + */ + public function getReturnType(Args $args, TypeContext $context): Node + { + return $this->getExpectedInputType($args, $context); + } + } diff --git a/src/Rules/Rule.php b/src/Rules/Rule.php index c5616873..f83a82ad 100644 --- a/src/Rules/Rule.php +++ b/src/Rules/Rule.php @@ -8,6 +8,7 @@ use Orisai\ObjectMapper\Context\TypeContext; use Orisai\ObjectMapper\Exception\InvalidData; use Orisai\ObjectMapper\Exception\ValueDoesNotMatch; +use Orisai\ObjectMapper\PhpTypes\Node; use Orisai\ObjectMapper\Types\Type; /** @@ -41,4 +42,14 @@ public function processValue($value, Args $args, FieldContext $context); */ public function createType(Args $args, TypeContext $context): Type; + /** + * @phpstan-param T_ARGS $args + */ + public function getExpectedInputType(Args $args, TypeContext $context): Node; + + /** + * @phpstan-param T_ARGS $args + */ + public function getReturnType(Args $args, TypeContext $context): Node; + } diff --git a/src/Rules/ScalarRule.php b/src/Rules/ScalarRule.php index 064b08b9..c3ce1c81 100644 --- a/src/Rules/ScalarRule.php +++ b/src/Rules/ScalarRule.php @@ -7,6 +7,9 @@ use Orisai\ObjectMapper\Context\FieldContext; use Orisai\ObjectMapper\Context\TypeContext; use Orisai\ObjectMapper\Exception\ValueDoesNotMatch; +use Orisai\ObjectMapper\PhpTypes\CompoundNode; +use Orisai\ObjectMapper\PhpTypes\Node; +use Orisai\ObjectMapper\PhpTypes\SimpleNode; use Orisai\ObjectMapper\Types\CompoundType; use Orisai\ObjectMapper\Types\SimpleValueType; use Orisai\ObjectMapper\Types\Value; @@ -70,4 +73,25 @@ private function getSubtypes(): array ]; } + /** + * @param EmptyArgs $args + */ + public function getExpectedInputType(Args $args, TypeContext $context): Node + { + return CompoundNode::createOrType([ + new SimpleNode('int'), + new SimpleNode('float'), + new SimpleNode('string'), + new SimpleNode('bool'), + ]); + } + + /** + * @param EmptyArgs $args + */ + public function getReturnType(Args $args, TypeContext $context): Node + { + return $this->getExpectedInputType($args, $context); + } + } diff --git a/src/Rules/StringRule.php b/src/Rules/StringRule.php index aefe4570..0419dbec 100644 --- a/src/Rules/StringRule.php +++ b/src/Rules/StringRule.php @@ -8,6 +8,8 @@ use Orisai\ObjectMapper\Context\RuleArgsContext; use Orisai\ObjectMapper\Context\TypeContext; use Orisai\ObjectMapper\Exception\ValueDoesNotMatch; +use Orisai\ObjectMapper\PhpTypes\Node; +use Orisai\ObjectMapper\PhpTypes\SimpleNode; use Orisai\ObjectMapper\Types\SimpleValueType; use Orisai\ObjectMapper\Types\Value; use function is_string; @@ -126,4 +128,24 @@ public function createType(Args $args, TypeContext $context): SimpleValueType return $type; } + /** + * @param StringArgs $args + */ + public function getExpectedInputType(Args $args, TypeContext $context): Node + { + if ($args->notEmpty || ($args->minLength ?? 0) > 0) { + return new SimpleNode('non-empty-string'); + } + + return new SimpleNode('string'); + } + + /** + * @param StringArgs $args + */ + public function getReturnType(Args $args, TypeContext $context): Node + { + return $this->getExpectedInputType($args, $context); + } + } diff --git a/src/Rules/UrlRule.php b/src/Rules/UrlRule.php index 7c8693e9..fe533ccb 100644 --- a/src/Rules/UrlRule.php +++ b/src/Rules/UrlRule.php @@ -7,6 +7,8 @@ use Orisai\ObjectMapper\Context\FieldContext; use Orisai\ObjectMapper\Context\TypeContext; use Orisai\ObjectMapper\Exception\ValueDoesNotMatch; +use Orisai\ObjectMapper\PhpTypes\Node; +use Orisai\ObjectMapper\PhpTypes\SimpleNode; use Orisai\ObjectMapper\Types\SimpleValueType; use Orisai\ObjectMapper\Types\Value; use function is_string; @@ -42,4 +44,20 @@ public function createType(Args $args, TypeContext $context): SimpleValueType return new SimpleValueType('url'); } + /** + * @param EmptyArgs $args + */ + public function getExpectedInputType(Args $args, TypeContext $context): Node + { + return new SimpleNode('string'); + } + + /** + * @param EmptyArgs $args + */ + public function getReturnType(Args $args, TypeContext $context): Node + { + return new SimpleNode('string'); + } + } diff --git a/tests/Doubles/AlwaysInvalidRule.php b/tests/Doubles/AlwaysInvalidRule.php index 40df1f4d..b30eeadb 100644 --- a/tests/Doubles/AlwaysInvalidRule.php +++ b/tests/Doubles/AlwaysInvalidRule.php @@ -7,6 +7,8 @@ use Orisai\ObjectMapper\Context\FieldContext; use Orisai\ObjectMapper\Context\TypeContext; use Orisai\ObjectMapper\Exception\ValueDoesNotMatch; +use Orisai\ObjectMapper\PhpTypes\Node; +use Orisai\ObjectMapper\PhpTypes\SimpleNode; use Orisai\ObjectMapper\Rules\NoArgsRule; use Orisai\ObjectMapper\Rules\Rule; use Orisai\ObjectMapper\Types\MessageType; @@ -39,4 +41,20 @@ public function createType(Args $args, TypeContext $context): MessageType return new MessageType('Always invalid'); } + /** + * @param EmptyArgs $args + */ + public function getExpectedInputType(Args $args, TypeContext $context): Node + { + return new SimpleNode('TODO'); + } + + /** + * @param EmptyArgs $args + */ + public function getReturnType(Args $args, TypeContext $context): Node + { + return new SimpleNode('TODO'); + } + } diff --git a/tests/Doubles/IODoesNotMatchVO.php b/tests/Doubles/IODoesNotMatchVO.php new file mode 100644 index 00000000..9dd289c3 --- /dev/null +++ b/tests/Doubles/IODoesNotMatchVO.php @@ -0,0 +1,19 @@ +getArrayShape()); + } + + public function provide(): Generator + { + yield [ + new ClassReferenceNode(stdClass::class, []), + stdClass::class, + 'array{}', + ]; + + yield [ + new ClassReferenceNode(MappedObject::class, [ + 0 => new MultiValueNode('array', new SimpleNode('string'), new SimpleNode('int')), + ]), + MappedObject::class, + 'array{0: array}', + ]; + + yield [ + new ClassReferenceNode(MappedObject::class, [ + 0 => new SimpleNode('int'), + 'key' => new SimpleNode('string'), + 2 => new LiteralNode(true), + 3 => new LiteralNode(''), + ]), + MappedObject::class, + "array{0: int, key: string, 2: true, 3: ''}", + ]; + } + +} diff --git a/tests/Unit/PhpTypes/CompoundNodeTest.php b/tests/Unit/PhpTypes/CompoundNodeTest.php new file mode 100644 index 00000000..3af633ca --- /dev/null +++ b/tests/Unit/PhpTypes/CompoundNodeTest.php @@ -0,0 +1,52 @@ +', + ]; + + yield [ + new MultiValueNode( + 'list', + null, + new SimpleNode('string'), + ), + 'list', + ]; + } + +} diff --git a/tests/Unit/PhpTypes/SimpleNodeTest.php b/tests/Unit/PhpTypes/SimpleNodeTest.php new file mode 100644 index 00000000..71b801f8 --- /dev/null +++ b/tests/Unit/PhpTypes/SimpleNodeTest.php @@ -0,0 +1,33 @@ +'), + 'int', + ]; + } + +} diff --git a/tests/Unit/Rules/AllOfRuleTest.php b/tests/Unit/Rules/AllOfRuleTest.php index 907901bc..fdf8461a 100644 --- a/tests/Unit/Rules/AllOfRuleTest.php +++ b/tests/Unit/Rules/AllOfRuleTest.php @@ -2,6 +2,7 @@ namespace Tests\Orisai\ObjectMapper\Unit\Rules; +use Generator; use Orisai\Exceptions\Logic\InvalidArgument; use Orisai\ObjectMapper\Args\EmptyArgs; use Orisai\ObjectMapper\Exception\ValueDoesNotMatch; @@ -12,6 +13,10 @@ use Orisai\ObjectMapper\Rules\MappedObjectArgs; use Orisai\ObjectMapper\Rules\MappedObjectRule; use Orisai\ObjectMapper\Rules\MixedRule; +use Orisai\ObjectMapper\Rules\NullArgs; +use Orisai\ObjectMapper\Rules\NullRule; +use Orisai\ObjectMapper\Rules\StringArgs; +use Orisai\ObjectMapper\Rules\StringRule; use Orisai\ObjectMapper\Types\CompoundType; use Orisai\ObjectMapper\Types\MessageType; use Orisai\ObjectMapper\Types\SimpleValueType; @@ -169,4 +174,41 @@ public function testInnerRuleResolved(): void ); } + /** + * @dataProvider providePhpNode + */ + public function testPhpNode(CompoundRuleArgs $args, string $input, string $output): void + { + self::assertSame( + $input, + (string) $this->rule->getExpectedInputType($args, $this->fieldContext()), + ); + + self::assertSame( + $output, + (string) $this->rule->getReturnType($args, $this->fieldContext()), + ); + } + + public function providePhpNode(): Generator + { + yield [ + new CompoundRuleArgs([ + new RuleRuntimeMeta(StringRule::class, new StringArgs()), + new RuleRuntimeMeta(NullRule::class, new NullArgs()), + ]), + '(string&null)', + '(string&null)', + ]; + + yield [ + new CompoundRuleArgs([ + new RuleRuntimeMeta(StringRule::class, new StringArgs()), + new RuleRuntimeMeta(NullRule::class, new NullArgs(true)), + ]), + "(string&(null|''))", + '(string&null)', + ]; + } + } diff --git a/tests/Unit/Rules/AnyOfRuleTest.php b/tests/Unit/Rules/AnyOfRuleTest.php index 01ecabfb..a6ecb3f6 100644 --- a/tests/Unit/Rules/AnyOfRuleTest.php +++ b/tests/Unit/Rules/AnyOfRuleTest.php @@ -2,6 +2,7 @@ namespace Tests\Orisai\ObjectMapper\Unit\Rules; +use Generator; use Orisai\Exceptions\Logic\InvalidArgument; use Orisai\ObjectMapper\Args\EmptyArgs; use Orisai\ObjectMapper\Exception\ValueDoesNotMatch; @@ -12,6 +13,10 @@ use Orisai\ObjectMapper\Rules\MappedObjectArgs; use Orisai\ObjectMapper\Rules\MappedObjectRule; use Orisai\ObjectMapper\Rules\MixedRule; +use Orisai\ObjectMapper\Rules\NullArgs; +use Orisai\ObjectMapper\Rules\NullRule; +use Orisai\ObjectMapper\Rules\StringArgs; +use Orisai\ObjectMapper\Rules\StringRule; use Orisai\ObjectMapper\Types\CompoundType; use Orisai\ObjectMapper\Types\MessageType; use Orisai\ObjectMapper\Types\SimpleValueType; @@ -165,4 +170,41 @@ public function testInnerRuleResolved(): void ); } + /** + * @dataProvider providePhpNode + */ + public function testPhpNode(CompoundRuleArgs $args, string $input, string $output): void + { + self::assertSame( + $input, + (string) $this->rule->getExpectedInputType($args, $this->fieldContext()), + ); + + self::assertSame( + $output, + (string) $this->rule->getReturnType($args, $this->fieldContext()), + ); + } + + public function providePhpNode(): Generator + { + yield [ + new CompoundRuleArgs([ + new RuleRuntimeMeta(StringRule::class, new StringArgs()), + new RuleRuntimeMeta(NullRule::class, new NullArgs()), + ]), + '(string|null)', + '(string|null)', + ]; + + yield [ + new CompoundRuleArgs([ + new RuleRuntimeMeta(StringRule::class, new StringArgs()), + new RuleRuntimeMeta(NullRule::class, new NullArgs(true)), + ]), + "(string|(null|''))", + '(string|null)', + ]; + } + } diff --git a/tests/Unit/Rules/ArrayEnumRuleTest.php b/tests/Unit/Rules/ArrayEnumRuleTest.php index cc47088e..52b9ff3d 100644 --- a/tests/Unit/Rules/ArrayEnumRuleTest.php +++ b/tests/Unit/Rules/ArrayEnumRuleTest.php @@ -111,4 +111,41 @@ public function testType(): void self::assertSame(['foo', 'bar'], $type->getValues()); } + /** + * @dataProvider providePhpNode + */ + public function testPhpNode(ArrayEnumArgs $args, string $input, string $output): void + { + self::assertSame( + $input, + (string) $this->rule->getExpectedInputType($args, $this->fieldContext()), + ); + + self::assertSame( + $output, + (string) $this->rule->getReturnType($args, $this->fieldContext()), + ); + } + + public function providePhpNode(): Generator + { + yield [ + new ArrayEnumArgs(['foo', 'bar']), + "('foo'|'bar')", + "('foo'|'bar')", + ]; + + yield [ + new ArrayEnumArgs(['foo' => 123, 'bar' => 456], true), + "('foo'|'bar')", + "('foo'|'bar')", + ]; + + yield [ + new ArrayEnumArgs(['foo' => 123, 'bar' => 456]), + '(123|456)', + '(123|456)', + ]; + } + } diff --git a/tests/Unit/Rules/ArrayOfRuleTest.php b/tests/Unit/Rules/ArrayOfRuleTest.php index 7fa8c3dd..0f112337 100644 --- a/tests/Unit/Rules/ArrayOfRuleTest.php +++ b/tests/Unit/Rules/ArrayOfRuleTest.php @@ -2,6 +2,7 @@ namespace Tests\Orisai\ObjectMapper\Unit\Rules; +use Generator; use Orisai\ObjectMapper\Args\EmptyArgs; use Orisai\ObjectMapper\Exception\ValueDoesNotMatch; use Orisai\ObjectMapper\Exception\WithTypeAndValue; @@ -10,6 +11,8 @@ use Orisai\ObjectMapper\Rules\ArrayOfArgs; use Orisai\ObjectMapper\Rules\ArrayOfRule; use Orisai\ObjectMapper\Rules\MixedRule; +use Orisai\ObjectMapper\Rules\NullArgs; +use Orisai\ObjectMapper\Rules\NullRule; use Orisai\ObjectMapper\Rules\StringArgs; use Orisai\ObjectMapper\Rules\StringRule; use Orisai\ObjectMapper\Types\ArrayType; @@ -253,4 +256,69 @@ public function testTypeWithArgs(): void self::assertSame(100, $type->getParameter(ArrayOfRule::MaxItems)->getValue()); } + /** + * @dataProvider providePhpNode + */ + public function testPhpNode(ArrayOfArgs $args, string $input, string $output): void + { + self::assertSame( + $input, + (string) $this->rule->getExpectedInputType($args, $this->fieldContext()), + ); + + self::assertSame( + $output, + (string) $this->rule->getReturnType($args, $this->fieldContext()), + ); + } + + public function providePhpNode(): Generator + { + yield [ + new ArrayOfArgs( + new RuleRuntimeMeta(MixedRule::class, new EmptyArgs()), + ), + 'array', + 'array', + ]; + + yield [ + new ArrayOfArgs( + new RuleRuntimeMeta(MixedRule::class, new EmptyArgs()), + new RuleRuntimeMeta(StringRule::class, new StringArgs()), + ), + 'array', + 'array', + ]; + + yield [ + new ArrayOfArgs( + new RuleRuntimeMeta(MixedRule::class, new EmptyArgs()), + null, + 0, + ), + 'array', + 'array', + ]; + + yield [ + new ArrayOfArgs( + new RuleRuntimeMeta(MixedRule::class, new EmptyArgs()), + null, + 1, + ), + 'non-empty-array', + 'non-empty-array', + ]; + + yield [ + new ArrayOfArgs( + new RuleRuntimeMeta(NullRule::class, new NullArgs(true)), + new RuleRuntimeMeta(NullRule::class, new NullArgs(true)), + ), + "array<(null|''), (null|'')>", + 'array', + ]; + } + } diff --git a/tests/Unit/Rules/BackedEnumRuleTest.php b/tests/Unit/Rules/BackedEnumRuleTest.php index 1da24f5b..c61fccfa 100644 --- a/tests/Unit/Rules/BackedEnumRuleTest.php +++ b/tests/Unit/Rules/BackedEnumRuleTest.php @@ -131,4 +131,43 @@ public function testType(): void self::assertSame(['foo', 'bar'], $type->getValues()); } + /** + * @dataProvider providePhpNode + */ + public function testPhpNode(BackedEnumArgs $args, string $input, string $output): void + { + self::assertSame( + $input, + (string) $this->rule->getExpectedInputType($args, $this->fieldContext()), + ); + + self::assertSame( + $output, + (string) $this->rule->getReturnType($args, $this->fieldContext()), + ); + } + + public function providePhpNode(): Generator + { + yield [ + new BackedEnumArgs(ExampleStringEnum::class), + "('foo'|'bar')", + ExampleStringEnum::class, + ]; + + $class = ExampleStringEnum::class; + + yield [ + new BackedEnumArgs(ExampleStringEnum::class, true), + "('foo'|'bar')", + "($class|null)", + ]; + + yield [ + new BackedEnumArgs(ExampleIntEnum::class), + '(0|1)', + ExampleIntEnum::class, + ]; + } + } diff --git a/tests/Unit/Rules/BoolRuleTest.php b/tests/Unit/Rules/BoolRuleTest.php index 75cebf28..7db8ccbd 100644 --- a/tests/Unit/Rules/BoolRuleTest.php +++ b/tests/Unit/Rules/BoolRuleTest.php @@ -156,4 +156,35 @@ public function testTypeWithArgs(): void self::assertFalse($type->getParameter('acceptsBoolLike')->hasValue()); } + /** + * @dataProvider providePhpNode + */ + public function testPhpNode(BoolArgs $args, string $input, string $output): void + { + self::assertSame( + $input, + (string) $this->rule->getExpectedInputType($args, $this->fieldContext()), + ); + + self::assertSame( + $output, + (string) $this->rule->getReturnType($args, $this->fieldContext()), + ); + } + + public function providePhpNode(): Generator + { + yield [ + new BoolArgs(), + 'bool', + 'bool', + ]; + + yield [ + new BoolArgs(true), + "(bool|'true'|'false'|1|0)", + 'bool', + ]; + } + } diff --git a/tests/Unit/Rules/DateTimeRuleTest.php b/tests/Unit/Rules/DateTimeRuleTest.php index 779c9438..27aaf1cd 100644 --- a/tests/Unit/Rules/DateTimeRuleTest.php +++ b/tests/Unit/Rules/DateTimeRuleTest.php @@ -198,4 +198,47 @@ public function testTypeWithArgs(): void self::assertSame(DateTimeInterface::COOKIE, $type->getParameter(DateTimeRule::Format)->getValue()); } + /** + * @dataProvider providePhpNode + */ + public function testPhpNode(DateTimeArgs $args, string $input, string $output): void + { + self::assertSame( + $input, + (string) $this->rule->getExpectedInputType($args, $this->fieldContext()), + ); + + self::assertSame( + $output, + (string) $this->rule->getReturnType($args, $this->fieldContext()), + ); + } + + public function providePhpNode(): Generator + { + yield [ + new DateTimeArgs(), + 'string', + DateTimeImmutable::class, + ]; + + yield [ + new DateTimeArgs(DateTimeInterface::ATOM, DateTime::class), + 'string', + DateTime::class, + ]; + + yield [ + new DateTimeArgs(DateTimeRule::FormatTimestamp), + 'int', + DateTimeImmutable::class, + ]; + + yield [ + new DateTimeArgs(DateTimeRule::FormatAny), + '(int|string)', + DateTimeImmutable::class, + ]; + } + } diff --git a/tests/Unit/Rules/FloatRuleTest.php b/tests/Unit/Rules/FloatRuleTest.php index c2a12aa6..3bdabc8e 100644 --- a/tests/Unit/Rules/FloatRuleTest.php +++ b/tests/Unit/Rules/FloatRuleTest.php @@ -235,4 +235,71 @@ public function testTypeWithArgs(): void self::assertFalse($type->getParameter('acceptsNumericString')->hasValue()); } + /** + * @dataProvider providePhpNode + */ + public function testPhpNode(FloatArgs $args, string $input, string $output): void + { + self::assertSame( + $input, + (string) $this->rule->getExpectedInputType($args, $this->fieldContext()), + ); + + self::assertSame( + $output, + (string) $this->rule->getReturnType($args, $this->fieldContext()), + ); + } + + public function providePhpNode(): Generator + { + yield [ + new FloatArgs(), + 'float', + 'float', + ]; + + yield [ + new FloatArgs(null, 100), + 'float', + 'float', + ]; + + yield [ + new FloatArgs(100, null), + 'float<100, max>', + 'float<100, max>', + ]; + + yield [ + new FloatArgs(-200.5, -100.7), + 'float<-200.5, -100.7>', + 'float<-200.5, -100.7>', + ]; + + yield [ + new FloatArgs(100, null, true), + 'float<100, max>', + 'float<100, max>', + ]; + + yield [ + new FloatArgs(-1, null, true), + 'float<0, max>', + 'float<0, max>', + ]; + + yield [ + new FloatArgs(null, null, true), + 'float<0, max>', + 'float<0, max>', + ]; + + yield [ + new FloatArgs(null, null, false, true), + '(float|numeric-string)', + 'float', + ]; + } + } diff --git a/tests/Unit/Rules/InstanceRuleTest.php b/tests/Unit/Rules/InstanceRuleTest.php index 6383db3f..42e67f46 100644 --- a/tests/Unit/Rules/InstanceRuleTest.php +++ b/tests/Unit/Rules/InstanceRuleTest.php @@ -111,4 +111,29 @@ public function testType(): void ); } + /** + * @dataProvider providePhpNode + */ + public function testPhpNode(InstanceArgs $args, string $input, string $output): void + { + self::assertSame( + $input, + (string) $this->rule->getExpectedInputType($args, $this->fieldContext()), + ); + + self::assertSame( + $output, + (string) $this->rule->getReturnType($args, $this->fieldContext()), + ); + } + + public function providePhpNode(): Generator + { + yield [ + new InstanceArgs(stdClass::class), + stdClass::class, + stdClass::class, + ]; + } + } diff --git a/tests/Unit/Rules/IntRuleTest.php b/tests/Unit/Rules/IntRuleTest.php index 86f85dcb..8681ecf7 100644 --- a/tests/Unit/Rules/IntRuleTest.php +++ b/tests/Unit/Rules/IntRuleTest.php @@ -213,4 +213,71 @@ public function testTypeWithArgs(): void self::assertFalse($type->getParameter('acceptsNumericString')->hasValue()); } + /** + * @dataProvider providePhpNode + */ + public function testPhpNode(IntArgs $args, string $input, string $output): void + { + self::assertSame( + $input, + (string) $this->rule->getExpectedInputType($args, $this->fieldContext()), + ); + + self::assertSame( + $output, + (string) $this->rule->getReturnType($args, $this->fieldContext()), + ); + } + + public function providePhpNode(): Generator + { + yield [ + new IntArgs(), + 'int', + 'int', + ]; + + yield [ + new IntArgs(null, 100), + 'int', + 'int', + ]; + + yield [ + new IntArgs(100, null), + 'int<100, max>', + 'int<100, max>', + ]; + + yield [ + new IntArgs(-200, -100), + 'int<-200, -100>', + 'int<-200, -100>', + ]; + + yield [ + new IntArgs(100, null, true), + 'int<100, max>', + 'int<100, max>', + ]; + + yield [ + new IntArgs(-1, null, true), + 'int<0, max>', + 'int<0, max>', + ]; + + yield [ + new IntArgs(null, null, true), + 'int<0, max>', + 'int<0, max>', + ]; + + yield [ + new IntArgs(null, null, false, true), + '(int|numeric-string)', + 'int', + ]; + } + } diff --git a/tests/Unit/Rules/ListOfRuleTest.php b/tests/Unit/Rules/ListOfRuleTest.php index 3002c0e9..25f43767 100644 --- a/tests/Unit/Rules/ListOfRuleTest.php +++ b/tests/Unit/Rules/ListOfRuleTest.php @@ -2,6 +2,7 @@ namespace Tests\Orisai\ObjectMapper\Unit\Rules; +use Generator; use Orisai\ObjectMapper\Args\EmptyArgs; use Orisai\ObjectMapper\Exception\ValueDoesNotMatch; use Orisai\ObjectMapper\Meta\DefaultValueMeta; @@ -9,6 +10,8 @@ use Orisai\ObjectMapper\Rules\ListOfRule; use Orisai\ObjectMapper\Rules\MixedRule; use Orisai\ObjectMapper\Rules\MultiValueArgs; +use Orisai\ObjectMapper\Rules\NullArgs; +use Orisai\ObjectMapper\Rules\NullRule; use Orisai\ObjectMapper\Rules\StringArgs; use Orisai\ObjectMapper\Rules\StringRule; use Orisai\ObjectMapper\Types\ArrayType; @@ -228,4 +231,57 @@ public function testTypeWithArgs(): void self::assertSame(100, $type->getParameter(ListOfRule::MaxItems)->getValue()); } + /** + * @dataProvider providePhpNode + */ + public function testPhpNode(MultiValueArgs $args, string $input, string $output): void + { + self::assertSame( + $input, + (string) $this->rule->getExpectedInputType($args, $this->fieldContext()), + ); + + self::assertSame( + $output, + (string) $this->rule->getReturnType($args, $this->fieldContext()), + ); + } + + public function providePhpNode(): Generator + { + yield [ + new MultiValueArgs( + new RuleRuntimeMeta(MixedRule::class, new EmptyArgs()), + ), + 'list', + 'list', + ]; + + yield [ + new MultiValueArgs( + new RuleRuntimeMeta(MixedRule::class, new EmptyArgs()), + 0, + ), + 'list', + 'list', + ]; + + yield [ + new MultiValueArgs( + new RuleRuntimeMeta(MixedRule::class, new EmptyArgs()), + 1, + ), + 'non-empty-list', + 'non-empty-list', + ]; + + yield [ + new MultiValueArgs( + new RuleRuntimeMeta(NullRule::class, new NullArgs(true)), + ), + "list<(null|'')>", + 'list', + ]; + } + } diff --git a/tests/Unit/Rules/MappedObjectRuleTest.php b/tests/Unit/Rules/MappedObjectRuleTest.php index f475b5e2..39a175b6 100644 --- a/tests/Unit/Rules/MappedObjectRuleTest.php +++ b/tests/Unit/Rules/MappedObjectRuleTest.php @@ -2,11 +2,15 @@ namespace Tests\Orisai\ObjectMapper\Unit\Rules; +use Generator; use Orisai\ObjectMapper\Exception\InvalidData; use Orisai\ObjectMapper\Processing\Options; use Orisai\ObjectMapper\Rules\MappedObjectArgs; use Orisai\ObjectMapper\Rules\MappedObjectRule; use Tests\Orisai\ObjectMapper\Doubles\DefaultsVO; +use Tests\Orisai\ObjectMapper\Doubles\EmptyVO; +use Tests\Orisai\ObjectMapper\Doubles\FieldNamesVO; +use Tests\Orisai\ObjectMapper\Doubles\IODoesNotMatchVO; use Tests\Orisai\ObjectMapper\Toolkit\ProcessingTestCase; use function array_keys; @@ -85,4 +89,44 @@ public function testType(): void ); } + /** + * @dataProvider providePhpNode + */ + public function testPhpNode(MappedObjectArgs $args, string $input, string $inputShape, string $output): void + { + $inputNode = $this->rule->getExpectedInputType($args, $this->fieldContext()); + self::assertSame($input, (string) $inputNode); + self::assertSame( + $inputShape, + $inputNode->getArrayShape(), + ); + + $outputNode = $this->rule->getReturnType($args, $this->fieldContext()); + self::assertSame($output, (string) $outputNode); + } + + public function providePhpNode(): Generator + { + yield [ + new MappedObjectArgs(EmptyVO::class), + EmptyVO::class, + 'array{}', + EmptyVO::class, + ]; + + yield [ + new MappedObjectArgs(FieldNamesVO::class), + FieldNamesVO::class, + 'array{original: string, field: string, 123: string, swap2: string, swap1: string}', + FieldNamesVO::class, + ]; + + yield [ + new MappedObjectArgs(IODoesNotMatchVO::class), + IODoesNotMatchVO::class, + "array{bool: (bool|'true'|'false'|1|0), dateTime: string}", + IODoesNotMatchVO::class, + ]; + } + } diff --git a/tests/Unit/Rules/MixedRuleTest.php b/tests/Unit/Rules/MixedRuleTest.php index 125f1ce4..0a71fe8f 100644 --- a/tests/Unit/Rules/MixedRuleTest.php +++ b/tests/Unit/Rules/MixedRuleTest.php @@ -69,4 +69,29 @@ public function testType(): void ); } + /** + * @dataProvider providePhpNode + */ + public function testPhpNode(EmptyArgs $args, string $input, string $output): void + { + self::assertSame( + $input, + (string) $this->rule->getExpectedInputType($args, $this->fieldContext()), + ); + + self::assertSame( + $output, + (string) $this->rule->getReturnType($args, $this->fieldContext()), + ); + } + + public function providePhpNode(): Generator + { + yield [ + new EmptyArgs(), + 'mixed', + 'mixed', + ]; + } + } diff --git a/tests/Unit/Rules/NullRuleTest.php b/tests/Unit/Rules/NullRuleTest.php index 185ddb0a..094b0c6f 100644 --- a/tests/Unit/Rules/NullRuleTest.php +++ b/tests/Unit/Rules/NullRuleTest.php @@ -134,4 +134,35 @@ public function testTypeWithArgs(): void self::assertFalse($type->getParameter('acceptsEmptyString')->hasValue()); } + /** + * @dataProvider providePhpNode + */ + public function testPhpNode(NullArgs $args, string $input, string $output): void + { + self::assertSame( + $input, + (string) $this->rule->getExpectedInputType($args, $this->fieldContext()), + ); + + self::assertSame( + $output, + (string) $this->rule->getReturnType($args, $this->fieldContext()), + ); + } + + public function providePhpNode(): Generator + { + yield [ + new NullArgs(), + 'null', + 'null', + ]; + + yield [ + new NullArgs(true), + "(null|'')", + 'null', + ]; + } + } diff --git a/tests/Unit/Rules/ObjectRuleTest.php b/tests/Unit/Rules/ObjectRuleTest.php index dc134d9c..e2cdf51d 100644 --- a/tests/Unit/Rules/ObjectRuleTest.php +++ b/tests/Unit/Rules/ObjectRuleTest.php @@ -112,4 +112,29 @@ public function testType(): void ); } + /** + * @dataProvider providePhpNode + */ + public function testPhpNode(EmptyArgs $args, string $input, string $output): void + { + self::assertSame( + $input, + (string) $this->rule->getExpectedInputType($args, $this->fieldContext()), + ); + + self::assertSame( + $output, + (string) $this->rule->getReturnType($args, $this->fieldContext()), + ); + } + + public function providePhpNode(): Generator + { + yield [ + new EmptyArgs(), + 'object', + 'object', + ]; + } + } diff --git a/tests/Unit/Rules/ScalarRuleTest.php b/tests/Unit/Rules/ScalarRuleTest.php index 9e41dee0..46552621 100644 --- a/tests/Unit/Rules/ScalarRuleTest.php +++ b/tests/Unit/Rules/ScalarRuleTest.php @@ -126,4 +126,29 @@ public function testType(): void self::assertSame('bool', $subtypes[3]->getName()); } + /** + * @dataProvider providePhpNode + */ + public function testPhpNode(EmptyArgs $args, string $input, string $output): void + { + self::assertSame( + $input, + (string) $this->rule->getExpectedInputType($args, $this->fieldContext()), + ); + + self::assertSame( + $output, + (string) $this->rule->getReturnType($args, $this->fieldContext()), + ); + } + + public function providePhpNode(): Generator + { + yield [ + new EmptyArgs(), + '(int|float|string|bool)', + '(int|float|string|bool)', + ]; + } + } diff --git a/tests/Unit/Rules/StringRuleTest.php b/tests/Unit/Rules/StringRuleTest.php index d16e64ab..2fb64f16 100644 --- a/tests/Unit/Rules/StringRuleTest.php +++ b/tests/Unit/Rules/StringRuleTest.php @@ -218,4 +218,53 @@ public function testTypeWithArgs(): void self::assertSame('/[\s\S]/', $type->getParameter(StringRule::Pattern)->getValue()); } + /** + * @dataProvider providePhpNode + */ + public function testPhpNode(StringArgs $args, string $input, string $output): void + { + self::assertSame( + $input, + (string) $this->rule->getExpectedInputType($args, $this->fieldContext()), + ); + + self::assertSame( + $output, + (string) $this->rule->getReturnType($args, $this->fieldContext()), + ); + } + + public function providePhpNode(): Generator + { + yield [ + new StringArgs(), + 'string', + 'string', + ]; + + yield [ + new StringArgs(null, true, 0), + 'non-empty-string', + 'non-empty-string', + ]; + + yield [ + new StringArgs(null, true, 1), + 'non-empty-string', + 'non-empty-string', + ]; + + yield [ + new StringArgs(null, false, 1), + 'non-empty-string', + 'non-empty-string', + ]; + + yield [ + new StringArgs(null, false, 0), + 'string', + 'string', + ]; + } + } diff --git a/tests/Unit/Rules/UrlRuleTest.php b/tests/Unit/Rules/UrlRuleTest.php index 7a24cee5..ee4f4611 100644 --- a/tests/Unit/Rules/UrlRuleTest.php +++ b/tests/Unit/Rules/UrlRuleTest.php @@ -104,4 +104,29 @@ public function testType(): void ); } + /** + * @dataProvider providePhpNode + */ + public function testPhpNode(EmptyArgs $args, string $input, string $output): void + { + self::assertSame( + $input, + (string) $this->rule->getExpectedInputType($args, $this->fieldContext()), + ); + + self::assertSame( + $output, + (string) $this->rule->getReturnType($args, $this->fieldContext()), + ); + } + + public function providePhpNode(): Generator + { + yield [ + new EmptyArgs(), + 'string', + 'string', + ]; + } + }