Skip to content

Commit 2f464e0

Browse files
author
Enno Woortmann
committed
Improve the resolving of references for correct usage of URLs in $id and absolute paths (compare #94)
1 parent 0fc7d6b commit 2f464e0

File tree

8 files changed

+137
-25
lines changed

8 files changed

+137
-25
lines changed

src/Model/Schema.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ public function __construct(
6565
protected bool $initialClass = false,
6666
) {
6767
$this->jsonSchema = $schema;
68-
$this->schemaDefinitionDictionary = $dictionary ?? new SchemaDefinitionDictionary('');
68+
$this->schemaDefinitionDictionary = $dictionary ?? new SchemaDefinitionDictionary();
6969
$this->description = $schema->getJson()['description'] ?? '';
7070

7171
$this->addInterface(JSONModelInterface::class);

src/Model/SchemaDefinition/SchemaDefinitionDictionary.php

Lines changed: 99 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ class SchemaDefinitionDictionary extends ArrayObject
2222
/**
2323
* SchemaDefinitionDictionary constructor.
2424
*/
25-
public function __construct(private string $sourceDirectory)
25+
public function __construct(private ?JsonSchema $schema = null)
2626
{
2727
parent::__construct();
2828
}
@@ -129,18 +129,16 @@ public function getDefinition(string $key, SchemaProcessor $schemaProcessor, arr
129129
/**
130130
* @throws SchemaException
131131
*/
132-
protected function parseExternalFile(
132+
private function parseExternalFile(
133133
string $jsonSchemaFile,
134134
string $externalKey,
135135
SchemaProcessor $schemaProcessor,
136136
array &$path,
137137
): ?SchemaDefinition {
138-
$jsonSchemaFilePath = filter_var($jsonSchemaFile, FILTER_VALIDATE_URL)
139-
? $jsonSchemaFile
140-
: $this->sourceDirectory . '/' . $jsonSchemaFile;
138+
$jsonSchemaFilePath = $this->getFullRefURL($jsonSchemaFile) ?: $this->getLocalRefPath($jsonSchemaFile);
141139

142-
if (!filter_var($jsonSchemaFilePath, FILTER_VALIDATE_URL) && !is_file($jsonSchemaFilePath)) {
143-
throw new SchemaException("Reference to non existing JSON-Schema file $jsonSchemaFilePath");
140+
if ($jsonSchemaFilePath === null) {
141+
throw new SchemaException("Reference to non existing JSON-Schema file $jsonSchemaFile");
144142
}
145143

146144
$jsonSchema = file_get_contents($jsonSchemaFilePath);
@@ -154,13 +152,105 @@ protected function parseExternalFile(
154152
'',
155153
$schemaProcessor->getCurrentClassPath(),
156154
'ExternalSchema',
157-
new JsonSchema($jsonSchemaFilePath, $decodedJsonSchema),
158-
new self(dirname($jsonSchemaFilePath)),
155+
$externalSchema = new JsonSchema($jsonSchemaFilePath, $decodedJsonSchema),
156+
new self($externalSchema),
159157
);
160158

161159
$schema->getSchemaDictionary()->setUpDefinitionDictionary($schemaProcessor, $schema);
162160
$this->parsedExternalFileSchemas[$jsonSchemaFile] = $schema;
163161

164162
return $schema->getSchemaDictionary()->getDefinition($externalKey, $schemaProcessor, $path);
165163
}
164+
165+
/**
166+
* Try to build a full URL to fetch the schema from utilizing the $id field of the schema
167+
*/
168+
private function getFullRefURL(string $jsonSchemaFile): ?string
169+
{
170+
if (filter_var($jsonSchemaFile, FILTER_VALIDATE_URL)) {
171+
return $jsonSchemaFile;
172+
}
173+
174+
if ($this->schema === null
175+
|| !filter_var($this->schema->getJson()['$id'] ?? $this->schema->getFile(), FILTER_VALIDATE_URL)
176+
|| ($idURL = parse_url($this->schema->getJson()['$id'] ?? $this->schema->getFile())) === false
177+
) {
178+
return null;
179+
}
180+
181+
$baseURL = $idURL['scheme'] . '://' . $idURL['host'] . (isset($idURL['port']) ? ':' . $idURL['port'] : '');
182+
183+
// root relative $ref
184+
if (str_starts_with($jsonSchemaFile, '/')) {
185+
return $baseURL . $jsonSchemaFile;
186+
}
187+
188+
// relative $ref against the path of $id
189+
$segments = explode('/', rtrim(dirname($idURL['path'] ?? '/'), '/') . '/' . $jsonSchemaFile);
190+
$output = [];
191+
192+
foreach ($segments as $seg) {
193+
if ($seg === '' || $seg === '.') {
194+
continue;
195+
}
196+
if ($seg === '..') {
197+
array_pop($output);
198+
continue;
199+
}
200+
$output[] = $seg;
201+
}
202+
203+
return $baseURL . '/' . implode('/', $output);
204+
}
205+
206+
private function getLocalRefPath(string $jsonSchemaFile): ?string
207+
{
208+
$currentDir = dirname($this->schema->getFile());
209+
// windows compatibility
210+
$jsonSchemaFile = str_replace('\\', '/', $jsonSchemaFile);
211+
212+
// relative paths to the current location
213+
if (!str_starts_with($jsonSchemaFile, '/')) {
214+
$candidate = $this->normalizePath($currentDir . '/' . $jsonSchemaFile);
215+
return file_exists($candidate) ? $candidate : null;
216+
}
217+
218+
// absolute paths: traverse up to find the context root directory
219+
$relative = ltrim($jsonSchemaFile, '/');
220+
221+
$dir = $currentDir;
222+
while (true) {
223+
$candidate = $this->normalizePath($dir . '/' . $relative);
224+
if (file_exists($candidate)) {
225+
return $candidate;
226+
}
227+
228+
$parent = dirname($dir);
229+
if ($parent === $dir) {
230+
break;
231+
}
232+
$dir = $parent;
233+
}
234+
235+
return null;
236+
}
237+
238+
private function normalizePath(string $path): string
239+
{
240+
$segments = explode('/', str_replace('\\', '/', $path));
241+
$output = [];
242+
243+
foreach ($segments as $seg) {
244+
if ($seg === '' || $seg === '.') {
245+
continue;
246+
}
247+
if ($seg === '..') {
248+
array_pop($output);
249+
continue;
250+
}
251+
$output[] = $seg;
252+
}
253+
254+
return str_replace('/', DIRECTORY_SEPARATOR, implode('/', $output));
255+
}
166256
}

src/SchemaProcessor/PostProcessor/EnumPostProcessor.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,9 @@ public function process(Schema $schema, GeneratorConfiguration $generatorConfigu
7575
$this->checkForExistingTransformingFilter($property);
7676

7777
$values = $json['enum'];
78-
$enumSignature = ArrayHash::hash($json, ['enum', 'enum-map', '$id']);
79-
$enumName = $json['$id'] ?? $schema->getClassName() . ucfirst($property->getName());
78+
$enumSignature = ArrayHash::hash($json, ['enum', 'enum-map', 'title', '$id']);
79+
$enumName = $json['title']
80+
?? basename($json['$id'] ?? $schema->getClassName() . ucfirst($property->getName()));
8081

8182
if (!isset($this->generatedEnums[$enumSignature])) {
8283
$this->generatedEnums[$enumSignature] = [

src/SchemaProcessor/SchemaProcessor.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ public function process(JsonSchema $jsonSchema): void
6868
$jsonSchema,
6969
$this->currentClassPath,
7070
$this->currentClassName,
71-
new SchemaDefinitionDictionary(dirname($jsonSchema->getFile())),
71+
new SchemaDefinitionDictionary($jsonSchema),
7272
true,
7373
);
7474
}

src/Utils/ClassNameGenerator.php

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,11 @@ public function getClassName(
2929
$className = sprintf(
3030
$isMergeClass ? '%s_Merged_%s' : '%s_%s',
3131
$currentClassName,
32-
ucfirst(
33-
isset($json['$id'])
34-
? str_replace('#', '', $json['$id'])
35-
: ($propertyName . ($currentClassName ? uniqid() : '')),
36-
)
32+
ucfirst(match(true) {
33+
isset($json['title']) => $json['title'],
34+
isset($json['$id']) => basename($json['$id']),
35+
default => ($propertyName . ($currentClassName ? uniqid() : '')),
36+
}),
3737
);
3838

3939
return ucfirst(preg_replace('/\W/', '', trim($className, '_')));

tests/AbstractPHPModelGeneratorTestCase.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ protected function generateClass(
204204
$className = $this->getClassName();
205205

206206
if (!$originalClassNames) {
207-
// extend the class name generator to attach a uniqid as multiple test executions use identical $id
207+
// extend the class name generator to attach a uniqid as multiple test executions use identical title
208208
// properties which would lead to name collisions
209209
$generatorConfiguration->setClassNameGenerator(new class extends ClassNameGenerator {
210210
public function getClassName(
@@ -221,12 +221,12 @@ public function getClassName(
221221
// generate an object ID for valid JSON schema files to avoid class name collisions in the testing process
222222
$jsonSchemaArray = json_decode($jsonSchema, true);
223223
if ($jsonSchemaArray) {
224-
$jsonSchemaArray['$id'] = $className;
224+
$jsonSchemaArray['title'] = $className;
225225

226226
if (isset($jsonSchemaArray['components']['schemas'])) {
227227
$counter = 0;
228228
foreach ($jsonSchemaArray['components']['schemas'] as &$schema) {
229-
$schema['$id'] = $className . '_' . $counter++;
229+
$schema['title'] = $className . '_' . $counter++;
230230
}
231231
}
232232

tests/Objects/ReferencePropertyTest.php

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -395,9 +395,9 @@ public function invalidCombinedReferenceObjectPropertyTypeDataProvider(): array
395395
* @throws RenderException
396396
* @throws SchemaException
397397
*/
398-
public function testNestedExternalReference(string $reference): void
398+
public function testNestedExternalReference(string $id, string $reference): void
399399
{
400-
$className = $this->generateClassFromFileTemplate('NestedExternalReference.json', [$reference]);
400+
$className = $this->generateClassFromFileTemplate('NestedExternalReference.json', [$id, $reference]);
401401

402402
$object = new $className([
403403
'family' => [
@@ -426,9 +426,29 @@ public function testNestedExternalReference(string $reference): void
426426

427427
public function nestedReferenceProvider(): array
428428
{
429+
$baseURL = 'https://raw.githubusercontent.com/wol-soft/php-json-schema-model-generator/master/tests/Schema/';
430+
429431
return [
430-
'Local reference' => ['../ReferencePropertyTest_external/library.json'],
431-
'Network reference' => ['https://raw.githubusercontent.com/wol-soft/php-json-schema-model-generator/master/tests/Schema/ReferencePropertyTest_external/library.json'],
432+
'local reference - relative' => [
433+
'NestedExternalReference.json',
434+
'../ReferencePropertyTest_external/library.json',
435+
],
436+
'local reference - context absolute' => [
437+
'NestedExternalReference.json',
438+
'/ReferencePropertyTest_external/library.json',
439+
],
440+
'network reference - full URL' => [
441+
'NestedExternalReference.json',
442+
$baseURL . 'ReferencePropertyTest_external/library.json',
443+
],
444+
'network reference - relative path to full URL $id' => [
445+
$baseURL . 'ReferencePropertyTest/NestedExternalReference.json',
446+
'../ReferencePropertyTest_external/library.json',
447+
],
448+
'network reference - absolute path to full URL $id' => [
449+
$baseURL . 'ReferencePropertyTest/NestedExternalReference.json',
450+
'/wol-soft/php-json-schema-model-generator/master/tests/Schema/ReferencePropertyTest_external/library.json',
451+
],
432452
];
433453
}
434454

tests/Schema/ReferencePropertyTest/NestedExternalReference.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
22
"type": "object",
3+
"$id": "%s",
34
"properties": {
45
"family": {
56
"$ref": "%s#/definitions/family"

0 commit comments

Comments
 (0)