From aa28f98ac6df230383c5e1837086c70dc60d2369 Mon Sep 17 00:00:00 2001 From: EdJoPaTo Date: Fri, 31 Jan 2025 07:50:43 +0100 Subject: [PATCH] feat(error)!: more specific errors including their source (#252) * feat(error)!: more specific errors including their source A failing file doesnt panic anymore with client-ureq. BREAKING CHANGE: decode_response methods are now private. They probably shouldnt have been public in the first place BREAKING CHANGE: mostly all Error variants are now different (only the Api error response is the same) * fix(error): add error for unuspported files in Wasm * fixup! fix(error): add error for unuspported files in Wasm --- Cargo.toml | 1 + src/client_reqwest.rs | 39 +++------ src/client_ureq.rs | 193 ++++++++++++++++++++---------------------- src/error.rs | 43 +++++++--- src/json.rs | 38 ++------- src/lib.rs | 4 +- src/parse_mode.rs | 4 +- src/test_json.rs | 26 ++++++ 8 files changed, 178 insertions(+), 170 deletions(-) create mode 100644 src/test_json.rs diff --git a/Cargo.toml b/Cargo.toml index 89fa1ad..1f4a368 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,7 @@ unsafe_code = "forbid" [lints.clippy] pedantic = { level = "warn", priority = -1 } nursery = { level = "warn", priority = -1 } +result_large_err = "allow" # due to ureq::Error struct_excessive_bools = "allow" # due to Telegram API # TODO: remove and fix (or allow explicitly on the specific problem) diff --git a/src/client_reqwest.rs b/src/client_reqwest.rs index 59fa97e..e9cd21d 100644 --- a/src/client_reqwest.rs +++ b/src/client_reqwest.rs @@ -39,31 +39,24 @@ impl AsyncApi { Self::builder().api_url(api_url).build() } - pub async fn decode_response(response: reqwest::Response) -> Result + async fn decode_response(response: reqwest::Response) -> Result where Output: serde::de::DeserializeOwned, { - let status_code = response.status().as_u16(); - match response.text().await { - Ok(message) => { - if status_code == 200 { - Ok(crate::json::decode(&message)?) - } else { - Err(Error::Api(crate::json::decode(&message)?)) - } - } - Err(error) => Err(Error::Decode(format!("{error:?}"))), + let success = response.status().is_success(); + let message = response.text().await?; + if success { + Ok(crate::json::decode(&message)?) + } else { + Err(Error::Api(crate::json::decode(&message)?)) } } } impl From for Error { fn from(error: reqwest::Error) -> Self { - let message = error.to_string(); - let code = error - .status() - .map_or(500, |status_code| status_code.as_u16()); - Self::Http { code, message } + // Prevent leakage of the bot token as its within the path + Self::HttpReqwest(error.without_url()) } } @@ -95,6 +88,7 @@ impl AsyncTelegramApi for AsyncApi { Self::decode_response(response).await } + #[cfg_attr(target_arch = "wasm32", allow(unused_variables))] async fn request_with_form_data( &self, method: &str, @@ -140,7 +134,7 @@ impl AsyncTelegramApi for AsyncApi { for (parameter_name, file_path, file_name) in files_with_paths { let file = tokio::fs::File::open(file_path) .await - .map_err(|error| Error::Encode(error.to_string()))?; + .map_err(Error::ReadFile)?; let part = multipart::Part::stream(file).file_name(file_name); form = form.part(parameter_name, part); } @@ -152,19 +146,14 @@ impl AsyncTelegramApi for AsyncApi { } #[cfg(target_arch = "wasm32")] - { - Err(Error::Encode(format!( - "calling {method:?} with files is currently unsupported in WASM due to missing form_data / attachment support. Was called with params {params:?} and files {files:?}", - ))) - } + Err(Error::WasmHasNoFileSupportYet) } } #[cfg(test)] -mod async_tests { +mod tests { use super::*; use crate::api_params::SendMessageParams; - use crate::json; #[tokio::test] async fn async_send_message_success() { @@ -186,7 +175,7 @@ mod async_tests { mock.assert(); drop(server); - json::assert_str(&response, response_string); + crate::test_json::assert_json_str(&response, response_string); } #[tokio::test] diff --git a/src/client_ureq.rs b/src/client_ureq.rs index 037635d..3ffb037 100644 --- a/src/client_ureq.rs +++ b/src/client_ureq.rs @@ -4,7 +4,6 @@ use std::time::Duration; use bon::Builder; use multipart::client::lazy::Multipart; use serde_json::Value; -use ureq::Response; use crate::trait_sync::TelegramApi; use crate::Error; @@ -31,34 +30,22 @@ impl Api { Self::builder().api_url(api_url).build() } - pub fn decode_response(response: Response) -> Result + fn decode_response( + response: Result, + ) -> Result where Output: serde::de::DeserializeOwned, { - match response.into_string() { - Ok(message) => crate::json::decode(&message), - Err(error) => Err(Error::Decode(format!("{error:?}"))), - } - } -} - -impl From for Error { - fn from(error: ureq::Error) -> Self { - match error { - ureq::Error::Status(code, response) => match response.into_string() { - Ok(message) => match serde_json::from_str(&message) { - Ok(json_result) => Self::Api(json_result), - Err(_) => Self::Http { code, message }, - }, - Err(_) => Self::Http { - code, - message: "Failed to decode response".to_string(), - }, - }, - ureq::Error::Transport(transport_error) => Self::Http { - message: format!("{transport_error:?}"), - code: 500, - }, + match response { + Ok(response) => { + let message = response.into_string().map_err(Error::DecodeUreqBody)?; + crate::json::decode(&message) + } + Err(ureq::Error::Status(_code, response)) => { + let message = response.into_string().map_err(Error::DecodeUreqBody)?; + Err(Error::Api(crate::json::decode(&message)?)) + } + Err(ureq::Error::Transport(error)) => Err(Error::HttpUreq(error)), } } } @@ -77,10 +64,10 @@ impl TelegramApi for Api { .post(&url) .set("Content-Type", "application/json"); let response = match params { - None => prepared_request.call()?, + None => prepared_request.call(), Some(data) => { let json = crate::json::encode(&data)?; - prepared_request.send_string(&json)? + prepared_request.send_string(&json) } }; Self::decode_response(response) @@ -117,7 +104,7 @@ impl TelegramApi for Api { } for (parameter_name, file_name, file_path) in files_with_names { - let file = std::fs::File::open(&file_path).unwrap(); + let file = std::fs::File::open(&file_path).map_err(Error::ReadFile)?; let file_extension = file_path .extension() .and_then(std::ffi::OsStr::to_str) @@ -136,7 +123,7 @@ impl TelegramApi for Api { "Content-Type", &format!("multipart/form-data; boundary={}", form_data.boundary()), ) - .send(form_data)?; + .send(form_data); Self::decode_response(response) } } @@ -164,10 +151,10 @@ mod tests { SetMyCommandsParams, SetWebhookParams, StopMessageLiveLocationParams, StopPollParams, UnbanChatMemberParams, UnpinChatMessageParams, }; - use crate::json; use crate::objects::{ AllowedUpdate, BotCommand, ChatPermissions, InlineQueryResultVenue, InputPollOption, }; + use crate::test_json::assert_json_str; macro_rules! case { ($method:ident, $status:literal, $body:ident $(, $params:ident )? ) => {{ @@ -200,7 +187,7 @@ mod tests { .allowed_updates(vec![AllowedUpdate::Message]) .build(); let response = case!(getUpdates, 200, response_string, params).unwrap(); - json::assert_str(&response, response_string); + assert_json_str(&response, response_string); assert_eq!(1, response.result.len()); let update = &response.result[0]; @@ -215,7 +202,7 @@ mod tests { .text("Hello!") .build(); let response = case!(sendMessage, 200, response_string, params).unwrap(); - json::assert_str(&response, response_string); + assert_json_str(&response, response_string); } #[test] @@ -241,7 +228,7 @@ mod tests { "{\"ok\":true,\"result\":true,\"description\":\"Webhook is already deleted\"}"; let params = SetWebhookParams::builder().url("").build(); let response = case!(setWebhook, 200, response_string, params).unwrap(); - json::assert_str(&response, response_string); + assert_json_str(&response, response_string); } #[test] @@ -250,42 +237,42 @@ mod tests { "{\"ok\":true,\"result\":true,\"description\":\"Webhook is already deleted\"}"; let params = DeleteWebhookParams::builder().build(); let response = case!(deleteWebhook, 200, response_string, params).unwrap(); - json::assert_str(&response, response_string); + assert_json_str(&response, response_string); } #[test] fn get_webhook_info_success() { let response_string = "{\"ok\":true,\"result\":{\"url\":\"\",\"has_custom_certificate\":false,\"pending_update_count\":0,\"allowed_updates\":[\"message\"]}}"; let response = case!(getWebhookInfo, 200, response_string).unwrap(); - json::assert_str(&response, response_string); + assert_json_str(&response, response_string); } #[test] fn get_me_success() { let response_string = "{\"ok\":true,\"result\":{\"id\":1276618370,\"is_bot\":true,\"first_name\":\"test_el_bot\",\"username\":\"el_mon_test_bot\",\"can_join_groups\":true,\"can_read_all_group_messages\":false,\"supports_inline_queries\":false}}"; let response = case!(getMe, 200, response_string).unwrap(); - json::assert_str(&response, response_string); + assert_json_str(&response, response_string); } #[test] fn log_out_success() { let response_string = "{\"ok\":true,\"result\":true}"; let response = case!(logOut, 200, response_string).unwrap(); - json::assert_str(&response, response_string); + assert_json_str(&response, response_string); } #[test] fn close_failure() { let response_string = "{\"ok\":false,\"description\":\"Unauthorized\",\"error_code\":401}"; - let response = case!(close, 400, response_string).unwrap_err(); - json::assert_str(&response, response_string); + let response = case!(close, 400, response_string).unwrap_err().unwrap_api(); + assert_json_str(&response, response_string); } #[test] fn close_success() { let response_string = "{\"ok\":true,\"result\":true}"; let response = case!(close, 200, response_string).unwrap(); - json::assert_str(&response, response_string); + assert_json_str(&response, response_string); } #[test] @@ -297,7 +284,7 @@ mod tests { .message_id(2747) .build(); let response = case!(forwardMessage, 200, response_string, params).unwrap(); - json::assert_str(&response, response_string); + assert_json_str(&response, response_string); } #[test] @@ -309,7 +296,7 @@ mod tests { .message_id(2747) .build(); let response = case!(copyMessage, 200, response_string, params).unwrap(); - json::assert_str(&response, response_string); + assert_json_str(&response, response_string); } #[test] @@ -321,7 +308,7 @@ mod tests { .longitude(6.95) .build(); let response = case!(sendLocation, 200, response_string, params).unwrap(); - json::assert_str(&response, response_string); + assert_json_str(&response, response_string); } #[test] @@ -334,7 +321,7 @@ mod tests { .longitude(6.95) .build(); let response = case!(editMessageLiveLocation, 200, response_string, params).unwrap(); - json::assert_str(&response, response_string); + assert_json_str(&response, response_string); } #[test] @@ -345,7 +332,7 @@ mod tests { .message_id(2752) .build(); let response = case!(stopMessageLiveLocation, 200, response_string, params).unwrap(); - json::assert_str(&response, response_string); + assert_json_str(&response, response_string); } #[test] @@ -359,7 +346,7 @@ mod tests { .address("Hoof") .build(); let response = case!(sendVenue, 200, response_string, params).unwrap(); - json::assert_str(&response, response_string); + assert_json_str(&response, response_string); } #[test] @@ -371,7 +358,7 @@ mod tests { .first_name("Meow") .build(); let response = case!(sendContact, 200, response_string, params).unwrap(); - json::assert_str(&response, response_string); + assert_json_str(&response, response_string); } #[test] @@ -386,7 +373,7 @@ mod tests { ]) .build(); let response = case!(sendPoll, 200, response_string, params).unwrap(); - json::assert_str(&response, response_string); + assert_json_str(&response, response_string); } #[test] @@ -394,7 +381,7 @@ mod tests { let response_string = "{\"ok\":true,\"result\":{\"message_id\":2757,\"from\":{\"id\":1276618370,\"is_bot\":true,\"first_name\":\"test_el_bot\",\"username\":\"el_mon_test_bot\"},\"date\":1618467133,\"chat\":{\"id\":275808073,\"type\":\"private\",\"username\":\"Ayrat555\",\"first_name\":\"Ayrat\",\"last_name\":\"Badykov\"},\"dice\":{\"emoji\":\"🎲\",\"value\":5}}}"; let params = SendDiceParams::builder().chat_id(275808073).build(); let response = case!(sendDice, 200, response_string, params).unwrap(); - json::assert_str(&response, response_string); + assert_json_str(&response, response_string); } #[test] @@ -405,7 +392,7 @@ mod tests { .action(ChatAction::Typing) .build(); let response = case!(sendChatAction, 200, response_string, params).unwrap(); - json::assert_str(&response, response_string); + assert_json_str(&response, response_string); } #[test] @@ -415,7 +402,7 @@ mod tests { .user_id(275808073_u64) .build(); let response = case!(getUserProfilePhotos, 200, response_string, params).unwrap(); - json::assert_str(&response, response_string); + assert_json_str(&response, response_string); } #[test] @@ -425,7 +412,7 @@ mod tests { .file_id("AgACAgIAAxUAAWB332IlzabFGWzaMrOdQ4ODVLyaAAKypzEbSX9wEEzMxT7F-grc3UA5DwAEAQADAgADYQADg0kCAAEfBA") .build(); let response = case!(getFile, 200, response_string, params).unwrap(); - json::assert_str(&response, response_string); + assert_json_str(&response, response_string); } #[test] @@ -436,7 +423,7 @@ mod tests { .user_id(275808073_u64) .build(); let response = case!(banChatMember, 200, response_string, params).unwrap(); - json::assert_str(&response, response_string); + assert_json_str(&response, response_string); } #[test] @@ -447,7 +434,7 @@ mod tests { .user_id(275808072_u64) .build(); let response = case!(unbanChatMember, 200, response_string, params).unwrap(); - json::assert_str(&response, response_string); + assert_json_str(&response, response_string); } #[test] @@ -462,7 +449,7 @@ mod tests { .permissions(perm) .build(); let response = case!(restrictChatMember, 200, response_string, params).unwrap(); - json::assert_str(&response, response_string); + assert_json_str(&response, response_string); } #[test] @@ -474,7 +461,7 @@ mod tests { .can_change_info(true) .build(); let response = case!(promoteChatMember, 200, response_string, params).unwrap(); - json::assert_str(&response, response_string); + assert_json_str(&response, response_string); } #[test] @@ -492,7 +479,7 @@ mod tests { params ) .unwrap(); - json::assert_str(&response, response_string); + assert_json_str(&response, response_string); } #[test] @@ -506,7 +493,7 @@ mod tests { .permissions(perm) .build(); let response = case!(setChatPermissions, 200, response_string, params).unwrap(); - json::assert_str(&response, response_string); + assert_json_str(&response, response_string); } #[test] @@ -516,7 +503,7 @@ mod tests { .chat_id(-1001368460856) .build(); let response = case!(exportChatInviteLink, 200, response_string, params).unwrap(); - json::assert_str(&response, response_string); + assert_json_str(&response, response_string); } #[test] @@ -526,7 +513,7 @@ mod tests { .chat_id(-1001368460856) .build(); let response = case!(createChatInviteLink, 200, response_string, params).unwrap(); - json::assert_str(&response, response_string); + assert_json_str(&response, response_string); } #[test] @@ -537,7 +524,7 @@ mod tests { .invite_link("https://t.me/joinchat/O458bA8hQ0MzNmQy") .build(); let response = case!(editChatInviteLink, 200, response_string, params).unwrap(); - json::assert_str(&response, response_string); + assert_json_str(&response, response_string); } #[test] @@ -548,7 +535,7 @@ mod tests { .invite_link("https://t.me/joinchat/O458bA8hQ0MzNmQy") .build(); let response = case!(revokeChatInviteLink, 200, response_string, params).unwrap(); - json::assert_str(&response, response_string); + assert_json_str(&response, response_string); } #[test] @@ -558,7 +545,7 @@ mod tests { .chat_id(-1001368460856) .build(); let response = case!(deleteChatPhoto, 200, response_string, params).unwrap(); - json::assert_str(&response, response_string); + assert_json_str(&response, response_string); } #[test] @@ -570,7 +557,7 @@ mod tests { .photo(file) .build(); let response = case!(sendPhoto, 200, response_string, params).unwrap(); - json::assert_str(&response, response_string); + assert_json_str(&response, response_string); } #[test] @@ -582,7 +569,7 @@ mod tests { .audio(file) .build(); let response = case!(sendAudio, 200, response_string, params).unwrap(); - json::assert_str(&response, response_string); + assert_json_str(&response, response_string); } #[test] @@ -595,7 +582,7 @@ mod tests { .thumbnail(file) .build(); let response = case!(sendAudio, 200, response_string, params).unwrap(); - json::assert_str(&response, response_string); + assert_json_str(&response, response_string); } #[test] @@ -609,7 +596,7 @@ mod tests { .audio(file) .build(); let response = case!(sendAudio, 200, response_string, params).unwrap(); - json::assert_str(&response, response_string); + assert_json_str(&response, response_string); } #[test] @@ -621,7 +608,7 @@ mod tests { .document(file) .build(); let response = case!(sendDocument, 200, response_string, params).unwrap(); - json::assert_str(&response, response_string); + assert_json_str(&response, response_string); } #[test] @@ -633,7 +620,7 @@ mod tests { .video(file) .build(); let response = case!(sendVideo, 200, response_string, params).unwrap(); - json::assert_str(&response, response_string); + assert_json_str(&response, response_string); } #[test] @@ -645,7 +632,7 @@ mod tests { .animation(file) .build(); let response = case!(sendAnimation, 200, response_string, params).unwrap(); - json::assert_str(&response, response_string); + assert_json_str(&response, response_string); } #[test] @@ -657,7 +644,7 @@ mod tests { .voice(file) .build(); let response = case!(sendVoice, 200, response_string, params).unwrap(); - json::assert_str(&response, response_string); + assert_json_str(&response, response_string); } #[test] @@ -669,7 +656,7 @@ mod tests { .video_note(file) .build(); let response = case!(sendVideoNote, 200, response_string, params).unwrap(); - json::assert_str(&response, response_string); + assert_json_str(&response, response_string); } #[test] @@ -683,7 +670,7 @@ mod tests { .photo(file) .build(); let response = case!(setChatPhoto, 200, response_string, params).unwrap(); - json::assert_str(&response, response_string); + assert_json_str(&response, response_string); } #[test] @@ -694,7 +681,7 @@ mod tests { .title("Frankenstein") .build(); let response = case!(setChatTitle, 200, response_string, params).unwrap(); - json::assert_str(&response, response_string); + assert_json_str(&response, response_string); } #[test] @@ -705,7 +692,7 @@ mod tests { .description("Frankenstein group") .build(); let response = case!(setChatDescription, 200, response_string, params).unwrap(); - json::assert_str(&response, response_string); + assert_json_str(&response, response_string); } #[test] @@ -716,7 +703,7 @@ mod tests { .message_id(2766) .build(); let response = case!(pinChatMessage, 200, response_string, params).unwrap(); - json::assert_str(&response, response_string); + assert_json_str(&response, response_string); } #[test] @@ -724,7 +711,7 @@ mod tests { let response_string = "{\"ok\":true,\"result\":true}"; let params = UnpinChatMessageParams::builder().chat_id(275808073).build(); let response = case!(unpinChatMessage, 200, response_string, params).unwrap(); - json::assert_str(&response, response_string); + assert_json_str(&response, response_string); } #[test] @@ -732,7 +719,7 @@ mod tests { let response_string = "{\"ok\":true,\"result\":true}"; let params = LeaveChatParams::builder().chat_id(-1001368460856).build(); let response = case!(leaveChat, 200, response_string, params).unwrap(); - json::assert_str(&response, response_string); + assert_json_str(&response, response_string); } #[test] @@ -740,7 +727,7 @@ mod tests { let response_string = "{\"ok\":true,\"result\":{\"id\":-1001368460856,\"type\":\"supergroup\",\"title\":\"Frankenstein\",\"photo\":{\"small_file_id\":\"AQADAgAT-kgrmy4AAwIAA8jhydkW____s1Cm6Dc_w8Ge7QUAAR8E\",\"small_file_unique_id\":\"AQAD-kgrmy4AA57tBQAB\",\"big_file_id\":\"AQADAgAT-kgrmy4AAwMAA8jhydkW____s1Cm6Dc_w8Gg7QUAAR8E\",\"big_file_unique_id\":\"AQAD-kgrmy4AA6DtBQAB\"},\"description\":\"Frankenstein group\",\"invite_link\":\"https://t.me/joinchat/smSXMzNKTwA0ZjFi\",\"permissions\":{\"can_send_messages\":true,\"can_send_polls\":true,\"can_send_other_messages\":true,\"can_add_web_page_previews\":true,\"can_change_info\":true,\"can_invite_users\":true,\"can_pin_messages\":true}}}"; let params = GetChatParams::builder().chat_id(-1001368460856).build(); let response = case!(getChat, 200, response_string, params).unwrap(); - json::assert_str(&response, response_string); + assert_json_str(&response, response_string); } #[test] @@ -750,7 +737,7 @@ mod tests { .chat_id(-1001368460856) .build(); let response = case!(getChatAdministrators, 200, response_string, params).unwrap(); - json::assert_str(&response, response_string); + assert_json_str(&response, response_string); } #[test] @@ -760,7 +747,7 @@ mod tests { .chat_id(-1001368460856) .build(); let response = case!(getChatMemberCount, 200, response_string, params).unwrap(); - json::assert_str(&response, response_string); + assert_json_str(&response, response_string); } #[test] @@ -771,7 +758,7 @@ mod tests { .user_id(275808073_u64) .build(); let response = case!(getChatMember, 200, response_string, params).unwrap(); - json::assert_str(&response, response_string); + assert_json_str(&response, response_string); } #[test] @@ -782,7 +769,7 @@ mod tests { .sticker_set_name("GBTDPack") .build(); let response = case!(setChatStickerSet, 200, response_string, params).unwrap(); - json::assert_str(&response, response_string); + assert_json_str(&response, response_string); } #[test] @@ -792,7 +779,7 @@ mod tests { .chat_id(-1001368460856) .build(); let response = case!(deleteChatStickerSet, 200, response_string, params).unwrap(); - json::assert_str(&response, response_string); + assert_json_str(&response, response_string); } #[test] @@ -803,7 +790,7 @@ mod tests { .text("text") .build(); let response = case!(answerCallbackQuery, 200, response_string, params).unwrap(); - json::assert_str(&response, response_string); + assert_json_str(&response, response_string); } #[test] @@ -822,7 +809,7 @@ mod tests { ]) .build(); let response = case!(setMyCommands, 200, response_string, params).unwrap(); - json::assert_str(&response, response_string); + assert_json_str(&response, response_string); } #[test] @@ -842,7 +829,7 @@ mod tests { .scope(BotCommandScope::Default) .build(); let response = case!(setMyCommands, 200, response_string, params).unwrap(); - json::assert_str(&response, response_string); + assert_json_str(&response, response_string); } #[test] @@ -856,7 +843,7 @@ mod tests { .language_code("es") .build(); let response = case!(deleteMyCommands, 200, response_string, params).unwrap(); - json::assert_str(&response, response_string); + assert_json_str(&response, response_string); } #[test] @@ -864,7 +851,7 @@ mod tests { let response_string = "{\"ok\":true,\"result\":[{\"command\":\"meow\",\"description\":\"mewo\"},{\"command\":\"meow1\",\"description\":\"mewo1\"}]}"; let params = GetMyCommandsParams::builder().build(); let response = case!(getMyCommands, 200, response_string, params).unwrap(); - json::assert_str(&response, response_string); + assert_json_str(&response, response_string); } #[test] @@ -875,7 +862,7 @@ mod tests { }); let params = GetMyCommandsParams::builder().scope(scope).build(); let response = case!(getMyCommands, 200, response_string, params).unwrap(); - json::assert_str(&response, response_string); + assert_json_str(&response, response_string); } #[test] @@ -895,7 +882,7 @@ mod tests { .results(vec![venue_result]) .build(); let response = case!(answerInlineQuery, 200, response_string, params).unwrap(); - json::assert_str(&response, response_string); + assert_json_str(&response, response_string); } #[test] @@ -907,7 +894,7 @@ mod tests { .message_id(2782) .build(); let response = case!(editMessageText, 200, response_string, params).unwrap(); - json::assert_str(&response, response_string); + assert_json_str(&response, response_string); } #[test] @@ -919,7 +906,7 @@ mod tests { .caption("Caption") .build(); let response = case!(editMessageCaption, 200, response_string, params).unwrap(); - json::assert_str(&response, response_string); + assert_json_str(&response, response_string); } #[test] @@ -930,7 +917,7 @@ mod tests { .message_id(495) .build(); let response = case!(stopPoll, 200, response_string, params).unwrap(); - json::assert_str(&response, response_string); + assert_json_str(&response, response_string); } #[test] @@ -941,7 +928,7 @@ mod tests { .message_id(2784) .build(); let response = case!(deleteMessage, 200, response_string, params).unwrap(); - json::assert_str(&response, response_string); + assert_json_str(&response, response_string); } #[test] @@ -953,7 +940,7 @@ mod tests { .sticker(file) .build(); let response = case!(sendSticker, 200, response_string, params).unwrap(); - json::assert_str(&response, response_string); + assert_json_str(&response, response_string); } #[test] @@ -961,7 +948,7 @@ mod tests { let response_string = "{\"ok\":true,\"result\":{\"name\":\"unocards\",\"title\":\"UNO Bot\",\"sticker_type\":\"regular\",\"contains_masks\":false,\"stickers\":[{\"file_id\":\"CAACAgQAAxUAAWCDxAQVJ6X7FGiBD5NyjN5DDvgfAALZAQACX1eZAAEqnpNt3SpG_x8E\",\"file_unique_id\":\"AgAD2QEAAl9XmQAB\",\"type\":\"regular\",\"width\":342,\"height\":512,\"is_animated\":false,\"is_video\":false,\"thumbnail\":{\"file_id\":\"AAMCBAADFQABYIPEBBUnpfsUaIEPk3KM3kMO-B8AAtkBAAJfV5kAASqek23dKkb_P75BGQAEAQAHbQADBBEAAh8E\",\"file_unique_id\":\"AQADP75BGQAEBBEAAg\",\"width\":85,\"height\":128,\"file_size\":2452},\"emoji\":\"dd\",\"set_name\":\"unocards\",\"file_size\":8898}]}}"; let params = GetStickerSetParams::builder().name("unocards").build(); let response = case!(getStickerSet, 200, response_string, params).unwrap(); - json::assert_str(&response, response_string); + assert_json_str(&response, response_string); } #[test] @@ -975,7 +962,7 @@ mod tests { .media(medias) .build(); let response = case!(sendMediaGroup, 200, response_string, params).unwrap(); - json::assert_str(&response, response_string); + assert_json_str(&response, response_string); } #[test] @@ -990,7 +977,7 @@ mod tests { .message_id(513) .build(); let response = case!(editMessageMedia, 200, response_string, params).unwrap(); - json::assert_str(&response, response_string); + assert_json_str(&response, response_string); } #[test] @@ -1000,9 +987,11 @@ mod tests { .allowed_updates(vec![AllowedUpdate::Message]) .build(); let response = case!(getUpdates, 200, response_string, params); + let err = response.unwrap_err(); + assert!(matches!(err, Error::JsonDecode { .. })); assert_eq!( - Err(Error::Decode("Error(\"key must be a string\", line: 1, column: 2) : \"{hey this json is invalid}\"".to_string())), - response + "JSON Decode Error: key must be a string at line 1 column 2 on {hey this json is invalid}", + err.to_string() ); } } diff --git a/src/error.rs b/src/error.rs index 80561a5..a59a702 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,19 +1,42 @@ -use serde::{Deserialize, Serialize}; - use crate::response::ErrorResponse; -#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, thiserror::Error)] +#[allow(clippy::large_enum_variant)] +#[derive(Debug, thiserror::Error)] #[non_exhaustive] -#[serde(untagged)] pub enum Error { - #[error("Http Error {code}: {message}")] - Http { code: u16, message: String }, #[error("Api Error {0:?}")] Api(ErrorResponse), - #[error("Decode Error {0}")] - Decode(String), - #[error("Encode Error {0}")] - Encode(String), + + #[cfg(any(feature = "client-reqwest", feature = "client-ureq"))] + #[error("JSON Decode Error: {source} on {input}")] + JsonDecode { + source: serde_json::Error, + input: String, + }, + #[cfg(any(feature = "client-reqwest", feature = "client-ureq"))] + #[error("JSON Encode Error: {source} on {input}")] + JsonEncode { + source: serde_json::Error, + input: String, + }, + + #[error("Read File Error: {0}")] + ReadFile(#[source] std::io::Error), + + #[cfg(all(feature = "client-reqwest", target_arch = "wasm32"))] + #[error("Handling files is not yet supported in Wasm due to missing form_data / attachment support. Pull Request welcome!")] + WasmHasNoFileSupportYet, + + #[cfg(feature = "client-reqwest")] + #[error("HTTP error: {0}")] + HttpReqwest(#[source] reqwest::Error), + + #[cfg(feature = "client-ureq")] + #[error("HTTP error: {0}")] + HttpUreq(#[source] ureq::Transport), + #[cfg(feature = "client-ureq")] + #[error("Decode Body Error: {0}")] + DecodeUreqBody(#[source] std::io::Error), } impl Error { diff --git a/src/json.rs b/src/json.rs index 389ca4a..8ea1567 100644 --- a/src/json.rs +++ b/src/json.rs @@ -5,7 +5,10 @@ pub fn decode(string: &str) -> Result where T: serde::de::DeserializeOwned, { - serde_json::from_str(string).map_err(|error| Error::Decode(format!("{error:?} : {string:?}"))) + serde_json::from_str(string).map_err(|error| Error::JsonDecode { + source: error, + input: string.to_owned(), + }) } /// Shortcut for [`serde_json::to_string`] with [`crate::Error`]. @@ -13,33 +16,8 @@ pub fn encode(value: &T) -> Result where T: serde::ser::Serialize + std::fmt::Debug, { - serde_json::to_string(value).map_err(|error| Error::Encode(format!("{error:?} : {value:?}"))) -} - -#[cfg(test)] -#[track_caller] -pub fn assert_str(value: &T, expected: &str) -where - T: serde::ser::Serialize, -{ - let actual = serde_json::to_string(value).expect("Should be able to stringify to JSON"); - assert!( - expected == actual, - "value should equal the expected JSON\n expected: {expected}\n actual: {actual}" - ); -} - -#[test] -fn assert_str_works() { - let value = serde_json::json!({"code": 42}); - let expected = r#"{"code":42}"#; - assert_str(&value, expected); -} - -#[test] -#[should_panic = "value should equal the expected JSON"] -fn assert_str_fails() { - let value = serde_json::json!({"code": 42}); - let expected = r#"{"foo":"bar"}"#; - assert_str(&value, expected); + serde_json::to_string(value).map_err(|error| Error::JsonEncode { + source: error, + input: format!("{value:?}"), + }) } diff --git a/src/lib.rs b/src/lib.rs index 66fb48a..76c5bff 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -26,12 +26,14 @@ mod client_reqwest; #[cfg(feature = "client-ureq")] mod client_ureq; mod error; -#[cfg(any(test, feature = "client-ureq", feature = "client-reqwest"))] +#[cfg(any(feature = "client-reqwest", feature = "client-ureq"))] mod json; mod macros; pub mod objects; mod parse_mode; pub mod response; +#[cfg(test)] +mod test_json; #[cfg(feature = "trait-async")] mod trait_async; #[cfg(feature = "trait-sync")] diff --git a/src/parse_mode.rs b/src/parse_mode.rs index 8e86059..13c9722 100644 --- a/src/parse_mode.rs +++ b/src/parse_mode.rs @@ -51,10 +51,10 @@ impl Display for ParseMode { #[test] fn serde_markdown_works() { - crate::json::assert_str(&ParseMode::MarkdownV2, r#""MarkdownV2""#); + crate::test_json::assert_json_str(&ParseMode::MarkdownV2, r#""MarkdownV2""#); } #[test] fn serde_html_works() { - crate::json::assert_str(&ParseMode::Html, r#""HTML""#); + crate::test_json::assert_json_str(&ParseMode::Html, r#""HTML""#); } diff --git a/src/test_json.rs b/src/test_json.rs new file mode 100644 index 0000000..8a0dee6 --- /dev/null +++ b/src/test_json.rs @@ -0,0 +1,26 @@ +#[track_caller] +pub fn assert_json_str(value: &T, expected: &str) +where + T: serde::ser::Serialize, +{ + let actual = serde_json::to_string(value).expect("Should be able to stringify to JSON"); + assert!( + expected == actual, + "value should equal the expected JSON\n expected: {expected}\n actual: {actual}" + ); +} + +#[test] +fn assert_str_works() { + let value = serde_json::json!({"code": 42}); + let expected = r#"{"code":42}"#; + assert_json_str(&value, expected); +} + +#[test] +#[should_panic = "value should equal the expected JSON"] +fn assert_str_fails() { + let value = serde_json::json!({"code": 42}); + let expected = r#"{"foo":"bar"}"#; + assert_json_str(&value, expected); +}