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 a0390f5
Show file tree
Hide file tree
Showing 12 changed files with 287 additions and 36 deletions.
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"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",
"psr/event-dispatcher": "^1.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
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
5 changes: 2 additions & 3 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,9 +17,7 @@
use Laminas\Code\Generator\MethodGenerator;

/**
* Class ClientBuilderGenerator
*
* @package Phpro\SoapClient\CodeGenerator
* @template-implements GeneratorInterface<ClientFactoryContext>
*/
class ClientFactoryGenerator implements GeneratorInterface
{
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;
}
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
48 changes: 30 additions & 18 deletions src/Phpro/SoapClient/Console/Command/GenerateTypesCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

namespace Phpro\SoapClient\Console\Command;

use Phpro\SoapClient\CodeGenerator\Config\Config;
use Phpro\SoapClient\CodeGenerator\EnumerationGenerator;
use Phpro\SoapClient\CodeGenerator\GeneratorInterface;
use Phpro\SoapClient\CodeGenerator\Model\Type;
use Phpro\SoapClient\CodeGenerator\Model\TypeMap;
use Phpro\SoapClient\CodeGenerator\TypeGenerator;
Expand Down Expand Up @@ -77,12 +80,11 @@ protected function execute(InputInterface $input, OutputInterface $output): int
non_empty_string()->assert($config->getTypeNamespace()),
$config->getManipulatedMetadata()->getTypes(),
);
$generator = new TypeGenerator($config->getRuleSet());

$typesDestination = non_empty_string()->assert($config->getTypeDestination());
foreach ($typeMap->getTypes() as $type) {
$fileInfo = $type->getFileInfo($typesDestination);
if ($this->handleType($generator, $type, $fileInfo)) {
if ($this->handleType($config, $type, $fileInfo)) {
$this->output->writeln(
sprintf('Generated class %s to %s', $type->getFullName(), $fileInfo->getPathname())
);
Expand All @@ -94,18 +96,29 @@ protected function execute(InputInterface $input, OutputInterface $output): int
return 0;
}

/**
* @return GeneratorInterface<Type>|null
*/
private function detectCodeGeneratorForType(Config $config, Type $type): ?GeneratorInterface
{
$isConsideredScalar = (new IsConsideredScalarType())($type->getMeta());

return match (true) {
$isConsideredScalar && $type->getMeta()->enums()->isSome() => new EnumerationGenerator(),
!$isConsideredScalar => new TypeGenerator($config->getRuleSet()),
default => null
};
}

/**
* Try to create a class for a type.
*
* @param TypeGenerator $generator
* @param Type $type
* @param SplFileInfo $fileInfo
* @return bool
*/
protected function handleType(TypeGenerator $generator, Type $type, SplFileInfo $fileInfo): bool
protected function handleType(Config $config, Type $type, SplFileInfo $fileInfo): bool
{
// Skip generation of simple types.
if ((new IsConsideredScalarType())($type->getMeta())) {
$generator = $this->detectCodeGeneratorForType($config, $type);

// Skip generation of "simple" types without generator.
if (!$generator) {
if ($this->output->isVeryVerbose()) {
$this->output->writeln('<fg=yellow>Skipped scalar type : '.$type->getFullName().'</fg=yellow>');
}
Expand All @@ -132,15 +145,14 @@ protected function handleType(TypeGenerator $generator, Type $type, SplFileInfo
}

/**
* Generates one type class
*
* @param FileGenerator $file
* @param TypeGenerator $generator
* @param Type $type
* @param SplFileInfo $fileInfo
* @param GeneratorInterface<Type> $generator
*/
protected function generateType(FileGenerator $file, TypeGenerator $generator, Type $type, SplFileInfo $fileInfo)
{
protected function generateType(
FileGenerator $file,
GeneratorInterface $generator,
Type $type,
SplFileInfo $fileInfo
): void {
$code = $generator->generate($file, $type);
$this->filesystem->putFileContents($fileInfo->getPathname(), $code);
}
Expand Down
Loading

0 comments on commit a0390f5

Please sign in to comment.