Parse and validate AT Protocol Lexicons with DTO generation for Laravel
1<?php
2
3namespace SocialDept\AtpSchema\Console;
4
5use Illuminate\Console\Command;
6use SocialDept\AtpSchema\Parser\SchemaLoader;
7
8class ListCommand extends Command
9{
10 /**
11 * The name and signature of the console command.
12 */
13 protected $signature = 'schema:list
14 {--filter= : Filter schemas by pattern (supports wildcards)}
15 {--type= : Filter by schema type (record, query, procedure, subscription)}';
16
17 /**
18 * The console command description.
19 */
20 protected $description = 'List all available ATProto Lexicon schemas';
21
22 /**
23 * Execute the console command.
24 */
25 public function handle(): int
26 {
27 $filter = $this->option('filter');
28 $type = $this->option('type');
29
30 try {
31 $sources = config('schema.sources', []);
32 $loader = new SchemaLoader($sources);
33
34 $schemas = $this->discoverSchemas($sources);
35
36 if ($filter) {
37 $schemas = $this->filterSchemas($schemas, $filter);
38 }
39
40 if ($type) {
41 $schemas = $this->filterByType($schemas, $type, $loader);
42 }
43
44 if (empty($schemas)) {
45 $this->info('No schemas found');
46
47 return self::SUCCESS;
48 }
49
50 $this->info('Found '.count($schemas).' schema(s):');
51 $this->newLine();
52
53 $tableData = [];
54
55 foreach ($schemas as $nsid) {
56 try {
57 $document = $loader->load($nsid);
58
59 $schemaType = 'unknown';
60 if ($document->isRecord()) {
61 $schemaType = 'record';
62 } elseif ($document->isQuery()) {
63 $schemaType = 'query';
64 } elseif ($document->isProcedure()) {
65 $schemaType = 'procedure';
66 } elseif ($document->isSubscription()) {
67 $schemaType = 'subscription';
68 }
69
70 $tableData[] = [
71 $nsid,
72 $schemaType,
73 $document->description ?? '-',
74 ];
75 } catch (\Exception $e) {
76 $tableData[] = [
77 $nsid,
78 'error',
79 $e->getMessage(),
80 ];
81 }
82 }
83
84 $this->table(['NSID', 'Type', 'Description'], $tableData);
85
86 return self::SUCCESS;
87 } catch (\Exception $e) {
88 $this->error('Failed to list schemas: '.$e->getMessage());
89
90 if ($this->output->isVerbose()) {
91 $this->error($e->getTraceAsString());
92 }
93
94 return self::FAILURE;
95 }
96 }
97
98 /**
99 * Discover all schema files in sources.
100 *
101 * @param array<string> $sources
102 * @return array<string>
103 */
104 protected function discoverSchemas(array $sources): array
105 {
106 $schemas = [];
107
108 foreach ($sources as $source) {
109 if (! is_dir($source)) {
110 continue;
111 }
112
113 $schemas = array_merge($schemas, $this->scanDirectory($source));
114 }
115
116 return array_unique($schemas);
117 }
118
119 /**
120 * Scan directory for schema files.
121 *
122 * @return array<string>
123 */
124 protected function scanDirectory(string $directory, string $prefix = ''): array
125 {
126 $schemas = [];
127 $items = scandir($directory);
128
129 foreach ($items as $item) {
130 if ($item === '.' || $item === '..') {
131 continue;
132 }
133
134 $path = $directory.'/'.$item;
135
136 if (is_dir($path)) {
137 $newPrefix = $prefix ? $prefix.'.'.$item : $item;
138 $schemas = array_merge($schemas, $this->scanDirectory($path, $newPrefix));
139 } elseif (pathinfo($item, PATHINFO_EXTENSION) === 'json' || pathinfo($item, PATHINFO_EXTENSION) === 'php') {
140 $name = pathinfo($item, PATHINFO_FILENAME);
141 $nsid = $prefix ? $prefix.'.'.$name : $name;
142 $schemas[] = $nsid;
143 }
144 }
145
146 return $schemas;
147 }
148
149 /**
150 * Filter schemas by pattern.
151 *
152 * @param array<string> $schemas
153 * @return array<string>
154 */
155 protected function filterSchemas(array $schemas, string $pattern): array
156 {
157 $pattern = str_replace('*', '.*', preg_quote($pattern, '/'));
158
159 return array_filter($schemas, function ($nsid) use ($pattern) {
160 return preg_match("/^{$pattern}$/", $nsid);
161 });
162 }
163
164 /**
165 * Filter schemas by type.
166 *
167 * @param array<string> $schemas
168 * @return array<string>
169 */
170 protected function filterByType(array $schemas, string $type, SchemaLoader $loader): array
171 {
172 return array_filter($schemas, function ($nsid) use ($type, $loader) {
173 try {
174 $document = $loader->load($nsid);
175
176 return match ($type) {
177 'record' => $document->isRecord(),
178 'query' => $document->isQuery(),
179 'procedure' => $document->isProcedure(),
180 'subscription' => $document->isSubscription(),
181 default => false,
182 };
183 } catch (\Exception) {
184 return false;
185 }
186 });
187 }
188}