Skip to content

Commit

Permalink
Do not assume scalar values are string, use mixed
Browse files Browse the repository at this point in the history
  • Loading branch information
spawnia committed Oct 9, 2023
1 parent 497f1f0 commit 0484160
Show file tree
Hide file tree
Showing 11 changed files with 210 additions and 19 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## Unreleased

## v0.29.2

### Fixed

- Do not assume scalar values are `string`, use `mixed`

## v0.29.1

### Fixed
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,8 @@ HelloSailor::setClient(null);

### Custom types

Custom scalars are commonly serialized as strings. Without knowing about the contents of the type,
Sailor can not do any conversions or provide more accurate type hints, so it uses `string`.
Custom scalars are commonly serialized as strings, but may also use other representations.
Without knowing about the contents of the type, Sailor can not do any conversions or provide more accurate type hints, so it uses `mixed`.

Enums are only supported from PHP 8.1. Many projects simply used scalar values or an implementation
that approximates enums through some kind of value class. Sailor is not opinionated and generates
Expand Down
48 changes: 48 additions & 0 deletions examples/custom-types/expected/Operations/MyDefaultDateQuery.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php

declare(strict_types=1);

namespace Spawnia\Sailor\CustomTypes\Operations;

/**
* @extends \Spawnia\Sailor\Operation<\Spawnia\Sailor\CustomTypes\Operations\MyDefaultDateQuery\MyDefaultDateQueryResult>
*/
class MyDefaultDateQuery extends \Spawnia\Sailor\Operation
{
/**
* @param mixed $value
*/
public static function execute($value): MyDefaultDateQuery\MyDefaultDateQueryResult
{
return self::executeOperation(
$value,
);
}

protected static function converters(): array
{
static $converters;

return $converters ??= [
['value', new \Spawnia\Sailor\Convert\NonNullConverter(new \Spawnia\Sailor\Convert\ScalarConverter)],
];
}

public static function document(): string
{
return /* @lang GraphQL */ 'query MyDefaultDateQuery($value: DefaultDate!) {
__typename
withDefaultDate(value: $value)
}';
}

public static function endpoint(): string
{
return 'custom-types';
}

public static function config(): string
{
return \Safe\realpath(__DIR__ . '/../../sailor.php');
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

declare(strict_types=1);

namespace Spawnia\Sailor\CustomTypes\Operations\MyDefaultDateQuery;

/**
* @property mixed $withDefaultDate
* @property string $__typename
*/
class MyDefaultDateQuery extends \Spawnia\Sailor\ObjectLike
{
/**
* @param mixed $withDefaultDate
*/
public static function make($withDefaultDate): self
{
$instance = new self;

if ($withDefaultDate !== self::UNDEFINED) {
$instance->withDefaultDate = $withDefaultDate;
}
$instance->__typename = 'Query';

return $instance;
}

protected function converters(): array
{
static $converters;

return $converters ??= [
'withDefaultDate' => new \Spawnia\Sailor\Convert\NonNullConverter(new \Spawnia\Sailor\Convert\ScalarConverter),
'__typename' => new \Spawnia\Sailor\Convert\NonNullConverter(new \Spawnia\Sailor\Convert\StringConverter),
];
}

public static function endpoint(): string
{
return 'custom-types';
}

public static function config(): string
{
return \Safe\realpath(__DIR__ . '/../../../sailor.php');
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

declare(strict_types=1);

namespace Spawnia\Sailor\CustomTypes\Operations\MyDefaultDateQuery;

class MyDefaultDateQueryErrorFreeResult extends \Spawnia\Sailor\ErrorFreeResult
{
public MyDefaultDateQuery $data;

public static function endpoint(): string
{
return 'custom-types';
}

public static function config(): string
{
return \Safe\realpath(__DIR__ . '/../../../sailor.php');
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

declare(strict_types=1);

namespace Spawnia\Sailor\CustomTypes\Operations\MyDefaultDateQuery;

class MyDefaultDateQueryResult extends \Spawnia\Sailor\Result
{
public ?MyDefaultDateQuery $data = null;

protected function setData(\stdClass $data): void
{
$this->data = MyDefaultDateQuery::fromStdClass($data);
}

/**
* Useful for instantiation of successful mocked results.
*
* @return static
*/
public static function fromData(MyDefaultDateQuery $data): self
{
$instance = new static;
$instance->data = $data;

return $instance;
}

public function errorFree(): MyDefaultDateQueryErrorFreeResult
{
return MyDefaultDateQueryErrorFreeResult::fromResult($this);
}

public static function endpoint(): string
{
return 'custom-types';
}

public static function config(): string
{
return \Safe\realpath(__DIR__ . '/../../../sailor.php');
}
}
1 change: 1 addition & 0 deletions examples/custom-types/schema.graphql
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
type Query {
withDefaultDate(value: DefaultDate!): DefaultDate!
withDefaultEnum(value: DefaultEnum!): DefaultEnum!
withCustomEnum(value: CustomEnum): CustomEnum
withBenSampoEnum(value: BenSampoEnum): BenSampoEnum
Expand Down
3 changes: 3 additions & 0 deletions examples/custom-types/src/withDefaultDate.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
query MyDefaultDateQuery($value: DefaultDate!) {
withDefaultDate(value: $value)
}
21 changes: 5 additions & 16 deletions src/Convert/ScalarConverter.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,17 @@

namespace Spawnia\Sailor\Convert;

/** Does no conversion, because custom scalars are opaque without further knowledge and bespoke implementations. */
class ScalarConverter implements TypeConverter
{
public function fromGraphQL($value): string
public function fromGraphQL($value)
{
return $this->toString($value);
}

public function toGraphQL($value): string
{
return $this->toString($value);
return $value;
}

/**
* @param mixed $value Should be string
*/
protected function toString($value): string
public function toGraphQL($value)
{
if (! is_string($value)) {
$notString = gettype($value);
throw new \InvalidArgumentException("Expected string, got {$notString}");
}

// @phpstan-ignore-next-line Assume the developer is passing a valid value, json_encode() will crash otherwise
return $value;
}
}
4 changes: 3 additions & 1 deletion src/Type/ScalarTypeConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ public function typeConverter(): string

protected function typeReference(): string
{
return 'string';
// While typically serialized as a string, custom scalars may use other data types.
// See https://spec.graphql.org/draft/#sec-Scalars.Custom-Scalars.
return 'mixed';
}

public function inputTypeReference(): string
Expand Down
32 changes: 32 additions & 0 deletions tests/Integration/CustomTypesTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use Spawnia\Sailor\Configuration;
use Spawnia\Sailor\CustomTypes\Operations\MyBenSampoEnumQuery;
use Spawnia\Sailor\CustomTypes\Operations\MyCustomEnumQuery;
use Spawnia\Sailor\CustomTypes\Operations\MyDefaultDateQuery;
use Spawnia\Sailor\CustomTypes\Operations\MyDefaultEnumQuery;
use Spawnia\Sailor\CustomTypes\Operations\MyEnumInputQuery;
use Spawnia\Sailor\CustomTypes\Types\BenSampoEnum;
Expand All @@ -18,6 +19,37 @@

final class CustomTypesTest extends TestCase
{
/**
* @dataProvider validScalarValues
*
* @param mixed $value Can be any JSON encodable value
*/
public function testDefaultDateAcceptsMixedScalarValues($value): void
{
MyDefaultDateQuery::mock()
->expects('execute')
->once()
->with($value)
->andReturn(MyDefaultDateQuery\MyDefaultDateQueryResult::fromData(
MyDefaultDateQuery\MyDefaultDateQuery::make(
/* withDefaultDate: */
$value,
)
));

$result = MyDefaultDateQuery::execute($value)->errorFree();
self::assertSame($value, $result->data->withDefaultDate);
}

/** @return iterable<array{mixed}> */
public static function validScalarValues(): iterable
{
yield [1];
yield ['1'];
yield [['1', 1]];
yield [(object) ['a' => 1]];
}

public function testDefaultEnum(): void
{
$value = DefaultEnum::A;
Expand Down

0 comments on commit 0484160

Please sign in to comment.