Skip to content

Commit d451d90

Browse files
author
Sean O'Brien
committed
feat: add cbor protocol
1 parent ca7f6fe commit d451d90

28 files changed

+9632
-51
lines changed

src/Api/DateTimeResult.php

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,6 @@ public static function fromEpoch($unixTimestamp)
3030
throw new ParserException('Invalid timestamp value passed to DateTimeResult::fromEpoch');
3131
}
3232

33-
// PHP 5.5 does not support sub-second precision
34-
if (\PHP_VERSION_ID < 56000) {
35-
return new self(gmdate('c', $unixTimestamp));
36-
}
37-
3833
$decimalSeparator = isset(localeconv()['decimal_point']) ? localeconv()['decimal_point'] : ".";
3934
$formatString = "U" . $decimalSeparator . "u";
4035
$dateTime = DateTime::createFromFormat(
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
<?php
2+
namespace Aws\Api\ErrorParser;
3+
4+
use Aws\Api\Service;
5+
use Aws\Api\StructureShape;
6+
use Aws\CommandInterface;
7+
use Psr\Http\Message\ResponseInterface;
8+
use Psr\Http\Message\StreamInterface;
9+
10+
/**
11+
* Base implementation for Smithy RPC V2 protocol error parsers.
12+
*
13+
* @internal
14+
*/
15+
abstract class AbstractRpcV2ErrorParser extends AbstractErrorParser
16+
{
17+
private const HEADER_QUERY_ERROR = 'x-amzn-query-error';
18+
private const HEADER_ERROR_TYPE = 'x-amzn-errortype';
19+
private const HEADER_REQUEST_ID = 'x-amzn-requestid';
20+
21+
/**
22+
* @param ResponseInterface $response
23+
* @param CommandInterface|null $command
24+
*
25+
* @return array
26+
*/
27+
public function __invoke(
28+
ResponseInterface $response,
29+
?CommandInterface $command = null
30+
) {
31+
$data = $this->parseError($response);
32+
if ($data['parsed']) {
33+
$data['parsed'] = array_change_key_case($data['parsed']);
34+
}
35+
36+
if (isset($data['parsed']['__type'])) {
37+
$data['code'] ??= $this->extractErrorCode($data['parsed']['__type']);
38+
$data['message'] = $data['parsed']['message'] ?? null;
39+
}
40+
41+
$this->populateShape($data, $response, $command);
42+
43+
return $data;
44+
}
45+
46+
/**
47+
* @param ResponseInterface $response
48+
* @param StructureShape $member
49+
*
50+
* @return array
51+
*/
52+
abstract protected function payload(
53+
ResponseInterface $response,
54+
StructureShape $member
55+
): array;
56+
57+
/**
58+
* @param StreamInterface $body
59+
* @param ResponseInterface $response
60+
*
61+
* @return mixed
62+
*/
63+
abstract protected function parseBody(
64+
StreamInterface $body,
65+
ResponseInterface $response
66+
): mixed;
67+
68+
/**
69+
* @param ResponseInterface $response
70+
*
71+
* @return array
72+
*/
73+
private function parseError(ResponseInterface $response): array
74+
{
75+
$statusCode = (string) $response->getStatusCode();
76+
$errorCode = null;
77+
$errorType = null;
78+
79+
if ($this->api?->getMetadata('awsQueryCompatible') !== null
80+
&& $response->hasHeader(self::HEADER_QUERY_ERROR)
81+
&& $awsQueryError = $this->parseQueryCompatibleHeader($response)
82+
) {
83+
$errorCode = $awsQueryError['code'];
84+
$errorType = $awsQueryError['type'];
85+
}
86+
87+
if (!$errorCode && $response->hasHeader(self::HEADER_ERROR_TYPE)) {
88+
$errorCode = $this->extractErrorCode(
89+
$response->getHeaderLine(self::HEADER_ERROR_TYPE)
90+
);
91+
}
92+
93+
$parsedBody = null;
94+
$body = $response->getBody();
95+
if (!$body->isSeekable() || $body->getSize()) {
96+
$parsedBody = $this->parseBody($body, $response);
97+
}
98+
99+
if (!$errorCode && $parsedBody) {
100+
$errorCode = $this->extractErrorCode(
101+
$parsedBody['code'] ?? $parsedBody['__type'] ?? ''
102+
);
103+
}
104+
105+
return [
106+
'request_id' => $response->getHeaderLine(self::HEADER_REQUEST_ID),
107+
'code' => $errorCode ?: null,
108+
'message' => null,
109+
'type' => $errorType ?? ($statusCode[0] === '4' ? 'client' : 'server'),
110+
'parsed' => $parsedBody,
111+
];
112+
}
113+
114+
/**
115+
* Parse AWS Query Compatible error from header
116+
*
117+
* @param ResponseInterface $response
118+
*
119+
* @return array|null Returns ['code' => string, 'type' => string] or null
120+
*/
121+
private function parseQueryCompatibleHeader(ResponseInterface $response): ?array
122+
{
123+
$parts = explode(';', $response->getHeaderLine(self::HEADER_QUERY_ERROR));
124+
if (count($parts) === 2 && $parts[0] && $parts[1]) {
125+
return [
126+
'code' => $parts[0],
127+
'type' => $parts[1],
128+
];
129+
}
130+
131+
return null;
132+
}
133+
134+
/**
135+
* Extract error code from raw error string containing # and/or : delimiters
136+
*
137+
* @param string $rawErrorCode
138+
* @return string
139+
*/
140+
private function extractErrorCode(string $rawErrorCode): string
141+
{
142+
// Handle format with both # and uri (e.g., "namespace#ErrorCode:http://foo-bar")
143+
if (str_contains($rawErrorCode, ':') && str_contains($rawErrorCode, '#')) {
144+
$start = strpos($rawErrorCode, '#') + 1;
145+
$end = strpos($rawErrorCode, ':', $start);
146+
return substr($rawErrorCode, $start, $end - $start);
147+
}
148+
149+
// Handle format with uri only : (e.g., "ErrorCode:http://foo-bar.com/baz")
150+
if (str_contains($rawErrorCode, ':')) {
151+
return substr($rawErrorCode, 0, strpos($rawErrorCode, ':'));
152+
}
153+
154+
// Handle format with only # (e.g., "namespace#ErrorCode")
155+
if (str_contains($rawErrorCode, '#')) {
156+
return substr($rawErrorCode, strpos($rawErrorCode, '#') + 1);
157+
}
158+
159+
return $rawErrorCode;
160+
}
161+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
<?php
2+
namespace Aws\Api\ErrorParser;
3+
4+
use Aws\Api\Parser\RpcV2ParserTrait;
5+
use Aws\Api\Service;
6+
use Aws\Api\StructureShape;
7+
use Aws\Cbor\CborDecoder;
8+
use Psr\Http\Message\ResponseInterface;
9+
use Psr\Http\Message\StreamInterface;
10+
11+
/**
12+
* Parses errors according to Smithy RPC V2 CBOR protocol standards.
13+
*
14+
* https://smithy.io/2.0/additional-specs/protocols/smithy-rpc-v2.html
15+
*
16+
* @internal
17+
*/
18+
final class RpcV2CborErrorParser extends AbstractRpcV2ErrorParser
19+
{
20+
/** @var CborDecoder */
21+
private CborDecoder $decoder;
22+
23+
use RpcV2ParserTrait;
24+
25+
/**
26+
* @param Service|null $api
27+
* @param CborDecoder|null $decoder
28+
*/
29+
public function __construct(
30+
?Service $api = null,
31+
?CborDecoder $decoder = null
32+
) {
33+
$this->decoder = $decoder ?: new CborDecoder();
34+
parent::__construct($api);
35+
}
36+
37+
/**
38+
* @param ResponseInterface $response
39+
* @param StructureShape $member
40+
*
41+
* @return array
42+
* @throws \Exception
43+
*/
44+
protected function payload(
45+
ResponseInterface $response,
46+
StructureShape $member
47+
): array
48+
{
49+
$body = $response->getBody();
50+
$cborBody = $this->parseCbor($body, $response);
51+
52+
return $this->resolveOutputShape($member, $cborBody);
53+
}
54+
55+
/**
56+
* @param StreamInterface $body
57+
* @param ResponseInterface $response
58+
*
59+
* @return mixed
60+
*/
61+
protected function parseBody(
62+
StreamInterface $body,
63+
ResponseInterface $response
64+
): mixed
65+
{
66+
return $this->parseCbor($body, $response);
67+
}
68+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
<?php
2+
namespace Aws\Api\Parser;
3+
4+
use Aws\Api\Operation;
5+
use Aws\Api\Parser\Exception\ParserException;
6+
use Aws\Result;
7+
use Aws\CommandInterface;
8+
use Psr\Http\Message\ResponseInterface;
9+
10+
/**
11+
* Base implementation for Smithy RPC V2 protocol parsers.
12+
*
13+
* Implementers MUST define the following static property representing
14+
* the `Smithy-Protocol` header value:
15+
* self::HEADER_SMITHY_PROTOCOL => static::$smithyProtocol
16+
*
17+
* @internal
18+
*/
19+
abstract class AbstractRpcV2Parser extends AbstractParser
20+
{
21+
private const HEADER_SMITHY_PROTOCOL = 'Smithy-Protocol';
22+
23+
/** @var string */
24+
protected static string $smithyProtocol;
25+
26+
public function __invoke(
27+
CommandInterface $command,
28+
ResponseInterface $response
29+
) {
30+
$operation = $this->api->getOperation($command->getName());
31+
32+
return $this->parseResponse($response, $operation);
33+
}
34+
35+
/**
36+
* Parses a response according to Smithy RPC V2 protocol standards.
37+
*
38+
* @param ResponseInterface $response the response to parse.
39+
* @param Operation $operation the operation which holds information for
40+
* parsing the response.
41+
*
42+
* @return Result
43+
*/
44+
private function parseResponse(
45+
ResponseInterface $response,
46+
Operation $operation
47+
): Result
48+
{
49+
$smithyProtocolHeader = $response->getHeaderLine(self::HEADER_SMITHY_PROTOCOL);
50+
if ($smithyProtocolHeader !== static::$smithyProtocol) {
51+
$statusCode = $response->getStatusCode();
52+
throw new ParserException(
53+
"Malformed response: Smithy-Protocol header mismatch (HTTP {$statusCode}). "
54+
. 'Expected ' . static::$smithyProtocol
55+
);
56+
}
57+
58+
if ($operation['output'] === null) {
59+
return new Result([]);
60+
}
61+
62+
$outputShape = $operation->getOutput();
63+
foreach ($outputShape->getMembers() as $memberName => $memberProps) {
64+
if (!empty($memberProps['eventstream'])) {
65+
return new Result([
66+
$memberName => new EventParsingIterator(
67+
$response->getBody(),
68+
$outputShape->getMember($memberName),
69+
$this
70+
)
71+
]);
72+
}
73+
}
74+
75+
$result = $this->parseMemberFromStream(
76+
$response->getBody(),
77+
$outputShape,
78+
$response
79+
);
80+
81+
return new Result(is_null($result) ? [] : $result);
82+
}
83+
}

src/Api/Parser/PayloadParserTrait.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
namespace Aws\Api\Parser;
33

44
use Aws\Api\Parser\Exception\ParserException;
5-
use Psr\Http\Message\ResponseInterface;
65

76
trait PayloadParserTrait
87
{

src/Api/Parser/RpcV2CborParser.php

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<?php
2+
namespace Aws\Api\Parser;
3+
4+
use Aws\Api\StructureShape;
5+
use Aws\Api\Service;
6+
use Aws\Cbor\CborDecoder;
7+
use Psr\Http\Message\StreamInterface;
8+
9+
/**
10+
* Parses responses according to Smithy RPC V2 CBOR protocol standards.
11+
*
12+
* https://smithy.io/2.0/additional-specs/protocols/smithy-rpc-v2.html
13+
*
14+
* @internal
15+
*/
16+
final class RpcV2CborParser extends AbstractRpcV2Parser
17+
{
18+
/** @var string */
19+
protected static string $smithyProtocol = 'rpc-v2-cbor';
20+
21+
/** @var CborDecoder */
22+
private CborDecoder $decoder;
23+
24+
use RpcV2ParserTrait;
25+
26+
/**
27+
* @param Service $api Service description
28+
* @param CborDecoder|null $decoder Used to decode CBOR-encoded response data
29+
*/
30+
public function __construct(
31+
Service $api,
32+
?CborDecoder $decoder = null
33+
)
34+
{
35+
$this->decoder = $decoder ?? new CborDecoder();
36+
parent::__construct($api);
37+
}
38+
39+
/**
40+
* @param StreamInterface $stream
41+
* @param StructureShape $member
42+
* @param $response
43+
*
44+
* @return mixed
45+
*/
46+
public function parseMemberFromStream(
47+
StreamInterface $stream,
48+
StructureShape $member,
49+
$response
50+
): mixed
51+
{
52+
return $this->resolveOutputShape($member, $this->parseCbor($stream, $response));
53+
}
54+
}

0 commit comments

Comments
 (0)