Repository: JsonMapper/JsonMapper (php)

Issue:
Different json key and PHP variable naming
I have a json structure that has a property called `categories[]`. The problem is that this thing cannot be mapped, because it contains special characters in the name, so I cannot declare `$categories[]` as variable name in PHP.

Having a possibility to add manual mapping description would be very useful. I can imagine this being used in many various scenarios where developers do not want the json key and the PHP variable having the exact same name.

Pull request description:
Add Rename mapping
This PR adds the option to rename the JSON to avoid naming conflicts regarding reserved names, illegal characters etc.

Would resolve #74 

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4842dd1..6c1a96e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
 ## [Unreleased]
+### Added
+- Support for renaming JSON properties before mapping values onto an object [PR#75](https://github.com/JsonMapper/JsonMapper/pull/75)
 
 ## [2.1.0] - 2021-01-28
 ### Added
diff --git a/src/Middleware/Rename/Mapping.php b/src/Middleware/Rename/Mapping.php
new file mode 100644
index 0000000..07ea687
--- /dev/null
+++ b/src/Middleware/Rename/Mapping.php
@@ -0,0 +1,37 @@
+<?php
+
+declare(strict_types=1);
+
+namespace JsonMapper\Middleware\Rename;
+
+class Mapping
+{
+    /** @var string */
+    private $class;
+    /** @var string */
+    private $from;
+    /** @var string */
+    private $to;
+
+    public function __construct(string $class, string $from, string $to)
+    {
+        $this->class = $class;
+        $this->from = $from;
+        $this->to = $to;
+    }
+
+    public function getClass(): string
+    {
+        return $this->class;
+    }
+
+    public function getFrom(): string
+    {
+        return $this->from;
+    }
+
+    public function getTo(): string
+    {
+        return $this->to;
+    }
+}
diff --git a/src/Middleware/Rename/Rename.php b/src/Middleware/Rename/Rename.php
new file mode 100644
index 0000000..9a8c997
--- /dev/null
+++ b/src/Middleware/Rename/Rename.php
@@ -0,0 +1,42 @@
+<?php
+
+declare(strict_types=1);
+
+namespace JsonMapper\Middleware\Rename;
+
+use JsonMapper\JsonMapperInterface;
+use JsonMapper\Middleware\AbstractMiddleware;
+use JsonMapper\ValueObjects\PropertyMap;
+use JsonMapper\Wrapper\ObjectWrapper;
+
+class Rename extends AbstractMiddleware
+{
+    /** @var Mapping[] */
+    private $mapping;
+
+    public function __construct(Mapping ...$mapping)
+    {
+        $this->mapping = $mapping;
+    }
+
+    public function addMapping(string $class, string $from, string $to): void
+    {
+        $this->mapping[] = new Mapping($class, $from, $to);
+    }
+
+    public function handle(\stdClass $json, ObjectWrapper $object, PropertyMap $propertyMap, JsonMapperInterface $mapper): void
+    {
+        $mapping = array_filter($this->mapping, function ($map) use ($object) {
+            return $map->getClass() === get_class($object->getObject());
+        });
+        foreach ($mapping as $map) {
+            $from = $map->getFrom();
+            $to = $map->getTo();
+
+            if (isset($json->$from)) {
+                $json->$to = $json->$from;
+                unset($json->$from);
+            }
+        }
+    }
+}


Test patch:
diff --git a/tests/Implementation/SimpleObject.php b/tests/Implementation/SimpleObject.php
index 0dce65f..5b50bfc 100644
--- a/tests/Implementation/SimpleObject.php
+++ b/tests/Implementation/SimpleObject.php
@@ -9,6 +9,11 @@ class SimpleObject
     /** @var string */
     private $name;
 
+    public function __construct(string $name = '')
+    {
+        $this->name = $name;
+    }
+
     public function getName(): string
     {
         return $this->name;
diff --git a/tests/Integration/JsonMapperTest.php b/tests/Integration/JsonMapperTest.php
index c66d781..9083572 100644
--- a/tests/Integration/JsonMapperTest.php
+++ b/tests/Integration/JsonMapperTest.php
@@ -5,7 +5,9 @@ declare(strict_types=1);
 namespace JsonMapper\Tests\Integration;
 
 use JsonMapper\JsonMapperFactory;
+use JsonMapper\Middleware\Rename\Rename;
 use JsonMapper\Tests\Implementation\ComplexObject;
+use JsonMapper\Tests\Implementation\Models\User;
 use JsonMapper\Tests\Implementation\Popo;
 use JsonMapper\Tests\Implementation\Php74;
 use JsonMapper\Tests\Implementation\SimpleObject;
@@ -458,6 +460,44 @@ class JsonMapperTest extends TestCase
         self::assertEquals($object->getPopos()[1]->name, $json->popos[1]->name);
     }
 
+    public function testItCanRenameJsonProperties(): void
+    {
+        // Arrange
+        $rename = new Rename();
+        $rename->addMapping(User::class, 'Full-Name', 'name');
+        $rename->addMapping(User::class, 'Identifier', 'id');
+        $mapper = (new JsonMapperFactory())->bestFit();
+        $mapper->unshift($rename);
+        $object = new User();
+        $json = (object) ['Full-Name' => 'John Doe', 'Identifier' => '42'];
+
+        // Act
+        $mapper->mapObject($json, $object);
+
+        // Assert
+        self::assertEquals('John Doe', $object->getName());
+        self::assertEquals(42, $object->getId());
+    }
+
+    public function testItCanRenameJsonPropertiesOnNestedObjects(): void
+    {
+        // Arrange
+        $rename = new Rename();
+        $rename->addMapping(SimpleObject::class, 'FULL-NAME', 'name');
+        $rename->addMapping(ComplexObject::class, 'sub', 'children');
+        $mapper = (new JsonMapperFactory())->bestFit();
+        $mapper->unshift($rename);
+        $object = new ComplexObject();
+        $json = (object) ['sub' => [(object) ['FULL-NAME' => 'John Doe'], (object) ['FULL-NAME' => 'Jane Doe']]];
+
+        // Act
+        $mapper->mapObject($json, $object);
+
+        // Assert
+        self::assertCount(2, $object->getChildren());
+        self::assertEquals([new SimpleObject('John Doe'), new SimpleObject('Jane Doe')], $object->getChildren());
+    }
+
     public function scalarValueDataTypes(): array
     {
         return [
diff --git a/tests/Unit/Middleware/Rename/MappingTest.php b/tests/Unit/Middleware/Rename/MappingTest.php
new file mode 100644
index 0000000..f26e18d
--- /dev/null
+++ b/tests/Unit/Middleware/Rename/MappingTest.php
@@ -0,0 +1,24 @@
+<?php
+
+declare(strict_types=1);
+
+namespace JsonMapper\Tests\Unit\Middleware\Rename;
+
+use JsonMapper\Middleware\Rename\Mapping;
+use JsonMapper\Tests\Implementation\Models\User;
+use PHPUnit\Framework\TestCase;
+
+class MappingTest extends TestCase
+{
+    /**
+     * @covers \JsonMapper\Middleware\Rename\Mapping
+     */
+    public function testMappingCanHoldProperties(): void
+    {
+        $mapping = new Mapping(User::class, 'municipality', 'city');
+
+        self::assertEquals(User::class, $mapping->getClass());
+        self::assertEquals('municipality', $mapping->getFrom());
+        self::assertEquals('city', $mapping->getTo());
+    }
+}
diff --git a/tests/Unit/Middleware/Rename/RenameTest.php b/tests/Unit/Middleware/Rename/RenameTest.php
new file mode 100644
index 0000000..7200f82
--- /dev/null
+++ b/tests/Unit/Middleware/Rename/RenameTest.php
@@ -0,0 +1,88 @@
+<?php
+
+declare(strict_types=1);
+
+namespace JsonMapper\Tests\Unit\Middleware\Rename;
+
+use JsonMapper\JsonMapperInterface;
+use JsonMapper\Middleware\Rename\Rename;
+use JsonMapper\Tests\Implementation\ComplexObject;
+use JsonMapper\Tests\Implementation\Models\User;
+use JsonMapper\ValueObjects\PropertyMap;
+use JsonMapper\Wrapper\ObjectWrapper;
+use PHPUnit\Framework\TestCase;
+
+class RenameTest extends TestCase
+{
+    /**
+     * @covers \JsonMapper\Middleware\Rename\Rename
+     */
+    public function testLeavesJsonUntouchedWithEmptyMapping(): void
+    {
+        $middleware = new Rename();
+        $json = (object) ['name' => 'John Doe', 'id' => 42];
+        $clone = clone $json;
+        $wrapper = new ObjectWrapper(new User());
+        $propertyMap = new PropertyMap();
+        $mapper = $this->createMock(JsonMapperInterface::class);
+
+        $middleware->handle($clone, $wrapper, $propertyMap, $mapper);
+
+        self::assertEquals($json, $clone);
+    }
+
+    /**
+     * @covers \JsonMapper\Middleware\Rename\Rename
+     */
+    public function testLeavesJsonUntouchedWithPropertyNotInMapping(): void
+    {
+        $middleware = new Rename();
+        $middleware->addMapping(User::class, 'municipality', 'city');
+        $json = (object) ['name' => 'John Doe', 'id' => 42];
+        $clone = clone $json;
+        $wrapper = new ObjectWrapper(new User());
+        $propertyMap = new PropertyMap();
+        $mapper = $this->createMock(JsonMapperInterface::class);
+
+        $middleware->handle($clone, $wrapper, $propertyMap, $mapper);
+
+        self::assertEquals($json, $clone);
+    }
+
+    /**
+     * @covers \JsonMapper\Middleware\Rename\Rename
+     */
+    public function testLeavesJsonUntouchedWithPropertyInMappingForDifferentClass(): void
+    {
+        $middleware = new Rename();
+        $middleware->addMapping(ComplexObject::class, 'name', 'fullName');
+        $json = (object) ['name' => 'John Doe', 'id' => 42];
+        $clone = clone $json;
+        $wrapper = new ObjectWrapper(new User());
+        $propertyMap = new PropertyMap();
+        $mapper = $this->createMock(JsonMapperInterface::class);
+
+        $middleware->handle($clone, $wrapper, $propertyMap, $mapper);
+
+        self::assertEquals($json, $clone);
+    }
+
+    /**
+     * @covers \JsonMapper\Middleware\Rename\Rename
+     */
+    public function testAltersJsonWithPropertyInMapping(): void
+    {
+        $middleware = new Rename();
+        $middleware->addMapping(User::class, 'name', 'fullName');
+        $json = (object) ['name' => 'John Doe', 'id' => 42];
+        $clone = clone $json;
+        $wrapper = new ObjectWrapper(new User());
+        $propertyMap = new PropertyMap();
+        $mapper = $this->createMock(JsonMapperInterface::class);
+
+        $middleware->handle($clone, $wrapper, $propertyMap, $mapper);
+
+        self::assertEquals($json->id, $clone->id);
+        self::assertEquals($json->name, $clone->fullName);
+    }
+}