Skip to content

Commit 4bbec1b

Browse files
committed
feat: add ability to resolve references
1 parent 8362537 commit 4bbec1b

24 files changed

+475
-25
lines changed

src/Parser/LexRefParser.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ public function parse(object | string $data): LexRef
2525
return new LexRef(
2626
description: $data->description ?? null,
2727
ref: $data->ref,
28+
parserFactory: $this->getParserFactory(),
2829
);
2930
}
3031

src/Parser/LexRefUnionParser.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ public function parse(object | string $data): LexRefUnion
2828
description: $data->description ?? null,
2929
refs: $data->refs,
3030
closed: $data->closed ?? null,
31+
parserFactory: $this->getParserFactory(),
3132
);
3233
}
3334

src/Parser/LexStringParser.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ public function parse(object | string $data): LexString
3939
enum: $data->enum ?? null,
4040
const: $data->const ?? null,
4141
knownValues: $data->knownValues ?? null,
42+
parserFactory: $this->getParserFactory(),
4243
);
4344
}
4445

src/Types/LexRef.php

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,17 @@
55
namespace SocialWeb\Atproto\Lexicon\Types;
66

77
use JsonSerializable;
8+
use SocialWeb\Atproto\Lexicon\Nsid\InvalidNsid;
9+
use SocialWeb\Atproto\Lexicon\Nsid\Nsid;
10+
use SocialWeb\Atproto\Lexicon\Parser\LexiconParser;
11+
use SocialWeb\Atproto\Lexicon\Parser\ParserFactory;
12+
13+
use function file_get_contents;
14+
use function is_array;
15+
use function json_encode;
16+
use function sprintf;
17+
18+
use const JSON_UNESCAPED_SLASHES;
819

920
/**
1021
* @phpstan-type TLexRef = object{
@@ -22,7 +33,54 @@ class LexRef implements JsonSerializable, LexEntity
2233
public function __construct(
2334
public readonly ?string $description = null,
2435
public readonly ?string $ref = null,
36+
private readonly ?ParserFactory $parserFactory = null,
2537
) {
2638
$this->type = LexType::Ref;
2739
}
40+
41+
public function resolve(): LexEntity
42+
{
43+
if ($this->parserFactory === null) {
44+
throw new ParserFactoryRequired(
45+
'You must provide a ParserFactory to the constructor to resolve references',
46+
);
47+
}
48+
49+
if ($this->ref === null) {
50+
throw new UnableToResolveReferences(
51+
'Unable to resolve LexRef without a ref: ' . json_encode($this, JSON_UNESCAPED_SLASHES),
52+
);
53+
}
54+
55+
try {
56+
$nsid = new Nsid($this->ref);
57+
} catch (InvalidNsid $exception) {
58+
throw new UnableToResolveReferences(
59+
message: 'Unable to resolve reference for invalid NSID: ' . $this->ref,
60+
previous: $exception,
61+
);
62+
}
63+
64+
$schemaFile = $this->parserFactory->getSchemaRepository()->findSchemaPathByNsid($nsid);
65+
66+
if ($schemaFile === null) {
67+
throw new UnableToResolveReferences('Unable to locate schema file for ref: ' . $this->ref);
68+
}
69+
70+
$schemaContents = (string) file_get_contents($schemaFile);
71+
72+
$entity = $this->parserFactory->getParser(LexiconParser::class)->parse($schemaContents);
73+
74+
/** @psalm-suppress NoInterfaceProperties */
75+
if (!isset($entity->defs) || !is_array($entity->defs) || !isset($entity->defs[$nsid->defId])) {
76+
throw new UnableToResolveReferences(sprintf(
77+
'Def ID "#%s" does not exist in schema for NSID "%s"',
78+
$nsid->defId,
79+
$nsid->nsid,
80+
));
81+
}
82+
83+
/** @var LexEntity */
84+
return $entity->defs[$nsid->defId];
85+
}
2886
}

src/Types/LexRefUnion.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace SocialWeb\Atproto\Lexicon\Types;
66

77
use JsonSerializable;
8+
use SocialWeb\Atproto\Lexicon\Parser\ParserFactory;
89

910
/**
1011
* @phpstan-type TLexRefUnion = object{
@@ -27,7 +28,25 @@ public function __construct(
2728
public readonly ?string $description = null,
2829
public readonly array $refs = [],
2930
public readonly ?bool $closed = null,
31+
private readonly ?ParserFactory $parserFactory = null,
3032
) {
3133
$this->type = LexType::Union;
3234
}
35+
36+
/**
37+
* Converts the string refs of the union to {@see LexRef} instances, which
38+
* may then be resolved for further processing.
39+
*
40+
* @return list<LexRef>
41+
*/
42+
public function getLexRefs(): array
43+
{
44+
$lexRefs = [];
45+
46+
foreach ($this->refs as $ref) {
47+
$lexRefs[] = new LexRef($this->description, $ref, $this->parserFactory);
48+
}
49+
50+
return $lexRefs;
51+
}
3352
}

src/Types/LexString.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace SocialWeb\Atproto\Lexicon\Types;
66

77
use JsonSerializable;
8+
use SocialWeb\Atproto\Lexicon\Parser\ParserFactory;
89

910
/**
1011
* @phpstan-import-type TLexStringFormat from LexStringFormat
@@ -43,7 +44,25 @@ public function __construct(
4344
public readonly ?array $enum = null,
4445
public readonly ?string $const = null,
4546
public readonly ?array $knownValues = null,
47+
private readonly ?ParserFactory $parserFactory = null,
4648
) {
4749
$this->type = LexType::String;
4850
}
51+
52+
/**
53+
* Converts the string refs of the known values to {@see LexRef} instances,
54+
* which may then be resolved for further processing.
55+
*
56+
* @return list<LexRef>
57+
*/
58+
public function getLexRefsForKnownValues(): array
59+
{
60+
$lexRefs = [];
61+
62+
foreach ($this->knownValues ?? [] as $ref) {
63+
$lexRefs[] = new LexRef($this->description, $ref, $this->parserFactory);
64+
}
65+
66+
return $lexRefs;
67+
}
4968
}

src/Types/ParserFactoryRequired.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace SocialWeb\Atproto\Lexicon\Types;
6+
7+
use LogicException;
8+
use SocialWeb\Atproto\Lexicon\LexiconException;
9+
10+
class ParserFactoryRequired extends LogicException implements LexiconException
11+
{
12+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace SocialWeb\Atproto\Lexicon\Types;
6+
7+
use RuntimeException;
8+
use SocialWeb\Atproto\Lexicon\LexiconException;
9+
10+
class UnableToResolveReferences extends RuntimeException implements LexiconException
11+
{
12+
}

tests/Parser/LexArrayParserTest.php

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
use SocialWeb\Atproto\Lexicon\Types\LexType;
2121
use SocialWeb\Atproto\Lexicon\Types\LexUnknown;
2222

23+
use function json_encode;
24+
2325
class LexArrayParserTest extends ParserTestCase
2426
{
2527
public function getParserClassName(): string
@@ -48,8 +50,13 @@ public function testParsesValidValues(
4850
$this->assertSame($checkValues['maxLength'] ?? null, $parsed->maxLength);
4951
$this->assertSame($checkValues['description'] ?? null, $parsed->description);
5052

51-
// We use assertEquals() here, since we can't assert sameness on the object.
52-
$this->assertEquals($checkValues['items'] ?? null, $parsed->items);
53+
// Compare as JSON strings to avoid problems where the LexRef or LexUnion
54+
// objects in the parsed values fail equality checks due to the parser
55+
// factory instances they contain in private properties.
56+
$this->assertJsonStringEqualsJsonString(
57+
(string) json_encode($checkValues['items'] ?? null),
58+
(string) json_encode($parsed->items),
59+
);
5360

5461
if ($isPrimitiveArray) {
5562
$this->assertInstanceOf(LexPrimitiveArray::class, $parsed);

tests/Parser/LexObjectParserTest.php

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
use SocialWeb\Atproto\Lexicon\Types\LexType;
2121
use SocialWeb\Atproto\Lexicon\Types\LexUnknown;
2222

23+
use function json_encode;
24+
2325
class LexObjectParserTest extends ParserTestCase
2426
{
2527
public function getParserClassName(): string
@@ -45,8 +47,13 @@ public function testParsesValidValues(object | string $value, array $checkValues
4547
$this->assertSame($checkValues['required'] ?? null, $parsed->required);
4648
$this->assertSame($checkValues['nullable'] ?? null, $parsed->nullable);
4749

48-
// We use assertEquals() here, since we can't assert sameness on the objects.
49-
$this->assertEquals($checkValues['properties'] ?? [], $parsed->properties);
50+
// Compare as JSON strings to avoid problems where the LexRef or LexUnion
51+
// objects in the parsed values fail equality checks due to the parser
52+
// factory instances they contain in private properties.
53+
$this->assertJsonStringEqualsJsonString(
54+
(string) json_encode($checkValues['properties'] ?? []),
55+
(string) json_encode($parsed->properties),
56+
);
5057
}
5158

5259
/**

tests/Parser/LexRefParserTest.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
use PHPUnit\Framework\Attributes\DataProvider;
88
use SocialWeb\Atproto\Lexicon\Parser\LexRefParser;
9+
use SocialWeb\Atproto\Lexicon\Parser\ParserFactory;
910
use SocialWeb\Atproto\Lexicon\Types\LexRef;
1011

1112
class LexRefParserTest extends ParserTestCase
@@ -21,7 +22,10 @@ public function getParserClassName(): string
2122
#[DataProvider('validValuesProvider')]
2223
public function testParsesValidValues(object | string $value, array $checkValues): void
2324
{
25+
$parserFactory = $this->mockery(ParserFactory::class);
26+
2427
$parser = new LexRefParser();
28+
$parser->setParserFactory($parserFactory);
2529
$parsed = $parser->parse($value);
2630

2731
$this->assertInstanceOf(LexRef::class, $parsed);

tests/Parser/LexRefUnionParserTest.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
use PHPUnit\Framework\Attributes\DataProvider;
88
use SocialWeb\Atproto\Lexicon\Parser\LexRefUnionParser;
9+
use SocialWeb\Atproto\Lexicon\Parser\ParserFactory;
910
use SocialWeb\Atproto\Lexicon\Types\LexRefUnion;
1011

1112
class LexRefUnionParserTest extends ParserTestCase
@@ -21,7 +22,10 @@ public function getParserClassName(): string
2122
#[DataProvider('validValuesProvider')]
2223
public function testParsesValidValues(object | string $value, array $checkValues): void
2324
{
25+
$parserFactory = $this->mockery(ParserFactory::class);
26+
2427
$parser = new LexRefUnionParser();
28+
$parser->setParserFactory($parserFactory);
2529
$parsed = $parser->parse($value);
2630

2731
$this->assertInstanceOf(LexRefUnion::class, $parsed);

tests/Parser/LexStringParserTest.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
use PHPUnit\Framework\Attributes\DataProvider;
88
use SocialWeb\Atproto\Lexicon\Parser\LexStringParser;
9+
use SocialWeb\Atproto\Lexicon\Parser\ParserFactory;
910
use SocialWeb\Atproto\Lexicon\Types\LexString;
1011
use SocialWeb\Atproto\Lexicon\Types\LexStringFormat;
1112
use SocialWeb\Atproto\Lexicon\Types\LexType;
@@ -23,7 +24,10 @@ public function getParserClassName(): string
2324
#[DataProvider('validValuesProvider')]
2425
public function testParsesValidValues(object | string $value, array $checkValues): void
2526
{
27+
$parserFactory = $this->mockery(ParserFactory::class);
28+
2629
$parser = new LexStringParser();
30+
$parser->setParserFactory($parserFactory);
2731
$parsed = $parser->parse($value);
2832

2933
$this->assertInstanceOf(LexString::class, $parsed);

tests/Parser/LexXrpcBodyParserTest.php

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
use SocialWeb\Atproto\Lexicon\Types\LexRef;
1414
use SocialWeb\Atproto\Lexicon\Types\LexXrpcBody;
1515

16+
use function json_encode;
17+
1618
class LexXrpcBodyParserTest extends ParserTestCase
1719
{
1820
public function getParserClassName(): string
@@ -36,8 +38,13 @@ public function testParsesValidValues(object | string $value, array $checkValues
3638
$this->assertSame($checkValues['encoding'], $parsed->encoding);
3739
$this->assertSame($checkValues['description'] ?? null, $parsed->description);
3840

39-
// We use assertEquals() here, since we can't assert sameness on the object.
40-
$this->assertEquals($checkValues['schema'], $parsed->schema);
41+
// Compare as JSON strings to avoid problems where the LexRef or LexUnion
42+
// objects in the parsed values fail equality checks due to the parser
43+
// factory instances they contain in private properties.
44+
$this->assertJsonStringEqualsJsonString(
45+
(string) json_encode($checkValues['schema']),
46+
(string) json_encode($parsed->schema),
47+
);
4148
}
4249

4350
/**

tests/Parser/LexXrpcParametersParserTest.php

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
use SocialWeb\Atproto\Lexicon\Types\LexUnknown;
1818
use SocialWeb\Atproto\Lexicon\Types\LexXrpcParameters;
1919

20+
use function json_encode;
21+
2022
class LexXrpcParametersParserTest extends ParserTestCase
2123
{
2224
public function getParserClassName(): string
@@ -41,8 +43,13 @@ public function testParsesValidValues(object | string $value, array $checkValues
4143
$this->assertSame($checkValues['description'] ?? null, $parsed->description);
4244
$this->assertSame($checkValues['required'] ?? null, $parsed->required);
4345

44-
// We use assertEquals() here, since we can't assert sameness on the objects.
45-
$this->assertEquals($checkValues['properties'] ?? [], $parsed->properties);
46+
// Compare as JSON strings to avoid problems where the LexRef or LexUnion
47+
// objects in the parsed values fail equality checks due to the parser
48+
// factory instances they contain in private properties.
49+
$this->assertJsonStringEqualsJsonString(
50+
(string) json_encode($checkValues['properties'] ?? []),
51+
(string) json_encode($parsed->properties),
52+
);
4653
}
4754

4855
/**

tests/Parser/LexXrpcProcedureParserTest.php

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
use SocialWeb\Atproto\Lexicon\Types\LexXrpcParameters;
1919
use SocialWeb\Atproto\Lexicon\Types\LexXrpcProcedure;
2020

21+
use function json_encode;
22+
2123
class LexXrpcProcedureParserTest extends ParserTestCase
2224
{
2325
public function getParserClassName(): string
@@ -41,11 +43,25 @@ public function testParsesValidValues(object | string $value, array $checkValues
4143
$this->assertSame(LexType::Procedure, $parsed->type);
4244
$this->assertSame($checkValues['description'] ?? null, $parsed->description);
4345

44-
// We use assertEquals() here, since we can't assert sameness on the objects.
45-
$this->assertEquals($checkValues['parameters'] ?? null, $parsed->parameters);
46-
$this->assertEquals($checkValues['errors'] ?? null, $parsed->errors);
47-
$this->assertEquals($checkValues['input'] ?? null, $parsed->input);
48-
$this->assertEquals($checkValues['output'] ?? null, $parsed->output);
46+
// Compare as JSON strings to avoid problems where the LexRef or LexUnion
47+
// objects in the parsed values fail equality checks due to the parser
48+
// factory instances they contain in private properties.
49+
$this->assertJsonStringEqualsJsonString(
50+
(string) json_encode($checkValues['parameters'] ?? null),
51+
(string) json_encode($parsed->parameters),
52+
);
53+
$this->assertJsonStringEqualsJsonString(
54+
(string) json_encode($checkValues['errors'] ?? null),
55+
(string) json_encode($parsed->errors),
56+
);
57+
$this->assertJsonStringEqualsJsonString(
58+
(string) json_encode($checkValues['input'] ?? null),
59+
(string) json_encode($parsed->input),
60+
);
61+
$this->assertJsonStringEqualsJsonString(
62+
(string) json_encode($checkValues['output'] ?? null),
63+
(string) json_encode($parsed->output),
64+
);
4965
}
5066

5167
/**

0 commit comments

Comments
 (0)