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.