Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).

## [11.2.1] - 2026-06-17
### Fixed
- Response body mappers now honor the `oneOf`/`anyOf` discriminator `mapping`, dispatching to the correct setter and mapper instead of deriving them from the raw discriminator value (which broke when a mapping value differed from its schema name)

## [11.2.0] - 2026-02-19
### Added
- RFC 6839 support: Content types with `+json` suffix (e.g., `application/vnd.api+json`, `application/hal+json`, `application/problem+json`) are now recognized as JSON-based formats
Expand Down
11 changes: 11 additions & 0 deletions src/Ast/Builder/CodeBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
use PhpParser\Node\Name\FullyQualified;
use PhpParser\Node\Name\Relative;
use PhpParser\Node\Stmt;
use PhpParser\Node\Stmt\Break_;
use PhpParser\Node\Stmt\Case_;
use PhpParser\Node\Stmt\Catch_;
use PhpParser\Node\Stmt\Class_;
Expand Down Expand Up @@ -289,6 +290,16 @@ public function case(Expr $condition, Stmt ...$stmts): Case_
return new Case_($condition, $stmts);
}

public function default(Stmt ...$stmts): Case_
{
return new Case_(null, $stmts);
}

public function break(): Break_
{
return new Break_();
}

public function if(Expr $condition, array $stmts, array $elseIfs = [], Else_ $else = null): If_
{
$subNodes = [];
Expand Down
176 changes: 129 additions & 47 deletions src/Generator/SchemaMapperGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@
use DoclerLabs\ApiClientGenerator\Input\Specification;
use DoclerLabs\ApiClientGenerator\Naming\SchemaMapperNaming;
use DoclerLabs\ApiClientGenerator\Output\Php\PhpFileCollection;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\Variable;
use PhpParser\Node\Stmt;
use PhpParser\Node\Stmt\Case_;
use PhpParser\Node\Stmt\ClassMethod;

class SchemaMapperGenerator extends MutatorAccessorClassGeneratorAbstract
Expand Down Expand Up @@ -501,53 +503,7 @@ protected function generateMapStatementsForObject(Field $root, Variable $payload

if ($root->hasOneOf() || $root->hasAnyOf()) {
if ($root->getDiscriminator()) {
$ifCondition = $this->builder->funcCall('array_key_exists', [
/** @phpstan-ignore-next-line */
$root->getDiscriminator()->propertyName,
$payloadVariable,
]);

$payloadDiscriminator = $this->builder->getArrayItem(
$payloadVariable,
/** @phpstan-ignore-next-line */
$this->builder->val($root->getDiscriminator()->propertyName)
);

$assignMethodName = $this->builder->expr(
$this->builder->assign(
$this->builder->var('methodName'),
$this->builder->concat(
$this->builder->val('set'),
$this->builder->funcCall('ucfirst', [$payloadDiscriminator])
)
)
);

$assignMapperName = $this->builder->expr(
$this->builder->assign(
$this->builder->var('mapperName'),
$this->builder->concat(
$payloadDiscriminator,
$this->builder->val('Mapper')
)
)
);

$schemaMethodCall = $this->builder->expr(
$this->builder->methodCall(
$this->builder->var('schema'),
'$methodName',
[
$this->builder->methodCall(
$this->builder->localPropertyFetch('$mapperName'),
'toSchema',
[$payloadVariable]
),
]
)
);

$statements[] = $this->builder->if($ifCondition, [$assignMethodName, $assignMapperName, $schemaMethodCall]);
$statements[] = $this->generateDiscriminatorStatement($root, $payloadVariable);
} else {
$statements[] = $this->builder->assign($matchesVar, $this->builder->val(0));

Expand Down Expand Up @@ -597,4 +553,130 @@ protected function generateMapStatementsForObject(Field $root, Variable $payload

return $statements;
}

private function generateDiscriminatorStatement(Field $root, Variable $payloadVariable): Stmt
{
$discriminator = $root->getDiscriminator();

/** @phpstan-ignore-next-line */
$propertyName = $discriminator->propertyName;

$ifCondition = $this->builder->funcCall('array_key_exists', [
$propertyName,
$payloadVariable,
]);

$payloadDiscriminator = $this->builder->getArrayItem(
$payloadVariable,
$this->builder->val($propertyName)
);

$fallbackStatements = $this->generateDiscriminatorFallbackStatements($payloadDiscriminator, $payloadVariable);

/** @phpstan-ignore-next-line */
$mapping = $discriminator->mapping ?? [];
$cases = $this->generateDiscriminatorMappingCases($root, $mapping, $payloadVariable);

if ($cases === []) {
return $this->builder->if($ifCondition, $fallbackStatements);
}

$defaultStatements = [...$fallbackStatements, $this->builder->break()];
$cases[] = $this->builder->default(...$defaultStatements);

return $this->builder->if($ifCondition, [$this->builder->switch($payloadDiscriminator, ...$cases)]);
}

/**
* @return Stmt[]
*/
private function generateDiscriminatorFallbackStatements(Expr $payloadDiscriminator, Variable $payloadVariable): array
{
$assignMethodName = $this->builder->expr(
$this->builder->assign(
$this->builder->var('methodName'),
$this->builder->concat(
$this->builder->val('set'),
$this->builder->funcCall('ucfirst', [$payloadDiscriminator])
)
)
);

$assignMapperName = $this->builder->expr(
$this->builder->assign(
$this->builder->var('mapperName'),
$this->builder->concat(
$payloadDiscriminator,
$this->builder->val('Mapper')
)
)
);

$schemaMethodCall = $this->builder->expr(
$this->builder->methodCall(
$this->builder->var('schema'),
'$methodName',
[
$this->builder->methodCall(
$this->builder->localPropertyFetch('$mapperName'),
'toSchema',
[$payloadVariable]
),
]
)
);

return [$assignMethodName, $assignMapperName, $schemaMethodCall];
}

/**
* @param string[] $mapping
*
* @return Case_[]
*/
private function generateDiscriminatorMappingCases(Field $root, array $mapping, Variable $payloadVariable): array
{
$childrenByClassName = [];
foreach ($root->getObjectProperties() as $child) {
if ($child->isComposite()) {
$childrenByClassName[$child->getPhpClassName()] = $child;
}
}

$cases = [];
foreach ($mapping as $discriminatorValue => $reference) {
$schemaName = $this->resolveSchemaNameFromReference($reference);
if (!isset($childrenByClassName[$schemaName])) {
continue;
}

$child = $childrenByClassName[$schemaName];
$cases[] = $this->builder->case(
$this->builder->val((string)$discriminatorValue),
$this->builder->expr(
$this->builder->methodCall(
$this->builder->var('schema'),
$this->getSetMethodName($child),
[
$this->builder->methodCall(
$this->builder->localPropertyFetch(SchemaMapperNaming::getPropertyName($child)),
'toSchema',
[$payloadVariable]
),
]
)
),
$this->builder->break()
);
}

return $cases;
}

private function resolveSchemaNameFromReference(string $reference): string
{
$segments = explode('/', $reference);

return (string)end($segments);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,23 +14,36 @@

class GetExampleResponseBodyMapper implements SchemaMapperInterface
{
private AnimalMapper $animalMapper;
private AnimalResponseMapper $animalResponseMapper;

private MachineMapper $machineMapper;
private MachineResponseMapper $machineResponseMapper;

public function __construct(AnimalMapper $animalMapper, MachineMapper $machineMapper)
public function __construct(AnimalResponseMapper $animalResponseMapper, MachineResponseMapper $machineResponseMapper)
{
$this->animalMapper = $animalMapper;
$this->machineMapper = $machineMapper;
$this->animalResponseMapper = $animalResponseMapper;
$this->machineResponseMapper = $machineResponseMapper;
}

public function toSchema(array $payload): GetExampleResponseBody
{
$schema = new GetExampleResponseBody();
if (array_key_exists('objectType', $payload)) {
$methodName = 'set' . ucfirst($payload['objectType']);
$mapperName = $payload['objectType'] . 'Mapper';
$schema->$methodName($this->$mapperName->toSchema($payload));
switch ($payload['objectType']) {
case 'animal':
$schema->setAnimalResponse($this->animalResponseMapper->toSchema($payload));

break;
case 'machine':
$schema->setMachineResponse($this->machineResponseMapper->toSchema($payload));

break;
default:
$methodName = 'set' . ucfirst($payload['objectType']);
$mapperName = $payload['objectType'] . 'Mapper';
$schema->$methodName($this->$mapperName->toSchema($payload));

break;
}
}

return $schema;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,30 @@

class GetExampleResponseBodyMapper implements SchemaMapperInterface
{
public function __construct(private AnimalMapper $animalMapper, private MachineMapper $machineMapper)
public function __construct(private AnimalResponseMapper $animalResponseMapper, private MachineResponseMapper $machineResponseMapper)
{
}

public function toSchema(array $payload): GetExampleResponseBody
{
$schema = new GetExampleResponseBody();
if (array_key_exists('objectType', $payload)) {
$methodName = 'set' . ucfirst($payload['objectType']);
$mapperName = $payload['objectType'] . 'Mapper';
$schema->$methodName($this->$mapperName->toSchema($payload));
switch ($payload['objectType']) {
case 'animal':
$schema->setAnimalResponse($this->animalResponseMapper->toSchema($payload));

break;
case 'machine':
$schema->setMachineResponse($this->machineResponseMapper->toSchema($payload));

break;
default:
$methodName = 'set' . ucfirst($payload['objectType']);
$mapperName = $payload['objectType'] . 'Mapper';
$schema->$methodName($this->$mapperName->toSchema($payload));

break;
}
}

return $schema;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,30 @@

class GetExampleResponseBodyMapper implements SchemaMapperInterface
{
public function __construct(private readonly AnimalMapper $animalMapper, private readonly MachineMapper $machineMapper)
public function __construct(private readonly AnimalResponseMapper $animalResponseMapper, private readonly MachineResponseMapper $machineResponseMapper)
{
}

public function toSchema(array $payload): GetExampleResponseBody
{
$schema = new GetExampleResponseBody();
if (array_key_exists('objectType', $payload)) {
$methodName = 'set' . ucfirst($payload['objectType']);
$mapperName = $payload['objectType'] . 'Mapper';
$schema->$methodName($this->$mapperName->toSchema($payload));
switch ($payload['objectType']) {
case 'animal':
$schema->setAnimalResponse($this->animalResponseMapper->toSchema($payload));

break;
case 'machine':
$schema->setMachineResponse($this->machineResponseMapper->toSchema($payload));

break;
default:
$methodName = 'set' . ucfirst($payload['objectType']);
$mapperName = $payload['objectType'] . 'Mapper';
$schema->$methodName($this->$mapperName->toSchema($payload));

break;
}
}

return $schema;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

declare(strict_types=1);

/*
* This file was generated by docler-labs/api-client-generator.
*
* Do not edit it manually.
*/

namespace Test\Schema\Mapper;

use Test\Schema\GetExampleResponseBody;

class GetExampleResponseBodyMapper implements SchemaMapperInterface
{
public function __construct(private readonly AnimalMapper $animalMapper, private readonly MachineMapper $machineMapper)
{
}

public function toSchema(array $payload): GetExampleResponseBody
{
$schema = new GetExampleResponseBody();
if (array_key_exists('objectType', $payload)) {
$methodName = 'set' . ucfirst($payload['objectType']);
$mapperName = $payload['objectType'] . 'Mapper';
$schema->$methodName($this->$mapperName->toSchema($payload));
}

return $schema;
}
}
Loading
Loading