From 42b0c4f108a5a933aabc8378fa9440bca1f865ae Mon Sep 17 00:00:00 2001
From: Nick <coding.nikazu@gmail.com>
Date: Fri, 24 Nov 2023 11:08:39 +0100
Subject: [PATCH] fix embedded root schema (#63)

* fix embedded root schema

* small fix
---
 .../{embedded => }/Collection.avsc            |   1 +
 src/Merger/SchemaMerger.php                   |  11 +-
 .../Registry/SchemaRegistryTest.php           |   2 +-
 tests/Unit/Merger/SchemaMergerTest.php        | 106 +++++++++++++++++-
 4 files changed, 114 insertions(+), 6 deletions(-)
 rename example/schemaTemplates/{embedded => }/Collection.avsc (90%)

diff --git a/example/schemaTemplates/embedded/Collection.avsc b/example/schemaTemplates/Collection.avsc
similarity index 90%
rename from example/schemaTemplates/embedded/Collection.avsc
rename to example/schemaTemplates/Collection.avsc
index 9a47df6..7d3b854 100644
--- a/example/schemaTemplates/embedded/Collection.avsc
+++ b/example/schemaTemplates/Collection.avsc
@@ -1,6 +1,7 @@
 {
     "type": "record",
     "namespace": "com.example",
+    "schema_level": "root",
     "name": "Collection",
     "fields": [
         { "name": "name", "type": "string" },
diff --git a/src/Merger/SchemaMerger.php b/src/Merger/SchemaMerger.php
index 113f681..9f23292 100644
--- a/src/Merger/SchemaMerger.php
+++ b/src/Merger/SchemaMerger.php
@@ -102,8 +102,17 @@ private function replaceSchemaIdWithDefinition(
     ): string {
         $idString = '"' . $schemaId . '"';
         $pos = (int) strpos($rootDefinition, $idString);
+        $embeddedDefinitionWithoutLevel = $this->removeSchemaLevel($embeddedDefinition);
 
-        return substr_replace($rootDefinition, $embeddedDefinition, $pos, strlen($idString));
+        return substr_replace($rootDefinition, $embeddedDefinitionWithoutLevel, $pos, strlen($idString));
+    }
+
+    private function removeSchemaLevel(string $embeddedDefinition): string
+    {
+        $arraySchema = json_decode($embeddedDefinition, true);
+        unset($arraySchema['schema_level']);
+
+        return json_encode($arraySchema, JSON_THROW_ON_ERROR | JSON_PRESERVE_ZERO_FRACTION);
     }
 
     /**
diff --git a/tests/Integration/Registry/SchemaRegistryTest.php b/tests/Integration/Registry/SchemaRegistryTest.php
index 87a0e00..a8c5586 100644
--- a/tests/Integration/Registry/SchemaRegistryTest.php
+++ b/tests/Integration/Registry/SchemaRegistryTest.php
@@ -59,7 +59,7 @@ public function testGetRootSchemas()
 
         $rootSchemas = $registry->getRootSchemas();
 
-        self::assertCount(2, $rootSchemas);
+        self::assertCount(3, $rootSchemas);
 
         foreach ($rootSchemas as $rootSchema) {
             self::assertInstanceOf(SchemaTemplateInterface::class, $rootSchema);
diff --git a/tests/Unit/Merger/SchemaMergerTest.php b/tests/Unit/Merger/SchemaMergerTest.php
index aede98d..a373e7d 100644
--- a/tests/Unit/Merger/SchemaMergerTest.php
+++ b/tests/Unit/Merger/SchemaMergerTest.php
@@ -86,14 +86,14 @@ public function testGetResolvedSchemaTemplate()
                 { "name": "items", "type": {"type": "array", "items": "com.example.Page" }, "default": [] }
             ]
         }';
-        $subschemaDefinition = '{
+        $subschemaDefinition = json_encode(json_decode('{
             "type": "record",
             "namespace": "com.example",
             "name": "Page",
             "fields": [
                 { "name": "number", "type": "int" }
             ]
-        }';
+        }'));
 
         $expectedResult = str_replace('"com.example.Page"', $subschemaDefinition, $rootDefinition);
 
@@ -124,6 +124,104 @@ public function testGetResolvedSchemaTemplate()
         $merger->getResolvedSchemaTemplate($rootSchemaTemplate);
     }
 
+    public function testGetResolvedSchemaTemplateWithEmbeddedRoot(): void
+    {
+        $rootDefinition = '{
+          "type": "record",
+          "namespace": "com.example",
+          "schema_level": "root",
+          "name": "Library",
+          "fields": [
+            {
+              "name": "name",
+              "type": "string"
+            },
+            {
+              "name": "foundingYear",
+              "type": [
+                "null",
+                "int"
+              ],
+              "default": null
+            },
+            {
+              "name": "type",
+              "type": [
+                "null",
+                {
+                  "name": "type",
+                  "type": "enum",
+                  "symbols": [
+                    "PUBLIC",
+                    "PRIVATE"
+                  ]
+                }
+              ],
+              "default": null
+            },
+            {
+              "name": "collection",
+              "type": {
+                "type": "array",
+                "items": "com.example.Collection"
+              },
+              "default": []
+            },
+            {
+              "name": "archive",
+              "type": {
+                "type": "array",
+                "items": "com.example.Collection"
+              },
+              "default": []
+            }
+          ]
+        }';
+        $subschemaDefinition = json_encode(json_decode('{
+            "type": "record",
+            "namespace": "com.example",
+            "schema_level": "root",
+            "name": "Collection",
+            "fields": [
+                { "name": "name", "type": "string" }
+            ]
+        }'));
+
+        $subschemaDefinitionArray = \Safe\json_decode($subschemaDefinition, true);
+        unset($subschemaDefinitionArray['schema_level']);
+        $subschemaDefinitionWithoutLevel = json_encode($subschemaDefinitionArray);
+
+        $subschemaId = '"com.example.Collection"';
+        $pos = strpos($rootDefinition, $subschemaId);
+        $expectedResult = substr_replace($rootDefinition, $subschemaDefinitionWithoutLevel, $pos, strlen($subschemaId));
+
+        $subschemaTemplate = $this->getMockForAbstractClass(SchemaTemplateInterface::class);
+        $subschemaTemplate
+            ->expects(self::once())
+            ->method('getSchemaDefinition')
+            ->willReturn($subschemaDefinition);
+        $schemaRegistry = $this->getMockForAbstractClass(SchemaRegistryInterface::class);
+        $schemaRegistry
+            ->expects(self::once())
+            ->method('getSchemaById')
+            ->with('com.example.Collection')
+            ->willReturn($subschemaTemplate);
+        $rootSchemaTemplate = $this->getMockForAbstractClass(SchemaTemplateInterface::class);
+        $rootSchemaTemplate
+            ->expects(self::once())
+            ->method('getSchemaDefinition')
+            ->willReturn($rootDefinition);
+        $rootSchemaTemplate
+            ->expects(self::once())
+            ->method('withSchemaDefinition')
+            ->with($expectedResult)
+            ->willReturn($rootSchemaTemplate);
+
+        $merger = new SchemaMerger($schemaRegistry);
+
+        $merger->getResolvedSchemaTemplate($rootSchemaTemplate);
+    }
+
     public function testGetResolvedSchemaTemplateWithMultiEmbedd()
     {
         $rootDefinition = $this->reformatJsonString('{
@@ -304,14 +402,14 @@ public function testGetResolvedSchemaTemplateWithDifferentNamespaceForEmbeddedSc
                 { "name": "items", "type": {"type": "array", "items": "com.example.other.Page" }, "default": [] }
             ]
         }';
-        $subschemaDefinition = '{
+        $subschemaDefinition = json_encode(json_decode('{
             "type": "record",
             "namespace": "com.example.other",
             "name": "Page",
             "fields": [
                 { "name": "number", "type": "int" }
             ]
-        }';
+        }'));
 
         $expectedResult = str_replace('"com.example.other.Page"', $subschemaDefinition, $rootDefinition);