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
10 changes: 5 additions & 5 deletions codegen/pkg/generator/collect.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ func (g *Generator) registerSchemaUsage(schema *base.SchemaProxy, suggestedName

name := g.classNameForSchema(schema)
if name == "" {
if suggestedName == "" || !schemaIsObject(schema) || schemaIsAdditionalPropertiesOnly(schema) {
if suggestedName == "" || !schemaShouldGenerateClass(schema) {
return ""
}
name = suggestedName
Expand Down Expand Up @@ -164,8 +164,8 @@ func (g *Generator) assignSchemasToTags(usage map[string]*schemaUsage) (map[stri
for schemaName, info := range usage {
targetTag := typesTagKey

// Skip schemas that are additionalProperties-only (they'll be treated as arrays)
if schemaIsAdditionalPropertiesOnly(info.schema) {
// Skip schemas that should remain generic maps in PHP.
if !schemaShouldGenerateClass(info.schema) {
continue
}

Expand Down Expand Up @@ -209,7 +209,7 @@ func (g *Generator) inlinePropertyClassName(parentName string, propertyName stri
return ""
}

if !schemaIsObject(schema) || schemaIsAdditionalPropertiesOnly(schema) {
if !schemaShouldGenerateClass(schema) {
return ""
}

Expand All @@ -225,7 +225,7 @@ func (g *Generator) inlineArrayItemClassName(parentName string, schema *base.Sch
return ""
}

if !schemaIsObject(schema) || schemaIsAdditionalPropertiesOnly(schema) {
if !schemaShouldGenerateClass(schema) {
return ""
}

Expand Down
6 changes: 5 additions & 1 deletion codegen/pkg/generator/operations.go
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,10 @@ func (g *Generator) buildResponseType(schema *base.SchemaProxy, currentNamespace
return g.buildResponseTypeFromSpec(schema.Schema(), currentNamespace)
}

if !schemaShouldGenerateClass(schema) {
return &responseType{Kind: responseTypeObject}
}

name := schemaClassName(schema)
namespace := g.schemaNamespaces[name]
typeName := name
Expand Down Expand Up @@ -342,7 +346,7 @@ func (g *Generator) buildResponseType(schema *base.SchemaProxy, currentNamespace
}
}

if hasSchemaType(spec, "object") && inlineBaseName != "" {
if hasSchemaType(spec, "object") && inlineBaseName != "" && schemaShouldGenerateClass(schema) {
return &responseType{
Kind: responseTypeClass,
ClassName: fmt.Sprintf("\\SumUp\\Services\\%s", inlineBaseName),
Expand Down
5 changes: 2 additions & 3 deletions codegen/pkg/generator/properties.go
Original file line number Diff line number Diff line change
Expand Up @@ -205,8 +205,7 @@ func (g *Generator) resolvePHPType(schema *base.SchemaProxy, currentNamespace st
return g.resolvePHPTypeFromSpec(schema, schema.Schema(), currentNamespace, parentSchemaName, propertyName)
}

// Check if this is an additionalProperties-only schema - treat as array
if schemaIsAdditionalPropertiesOnly(schema) {
if !schemaShouldGenerateClass(schema) {
return "array", "array<string, mixed>"
}

Expand Down Expand Up @@ -267,7 +266,7 @@ func (g *Generator) resolvePHPTypeFromSpec(schema *base.SchemaProxy, spec *base.
}
return "array", itemDoc + "[]"
case hasSchemaType(spec, "object"):
if schema != nil && !schemaIsAdditionalPropertiesOnly(schema) {
if schema != nil && schemaShouldGenerateClass(schema) {
typeName := g.classNameForSchema(schema)
if typeName == "" && parentSchemaName != "" && propertyName != "" {
typeName = phpInlineObjectName(parentSchemaName, propertyName)
Expand Down
54 changes: 54 additions & 0 deletions codegen/pkg/generator/schemas.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,60 @@ func schemaIsAdditionalPropertiesOnly(schema *base.SchemaProxy) bool {
return true
}

// schemaHasDeclaredProperties reports whether the schema exposes at least one
// explicit property directly or through allOf/oneOf/anyOf composition.
func schemaHasDeclaredProperties(schema *base.SchemaProxy) bool {
return schemaHasDeclaredPropertiesWithStack(schema, make(map[*base.SchemaProxy]struct{}))
}

// schemaShouldGenerateClass reports whether the schema should become a DTO
// class instead of a generic map. Bare object schemas and
// additionalProperties-only schemas stay as arrays in the PHP SDK.
func schemaShouldGenerateClass(schema *base.SchemaProxy) bool {
return schemaIsObject(schema) && schemaHasDeclaredProperties(schema) && !schemaIsAdditionalPropertiesOnly(schema)
}

func schemaHasDeclaredPropertiesWithStack(schema *base.SchemaProxy, stack map[*base.SchemaProxy]struct{}) bool {
if schema == nil {
return false
}

if _, ok := stack[schema]; ok {
return false
}
stack[schema] = struct{}{}
defer delete(stack, schema)

spec := schema.Schema()
if spec == nil {
return false
}

if spec.Properties != nil && spec.Properties.Len() > 0 {
return true
}

for _, composite := range spec.AllOf {
if schemaHasDeclaredPropertiesWithStack(composite, stack) {
return true
}
}

for _, composite := range spec.AnyOf {
if schemaHasDeclaredPropertiesWithStack(composite, stack) {
return true
}
}

for _, composite := range spec.OneOf {
if schemaHasDeclaredPropertiesWithStack(composite, stack) {
return true
}
}

return false
}

func schemaIsObjectWithStack(schema *base.SchemaProxy, stack map[*base.SchemaProxy]struct{}) bool {
if schema == nil {
return false
Expand Down
2 changes: 1 addition & 1 deletion codegen/pkg/generator/services.go
Original file line number Diff line number Diff line change
Expand Up @@ -453,7 +453,7 @@ func shouldGenerateRequestBodyClass(op *operation) bool {
return false
}

return !schemaIsAdditionalPropertiesOnly(op.BodySchema)
return schemaShouldGenerateClass(op.BodySchema)
}

func buildEmptyRequestBodyClass(className string) string {
Expand Down
12 changes: 5 additions & 7 deletions src/Checkouts/Checkouts.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,6 @@ public function __construct(array $data = [])

}

class CheckoutsCreateApplePaySessionResponse
{
}

class CheckoutsListAvailablePaymentMethodsResponse
{
/**
Expand Down Expand Up @@ -194,13 +190,13 @@ public function create(\SumUp\Types\CheckoutCreateRequest|array $body, ?RequestO
* @param CheckoutsCreateApplePaySessionRequest|array<string, mixed>|null $body Optional request payload
* @param RequestOptions|null $requestOptions Optional typed request options
*
* @return \SumUp\Services\CheckoutsCreateApplePaySessionResponse
* @return array<string, mixed>
* @throws \SumUp\Exception\ApiException
* @throws \SumUp\Exception\UnexpectedApiException
* @throws \SumUp\Exception\ConnectionException
* @throws \SumUp\Exception\SDKException
*/
public function createApplePaySession(string $id, CheckoutsCreateApplePaySessionRequest|array|null $body = null, ?RequestOptions $requestOptions = null): \SumUp\Services\CheckoutsCreateApplePaySessionResponse
public function createApplePaySession(string $id, CheckoutsCreateApplePaySessionRequest|array|null $body = null, ?RequestOptions $requestOptions = null): array
{
$path = sprintf('/v0.2/checkouts/%s/apple-pay-session', rawurlencode((string) $id));
$payload = [];
Expand All @@ -213,7 +209,9 @@ public function createApplePaySession(string $id, CheckoutsCreateApplePaySession

$response = $this->client->send('PUT', $path, $payload, $headers, $requestOptions);

return ResponseDecoder::decodeOrThrow($response, \SumUp\Services\CheckoutsCreateApplePaySessionResponse::class, [
return ResponseDecoder::decodeOrThrow($response, [
'200' => ['type' => 'object'],
], [
'400' => ['type' => 'mixed'],
'404' => ['type' => 'class', 'class' => \SumUp\Types\Error::class],
], 'PUT', $path);
Expand Down
10 changes: 9 additions & 1 deletion src/ResponseDecoder.php
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,15 @@ private static function castValue($value, array $descriptor)
case 'scalar':
return self::castScalar($value, isset($descriptor['scalar']) ? $descriptor['scalar'] : 'mixed');
case 'object':
return is_array($value) ? $value : [];
if (is_array($value)) {
return $value;
}

if (is_object($value)) {
return get_object_vars($value);
}

return [];
case 'void':
return null;
case 'mixed':
Expand Down
8 changes: 4 additions & 4 deletions src/Types/ProcessCheckout.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,16 +40,16 @@ class ProcessCheckout
/**
* Raw `PaymentData` object received from Google Pay. Send the Google Pay response payload as-is.
*
* @var ProcessCheckoutGooglePay|null
* @var array<string, mixed>|null
*/
public ?ProcessCheckoutGooglePay $googlePay = null;
public ?array $googlePay = null;

/**
* Raw payment token object received from Apple Pay. Send the Apple Pay response payload as-is.
*
* @var ProcessCheckoutApplePay|null
* @var array<string, mixed>|null
*/
public ?ProcessCheckoutApplePay $applePay = null;
public ?array $applePay = null;

/**
* Saved-card token to use instead of raw card details when processing with a previously stored payment instrument.
Expand Down
12 changes: 0 additions & 12 deletions src/Types/ProcessCheckoutApplePay.php

This file was deleted.

12 changes: 0 additions & 12 deletions src/Types/ProcessCheckoutGooglePay.php

This file was deleted.

4 changes: 2 additions & 2 deletions src/Types/Receipt.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ class Receipt
/**
* EMV-specific metadata returned for card-present payments.
*
* @var ReceiptEmvData|null
* @var array<string, mixed>|null
*/
public ?ReceiptEmvData $emvData = null;
public ?array $emvData = null;

/**
* Acquirer-specific metadata related to the card authorization.
Expand Down
12 changes: 0 additions & 12 deletions src/Types/ReceiptEmvData.php

This file was deleted.

18 changes: 18 additions & 0 deletions tests/HydratorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use SumUp\Types\CheckoutCurrency;
use SumUp\Types\MandateResponse;
use SumUp\Types\MandateResponseStatus;
use SumUp\Types\ProcessCheckout;

class HydratorTest extends TestCase
{
Expand Down Expand Up @@ -46,6 +47,23 @@ public function testHydrateNestedObjectWithNormalizedPropertyNames()
$this->assertSame(MandateResponseStatus::ACTIVE, $checkout->mandate->status);
}

public function testHydrateOpaqueObjectSchemaAsArray()
{
$checkout = Hydrator::hydrate([
'apple_pay' => [
'token' => [
'paymentData' => [
'version' => 'EC_v1',
],
],
],
], ProcessCheckout::class);

$this->assertInstanceOf(ProcessCheckout::class, $checkout);
$this->assertIsArray($checkout->applePay);
$this->assertSame('EC_v1', $checkout->applePay['token']['paymentData']['version']);
}

public function testHydrateArrayItemsFromDocblockClassNames()
{
$response = Hydrator::hydrate([
Expand Down
22 changes: 22 additions & 0 deletions tests/ResponseDecoderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,28 @@ public function testDecodeOrThrowDecodesSuccessArrayDescriptorWithClassItems()
$this->assertSame('ONE', $result[0]->errorCode);
}

public function testDecodeOrThrowDecodesOpaqueObjectDescriptorFromStdClass()
{
$response = new Response(200, (object) [
'merchantSessionIdentifier' => 'session_123',
'nested' => (object) ['value' => 'ok'],
]);

$result = ResponseDecoder::decodeOrThrow(
$response,
[
'200' => ['type' => 'object'],
],
null,
'POST',
'/v0.1/checkouts/chk_123/create_apple_pay_session'
);

$this->assertIsArray($result);
$this->assertSame('session_123', $result['merchantSessionIdentifier']);
$this->assertInstanceOf(\stdClass::class, $result['nested']);
}

public function testDecodeOrThrowUsesUnexpectedFallbackForUnknownStatusWithNestedPayload()
{
$rawBody = [
Expand Down
Loading