From fae07067a106f8a24d8ccfb683e2aba2a59c4245 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 19 Aug 2026 22:25:00 +0000 Subject: [PATCH] fix: Raise a Seam error for a success response that is malformed Generated methods read the resource straight off the decoded envelope, with no guard on any of the unwrap sites. A 200 whose body had been rewritten or truncated on the way back, by a proxy, a gateway maintenance page carrying a JSON content type, or a load balancer, left the read as null and the failure surfaced from the method's return type: TypeError: DevicesClient::get(): Return value must be of type Device, null returned A list endpoint got array_map(): Argument #2 must be of type array. Both are an Error rather than an Exception, so neither is caught by catch (\Exception) nor by catch (SeamException), and neither says what was wrong with the response. Add InvalidResponseError, a SeamException, and read the envelope through Body::read and Body::read_list, which raise it naming the endpoint and the key. Keeping the guard in one place leaves the generated call sites a single call rather than a block repeated at every one. The action attempt poll read the same way and raised a bare UnexpectedValueException, which was equally invisible to a Seam catch block. It now raises the same error. The malformed-response tests covered only 500s, which never reach the unwrap. Add the malformed 200 cases: a missing key, the wrong key, a body that is not an object, an empty body, unparseable JSON, an HTML gateway page, a list key holding something that is not a list, and a malformed poll response. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01HH3wdHh4Y6Wjyc5uHwk5iG --- README.md | 6 +- codegen/layouts/partials/route-method.hbs | 6 +- src/Http/Body.php | 52 +++++++++ src/Http/ResolveActionAttempt.php | 9 +- src/InvalidResponseError.php | 37 +++++++ src/Routes/AccessCodesClient.php | 28 ++++- src/Routes/AccessCodesSimulateClient.php | 8 +- src/Routes/AccessCodesUnmanagedClient.php | 10 +- src/Routes/AccessGrantsClient.php | 22 +++- src/Routes/AccessGrantsUnmanagedClient.php | 10 +- src/Routes/AccessMethodsClient.php | 30 ++++- src/Routes/AccessMethodsUnmanagedClient.php | 10 +- src/Routes/AcsAccessGroupsClient.php | 21 +++- src/Routes/AcsCredentialsClient.php | 16 ++- src/Routes/AcsEncodersClient.php | 30 ++++- src/Routes/AcsEntrancesClient.php | 16 ++- src/Routes/AcsSystemsClient.php | 18 ++- src/Routes/AcsUsersClient.php | 19 +++- src/Routes/ActionAttemptsClient.php | 6 +- src/Routes/ClientSessionsClient.php | 18 ++- src/Routes/ConnectWebviewsClient.php | 10 +- src/Routes/ConnectedAccountsClient.php | 10 +- src/Routes/CustomersClient.php | 4 +- src/Routes/DevicesClient.php | 13 ++- src/Routes/DevicesUnmanagedClient.php | 6 +- src/Routes/EventsClient.php | 7 +- src/Routes/InstantKeysClient.php | 6 +- src/Routes/LocksClient.php | 23 +++- src/Routes/LocksSimulateClient.php | 16 ++- src/Routes/NoiseSensorsClient.php | 5 +- .../NoiseSensorsNoiseThresholdsClient.php | 22 +++- src/Routes/PhonesClient.php | 7 +- src/Routes/PhonesSimulateClient.php | 4 +- src/Routes/SpacesClient.php | 15 ++- src/Routes/ThermostatsClient.php | 49 +++++++-- src/Routes/ThermostatsDailyProgramsClient.php | 14 ++- src/Routes/ThermostatsSchedulesClient.php | 22 +++- src/Routes/UserIdentitiesClient.php | 51 +++++++-- src/Routes/UserIdentitiesUnmanagedClient.php | 10 +- src/Routes/WebhooksClient.php | 11 +- src/Routes/WorkspacesClient.php | 17 ++- tests/MalformedResponseTest.php | 103 ++++++++++++++++++ 42 files changed, 670 insertions(+), 127 deletions(-) create mode 100644 src/InvalidResponseError.php diff --git a/README.md b/README.md index bb82b09c..da94f346 100644 --- a/README.md +++ b/README.md @@ -539,8 +539,10 @@ try { } ``` -A response that is not shaped like a Seam error, such as a gateway returning -HTML, raises the underlying Guzzle exception instead. +An error response that is not shaped like a Seam error, such as a gateway +returning HTML, raises the underlying Guzzle exception instead. A successful +response that does not carry the resource the endpoint returns raises +`Seam\InvalidResponseError`, with `getPath()` and `getKey()`. ## Development and Testing diff --git a/codegen/layouts/partials/route-method.hbs b/codegen/layouts/partials/route-method.hbs index 7ce8dfdc..0229b4dc 100644 --- a/codegen/layouts/partials/route-method.hbs +++ b/codegen/layouts/partials/route-method.hbs @@ -29,7 +29,7 @@ {{#if usesActionAttempt}} return ResolveActionAttempt::resolve_action_attempt( - {{returnResource}}::from_json($res->{{returnPath}}), + {{returnResource}}::from_json(Body::read($res, "{{returnPath}}", "{{path}}")), $this->client, $wait_for_action_attempt ?? $this->defaults["wait_for_action_attempt"] ); @@ -43,9 +43,9 @@ {{#unless returnsVoid}} {{#if isArrayResponse}} - return array_map(fn ($r) => {{returnResource}}::from_json($r), $res->{{returnPath}}); + return array_map(fn ($r) => {{returnResource}}::from_json($r), Body::read_list($res, "{{returnPath}}", "{{path}}")); {{else}} - return {{returnResource}}::from_json($res->{{returnPath}}); + return {{returnResource}}::from_json(Body::read($res, "{{returnPath}}", "{{path}}")); {{/if}} {{/unless}} {{/if}} diff --git a/src/Http/Body.php b/src/Http/Body.php index 47b36eef..47ccc2d6 100644 --- a/src/Http/Body.php +++ b/src/Http/Body.php @@ -4,6 +4,7 @@ use GuzzleHttp\Utils; use Psr\Http\Message\ResponseInterface; +use Seam\InvalidResponseError; /** * Reads the JSON body of a response. @@ -38,4 +39,55 @@ public static function decode(ResponseInterface $response): mixed return null; } } + + /** + * Reads the resource an endpoint returns out of its response envelope. + * + * @throws InvalidResponseError If the envelope does not carry the key + */ + public static function read(mixed $res, string $key, string $path): mixed + { + if (!is_object($res)) { + throw new InvalidResponseError( + $path, + $key, + "got " . get_debug_type($res) . " instead of a response object", + ); + } + + if (!property_exists($res, $key)) { + throw new InvalidResponseError( + $path, + $key, + "which the response does not contain", + ); + } + + return $res->$key; + } + + /** + * Reads a list of resources out of a response envelope. + * + * @return array + * + * @throws InvalidResponseError If the envelope does not carry a list + */ + public static function read_list( + mixed $res, + string $key, + string $path, + ): array { + $value = self::read($res, $key, $path); + + if (!is_array($value)) { + throw new InvalidResponseError( + $path, + $key, + "got " . get_debug_type($value) . " instead of a list", + ); + } + + return $value; + } } diff --git a/src/Http/ResolveActionAttempt.php b/src/Http/ResolveActionAttempt.php index 5139d9f9..90c3db54 100644 --- a/src/Http/ResolveActionAttempt.php +++ b/src/Http/ResolveActionAttempt.php @@ -6,6 +6,7 @@ use Seam\ActionAttemptFailedError; use Seam\ActionAttemptTimeoutError; use Seam\InvalidOptionsError; +use Seam\InvalidResponseError; use Seam\Resources\ActionAttempt; /** @@ -105,12 +106,14 @@ private static function get_action_attempt( ); $action_attempt = ActionAttempt::from_json( - $res->action_attempt ?? null, + Body::read($res, "action_attempt", "/action_attempts/get"), ); if ($action_attempt === null) { - throw new \UnexpectedValueException( - "Seam returned no action attempt for {$action_attempt_id}", + throw new InvalidResponseError( + "/action_attempts/get", + "action_attempt", + "which was empty for {$action_attempt_id}", ); } diff --git a/src/InvalidResponseError.php b/src/InvalidResponseError.php new file mode 100644 index 00000000..59b7c989 --- /dev/null +++ b/src/InvalidResponseError.php @@ -0,0 +1,37 @@ +path; + } + + /** + * The response key that should have carried the resource. + */ + public function getKey(): string + { + return $this->key; + } +} diff --git a/src/Routes/AccessCodesClient.php b/src/Routes/AccessCodesClient.php index 7b6f3638..c5211607 100644 --- a/src/Routes/AccessCodesClient.php +++ b/src/Routes/AccessCodesClient.php @@ -142,7 +142,9 @@ public function create( ]), ); - return AccessCode::from_json($res->access_code); + return AccessCode::from_json( + Body::read($res, "access_code", "/access_codes/create"), + ); } /** @@ -249,7 +251,11 @@ public function create_multiple( return array_map( fn($r) => AccessCode::from_json($r), - $res->access_codes, + Body::read_list( + $res, + "access_codes", + "/access_codes/create_multiple", + ), ); } @@ -294,7 +300,9 @@ public function generate_code(string $device_id): AccessCode ]), ); - return AccessCode::from_json($res->generated_code); + return AccessCode::from_json( + Body::read($res, "generated_code", "/access_codes/generate_code"), + ); } /** @@ -335,7 +343,9 @@ public function get( ]), ); - return AccessCode::from_json($res->access_code); + return AccessCode::from_json( + Body::read($res, "access_code", "/access_codes/get"), + ); } /** @@ -430,7 +440,7 @@ public function list( return array_map( fn($r) => AccessCode::from_json($r), - $res->access_codes, + Body::read_list($res, "access_codes", "/access_codes/list"), ); } @@ -462,7 +472,13 @@ public function pull_backup_access_code(string $access_code_id): AccessCode ), ); - return AccessCode::from_json($res->access_code); + return AccessCode::from_json( + Body::read( + $res, + "access_code", + "/access_codes/pull_backup_access_code", + ), + ); } /** diff --git a/src/Routes/AccessCodesSimulateClient.php b/src/Routes/AccessCodesSimulateClient.php index a2897660..a94074c3 100644 --- a/src/Routes/AccessCodesSimulateClient.php +++ b/src/Routes/AccessCodesSimulateClient.php @@ -51,6 +51,12 @@ public function create_unmanaged_access_code( ), ); - return UnmanagedAccessCode::from_json($res->access_code); + return UnmanagedAccessCode::from_json( + Body::read( + $res, + "access_code", + "/access_codes/simulate/create_unmanaged_access_code", + ), + ); } } diff --git a/src/Routes/AccessCodesUnmanagedClient.php b/src/Routes/AccessCodesUnmanagedClient.php index 8e28e0de..4c0739fc 100644 --- a/src/Routes/AccessCodesUnmanagedClient.php +++ b/src/Routes/AccessCodesUnmanagedClient.php @@ -123,7 +123,9 @@ public function get( ]), ); - return UnmanagedAccessCode::from_json($res->access_code); + return UnmanagedAccessCode::from_json( + Body::read($res, "access_code", "/access_codes/unmanaged/get"), + ); } /** @@ -173,7 +175,11 @@ public function list( return array_map( fn($r) => UnmanagedAccessCode::from_json($r), - $res->access_codes, + Body::read_list( + $res, + "access_codes", + "/access_codes/unmanaged/list", + ), ); } diff --git a/src/Routes/AccessGrantsClient.php b/src/Routes/AccessGrantsClient.php index 46ae3690..29e5df1c 100644 --- a/src/Routes/AccessGrantsClient.php +++ b/src/Routes/AccessGrantsClient.php @@ -120,7 +120,9 @@ public function create( ]), ); - return AccessGrant::from_json($res->access_grant); + return AccessGrant::from_json( + Body::read($res, "access_grant", "/access_grants/create"), + ); } /** @@ -171,7 +173,9 @@ public function get( ]), ); - return AccessGrant::from_json($res->access_grant); + return AccessGrant::from_json( + Body::read($res, "access_grant", "/access_grants/get"), + ); } /** @@ -220,7 +224,9 @@ public function get_related( ]), ); - return Batch::from_json($res->batch); + return Batch::from_json( + Body::read($res, "batch", "/access_grants/get_related"), + ); } /** @@ -312,7 +318,7 @@ public function list( return array_map( fn($r) => AccessGrant::from_json($r), - $res->access_grants, + Body::read_list($res, "access_grants", "/access_grants/list"), ); } @@ -342,7 +348,13 @@ public function request_access_methods( ), ); - return AccessGrant::from_json($res->access_grant); + return AccessGrant::from_json( + Body::read( + $res, + "access_grant", + "/access_grants/request_access_methods", + ), + ); } /** diff --git a/src/Routes/AccessGrantsUnmanagedClient.php b/src/Routes/AccessGrantsUnmanagedClient.php index d7b716c7..b22800b8 100644 --- a/src/Routes/AccessGrantsUnmanagedClient.php +++ b/src/Routes/AccessGrantsUnmanagedClient.php @@ -43,7 +43,9 @@ public function get(string $access_grant_id): UnmanagedAccessGrant ]), ); - return UnmanagedAccessGrant::from_json($res->access_grant); + return UnmanagedAccessGrant::from_json( + Body::read($res, "access_grant", "/access_grants/unmanaged/get"), + ); } /** @@ -100,7 +102,11 @@ public function list( return array_map( fn($r) => UnmanagedAccessGrant::from_json($r), - $res->access_grants, + Body::read_list( + $res, + "access_grants", + "/access_grants/unmanaged/list", + ), ); } diff --git a/src/Routes/AccessMethodsClient.php b/src/Routes/AccessMethodsClient.php index e36c36ca..41c22195 100644 --- a/src/Routes/AccessMethodsClient.php +++ b/src/Routes/AccessMethodsClient.php @@ -54,7 +54,13 @@ public function assign_card( ); return ResolveActionAttempt::resolve_action_attempt( - ActionAttempt::from_json($res->action_attempt), + ActionAttempt::from_json( + Body::read( + $res, + "action_attempt", + "/access_methods/assign_card", + ), + ), $this->client, $wait_for_action_attempt ?? $this->defaults["wait_for_action_attempt"], @@ -125,7 +131,9 @@ public function encode( ); return ResolveActionAttempt::resolve_action_attempt( - ActionAttempt::from_json($res->action_attempt), + ActionAttempt::from_json( + Body::read($res, "action_attempt", "/access_methods/encode"), + ), $this->client, $wait_for_action_attempt ?? $this->defaults["wait_for_action_attempt"], @@ -150,7 +158,9 @@ public function get(string $access_method_id): AccessMethod ]), ); - return AccessMethod::from_json($res->access_method); + return AccessMethod::from_json( + Body::read($res, "access_method", "/access_methods/get"), + ); } /** @@ -182,7 +192,9 @@ public function get_related( ]), ); - return Batch::from_json($res->batch); + return Batch::from_json( + Body::read($res, "batch", "/access_methods/get_related"), + ); } /** @@ -263,7 +275,7 @@ public function list( return array_map( fn($r) => AccessMethod::from_json($r), - $res->access_methods, + Body::read_list($res, "access_methods", "/access_methods/list"), ); } @@ -292,7 +304,13 @@ public function unlock_door( ); return ResolveActionAttempt::resolve_action_attempt( - ActionAttempt::from_json($res->action_attempt), + ActionAttempt::from_json( + Body::read( + $res, + "action_attempt", + "/access_methods/unlock_door", + ), + ), $this->client, $wait_for_action_attempt ?? $this->defaults["wait_for_action_attempt"], diff --git a/src/Routes/AccessMethodsUnmanagedClient.php b/src/Routes/AccessMethodsUnmanagedClient.php index 9bc0a43a..34c85538 100644 --- a/src/Routes/AccessMethodsUnmanagedClient.php +++ b/src/Routes/AccessMethodsUnmanagedClient.php @@ -42,7 +42,9 @@ public function get(string $access_method_id): UnmanagedAccessMethod ]), ); - return UnmanagedAccessMethod::from_json($res->access_method); + return UnmanagedAccessMethod::from_json( + Body::read($res, "access_method", "/access_methods/unmanaged/get"), + ); } /** @@ -81,7 +83,11 @@ public function list( return array_map( fn($r) => UnmanagedAccessMethod::from_json($r), - $res->access_methods, + Body::read_list( + $res, + "access_methods", + "/access_methods/unmanaged/list", + ), ); } } diff --git a/src/Routes/AcsAccessGroupsClient.php b/src/Routes/AcsAccessGroupsClient.php index 74cb4208..d52d009a 100644 --- a/src/Routes/AcsAccessGroupsClient.php +++ b/src/Routes/AcsAccessGroupsClient.php @@ -89,7 +89,9 @@ public function get(string $acs_access_group_id): AcsAccessGroup ]), ); - return AcsAccessGroup::from_json($res->acs_access_group); + return AcsAccessGroup::from_json( + Body::read($res, "acs_access_group", "/acs/access_groups/get"), + ); } /** @@ -130,7 +132,11 @@ public function list( return array_map( fn($r) => AcsAccessGroup::from_json($r), - $res->acs_access_groups, + Body::read_list( + $res, + "acs_access_groups", + "/acs/access_groups/list", + ), ); } @@ -157,7 +163,11 @@ public function list_accessible_entrances( return array_map( fn($r) => AcsEntrance::from_json($r), - $res->acs_entrances, + Body::read_list( + $res, + "acs_entrances", + "/acs/access_groups/list_accessible_entrances", + ), ); } @@ -179,7 +189,10 @@ public function list_users(string $acs_access_group_id): array ]), ); - return array_map(fn($r) => AcsUser::from_json($r), $res->acs_users); + return array_map( + fn($r) => AcsUser::from_json($r), + Body::read_list($res, "acs_users", "/acs/access_groups/list_users"), + ); } /** diff --git a/src/Routes/AcsCredentialsClient.php b/src/Routes/AcsCredentialsClient.php index 12291c33..b8a8774e 100644 --- a/src/Routes/AcsCredentialsClient.php +++ b/src/Routes/AcsCredentialsClient.php @@ -141,7 +141,9 @@ public function create( ]), ); - return AcsCredential::from_json($res->acs_credential); + return AcsCredential::from_json( + Body::read($res, "acs_credential", "/acs/credentials/create"), + ); } /** @@ -179,7 +181,9 @@ public function get(string $acs_credential_id): AcsCredential ]), ); - return AcsCredential::from_json($res->acs_credential); + return AcsCredential::from_json( + Body::read($res, "acs_credential", "/acs/credentials/get"), + ); } /** @@ -248,7 +252,7 @@ public function list( return array_map( fn($r) => AcsCredential::from_json($r), - $res->acs_credentials, + Body::read_list($res, "acs_credentials", "/acs/credentials/list"), ); } @@ -274,7 +278,11 @@ public function list_accessible_entrances(string $acs_credential_id): array return array_map( fn($r) => AcsEntrance::from_json($r), - $res->acs_entrances, + Body::read_list( + $res, + "acs_entrances", + "/acs/credentials/list_accessible_entrances", + ), ); } diff --git a/src/Routes/AcsEncodersClient.php b/src/Routes/AcsEncodersClient.php index 0ec0f2b0..91728f03 100644 --- a/src/Routes/AcsEncodersClient.php +++ b/src/Routes/AcsEncodersClient.php @@ -60,7 +60,13 @@ public function encode_credential( ); return ResolveActionAttempt::resolve_action_attempt( - ActionAttempt::from_json($res->action_attempt), + ActionAttempt::from_json( + Body::read( + $res, + "action_attempt", + "/acs/encoders/encode_credential", + ), + ), $this->client, $wait_for_action_attempt ?? $this->defaults["wait_for_action_attempt"], @@ -85,7 +91,9 @@ public function get(string $acs_encoder_id): AcsEncoder ]), ); - return AcsEncoder::from_json($res->acs_encoder); + return AcsEncoder::from_json( + Body::read($res, "acs_encoder", "/acs/encoders/get"), + ); } /** @@ -137,7 +145,7 @@ public function list( return array_map( fn($r) => AcsEncoder::from_json($r), - $res->acs_encoders, + Body::read_list($res, "acs_encoders", "/acs/encoders/list"), ); } @@ -168,7 +176,13 @@ public function scan_credential( ); return ResolveActionAttempt::resolve_action_attempt( - ActionAttempt::from_json($res->action_attempt), + ActionAttempt::from_json( + Body::read( + $res, + "action_attempt", + "/acs/encoders/scan_credential", + ), + ), $this->client, $wait_for_action_attempt ?? $this->defaults["wait_for_action_attempt"], @@ -214,7 +228,13 @@ public function scan_to_assign_credential( ); return ResolveActionAttempt::resolve_action_attempt( - ActionAttempt::from_json($res->action_attempt), + ActionAttempt::from_json( + Body::read( + $res, + "action_attempt", + "/acs/encoders/scan_to_assign_credential", + ), + ), $this->client, $wait_for_action_attempt ?? $this->defaults["wait_for_action_attempt"], diff --git a/src/Routes/AcsEntrancesClient.php b/src/Routes/AcsEntrancesClient.php index d9fcb87e..be84c517 100644 --- a/src/Routes/AcsEntrancesClient.php +++ b/src/Routes/AcsEntrancesClient.php @@ -46,7 +46,9 @@ public function get(string $acs_entrance_id): AcsEntrance ]), ); - return AcsEntrance::from_json($res->acs_entrance); + return AcsEntrance::from_json( + Body::read($res, "acs_entrance", "/acs/entrances/get"), + ); } /** @@ -156,7 +158,7 @@ public function list( return array_map( fn($r) => AcsEntrance::from_json($r), - $res->acs_entrances, + Body::read_list($res, "acs_entrances", "/acs/entrances/list"), ); } @@ -188,7 +190,11 @@ public function list_credentials_with_access( return array_map( fn($r) => AcsCredential::from_json($r), - $res->acs_credentials, + Body::read_list( + $res, + "acs_credentials", + "/acs/entrances/list_credentials_with_access", + ), ); } @@ -217,7 +223,9 @@ public function unlock( ); return ResolveActionAttempt::resolve_action_attempt( - ActionAttempt::from_json($res->action_attempt), + ActionAttempt::from_json( + Body::read($res, "action_attempt", "/acs/entrances/unlock"), + ), $this->client, $wait_for_action_attempt ?? $this->defaults["wait_for_action_attempt"], diff --git a/src/Routes/AcsSystemsClient.php b/src/Routes/AcsSystemsClient.php index af1d681d..b4b059db 100644 --- a/src/Routes/AcsSystemsClient.php +++ b/src/Routes/AcsSystemsClient.php @@ -42,7 +42,9 @@ public function get(string $acs_system_id): AcsSystem ]), ); - return AcsSystem::from_json($res->acs_system); + return AcsSystem::from_json( + Body::read($res, "acs_system", "/acs/systems/get"), + ); } /** @@ -78,7 +80,10 @@ public function list( ]), ); - return array_map(fn($r) => AcsSystem::from_json($r), $res->acs_systems); + return array_map( + fn($r) => AcsSystem::from_json($r), + Body::read_list($res, "acs_systems", "/acs/systems/list"), + ); } /** @@ -104,7 +109,14 @@ public function list_compatible_credential_manager_acs_systems( ), ); - return array_map(fn($r) => AcsSystem::from_json($r), $res->acs_systems); + return array_map( + fn($r) => AcsSystem::from_json($r), + Body::read_list( + $res, + "acs_systems", + "/acs/systems/list_compatible_credential_manager_acs_systems", + ), + ); } /** diff --git a/src/Routes/AcsUsersClient.php b/src/Routes/AcsUsersClient.php index 9c0d8399..64ce6cae 100644 --- a/src/Routes/AcsUsersClient.php +++ b/src/Routes/AcsUsersClient.php @@ -99,7 +99,9 @@ public function create( ]), ); - return AcsUser::from_json($res->acs_user); + return AcsUser::from_json( + Body::read($res, "acs_user", "/acs/users/create"), + ); } /** @@ -181,7 +183,9 @@ public function get( ]), ); - return AcsUser::from_json($res->acs_user); + return AcsUser::from_json( + Body::read($res, "acs_user", "/acs/users/get"), + ); } /** @@ -250,7 +254,10 @@ public function list( $on_response($res); } - return array_map(fn($r) => AcsUser::from_json($r), $res->acs_users); + return array_map( + fn($r) => AcsUser::from_json($r), + Body::read_list($res, "acs_users", "/acs/users/list"), + ); } /** @@ -297,7 +304,11 @@ public function list_accessible_entrances( return array_map( fn($r) => AcsEntrance::from_json($r), - $res->acs_entrances, + Body::read_list( + $res, + "acs_entrances", + "/acs/users/list_accessible_entrances", + ), ); } diff --git a/src/Routes/ActionAttemptsClient.php b/src/Routes/ActionAttemptsClient.php index 9e1e4944..74acf26f 100644 --- a/src/Routes/ActionAttemptsClient.php +++ b/src/Routes/ActionAttemptsClient.php @@ -48,7 +48,9 @@ public function get( ); return ResolveActionAttempt::resolve_action_attempt( - ActionAttempt::from_json($res->action_attempt), + ActionAttempt::from_json( + Body::read($res, "action_attempt", "/action_attempts/get"), + ), $this->client, $wait_for_action_attempt ?? $this->defaults["wait_for_action_attempt"], @@ -99,7 +101,7 @@ public function list( return array_map( fn($r) => ActionAttempt::from_json($r), - $res->action_attempts, + Body::read_list($res, "action_attempts", "/action_attempts/list"), ); } } diff --git a/src/Routes/ClientSessionsClient.php b/src/Routes/ClientSessionsClient.php index dbd36c2b..7146e7c8 100644 --- a/src/Routes/ClientSessionsClient.php +++ b/src/Routes/ClientSessionsClient.php @@ -80,7 +80,9 @@ public function create( ]), ); - return ClientSession::from_json($res->client_session); + return ClientSession::from_json( + Body::read($res, "client_session", "/client_sessions/create"), + ); } /** @@ -126,7 +128,9 @@ public function get( ]), ); - return ClientSession::from_json($res->client_session); + return ClientSession::from_json( + Body::read($res, "client_session", "/client_sessions/get"), + ); } /** @@ -175,7 +179,13 @@ public function get_or_create( ]), ); - return ClientSession::from_json($res->client_session); + return ClientSession::from_json( + Body::read( + $res, + "client_session", + "/client_sessions/get_or_create", + ), + ); } /** @@ -280,7 +290,7 @@ public function list( return array_map( fn($r) => ClientSession::from_json($r), - $res->client_sessions, + Body::read_list($res, "client_sessions", "/client_sessions/list"), ); } diff --git a/src/Routes/ConnectWebviewsClient.php b/src/Routes/ConnectWebviewsClient.php index 807dd4ae..5dc7b7e3 100644 --- a/src/Routes/ConnectWebviewsClient.php +++ b/src/Routes/ConnectWebviewsClient.php @@ -103,7 +103,9 @@ public function create( ]), ); - return ConnectWebview::from_json($res->connect_webview); + return ConnectWebview::from_json( + Body::read($res, "connect_webview", "/connect_webviews/create"), + ); } /** @@ -145,7 +147,9 @@ public function get(string $connect_webview_id): ConnectWebview ]), ); - return ConnectWebview::from_json($res->connect_webview); + return ConnectWebview::from_json( + Body::read($res, "connect_webview", "/connect_webviews/get"), + ); } /** @@ -202,7 +206,7 @@ public function list( return array_map( fn($r) => ConnectWebview::from_json($r), - $res->connect_webviews, + Body::read_list($res, "connect_webviews", "/connect_webviews/list"), ); } } diff --git a/src/Routes/ConnectedAccountsClient.php b/src/Routes/ConnectedAccountsClient.php index 673d1221..7dc68fef 100644 --- a/src/Routes/ConnectedAccountsClient.php +++ b/src/Routes/ConnectedAccountsClient.php @@ -81,7 +81,9 @@ public function get( ]), ); - return ConnectedAccount::from_json($res->connected_account); + return ConnectedAccount::from_json( + Body::read($res, "connected_account", "/connected_accounts/get"), + ); } /** @@ -143,7 +145,11 @@ public function list( return array_map( fn($r) => ConnectedAccount::from_json($r), - $res->connected_accounts, + Body::read_list( + $res, + "connected_accounts", + "/connected_accounts/list", + ), ); } diff --git a/src/Routes/CustomersClient.php b/src/Routes/CustomersClient.php index 0e275dca..7baced07 100644 --- a/src/Routes/CustomersClient.php +++ b/src/Routes/CustomersClient.php @@ -99,7 +99,9 @@ public function create_portal( ]), ); - return CustomerPortal::from_json($res->customer_portal); + return CustomerPortal::from_json( + Body::read($res, "customer_portal", "/customers/create_portal"), + ); } /** diff --git a/src/Routes/DevicesClient.php b/src/Routes/DevicesClient.php index e9cf9fae..7cd98776 100644 --- a/src/Routes/DevicesClient.php +++ b/src/Routes/DevicesClient.php @@ -60,7 +60,7 @@ public function get(?string $device_id = null, ?string $name = null): Device ]), ); - return Device::from_json($res->device); + return Device::from_json(Body::read($res, "device", "/devices/get")); } /** @@ -165,7 +165,10 @@ public function list( $on_response($res); } - return array_map(fn($r) => Device::from_json($r), $res->devices); + return array_map( + fn($r) => Device::from_json($r), + Body::read_list($res, "devices", "/devices/list"), + ); } /** @@ -195,7 +198,11 @@ public function list_device_providers( return array_map( fn($r) => DeviceProvider::from_json($r), - $res->device_providers, + Body::read_list( + $res, + "device_providers", + "/devices/list_device_providers", + ), ); } diff --git a/src/Routes/DevicesUnmanagedClient.php b/src/Routes/DevicesUnmanagedClient.php index a4974282..9ebb9e2f 100644 --- a/src/Routes/DevicesUnmanagedClient.php +++ b/src/Routes/DevicesUnmanagedClient.php @@ -60,7 +60,9 @@ public function get( ]), ); - return UnmanagedDevice::from_json($res->device); + return UnmanagedDevice::from_json( + Body::read($res, "device", "/devices/unmanaged/get"), + ); } /** @@ -149,7 +151,7 @@ public function list( return array_map( fn($r) => UnmanagedDevice::from_json($r), - $res->devices, + Body::read_list($res, "devices", "/devices/unmanaged/list"), ); } diff --git a/src/Routes/EventsClient.php b/src/Routes/EventsClient.php index e8fd51d5..f2c8177c 100644 --- a/src/Routes/EventsClient.php +++ b/src/Routes/EventsClient.php @@ -60,7 +60,7 @@ public function get( ]), ); - return Event::from_json($res->event); + return Event::from_json(Body::read($res, "event", "/events/get")); } /** @@ -253,6 +253,9 @@ public function list( ]), ); - return array_map(fn($r) => Event::from_json($r), $res->events); + return array_map( + fn($r) => Event::from_json($r), + Body::read_list($res, "events", "/events/list"), + ); } } diff --git a/src/Routes/InstantKeysClient.php b/src/Routes/InstantKeysClient.php index ce21eff7..d732d637 100644 --- a/src/Routes/InstantKeysClient.php +++ b/src/Routes/InstantKeysClient.php @@ -72,7 +72,9 @@ public function get( ]), ); - return InstantKey::from_json($res->instant_key); + return InstantKey::from_json( + Body::read($res, "instant_key", "/instant_keys/get"), + ); } /** @@ -97,7 +99,7 @@ public function list(?string $user_identity_id = null): array return array_map( fn($r) => InstantKey::from_json($r), - $res->instant_keys, + Body::read_list($res, "instant_keys", "/instant_keys/list"), ); } } diff --git a/src/Routes/LocksClient.php b/src/Routes/LocksClient.php index 96b6b9f0..d02b6263 100644 --- a/src/Routes/LocksClient.php +++ b/src/Routes/LocksClient.php @@ -59,7 +59,13 @@ public function configure_auto_lock( ); return ResolveActionAttempt::resolve_action_attempt( - ActionAttempt::from_json($res->action_attempt), + ActionAttempt::from_json( + Body::read( + $res, + "action_attempt", + "/locks/configure_auto_lock", + ), + ), $this->client, $wait_for_action_attempt ?? $this->defaults["wait_for_action_attempt"], @@ -96,7 +102,7 @@ public function get(?string $device_id = null, ?string $name = null): Device ]), ); - return Device::from_json($res->device); + return Device::from_json(Body::read($res, "device", "/locks/get")); } /** @@ -145,7 +151,10 @@ public function list( ]), ); - return array_map(fn($r) => Device::from_json($r), $res->devices); + return array_map( + fn($r) => Device::from_json($r), + Body::read_list($res, "devices", "/locks/list"), + ); } /** @@ -170,7 +179,9 @@ public function lock_door( ); return ResolveActionAttempt::resolve_action_attempt( - ActionAttempt::from_json($res->action_attempt), + ActionAttempt::from_json( + Body::read($res, "action_attempt", "/locks/lock_door"), + ), $this->client, $wait_for_action_attempt ?? $this->defaults["wait_for_action_attempt"], @@ -199,7 +210,9 @@ public function unlock_door( ); return ResolveActionAttempt::resolve_action_attempt( - ActionAttempt::from_json($res->action_attempt), + ActionAttempt::from_json( + Body::read($res, "action_attempt", "/locks/unlock_door"), + ), $this->client, $wait_for_action_attempt ?? $this->defaults["wait_for_action_attempt"], diff --git a/src/Routes/LocksSimulateClient.php b/src/Routes/LocksSimulateClient.php index 70a5f08d..02daf9fa 100644 --- a/src/Routes/LocksSimulateClient.php +++ b/src/Routes/LocksSimulateClient.php @@ -52,7 +52,13 @@ public function keypad_code_entry( ); return ResolveActionAttempt::resolve_action_attempt( - ActionAttempt::from_json($res->action_attempt), + ActionAttempt::from_json( + Body::read( + $res, + "action_attempt", + "/locks/simulate/keypad_code_entry", + ), + ), $this->client, $wait_for_action_attempt ?? $this->defaults["wait_for_action_attempt"], @@ -83,7 +89,13 @@ public function manual_lock_via_keypad( ); return ResolveActionAttempt::resolve_action_attempt( - ActionAttempt::from_json($res->action_attempt), + ActionAttempt::from_json( + Body::read( + $res, + "action_attempt", + "/locks/simulate/manual_lock_via_keypad", + ), + ), $this->client, $wait_for_action_attempt ?? $this->defaults["wait_for_action_attempt"], diff --git a/src/Routes/NoiseSensorsClient.php b/src/Routes/NoiseSensorsClient.php index 2bc01c76..4901eabb 100644 --- a/src/Routes/NoiseSensorsClient.php +++ b/src/Routes/NoiseSensorsClient.php @@ -76,6 +76,9 @@ public function list( ]), ); - return array_map(fn($r) => Device::from_json($r), $res->devices); + return array_map( + fn($r) => Device::from_json($r), + Body::read_list($res, "devices", "/noise_sensors/list"), + ); } } diff --git a/src/Routes/NoiseSensorsNoiseThresholdsClient.php b/src/Routes/NoiseSensorsNoiseThresholdsClient.php index 88558830..8d65eb62 100644 --- a/src/Routes/NoiseSensorsNoiseThresholdsClient.php +++ b/src/Routes/NoiseSensorsNoiseThresholdsClient.php @@ -68,7 +68,13 @@ public function create( ), ); - return NoiseThreshold::from_json($res->noise_threshold); + return NoiseThreshold::from_json( + Body::read( + $res, + "noise_threshold", + "/noise_sensors/noise_thresholds/create", + ), + ); } /** @@ -112,7 +118,13 @@ public function get(string $noise_threshold_id): NoiseThreshold ), ); - return NoiseThreshold::from_json($res->noise_threshold); + return NoiseThreshold::from_json( + Body::read( + $res, + "noise_threshold", + "/noise_sensors/noise_thresholds/get", + ), + ); } /** @@ -137,7 +149,11 @@ public function list(string $device_id): array return array_map( fn($r) => NoiseThreshold::from_json($r), - $res->noise_thresholds, + Body::read_list( + $res, + "noise_thresholds", + "/noise_sensors/noise_thresholds/list", + ), ); } diff --git a/src/Routes/PhonesClient.php b/src/Routes/PhonesClient.php index f56953e8..d8576183 100644 --- a/src/Routes/PhonesClient.php +++ b/src/Routes/PhonesClient.php @@ -60,7 +60,7 @@ public function get(string $device_id): Phone ]), ); - return Phone::from_json($res->phone); + return Phone::from_json(Body::read($res, "phone", "/phones/get")); } /** @@ -91,6 +91,9 @@ public function list( ]), ); - return array_map(fn($r) => Phone::from_json($r), $res->phones); + return array_map( + fn($r) => Phone::from_json($r), + Body::read_list($res, "phones", "/phones/list"), + ); } } diff --git a/src/Routes/PhonesSimulateClient.php b/src/Routes/PhonesSimulateClient.php index ba99c52e..353ceafb 100644 --- a/src/Routes/PhonesSimulateClient.php +++ b/src/Routes/PhonesSimulateClient.php @@ -62,6 +62,8 @@ public function create_sandbox_phone( ), ); - return Phone::from_json($res->phone); + return Phone::from_json( + Body::read($res, "phone", "/phones/simulate/create_sandbox_phone"), + ); } } diff --git a/src/Routes/SpacesClient.php b/src/Routes/SpacesClient.php index 9d3fca31..e4e3f398 100644 --- a/src/Routes/SpacesClient.php +++ b/src/Routes/SpacesClient.php @@ -136,7 +136,7 @@ public function create( ]), ); - return Space::from_json($res->space); + return Space::from_json(Body::read($res, "space", "/spaces/create")); } /** @@ -187,7 +187,7 @@ public function get( ]), ); - return Space::from_json($res->space); + return Space::from_json(Body::read($res, "space", "/spaces/get")); } /** @@ -236,7 +236,9 @@ public function get_related( ]), ); - return Batch::from_json($res->batch); + return Batch::from_json( + Body::read($res, "batch", "/spaces/get_related"), + ); } /** @@ -286,7 +288,10 @@ public function list( $on_response($res); } - return array_map(fn($r) => Space::from_json($r), $res->spaces); + return array_map( + fn($r) => Space::from_json($r), + Body::read_list($res, "spaces", "/spaces/list"), + ); } /** @@ -396,6 +401,6 @@ public function update( ]), ); - return Space::from_json($res->space); + return Space::from_json(Body::read($res, "space", "/spaces/update")); } } diff --git a/src/Routes/ThermostatsClient.php b/src/Routes/ThermostatsClient.php index 3af98e17..a20245d3 100644 --- a/src/Routes/ThermostatsClient.php +++ b/src/Routes/ThermostatsClient.php @@ -62,7 +62,13 @@ public function activate_climate_preset( ); return ResolveActionAttempt::resolve_action_attempt( - ActionAttempt::from_json($res->action_attempt), + ActionAttempt::from_json( + Body::read( + $res, + "action_attempt", + "/thermostats/activate_climate_preset", + ), + ), $this->client, $wait_for_action_attempt ?? $this->defaults["wait_for_action_attempt"], @@ -105,7 +111,9 @@ public function cool( ); return ResolveActionAttempt::resolve_action_attempt( - ActionAttempt::from_json($res->action_attempt), + ActionAttempt::from_json( + Body::read($res, "action_attempt", "/thermostats/cool"), + ), $this->client, $wait_for_action_attempt ?? $this->defaults["wait_for_action_attempt"], @@ -250,7 +258,9 @@ public function heat( ); return ResolveActionAttempt::resolve_action_attempt( - ActionAttempt::from_json($res->action_attempt), + ActionAttempt::from_json( + Body::read($res, "action_attempt", "/thermostats/heat"), + ), $this->client, $wait_for_action_attempt ?? $this->defaults["wait_for_action_attempt"], @@ -307,7 +317,9 @@ public function heat_cool( ); return ResolveActionAttempt::resolve_action_attempt( - ActionAttempt::from_json($res->action_attempt), + ActionAttempt::from_json( + Body::read($res, "action_attempt", "/thermostats/heat_cool"), + ), $this->client, $wait_for_action_attempt ?? $this->defaults["wait_for_action_attempt"], @@ -360,7 +372,10 @@ public function list( ]), ); - return array_map(fn($r) => Device::from_json($r), $res->devices); + return array_map( + fn($r) => Device::from_json($r), + Body::read_list($res, "devices", "/thermostats/list"), + ); } /** @@ -385,7 +400,9 @@ public function off( ); return ResolveActionAttempt::resolve_action_attempt( - ActionAttempt::from_json($res->action_attempt), + ActionAttempt::from_json( + Body::read($res, "action_attempt", "/thermostats/off"), + ), $this->client, $wait_for_action_attempt ?? $this->defaults["wait_for_action_attempt"], @@ -447,7 +464,9 @@ public function set_fan_mode( ); return ResolveActionAttempt::resolve_action_attempt( - ActionAttempt::from_json($res->action_attempt), + ActionAttempt::from_json( + Body::read($res, "action_attempt", "/thermostats/set_fan_mode"), + ), $this->client, $wait_for_action_attempt ?? $this->defaults["wait_for_action_attempt"], @@ -507,7 +526,13 @@ public function set_hvac_mode( ); return ResolveActionAttempt::resolve_action_attempt( - ActionAttempt::from_json($res->action_attempt), + ActionAttempt::from_json( + Body::read( + $res, + "action_attempt", + "/thermostats/set_hvac_mode", + ), + ), $this->client, $wait_for_action_attempt ?? $this->defaults["wait_for_action_attempt"], @@ -698,7 +723,13 @@ public function update_weekly_program( ); return ResolveActionAttempt::resolve_action_attempt( - ActionAttempt::from_json($res->action_attempt), + ActionAttempt::from_json( + Body::read( + $res, + "action_attempt", + "/thermostats/update_weekly_program", + ), + ), $this->client, $wait_for_action_attempt ?? $this->defaults["wait_for_action_attempt"], diff --git a/src/Routes/ThermostatsDailyProgramsClient.php b/src/Routes/ThermostatsDailyProgramsClient.php index 3d64dcbc..e6a71d7c 100644 --- a/src/Routes/ThermostatsDailyProgramsClient.php +++ b/src/Routes/ThermostatsDailyProgramsClient.php @@ -54,7 +54,11 @@ public function create( ); return ThermostatDailyProgram::from_json( - $res->thermostat_daily_program, + Body::read( + $res, + "thermostat_daily_program", + "/thermostats/daily_programs/create", + ), ); } @@ -109,7 +113,13 @@ public function update( ); return ResolveActionAttempt::resolve_action_attempt( - ActionAttempt::from_json($res->action_attempt), + ActionAttempt::from_json( + Body::read( + $res, + "action_attempt", + "/thermostats/daily_programs/update", + ), + ), $this->client, $wait_for_action_attempt ?? $this->defaults["wait_for_action_attempt"], diff --git a/src/Routes/ThermostatsSchedulesClient.php b/src/Routes/ThermostatsSchedulesClient.php index e9d1c21d..5f716d7f 100644 --- a/src/Routes/ThermostatsSchedulesClient.php +++ b/src/Routes/ThermostatsSchedulesClient.php @@ -70,7 +70,13 @@ public function create( ]), ); - return ThermostatSchedule::from_json($res->thermostat_schedule); + return ThermostatSchedule::from_json( + Body::read( + $res, + "thermostat_schedule", + "/thermostats/schedules/create", + ), + ); } /** @@ -108,7 +114,13 @@ public function get(string $thermostat_schedule_id): ThermostatSchedule ]), ); - return ThermostatSchedule::from_json($res->thermostat_schedule); + return ThermostatSchedule::from_json( + Body::read( + $res, + "thermostat_schedule", + "/thermostats/schedules/get", + ), + ); } /** @@ -137,7 +149,11 @@ public function list( return array_map( fn($r) => ThermostatSchedule::from_json($r), - $res->thermostat_schedules, + Body::read_list( + $res, + "thermostat_schedules", + "/thermostats/schedules/list", + ), ); } diff --git a/src/Routes/UserIdentitiesClient.php b/src/Routes/UserIdentitiesClient.php index 863a49b2..390fc72b 100644 --- a/src/Routes/UserIdentitiesClient.php +++ b/src/Routes/UserIdentitiesClient.php @@ -107,7 +107,9 @@ public function create( ]), ); - return UserIdentity::from_json($res->user_identity); + return UserIdentity::from_json( + Body::read($res, "user_identity", "/user_identities/create"), + ); } /** @@ -160,7 +162,13 @@ public function generate_instant_key( ), ); - return InstantKey::from_json($res->instant_key); + return InstantKey::from_json( + Body::read( + $res, + "instant_key", + "/user_identities/generate_instant_key", + ), + ); } /** @@ -194,7 +202,9 @@ public function get( ]), ); - return UserIdentity::from_json($res->user_identity); + return UserIdentity::from_json( + Body::read($res, "user_identity", "/user_identities/get"), + ); } /** @@ -276,7 +286,7 @@ public function list( return array_map( fn($r) => UserIdentity::from_json($r), - $res->user_identities, + Body::read_list($res, "user_identities", "/user_identities/list"), ); } @@ -300,7 +310,14 @@ public function list_accessible_devices(string $user_identity_id): array ), ); - return array_map(fn($r) => Device::from_json($r), $res->devices); + return array_map( + fn($r) => Device::from_json($r), + Body::read_list( + $res, + "devices", + "/user_identities/list_accessible_devices", + ), + ); } /** @@ -325,7 +342,11 @@ public function list_accessible_entrances(string $user_identity_id): array return array_map( fn($r) => AcsEntrance::from_json($r), - $res->acs_entrances, + Body::read_list( + $res, + "acs_entrances", + "/user_identities/list_accessible_entrances", + ), ); } @@ -347,7 +368,14 @@ public function list_acs_systems(string $user_identity_id): array ]), ); - return array_map(fn($r) => AcsSystem::from_json($r), $res->acs_systems); + return array_map( + fn($r) => AcsSystem::from_json($r), + Body::read_list( + $res, + "acs_systems", + "/user_identities/list_acs_systems", + ), + ); } /** @@ -368,7 +396,14 @@ public function list_acs_users(string $user_identity_id): array ]), ); - return array_map(fn($r) => AcsUser::from_json($r), $res->acs_users); + return array_map( + fn($r) => AcsUser::from_json($r), + Body::read_list( + $res, + "acs_users", + "/user_identities/list_acs_users", + ), + ); } /** diff --git a/src/Routes/UserIdentitiesUnmanagedClient.php b/src/Routes/UserIdentitiesUnmanagedClient.php index c794d2cb..9bf8f516 100644 --- a/src/Routes/UserIdentitiesUnmanagedClient.php +++ b/src/Routes/UserIdentitiesUnmanagedClient.php @@ -43,7 +43,9 @@ public function get(string $user_identity_id): UnmanagedUserIdentity ]), ); - return UnmanagedUserIdentity::from_json($res->user_identity); + return UnmanagedUserIdentity::from_json( + Body::read($res, "user_identity", "/user_identities/unmanaged/get"), + ); } /** @@ -90,7 +92,11 @@ public function list( return array_map( fn($r) => UnmanagedUserIdentity::from_json($r), - $res->user_identities, + Body::read_list( + $res, + "user_identities", + "/user_identities/unmanaged/list", + ), ); } diff --git a/src/Routes/WebhooksClient.php b/src/Routes/WebhooksClient.php index 29cbb648..9d53b37e 100644 --- a/src/Routes/WebhooksClient.php +++ b/src/Routes/WebhooksClient.php @@ -46,7 +46,9 @@ public function create(string $url, ?array $event_types = null): Webhook ]), ); - return Webhook::from_json($res->webhook); + return Webhook::from_json( + Body::read($res, "webhook", "/webhooks/create"), + ); } /** @@ -84,7 +86,7 @@ public function get(string $webhook_id): Webhook ]), ); - return Webhook::from_json($res->webhook); + return Webhook::from_json(Body::read($res, "webhook", "/webhooks/get")); } /** @@ -96,7 +98,10 @@ public function list(): array { $res = Body::decode($this->client->request("GET", "/webhooks/list")); - return array_map(fn($r) => Webhook::from_json($r), $res->webhooks); + return array_map( + fn($r) => Webhook::from_json($r), + Body::read_list($res, "webhooks", "/webhooks/list"), + ); } /** diff --git a/src/Routes/WorkspacesClient.php b/src/Routes/WorkspacesClient.php index a1336c9d..ab1ce1ac 100644 --- a/src/Routes/WorkspacesClient.php +++ b/src/Routes/WorkspacesClient.php @@ -99,7 +99,9 @@ public function create( ]), ); - return Workspace::from_json($res->workspace); + return Workspace::from_json( + Body::read($res, "workspace", "/workspaces/create"), + ); } /** @@ -111,7 +113,9 @@ public function get(): Workspace { $res = Body::decode($this->client->request("GET", "/workspaces/get")); - return Workspace::from_json($res->workspace); + return Workspace::from_json( + Body::read($res, "workspace", "/workspaces/get"), + ); } /** @@ -123,7 +127,10 @@ public function list(): array { $res = Body::decode($this->client->request("GET", "/workspaces/list")); - return array_map(fn($r) => Workspace::from_json($r), $res->workspaces); + return array_map( + fn($r) => Workspace::from_json($r), + Body::read_list($res, "workspaces", "/workspaces/list"), + ); } /** @@ -140,7 +147,9 @@ public function reset_sandbox( ); return ResolveActionAttempt::resolve_action_attempt( - ActionAttempt::from_json($res->action_attempt), + ActionAttempt::from_json( + Body::read($res, "action_attempt", "/workspaces/reset_sandbox"), + ), $this->client, $wait_for_action_attempt ?? $this->defaults["wait_for_action_attempt"], diff --git a/tests/MalformedResponseTest.php b/tests/MalformedResponseTest.php index e132c10a..e8cc0993 100644 --- a/tests/MalformedResponseTest.php +++ b/tests/MalformedResponseTest.php @@ -9,7 +9,9 @@ use GuzzleHttp\Psr7\Response; use PHPUnit\Framework\TestCase; use Seam\HttpApiError; +use Seam\InvalidResponseError; use Seam\Seam; +use Seam\SeamException; use Tests\Support\RecordingClient; /** @@ -84,6 +86,107 @@ public static function nonSeamErrorResponses(): array ]; } + /** + * @dataProvider malformedSuccessResponses + */ + public function testMalformedSuccessResponsesRaiseASeamError( + Response $response, + string $expected_message, + ): void { + $seam = $this->seam(new RecordingClient([$response])); + + try { + $seam->devices->get("d1"); + $this->fail("Expected an InvalidResponseError"); + } catch (InvalidResponseError $error) { + $this->assertInstanceOf(SeamException::class, $error); + $this->assertSame("/devices/get", $error->getPath()); + $this->assertSame("device", $error->getKey()); + $this->assertStringContainsString( + $expected_message, + $error->getMessage(), + ); + } + } + + public static function malformedSuccessResponses(): array + { + return [ + "missing the response key" => [ + RecordingClient::json(200, ["ok" => true]), + "which the response does not contain", + ], + "the wrong response key" => [ + RecordingClient::json(200, [ + "devices" => [["device_id" => "d1"]], + ]), + "which the response does not contain", + ], + "json that is not an object" => [ + RecordingClient::json(200, [1, 2]), + "instead of a response object", + ], + "empty body" => [ + RecordingClient::raw(200, "", "application/json"), + "instead of a response object", + ], + "malformed json" => [ + RecordingClient::raw(200, "{invalid", "application/json"), + "instead of a response object", + ], + "a gateway page with a json content type" => [ + RecordingClient::raw( + 200, + "Maintenance", + "application/json", + ), + "instead of a response object", + ], + ]; + } + + public function testAListResponseThatIsNotAListRaisesASeamError(): void + { + $seam = $this->seam( + new RecordingClient([ + RecordingClient::json(200, ["devices" => "not-a-list"]), + ]), + ); + + try { + $seam->devices->list(); + $this->fail("Expected an InvalidResponseError"); + } catch (InvalidResponseError $error) { + $this->assertSame("devices", $error->getKey()); + $this->assertStringContainsString( + "instead of a list", + $error->getMessage(), + ); + } + } + + public function testAMalformedActionAttemptPollRaisesASeamError(): void + { + $seam = $this->seam( + new RecordingClient([ + RecordingClient::json(200, [ + "action_attempt" => [ + "action_attempt_id" => "aa_1", + "status" => "pending", + ], + ]), + RecordingClient::json(200, ["ok" => true]), + ]), + ); + + $this->expectException(InvalidResponseError::class); + + $seam->action_attempts->get("aa_1", [ + "timeout" => 5.0, + "polling_interval" => 0.01, + ]); + } + /** * A redirect is not a success, so it must not be handed back to the * caller as though it were a resource.