Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

namespace Soap\WsdlReader\Metadata\Converter\Types\Configurator;

use GoetasWebservices\XML\XSDReader\Schema\Attribute\AttributeRef;
use GoetasWebservices\XML\XSDReader\Schema\Attribute\AttributeSingle;
use Soap\Engine\Metadata\Model\TypeMeta;
use Soap\Engine\Metadata\Model\XsdType as EngineType;
Expand All @@ -17,6 +18,10 @@ public function __invoke(EngineType $engineType, mixed $xsdType, TypesConverterC
return $engineType;
}

// A ref points to a global attribute declaration, which is always qualified with the target namespace
// of the schema that declares it. The form and attributeFormDefault rules only apply to local declarations.
$isQualified = $xsdType instanceof AttributeRef || $xsdType->isQualified();
Comment thread
coderabbitai[bot] marked this conversation as resolved.

$configure = pipe(
static fn (EngineType $engineType): EngineType => (new RestrictionsConfigurator())($engineType, $xsdType->getType()?->getRestriction(), $context),
);
Expand All @@ -25,7 +30,7 @@ public function __invoke(EngineType $engineType, mixed $xsdType, TypesConverterC
$engineType
->withMeta(
static fn (TypeMeta $meta): TypeMeta => $meta
->withIsQualified($xsdType->isQualified())
->withIsQualified($isQualified)
->withIsNil($xsdType->isNil())
->withIsNullable($meta->isNullable()->unwrapOr(false) || $xsdType->isNil())
->withUse($xsdType->getUse())
Expand Down
5 changes: 3 additions & 2 deletions src/Parser/Definitions/NamespacesParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use Dom\NamespaceInfo;
use Soap\WsdlReader\Model\Definitions\Namespaces;
use VeeWee\Xml\Dom\Document;
use VeeWee\Xml\Xmlns\Xmlns;
use function Psl\Dict\merge;
use function Psl\Iter\reduce;
use function VeeWee\Xml\Dom\Locator\document_element;
Expand All @@ -27,7 +28,7 @@ public static function tryParse(Document $wsdl): Namespaces
*/
static fn (array $map, NamespaceInfo $node): array
=> merge($map, [(string)$node->prefix => (string)$node->namespaceURI]),
[]
['xml' => Xmlns::xml()->value()]
),
reduce(
$allNamespaces,
Expand All @@ -37,7 +38,7 @@ public static function tryParse(Document $wsdl): Namespaces
*/
static fn (array $map, NamespaceInfo $node): array
=> merge($map, [(string)$node->namespaceURI => (string)$node->prefix]),
[]
[Xmlns::xml()->value() => 'xml']
)
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
<?php
declare(strict_types=1);

namespace Soap\WsdlReader\Test\Unit\Metadata\Coverter\Types\Configurator;

use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use Soap\Engine\Metadata\Metadata;
use Soap\Engine\Metadata\Model\Property;
use Soap\Wsdl\Loader\CallbackLoader;
use Soap\WsdlReader\Metadata\Wsdl1MetadataProvider;
use Soap\WsdlReader\Wsdl1Reader;

final class AttributeQualificationTest extends TestCase
{
private const TNS = 'https://test';
private const OTHER = 'urn:other';
private const QUALIFIED = 'urn:qualified';
private const XML = 'http://www.w3.org/XML/1998/namespace';

private static ?Metadata $metadata = null;

#[DataProvider('provideAttributes')]
public function test_it_detects_attribute_qualification(
string $typeName,
string $propertyName,
bool $expectedQualified,
string $expectedTargetNamespace,
): void {
$property = self::findProperty($typeName, $propertyName);
$type = $property->getType();

static::assertTrue($type->getMeta()->isAttribute()->unwrapOr(false));
static::assertSame($expectedQualified, $type->getMeta()->isQualified()->unwrapOr(false));
static::assertSame($expectedTargetNamespace, $type->getXmlTargetNamespace());
}

public static function provideAttributes(): iterable
{
yield 'ref to xml:lang' => ['Direct', 'lang', true, self::XML];
yield 'ref to global attribute in foreign schema' => ['Direct', 'flag', true, self::OTHER];
yield 'ref to global attribute in same schema' => ['Direct', 'glob', true, self::TNS];
yield 'local unqualified attribute' => ['Direct', 'plain', false, self::TNS];
yield 'local form=qualified attribute' => ['Direct', 'qual', true, self::TNS];

yield 'attributeFormDefault=qualified local attribute' => ['QualifiedByDefault', 'local', true, self::QUALIFIED];
yield 'attributeFormDefault=qualified local form=unqualified attribute' => ['QualifiedByDefault', 'unq', false, self::QUALIFIED];

yield 'ref inside attributeGroup' => ['Grouped', 'glob', true, self::TNS];
yield 'xml:lang inside attributeGroup' => ['Grouped', 'lang', true, self::XML];
yield 'local inside attributeGroup' => ['Grouped', 'grouped', false, self::TNS];
yield 'ref inside nested attributeGroup' => ['Grouped', 'flag', true, self::OTHER];

yield 'ref inherited from complexContent extension base' => ['Extended', 'glob', true, self::TNS];
yield 'ref declared on complexContent extension' => ['Extended', 'lang', true, self::XML];
yield 'local declared on complexContent extension' => ['Extended', 'extra', false, self::TNS];

yield 'ref declared on complexContent restriction' => ['Restricted', 'glob', true, self::TNS];

yield 'ref declared on simpleContent extension' => ['Text', 'lang', true, self::XML];
yield 'local declared on simpleContent extension' => ['Text', 'plain', false, self::TNS];
}

private static function findProperty(string $typeName, string $propertyName): Property
{
$type = self::metadata()->getTypes()->fetchFirstByName($typeName);
foreach ($type->getProperties() as $property) {
if ($property->getName() === $propertyName) {
return $property;
}
}

static::fail(sprintf('Property %s not found on type %s', $propertyName, $typeName));
}

private static function metadata(): Metadata
{
if (self::$metadata) {
return self::$metadata;
}

$tns = self::TNS;
$other = self::OTHER;
$qualified = self::QUALIFIED;
$wsdl = <<<EOXML
<definitions xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:tns="{$tns}"
xmlns:o="{$other}"
xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
xmlns="http://schemas.xmlsoap.org/wsdl/"
targetNamespace="{$tns}">
<types>
<schema xmlns="http://www.w3.org/2001/XMLSchema" targetNamespace="{$other}">
<attribute name="flag" type="xsd:string"/>
<attributeGroup name="Nested">
<attribute ref="o:flag"/>
</attributeGroup>
</schema>
<schema xmlns="http://www.w3.org/2001/XMLSchema" targetNamespace="{$qualified}" attributeFormDefault="qualified">
<complexType name="QualifiedByDefault">
<attribute name="local" type="xsd:string"/>
<attribute name="unq" type="xsd:string" form="unqualified"/>
</complexType>
</schema>
<schema xmlns="http://www.w3.org/2001/XMLSchema" targetNamespace="{$tns}">
<import namespace="http://www.w3.org/XML/1998/namespace"/>
<import namespace="{$other}"/>
<attribute name="glob" type="xsd:string"/>
<attributeGroup name="Group">
<attribute ref="tns:glob"/>
<attribute ref="xml:lang"/>
<attribute name="grouped" type="xsd:string"/>
<attributeGroup ref="o:Nested"/>
</attributeGroup>
<complexType name="Direct">
<sequence><element name="v" type="xsd:string"/></sequence>
<attribute ref="xml:lang"/>
<attribute ref="o:flag"/>
<attribute ref="tns:glob"/>
<attribute name="plain" type="xsd:string"/>
<attribute name="qual" type="xsd:string" form="qualified"/>
</complexType>
<complexType name="Grouped">
<attributeGroup ref="tns:Group"/>
</complexType>
<complexType name="Base">
<attribute ref="tns:glob"/>
</complexType>
<complexType name="Extended">
<complexContent>
<extension base="tns:Base">
<attribute ref="xml:lang"/>
<attribute name="extra" type="xsd:string"/>
</extension>
</complexContent>
</complexType>
<complexType name="Restricted">
<complexContent>
<restriction base="tns:Base">
<attribute ref="tns:glob" use="required"/>
</restriction>
</complexContent>
</complexType>
<complexType name="Text">
<simpleContent>
<extension base="xsd:string">
<attribute ref="xml:lang"/>
<attribute name="plain" type="xsd:string"/>
</extension>
</simpleContent>
</complexType>
</schema>
</types>
<message name="m"><part name="p" type="tns:Direct"/></message>
<portType name="pt"><operation name="test"><input message="tns:m"/></operation></portType>
<binding name="b" type="tns:pt">
<soap:binding style="rpc" transport="http://schemas.xmlsoap.org/soap/http"/>
<operation name="test">
<soap:operation soapAction="#test"/>
<input><soap:body use="literal" namespace="{$tns}"/></input>
</operation>
</binding>
<service name="s"><port name="p" binding="tns:b"><soap:address location="test://"/></port></service>
</definitions>
EOXML;

$info = (new Wsdl1Reader(new CallbackLoader(static fn (): string => $wsdl)))('some.wsdl');

return self::$metadata = (new Wsdl1MetadataProvider($info))->getMetadata();
}
}
34 changes: 34 additions & 0 deletions tests/Unit/Parser/Definitions/NamespacesParserTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);

namespace Soap\WsdlReader\Test\Unit\Parser\Definitions;

use PHPUnit\Framework\TestCase;
use Soap\WsdlReader\Parser\Definitions\NamespacesParser;
use VeeWee\Xml\Dom\Document;

final class NamespacesParserTest extends TestCase
{
private const XML = 'http://www.w3.org/XML/1998/namespace';

public function test_it_knows_the_implicit_xml_prefix(): void
{
$namespaces = NamespacesParser::tryParse(Document::fromXmlString(
'<definitions xmlns="http://schemas.xmlsoap.org/wsdl/" xmlns:tns="https://test" targetNamespace="https://test"/>'
));

static::assertSame(self::XML, $namespaces->lookupNamespaceFromName('xml')->unwrap());
static::assertSame('xml', $namespaces->lookupNameFromNamespace(self::XML)->unwrap());
static::assertSame('https://test', $namespaces->lookupNamespaceFromName('tns')->unwrap());
}

public function test_it_keeps_an_explicit_xml_declaration(): void
{
$namespaces = NamespacesParser::tryParse(Document::fromXmlString(
'<definitions xmlns="http://schemas.xmlsoap.org/wsdl/" xmlns:xml="' . self::XML . '"/>'
));

static::assertSame(self::XML, $namespaces->lookupNamespaceFromName('xml')->unwrap());
static::assertSame('xml', $namespaces->lookupNameFromNamespace(self::XML)->unwrap());
}
}
Loading