From 5ea8ffe06dc7ed651da3a121895f51ac2d0c3cea Mon Sep 17 00:00:00 2001
From: Toon Verwerft <toonverwerft@gmail.com>
Date: Thu, 2 Jan 2025 11:29:52 +0100
Subject: [PATCH 1/3] Prefix attribute type-names with parent type name.

---
 .../Converter/SchemaToTypesConverter.php      | 16 ++++-
 .../AttributeDeclaringParentTypeDetector.php  | 67 +++++++++++++++++++
 .../Converter/Types/ParentContext.php         | 38 +++++++++++
 .../Converter/Types/TypesConverterContext.php | 24 +++++++
 .../Visitor/AttributeContainerVisitor.php     | 25 ++++++-
 .../Visitor/InlineElementTypeVisitor.php      |  4 +-
 tests/PhpCompatibility/schema1013.phpt        | 32 +++++++++
 7 files changed, 200 insertions(+), 6 deletions(-)
 create mode 100644 src/Metadata/Converter/Types/Detector/AttributeDeclaringParentTypeDetector.php
 create mode 100644 src/Metadata/Converter/Types/ParentContext.php
 create mode 100644 tests/PhpCompatibility/schema1013.phpt

diff --git a/src/Metadata/Converter/SchemaToTypesConverter.php b/src/Metadata/Converter/SchemaToTypesConverter.php
index 48db279..a002d7c 100644
--- a/src/Metadata/Converter/SchemaToTypesConverter.php
+++ b/src/Metadata/Converter/SchemaToTypesConverter.php
@@ -7,6 +7,7 @@
 use GoetasWebservices\XML\XSDReader\Schema\Schema;
 use GoetasWebservices\XML\XSDReader\Schema\Type\Type;
 use Soap\Engine\Metadata\Collection\TypeCollection;
+use Soap\WsdlReader\Metadata\Converter\Types\ParentContext;
 use Soap\WsdlReader\Metadata\Converter\Types\TypesConverterContext;
 use Soap\WsdlReader\Metadata\Converter\Types\Visitor\ElementVisitor;
 use Soap\WsdlReader\Metadata\Converter\Types\Visitor\TypeVisitor;
@@ -22,15 +23,24 @@ public function __invoke(Schema $schema, TypesConverterContext $context): TypeCo
                 ...filter_nulls([
                     ...flat_map(
                         $schema->getTypes(),
-                        static fn (Type $type): TypeCollection => (new TypeVisitor())($type, $context)
+                        static fn (Type $type): TypeCollection => (new TypeVisitor())(
+                            $type,
+                            $context->onParent(ParentContext::create($type))
+                        )
                     ),
                     ...flat_map(
                         $schema->getElements(),
-                        static fn (ElementDef $element): TypeCollection => (new ElementVisitor())($element, $context)
+                        static fn (ElementDef $element): TypeCollection => (new ElementVisitor())(
+                            $element,
+                            $context->onParent(ParentContext::create($element))
+                        )
                     ),
                     ...flat_map(
                         $schema->getSchemas(),
-                        fn (Schema $childSchema): TypeCollection => $this->__invoke($childSchema, $context)
+                        fn (Schema $childSchema): TypeCollection => $this->__invoke(
+                            $childSchema,
+                            $context->onParent(null)
+                        )
                     )
                 ])
             );
diff --git a/src/Metadata/Converter/Types/Detector/AttributeDeclaringParentTypeDetector.php b/src/Metadata/Converter/Types/Detector/AttributeDeclaringParentTypeDetector.php
new file mode 100644
index 0000000..3aab317
--- /dev/null
+++ b/src/Metadata/Converter/Types/Detector/AttributeDeclaringParentTypeDetector.php
@@ -0,0 +1,67 @@
+<?php declare(strict_types=1);
+
+namespace Soap\WsdlReader\Metadata\Converter\Types\Detector;
+
+use GoetasWebservices\XML\XSDReader\Schema\Attribute\AttributeContainer;
+use GoetasWebservices\XML\XSDReader\Schema\Attribute\AttributeItem;
+use GoetasWebservices\XML\XSDReader\Schema\Item;
+use GoetasWebservices\XML\XSDReader\Schema\SchemaItem;
+use GoetasWebservices\XML\XSDReader\Schema\Type\Type;
+use Psl\Option\Option;
+use Soap\WsdlReader\Metadata\Converter\Types\ParentContext;
+use function Psl\Option\none;
+use function Psl\Option\some;
+
+final class AttributeDeclaringParentTypeDetector
+{
+    /**
+     * This class detects the declaring parent type of an attribute.
+     * It can be used together with the ParentContext and works as followed
+     *
+     * - If the parent is an AttributeContainer, it will check if the parent has the attribute
+     * - If the parent is not declaring the attribute, it will check if the parent is extending another type and test this extended type.
+     *
+     * @return Option<Type>
+     */
+    public function __invoke(AttributeItem $item, ?SchemaItem $parent): Option
+    {
+        $parent = match(true) {
+            $parent instanceof Item => $parent->getType(),
+            default => $parent,
+        };
+
+        if (!$parent instanceof Type) {
+            return none();
+        }
+
+        if ($parent instanceof AttributeContainer) {
+            foreach ($parent->getAttributes() as $parentAttribute) {
+                if ($parentAttribute->getName() === $item->getName()) {
+                    /** @var Option<Type> */
+                    return some($parent);
+                }
+            }
+        }
+
+        $extensionBase = $parent->getExtension()?->getBase();
+        if ($extensionBase) {
+            return $this->__invoke($item, $extensionBase);
+        }
+
+        return none();
+    }
+
+
+    /**
+     * @param Option<ParentContext> $parentContext
+     * @return Option<Type>
+     */
+    public static function detectWithParentContext(AttributeItem $item, Option $parentContext): Option
+    {
+        /** @var self $calculate */
+        static $calculate = new self();
+
+        return $parentContext
+            ->andThen(static fn (ParentContext $context) => $calculate($item, $context->currentParent()));
+    }
+}
diff --git a/src/Metadata/Converter/Types/ParentContext.php b/src/Metadata/Converter/Types/ParentContext.php
new file mode 100644
index 0000000..724be71
--- /dev/null
+++ b/src/Metadata/Converter/Types/ParentContext.php
@@ -0,0 +1,38 @@
+<?php declare(strict_types=1);
+
+namespace Soap\WsdlReader\Metadata\Converter\Types;
+
+use GoetasWebservices\XML\XSDReader\Schema\SchemaItem;
+use function Psl\Iter\first;
+use function Psl\Iter\last;
+
+final class ParentContext
+{
+    /**
+     * @param non-empty-list<SchemaItem> $items
+     */
+    private function __construct(
+        public readonly array $items,
+    ) {
+    }
+
+    public static function create(SchemaItem $item): self
+    {
+        return new self([$item]);
+    }
+
+    public function withNextParent(SchemaItem $item): self
+    {
+        return new self([...$this->items, $item]);
+    }
+
+    public function rootParent(): SchemaItem
+    {
+        return first($this->items);
+    }
+
+    public function currentParent(): SchemaItem
+    {
+        return last($this->items);
+    }
+}
diff --git a/src/Metadata/Converter/Types/TypesConverterContext.php b/src/Metadata/Converter/Types/TypesConverterContext.php
index 857e45a..56590f8 100644
--- a/src/Metadata/Converter/Types/TypesConverterContext.php
+++ b/src/Metadata/Converter/Types/TypesConverterContext.php
@@ -4,9 +4,12 @@
 namespace Soap\WsdlReader\Metadata\Converter\Types;
 
 use GoetasWebservices\XML\XSDReader\Schema\Schema;
+use Psl\Option\Option;
 use Soap\Engine\Metadata\Collection\TypeCollection;
 use Soap\WsdlReader\Model\Definitions\Namespaces;
 use Soap\WsdlReader\Parser\Definitions\SchemaParser;
+use function Psl\Option\from_nullable;
+use function Psl\Option\none;
 
 final class TypesConverterContext
 {
@@ -20,9 +23,15 @@ final class TypesConverterContext
      */
     private array $visited = [];
 
+    /**
+     * @var Option<ParentContext>
+     */
+    private Option $parentContext;
+
     private function __construct(
         public readonly Namespaces $knownNamespaces
     ) {
+        $this->parentContext = none();
     }
 
     public static function default(Namespaces $knownNamespaces): self
@@ -57,4 +66,19 @@ public function visit(Schema $schema, callable $visitor): TypeCollection
 
         return $visitor($schema);
     }
+
+    public function onParent(?ParentContext $parentContext): self
+    {
+        $this->parentContext = from_nullable($parentContext);
+
+        return $this;
+    }
+
+    /**
+     * @return Option<ParentContext>
+     */
+    public function parent(): Option
+    {
+        return $this->parentContext;
+    }
 }
diff --git a/src/Metadata/Converter/Types/Visitor/AttributeContainerVisitor.php b/src/Metadata/Converter/Types/Visitor/AttributeContainerVisitor.php
index c6a2a9d..2ef860d 100644
--- a/src/Metadata/Converter/Types/Visitor/AttributeContainerVisitor.php
+++ b/src/Metadata/Converter/Types/Visitor/AttributeContainerVisitor.php
@@ -8,13 +8,16 @@
 use GoetasWebservices\XML\XSDReader\Schema\Attribute\AttributeSingle;
 use GoetasWebservices\XML\XSDReader\Schema\Attribute\Group;
 use GoetasWebservices\XML\XSDReader\Schema\Type\Type;
+use Psl\Option\Option;
 use Soap\Engine\Metadata\Collection\PropertyCollection;
 use Soap\Engine\Metadata\Model\Property;
 use Soap\Engine\Metadata\Model\TypeMeta;
 use Soap\Engine\Metadata\Model\XsdType as EngineType;
 use Soap\WsdlReader\Metadata\Converter\Types\Configurator;
+use Soap\WsdlReader\Metadata\Converter\Types\Detector\AttributeDeclaringParentTypeDetector;
 use Soap\WsdlReader\Metadata\Converter\Types\TypesConverterContext;
 use function Psl\Fun\pipe;
+use function Psl\Option\from_nullable;
 use function Psl\Result\wrap;
 use function Psl\Type\instance_of;
 use function Psl\Vec\flat_map;
@@ -88,8 +91,26 @@ private function parseAttribute(AttributeItem $attribute, TypesConverterContext
             return $this->parseAttributes($attribute, $context);
         }
 
+        // Detecting the type-name for an attribute is complex.
+        // We first try to use the type name,
+        // Next up is the base type of the restriction if there aren't any restriction checks configured.
+        // Finally there is a fallback to the attribute name
         $attributeType = $attribute instanceof AttributeSingle ? $attribute->getType() : null;
-        $typeName = $attributeType?->getName() ?: $attribute->getName();
+        $attributeRestriction = $attributeType?->getRestriction();
+        $attributeTypeName = $attributeType?->getName();
+        $attributeRestrictionName = ($attributeRestriction && !$attributeRestriction->getChecks()) ? $attributeRestriction->getBase()?->getName() : null;
+
+        $typeName = $attributeTypeName ?: ($attributeRestrictionName ?: $attribute->getName());
+        $engineType = EngineType::guess($typeName);
+
+        // If a name cannot be determined from the type, we fallback to the attribute name:
+        // Prefix the attribute name with the parent element name resulting in a more unique type-name.
+        if (!$attributeTypeName && !$attributeRestrictionName) {
+            $engineType = AttributeDeclaringParentTypeDetector::detectWithParentContext($attribute, $context->parent())
+                ->andThen(static fn (Type $parent): Option => from_nullable($parent->getName()))
+                ->map(static fn (string $parentName): EngineType => $engineType->copy($parentName . ucfirst($typeName)))
+                ->unwrapOr($engineType);
+        }
 
         $configure = pipe(
             static fn (EngineType $engineType): EngineType => (new Configurator\AttributeConfigurator())($engineType, $attribute, $context),
@@ -98,7 +119,7 @@ private function parseAttribute(AttributeItem $attribute, TypesConverterContext
         return new PropertyCollection(
             new Property(
                 $attribute->getName(),
-                $configure(EngineType::guess($typeName))
+                $configure($engineType)
             )
         );
     }
diff --git a/src/Metadata/Converter/Types/Visitor/InlineElementTypeVisitor.php b/src/Metadata/Converter/Types/Visitor/InlineElementTypeVisitor.php
index 68278f3..05d1d8a 100644
--- a/src/Metadata/Converter/Types/Visitor/InlineElementTypeVisitor.php
+++ b/src/Metadata/Converter/Types/Visitor/InlineElementTypeVisitor.php
@@ -62,6 +62,8 @@ private function detectInlineTypes(ElementItem $element, TypesConverterContext $
             return new TypeCollection();
         }
 
-        return $elementVisitor($element, $context);
+        return $elementVisitor($element, $context->onParent(
+            $context->parent()->unwrap()->withNextParent($element)
+        ));
     }
 }
diff --git a/tests/PhpCompatibility/schema1013.phpt b/tests/PhpCompatibility/schema1013.phpt
new file mode 100644
index 0000000..cfd386b
--- /dev/null
+++ b/tests/PhpCompatibility/schema1013.phpt
@@ -0,0 +1,32 @@
+--TEST--
+SOAP XML Schema 1001: Prepend element name before attribute type names for more unique type-names.
+--FILE--
+<?php
+include __DIR__."/test_schema.inc";
+$schema = <<<EOF
+    <complexType name="VehicleCoreType">
+        <sequence>
+            <element name="VehType" minOccurs="0" type="string" />
+        </sequence>
+        <attribute name="DriveType" use="optional">
+            <simpleType>
+                <restriction base="NMTOKEN">
+                    <enumeration value="AWD" />
+                    <enumeration value="4WD" />
+                    <enumeration value="Unspecified" />
+                </restriction>
+            </simpleType>
+        </attribute>
+    </complexType>
+EOF;
+test_schema($schema,'type="tns:VehicleCoreType"');
+?>
+--EXPECT--
+Methods:
+  > test(VehicleCoreType $testParam): void
+
+Types:
+  > http://test-uri/:VehicleCoreType {
+    ?string $VehType
+    @?VehicleCoreTypeDriveType in (AWD|4WD|Unspecified) $DriveType
+  }

From 77a255088ccc1274ab6c7ca0c9ce8f65c97d37f6 Mon Sep 17 00:00:00 2001
From: Toon Verwerft <toonverwerft@gmail.com>
Date: Thu, 2 Jan 2025 15:28:07 +0100
Subject: [PATCH 2/3] Also prefix inline element simple-type names

---
 .../Detector/ElementTypeNameDetector.php      | 31 +++++++++++++++++++
 .../Types/Visitor/ElementContainerVisitor.php |  5 ++-
 tests/PhpCompatibility/schema1013.phpt        |  2 +-
 tests/PhpCompatibility/schema1014.phpt        | 31 +++++++++++++++++++
 4 files changed, 65 insertions(+), 4 deletions(-)
 create mode 100644 src/Metadata/Converter/Types/Detector/ElementTypeNameDetector.php
 create mode 100644 tests/PhpCompatibility/schema1014.phpt

diff --git a/src/Metadata/Converter/Types/Detector/ElementTypeNameDetector.php b/src/Metadata/Converter/Types/Detector/ElementTypeNameDetector.php
new file mode 100644
index 0000000..fb8db0a
--- /dev/null
+++ b/src/Metadata/Converter/Types/Detector/ElementTypeNameDetector.php
@@ -0,0 +1,31 @@
+<?php declare(strict_types=1);
+
+namespace Soap\WsdlReader\Metadata\Converter\Types\Detector;
+
+use GoetasWebservices\XML\XSDReader\Schema\Element\ElementItem;
+use GoetasWebservices\XML\XSDReader\Schema\Element\ElementSingle;
+use GoetasWebservices\XML\XSDReader\Schema\Type\SimpleType;
+use GoetasWebservices\XML\XSDReader\Schema\Type\Type;
+use Soap\WsdlReader\Metadata\Converter\Types\ParentContext;
+
+final class ElementTypeNameDetector
+{
+    public function __invoke(ElementItem $element, ParentContext $parentContext, ?string $calculatedTypeName = null): string
+    {
+        $type = $element instanceof ElementSingle ? $element->getType() : null;
+        $typeName = $calculatedTypeName ?? ($type?->getName() ?: $element->getName());
+
+        // For inline simple types, we prefix the name of the element with the name of the parent type.
+        if ($type instanceof SimpleType && !$type->getName()) {
+            $parent = $parentContext->currentParent();
+
+            if ($parent instanceof Type || $parent instanceof ElementItem) {
+                if ($parentName = $parent->getName()) {
+                    $typeName = $parentName . ucfirst($typeName);
+                }
+            }
+        }
+
+        return $typeName;
+    }
+}
diff --git a/src/Metadata/Converter/Types/Visitor/ElementContainerVisitor.php b/src/Metadata/Converter/Types/Visitor/ElementContainerVisitor.php
index 5820f4b..8b99ef4 100644
--- a/src/Metadata/Converter/Types/Visitor/ElementContainerVisitor.php
+++ b/src/Metadata/Converter/Types/Visitor/ElementContainerVisitor.php
@@ -6,13 +6,13 @@
 use GoetasWebservices\XML\XSDReader\Schema\Element\Choice;
 use GoetasWebservices\XML\XSDReader\Schema\Element\ElementContainer;
 use GoetasWebservices\XML\XSDReader\Schema\Element\ElementItem;
-use GoetasWebservices\XML\XSDReader\Schema\Element\ElementSingle;
 use GoetasWebservices\XML\XSDReader\Schema\Element\Group;
 use GoetasWebservices\XML\XSDReader\Schema\Element\Sequence;
 use Soap\Engine\Metadata\Collection\PropertyCollection;
 use Soap\Engine\Metadata\Model\Property;
 use Soap\Engine\Metadata\Model\XsdType as EngineType;
 use Soap\WsdlReader\Metadata\Converter\Types\Configurator;
+use Soap\WsdlReader\Metadata\Converter\Types\Detector\ElementTypeNameDetector;
 use Soap\WsdlReader\Metadata\Converter\Types\TypesConverterContext;
 use function Psl\Fun\pipe;
 use function Psl\Vec\flat_map;
@@ -35,8 +35,7 @@ private function parseElementItem(ElementItem $element, TypesConverterContext $c
             return $this->__invoke($element, $context);
         }
 
-        $type = $element instanceof ElementSingle ? $element->getType() : null;
-        $typeName = $type?->getName() ?: $element->getName();
+        $typeName = (new ElementTypeNameDetector())($element, $context->parent()->unwrap());
         $configure = pipe(
             static fn (EngineType $engineType): EngineType => (new Configurator\ElementConfigurator())($engineType, $element, $context),
             static fn (EngineType $engineType): EngineType => (new Configurator\AnyElementConfigurator())($engineType, $element, $context),
diff --git a/tests/PhpCompatibility/schema1013.phpt b/tests/PhpCompatibility/schema1013.phpt
index cfd386b..3f179f7 100644
--- a/tests/PhpCompatibility/schema1013.phpt
+++ b/tests/PhpCompatibility/schema1013.phpt
@@ -1,5 +1,5 @@
 --TEST--
-SOAP XML Schema 1001: Prepend element name before attribute type names for more unique type-names.
+SOAP XML Schema 1001: Prepend parent type name before attribute type names for more unique type-names.
 --FILE--
 <?php
 include __DIR__."/test_schema.inc";
diff --git a/tests/PhpCompatibility/schema1014.phpt b/tests/PhpCompatibility/schema1014.phpt
new file mode 100644
index 0000000..4d7af1a
--- /dev/null
+++ b/tests/PhpCompatibility/schema1014.phpt
@@ -0,0 +1,31 @@
+--TEST--
+SOAP XML Schema 1001: Prepend parent type name before local simple element type names for more unique type-names.
+--FILE--
+<?php
+include __DIR__."/test_schema.inc";
+$schema = <<<EOF
+    <element name="Element">
+        <complexType>
+            <sequence>
+                <element name="Enum">
+                    <simpleType>
+                        <restriction base="string">
+                            <enumeration value="foo" />
+                            <enumeration value="bar" />
+                        </restriction>
+                    </simpleType>
+                </element>
+            </sequence>
+        </complexType>
+    </element>
+EOF;
+test_schema($schema,'type="tns:Element"');
+?>
+--EXPECT--
+Methods:
+  > test(Element $testParam): void
+
+Types:
+  > http://test-uri/:Element {
+    ElementEnum in (foo|bar) $Enum
+  }

From b25d6089fd222d62398eea159911086b60fe760f Mon Sep 17 00:00:00 2001
From: Toon Verwerft <toonverwerft@gmail.com>
Date: Thu, 2 Jan 2025 15:42:08 +0100
Subject: [PATCH 3/3] Use the new naming logic for specifying the xml type name
 as well so that the encoder doesnt get confused

---
 .../Configurator/XmlTypeInfoConfigurator.php  | 13 ++++++-
 .../AttributeDeclaringParentTypeDetector.php  | 15 --------
 .../Detector/AttributeTypeNameDetector.php    | 34 +++++++++++++++++++
 .../Visitor/AttributeContainerVisitor.php     | 29 ++--------------
 4 files changed, 49 insertions(+), 42 deletions(-)
 create mode 100644 src/Metadata/Converter/Types/Detector/AttributeTypeNameDetector.php

diff --git a/src/Metadata/Converter/Types/Configurator/XmlTypeInfoConfigurator.php b/src/Metadata/Converter/Types/Configurator/XmlTypeInfoConfigurator.php
index 0ae37b2..5a2e0b5 100644
--- a/src/Metadata/Converter/Types/Configurator/XmlTypeInfoConfigurator.php
+++ b/src/Metadata/Converter/Types/Configurator/XmlTypeInfoConfigurator.php
@@ -3,10 +3,14 @@
 
 namespace Soap\WsdlReader\Metadata\Converter\Types\Configurator;
 
+use GoetasWebservices\XML\XSDReader\Schema\Attribute\AttributeItem;
+use GoetasWebservices\XML\XSDReader\Schema\Element\ElementItem;
 use GoetasWebservices\XML\XSDReader\Schema\Item;
 use GoetasWebservices\XML\XSDReader\Schema\SchemaItem;
 use GoetasWebservices\XML\XSDReader\Schema\Type\Type;
 use Soap\Engine\Metadata\Model\XsdType as EngineType;
+use Soap\WsdlReader\Metadata\Converter\Types\Detector\AttributeTypeNameDetector;
+use Soap\WsdlReader\Metadata\Converter\Types\Detector\ElementTypeNameDetector;
 use Soap\WsdlReader\Metadata\Converter\Types\TypesConverterContext;
 
 final class XmlTypeInfoConfigurator
@@ -25,9 +29,16 @@ public function __invoke(EngineType $engineType, mixed $xsdType, TypesConverterC
         $targetNamespace = $xsdType->getSchema()->getTargetNamespace() ?? '';
         $typeNamespace = $type?->getSchema()->getTargetNamespace() ?: $targetNamespace;
 
+        $parentContext = $context->parent()->unwrapOr(null);
+        $xmlTypeName = match(true) {
+            $parentContext && $item instanceof ElementItem => (new ElementTypeNameDetector())($item, $parentContext),
+            $parentContext && $item instanceof AttributeItem => (new AttributeTypeNameDetector())($item, $parentContext),
+            default => $typeName,
+        };
+
         return $engineType
             ->withXmlTargetNodeName($itemName ?: $typeName)
-            ->withXmlTypeName($typeName ?: $itemName ?: '')
+            ->withXmlTypeName($xmlTypeName)
             ->withXmlNamespace($typeNamespace)
             ->withXmlNamespaceName(
                 $context->knownNamespaces->lookupNameFromNamespace($typeNamespace)->unwrapOr(
diff --git a/src/Metadata/Converter/Types/Detector/AttributeDeclaringParentTypeDetector.php b/src/Metadata/Converter/Types/Detector/AttributeDeclaringParentTypeDetector.php
index 3aab317..c523078 100644
--- a/src/Metadata/Converter/Types/Detector/AttributeDeclaringParentTypeDetector.php
+++ b/src/Metadata/Converter/Types/Detector/AttributeDeclaringParentTypeDetector.php
@@ -8,7 +8,6 @@
 use GoetasWebservices\XML\XSDReader\Schema\SchemaItem;
 use GoetasWebservices\XML\XSDReader\Schema\Type\Type;
 use Psl\Option\Option;
-use Soap\WsdlReader\Metadata\Converter\Types\ParentContext;
 use function Psl\Option\none;
 use function Psl\Option\some;
 
@@ -50,18 +49,4 @@ public function __invoke(AttributeItem $item, ?SchemaItem $parent): Option
 
         return none();
     }
-
-
-    /**
-     * @param Option<ParentContext> $parentContext
-     * @return Option<Type>
-     */
-    public static function detectWithParentContext(AttributeItem $item, Option $parentContext): Option
-    {
-        /** @var self $calculate */
-        static $calculate = new self();
-
-        return $parentContext
-            ->andThen(static fn (ParentContext $context) => $calculate($item, $context->currentParent()));
-    }
 }
diff --git a/src/Metadata/Converter/Types/Detector/AttributeTypeNameDetector.php b/src/Metadata/Converter/Types/Detector/AttributeTypeNameDetector.php
new file mode 100644
index 0000000..a3de8cf
--- /dev/null
+++ b/src/Metadata/Converter/Types/Detector/AttributeTypeNameDetector.php
@@ -0,0 +1,34 @@
+<?php declare(strict_types=1);
+
+namespace Soap\WsdlReader\Metadata\Converter\Types\Detector;
+
+use GoetasWebservices\XML\XSDReader\Schema\Attribute\AttributeItem;
+use GoetasWebservices\XML\XSDReader\Schema\Attribute\AttributeSingle;
+use GoetasWebservices\XML\XSDReader\Schema\Type\Type;
+use Psl\Option\Option;
+use Soap\WsdlReader\Metadata\Converter\Types\ParentContext;
+use function Psl\Option\from_nullable;
+
+final class AttributeTypeNameDetector
+{
+    public function __invoke(AttributeItem $attribute, ParentContext $parentContext): string
+    {
+        $attributeType = $attribute instanceof AttributeSingle ? $attribute->getType() : null;
+        $attributeRestriction = $attributeType?->getRestriction();
+        $attributeTypeName = $attributeType?->getName();
+        $attributeRestrictionName = ($attributeRestriction && !$attributeRestriction->getChecks()) ? $attributeRestriction->getBase()?->getName() : null;
+
+        $typeName = $attributeTypeName ?: ($attributeRestrictionName ?: $attribute->getName());
+
+        // If a name cannot be determined from the type, we fallback to the attribute name:
+        // Prefix the attribute name with the parent element name resulting in a more unique type-name.
+        if (!$attributeTypeName && !$attributeRestrictionName) {
+            $typeName = (new AttributeDeclaringParentTypeDetector())($attribute, $parentContext->currentParent())
+                ->andThen(static fn (Type $parent): Option => from_nullable($parent->getName()))
+                ->map(static fn (string $parentName): string => $parentName . ucfirst($typeName))
+                ->unwrapOr($typeName);
+        }
+
+        return $typeName;
+    }
+}
diff --git a/src/Metadata/Converter/Types/Visitor/AttributeContainerVisitor.php b/src/Metadata/Converter/Types/Visitor/AttributeContainerVisitor.php
index 2ef860d..d746da2 100644
--- a/src/Metadata/Converter/Types/Visitor/AttributeContainerVisitor.php
+++ b/src/Metadata/Converter/Types/Visitor/AttributeContainerVisitor.php
@@ -5,19 +5,16 @@
 
 use GoetasWebservices\XML\XSDReader\Schema\Attribute\AttributeContainer;
 use GoetasWebservices\XML\XSDReader\Schema\Attribute\AttributeItem;
-use GoetasWebservices\XML\XSDReader\Schema\Attribute\AttributeSingle;
 use GoetasWebservices\XML\XSDReader\Schema\Attribute\Group;
 use GoetasWebservices\XML\XSDReader\Schema\Type\Type;
-use Psl\Option\Option;
 use Soap\Engine\Metadata\Collection\PropertyCollection;
 use Soap\Engine\Metadata\Model\Property;
 use Soap\Engine\Metadata\Model\TypeMeta;
 use Soap\Engine\Metadata\Model\XsdType as EngineType;
 use Soap\WsdlReader\Metadata\Converter\Types\Configurator;
-use Soap\WsdlReader\Metadata\Converter\Types\Detector\AttributeDeclaringParentTypeDetector;
+use Soap\WsdlReader\Metadata\Converter\Types\Detector\AttributeTypeNameDetector;
 use Soap\WsdlReader\Metadata\Converter\Types\TypesConverterContext;
 use function Psl\Fun\pipe;
-use function Psl\Option\from_nullable;
 use function Psl\Result\wrap;
 use function Psl\Type\instance_of;
 use function Psl\Vec\flat_map;
@@ -91,27 +88,7 @@ private function parseAttribute(AttributeItem $attribute, TypesConverterContext
             return $this->parseAttributes($attribute, $context);
         }
 
-        // Detecting the type-name for an attribute is complex.
-        // We first try to use the type name,
-        // Next up is the base type of the restriction if there aren't any restriction checks configured.
-        // Finally there is a fallback to the attribute name
-        $attributeType = $attribute instanceof AttributeSingle ? $attribute->getType() : null;
-        $attributeRestriction = $attributeType?->getRestriction();
-        $attributeTypeName = $attributeType?->getName();
-        $attributeRestrictionName = ($attributeRestriction && !$attributeRestriction->getChecks()) ? $attributeRestriction->getBase()?->getName() : null;
-
-        $typeName = $attributeTypeName ?: ($attributeRestrictionName ?: $attribute->getName());
-        $engineType = EngineType::guess($typeName);
-
-        // If a name cannot be determined from the type, we fallback to the attribute name:
-        // Prefix the attribute name with the parent element name resulting in a more unique type-name.
-        if (!$attributeTypeName && !$attributeRestrictionName) {
-            $engineType = AttributeDeclaringParentTypeDetector::detectWithParentContext($attribute, $context->parent())
-                ->andThen(static fn (Type $parent): Option => from_nullable($parent->getName()))
-                ->map(static fn (string $parentName): EngineType => $engineType->copy($parentName . ucfirst($typeName)))
-                ->unwrapOr($engineType);
-        }
-
+        $typeName = (new AttributeTypeNameDetector())($attribute, $context->parent()->unwrap());
         $configure = pipe(
             static fn (EngineType $engineType): EngineType => (new Configurator\AttributeConfigurator())($engineType, $attribute, $context),
         );
@@ -119,7 +96,7 @@ private function parseAttribute(AttributeItem $attribute, TypesConverterContext
         return new PropertyCollection(
             new Property(
                 $attribute->getName(),
-                $configure($engineType)
+                $configure(EngineType::guess($typeName))
             )
         );
     }