FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

Add nullable and discriminatedUnion field encoding coercion by jar-stripe · Pull Request #2113 · stripe/stripe-php · GitHub

Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension .php  (3) All 1 file type selected
Viewed files
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Unified
Split
Hide whitespace
Diff view
Unified
Split
Hide whitespace
44 changes: 39 additions & 5 deletions lib/Util/Int64.php
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,16 @@
namespace Stripe\Util;

/**
* Handles coercion between PHP int and JSON string for int64_string fields.
* Handles coercion for V2 API fields with special wire encoding.
*
* V2 API fields marked as int64_string are transmitted as JSON strings on
* the wire but exposed as PHP ints in the SDK.
* V2 API fields may require coercion between PHP native types and their
* wire representations (e.g. int64_string, decimal_string), and may be
* wrapped in nullable or discriminatedUnion schemas.
*/
class Int64
{
/**
* Coerce outbound request params: convert PHP ints to strings where
* the request schema indicates an int64_string field.
* Coerce outbound request params according to the field's wire schema.
*
* @param mixed $params
* @param array $schema e.g. ['kind' => 'object', 'fields' => ['amount' => ['kind' => 'int64_string']]]
Expand All @@ -37,6 +37,29 @@ public static function coerceRequestParams($params, $schema)
return $params;
}

if ('decimal_string' === $schema['kind']) {
if (\is_float($params) || \is_int($params)) {
return (string) $params;
}

return $params;
}

if ('nullable' === $schema['kind'] && isset($schema['inner'])) {
return self::coerceRequestParams($params, $schema['inner']);
}

if ('discriminatedUnion' === $schema['kind'] && isset($schema['discriminator'], $schema['variants'])) {
if (\is_array($params) && \array_key_exists($schema['discriminator'], $params)) {
$discriminatorValue = $params[$schema['discriminator']];
if (\is_string($discriminatorValue) && \array_key_exists($discriminatorValue, $schema['variants'])) {
return self::coerceRequestParams($params, $schema['variants'][$discriminatorValue]);
}
}
Comment on lines +52 to +58

return $params;
}

if ('array' === $schema['kind'] && isset($schema['items'])) {
if (\is_array($params)) {
$result = [];
Expand Down Expand Up @@ -98,6 +121,17 @@ public static function coerceResponseValues($values, $encodings)
if (\is_string($value) && \is_numeric($value)) {
$values[$field] = (int) $value;
}
} elseif ('nullable' === $encoding['kind'] && isset($encoding['inner'])) {
if (null !== $value) {
$values = self::coerceResponseValues($values, [$field => $encoding['inner']]);
}
} elseif ('discriminatedUnion' === $encoding['kind'] && isset($encoding['discriminator'], $encoding['variants'])) {
if (\is_array($value) && \array_key_exists($encoding['discriminator'], $value)) {
$discriminatorValue = $value[$encoding['discriminator']];
if (\is_string($discriminatorValue) && \array_key_exists($discriminatorValue, $encoding['variants'])) {
$values = self::coerceResponseValues($values, [$field => $encoding['variants'][$discriminatorValue]]);
}
}
Comment on lines +128 to +134
} elseif ('array' === $encoding['kind'] && isset($encoding['items'])) {
if (\is_array($value)) {
foreach ($value as $i => $item) {
Expand Down
308 changes: 308 additions & 0 deletions tests/Stripe/DiscriminatedUnionTest.php
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
@@ -0,0 +1,308 @@
<?php

namespace Stripe;

/**
* Tests for discriminated union serialization.
*
* Stripe APIs use discriminated unions where a literal discriminator field
* (e.g. `model`) selects which variant of a union type is being used.
*
* Request-side: PHP users pass plain arrays; the discriminator is just a
* regular key in the nested array.
*
* Response-side: The API returns an object whose fields include the
* discriminator; stripe-php deserializes it into a StripeObject.
*
* @internal
*
* @covers \Stripe\StripeObject
* @covers \Stripe\Util\Util
*/
final class DiscriminatedUnionTest extends TestCase
{
// -------------------------------------------------------------------------
// Request-side: array params structure
// -------------------------------------------------------------------------

/**
* Standalone union: the discriminated union value is a nested associative
* array where one of the keys is the discriminator literal.
*
* Example: setting a color with model = 'rgb' and component values.
*/
public function testStandaloneUnionParamsStructure()
{
$params = [
'color' => [
'model' => 'rgb',
'r' => 255,
'g' => 128,
'b' => 0,
],
'name' => 'sunset',
];

// The discriminator 'model' must be present in the nested array.
self::assertArrayHasKey('model', $params['color']);
self::assertSame('rgb', $params['color']['model']);

// Variant-specific fields are siblings of the discriminator.
self::assertSame(255, $params['color']['r']);
self::assertSame(128, $params['color']['g']);
self::assertSame(0, $params['color']['b']);

// The parent level has the wrapper key and other sibling fields.
self::assertArrayHasKey('color', $params);
self::assertArrayHasKey('name', $params);
self::assertSame('sunset', $params['name']);
}

/**
* Inline union: the discriminator lives at the parent level and the
* variant-specific fields are in a nested object keyed by the variant name.
*
* Example: payment method type = 'card' with card-specific details.
*/
public function testInlineUnionParamsStructure()
{
$params = [
'type' => 'card',
'card' => [
'number' => '4242424242424242',
'exp_month' => 12,
'exp_year' => 2026,
'cvc' => '123',
],
];

// The discriminator is a sibling of the variant data.
self::assertSame('card', $params['type']);

// Variant fields are nested under their own key.
self::assertArrayHasKey('card', $params);
self::assertSame('4242424242424242', $params['card']['number']);
self::assertSame(12, $params['card']['exp_month']);
self::assertSame(2026, $params['card']['exp_year']);
self::assertSame('123', $params['card']['cvc']);
}

// -------------------------------------------------------------------------
// Request encoding: discriminator survives form-encoding
// -------------------------------------------------------------------------

/**
* Verifies that a discriminated union array encodes correctly to form
* params. The discriminator and all variant fields must appear as nested
* bracket-notation keys (e.g. color[model]=rgb&color[r]=255).
*/
public function testStandaloneUnionEncodesToFormParams()
{
$params = [
'color' => [
'model' => 'rgb',
'r' => 255,
'g' => 128,
'b' => 0,
],
];

$encoded = Util\Util::encodeParameters($params);

// The discriminator must be present in the encoded output.
self::assertStringContainsString('color[model]=rgb', $encoded);

// Variant fields must also be encoded with the same prefix.
self::assertStringContainsString('color[r]=255', $encoded);
self::assertStringContainsString('color[g]=128', $encoded);
self::assertStringContainsString('color[b]=0', $encoded);
}

/**
* Verifies that an inline-discriminated union encodes correctly.
* The discriminator at the top level encodes as a plain key, while the
* variant-specific nested object encodes with bracket notation.
*/
public function testInlineUnionEncodesToFormParams()
{
$params = [
'type' => 'card',
'card' => [
'number' => '4242424242424242',
'exp_month' => 12,
],
];

$encoded = Util\Util::encodeParameters($params);

// Top-level discriminator encodes as a plain key.
self::assertStringContainsString('type=card', $encoded);

// Variant fields encode with bracket notation.
self::assertStringContainsString('card[number]=4242424242424242', $encoded);
self::assertStringContainsString('card[exp_month]=12', $encoded);
}

/**
* Verifies that flattenParams produces the correct key-value pairs for a
* discriminated union, preserving both discriminator and variant fields.
*/
public function testFlattenParamsPreservesDiscriminator()
{
$params = [
'color' => [
'model' => 'rgb',
'r' => 255,
'g' => 0,
'b' => 0,
],
];

$flat = Util\Util::flattenParams($params);

// Build a map of key => value from the flattened list for easy lookup.
$flatMap = [];
foreach ($flat as [$key, $value]) {
$flatMap[$key] = $value;
}

self::assertArrayHasKey('color[model]', $flatMap);
self::assertSame('rgb', $flatMap['color[model]']);
self::assertSame(255, $flatMap['color[r]']);
self::assertSame(0, $flatMap['color[g]']);
self::assertSame(0, $flatMap['color[b]']);
}

// -------------------------------------------------------------------------
// Response-side: StripeObject deserialization
// -------------------------------------------------------------------------

/**
* Verifies that a discriminated union value returned by the API is
* correctly deserialized into a StripeObject whose fields — including the
* discriminator — are accessible.
*/
public function testResponseDeserializationStandaloneUnion()
{
$obj = Util\Util::convertToStripeObject(
['model' => 'rgb', 'r' => 255, 'g' => 128, 'b' => 0],
null
);

self::assertInstanceOf(StripeObject::class, $obj);
self::assertSame('rgb', $obj->model); // @phpstan-ignore-line
self::assertSame(255, $obj->r); // @phpstan-ignore-line
self::assertSame(128, $obj->g); // @phpstan-ignore-line
self::assertSame(0, $obj->b); // @phpstan-ignore-line
}

/**
* Verifies array-syntax access works the same as property access for
* discriminated union response objects.
*/
public function testResponseDeserializationArrayAccess()
{
$obj = Util\Util::convertToStripeObject(
['model' => 'rgb', 'r' => 255, 'g' => 128, 'b' => 0],
null
);

self::assertInstanceOf(StripeObject::class, $obj);
self::assertSame('rgb', $obj['model']);
self::assertSame(255, $obj['r']);
}

/**
* Verifies that an inline-discriminated union response is deserialized
* correctly: the discriminator and all variant-specific fields are
* accessible on the parent object.
*/
public function testResponseDeserializationInlineUnion()
{
$obj = Util\Util::convertToStripeObject(
[
'id' => 'pm_123',
'object' => 'payment_method',
'type' => 'card',
'card' => [
'brand' => 'visa',
'last4' => '4242',
'exp_month' => 12,
'exp_year' => 2026,
],
],
null
);

self::assertInstanceOf(StripeObject::class, $obj);
// Discriminator is directly accessible.
self::assertSame('card', $obj->type); // @phpstan-ignore-line

// Nested variant data is deserialized into a StripeObject.
self::assertInstanceOf(StripeObject::class, $obj->card); // @phpstan-ignore-line
self::assertSame('visa', $obj->card->brand); // @phpstan-ignore-line
self::assertSame('4242', $obj->card->last4); // @phpstan-ignore-line
self::assertSame(12, $obj->card->exp_month); // @phpstan-ignore-line
self::assertSame(2026, $obj->card->exp_year); // @phpstan-ignore-line
}

/**
* Verifies that a response with multiple discriminated union variants
* at different levels is fully deserialized.
*/
public function testResponseDeserializationNestedUnion()
{
$obj = Util\Util::convertToStripeObject(
[
'name' => 'sunset',
'color' => [
'model' => 'rgb',
'r' => 255,
'g' => 128,
'b' => 0,
],
],
null
);

self::assertInstanceOf(StripeObject::class, $obj);
self::assertSame('sunset', $obj->name); // @phpstan-ignore-line

// Nested discriminated union is also a StripeObject.
self::assertInstanceOf(StripeObject::class, $obj->color); // @phpstan-ignore-line
self::assertSame('rgb', $obj->color->model); // @phpstan-ignore-line
self::assertSame(255, $obj->color->r); // @phpstan-ignore-line
self::assertSame(128, $obj->color->g); // @phpstan-ignore-line
self::assertSame(0, $obj->color->b); // @phpstan-ignore-line
}

// -------------------------------------------------------------------------
// Round-trip: serialized request includes discriminator
// -------------------------------------------------------------------------

/**
* Verifies that when params containing a discriminated union are passed
* through objectsToIds (the pre-encoding step), the discriminator and
* variant fields are preserved intact.
*/
public function testObjectsToIdsPreservesDiscriminatorFields()
{
$params = [
'color' => [
'model' => 'rgb',
'r' => 255,
'g' => 128,
'b' => 0,
],
'name' => 'sunset',
];

$result = Util\Util::objectsToIds($params, false);

self::assertArrayHasKey('color', $result);
self::assertArrayHasKey('model', $result['color']);
self::assertSame('rgb', $result['color']['model']);
self::assertSame(255, $result['color']['r']);
self::assertSame('sunset', $result['name']);
}
}
Loading
Loading

Back | FazBrowse Home | New Git URL