diff --git a/clover.xml b/clover.xml index 7c7a5f6..f7e6961 100644 --- a/clover.xml +++ b/clover.xml @@ -1,6 +1,6 @@ - - + + @@ -8,17 +8,17 @@ - - - - - - - - - - - + + + + + + + + + + + @@ -37,15 +37,15 @@ - - - + + + - - - - + + + + @@ -58,20 +58,20 @@ - - + + - - + + - - + + @@ -87,8 +87,8 @@ - - + + @@ -106,9 +106,9 @@ - - - + + + @@ -151,8 +151,8 @@ - - + + @@ -161,24 +161,24 @@ - - - - - - + + + + + + - - - - - - - - + + + + + + + + @@ -197,8 +197,8 @@ - - + + @@ -207,8 +207,8 @@ - - + + @@ -225,9 +225,23 @@ - + - + + + + + + + + + + + + + + + @@ -242,10 +256,10 @@ - - - - + + + + @@ -269,13 +283,13 @@ - - - + + + - - + + @@ -293,33 +307,33 @@ - - - - - - - - - - - + + + + + + + + + + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + @@ -340,14 +354,14 @@ - - - - - - - - + + + + + + + + @@ -374,9 +388,9 @@ - - - + + + @@ -389,8 +403,8 @@ - - + + @@ -404,158 +418,158 @@ - - - - - - - + + + + + + + - - - - + + + + - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - + + + + + + + + + - - - - - - - - - - - - + + + + + + + + + + + + - - - - + + + + - - - - - - - - - - - + + + + + + + + + + + - + - - - - - - - + + + + + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - - + + + + - - - - + + + + - - - - + + + + - - - - + + + + - + @@ -565,12 +579,12 @@ - - + + - - + + @@ -585,9 +599,9 @@ - - - + + + @@ -600,41 +614,41 @@ - - + + - - + + - - - - - + + + + + - - + + - - + + - - + + @@ -648,32 +662,32 @@ - - - - + + + + - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + @@ -681,283 +695,283 @@ - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + - - - + + + - - + + - - - - - - - - - + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + - - + + - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + @@ -974,348 +988,348 @@ - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - + + + + + + + + + - - - - + + + + - - - + + + - - - - + + + + - - - - + + + + - - - - - + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - + - - - - - - + + + + + + @@ -1336,35 +1350,35 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + - + @@ -1382,137 +1396,137 @@ - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - + + + - - - - + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - + + + - - + + - - + + @@ -1558,6 +1572,6 @@ - + diff --git a/src/Schema/Element/Choice.php b/src/Schema/Element/Choice.php index cb27586..398882f 100644 --- a/src/Schema/Element/Choice.php +++ b/src/Schema/Element/Choice.php @@ -10,4 +10,42 @@ class Choice extends AbstractNamedGroupItem implements ElementItem, ElementConta { use ElementContainerTrait; use MinMaxTrait; + + /** + * A choice selects exactly one of its members per occurrence. When the + * choice itself is repeatable, each member can therefore occur any number + * of times across the repetitions. Multiplying the occurrences (as is done + * for a group reference around a sequence) is unsound here, so repeatable + * members are exposed as unbounded lists (maxOccurs unbounded). + * + * For minOccurs: when the choice has more than one member, every member is + * optional (another branch may be selected instead), so minOccurs becomes + * 0. A single-member choice degenerates to that one member, so it keeps the + * choice's own minOccurs. + * + * @return ElementItem[] + */ + public function getElements(): array + { + $max = $this->getMax(); + if (-1 !== $max && $max <= 1) { + return $this->elements; + } + + $min = count($this->elements) > 1 ? 0 : $this->getMin(); + + $elements = $this->elements; + foreach ($elements as $k => $element) { + if (!$element instanceof InterfaceSetMinMax) { + continue; + } + + $clonedElement = clone $element; + $clonedElement->setMin($min); + $clonedElement->setMax(-1); + $elements[$k] = $clonedElement; + } + + return $elements; + } } diff --git a/tests/ChoiceTest.php b/tests/ChoiceTest.php index bf47901..ad23340 100644 --- a/tests/ChoiceTest.php +++ b/tests/ChoiceTest.php @@ -196,6 +196,68 @@ public function testChoiceUnbounded(): void self::assertCount(3, $choice->getElements()); } + public function testRepeatingChoiceExposesMembersAsOptionalUnboundedLists(): void + { + $schema = $this->reader->readString( + ' + + + + + + + + + + ' + ); + + $choice = $schema->getElements()['Languages']->getType()->getElements()[0]; + self::assertInstanceOf(Choice::class, $choice); + self::assertEquals(1, $choice->getMin()); + self::assertEquals(-1, $choice->getMax()); + + // More than one member: a single occurrence selects exactly one branch, + // so every member is optional (minOccurs 0) and - because the choice + // repeats - unbounded (maxOccurs -1), regardless of the choice's own min. + $members = $choice->getElements(); + self::assertCount(2, $members); + foreach ($members as $member) { + self::assertEquals(0, $member->getMin()); + self::assertEquals(-1, $member->getMax()); + } + } + + public function testRepeatingSingleMemberChoiceKeepsChoiceMinForItsMember(): void + { + $schema = $this->reader->readString( + ' + + + + + + + + + ' + ); + + $choice = $schema->getElements()['Languages']->getType()->getElements()[0]; + self::assertInstanceOf(Choice::class, $choice); + self::assertEquals(1, $choice->getMin()); + self::assertEquals(-1, $choice->getMax()); + + // A single-member choice degenerates to that one member: it is always + // the selected branch, so it keeps the choice's own minOccurs (1 here) + // instead of dropping to 0, while still being an unbounded list. + $members = $choice->getElements(); + self::assertCount(1, $members); + self::assertEquals('german', $members[0]->getName()); + self::assertEquals(1, $members[0]->getMin()); + self::assertEquals(-1, $members[0]->getMax()); + } + public function testChoiceNested(): void { $schema = $this->reader->readString( diff --git a/tests/ElementsTest.php b/tests/ElementsTest.php index 5ba6961..7ab03d7 100644 --- a/tests/ElementsTest.php +++ b/tests/ElementsTest.php @@ -4,6 +4,7 @@ namespace GoetasWebservices\XML\XSDReader\Tests; +use GoetasWebservices\XML\XSDReader\Schema\Element\Choice; use GoetasWebservices\XML\XSDReader\Schema\Element\Element; use GoetasWebservices\XML\XSDReader\Schema\Element\ElementDef; use GoetasWebservices\XML\XSDReader\Schema\Element\ElementItem; @@ -263,6 +264,50 @@ public function testGroupRefOccurrences(): void // self::assertEquals(5, $myGroupRef->getMax()); } + public function testRepeatingGroupRefAroundChoiceMakesMembersOptionalUnboundedLists(): void + { + $schema = $this->reader->readString( + ' + + + + + + + + + + + + + + '); + + $myType = $schema->findType('myType', 'http://www.example.com'); + self::assertInstanceOf(ComplexType::class, $myType); + + $myGroupRef = $myType->getElements()[0]; + self::assertInstanceOf(GroupRef::class, $myGroupRef); + + $choice = $myGroupRef->getElements()[0]; + self::assertInstanceOf(Choice::class, $choice); + self::assertEquals(-1, $choice->getMax()); + + // The group repeats unbounded, so each choice member can occur any number + // of times. Because it is a choice, every member is also optional (one + // branch is picked per occurrence) - so min must be 0, not multiplied. + $members = $choice->getElements(); + self::assertCount(2, $members); + + self::assertEquals('delete', $members[0]->getName()); + self::assertEquals(0, $members[0]->getMin()); + self::assertEquals(-1, $members[0]->getMax()); + + self::assertEquals('write', $members[1]->getName()); + self::assertEquals(0, $members[1]->getMin()); + self::assertEquals(-1, $members[1]->getMax()); + } + public function testAnonym(): void { $schema = $this->reader->readString(