diff --git a/src/Service/InlineService.php b/src/Service/InlineService.php index f1f3d39..8317c00 100644 --- a/src/Service/InlineService.php +++ b/src/Service/InlineService.php @@ -21,6 +21,8 @@ final class InlineService private array $components = []; /** @var list */ private array $allOfPaths = []; + /** @var array> */ + private array $modelAllOfPaths = []; private readonly DocumentFactory $documentFactory; private LocalReferenceResolver $localReferenceResolver; @@ -97,6 +99,7 @@ private function getRefName(Reference $reference, SchemaType $type): string private function processDocument( Document&WritableDocument $document, ?Reference $rootReference = null, + ?string $parentPath = null, ): Document&WritableDocument { $isAllOfDocument = ($document->get()['allOf'] ?? null) !== null; foreach ($this->findReferences($document, $rootReference) as $ref => $documentPaths) { @@ -104,6 +107,7 @@ private function processDocument( $type = $this->resolveSchemaType($documentPaths); $refName = $this->getRefName($reference, $type); + $nextParentPath = $parentPath; if ( isset($this->components[$type->name][$refName]) && $this->components[$type->name][$refName] @@ -112,9 +116,14 @@ private function processDocument( if (is_string($schema)) { $schema = ['$ref' => $schema]; } - foreach ($documentPaths as $path) { - $document->set($path, $schema); - } + $document = $this->processDocumentPaths( + $document, + $schema, + $documentPaths, + $refName, + $rootReference?->getName(), + $parentPath, + ); continue; } @@ -130,6 +139,7 @@ private function processDocument( } else { $referenceDocument = BasicDocument::fromDocument($this->referenceResolver->resolve($reference)); + $nextParentPath .= $documentPaths[0]; } $schemaId = $referenceDocument->get()['$id'] ?? $refName; // reserve schema to avoid infinite recursion @@ -137,16 +147,19 @@ private function processDocument( $this->components[$type->name][$refName] = $this->processDocument( $referenceDocument, $reference, + $nextParentPath, )->get(); $this->components[$type->name][$refName]['$id'] = $schemaId; // update $ref to new ref - foreach ($documentPaths as $path) { - if (str_contains($path, '/allOf/')) { - $this->allOfPaths[] = substr($path, 0, (int)strpos($path, '/allOf/')); - } - $document->set($path, $this->components[$type->name][$refName]); - } + $document = $this->processDocumentPaths( + $document, + $this->components[$type->name][$refName], + $documentPaths, + $refName, + $rootReference?->getName(), + $parentPath, + ); } if ($isAllOfDocument) { $merger = new AllOfMerger($document); @@ -156,6 +169,35 @@ private function processDocument( return $document; } + /** + * @param list $documentPaths + */ + private function processDocumentPaths( + Document&WritableDocument $document, + mixed $schema, + array $documentPaths, + string $refName, + ?string $parentName = null, + ?string $parentPath = null, + ): Document&WritableDocument { + $parentName ??= 'root'; + foreach ($documentPaths as $path) { + if (array_key_exists($refName, $this->modelAllOfPaths)) { + foreach ($this->modelAllOfPaths[$refName] as $allOfPath) { + $this->allOfPaths[] = ($parentPath ?? '') . $path . $allOfPath; + } + } + elseif (str_contains($path, '/allOf/')) { + $stripped = substr($path, 0, (int)strpos($path, '/allOf/')); + $this->modelAllOfPaths[$parentName][] = $stripped; + $this->allOfPaths[] = $parentPath . $stripped; + } + $document->set($path, $schema); + } + + return $document; + } + /** * @return array> */ diff --git a/tests/inlined-schemas/inline-allOf-multiple/schema.dist.json b/tests/inlined-schemas/inline-allOf-multiple/schema.dist.json new file mode 100644 index 0000000..52f54cb --- /dev/null +++ b/tests/inlined-schemas/inline-allOf-multiple/schema.dist.json @@ -0,0 +1,145 @@ +{ + "title": "Account", + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "new", + "verified", + "active", + "blocked", + "deleted" + ], + "readOnly": true + }, + "createdOn": { + "type": "string", + "format": "date-time", + "readOnly": true + }, + "anotherRefWithAllOf": { + "$id": "OtherNestedAllOf.json", + "properties": { + "otherNestedAllOf": { + "properties": { + "sub1": { + "type": "string" + }, + "sub2": { + "format": "uuid", + "type": "string" + } + }, + "readOnly": true, + "required": [ + "sub1" + ], + "type": "object" + } + }, + "type": "object" + }, + "blockedUntil": { + "type": "string", + "format": "date-time" + }, + "role": { + "readOnly": true, + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "sub2": { + "type": "string" + }, + "externalId": { + "type": "string", + "format": "uuid" + } + } + }, + "complexAllOfMerge": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "sub2": { + "type": "string", + "format": "uuid" + }, + "externalId": { + "type": "string", + "format": "uuid" + }, + "sub1": { + "type": "string" + }, + "extraFields": { + "type": "string" + } + }, + "required": [ + "sub1", + "extraFields" + ] + }, + "otherRefWithAllOf": { + "$id": "OtherNestedAllOf.json", + "properties": { + "otherNestedAllOf": { + "properties": { + "sub1": { + "type": "string" + }, + "sub2": { + "format": "uuid", + "type": "string" + } + }, + "readOnly": true, + "required": [ + "sub1" + ], + "type": "object" + } + }, + "type": "object" + }, + "refWithAllOf": { + "$id": "NestedAllOf.json", + "properties": { + "nestedAllOf": { + "properties": { + "sub1": { + "type": "string" + }, + "sub2": { + "format": "uuid", + "type": "string" + } + }, + "readOnly": true, + "required": [ + "sub1" + ], + "type": "object" + } + }, + "type": "object" + }, + "required": [ + "id", + "status" + ] + } +} diff --git a/tests/inlined-schemas/inline-allOf-multiple/schema.json b/tests/inlined-schemas/inline-allOf-multiple/schema.json new file mode 100644 index 0000000..d6b7566 --- /dev/null +++ b/tests/inlined-schemas/inline-allOf-multiple/schema.json @@ -0,0 +1,74 @@ +{ + "title": "Account", + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "new", + "verified", + "active", + "blocked", + "deleted" + ], + "readOnly": true + }, + "createdOn": { + "type": "string", + "format": "date-time", + "readOnly": true + }, + "blockedUntil": { + "type": "string", + "format": "date-time" + }, + "role": { + "allOf": [ + { + "readOnly": true + }, + { + "$ref": "sub/Role.json" + } + ] + }, + "complexAllOfMerge": { + "allOf": [ + { + "$ref": "sub/Role.json" + }, + { + "$ref": "sub/Sub.json" + }, + { + "properties": { + "extraFields": { + "type": "string" + } + }, + "required": ["extraFields", "sub1"] + } + ] + }, + "refWithAllOf": { + "$ref": "sub/NestedAllOf.json" + }, + "otherRefWithAllOf": { + "$ref": "sub/OtherNestedAllOf.json" + }, + "anotherRefWithAllOf": { + "$ref": "sub/OtherNestedAllOf.json" + }, + "required": [ + "id", + "status" + ] + } +} diff --git a/tests/inlined-schemas/inline-allOf-multiple/sub/NestedAllOf.json b/tests/inlined-schemas/inline-allOf-multiple/sub/NestedAllOf.json new file mode 100644 index 0000000..ffaf42e --- /dev/null +++ b/tests/inlined-schemas/inline-allOf-multiple/sub/NestedAllOf.json @@ -0,0 +1,15 @@ +{ + "type": "object", + "properties": { + "nestedAllOf": { + "allOf": [ + { + "$ref": "Sub.json" + }, + { + "readOnly": true + } + ] + } + } +} diff --git a/tests/inlined-schemas/inline-allOf-multiple/sub/OtherNestedAllOf.json b/tests/inlined-schemas/inline-allOf-multiple/sub/OtherNestedAllOf.json new file mode 100644 index 0000000..20833e2 --- /dev/null +++ b/tests/inlined-schemas/inline-allOf-multiple/sub/OtherNestedAllOf.json @@ -0,0 +1,15 @@ +{ + "type": "object", + "properties": { + "otherNestedAllOf": { + "allOf": [ + { + "$ref": "Sub.json" + }, + { + "readOnly": true + } + ] + } + } +} diff --git a/tests/inlined-schemas/inline-allOf-multiple/sub/Role.json b/tests/inlined-schemas/inline-allOf-multiple/sub/Role.json new file mode 100644 index 0000000..ecd8182 --- /dev/null +++ b/tests/inlined-schemas/inline-allOf-multiple/sub/Role.json @@ -0,0 +1,15 @@ +{ + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "sub2": { + "type": "string" + }, + "externalId": { + "type": "string", + "format": "uuid" + } + } +} diff --git a/tests/inlined-schemas/inline-allOf-multiple/sub/Sub.json b/tests/inlined-schemas/inline-allOf-multiple/sub/Sub.json new file mode 100644 index 0000000..b6a958e --- /dev/null +++ b/tests/inlined-schemas/inline-allOf-multiple/sub/Sub.json @@ -0,0 +1,13 @@ +{ + "type": "object", + "properties": { + "sub1": { + "type": "string" + }, + "sub2": { + "type": "string", + "format": "uuid" + } + }, + "required": ["sub1"] +}