Skip to content

Commit a52c9d0

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

28 files changed

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

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)