Parse and validate AT Protocol Lexicons with DTO generation for Laravel
at dev 6.8 kB view raw
1<?php 2 3namespace SocialDept\AtpSchema\Validation; 4 5use SocialDept\AtpSchema\Contracts\LexiconValidator as LexiconValidatorContract; 6use SocialDept\AtpSchema\Data\LexiconDocument; 7use SocialDept\AtpSchema\Exceptions\RecordValidationException; 8use SocialDept\AtpSchema\Exceptions\SchemaValidationException; 9use SocialDept\AtpSchema\Parser\SchemaLoader; 10use SocialDept\AtpSchema\Parser\TypeParser; 11 12class LexiconValidator implements LexiconValidatorContract 13{ 14 /** 15 * Schema loader for loading lexicon documents. 16 */ 17 protected SchemaLoader $schemaLoader; 18 19 /** 20 * Type parser for parsing and resolving types. 21 */ 22 protected TypeParser $typeParser; 23 24 /** 25 * Validation mode. 26 */ 27 protected string $mode = 'strict'; 28 29 /** 30 * Create a new LexiconValidator. 31 */ 32 public function __construct( 33 SchemaLoader $schemaLoader, 34 ?TypeParser $typeParser = null 35 ) { 36 $this->schemaLoader = $schemaLoader; 37 $this->typeParser = $typeParser ?? new TypeParser(schemaLoader: $schemaLoader); 38 } 39 40 /** 41 * Validate data against Lexicon schema. 42 */ 43 public function validate(array $data, LexiconDocument $schema): bool 44 { 45 try { 46 $this->validateRecord($schema, $data); 47 48 return true; 49 } catch (RecordValidationException|SchemaValidationException) { 50 return false; 51 } 52 } 53 54 /** 55 * Validate and return errors. 56 * 57 * @return array<string, array<string>> 58 */ 59 public function validateWithErrors(array $data, LexiconDocument $schema): array 60 { 61 try { 62 $this->validateRecord($schema, $data); 63 64 return []; 65 } catch (RecordValidationException $e) { 66 return ['record' => [$e->getMessage()]]; 67 } catch (SchemaValidationException $e) { 68 return ['schema' => [$e->getMessage()]]; 69 } 70 } 71 72 /** 73 * Validate a specific field. 74 */ 75 public function validateField(mixed $value, string $field, LexiconDocument $schema): bool 76 { 77 try { 78 $mainDef = $schema->getMainDefinition(); 79 80 if ($mainDef === null) { 81 return false; 82 } 83 84 $recordSchema = $mainDef['record'] ?? null; 85 86 if ($recordSchema === null || ! is_array($recordSchema)) { 87 return false; 88 } 89 90 $properties = $recordSchema['properties'] ?? []; 91 92 if (! isset($properties[$field])) { 93 return false; 94 } 95 96 $fieldType = $this->typeParser->parse($properties[$field], $schema); 97 $fieldType->validate($value, $field); 98 99 return true; 100 } catch (RecordValidationException) { 101 return false; 102 } 103 } 104 105 /** 106 * Set validation mode (strict, optimistic, lenient). 107 */ 108 public function setMode(string $mode): void 109 { 110 $this->mode = $mode; 111 } 112 113 /** 114 * Validate a record by NSID string. 115 */ 116 public function validateByNsid(string $nsid, array $record): void 117 { 118 $document = $this->schemaLoader->load($nsid); 119 120 $this->validateRecord($document, $record); 121 } 122 123 /** 124 * Validate a record against a lexicon document. 125 */ 126 public function validateRecord(LexiconDocument $document, array $record): void 127 { 128 if (! $document->isRecord()) { 129 throw SchemaValidationException::invalidStructure( 130 $document->getNsid(), 131 ['Schema is not a record type'] 132 ); 133 } 134 135 $mainDef = $document->getMainDefinition(); 136 137 if ($mainDef === null) { 138 throw SchemaValidationException::invalidStructure( 139 $document->getNsid(), 140 ['Missing main definition'] 141 ); 142 } 143 144 // Get the record schema 145 $recordSchema = $mainDef['record'] ?? null; 146 147 if ($recordSchema === null || ! is_array($recordSchema)) { 148 throw SchemaValidationException::invalidStructure( 149 $document->getNsid(), 150 ['Invalid record schema'] 151 ); 152 } 153 154 // Parse and validate the record type 155 $recordType = $this->typeParser->parse($recordSchema, $document); 156 $recordType->validate($record, '$'); 157 } 158 159 /** 160 * Validate a query against its lexicon schema. 161 */ 162 public function validateQuery(LexiconDocument $document, array $params): void 163 { 164 if (! $document->isQuery()) { 165 throw SchemaValidationException::invalidStructure( 166 $document->getNsid(), 167 ['Schema is not a query type'] 168 ); 169 } 170 171 $mainDef = $document->getMainDefinition(); 172 173 if ($mainDef === null) { 174 throw SchemaValidationException::invalidStructure( 175 $document->getNsid(), 176 ['Missing main definition'] 177 ); 178 } 179 180 // Get the parameters schema 181 $paramsSchema = $mainDef['parameters'] ?? null; 182 183 if ($paramsSchema !== null && is_array($paramsSchema)) { 184 $paramsType = $this->typeParser->parse($paramsSchema, $document); 185 $paramsType->validate($params, '$'); 186 } 187 } 188 189 /** 190 * Validate a procedure against its lexicon schema. 191 */ 192 public function validateProcedure(LexiconDocument $document, array $input): void 193 { 194 if (! $document->isProcedure()) { 195 throw SchemaValidationException::invalidStructure( 196 $document->getNsid(), 197 ['Schema is not a procedure type'] 198 ); 199 } 200 201 $mainDef = $document->getMainDefinition(); 202 203 if ($mainDef === null) { 204 throw SchemaValidationException::invalidStructure( 205 $document->getNsid(), 206 ['Missing main definition'] 207 ); 208 } 209 210 // Get the input schema 211 $inputSchema = $mainDef['input'] ?? null; 212 213 if ($inputSchema !== null && is_array($inputSchema)) { 214 $inputType = $this->typeParser->parse($inputSchema, $document); 215 $inputType->validate($input, '$'); 216 } 217 } 218 219 /** 220 * Check if a record is valid. 221 */ 222 public function isValid(string $nsid, array $record): bool 223 { 224 try { 225 $this->validate($nsid, $record); 226 227 return true; 228 } catch (RecordValidationException) { 229 return false; 230 } 231 } 232 233 /** 234 * Get validation errors for a record. 235 * 236 * @return array<string> 237 */ 238 public function getErrors(string $nsid, array $record): array 239 { 240 try { 241 $this->validate($nsid, $record); 242 243 return []; 244 } catch (RecordValidationException $e) { 245 return [$e->getMessage()]; 246 } 247 } 248}