+152
-9
src/Console/GenerateCommand.php
+152
-9
src/Console/GenerateCommand.php
···
16
16
{--output= : Output directory for generated files}
17
17
{--namespace= : Base namespace for generated classes}
18
18
{--force : Overwrite existing files}
19
-
{--dry-run : Preview generated code without writing files}';
19
+
{--dry-run : Preview generated code without writing files}
20
+
{--with-dependencies : Also generate all referenced lexicons recursively}
21
+
{--r|recursive : Alias for --with-dependencies}';
20
22
21
23
/**
22
24
* The console command description.
···
24
26
protected $description = 'Generate PHP DTO classes from ATProto Lexicon schemas';
25
27
26
28
/**
29
+
* Track generated NSIDs to avoid duplicates.
30
+
*/
31
+
protected array $generated = [];
32
+
33
+
/**
27
34
* Execute the console command.
28
35
*/
29
36
public function handle(): int
···
33
40
$namespace = $this->option('namespace') ?? config('schema.lexicons.base_namespace');
34
41
$force = $this->option('force');
35
42
$dryRun = $this->option('dry-run');
43
+
$withDependencies = $this->option('with-dependencies') || $this->option('recursive');
36
44
37
45
$this->info("Generating DTO classes for schema: {$nsid}");
38
46
···
55
63
56
64
if ($dryRun) {
57
65
$this->info('Dry run mode - no files will be written');
58
-
$schema = $loader->load($nsid);
59
-
$document = \SocialDept\Schema\Data\LexiconDocument::fromArray($schema);
66
+
$document = $loader->load($nsid);
60
67
$code = $generator->preview($document);
61
68
62
69
$this->line('');
···
68
75
return self::SUCCESS;
69
76
}
70
77
71
-
$files = $generator->generateByNsid($nsid, [
72
-
'dryRun' => false,
73
-
'overwrite' => $force,
74
-
]);
78
+
$allFiles = [];
79
+
80
+
if ($withDependencies) {
81
+
$this->info('Generating with dependencies...');
82
+
$allFiles = $this->generateWithDependencies($nsid, $loader, $generator, $force);
83
+
} else {
84
+
$allFiles = $generator->generateByNsid($nsid, [
85
+
'dryRun' => false,
86
+
'overwrite' => $force,
87
+
]);
88
+
}
75
89
76
-
$this->info('Generated '.count($files).' file(s):');
90
+
$this->newLine();
91
+
$this->info('Generated '.count($allFiles).' file(s):');
77
92
78
-
foreach ($files as $file) {
93
+
foreach ($allFiles as $file) {
79
94
$this->line(" - {$file}");
80
95
}
81
96
···
92
107
93
108
return self::FAILURE;
94
109
}
110
+
}
111
+
112
+
/**
113
+
* Generate schema with all its dependencies recursively.
114
+
*/
115
+
protected function generateWithDependencies(
116
+
string $nsid,
117
+
SchemaLoader $loader,
118
+
DTOGenerator $generator,
119
+
bool $force
120
+
): array {
121
+
// Skip if already generated
122
+
if (in_array($nsid, $this->generated)) {
123
+
return [];
124
+
}
125
+
126
+
$this->line(" → Loading schema: {$nsid}");
127
+
128
+
try {
129
+
$schema = $loader->load($nsid);
130
+
} catch (\Exception $e) {
131
+
$this->warn(" ⚠ Could not load {$nsid}: ".$e->getMessage());
132
+
133
+
return [];
134
+
}
135
+
136
+
// Extract all referenced NSIDs from this schema
137
+
$dependencies = $this->extractDependencies($schema);
138
+
139
+
$allFiles = [];
140
+
141
+
// Generate dependencies first
142
+
foreach ($dependencies as $depNsid) {
143
+
$depFiles = $this->generateWithDependencies($depNsid, $loader, $generator, $force);
144
+
$allFiles = array_merge($allFiles, $depFiles);
145
+
}
146
+
147
+
// Mark as generated before generating to prevent circular references
148
+
$this->generated[] = $nsid;
149
+
150
+
// Generate current schema
151
+
try {
152
+
$files = $generator->generateByNsid($nsid, [
153
+
'dryRun' => false,
154
+
'overwrite' => $force,
155
+
]);
156
+
$allFiles = array_merge($allFiles, $files);
157
+
} catch (\Exception $e) {
158
+
$this->warn(" ⚠ Could not generate {$nsid}: ".$e->getMessage());
159
+
}
160
+
161
+
return $allFiles;
162
+
}
163
+
164
+
/**
165
+
* Extract all NSID dependencies from a schema.
166
+
*/
167
+
protected function extractDependencies(\SocialDept\Schema\Data\LexiconDocument $schema): array
168
+
{
169
+
$dependencies = [];
170
+
$currentNsid = $schema->getNsid();
171
+
172
+
// Walk through all definitions
173
+
foreach ($schema->defs as $def) {
174
+
$dependencies = array_merge($dependencies, $this->extractRefsFromDefinition($def));
175
+
}
176
+
177
+
// Filter out refs that are definitions within the same schema
178
+
// (refs that start with the current NSID followed by a dot)
179
+
$dependencies = array_filter($dependencies, function ($ref) use ($currentNsid) {
180
+
return ! str_starts_with($ref, $currentNsid.'.');
181
+
});
182
+
183
+
return array_unique($dependencies);
184
+
}
185
+
186
+
/**
187
+
* Recursively extract refs from a definition.
188
+
*/
189
+
protected function extractRefsFromDefinition(array $definition): array
190
+
{
191
+
$refs = [];
192
+
193
+
// Handle direct ref
194
+
if (isset($definition['ref'])) {
195
+
$ref = $definition['ref'];
196
+
// Skip local references (starting with #)
197
+
if (! str_starts_with($ref, '#')) {
198
+
// Extract NSID part (before fragment)
199
+
if (str_contains($ref, '#')) {
200
+
$ref = explode('#', $ref)[0];
201
+
}
202
+
$refs[] = $ref;
203
+
}
204
+
}
205
+
206
+
// Handle union refs
207
+
if (isset($definition['refs']) && is_array($definition['refs'])) {
208
+
foreach ($definition['refs'] as $ref) {
209
+
// Skip local references
210
+
if (! str_starts_with($ref, '#')) {
211
+
// Extract NSID part
212
+
if (str_contains($ref, '#')) {
213
+
$ref = explode('#', $ref)[0];
214
+
}
215
+
$refs[] = $ref;
216
+
}
217
+
}
218
+
}
219
+
220
+
// Recursively check properties
221
+
if (isset($definition['properties']) && is_array($definition['properties'])) {
222
+
foreach ($definition['properties'] as $propDef) {
223
+
$refs = array_merge($refs, $this->extractRefsFromDefinition($propDef));
224
+
}
225
+
}
226
+
227
+
// Recursively check record
228
+
if (isset($definition['record']) && is_array($definition['record'])) {
229
+
$refs = array_merge($refs, $this->extractRefsFromDefinition($definition['record']));
230
+
}
231
+
232
+
// Recursively check array items
233
+
if (isset($definition['items']) && is_array($definition['items'])) {
234
+
$refs = array_merge($refs, $this->extractRefsFromDefinition($definition['items']));
235
+
}
236
+
237
+
return $refs;
95
238
}
96
239
}
+2
-5
src/Console/ListCommand.php
+2
-5
src/Console/ListCommand.php
···
3
3
namespace SocialDept\Schema\Console;
4
4
5
5
use Illuminate\Console\Command;
6
-
use SocialDept\Schema\Data\LexiconDocument;
7
6
use SocialDept\Schema\Parser\SchemaLoader;
8
7
9
8
class ListCommand extends Command
···
55
54
56
55
foreach ($schemas as $nsid) {
57
56
try {
58
-
$schema = $loader->load($nsid);
59
-
$document = LexiconDocument::fromArray($schema);
57
+
$document = $loader->load($nsid);
60
58
61
59
$schemaType = 'unknown';
62
60
if ($document->isRecord()) {
···
173
171
{
174
172
return array_filter($schemas, function ($nsid) use ($type, $loader) {
175
173
try {
176
-
$schema = $loader->load($nsid);
177
-
$document = LexiconDocument::fromArray($schema);
174
+
$document = $loader->load($nsid);
178
175
179
176
return match ($type) {
180
177
'record' => $document->isRecord(),
+1
-3
src/Console/ValidateCommand.php
+1
-3
src/Console/ValidateCommand.php
···
3
3
namespace SocialDept\Schema\Console;
4
4
5
5
use Illuminate\Console\Command;
6
-
use SocialDept\Schema\Data\LexiconDocument;
7
6
use SocialDept\Schema\Parser\SchemaLoader;
8
7
use SocialDept\Schema\Validation\LexiconValidator;
9
8
···
64
63
65
64
$this->info("Validating data against schema: {$nsid}");
66
65
67
-
$schema = $loader->load($nsid);
68
-
$document = LexiconDocument::fromArray($schema);
66
+
$document = $loader->load($nsid);
69
67
70
68
$errors = $validator->validateWithErrors($data, $document);
71
69
+21
src/Contracts/DiscriminatedUnion.php
+21
src/Contracts/DiscriminatedUnion.php
···
1
+
<?php
2
+
3
+
namespace SocialDept\Schema\Contracts;
4
+
5
+
/**
6
+
* Contract for Data classes that can participate in AT Protocol discriminated unions.
7
+
*
8
+
* Union types in AT Protocol use the $type field to discriminate between
9
+
* different variants. This interface marks classes that can be used as
10
+
* union variants and provides access to their discriminator value.
11
+
*/
12
+
interface DiscriminatedUnion
13
+
{
14
+
/**
15
+
* Get the lexicon NSID that identifies this union variant.
16
+
*
17
+
* This value is used as the $type discriminator in AT Protocol records
18
+
* to identify which specific type a union contains.
19
+
*/
20
+
public static function getDiscriminator(): string;
21
+
}
+25
-6
src/Data/Data.php
+25
-6
src/Data/Data.php
···
5
5
use Illuminate\Contracts\Support\Arrayable;
6
6
use Illuminate\Contracts\Support\Jsonable;
7
7
use JsonSerializable;
8
+
use SocialDept\Schema\Contracts\DiscriminatedUnion;
8
9
use Stringable;
9
10
10
-
abstract class Data implements Arrayable, Jsonable, JsonSerializable, Stringable
11
+
abstract class Data implements Arrayable, DiscriminatedUnion, Jsonable, JsonSerializable, Stringable
11
12
{
12
13
/**
13
14
* Get the lexicon NSID for this data type.
···
15
16
abstract public static function getLexicon(): string;
16
17
17
18
/**
19
+
* Get the lexicon NSID that identifies this union variant.
20
+
*
21
+
* This is an alias for getLexicon() to satisfy the DiscriminatedUnion contract.
22
+
*/
23
+
public static function getDiscriminator(): string
24
+
{
25
+
return static::getLexicon();
26
+
}
27
+
28
+
/**
18
29
* Convert the data to an array.
19
30
*/
20
31
public function toArray(): array
···
22
33
$result = [];
23
34
24
35
foreach (get_object_vars($this) as $property => $value) {
25
-
$result[$property] = $this->serializeValue($value);
36
+
// Skip null values to exclude optional fields that aren't set
37
+
if ($value !== null) {
38
+
$result[$property] = $this->serializeValue($value);
39
+
}
26
40
}
27
41
28
-
return $result;
42
+
return array_filter($result);
29
43
}
30
44
31
45
/**
···
57
71
*/
58
72
protected function serializeValue(mixed $value): mixed
59
73
{
74
+
// Union variants must include $type for discrimination
60
75
if ($value instanceof self) {
61
-
return $value->toArray();
76
+
return $value->toRecord();
62
77
}
63
78
64
79
if ($value instanceof Arrayable) {
65
80
return $value->toArray();
66
81
}
67
82
83
+
// Preserve arrays with $type (open union data)
68
84
if (is_array($value)) {
69
85
return array_map(fn ($item) => $this->serializeValue($item), $value);
70
86
}
···
103
119
*/
104
120
public static function fromRecord(array $record): static
105
121
{
106
-
return static::fromArray($record);
122
+
return static::fromArray($record['value'] ?? $record);
107
123
}
108
124
109
125
/**
···
114
130
*/
115
131
public function toRecord(): array
116
132
{
117
-
return $this->toArray();
133
+
return [
134
+
...$this->toArray(),
135
+
'$type' => $this->getLexicon(),
136
+
];
118
137
}
119
138
120
139
/**
+1
-1
src/Exceptions/BlobException.php
+1
-1
src/Exceptions/BlobException.php
···
32
32
public static function invalidMimeType(string $mimeType, array $accepted): self
33
33
{
34
34
return static::withContext(
35
-
"Invalid MIME type {$mimeType}. Accepted: " . implode(', ', $accepted),
35
+
"Invalid MIME type {$mimeType}. Accepted: ".implode(', ', $accepted),
36
36
['mimeType' => $mimeType, 'accepted' => $accepted]
37
37
);
38
38
}
+1
-1
src/Exceptions/SchemaValidationException.php
+1
-1
src/Exceptions/SchemaValidationException.php
···
9
9
*/
10
10
public static function invalidStructure(string $nsid, array $errors): self
11
11
{
12
-
$message = "Schema validation failed for {$nsid}:\n" . implode("\n", $errors);
12
+
$message = "Schema validation failed for {$nsid}:\n".implode("\n", $errors);
13
13
14
14
return static::withContext($message, [
15
15
'nsid' => $nsid,
+5
-4
src/Facades/Schema.php
+5
-4
src/Facades/Schema.php
···
6
6
use SocialDept\Schema\Data\LexiconDocument;
7
7
8
8
/**
9
-
* @method static array load(string $nsid)
9
+
* @method static LexiconDocument load(string $nsid)
10
+
* @method static LexiconDocument|null find(string $nsid)
10
11
* @method static bool exists(string $nsid)
11
-
* @method static LexiconDocument parse(string $nsid)
12
+
* @method static array all()
13
+
* @method static void clearCache(?string $nsid = null)
14
+
* @method static string generate(string $nsid, ?string $outputPath = null)
12
15
* @method static bool validate(string $nsid, array $data)
13
16
* @method static array validateWithErrors(string $nsid, array $data)
14
-
* @method static string generate(string $nsid, array $options = [])
15
-
* @method static void clearCache(?string $nsid = null)
16
17
*
17
18
* @see \SocialDept\Schema\SchemaManager
18
19
*/
+1
-1
src/Parser/ComplexTypeParser.php
+1
-1
src/Parser/ComplexTypeParser.php
+105
src/Parser/DefaultLexiconParser.php
+105
src/Parser/DefaultLexiconParser.php
···
1
+
<?php
2
+
3
+
namespace SocialDept\Schema\Parser;
4
+
5
+
use SocialDept\Schema\Contracts\LexiconParser;
6
+
use SocialDept\Schema\Data\LexiconDocument;
7
+
use SocialDept\Schema\Exceptions\SchemaParseException;
8
+
9
+
class 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
+
}
+206
src/Parser/DnsLexiconResolver.php
+206
src/Parser/DnsLexiconResolver.php
···
1
+
<?php
2
+
3
+
namespace SocialDept\Schema\Parser;
4
+
5
+
use Illuminate\Support\Facades\Http;
6
+
use SocialDept\Schema\Contracts\LexiconParser;
7
+
use SocialDept\Schema\Contracts\LexiconResolver;
8
+
use SocialDept\Schema\Data\LexiconDocument;
9
+
use SocialDept\Schema\Exceptions\SchemaNotFoundException;
10
+
11
+
class DnsLexiconResolver implements LexiconResolver
12
+
{
13
+
/**
14
+
* Whether DNS resolution is enabled.
15
+
*/
16
+
protected bool $enabled;
17
+
18
+
/**
19
+
* HTTP timeout in seconds.
20
+
*/
21
+
protected int $httpTimeout;
22
+
23
+
/**
24
+
* Lexicon parser instance.
25
+
*/
26
+
protected LexiconParser $parser;
27
+
28
+
/**
29
+
* Whether the atp-resolver package is available.
30
+
*/
31
+
protected bool $hasResolver;
32
+
33
+
/**
34
+
* Whether we've shown the resolver warning.
35
+
*/
36
+
protected static bool $resolverWarningShown = false;
37
+
38
+
/**
39
+
* Create a new DnsLexiconResolver.
40
+
*/
41
+
public function __construct(
42
+
bool $enabled = true,
43
+
int $httpTimeout = 10,
44
+
?LexiconParser $parser = null
45
+
) {
46
+
$this->enabled = $enabled;
47
+
$this->httpTimeout = $httpTimeout;
48
+
$this->parser = $parser ?? new DefaultLexiconParser;
49
+
$this->hasResolver = class_exists('SocialDept\\Resolver\\Resolver');
50
+
}
51
+
52
+
/**
53
+
* Resolve NSID to Lexicon schema via DNS and XRPC.
54
+
*/
55
+
public function resolve(string $nsid): LexiconDocument
56
+
{
57
+
if (! $this->enabled) {
58
+
throw SchemaNotFoundException::forNsid($nsid);
59
+
}
60
+
61
+
if (! $this->hasResolver) {
62
+
$this->showResolverWarning();
63
+
throw SchemaNotFoundException::forNsid($nsid);
64
+
}
65
+
66
+
try {
67
+
$nsidParsed = Nsid::parse($nsid);
68
+
69
+
// Step 1: Query DNS TXT record for DID
70
+
$did = $this->lookupDns($nsidParsed->getAuthority());
71
+
if ($did === null) {
72
+
throw SchemaNotFoundException::forNsid($nsid);
73
+
}
74
+
75
+
// Step 2: Resolve DID to PDS endpoint
76
+
$pdsUrl = $this->resolvePdsEndpoint($did);
77
+
if ($pdsUrl === null) {
78
+
throw SchemaNotFoundException::forNsid($nsid);
79
+
}
80
+
81
+
// Step 3: Fetch lexicon schema from repository
82
+
$schema = $this->retrieveSchema($pdsUrl, $did, $nsid);
83
+
84
+
return $this->parser->parseArray($schema);
85
+
} catch (SchemaNotFoundException $e) {
86
+
throw $e;
87
+
} catch (\Exception $e) {
88
+
throw SchemaNotFoundException::forNsid($nsid);
89
+
}
90
+
}
91
+
92
+
/**
93
+
* Perform DNS TXT lookup for _lexicon.{authority}.
94
+
*/
95
+
public function lookupDns(string $authority): ?string
96
+
{
97
+
// Convert authority to domain (e.g., pub.leaflet -> leaflet.pub)
98
+
$parts = explode('.', $authority);
99
+
$domain = implode('.', array_reverse($parts));
100
+
101
+
// Query DNS TXT record at _lexicon.<domain>
102
+
$hostname = "_lexicon.{$domain}";
103
+
104
+
try {
105
+
$records = dns_get_record($hostname, DNS_TXT);
106
+
107
+
if ($records === false || empty($records)) {
108
+
return null;
109
+
}
110
+
111
+
// Look for TXT record with did= prefix
112
+
foreach ($records as $record) {
113
+
if (isset($record['txt']) && str_starts_with($record['txt'], 'did=')) {
114
+
return substr($record['txt'], 4); // Remove 'did=' prefix
115
+
}
116
+
}
117
+
} catch (\Exception $e) {
118
+
// DNS query failed
119
+
return null;
120
+
}
121
+
122
+
return null;
123
+
}
124
+
125
+
/**
126
+
* Retrieve schema via XRPC from PDS.
127
+
*/
128
+
public function retrieveSchema(string $pdsEndpoint, string $did, string $nsid): array
129
+
{
130
+
try {
131
+
// Construct XRPC call to com.atproto.repo.getRecord
132
+
$response = Http::timeout($this->httpTimeout)
133
+
->get("{$pdsEndpoint}/xrpc/com.atproto.repo.getRecord", [
134
+
'repo' => $did,
135
+
'collection' => 'com.atproto.lexicon.schema',
136
+
'rkey' => $nsid,
137
+
]);
138
+
139
+
if ($response->successful()) {
140
+
$data = $response->json();
141
+
142
+
// Extract the lexicon schema from the record value
143
+
if (isset($data['value']) && is_array($data['value']) && isset($data['value']['lexicon'])) {
144
+
return $data['value'];
145
+
}
146
+
}
147
+
} catch (\Exception $e) {
148
+
throw SchemaNotFoundException::forNsid($nsid);
149
+
}
150
+
151
+
throw SchemaNotFoundException::forNsid($nsid);
152
+
}
153
+
154
+
/**
155
+
* Check if DNS resolution is enabled.
156
+
*/
157
+
public function isEnabled(): bool
158
+
{
159
+
return $this->enabled;
160
+
}
161
+
162
+
/**
163
+
* Resolve DID to PDS endpoint using atp-resolver.
164
+
*/
165
+
protected function resolvePdsEndpoint(string $did): ?string
166
+
{
167
+
if (! $this->hasResolver) {
168
+
return null;
169
+
}
170
+
171
+
try {
172
+
// Get resolver from Laravel container if available
173
+
if (function_exists('app') && app()->has(\SocialDept\Resolver\Resolver::class)) {
174
+
$resolver = app(\SocialDept\Resolver\Resolver::class);
175
+
} else {
176
+
// Can't instantiate without dependencies
177
+
return null;
178
+
}
179
+
180
+
// Use the resolvePds method which handles DID resolution and PDS extraction
181
+
return $resolver->resolvePds($did);
182
+
} catch (\Exception $e) {
183
+
return null;
184
+
}
185
+
}
186
+
187
+
/**
188
+
* Show warning about missing atp-resolver package.
189
+
*/
190
+
protected function showResolverWarning(): void
191
+
{
192
+
if (self::$resolverWarningShown) {
193
+
return;
194
+
}
195
+
196
+
if (function_exists('logger')) {
197
+
logger()->warning(
198
+
'DNS-based lexicon resolution requires the socialdept/atp-resolver package. '.
199
+
'Install it with: composer require socialdept/atp-resolver '.
200
+
'Falling back to local lexicon sources only.'
201
+
);
202
+
}
203
+
204
+
self::$resolverWarningShown = true;
205
+
}
206
+
}
+58
src/Parser/InMemoryLexiconRegistry.php
+58
src/Parser/InMemoryLexiconRegistry.php
···
1
+
<?php
2
+
3
+
namespace SocialDept\Schema\Parser;
4
+
5
+
use SocialDept\Schema\Contracts\LexiconRegistry;
6
+
use SocialDept\Schema\Data\LexiconDocument;
7
+
8
+
class InMemoryLexiconRegistry implements LexiconRegistry
9
+
{
10
+
/**
11
+
* Registered lexicon documents.
12
+
*
13
+
* @var array<string, LexiconDocument>
14
+
*/
15
+
protected array $documents = [];
16
+
17
+
/**
18
+
* Register a lexicon document.
19
+
*/
20
+
public function register(LexiconDocument $document): void
21
+
{
22
+
$this->documents[$document->getNsid()] = $document;
23
+
}
24
+
25
+
/**
26
+
* Get a lexicon document by NSID.
27
+
*/
28
+
public function get(string $nsid): ?LexiconDocument
29
+
{
30
+
return $this->documents[$nsid] ?? null;
31
+
}
32
+
33
+
/**
34
+
* Check if a lexicon document exists.
35
+
*/
36
+
public function has(string $nsid): bool
37
+
{
38
+
return isset($this->documents[$nsid]);
39
+
}
40
+
41
+
/**
42
+
* Get all registered lexicon documents.
43
+
*
44
+
* @return array<string, LexiconDocument>
45
+
*/
46
+
public function all(): array
47
+
{
48
+
return $this->documents;
49
+
}
50
+
51
+
/**
52
+
* Clear all registered lexicon documents.
53
+
*/
54
+
public function clear(): void
55
+
{
56
+
$this->documents = [];
57
+
}
58
+
}
+2
-2
src/Parser/Nsid.php
+2
-2
src/Parser/Nsid.php
···
50
50
51
51
if (strlen($this->nsid) > self::MAX_LENGTH) {
52
52
throw SchemaException::withContext(
53
-
"NSID exceeds maximum length of " . self::MAX_LENGTH . " characters",
53
+
'NSID exceeds maximum length of '.self::MAX_LENGTH.' characters',
54
54
['nsid' => $this->nsid, 'length' => strlen($this->nsid)]
55
55
);
56
56
}
···
65
65
$segments = explode('.', $this->nsid);
66
66
if (count($segments) < self::MIN_SEGMENTS) {
67
67
throw SchemaException::withContext(
68
-
'NSID must have at least ' . self::MIN_SEGMENTS . ' segments',
68
+
'NSID must have at least '.self::MIN_SEGMENTS.' segments',
69
69
['nsid' => $this->nsid, 'segments' => count($segments)]
70
70
);
71
71
}
+1
-1
src/Parser/TypeParser.php
+1
-1
src/Parser/TypeParser.php
···
45
45
?ComplexTypeParser $complexParser = null,
46
46
?SchemaLoader $schemaLoader = null
47
47
) {
48
-
$this->primitiveParser = $primitiveParser ?? new PrimitiveParser();
48
+
$this->primitiveParser = $primitiveParser ?? new PrimitiveParser;
49
49
$this->complexParser = $complexParser ?? new ComplexTypeParser($this->primitiveParser);
50
50
$this->schemaLoader = $schemaLoader;
51
51
}
+20
-12
src/SchemaManager.php
+20
-12
src/SchemaManager.php
···
40
40
/**
41
41
* Load a schema by NSID.
42
42
*/
43
-
public function load(string $nsid): array
43
+
public function load(string $nsid): LexiconDocument
44
44
{
45
45
return $this->loader->load($nsid);
46
46
}
47
47
48
48
/**
49
-
* Check if a schema exists.
49
+
* Find a schema by NSID (nullable).
50
50
*/
51
-
public function exists(string $nsid): bool
51
+
public function find(string $nsid): ?LexiconDocument
52
52
{
53
-
return $this->loader->exists($nsid);
53
+
return $this->loader->find($nsid);
54
54
}
55
55
56
56
/**
57
-
* Parse a schema into a LexiconDocument.
57
+
* Get all available schemas.
58
+
*
59
+
* @return array<string>
58
60
*/
59
-
public function parse(string $nsid): LexiconDocument
61
+
public function all(): array
60
62
{
61
-
$schema = $this->loader->load($nsid);
63
+
return $this->loader->all();
64
+
}
62
65
63
-
return LexiconDocument::fromArray($schema);
66
+
/**
67
+
* Check if a schema exists.
68
+
*/
69
+
public function exists(string $nsid): bool
70
+
{
71
+
return $this->loader->exists($nsid);
64
72
}
65
73
66
74
/**
···
68
76
*/
69
77
public function validate(string $nsid, array $data): bool
70
78
{
71
-
$document = $this->parse($nsid);
79
+
$document = $this->load($nsid);
72
80
73
81
return $this->validator->validate($data, $document);
74
82
}
···
80
88
*/
81
89
public function validateWithErrors(string $nsid, array $data): array
82
90
{
83
-
$document = $this->parse($nsid);
91
+
$document = $this->load($nsid);
84
92
85
93
return $this->validator->validateWithErrors($data, $document);
86
94
}
···
88
96
/**
89
97
* Generate DTO code from a schema.
90
98
*/
91
-
public function generate(string $nsid, array $options = []): string
99
+
public function generate(string $nsid, ?string $outputPath = null): string
92
100
{
93
101
if ($this->generator === null) {
94
102
throw new \RuntimeException('Generator not available');
95
103
}
96
104
97
-
$document = $this->parse($nsid);
105
+
$document = $this->load($nsid);
98
106
99
107
return $this->generator->generate($document);
100
108
}
+51
-7
src/SchemaServiceProvider.php
+51
-7
src/SchemaServiceProvider.php
···
46
46
);
47
47
});
48
48
49
+
// Register UnionResolver
50
+
$this->app->singleton(Services\UnionResolver::class);
51
+
52
+
// Register ExtensionManager
53
+
$this->app->singleton(Support\ExtensionManager::class);
54
+
55
+
// Register DefaultLexiconParser
56
+
$this->app->singleton(Parser\DefaultLexiconParser::class);
57
+
58
+
// Register InMemoryLexiconRegistry
59
+
$this->app->singleton(Parser\InMemoryLexiconRegistry::class);
60
+
61
+
// Register DnsLexiconResolver
62
+
$this->app->singleton(Parser\DnsLexiconResolver::class, function ($app) {
63
+
return new Parser\DnsLexiconResolver(
64
+
enabled: config('schema.dns_resolution.enabled', true),
65
+
httpTimeout: config('schema.http.timeout', 10),
66
+
parser: $app->make(Parser\DefaultLexiconParser::class)
67
+
);
68
+
});
69
+
70
+
// Register DefaultBlobHandler
71
+
$this->app->singleton(Support\DefaultBlobHandler::class, function ($app) {
72
+
return new Support\DefaultBlobHandler(
73
+
disk: config('schema.blobs.disk'),
74
+
path: config('schema.blobs.path', 'blobs')
75
+
);
76
+
});
77
+
78
+
// Bind BlobHandler contract to DefaultBlobHandler
79
+
$this->app->bind(Contracts\BlobHandler::class, Support\DefaultBlobHandler::class);
80
+
81
+
// Bind LexiconParser contract to DefaultLexiconParser
82
+
$this->app->bind(Contracts\LexiconParser::class, Parser\DefaultLexiconParser::class);
83
+
84
+
// Bind LexiconRegistry contract to InMemoryLexiconRegistry
85
+
$this->app->bind(Contracts\LexiconRegistry::class, Parser\InMemoryLexiconRegistry::class);
86
+
87
+
// Bind LexiconResolver contract to DnsLexiconResolver
88
+
$this->app->bind(Contracts\LexiconResolver::class, Parser\DnsLexiconResolver::class);
89
+
90
+
// Bind SchemaRepository contract to SchemaLoader
91
+
$this->app->bind(Contracts\SchemaRepository::class, Parser\SchemaLoader::class);
92
+
49
93
// Register DTOGenerator
50
94
$this->app->singleton(Generator\DTOGenerator::class, function ($app) {
51
95
return new Generator\DTOGenerator(
···
88
132
89
133
// Register AT Protocol validation rules
90
134
$validator->extend('nsid', function ($attribute, $value) {
91
-
$rule = new Validation\Rules\Nsid();
135
+
$rule = new Validation\Rules\Nsid;
92
136
$failed = false;
93
137
$rule->validate($attribute, $value, function () use (&$failed) {
94
138
$failed = true;
···
98
142
}, 'The :attribute is not a valid NSID.');
99
143
100
144
$validator->extend('did', function ($attribute, $value) {
101
-
$rule = new Validation\Rules\Did();
145
+
$rule = new Validation\Rules\Did;
102
146
$failed = false;
103
147
$rule->validate($attribute, $value, function () use (&$failed) {
104
148
$failed = true;
···
108
152
}, 'The :attribute is not a valid DID.');
109
153
110
154
$validator->extend('handle', function ($attribute, $value) {
111
-
$rule = new Validation\Rules\Handle();
155
+
$rule = new Validation\Rules\Handle;
112
156
$failed = false;
113
157
$rule->validate($attribute, $value, function () use (&$failed) {
114
158
$failed = true;
···
118
162
}, 'The :attribute is not a valid handle.');
119
163
120
164
$validator->extend('at_uri', function ($attribute, $value) {
121
-
$rule = new Validation\Rules\AtUri();
165
+
$rule = new Validation\Rules\AtUri;
122
166
$failed = false;
123
167
$rule->validate($attribute, $value, function () use (&$failed) {
124
168
$failed = true;
···
128
172
}, 'The :attribute is not a valid AT URI.');
129
173
130
174
$validator->extend('at_datetime', function ($attribute, $value) {
131
-
$rule = new Validation\Rules\AtDatetime();
175
+
$rule = new Validation\Rules\AtDatetime;
132
176
$failed = false;
133
177
$rule->validate($attribute, $value, function () use (&$failed) {
134
178
$failed = true;
···
138
182
}, 'The :attribute is not a valid AT Protocol datetime.');
139
183
140
184
$validator->extend('cid', function ($attribute, $value) {
141
-
$rule = new Validation\Rules\Cid();
185
+
$rule = new Validation\Rules\Cid;
142
186
$failed = false;
143
187
$rule->validate($attribute, $value, function () use (&$failed) {
144
188
$failed = true;
···
174
218
}, 'The :attribute must be at least :min_graphemes graphemes.');
175
219
176
220
$validator->extend('language', function ($attribute, $value) {
177
-
$rule = new Validation\Rules\Language();
221
+
$rule = new Validation\Rules\Language;
178
222
$failed = false;
179
223
$rule->validate($attribute, $value, function () use (&$failed) {
180
224
$failed = true;
+1
src/Services/BlobHandler.php
+1
src/Services/BlobHandler.php
+1
src/Services/ModelMapper.php
+1
src/Services/ModelMapper.php
+2
-2
src/Services/UnionResolver.php
+2
-2
src/Services/UnionResolver.php
+148
src/Support/DefaultBlobHandler.php
+148
src/Support/DefaultBlobHandler.php
···
1
+
<?php
2
+
3
+
namespace SocialDept\Schema\Support;
4
+
5
+
use Illuminate\Http\UploadedFile;
6
+
use Illuminate\Support\Facades\Storage;
7
+
use SocialDept\Schema\Contracts\BlobHandler;
8
+
use SocialDept\Schema\Data\BlobReference;
9
+
10
+
class DefaultBlobHandler implements BlobHandler
11
+
{
12
+
/**
13
+
* Storage disk to use.
14
+
*/
15
+
protected string $disk;
16
+
17
+
/**
18
+
* Storage path prefix.
19
+
*/
20
+
protected string $path;
21
+
22
+
/**
23
+
* Create a new DefaultBlobHandler.
24
+
*/
25
+
public function __construct(
26
+
?string $disk = null,
27
+
string $path = 'blobs'
28
+
) {
29
+
$this->disk = $disk ?? config('filesystems.default', 'local');
30
+
$this->path = $path;
31
+
}
32
+
33
+
/**
34
+
* Upload blob and create reference.
35
+
*/
36
+
public function upload(UploadedFile $file): BlobReference
37
+
{
38
+
$hash = hash_file('sha256', $file->getPathname());
39
+
$mimeType = $file->getMimeType() ?? 'application/octet-stream';
40
+
$size = $file->getSize();
41
+
42
+
// Store with hash as filename to enable deduplication
43
+
$storagePath = $this->path.'/'.$hash;
44
+
Storage::disk($this->disk)->put($storagePath, file_get_contents($file->getPathname()));
45
+
46
+
return new BlobReference(
47
+
cid: $hash, // Using hash as CID for simplicity
48
+
mimeType: $mimeType,
49
+
size: $size
50
+
);
51
+
}
52
+
53
+
/**
54
+
* Upload blob from path.
55
+
*/
56
+
public function uploadFromPath(string $path): BlobReference
57
+
{
58
+
$hash = hash_file('sha256', $path);
59
+
$mimeType = mime_content_type($path) ?: 'application/octet-stream';
60
+
$size = filesize($path);
61
+
62
+
// Store with hash as filename
63
+
$storagePath = $this->path.'/'.$hash;
64
+
Storage::disk($this->disk)->put($storagePath, file_get_contents($path));
65
+
66
+
return new BlobReference(
67
+
cid: $hash,
68
+
mimeType: $mimeType,
69
+
size: $size
70
+
);
71
+
}
72
+
73
+
/**
74
+
* Upload blob from content.
75
+
*/
76
+
public function uploadFromContent(string $content, string $mimeType): BlobReference
77
+
{
78
+
$hash = hash('sha256', $content);
79
+
$size = strlen($content);
80
+
81
+
// Store with hash as filename
82
+
$storagePath = $this->path.'/'.$hash;
83
+
Storage::disk($this->disk)->put($storagePath, $content);
84
+
85
+
return new BlobReference(
86
+
cid: $hash,
87
+
mimeType: $mimeType,
88
+
size: $size
89
+
);
90
+
}
91
+
92
+
/**
93
+
* Download blob content.
94
+
*/
95
+
public function download(BlobReference $blob): string
96
+
{
97
+
$storagePath = $this->path.'/'.$blob->cid;
98
+
99
+
if (! Storage::disk($this->disk)->exists($storagePath)) {
100
+
throw new \RuntimeException("Blob not found: {$blob->cid}");
101
+
}
102
+
103
+
return Storage::disk($this->disk)->get($storagePath);
104
+
}
105
+
106
+
/**
107
+
* Generate signed URL for blob.
108
+
*/
109
+
public function url(BlobReference $blob): string
110
+
{
111
+
$storagePath = $this->path.'/'.$blob->cid;
112
+
113
+
// Try to generate a temporary URL if the disk supports it
114
+
try {
115
+
return Storage::disk($this->disk)->temporaryUrl(
116
+
$storagePath,
117
+
now()->addHour()
118
+
);
119
+
} catch (\RuntimeException $e) {
120
+
// Fallback to regular URL for disks that don't support temporary URLs
121
+
return Storage::disk($this->disk)->url($storagePath);
122
+
}
123
+
}
124
+
125
+
/**
126
+
* Check if blob exists in storage.
127
+
*/
128
+
public function exists(BlobReference $blob): bool
129
+
{
130
+
$storagePath = $this->path.'/'.$blob->cid;
131
+
132
+
return Storage::disk($this->disk)->exists($storagePath);
133
+
}
134
+
135
+
/**
136
+
* Delete blob from storage.
137
+
*/
138
+
public function delete(BlobReference $blob): bool
139
+
{
140
+
$storagePath = $this->path.'/'.$blob->cid;
141
+
142
+
if (! Storage::disk($this->disk)->exists($storagePath)) {
143
+
return false;
144
+
}
145
+
146
+
return Storage::disk($this->disk)->delete($storagePath);
147
+
}
148
+
}
+100
src/Support/UnionHelper.php
+100
src/Support/UnionHelper.php
···
1
+
<?php
2
+
3
+
namespace SocialDept\Schema\Support;
4
+
5
+
use InvalidArgumentException;
6
+
use SocialDept\Schema\Contracts\DiscriminatedUnion;
7
+
use SocialDept\Schema\Data\Data;
8
+
9
+
/**
10
+
* Helper for resolving discriminated unions based on $type field.
11
+
*
12
+
* This class uses the DiscriminatedUnion interface to build type maps
13
+
* and resolve union data to the correct variant class.
14
+
*/
15
+
class UnionHelper
16
+
{
17
+
/**
18
+
* Resolve a closed union to the correct variant class.
19
+
*
20
+
* @param array $data The union data containing a $type field
21
+
* @param array<class-string<Data>> $variants Array of possible variant class names
22
+
* @return Data The resolved variant instance
23
+
*
24
+
* @throws InvalidArgumentException If $type is missing or unknown
25
+
*/
26
+
public static function resolveClosedUnion(array $data, array $variants): Data
27
+
{
28
+
// Validate $type field exists
29
+
if (! isset($data['$type'])) {
30
+
throw new InvalidArgumentException(
31
+
'Closed union data must contain a $type field for discrimination'
32
+
);
33
+
}
34
+
35
+
$type = $data['$type'];
36
+
37
+
// Build type map using DiscriminatedUnion interface
38
+
$typeMap = static::buildTypeMap($variants);
39
+
40
+
// Check if type is known
41
+
if (! isset($typeMap[$type])) {
42
+
$knownTypes = implode(', ', array_keys($typeMap));
43
+
throw new InvalidArgumentException(
44
+
"Unknown union type '{$type}'. Expected one of: {$knownTypes}"
45
+
);
46
+
}
47
+
48
+
// Resolve to correct variant class
49
+
$class = $typeMap[$type];
50
+
51
+
return $class::fromArray($data);
52
+
}
53
+
54
+
/**
55
+
* Validate an open union has $type field.
56
+
*
57
+
* Open unions pass data through as-is but must have $type for future discrimination.
58
+
*
59
+
* @param array $data The union data
60
+
* @return array The validated union data
61
+
*
62
+
* @throws InvalidArgumentException If $type is missing
63
+
*/
64
+
public static function validateOpenUnion(array $data): array
65
+
{
66
+
if (! isset($data['$type'])) {
67
+
throw new InvalidArgumentException(
68
+
'Open union data must contain a $type field for future discrimination'
69
+
);
70
+
}
71
+
72
+
return $data;
73
+
}
74
+
75
+
/**
76
+
* Build a type map from variant classes using DiscriminatedUnion interface.
77
+
*
78
+
* @param array<class-string<Data>> $variants Array of variant class names
79
+
* @return array<string, class-string<Data>> Map of discriminator => class name
80
+
*/
81
+
protected static function buildTypeMap(array $variants): array
82
+
{
83
+
$typeMap = [];
84
+
85
+
foreach ($variants as $class) {
86
+
// Ensure class implements DiscriminatedUnion
87
+
if (! is_subclass_of($class, DiscriminatedUnion::class)) {
88
+
throw new InvalidArgumentException(
89
+
"Variant class {$class} must implement DiscriminatedUnion interface"
90
+
);
91
+
}
92
+
93
+
// Get discriminator from the class
94
+
$discriminator = $class::getDiscriminator();
95
+
$typeMap[$discriminator] = $class;
96
+
}
97
+
98
+
return $typeMap;
99
+
}
100
+
}
+1
-2
src/Validation/LexiconValidator.php
+1
-2
src/Validation/LexiconValidator.php
···
115
115
*/
116
116
public function validateByNsid(string $nsid, array $record): void
117
117
{
118
-
$schema = $this->schemaLoader->load($nsid);
119
-
$document = LexiconDocument::fromArray($schema);
118
+
$document = $this->schemaLoader->load($nsid);
120
119
121
120
$this->validateRecord($document, $record);
122
121
}
+2
-2
src/Validation/Rules/AtUri.php
+2
-2
src/Validation/Rules/AtUri.php
+5
-5
src/Validation/TypeValidators/ArrayValidator.php
+5
-5
src/Validation/TypeValidators/ArrayValidator.php
···
75
75
$type = $definition['type'] ?? null;
76
76
77
77
$validator = match ($type) {
78
-
'string' => new StringValidator(),
79
-
'integer' => new IntegerValidator(),
80
-
'boolean' => new BooleanValidator(),
81
-
'object' => new ObjectValidator(),
82
-
'array' => new ArrayValidator(),
78
+
'string' => new StringValidator,
79
+
'integer' => new IntegerValidator,
80
+
'boolean' => new BooleanValidator,
81
+
'object' => new ObjectValidator,
82
+
'array' => new ArrayValidator,
83
83
default => null,
84
84
};
85
85
+5
-5
src/Validation/TypeValidators/ObjectValidator.php
+5
-5
src/Validation/TypeValidators/ObjectValidator.php
···
48
48
$type = $definition['type'] ?? null;
49
49
50
50
$validator = match ($type) {
51
-
'string' => new StringValidator(),
52
-
'integer' => new IntegerValidator(),
53
-
'boolean' => new BooleanValidator(),
54
-
'object' => new ObjectValidator(),
55
-
'array' => new ArrayValidator(),
51
+
'string' => new StringValidator,
52
+
'integer' => new IntegerValidator,
53
+
'boolean' => new BooleanValidator,
54
+
'object' => new ObjectValidator,
55
+
'array' => new ArrayValidator,
56
56
default => null,
57
57
};
58
58
+19
-21
src/Validation/TypeValidators/UnionValidator.php
+19
-21
src/Validation/TypeValidators/UnionValidator.php
···
3
3
namespace SocialDept\Schema\Validation\TypeValidators;
4
4
5
5
use SocialDept\Schema\Exceptions\RecordValidationException;
6
+
use SocialDept\Schema\Services\UnionResolver;
6
7
7
8
class UnionValidator
8
9
{
10
+
/**
11
+
* Create a new UnionValidator.
12
+
*/
13
+
public function __construct(
14
+
protected ?UnionResolver $resolver = null
15
+
) {
16
+
$this->resolver = $resolver ?? new UnionResolver;
17
+
}
18
+
9
19
/**
10
20
* Validate a union value against constraints.
11
21
*
···
23
33
$closed = $definition['closed'] ?? false;
24
34
25
35
if ($closed) {
26
-
$this->validateDiscriminatedUnion($value, $refs, $path);
36
+
$this->validateDiscriminatedUnion($value, $refs, $path, $definition);
27
37
} else {
28
38
$this->validateOpenUnion($value, $refs, $path);
29
39
}
···
33
43
* Validate discriminated (closed) union.
34
44
*
35
45
* @param array<string> $refs
46
+
* @param array<string, mixed> $definition
36
47
*/
37
-
protected function validateDiscriminatedUnion(mixed $value, array $refs, string $path): void
48
+
protected function validateDiscriminatedUnion(mixed $value, array $refs, string $path, array $definition): void
38
49
{
39
-
if (! is_array($value)) {
40
-
throw RecordValidationException::invalidType($path, 'object', gettype($value));
41
-
}
42
-
43
-
// Check for $type discriminator
44
-
if (! isset($value['$type'])) {
45
-
throw RecordValidationException::invalidValue(
46
-
$path,
47
-
'Discriminated union must have $type field'
48
-
);
49
-
}
50
-
51
-
$type = $value['$type'];
52
-
53
-
// Validate that $type is one of the allowed refs
54
-
if (! in_array($type, $refs, true)) {
55
-
$allowed = implode(', ', $refs);
56
-
50
+
// Delegate validation to UnionResolver which handles all the logic
51
+
try {
52
+
$this->resolver->resolve($value, $definition);
53
+
} catch (RecordValidationException $e) {
54
+
// Re-throw with path context
57
55
throw RecordValidationException::invalidValue(
58
56
$path,
59
-
"Union type '{$type}' not allowed. Must be one of: {$allowed}"
57
+
$e->getMessage()
60
58
);
61
59
}
62
60
}
+2
-1
src/Validation/Validator.php
+2
-1
src/Validation/Validator.php
···
13
13
class Validator implements LexiconValidatorContract
14
14
{
15
15
use Macroable;
16
+
16
17
/**
17
18
* Validation mode constants.
18
19
*/
···
165
166
if ($type !== 'record' && $type !== 'object') {
166
167
throw SchemaValidationException::invalidStructure(
167
168
$schema->getNsid(),
168
-
['Schema must be a record or object type, got: ' . ($type ?? 'unknown')]
169
+
['Schema must be a record or object type, got: '.($type ?? 'unknown')]
169
170
);
170
171
}
171
172
+21
-12
src/helpers.php
+21
-12
src/helpers.php
···
7
7
/**
8
8
* Get the SchemaManager instance or load a schema.
9
9
*
10
-
* @param string|null $nsid
11
-
* @return \SocialDept\Schema\SchemaManager|array
10
+
* @return \SocialDept\Schema\SchemaManager|LexiconDocument
12
11
*/
13
12
function schema(?string $nsid = null)
14
13
{
···
20
19
}
21
20
}
22
21
23
-
if (! function_exists('schema_validate')) {
22
+
if (! function_exists('schema_find')) {
23
+
/**
24
+
* Find a schema by NSID (nullable version).
25
+
*/
26
+
function schema_find(string $nsid): ?LexiconDocument
27
+
{
28
+
return Schema::find($nsid);
29
+
}
30
+
}
31
+
32
+
if (! function_exists('schema_exists')) {
24
33
/**
25
-
* Validate data against a schema.
34
+
* Check if a schema exists.
26
35
*/
27
-
function schema_validate(string $nsid, array $data): bool
36
+
function schema_exists(string $nsid): bool
28
37
{
29
-
return Schema::validate($nsid, $data);
38
+
return Schema::exists($nsid);
30
39
}
31
40
}
32
41
33
-
if (! function_exists('schema_parse')) {
42
+
if (! function_exists('schema_validate')) {
34
43
/**
35
-
* Parse a schema into a LexiconDocument.
44
+
* Validate data against a schema.
36
45
*/
37
-
function schema_parse(string $nsid): LexiconDocument
46
+
function schema_validate(string $nsid, array $data): bool
38
47
{
39
-
return Schema::parse($nsid);
48
+
return Schema::validate($nsid, $data);
40
49
}
41
50
}
42
51
···
44
53
/**
45
54
* Generate DTO code from a schema.
46
55
*/
47
-
function schema_generate(string $nsid, array $options = []): string
56
+
function schema_generate(string $nsid, ?string $outputPath = null): string
48
57
{
49
-
return Schema::generate($nsid, $options);
58
+
return Schema::generate($nsid, $outputPath);
50
59
}
51
60
}