diff --git a/src/File/Validation/ChoiceMaxOccursRule.php b/src/File/Validation/ChoiceMaxOccursRule.php index 0e7f6156..7b60481b 100644 --- a/src/File/Validation/ChoiceMaxOccursRule.php +++ b/src/File/Validation/ChoiceMaxOccursRule.php @@ -2,10 +2,35 @@ namespace WsdlToPhp\PackageGenerator\File\Validation; +/** + * The choice's maxOccurs attribute bounds the number of times the choice group may repeat, + * not the number of times the chosen element may occur within the group. When the element + * itself may occur more than once (its own maxOccurs is greater than 1 or unbounded), the + * element's occurrences count is constrained by its own maxOccurs rule, and applying the + * choice's bound to the element's items count would wrongly reject valid contents. + * + * @see https://github.com/WsdlToPhp/PackageGenerator/issues/340 + */ class ChoiceMaxOccursRule extends MaxOccursRule { public function name(): string { return 'choiceMaxOccurs'; } + + public function testConditions(string $parameterName, $value, bool $itemType = false): string + { + $elementMaxOccurs = $this->getAttribute()->getMetaValueFirstSet([ + 'maxOccurs', + 'maxoccurs', + 'MaxOccurs', + 'Maxoccurs', + ], 1); + + if ('unbounded' === $elementMaxOccurs || 1 < (int) $elementMaxOccurs) { + return ''; + } + + return parent::testConditions($parameterName, $value, $itemType); + } } diff --git a/src/File/Validation/MaxOccursRule.php b/src/File/Validation/MaxOccursRule.php index 9752dfd8..d5a0fb5a 100644 --- a/src/File/Validation/MaxOccursRule.php +++ b/src/File/Validation/MaxOccursRule.php @@ -45,7 +45,7 @@ public function symbol(): string * * @param mixed $value */ - final public function testConditions(string $parameterName, $value, bool $itemType = false): string + public function testConditions(string $parameterName, $value, bool $itemType = false): string { $test = ''; if ($this->getAttribute()->isArray() && ((is_scalar($value) && 'unbounded' !== $value) || (is_array($value) && !in_array('unbounded', $value)))) { diff --git a/tests/File/StructTest.php b/tests/File/StructTest.php index 5703bc10..770cb0eb 100755 --- a/tests/File/StructTest.php +++ b/tests/File/StructTest.php @@ -510,6 +510,27 @@ public function testStructValueListTypeFromUnitTests(): void } } + /** + * The choice elements have their own maxOccurs="5", the choice's maxOccurs (1) must + * not be applied to the elements' items count. + * + * @see https://github.com/WsdlToPhp/PackageGenerator/issues/340 + */ + public function testStructItemsChoiceTypeFromUnitTests(): void + { + $generator = self::unitTestsInstance(); + if (($model = $generator->getStructByName('ItemsChoiceType')) instanceof StructModel) { + $struct = new StructFile($generator, $model->getName()); + $struct + ->setModel($model) + ->write() + ; + $this->assertSameFileContent('ValidUnitTestsStructItemsChoiceType', $struct); + } else { + $this->fail('Unable to find ItemsChoiceType struct for file generation'); + } + } + public function testWriteDeliveryDetails(): void { $generator = self::deliveryServiceInstance(); diff --git a/tests/File/Validation/AbstractRule.php b/tests/File/Validation/AbstractRule.php index ffd69272..f85e58fa 100644 --- a/tests/File/Validation/AbstractRule.php +++ b/tests/File/Validation/AbstractRule.php @@ -103,6 +103,11 @@ public static function getOrderContractAddressDeliveryTypeInstance(bool $reset = return self::getClassInstance('orderContractInstance', 'AddressDelivery_Type', $reset); } + public static function getUnitTestsItemsChoiceTypeInstance(bool $reset = false) + { + return self::getClassInstance('unitTestsInstance', 'ItemsChoiceType', $reset); + } + public static function getEwsWorkingPeriodInstance(bool $reset = false) { // required for validating enumeration values diff --git a/tests/File/Validation/ChoiceMaxOccursRuleTest.php b/tests/File/Validation/ChoiceMaxOccursRuleTest.php new file mode 100644 index 00000000..b0b16b63 --- /dev/null +++ b/tests/File/Validation/ChoiceMaxOccursRuleTest.php @@ -0,0 +1,91 @@ +assertSame($instance, $instance->setItemIdentifier([1, 2, 3, 4, 5])); + } + + /** + * The ItemIdentifier + * Meta information extracted from the WSDL + * - choice: ItemIdentifier | ItemName + * - choiceMaxOccurs: 1 + * - choiceMinOccurs: 1 + * - maxOccurs: 5 + * - minOccurs: 1. + */ + public function testAddToItemIdentifierWithSeveralItemsMustPass(): void + { + $instance = self::getUnitTestsItemsChoiceTypeInstance(true); + + $this->assertSame($instance, $instance->setItemIdentifier([1])->addToItemIdentifier(2)->addToItemIdentifier(3)); + } + + /** + * The ItemIdentifier + * Meta information extracted from the WSDL + * - choice: ItemIdentifier | ItemName + * - choiceMaxOccurs: 1 + * - choiceMinOccurs: 1 + * - maxOccurs: 5 + * - minOccurs: 1. + */ + public function testSetItemIdentifierWithTooManyItemsMustThrowAnException(): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid count of 6, the number of elements contained by the property must be less than or equal to 5'); + + $instance = self::getUnitTestsItemsChoiceTypeInstance(true); + + $instance->setItemIdentifier([1, 2, 3, 4, 5, 6]); + } + + /** + * The ItemName + * Meta information extracted from the WSDL + * - choice: ItemIdentifier | ItemName + * - choiceMaxOccurs: 1 + * - choiceMinOccurs: 1 + * - maxOccurs: 5 + * - minOccurs: 1. + */ + public function testSetItemNameAfterItemIdentifierMustThrowAnException(): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The property ItemName can\'t be set as the property ItemIdentifier is already set. Only one property must be set among these properties: ItemName, ItemIdentifier.'); + + $instance = self::getUnitTestsItemsChoiceTypeInstance(true); + + $instance + ->setItemIdentifier([1, 2]) + ->setItemName(['one', 'two']) + ; + } +} diff --git a/tests/resources/generated/ValidUnitTestsStructItemsChoiceType.php b/tests/resources/generated/ValidUnitTestsStructItemsChoiceType.php new file mode 100644 index 00000000..e41ff05c --- /dev/null +++ b/tests/resources/generated/ValidUnitTestsStructItemsChoiceType.php @@ -0,0 +1,350 @@ +setItemIdentifier($itemIdentifier) + ->setItemName($itemName); + } + /** + * Get ItemIdentifier value + * @return int[]|null + */ + public function getItemIdentifier(): ?array + { + return $this->ItemIdentifier ?? null; + } + /** + * This method is responsible for validating the value(s) passed to the setItemIdentifier method + * This method is willingly generated in order to preserve the one-line inline validation within the setItemIdentifier method + * This has to validate that each item contained by the array match the itemType constraint + * @param array $values + * @return string A non-empty message if the values does not match the validation rules + */ + public static function validateItemIdentifierForArrayConstraintFromSetItemIdentifier(?array $values = []): string + { + if (!is_array($values)) { + return ''; + } + $message = ''; + $invalidValues = []; + foreach ($values as $itemsChoiceTypeItemIdentifierItem) { + // validation for constraint: itemType + if (!(is_int($itemsChoiceTypeItemIdentifierItem) || ctype_digit($itemsChoiceTypeItemIdentifierItem))) { + $invalidValues[] = is_object($itemsChoiceTypeItemIdentifierItem) ? get_class($itemsChoiceTypeItemIdentifierItem) : sprintf('%s(%s)', gettype($itemsChoiceTypeItemIdentifierItem), var_export($itemsChoiceTypeItemIdentifierItem, true)); + } + } + if (!empty($invalidValues)) { + $message = sprintf('The ItemIdentifier property can only contain items of type int, %s given', is_object($invalidValues) ? get_class($invalidValues) : (is_array($invalidValues) ? implode(', ', $invalidValues) : gettype($invalidValues))); + } + unset($invalidValues); + + return $message; + } + /** + * This method is responsible for validating the value(s) passed to the setItemIdentifier method + * This method is willingly generated in order to preserve the one-line inline validation within the setItemIdentifier method + * This has to validate that the property which is being set is the only one among the given choices + * @param mixed $value + * @return string A non-empty message if the values does not match the validation rules + */ + public function validateItemIdentifierForChoiceConstraintFromSetItemIdentifier($value): string + { + $message = ''; + if (is_null($value)) { + return $message; + } + $properties = [ + 'ItemName', + ]; + try { + foreach ($properties as $property) { + if (isset($this->{$property})) { + throw new InvalidArgumentException(sprintf('The property ItemIdentifier can\'t be set as the property %s is already set. Only one property must be set among these properties: ItemIdentifier, %s.', $property, implode(', ', $properties)), __LINE__); + } + } + } catch (InvalidArgumentException $e) { + $message = $e->getMessage(); + } + + return $message; + } + /** + * Set ItemIdentifier value + * This property belongs to a choice that allows only one property to exist. It is + * therefore removable from the request, consequently if the value assigned to this + * property is null, the property is removed from this object + * @throws InvalidArgumentException + * @throws InvalidArgumentException + * @param int[] $itemIdentifier + * @return \StructType\ApiItemsChoiceType + */ + public function setItemIdentifier(?array $itemIdentifier = null): self + { + // validation for constraint: array + if ('' !== ($itemIdentifierArrayErrorMessage = self::validateItemIdentifierForArrayConstraintFromSetItemIdentifier($itemIdentifier))) { + throw new InvalidArgumentException($itemIdentifierArrayErrorMessage, __LINE__); + } + // validation for constraint: choice(ItemIdentifier, ItemName) + if ('' !== ($itemIdentifierChoiceErrorMessage = self::validateItemIdentifierForChoiceConstraintFromSetItemIdentifier($itemIdentifier))) { + throw new InvalidArgumentException($itemIdentifierChoiceErrorMessage, __LINE__); + } + // validation for constraint: maxOccurs(5) + if (is_array($itemIdentifier) && count($itemIdentifier) > 5) { + throw new InvalidArgumentException(sprintf('Invalid count of %s, the number of elements contained by the property must be less than or equal to 5', count($itemIdentifier)), __LINE__); + } + if (is_null($itemIdentifier) || (is_array($itemIdentifier) && empty($itemIdentifier))) { + unset($this->ItemIdentifier); + } else { + $this->ItemIdentifier = $itemIdentifier; + } + + return $this; + } + /** + * This method is responsible for validating the value(s) passed to the addToItemIdentifier method + * This method is willingly generated in order to preserve the one-line inline validation within the addToItemIdentifier method + * This has to validate that the property which is being set is the only one among the given choices + * @param mixed $value + * @return string A non-empty message if the values does not match the validation rules + */ + public function validateItemForChoiceConstraintFromAddToItemIdentifier($value): string + { + $message = ''; + if (is_null($value)) { + return $message; + } + $properties = [ + 'ItemName', + ]; + try { + foreach ($properties as $property) { + if (isset($this->{$property})) { + throw new InvalidArgumentException(sprintf('The property ItemIdentifier can\'t be set as the property %s is already set. Only one property must be set among these properties: ItemIdentifier, %s.', $property, implode(', ', $properties)), __LINE__); + } + } + } catch (InvalidArgumentException $e) { + $message = $e->getMessage(); + } + + return $message; + } + /** + * Add item to ItemIdentifier value + * @throws InvalidArgumentException + * @param int $item + * @return \StructType\ApiItemsChoiceType + */ + public function addToItemIdentifier(int $item): self + { + // validation for constraint: itemType + if (!(is_int($item) || ctype_digit($item))) { + throw new InvalidArgumentException(sprintf('The ItemIdentifier property can only contain items of type int, %s given', is_object($item) ? get_class($item) : (is_array($item) ? implode(', ', $item) : gettype($item))), __LINE__); + } + // validation for constraint: choice(ItemIdentifier, ItemName) + if ('' !== ($itemChoiceErrorMessage = self::validateItemForChoiceConstraintFromAddToItemIdentifier($item))) { + throw new InvalidArgumentException($itemChoiceErrorMessage, __LINE__); + } + // validation for constraint: maxOccurs(5) + if (is_array($this->ItemIdentifier) && count($this->ItemIdentifier) >= 5) { + throw new InvalidArgumentException(sprintf('You can\'t add anymore element to this property that already contains %s elements, the number of elements contained by the property must be less than or equal to 5', count($this->ItemIdentifier)), __LINE__); + } + $this->ItemIdentifier[] = $item; + + return $this; + } + /** + * Get ItemName value + * @return string[]|null + */ + public function getItemName(): ?array + { + return $this->ItemName ?? null; + } + /** + * This method is responsible for validating the value(s) passed to the setItemName method + * This method is willingly generated in order to preserve the one-line inline validation within the setItemName method + * This has to validate that each item contained by the array match the itemType constraint + * @param array $values + * @return string A non-empty message if the values does not match the validation rules + */ + public static function validateItemNameForArrayConstraintFromSetItemName(?array $values = []): string + { + if (!is_array($values)) { + return ''; + } + $message = ''; + $invalidValues = []; + foreach ($values as $itemsChoiceTypeItemNameItem) { + // validation for constraint: itemType + if (!is_string($itemsChoiceTypeItemNameItem)) { + $invalidValues[] = is_object($itemsChoiceTypeItemNameItem) ? get_class($itemsChoiceTypeItemNameItem) : sprintf('%s(%s)', gettype($itemsChoiceTypeItemNameItem), var_export($itemsChoiceTypeItemNameItem, true)); + } + } + if (!empty($invalidValues)) { + $message = sprintf('The ItemName property can only contain items of type string, %s given', is_object($invalidValues) ? get_class($invalidValues) : (is_array($invalidValues) ? implode(', ', $invalidValues) : gettype($invalidValues))); + } + unset($invalidValues); + + return $message; + } + /** + * This method is responsible for validating the value(s) passed to the setItemName method + * This method is willingly generated in order to preserve the one-line inline validation within the setItemName method + * This has to validate that the property which is being set is the only one among the given choices + * @param mixed $value + * @return string A non-empty message if the values does not match the validation rules + */ + public function validateItemNameForChoiceConstraintFromSetItemName($value): string + { + $message = ''; + if (is_null($value)) { + return $message; + } + $properties = [ + 'ItemIdentifier', + ]; + try { + foreach ($properties as $property) { + if (isset($this->{$property})) { + throw new InvalidArgumentException(sprintf('The property ItemName can\'t be set as the property %s is already set. Only one property must be set among these properties: ItemName, %s.', $property, implode(', ', $properties)), __LINE__); + } + } + } catch (InvalidArgumentException $e) { + $message = $e->getMessage(); + } + + return $message; + } + /** + * Set ItemName value + * This property belongs to a choice that allows only one property to exist. It is + * therefore removable from the request, consequently if the value assigned to this + * property is null, the property is removed from this object + * @throws InvalidArgumentException + * @throws InvalidArgumentException + * @param string[] $itemName + * @return \StructType\ApiItemsChoiceType + */ + public function setItemName(?array $itemName = null): self + { + // validation for constraint: array + if ('' !== ($itemNameArrayErrorMessage = self::validateItemNameForArrayConstraintFromSetItemName($itemName))) { + throw new InvalidArgumentException($itemNameArrayErrorMessage, __LINE__); + } + // validation for constraint: choice(ItemIdentifier, ItemName) + if ('' !== ($itemNameChoiceErrorMessage = self::validateItemNameForChoiceConstraintFromSetItemName($itemName))) { + throw new InvalidArgumentException($itemNameChoiceErrorMessage, __LINE__); + } + // validation for constraint: maxOccurs(5) + if (is_array($itemName) && count($itemName) > 5) { + throw new InvalidArgumentException(sprintf('Invalid count of %s, the number of elements contained by the property must be less than or equal to 5', count($itemName)), __LINE__); + } + if (is_null($itemName) || (is_array($itemName) && empty($itemName))) { + unset($this->ItemName); + } else { + $this->ItemName = $itemName; + } + + return $this; + } + /** + * This method is responsible for validating the value(s) passed to the addToItemName method + * This method is willingly generated in order to preserve the one-line inline validation within the addToItemName method + * This has to validate that the property which is being set is the only one among the given choices + * @param mixed $value + * @return string A non-empty message if the values does not match the validation rules + */ + public function validateItemForChoiceConstraintFromAddToItemName($value): string + { + $message = ''; + if (is_null($value)) { + return $message; + } + $properties = [ + 'ItemIdentifier', + ]; + try { + foreach ($properties as $property) { + if (isset($this->{$property})) { + throw new InvalidArgumentException(sprintf('The property ItemName can\'t be set as the property %s is already set. Only one property must be set among these properties: ItemName, %s.', $property, implode(', ', $properties)), __LINE__); + } + } + } catch (InvalidArgumentException $e) { + $message = $e->getMessage(); + } + + return $message; + } + /** + * Add item to ItemName value + * @throws InvalidArgumentException + * @param string $item + * @return \StructType\ApiItemsChoiceType + */ + public function addToItemName(string $item): self + { + // validation for constraint: itemType + if (!is_string($item)) { + throw new InvalidArgumentException(sprintf('The ItemName property can only contain items of type string, %s given', is_object($item) ? get_class($item) : (is_array($item) ? implode(', ', $item) : gettype($item))), __LINE__); + } + // validation for constraint: choice(ItemIdentifier, ItemName) + if ('' !== ($itemChoiceErrorMessage = self::validateItemForChoiceConstraintFromAddToItemName($item))) { + throw new InvalidArgumentException($itemChoiceErrorMessage, __LINE__); + } + // validation for constraint: maxOccurs(5) + if (is_array($this->ItemName) && count($this->ItemName) >= 5) { + throw new InvalidArgumentException(sprintf('You can\'t add anymore element to this property that already contains %s elements, the number of elements contained by the property must be less than or equal to 5', count($this->ItemName)), __LINE__); + } + $this->ItemName[] = $item; + + return $this; + } +} diff --git a/tests/resources/generated/parsed_unit_tests_none.json b/tests/resources/generated/parsed_unit_tests_none.json index 358d7685..8ee236ec 100644 --- a/tests/resources/generated/parsed_unit_tests_none.json +++ b/tests/resources/generated/parsed_unit_tests_none.json @@ -283,6 +283,60 @@ }, "name": "UserType", "__CLASS__": "WsdlToPhp\\PackageGenerator\\Model\\Struct" + }, + { + "attributes": [ + { + "containsElements": true, + "removableFromRequest": false, + "type": "int", + "inheritance": "", + "abstract": false, + "meta": { + "choice": [ + "ItemIdentifier", + "ItemName" + ], + "choiceMaxOccurs": 1, + "choiceMinOccurs": 1, + "maxOccurs": "5", + "minOccurs": "1", + "ref": "tns:ItemIdentifier" + }, + "name": "ItemIdentifier", + "__CLASS__": "WsdlToPhp\\PackageGenerator\\Model\\StructAttribute" + }, + { + "containsElements": true, + "removableFromRequest": false, + "type": "string", + "inheritance": "", + "abstract": false, + "meta": { + "choice": [ + "ItemIdentifier", + "ItemName" + ], + "choiceMaxOccurs": 1, + "choiceMinOccurs": 1, + "maxOccurs": "5", + "minOccurs": "1", + "ref": "tns:ItemName" + }, + "name": "ItemName", + "__CLASS__": "WsdlToPhp\\PackageGenerator\\Model\\StructAttribute" + } + ], + "restriction": false, + "struct": true, + "types": [], + "values": [], + "list": "", + "inheritance": "", + "abstract": false, + "meta": [], + "name": "ItemsChoiceType", + "__CLASS__": "WsdlToPhp\\PackageGenerator\\Model\\Struct" } ] }, diff --git a/tests/resources/generated/parsed_unit_tests_start.json b/tests/resources/generated/parsed_unit_tests_start.json index 4a0d7663..102b1247 100644 --- a/tests/resources/generated/parsed_unit_tests_start.json +++ b/tests/resources/generated/parsed_unit_tests_start.json @@ -283,6 +283,60 @@ }, "name": "UserType", "__CLASS__": "WsdlToPhp\\PackageGenerator\\Model\\Struct" + }, + { + "attributes": [ + { + "containsElements": true, + "removableFromRequest": false, + "type": "int", + "inheritance": "", + "abstract": false, + "meta": { + "choice": [ + "ItemIdentifier", + "ItemName" + ], + "choiceMaxOccurs": 1, + "choiceMinOccurs": 1, + "maxOccurs": "5", + "minOccurs": "1", + "ref": "tns:ItemIdentifier" + }, + "name": "ItemIdentifier", + "__CLASS__": "WsdlToPhp\\PackageGenerator\\Model\\StructAttribute" + }, + { + "containsElements": true, + "removableFromRequest": false, + "type": "string", + "inheritance": "", + "abstract": false, + "meta": { + "choice": [ + "ItemIdentifier", + "ItemName" + ], + "choiceMaxOccurs": 1, + "choiceMinOccurs": 1, + "maxOccurs": "5", + "minOccurs": "1", + "ref": "tns:ItemName" + }, + "name": "ItemName", + "__CLASS__": "WsdlToPhp\\PackageGenerator\\Model\\StructAttribute" + } + ], + "restriction": false, + "struct": true, + "types": [], + "values": [], + "list": "", + "inheritance": "", + "abstract": false, + "meta": [], + "name": "ItemsChoiceType", + "__CLASS__": "WsdlToPhp\\PackageGenerator\\Model\\Struct" } ] }, diff --git a/tests/resources/unit_tests.wsdl b/tests/resources/unit_tests.wsdl index 59a74290..95f2a68c 100644 --- a/tests/resources/unit_tests.wsdl +++ b/tests/resources/unit_tests.wsdl @@ -62,6 +62,16 @@ + + + + + + + + + +