Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for <any /> #29

Merged
merged 1 commit into from
Dec 19, 2024
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
6 changes: 3 additions & 3 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,12 @@
"require": {
"php": "~8.2.0 || ~8.3.0 || ~8.4.0",
"azjezz/psl": "^3.0",
"veewee/reflecta": "~0.10",
"veewee/reflecta": "~0.11",
"veewee/xml": "^3.3",
"php-soap/engine": "^2.13",
"php-soap/engine": "^2.14",
"php-soap/wsdl": "^1.12",
"php-soap/xml": "^1.8",
"php-soap/wsdl-reader": "~0.18"
"php-soap/wsdl-reader": "~0.20"
},
"require-dev": {
"vimeo/psalm": "^5.26",
Expand Down
92 changes: 92 additions & 0 deletions src/Encoder/AnyElementEncoder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
<?php declare(strict_types=1);

namespace Soap\Encoding\Encoder;

use Soap\Encoding\Xml\Node\Element;
use Soap\Encoding\Xml\Node\ElementList;
use Soap\Encoding\Xml\Reader\DocumentToLookupArrayReader;
use Soap\Engine\Metadata\Model\Property;
use Soap\Engine\Metadata\Model\Type;
use VeeWee\Reflecta\Iso\Iso;
use VeeWee\Reflecta\Lens\Lens;
use function is_array;
use function is_string;
use function Psl\Dict\diff_by_key;
use function Psl\Iter\first;
use function Psl\Iter\reduce;
use function Psl\Str\join;
use function Psl\Type\string;
use function Psl\Type\vec;

/**
* @implements XmlEncoder<array|string|null, string>
*
* @psalm-import-type LookupArray from DocumentToLookupArrayReader
*
* @template-implements Feature\ProvidesObjectDecoderLens<LookupArray, ElementList>
*/
final class AnyElementEncoder implements Feature\ListAware, Feature\OptionalAware, Feature\ProvidesObjectDecoderLens, XmlEncoder
{
/**
* This lens will be used to decode XML into an 'any' property.
* It will contain all the XML tags available in the object that is surrounding the 'any' property.
* Properties that are already known by the object, will be omitted.
*
* @return Lens<LookupArray, ElementList>
*/
public static function createObjectDecoderLens(Type $parentType, Property $currentProperty): Lens
{
$omittedKeys = reduce(
$parentType->getProperties(),
static fn (array $omit, Property $property): array => [
...$omit,
...($property->getName() !== $currentProperty->getName() ? [$property->getName()] : []),
],
[]
);

/**
* @param LookupArray $data
* @return LookupArray
*/
$omit = static fn (array $data): array => diff_by_key($data, array_flip($omittedKeys));

/** @var Lens<LookupArray, ElementList> */
return Lens::readonly(
/**
* @psalm-suppress MixedArgumentTypeCoercion - Psalm gets confused about the result of omit.
* @param LookupArray $data
*/
static fn (array $data): ElementList => ElementList::fromLookupArray($omit($data))
);
}

/**
* @return Iso<array|string|null, string>
*/
public function iso(Context $context): Iso
{
$meta = $context->type->getMeta();
$isNullable = $meta->isNullable()->unwrapOr(false);
$isList = $meta->isList()->unwrapOr(false);

return new Iso(
static fn (string|array|null $raw): string => match (true) {
is_string($raw) => $raw,
is_array($raw) => join(vec(string())->assert($raw), ''),
default => '',
},
/**
* @psalm-suppress DocblockTypeContradiction - Psalm gets confused about the return type of first() in default case.
* @psalm-return null|array<array-key, string>|string
*/
static fn (ElementList|string $xml): mixed => match(true) {
is_string($xml) => $xml,
$isList && !$xml->hasElements() => [],
$isNullable && !$xml->hasElements() => null,
$isList => $xml->traverse(static fn (Element $element) => $element->value()),
default => first($xml->elements())?->value(),
}
);
}
}
12 changes: 10 additions & 2 deletions src/Encoder/ErrorHandlingEncoder.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@
* @template-covariant TXml
*
* @implements XmlEncoder<TData, TXml>
*
* @implements Feature\DecoratingEncoder<TData, TXml>
*/
final class ErrorHandlingEncoder implements XmlEncoder
final class ErrorHandlingEncoder implements Feature\DecoratingEncoder, XmlEncoder
{
/**
* @param XmlEncoder<TData, TXml> $encoder
Expand All @@ -24,6 +24,14 @@ public function __construct(
) {
}

/**
* @return XmlEncoder<TData, TXml>
*/
public function decoratedEncoder(): XmlEncoder
{
return $this->encoder;
}

/**
* @return Iso<TData, TXml>
*/
Expand Down
17 changes: 17 additions & 0 deletions src/Encoder/Feature/DecoratingEncoder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php declare(strict_types=1);

namespace Soap\Encoding\Encoder\Feature;

use Soap\Encoding\Encoder\XmlEncoder;

/**
* @template-covariant TData
* @template-covariant TXml
*/
interface DecoratingEncoder
{
/**
* @return XmlEncoder<TData, TXml>
*/
public function decoratedEncoder(): XmlEncoder;
}
21 changes: 21 additions & 0 deletions src/Encoder/Feature/ProvidesObjectDecoderLens.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php declare(strict_types=1);

namespace Soap\Encoding\Encoder\Feature;

use Soap\Engine\Metadata\Model\Property;
use Soap\Engine\Metadata\Model\Type;
use VeeWee\Reflecta\Lens\Lens;

/**
* When an encoder implements this feature interface, it knows how to create a lens that will be applied on the parent data that is being decoded.
*
* @template-covariant S
* @template-covariant A
*/
interface ProvidesObjectDecoderLens
{
/**
* @return Lens<S, A>
*/
public static function createObjectDecoderLens(Type $parentType, Property $currentProperty): Lens;
}
21 changes: 21 additions & 0 deletions src/Encoder/Feature/ProvidesObjectEncoderLens.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php declare(strict_types=1);

namespace Soap\Encoding\Encoder\Feature;

use Soap\Engine\Metadata\Model\Property;
use Soap\Engine\Metadata\Model\Type;
use VeeWee\Reflecta\Lens\Lens;

/**
* When an encoder implements this feature interface, it knows how to create a lens that will be applied on the parent data that is being encoded.
*
* @template-covariant S
* @template-covariant A
*/
interface ProvidesObjectEncoderLens
{
/**
* @return Lens<S, A>
*/
public static function createObjectEncoderLens(Type $parentType, Property $currentProperty): Lens;
}
68 changes: 51 additions & 17 deletions src/Encoder/ObjectAccess.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use Soap\Encoding\Normalizer\PhpPropertyNameNormalizer;
use Soap\Encoding\TypeInference\ComplexTypeBuilder;
use Soap\Engine\Metadata\Model\Property;
use Soap\Engine\Metadata\Model\Type;
use Soap\Engine\Metadata\Model\TypeMeta;
use VeeWee\Reflecta\Iso\Iso;
use VeeWee\Reflecta\Lens\Lens;
Expand Down Expand Up @@ -47,17 +48,21 @@ public static function forContext(Context $context): self
$isAnyPropertyQualified = false;

foreach ($sortedProperties as $property) {
$typeMeta = $property->getType()->getMeta();
$propertyType = $property->getType();
$propertyTypeMeta = $propertyType->getMeta();
$propertyContext = $context->withType($propertyType);
$name = $property->getName();
$normalizedName = PhpPropertyNameNormalizer::normalize($name);

$shouldLensBeOptional = self::shouldLensBeOptional($typeMeta);
$encoder = $context->registry->detectEncoderForContext($propertyContext);
$shouldLensBeOptional = self::shouldLensBeOptional($propertyTypeMeta);
$normalizedProperties[$normalizedName] = $property;
$encoderLenses[$normalizedName] = $shouldLensBeOptional ? optional(property($normalizedName)) : property($normalizedName);
$decoderLenses[$normalizedName] = $shouldLensBeOptional ? optional(index($name)) : index($name);
$isos[$normalizedName] = self::grabIsoForProperty($context, $property);

$isAnyPropertyQualified = $isAnyPropertyQualified || $typeMeta->isQualified()->unwrapOr(false);
$encoderLenses[$normalizedName] = self::createEncoderLensForType($shouldLensBeOptional, $normalizedName, $encoder, $type, $property);
$decoderLenses[$normalizedName] = self::createDecoderLensForType($shouldLensBeOptional, $name, $encoder, $type, $property);
$isos[$normalizedName] = $encoder->iso($propertyContext);

$isAnyPropertyQualified = $isAnyPropertyQualified || $propertyTypeMeta->isQualified()->unwrapOr(false);
}

return new self(
Expand All @@ -69,6 +74,46 @@ public static function forContext(Context $context): self
);
}

/**
* @return Lens<object, mixed>
*/
private static function createEncoderLensForType(
bool $shouldLensBeOptional,
string $normalizedName,
XmlEncoder $encoder,
Type $type,
Property $property,
): Lens {
$lens = match (true) {
$encoder instanceof Feature\DecoratingEncoder => self::createEncoderLensForType($shouldLensBeOptional, $normalizedName, $encoder->decoratedEncoder(), $type, $property),
$encoder instanceof Feature\ProvidesObjectEncoderLens => $encoder::createObjectEncoderLens($type, $property),
default => property($normalizedName)
};

/** @var Lens<object, mixed> */
return $shouldLensBeOptional ? optional($lens) : $lens;
}

/**
* @return Lens<array, mixed>
*/
private static function createDecoderLensForType(
bool $shouldLensBeOptional,
string $name,
XmlEncoder $encoder,
Type $type,
Property $property,
): Lens {
$lens = match(true) {
$encoder instanceof Feature\DecoratingEncoder => self::createDecoderLensForType($shouldLensBeOptional, $name, $encoder->decoratedEncoder(), $type, $property),
$encoder instanceof Feature\ProvidesObjectDecoderLens => $encoder::createObjectDecoderLens($type, $property),
default => index($name),
};

/** @var Lens<array, mixed> */
return $shouldLensBeOptional ? optional($lens) : $lens;
}

private static function shouldLensBeOptional(TypeMeta $meta): bool
{
if ($meta->isNullable()->unwrapOr(false)) {
Expand All @@ -84,15 +129,4 @@ private static function shouldLensBeOptional(TypeMeta $meta): bool

return false;
}

/**
* @return Iso<mixed, string>
*/
private static function grabIsoForProperty(Context $context, Property $property): Iso
{
$propertyContext = $context->withType($property->getType());

return $context->registry->detectEncoderForContext($propertyContext)
->iso($propertyContext);
}
}
5 changes: 4 additions & 1 deletion src/EncoderRegistry.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

use Psl\Collection\MutableMap;
use Soap\Encoding\ClassMap\ClassMapCollection;
use Soap\Encoding\Encoder\AnyElementEncoder;
use Soap\Encoding\Encoder\Context;
use Soap\Encoding\Encoder\ElementEncoder;
use Soap\Encoding\Encoder\EncoderDetector;
Expand Down Expand Up @@ -106,7 +107,6 @@ public static function default(): self
$qNameFormatter($xsd, 'decimal') => new SimpleType\FloatTypeEncoder(),

// Scalar:
$qNameFormatter($xsd, 'any') => new SimpleType\ScalarTypeEncoder(),
$qNameFormatter($xsd, 'anyType') => new SimpleType\ScalarTypeEncoder(),
$qNameFormatter($xsd, 'anyXML') => new SimpleType\ScalarTypeEncoder(),
$qNameFormatter($xsd, 'anySimpleType') => new SimpleType\ScalarTypeEncoder(),
Expand Down Expand Up @@ -159,6 +159,9 @@ public static function default(): self

// Apache Map
$qNameFormatter(ApacheMapDetector::NAMESPACE, 'Map') => new SoapEnc\ApacheMapEncoder(),

// Special XSD cases
$qNameFormatter($xsd, 'any') => new AnyElementEncoder(),
])
);
}
Expand Down
Loading
Loading