Parse and validate AT Protocol Lexicons with DTO generation for Laravel
1<?php
2
3namespace SocialDept\AtpSchema\Parser;
4
5use SocialDept\AtpSchema\Contracts\LexiconParser;
6use SocialDept\AtpSchema\Data\LexiconDocument;
7use SocialDept\AtpSchema\Exceptions\SchemaParseException;
8
9class DefaultLexiconParser implements LexiconParser
10{
11 /**
12 * Parse raw Lexicon JSON into structured objects.
13 */
14 public function parse(string $json): LexiconDocument
15 {
16 $data = json_decode($json, true);
17
18 if (json_last_error() !== JSON_ERROR_NONE) {
19 throw SchemaParseException::invalidJson('unknown', json_last_error_msg());
20 }
21
22 if (! is_array($data)) {
23 throw SchemaParseException::malformed('unknown', 'Schema must be a JSON object');
24 }
25
26 return $this->parseArray($data);
27 }
28
29 /**
30 * Parse Lexicon from array data.
31 */
32 public function parseArray(array $data): LexiconDocument
33 {
34 return LexiconDocument::fromArray($data);
35 }
36
37 /**
38 * Validate Lexicon schema structure.
39 */
40 public function validate(array $data): bool
41 {
42 try {
43 // Required fields
44 if (! isset($data['lexicon'])) {
45 return false;
46 }
47
48 if (! isset($data['id'])) {
49 return false;
50 }
51
52 if (! isset($data['defs'])) {
53 return false;
54 }
55
56 // Validate lexicon version
57 $lexicon = (int) $data['lexicon'];
58 if ($lexicon !== 1) {
59 return false;
60 }
61
62 // Validate NSID format
63 Nsid::parse($data['id']);
64
65 // Validate defs is an object/array
66 if (! is_array($data['defs'])) {
67 return false;
68 }
69
70 return true;
71 } catch (\Exception) {
72 return false;
73 }
74 }
75
76 /**
77 * Resolve $ref references to other schemas.
78 */
79 public function resolveReference(string $ref, LexiconDocument $context): mixed
80 {
81 // Local reference (starting with #)
82 if (str_starts_with($ref, '#')) {
83 $defName = substr($ref, 1);
84
85 return $context->getDefinition($defName);
86 }
87
88 // External reference with fragment (e.g., com.atproto.label.defs#selfLabels)
89 if (str_contains($ref, '#')) {
90 [$nsid, $defName] = explode('#', $ref, 2);
91
92 // Return the ref as-is - external refs need schema loading which should be handled by caller
93 return [
94 'type' => 'ref',
95 'ref' => $ref,
96 ];
97 }
98
99 // Full NSID reference - return as ref definition
100 return [
101 'type' => 'ref',
102 'ref' => $ref,
103 ];
104 }
105}