Parse and validate AT Protocol Lexicons with DTO generation for Laravel

Use DNS lexicon lookups as fallback

+8 -1
src/Console/GenerateCommand.php
··· 38 38 39 39 try { 40 40 $sources = config('schema.sources', []); 41 - $loader = new SchemaLoader($sources); 41 + $loader = new SchemaLoader( 42 + sources: $sources, 43 + useCache: config('schema.cache.enabled', true), 44 + cacheTtl: config('schema.cache.schema_ttl', 3600), 45 + cachePrefix: config('schema.cache.prefix', 'schema'), 46 + dnsResolutionEnabled: config('schema.dns_resolution.enabled', true), 47 + httpTimeout: config('schema.http.timeout', 10) 48 + ); 42 49 43 50 $generator = new DTOGenerator( 44 51 schemaLoader: $loader,
+2 -2
src/Generator/DTOGenerator.php
··· 61 61 $this->outputDirectory = rtrim($outputDirectory, '/'); 62 62 $this->typeParser = $typeParser ?? new TypeParser(schemaLoader: $schemaLoader); 63 63 $this->namespaceResolver = $namespaceResolver ?? new NamespaceResolver($baseNamespace); 64 - $this->templateRenderer = $templateRenderer ?? new TemplateRenderer; 65 - $this->fileWriter = $fileWriter ?? new FileWriter; 64 + $this->templateRenderer = $templateRenderer ?? new TemplateRenderer(); 65 + $this->fileWriter = $fileWriter ?? new FileWriter(); 66 66 } 67 67 68 68 /**
+76 -5
src/Parser/SchemaLoader.php
··· 3 3 namespace SocialDept\Schema\Parser; 4 4 5 5 use Illuminate\Support\Facades\Cache; 6 + use Illuminate\Support\Facades\Http; 6 7 use SocialDept\Schema\Exceptions\SchemaNotFoundException; 7 8 use SocialDept\Schema\Exceptions\SchemaParseException; 8 9 ··· 36 37 * Cache key prefix. 37 38 */ 38 39 protected string $cachePrefix; 40 + 41 + /** 42 + * Whether DNS resolution is enabled. 43 + */ 44 + protected bool $dnsResolutionEnabled; 45 + 46 + /** 47 + * HTTP timeout for schema fetching. 48 + */ 49 + protected int $httpTimeout; 39 50 40 51 /** 41 52 * Create a new SchemaLoader instance. ··· 46 57 array $sources, 47 58 bool $useCache = true, 48 59 int $cacheTtl = 3600, 49 - string $cachePrefix = 'schema' 60 + string $cachePrefix = 'schema', 61 + bool $dnsResolutionEnabled = true, 62 + int $httpTimeout = 10 50 63 ) { 51 64 $this->sources = $sources; 52 65 $this->useCache = $useCache; 53 66 $this->cacheTtl = $cacheTtl; 54 67 $this->cachePrefix = $cachePrefix; 68 + $this->dnsResolutionEnabled = $dnsResolutionEnabled; 69 + $this->httpTimeout = $httpTimeout; 55 70 } 56 71 57 72 /** ··· 117 132 } 118 133 } 119 134 135 + // Try DNS resolution as fallback if enabled 136 + if ($this->dnsResolutionEnabled) { 137 + $schema = $this->loadViaDns($nsid); 138 + 139 + if ($schema !== null) { 140 + return $schema; 141 + } 142 + } 143 + 120 144 throw SchemaNotFoundException::forNsid($nsid); 121 145 } 122 146 ··· 127 151 { 128 152 // Try NSID-based path (app.bsky.feed.post -> app/bsky/feed/post.json) 129 153 $nsidPath = $this->nsidToPath($nsid); 130 - $jsonPath = $source . '/' . $nsidPath . '.json'; 154 + $jsonPath = $source.'/'.$nsidPath.'.json'; 131 155 132 156 if (file_exists($jsonPath)) { 133 157 return $this->loadJsonFile($jsonPath, $nsid); 134 158 } 135 159 136 160 // Try PHP file 137 - $phpPath = $source . '/' . $nsidPath . '.php'; 161 + $phpPath = $source.'/'.$nsidPath.'.php'; 138 162 139 163 if (file_exists($phpPath)) { 140 164 return $this->loadPhpFile($phpPath, $nsid); 141 165 } 142 166 143 167 // Try flat structure (app.bsky.feed.post.json) 144 - $flatJsonPath = $source . '/' . $nsid . '.json'; 168 + $flatJsonPath = $source.'/'.$nsid.'.json'; 145 169 146 170 if (file_exists($flatJsonPath)) { 147 171 return $this->loadJsonFile($flatJsonPath, $nsid); 148 172 } 149 173 150 - $flatPhpPath = $source . '/' . $nsid . '.php'; 174 + $flatPhpPath = $source.'/'.$nsid.'.php'; 151 175 152 176 if (file_exists($flatPhpPath)) { 153 177 return $this->loadPhpFile($flatPhpPath, $nsid); ··· 241 265 public function getCachedNsids(): array 242 266 { 243 267 return array_keys($this->memoryCache); 268 + } 269 + 270 + /** 271 + * Load schema via DNS resolution from official sources. 272 + */ 273 + protected function loadViaDns(string $nsid): ?array 274 + { 275 + // Try to fetch from official AT Protocol GitHub repository 276 + $urls = $this->getOfficialSchemaUrls($nsid); 277 + 278 + foreach ($urls as $url) { 279 + try { 280 + $response = Http::timeout($this->httpTimeout) 281 + ->get($url); 282 + 283 + if ($response->successful()) { 284 + $data = $response->json(); 285 + 286 + if (is_array($data) && isset($data['lexicon'])) { 287 + return $data; 288 + } 289 + } 290 + } catch (\Exception $e) { 291 + // Continue to next URL 292 + continue; 293 + } 294 + } 295 + 296 + return null; 297 + } 298 + 299 + /** 300 + * Get official schema URLs for an NSID. 301 + * 302 + * @return array<string> 303 + */ 304 + protected function getOfficialSchemaUrls(string $nsid): array 305 + { 306 + $path = str_replace('.', '/', $nsid); 307 + 308 + return [ 309 + // Official AT Protocol lexicons repository (main branch) 310 + "https://raw.githubusercontent.com/bluesky-social/atproto/main/lexicons/{$path}.json", 311 + 312 + // Fallback to legacy location 313 + "https://raw.githubusercontent.com/bluesky-social/atproto/main/lexicons/{$nsid}.json", 314 + ]; 244 315 } 245 316 }
+11 -9
src/SchemaServiceProvider.php
··· 18 18 return new Parser\SchemaLoader( 19 19 sources: config('schema.sources', []), 20 20 useCache: config('schema.cache.enabled', true), 21 - cacheTtl: config('schema.cache.ttl', 3600), 22 - cachePrefix: config('schema.cache.prefix', 'schema') 21 + cacheTtl: config('schema.cache.schema_ttl', 3600), 22 + cachePrefix: config('schema.cache.prefix', 'schema'), 23 + dnsResolutionEnabled: config('schema.dns_resolution.enabled', true), 24 + httpTimeout: config('schema.http.timeout', 10) 23 25 ); 24 26 }); 25 27 ··· 86 88 87 89 // Register AT Protocol validation rules 88 90 $validator->extend('nsid', function ($attribute, $value) { 89 - $rule = new Validation\Rules\Nsid; 91 + $rule = new Validation\Rules\Nsid(); 90 92 $failed = false; 91 93 $rule->validate($attribute, $value, function () use (&$failed) { 92 94 $failed = true; ··· 96 98 }, 'The :attribute is not a valid NSID.'); 97 99 98 100 $validator->extend('did', function ($attribute, $value) { 99 - $rule = new Validation\Rules\Did; 101 + $rule = new Validation\Rules\Did(); 100 102 $failed = false; 101 103 $rule->validate($attribute, $value, function () use (&$failed) { 102 104 $failed = true; ··· 106 108 }, 'The :attribute is not a valid DID.'); 107 109 108 110 $validator->extend('handle', function ($attribute, $value) { 109 - $rule = new Validation\Rules\Handle; 111 + $rule = new Validation\Rules\Handle(); 110 112 $failed = false; 111 113 $rule->validate($attribute, $value, function () use (&$failed) { 112 114 $failed = true; ··· 116 118 }, 'The :attribute is not a valid handle.'); 117 119 118 120 $validator->extend('at_uri', function ($attribute, $value) { 119 - $rule = new Validation\Rules\AtUri; 121 + $rule = new Validation\Rules\AtUri(); 120 122 $failed = false; 121 123 $rule->validate($attribute, $value, function () use (&$failed) { 122 124 $failed = true; ··· 126 128 }, 'The :attribute is not a valid AT URI.'); 127 129 128 130 $validator->extend('at_datetime', function ($attribute, $value) { 129 - $rule = new Validation\Rules\AtDatetime; 131 + $rule = new Validation\Rules\AtDatetime(); 130 132 $failed = false; 131 133 $rule->validate($attribute, $value, function () use (&$failed) { 132 134 $failed = true; ··· 136 138 }, 'The :attribute is not a valid AT Protocol datetime.'); 137 139 138 140 $validator->extend('cid', function ($attribute, $value) { 139 - $rule = new Validation\Rules\Cid; 141 + $rule = new Validation\Rules\Cid(); 140 142 $failed = false; 141 143 $rule->validate($attribute, $value, function () use (&$failed) { 142 144 $failed = true; ··· 172 174 }, 'The :attribute must be at least :min_graphemes graphemes.'); 173 175 174 176 $validator->extend('language', function ($attribute, $value) { 175 - $rule = new Validation\Rules\Language; 177 + $rule = new Validation\Rules\Language(); 176 178 $failed = false; 177 179 $rule->validate($attribute, $value, function () use (&$failed) { 178 180 $failed = true;
-2
src/helpers.php
··· 49 49 return Schema::generate($nsid, $options); 50 50 } 51 51 } 52 - 53 -