Parse and validate AT Protocol Lexicons with DTO generation for Laravel
1<?php
2
3namespace SocialDept\AtpSchema\Data;
4
5use SocialDept\AtpSchema\Exceptions\SchemaValidationException;
6use SocialDept\AtpSchema\Parser\Nsid;
7
8class LexiconDocument
9{
10 /**
11 * Lexicon version.
12 */
13 public readonly int $lexicon;
14
15 /**
16 * NSID identifier.
17 */
18 public readonly Nsid $id;
19
20 /**
21 * Schema description.
22 */
23 public readonly ?string $description;
24
25 /**
26 * Schema definitions.
27 *
28 * @var array<string, array>
29 */
30 public readonly array $defs;
31
32 /**
33 * Raw schema data.
34 */
35 public readonly array $raw;
36
37 /**
38 * Source of the schema (file path, URL, etc).
39 */
40 public readonly ?string $source;
41
42 /**
43 * Create a new LexiconDocument.
44 *
45 * @param array<string, array> $defs
46 */
47 public function __construct(
48 int $lexicon,
49 Nsid $id,
50 array $defs,
51 ?string $description = null,
52 ?string $source = null,
53 ?array $raw = null
54 ) {
55 $this->lexicon = $lexicon;
56 $this->id = $id;
57 $this->defs = $defs;
58 $this->description = $description;
59 $this->source = $source;
60 $this->raw = $raw ?? [];
61 }
62
63 /**
64 * Create from array data.
65 */
66 public static function fromArray(array $data, ?string $source = null): self
67 {
68 if (! isset($data['lexicon'])) {
69 throw SchemaValidationException::missingField('unknown', 'lexicon');
70 }
71
72 if (! isset($data['id'])) {
73 throw SchemaValidationException::missingField('unknown', 'id');
74 }
75
76 if (! isset($data['defs'])) {
77 throw SchemaValidationException::missingField($data['id'], 'defs');
78 }
79
80 $lexicon = (int) $data['lexicon'];
81 if ($lexicon !== 1) {
82 throw SchemaValidationException::invalidVersion($data['id'], $lexicon);
83 }
84
85 return new self(
86 lexicon: $lexicon,
87 id: Nsid::parse($data['id']),
88 defs: $data['defs'],
89 description: $data['description'] ?? null,
90 source: $source,
91 raw: $data
92 );
93 }
94
95 /**
96 * Create from JSON string.
97 */
98 public static function fromJson(string $json, ?string $source = null): self
99 {
100 $data = json_decode($json, true);
101
102 if (json_last_error() !== JSON_ERROR_NONE) {
103 throw new \InvalidArgumentException('Invalid JSON: '.json_last_error_msg());
104 }
105
106 if (! is_array($data)) {
107 throw new \InvalidArgumentException('JSON must decode to an array');
108 }
109
110 return self::fromArray($data, $source);
111 }
112
113 /**
114 * Get a definition by name.
115 */
116 public function getDefinition(string $name): ?array
117 {
118 return $this->defs[$name] ?? null;
119 }
120
121 /**
122 * Check if definition exists.
123 */
124 public function hasDefinition(string $name): bool
125 {
126 return isset($this->defs[$name]);
127 }
128
129 /**
130 * Get the main definition.
131 */
132 public function getMainDefinition(): ?array
133 {
134 return $this->getDefinition('main');
135 }
136
137 /**
138 * Get all definition names.
139 *
140 * @return array<string>
141 */
142 public function getDefinitionNames(): array
143 {
144 return array_keys($this->defs);
145 }
146
147 /**
148 * Get NSID as string.
149 */
150 public function getNsid(): string
151 {
152 return $this->id->toString();
153 }
154
155 /**
156 * Get lexicon version.
157 */
158 public function getVersion(): int
159 {
160 return $this->lexicon;
161 }
162
163 /**
164 * Convert to array.
165 */
166 public function toArray(): array
167 {
168 return [
169 'lexicon' => $this->lexicon,
170 'id' => $this->id->toString(),
171 'description' => $this->description,
172 'defs' => $this->defs,
173 ];
174 }
175
176 /**
177 * Check if this is a record schema.
178 */
179 public function isRecord(): bool
180 {
181 $main = $this->getMainDefinition();
182
183 return $main !== null && ($main['type'] ?? null) === 'record';
184 }
185
186 /**
187 * Check if this is a query schema.
188 */
189 public function isQuery(): bool
190 {
191 $main = $this->getMainDefinition();
192
193 return $main !== null && ($main['type'] ?? null) === 'query';
194 }
195
196 /**
197 * Check if this is a procedure schema.
198 */
199 public function isProcedure(): bool
200 {
201 $main = $this->getMainDefinition();
202
203 return $main !== null && ($main['type'] ?? null) === 'procedure';
204 }
205
206 /**
207 * Check if this is a subscription schema.
208 */
209 public function isSubscription(): bool
210 {
211 $main = $this->getMainDefinition();
212
213 return $main !== null && ($main['type'] ?? null) === 'subscription';
214 }
215}