Parse and validate AT Protocol Lexicons with DTO generation for Laravel
at main 173 lines 4.3 kB view raw
1<?php 2 3namespace SocialDept\AtpSchema\Services; 4 5use Illuminate\Support\Traits\Macroable; 6use SocialDept\AtpSchema\Contracts\LexiconRegistry; 7use SocialDept\AtpSchema\Data\LexiconDocument; 8use SocialDept\AtpSchema\Exceptions\RecordValidationException; 9 10class UnionResolver 11{ 12 use Macroable; 13 14 /** 15 * Create a new UnionResolver. 16 */ 17 public function __construct( 18 protected ?LexiconRegistry $registry = null 19 ) { 20 } 21 22 /** 23 * Resolve union type from data. 24 * 25 * Returns the NSID of the matched type for discriminated unions, 26 * or null for open unions. 27 */ 28 public function resolve(mixed $data, array $unionDef): ?string 29 { 30 // Check if this is a closed/discriminated union 31 $closed = $unionDef['closed'] ?? false; 32 33 if ($closed) { 34 return $this->resolveDiscriminated($data, $unionDef); 35 } 36 37 return null; 38 } 39 40 /** 41 * Resolve discriminated union type. 42 */ 43 protected function resolveDiscriminated(mixed $data, array $unionDef): string 44 { 45 if (! is_array($data)) { 46 throw RecordValidationException::invalidType('union', 'object', gettype($data)); 47 } 48 49 if (! isset($data['$type'])) { 50 throw RecordValidationException::invalidValue('union', 'Missing required $type field'); 51 } 52 53 $type = $data['$type']; 54 $refs = $unionDef['refs'] ?? []; 55 56 if (! in_array($type, $refs, true)) { 57 throw RecordValidationException::invalidValue( 58 'union', 59 "Type '{$type}' not in union. Allowed: ".implode(', ', $refs) 60 ); 61 } 62 63 return $type; 64 } 65 66 /** 67 * Check if data matches a specific union type. 68 */ 69 public function matches(mixed $data, string $expectedType, array $unionDef): bool 70 { 71 try { 72 $resolvedType = $this->resolve($data, $unionDef); 73 74 if ($resolvedType === null) { 75 // Open union - can't determine type 76 return false; 77 } 78 79 return $resolvedType === $expectedType; 80 } catch (RecordValidationException) { 81 return false; 82 } 83 } 84 85 /** 86 * Get the definition for the resolved type. 87 */ 88 public function getTypeDefinition(mixed $data, array $unionDef): ?LexiconDocument 89 { 90 if ($this->registry === null) { 91 return null; 92 } 93 94 $type = $this->resolve($data, $unionDef); 95 96 if ($type === null) { 97 return null; 98 } 99 100 return $this->registry->get($type); 101 } 102 103 /** 104 * Validate that data is a valid discriminated union. 105 */ 106 public function validateDiscriminated(mixed $data, array $refs): void 107 { 108 if (! is_array($data)) { 109 throw RecordValidationException::invalidType('union', 'object', gettype($data)); 110 } 111 112 if (! isset($data['$type'])) { 113 throw RecordValidationException::invalidValue('union', 'Missing required $type field'); 114 } 115 116 $type = $data['$type']; 117 118 if (! in_array($type, $refs, true)) { 119 throw RecordValidationException::invalidValue( 120 'union', 121 "Type '{$type}' not in union. Allowed: ".implode(', ', $refs) 122 ); 123 } 124 } 125 126 /** 127 * Extract type from discriminated union data. 128 */ 129 public function extractType(mixed $data): ?string 130 { 131 if (! is_array($data)) { 132 return null; 133 } 134 135 return $data['$type'] ?? null; 136 } 137 138 /** 139 * Create discriminated union data. 140 */ 141 public function createDiscriminated(string $type, array $data): array 142 { 143 return [...$data, '$type' => $type]; 144 } 145 146 /** 147 * Check if union definition is closed/discriminated. 148 */ 149 public function isClosed(array $unionDef): bool 150 { 151 return $unionDef['closed'] ?? false; 152 } 153 154 /** 155 * Get all possible types from union definition. 156 * 157 * @return array<string> 158 */ 159 public function getTypes(array $unionDef): array 160 { 161 return $unionDef['refs'] ?? []; 162 } 163 164 /** 165 * Set the lexicon registry. 166 */ 167 public function setRegistry(LexiconRegistry $registry): self 168 { 169 $this->registry = $registry; 170 171 return $this; 172 } 173}