Skip to content

Commit

Permalink
Generate backed enums from XSD
Browse files Browse the repository at this point in the history
  • Loading branch information
veewee committed Dec 20, 2024
1 parent 25a2f2e commit 9b24e19
Show file tree
Hide file tree
Showing 18 changed files with 391 additions and 73 deletions.
4 changes: 2 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@
"laminas/laminas-code": "^4.14.0",
"php-soap/cached-engine": "~0.3",
"php-soap/engine": "^2.14.0",
"php-soap/encoding": "~0.14",
"php-soap/encoding": "~0.15",
"php-soap/psr18-transport": "^1.7",
"php-soap/wsdl-reader": "~0.20",
"php-soap/wsdl-reader": "~0.21",
"psr/event-dispatcher": "^1.0",
"psr/log": "^1.0 || ^2.0 || ^3.0",
"symfony/console": "~5.4 || ~6.0 || ~7.0",
Expand Down
18 changes: 18 additions & 0 deletions spec/Phpro/SoapClient/CodeGenerator/Util/NormalizerSpec.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,24 @@ function it_noramizes_properties()
$this->normalizeProperty('My-./final*prop_123')->shouldReturn('MyFinalProp_123');
}

function it_normalizes_enum_cases()
{
$this->normalizeEnumCaseName('')->shouldReturn('Empty');

$this->normalizeEnumCaseName('0')->shouldReturn('Value_0');
$this->normalizeEnumCaseName('1')->shouldReturn('Value_1');
$this->normalizeEnumCaseName('10000')->shouldReturn('Value_10000');

$this->normalizeEnumCaseName('final')->shouldReturn('final');
$this->normalizeEnumCaseName('Final')->shouldReturn('Final');
$this->normalizeEnumCaseName('UpperCased')->shouldReturn('UpperCased');
$this->normalizeEnumCaseName('my-./*prop_123')->shouldReturn('myProp_123');
$this->normalizeEnumCaseName('My-./*prop_123')->shouldReturn('MyProp_123');
$this->normalizeEnumCaseName('My-./final*prop_123')->shouldReturn('MyFinalProp_123');

$this->normalizeEnumCaseName('1 specific option')->shouldReturn('Value_1SpecificOption');
}

function it_normalizes_datatypes()
{
$this->normalizeDataType('string')->shouldReturn('string');
Expand Down
70 changes: 53 additions & 17 deletions src/Phpro/SoapClient/CodeGenerator/Assembler/ClassMapAssembler.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Phpro\SoapClient\CodeGenerator\Context\ClassMapContext;
use Phpro\SoapClient\CodeGenerator\Context\ContextInterface;
use Phpro\SoapClient\CodeGenerator\Model\Type;
use Phpro\SoapClient\CodeGenerator\Model\TypeMap;
use Phpro\SoapClient\Exception\AssemblerException;
use Laminas\Code\Generator\ClassGenerator;
Expand Down Expand Up @@ -48,31 +49,66 @@ public function assemble(ContextInterface $context)
$file->setUse(ClassMapCollection::class);
$file->setUse(ClassMap::class);
$linefeed = $file::LINE_FEED;
$classMap = $this->assembleClassMap($typeMap, $linefeed, $file->getIndentation());
$code = $this->assembleClassMapCollection($classMap, $linefeed).$linefeed;
$class->addMethodFromGenerator(
(new MethodGenerator('getCollection'))
->setStatic(true)
->setBody('return '.$code)
->setReturnType(ClassMapCollection::class)
);
$indentation = $file->getIndentation();

$class->addMethodFromGenerator($this->generateTypes($typeMap, $linefeed, $indentation));
$class->addMethodFromGenerator($this->generateEnums($typeMap, $linefeed, $indentation));
} catch (\Exception $e) {
throw AssemblerException::fromException($e);
}
}

/***
* @param TypeMap $typeMap
* @param string $linefeed
* @param string $indentation
*
* @return string
private function generateTypes(
TypeMap $typeMap,
string $linefeed,
string $indentation,
): MethodGenerator {
$classMap = $this->assembleClassMap(
$typeMap,
$linefeed,
$indentation,
static fn (Type $type) => !(new IsConsideredScalarType())($type->getMeta())
);
$code = $this->assembleClassMapCollection($classMap, $linefeed).$linefeed;

return (new MethodGenerator('types'))
->setStatic(true)
->setBody('return '.$code)
->setReturnType(ClassMapCollection::class);
}

private function generateEnums(
TypeMap $typeMap,
string $linefeed,
string $indentation,
): MethodGenerator {
$classMap = $this->assembleClassMap(
$typeMap,
$linefeed,
$indentation,
static fn (Type $type) => (new IsConsideredScalarType())($type->getMeta())
&& $type->getMeta()->enums()->isSome()
);
$code = $this->assembleClassMapCollection($classMap, $linefeed).$linefeed;

return (new MethodGenerator('enums'))
->setStatic(true)
->setBody('return '.$code)
->setReturnType(ClassMapCollection::class);
}

/**
* @param \Closure(Type): bool $predicate
*/
private function assembleClassMap(TypeMap $typeMap, string $linefeed, string $indentation): string
{
private function assembleClassMap(
TypeMap $typeMap,
string $linefeed,
string $indentation,
\Closure $predicate
): string {
$classMap = [];
foreach ($typeMap->getTypes() as $type) {
if ((new IsConsideredScalarType())($type->getMeta())) {
if (!$predicate($type)) {
continue;
}

Expand Down
5 changes: 2 additions & 3 deletions src/Phpro/SoapClient/CodeGenerator/ClassMapGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,13 @@

use Phpro\SoapClient\CodeGenerator\Context\ClassMapContext;
use Phpro\SoapClient\CodeGenerator\Context\FileContext;
use Phpro\SoapClient\CodeGenerator\Model\Type;
use Phpro\SoapClient\CodeGenerator\Model\TypeMap;
use Phpro\SoapClient\CodeGenerator\Rules\RuleSetInterface;
use Laminas\Code\Generator\FileGenerator;

/**
* Class ClassMapGenerator
*
* @package Phpro\SoapClient\CodeGenerator
* @template-implements GeneratorInterface<TypeMap>
*/
class ClassMapGenerator implements GeneratorInterface
{
Expand Down
11 changes: 5 additions & 6 deletions src/Phpro/SoapClient/CodeGenerator/ClientFactoryGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Phpro\SoapClient\Caller\EngineCaller;
use Phpro\SoapClient\Caller\EventDispatchingCaller;
use Phpro\SoapClient\CodeGenerator\Context\ClientFactoryContext;
use Phpro\SoapClient\CodeGenerator\Model\Type;
use Phpro\SoapClient\Soap\DefaultEngineFactory;
use Phpro\SoapClient\Soap\EngineOptions;
use Soap\Encoding\EncoderRegistry;
Expand All @@ -16,19 +17,17 @@
use Laminas\Code\Generator\MethodGenerator;

/**
* Class ClientBuilderGenerator
*
* @package Phpro\SoapClient\CodeGenerator
* @template-implements GeneratorInterface<ClientFactoryContext>
*/
class ClientFactoryGenerator implements GeneratorInterface
{
const BODY = <<<BODY
\$engine = DefaultEngineFactory::create(
EngineOptions::defaults(\$wsdl)
->withEncoderRegistry(
EncoderRegistry::default()->addClassMapCollection(
%2\$s::getCollection()
)
EncoderRegistry::default()
->addClassMapCollection(%2\$s::types())
->addBackedEnumClassMapCollection(%2\$s::enums())
)
// If you want to enable WSDL caching:
// ->withCache()
Expand Down
4 changes: 1 addition & 3 deletions src/Phpro/SoapClient/CodeGenerator/ClientGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,7 @@
use Laminas\Code\Generator\FileGenerator;

/**
* Class ClientGenerator
*
* @package Phpro\SoapClient\CodeGenerator
* @template-implements GeneratorInterface<Client>
*/
class ClientGenerator implements GeneratorInterface
{
Expand Down
5 changes: 2 additions & 3 deletions src/Phpro/SoapClient/CodeGenerator/ConfigGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,12 @@
use Phpro\SoapClient\CodeGenerator\Config\Config;
use Phpro\SoapClient\CodeGenerator\Context\ConfigContext;
use Laminas\Code\Generator\FileGenerator;
use Phpro\SoapClient\CodeGenerator\Model\Type;
use Phpro\SoapClient\Soap\DefaultEngineFactory;
use Phpro\SoapClient\Soap\EngineOptions;

/**
* Class ConfigGenerator
*
* @package Phpro\SoapClient\CodeGenerator
* @template-implements GeneratorInterface<ConfigContext>
*/
class ConfigGenerator implements GeneratorInterface
{
Expand Down
77 changes: 77 additions & 0 deletions src/Phpro/SoapClient/CodeGenerator/EnumerationGenerator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<?php

namespace Phpro\SoapClient\CodeGenerator;

use Laminas\Code\Generator\DocBlockGenerator;
use Phpro\SoapClient\CodeGenerator\Model\Type;
use Phpro\SoapClient\CodeGenerator\Util\Normalizer;
use Laminas\Code\Generator\EnumGenerator\EnumGenerator;
use Laminas\Code\Generator\FileGenerator;
use Soap\Engine\Metadata\Model\XsdType;
use function Psl\Dict\pull;
use function Psl\Type\int;

/**
* @template-implements GeneratorInterface<Type>
*/
class EnumerationGenerator implements GeneratorInterface
{
/**
* @param FileGenerator $file
* @param Type $context
* @return string
*/
public function generate(FileGenerator $file, $context): string
{
$file->setNamespace($context->getNamespace());
$file->setBody($this->generateBody($context));

return $file->generate();
}

private function generateBody(Type $type): string
{
$xsdType = $type->getXsdType();
$xsdMeta = $xsdType->getMeta();
$enumType = match ($xsdType->getBaseType()) {
'int', 'integer' => 'int',
default => 'string',
};

$body = EnumGenerator::withConfig([
'name' => Normalizer::normalizeClassname($type->getName()),
'backedCases' => [
'type' => $enumType,
'cases' => $this->buildCases($xsdType, $enumType),
]
])->generate();

if ($docs = $xsdMeta->docs()->unwrapOr('')) {
$docblock = (new DocBlockGenerator())
->setWordWrap(false)
->setLongDescription($docs)
->generate();
$body = $docblock . $body;
}

return $body;
}

/**
* @param 'string'|'int' $enumType
* @return array<string, int|string>
*/
private function buildCases(XsdType $xsdType, string $enumType): array
{
$enums = $xsdType->getMeta()->enums()->unwrapOr([]);

return pull(
$enums,
static fn(string $value): int|string => match ($enumType) {
'int' => int()->coerce($value),
'string' => $value,
},
static fn(string $value): string => Normalizer::normalizeEnumCaseName($value)
);
}
}
6 changes: 4 additions & 2 deletions src/Phpro/SoapClient/CodeGenerator/GeneratorInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
* Interface GeneratorInterface
*
* @package Phpro\SoapClient\CodeGenerator
*
* @template Context
*/
interface GeneratorInterface
{
Expand All @@ -16,9 +18,9 @@ interface GeneratorInterface

/**
* @param FileGenerator $file
* @param mixed $model
* @param Context $context
*
* @return string
*/
public function generate(FileGenerator $file, $model): string;
public function generate(FileGenerator $file, $context): string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@ public function __invoke(XsdType $type): string
{
$meta = $type->getMeta();
$isSimpleType = $meta->isSimple()->unwrapOr(false);
$isGlobalEnum = $isSimpleType && $meta->enums()->isSome() && !$meta->isLocal()->unwrapOr(false);

// For non-simple types, we always want to use the name of the type.
if (!$isSimpleType) {
// For non-simple types or backed enums, we always want to use the name of the type.
if (!$isSimpleType || $isGlobalEnum) {
return $type->getName();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,13 @@ public function __construct(
*/
public function asDocBlockType(string $type): string
{
$isLocal = $this->meta->isLocal()->unwrapOr(false);
$isEnum = (bool) $this->meta->enums()->unwrapOr([]);
$isUnion = (bool) $this->meta->unions()->unwrapOr([]);

$type = match (true) {
(bool) $this->meta->enums()->unwrapOr([]) => (new EnumValuesCalculator())($this->meta),
(bool) $this->meta->unions()->unwrapOr([]) => (new UnionTypesCalculator())($this->meta),
$isLocal && $isEnum => (new EnumValuesCalculator())($this->meta),
$isUnion => (new UnionTypesCalculator())($this->meta),
default => $type
};

Expand Down
4 changes: 1 addition & 3 deletions src/Phpro/SoapClient/CodeGenerator/TypeGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,7 @@
use Laminas\Code\Generator\FileGenerator;

/**
* Class TypeGenerator
*
* @package Phpro\SoapClient\CodeGenerator
* @template-implements GeneratorInterface<Type>
*/
class TypeGenerator implements GeneratorInterface
{
Expand Down
26 changes: 26 additions & 0 deletions src/Phpro/SoapClient/CodeGenerator/Util/Normalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,32 @@ public static function normalizeProperty(string $property): string
return self::camelCase($property, '{[^a-z0-9_]+}i');
}

/**
* @param non-empty-string $emptyCase
* @param non-empty-string $conflictPrefix
* @return non-empty-string
*/
public static function normalizeEnumCaseName(
string $value,
string $emptyCase = 'Empty',
string $conflictPrefix = 'Value_'
): string {
if ($value === '') {
return $emptyCase;
}

if (is_numeric($value)) {
return $conflictPrefix.$value;
}

$normalized = self::normalizeProperty($value);
if (preg_match('/^[0-9]/', $normalized)) {
$normalized = $conflictPrefix.$normalized;
}

return $normalized;
}

/**
* @param non-empty-string $type
*
Expand Down
Loading

0 comments on commit 9b24e19

Please sign in to comment.