Skip to content

Commit

Permalink
add #[SourceKey] attribute (#62)
Browse files Browse the repository at this point in the history
  • Loading branch information
JanTvrdik committed Jun 9, 2024
1 parent cc5fb15 commit b422412
Show file tree
Hide file tree
Showing 7 changed files with 189 additions and 0 deletions.
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,22 @@ class Person
}
```

### Renaming keys

If the input keys do not match the property names, you can use the `#[SourceKey]` attribute to specify the key name:

```php
use ShipMonk\InputMapper\Compiler\Mapper\Object\SourceKey;

class Person
{
public function __construct(
#[SourceKey('full_name')]
public readonly string $name,
) {}
}
```

### Using custom mappers

To map classes with your custom mapper, you need to implement `ShipMonk\InputMapper\Runtime\Mapper` interface and register it with `MapperProvider`:
Expand Down
17 changes: 17 additions & 0 deletions src/Compiler/Mapper/Object/SourceKey.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php declare(strict_types = 1);

namespace ShipMonk\InputMapper\Compiler\Mapper\Object;

use Attribute;

#[Attribute(Attribute::TARGET_PARAMETER | Attribute::TARGET_PROPERTY)]
class SourceKey
{

public function __construct(
public readonly string $key,
)
{
}

}
6 changes: 6 additions & 0 deletions src/Compiler/MapperFactory/DefaultMapperCompilerFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
use ShipMonk\InputMapper\Compiler\Mapper\Object\MapDateTimeImmutable;
use ShipMonk\InputMapper\Compiler\Mapper\Object\MapEnum;
use ShipMonk\InputMapper\Compiler\Mapper\Object\MapObject;
use ShipMonk\InputMapper\Compiler\Mapper\Object\SourceKey;
use ShipMonk\InputMapper\Compiler\Mapper\Optional as OptionalAttribute;
use ShipMonk\InputMapper\Compiler\Mapper\Scalar\MapBool;
use ShipMonk\InputMapper\Compiler\Mapper\Scalar\MapFloat;
Expand Down Expand Up @@ -314,6 +315,11 @@ protected function createObjectMappingByConstructorInvocation(
foreach ($constructor->getParameters() as $parameter) {
$name = $parameter->getName();
$type = $constructorParameterTypes[$name];

foreach ($parameter->getAttributes(SourceKey::class) as $attribute) {
$name = $attribute->newInstance()->key;
}

$constructorParameterMapperCompilers[$name] = $this->createParameterMapperCompiler($parameter, $type, $options);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
<?php declare (strict_types=1);

namespace ShipMonkTests\InputMapper\Compiler\Mapper\Object\Data;

use ShipMonk\InputMapper\Compiler\Mapper\Object\MapObject;
use ShipMonk\InputMapper\Runtime\Exception\MappingFailedException;
use ShipMonk\InputMapper\Runtime\Mapper;
use ShipMonk\InputMapper\Runtime\MapperProvider;
use ShipMonk\InputMapper\Runtime\Optional;
use ShipMonk\InputMapper\Runtime\OptionalSome;
use function array_diff_key;
use function array_key_exists;
use function array_keys;
use function count;
use function is_array;
use function is_int;
use function is_string;

/**
* Generated mapper by {@see MapObject}. Do not edit directly.
*
* @implements Mapper<PersonInput>
*/
class PersonWithRenamedSourceKeysMapper implements Mapper
{
public function __construct(private readonly MapperProvider $provider)
{
}

/**
* @param list<string|int> $path
* @throws MappingFailedException
*/
public function map(mixed $data, array $path = []): PersonInput
{
if (!is_array($data)) {
throw MappingFailedException::incorrectType($data, $path, 'array');
}

if (!array_key_exists('ID', $data)) {
throw MappingFailedException::missingKey($path, 'ID');
}

if (!array_key_exists('NAME', $data)) {
throw MappingFailedException::missingKey($path, 'NAME');
}

$knownKeys = ['ID' => true, 'NAME' => true, 'AGE' => true];
$extraKeys = array_diff_key($data, $knownKeys);

if (count($extraKeys) > 0) {
throw MappingFailedException::extraKeys($path, array_keys($extraKeys));
}

return new PersonInput(
$this->mapID($data['ID'], [...$path, 'ID']),
$this->mapNAME($data['NAME'], [...$path, 'NAME']),
array_key_exists('AGE', $data) ? $this->mapAGE($data['AGE'], [...$path, 'AGE']) : Optional::none($path, 'AGE'),
);
}

/**
* @param list<string|int> $path
* @throws MappingFailedException
*/
private function mapID(mixed $data, array $path = []): int
{
if (!is_int($data)) {
throw MappingFailedException::incorrectType($data, $path, 'int');
}

return $data;
}

/**
* @param list<string|int> $path
* @throws MappingFailedException
*/
private function mapNAME(mixed $data, array $path = []): string
{
if (!is_string($data)) {
throw MappingFailedException::incorrectType($data, $path, 'string');
}

return $data;
}

/**
* @param list<string|int> $path
* @return OptionalSome<int>
* @throws MappingFailedException
*/
private function mapAGE(mixed $data, array $path = []): OptionalSome
{
if (!is_int($data)) {
throw MappingFailedException::incorrectType($data, $path, 'int');
}

return Optional::of($data);
}
}
16 changes: 16 additions & 0 deletions tests/Compiler/Mapper/Object/MapObjectTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,22 @@ className: CollectionInput::class,
);
}

public function testCompileWithRenamedSourceKey(): void
{
$mapperCompiler = new MapObject(PersonInput::class, [
'ID' => new MapInt(),
'NAME' => new MapString(),
'AGE' => new MapOptional(new MapInt()),
]);

$mapper = $this->compileMapper('PersonWithRenamedSourceKeys', $mapperCompiler);

self::assertEquals(
new PersonInput(1, 'John', Optional::none([], 'AGE')),
$mapper->map(['ID' => 1, 'NAME' => 'John']),
);
}

private function createMovieInputMapperCompiler(): MapperCompiler
{
return new MapObject(MovieInput::class, [
Expand Down
20 changes: 20 additions & 0 deletions tests/Compiler/MapperFactory/Data/InputWithRenamedSourceKey.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php declare(strict_types = 1);

namespace ShipMonkTests\InputMapper\Compiler\MapperFactory\Data;

use ShipMonk\InputMapper\Compiler\Mapper\Object\SourceKey;

class InputWithRenamedSourceKey
{

public function __construct(
#[SourceKey('old_value')]
public readonly int $oldValue,

#[SourceKey('new_value')]
public readonly int $newValue,
)
{
}

}
13 changes: 13 additions & 0 deletions tests/Compiler/MapperFactory/DefaultMapperCompilerFactoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
use ShipMonkTests\InputMapper\Compiler\MapperFactory\Data\InputWithIncompatibleMapperCompiler;
use ShipMonkTests\InputMapper\Compiler\MapperFactory\Data\InputWithoutConstructor;
use ShipMonkTests\InputMapper\Compiler\MapperFactory\Data\InputWithPrivateConstructor;
use ShipMonkTests\InputMapper\Compiler\MapperFactory\Data\InputWithRenamedSourceKey;
use ShipMonkTests\InputMapper\InputMapperTestCase;

class DefaultMapperCompilerFactoryTest extends InputMapperTestCase
Expand Down Expand Up @@ -211,6 +212,18 @@ className: EqualsFilterInput::class,
]),
];

yield 'InputWithRenamedSourceKey' => [
InputWithRenamedSourceKey::class,
[],
new MapObject(
className: InputWithRenamedSourceKey::class,
constructorArgsMapperCompilers: [
'old_value' => new MapInt(),
'new_value' => new MapInt(),
],
),
];

yield 'array' => [
'array',
[],
Expand Down

0 comments on commit b422412

Please sign in to comment.