Skip to content

Commit 3950d94

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

28 files changed

+9618
-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: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
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+
33+
if (isset($data['parsed']['__type'])) {
34+
$data['message'] = $data['parsed']['message'] ?? null;
35+
}
36+
37+
$this->populateShape($data, $response, $command);
38+
39+
return $data;
40+
}
41+
42+
/**
43+
* @param ResponseInterface $response
44+
* @param StructureShape $member
45+
*
46+
* @return array
47+
*/
48+
abstract protected function payload(
49+
ResponseInterface $response,
50+
StructureShape $member
51+
): array;
52+
53+
/**
54+
* @param StreamInterface $body
55+
* @param ResponseInterface $response
56+
*
57+
* @return mixed
58+
*/
59+
abstract protected function parseBody(
60+
StreamInterface $body,
61+
ResponseInterface $response
62+
): mixed;
63+
64+
/**
65+
* @param ResponseInterface $response
66+
*
67+
* @return array
68+
*/
69+
private function parseError(ResponseInterface $response): array
70+
{
71+
$statusCode = (string) $response->getStatusCode();
72+
$errorCode = null;
73+
$errorType = null;
74+
75+
if ($this->api?->getMetadata('awsQueryCompatible') !== null
76+
&& $response->hasHeader(self::HEADER_QUERY_ERROR)
77+
&& $awsQueryError = $this->parseQueryCompatibleHeader($response)
78+
) {
79+
$errorCode = $awsQueryError['code'];
80+
$errorType = $awsQueryError['type'];
81+
}
82+
83+
if (!$errorCode && $response->hasHeader(self::HEADER_ERROR_TYPE)) {
84+
$errorCode = $this->extractErrorCode(
85+
$response->getHeaderLine(self::HEADER_ERROR_TYPE)
86+
);
87+
}
88+
89+
$parsedBody = null;
90+
$body = $response->getBody();
91+
if ($body->getSize()) {
92+
//TODO handle unseekable streams with CachingStream
93+
$parsedBody = array_change_key_case($this->parseBody($body, $response));
94+
}
95+
96+
if (!$errorCode && $parsedBody) {
97+
$errorCode = $this->extractErrorCode(
98+
$parsedBody['code'] ?? $parsedBody['__type'] ?? ''
99+
);
100+
}
101+
102+
return [
103+
'request_id' => $response->getHeaderLine(self::HEADER_REQUEST_ID),
104+
'code' => $errorCode ?: null,
105+
'message' => null,
106+
'type' => $errorType ?? ($statusCode[0] === '4' ? 'client' : 'server'),
107+
'parsed' => $parsedBody,
108+
];
109+
}
110+
111+
/**
112+
* Parse AWS Query Compatible error from header
113+
*
114+
* @param ResponseInterface $response
115+
*
116+
* @return array|null Returns ['code' => string, 'type' => string] or null
117+
*/
118+
private function parseQueryCompatibleHeader(ResponseInterface $response): ?array
119+
{
120+
$parts = explode(';', $response->getHeaderLine(self::HEADER_QUERY_ERROR));
121+
if (count($parts) === 2 && $parts[0] && $parts[1]) {
122+
return [
123+
'code' => $parts[0],
124+
'type' => $parts[1],
125+
];
126+
}
127+
128+
return null;
129+
}
130+
131+
/**
132+
* Extract error code from raw error string containing # and/or : delimiters
133+
*
134+
* @param string $rawErrorCode
135+
* @return string
136+
*/
137+
private function extractErrorCode(string $rawErrorCode): string
138+
{
139+
// Handle format with both # and uri (e.g., "namespace#ErrorCode:http://foo-bar")
140+
if (str_contains($rawErrorCode, ':') && str_contains($rawErrorCode, '#')) {
141+
$start = strpos($rawErrorCode, '#') + 1;
142+
$end = strpos($rawErrorCode, ':', $start);
143+
return substr($rawErrorCode, $start, $end - $start);
144+
}
145+
146+
// Handle format with uri only : (e.g., "ErrorCode:http://foo-bar.com/baz")
147+
if (str_contains($rawErrorCode, ':')) {
148+
return substr($rawErrorCode, 0, strpos($rawErrorCode, ':'));
149+
}
150+
151+
// Handle format with only # (e.g., "namespace#ErrorCode")
152+
if (str_contains($rawErrorCode, '#')) {
153+
return substr($rawErrorCode, strpos($rawErrorCode, '#') + 1);
154+
}
155+
156+
return $rawErrorCode;
157+
}
158+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
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+
*/
28+
public function __construct(?Service $api = null)
29+
{
30+
$this->decoder = new CborDecoder();
31+
parent::__construct($api);
32+
}
33+
34+
/**
35+
* @param ResponseInterface $response
36+
* @param StructureShape $member
37+
*
38+
* @return array
39+
* @throws \Exception
40+
*/
41+
protected function payload(
42+
ResponseInterface $response,
43+
StructureShape $member
44+
): array
45+
{
46+
$body = $response->getBody();
47+
$cborBody = $this->parseCbor($body, $response);
48+
49+
return $this->resolveOutputShape($member, $cborBody);
50+
}
51+
52+
/**
53+
* @param StreamInterface $body
54+
* @param ResponseInterface $response
55+
*
56+
* @return mixed
57+
*/
58+
protected function parseBody(
59+
StreamInterface $body,
60+
ResponseInterface $response
61+
): mixed
62+
{
63+
return $this->parseCbor($body, $response);
64+
}
65+
}
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: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
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+
*/
29+
public function __construct(Service $api)
30+
{
31+
$this->decoder = new CborDecoder();
32+
parent::__construct($api);
33+
}
34+
35+
/**
36+
* @param StreamInterface $stream
37+
* @param StructureShape $member
38+
* @param $response
39+
*
40+
* @return mixed
41+
*/
42+
public function parseMemberFromStream(
43+
StreamInterface $stream,
44+
StructureShape $member,
45+
$response
46+
): mixed
47+
{
48+
return $this->resolveOutputShape($member, $this->parseCbor($stream, $response));
49+
}
50+
}

0 commit comments

Comments
 (0)