Parse and validate AT Protocol Lexicons with DTO generation for Laravel
at dev 5.2 kB view raw
1<?php 2 3namespace SocialDept\AtpSchema\Parser; 4 5use SocialDept\AtpSchema\Data\LexiconDocument; 6use SocialDept\AtpSchema\Data\TypeDefinition; 7use SocialDept\AtpSchema\Exceptions\TypeResolutionException; 8 9class TypeParser 10{ 11 /** 12 * Primitive type parser. 13 */ 14 protected PrimitiveParser $primitiveParser; 15 16 /** 17 * Complex type parser. 18 */ 19 protected ComplexTypeParser $complexParser; 20 21 /** 22 * Schema loader for resolving external references. 23 */ 24 protected ?SchemaLoader $schemaLoader; 25 26 /** 27 * Cache of resolved types to prevent infinite loops. 28 * 29 * @var array<string, TypeDefinition> 30 */ 31 protected array $resolvedTypes = []; 32 33 /** 34 * Current resolution chain to detect circular references. 35 * 36 * @var array<string> 37 */ 38 protected array $resolutionChain = []; 39 40 /** 41 * Create a new TypeParser. 42 */ 43 public function __construct( 44 ?PrimitiveParser $primitiveParser = null, 45 ?ComplexTypeParser $complexParser = null, 46 ?SchemaLoader $schemaLoader = null 47 ) { 48 $this->primitiveParser = $primitiveParser ?? new PrimitiveParser(); 49 $this->complexParser = $complexParser ?? new ComplexTypeParser($this->primitiveParser); 50 $this->schemaLoader = $schemaLoader; 51 } 52 53 /** 54 * Parse a type definition from array data. 55 * 56 * @throws TypeResolutionException 57 */ 58 public function parse(array $data, ?LexiconDocument $context = null): TypeDefinition 59 { 60 $type = $data['type'] ?? null; 61 62 if ($type === null) { 63 throw TypeResolutionException::unknownType('(missing type field)'); 64 } 65 66 // Handle primitive types 67 if ($this->primitiveParser->isPrimitive($type)) { 68 return $this->primitiveParser->parse($data); 69 } 70 71 // Handle complex types 72 if ($this->complexParser->isComplex($type)) { 73 return $this->complexParser->parse($data); 74 } 75 76 throw TypeResolutionException::unknownType($type); 77 } 78 79 /** 80 * Resolve a reference to its actual type definition. 81 * 82 * @throws TypeResolutionException 83 */ 84 public function resolveReference(string $ref, LexiconDocument $context): TypeDefinition 85 { 86 // Check if already resolved 87 $cacheKey = $context->getNsid().':'.$ref; 88 89 if (isset($this->resolvedTypes[$cacheKey])) { 90 return $this->resolvedTypes[$cacheKey]; 91 } 92 93 // Check for circular reference 94 if (in_array($cacheKey, $this->resolutionChain)) { 95 throw TypeResolutionException::circularReference($ref, $this->resolutionChain); 96 } 97 98 $this->resolutionChain[] = $cacheKey; 99 100 try { 101 $type = $this->resolveReferenceInternal($ref, $context); 102 $this->resolvedTypes[$cacheKey] = $type; 103 104 return $type; 105 } finally { 106 array_pop($this->resolutionChain); 107 } 108 } 109 110 /** 111 * Internal reference resolution logic. 112 * 113 * @throws TypeResolutionException 114 */ 115 protected function resolveReferenceInternal(string $ref, LexiconDocument $context): TypeDefinition 116 { 117 // Local reference (#defName) 118 if (str_starts_with($ref, '#')) { 119 $defName = substr($ref, 1); 120 121 if (! $context->hasDefinition($defName)) { 122 throw TypeResolutionException::unresolvableReference($ref, $context->getNsid()); 123 } 124 125 $defData = $context->getDefinition($defName); 126 127 return $this->parse($defData, $context); 128 } 129 130 // External reference (nsid#defName or just nsid for #main) 131 if ($this->schemaLoader === null) { 132 throw new \RuntimeException('Cannot resolve external reference without SchemaLoader'); 133 } 134 135 [$nsid, $defName] = $this->parseExternalReference($ref); 136 137 // Load external schema 138 $externalSchema = $this->schemaLoader->load($nsid); 139 $externalDoc = LexiconDocument::fromArray($externalSchema); 140 141 // Get the definition 142 if (! $externalDoc->hasDefinition($defName)) { 143 throw TypeResolutionException::unresolvableReference($ref, $context->getNsid()); 144 } 145 146 $defData = $externalDoc->getDefinition($defName); 147 148 return $this->parse($defData, $externalDoc); 149 } 150 151 /** 152 * Parse an external reference into NSID and definition name. 153 * 154 * @return array{0: string, 1: string} 155 */ 156 protected function parseExternalReference(string $ref): array 157 { 158 if (str_contains($ref, '#')) { 159 [$nsid, $defName] = explode('#', $ref, 2); 160 161 return [$nsid, $defName]; 162 } 163 164 // If no # is present, default to 'main' 165 return [$ref, 'main']; 166 } 167 168 /** 169 * Clear the resolution cache. 170 */ 171 public function clearCache(): void 172 { 173 $this->resolvedTypes = []; 174 $this->resolutionChain = []; 175 } 176 177 /** 178 * Get the resolved types cache. 179 * 180 * @return array<string, TypeDefinition> 181 */ 182 public function getResolvedTypes(): array 183 { 184 return $this->resolvedTypes; 185 } 186}