Skip to content

Commit

Permalink
Merge pull request #29 from veewee/any
Browse files Browse the repository at this point in the history
Add support for <any />
  • Loading branch information
veewee authored Dec 19, 2024
2 parents 6e95c7e + 9eee0e7 commit b90da84
Show file tree
Hide file tree
Showing 15 changed files with 528 additions and 26 deletions.
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

0 comments on commit b90da84

Please sign in to comment.