diff --git a/README.md b/README.md index e9d7e87..57d9f3e 100644 --- a/README.md +++ b/README.md @@ -1 +1,40 @@ -# Upstash Rust QStash SDK + +# Upstash QStash Rust SDK + +**QStash** is a robust, HTTP-based messaging and scheduling solution optimized for serverless and edge runtimes. With a stateless, HTTP-driven design, it supports a broad range of environments and platforms, including: + +- Serverless functions (e.g., AWS Lambda) – [Example](https://github.com/mertakman/qstash-rs/tree/main/examples/aws-lambda/main.rs) +- Cloudflare Workers – [Example](https://github.com/mertakman/qstash-rs/tree/main/examples/cloudflare-workers/main.rs) +- Fastly Compute@Edge +- Next.js, including [Edge runtime](https://nextjs.org/docs/api-reference/edge-runtime) +- Deno +- Client-side web and mobile applications +- WebAssembly +- Any other environment where HTTP-based communication is preferred over TCP + +## How QStash Works + +QStash serves as the intermediary message broker for serverless applications. By sending a simple HTTP request to QStash, you can include a destination, payload, and optional configurations. QStash then stores the message securely and reliably delivers it to the designated API endpoint. In cases where the destination is temporarily unavailable, QStash ensures at-least-once delivery by automatically retrying until the message is successfully received. + +## Quick Start + +### 1. Obtain Your Authorization Token + +To get started, head to the [Upstash Console](https://console.upstash.com/qstash) and copy your **QSTASH_TOKEN**. + +### 2. Explore Examples + +For API documentation and a quickstart guide, refer to the official [QStash API Documentation](https://upstash.com/docs/qstash/api/). Below, you'll find links to additional examples that demonstrate usage for each endpoint: + +- **Dead Letter Queue** – [Example](https://github.com/mertakman/qstash-rs/blob/main/examples/dead_letter_queue/main.rs) +- **Events** – [Example](https://github.com/mertakman/qstash-rs/blob/main/examples/events/main.rs) +- **LLM** – [Example](https://github.com/mertakman/qstash-rs/blob/main/examples/llm/main.rs) +- **Messages** – [Example](https://github.com/mertakman/qstash-rs/blob/main/examples/messages/main.rs) +- **Queues** – [Example](https://github.com/mertakman/qstash-rs/blob/main/examples/queues/main.rs) +- **Schedulers** – [Example](https://github.com/mertakman/qstash-rs/blob/main/examples/schedulers/main.rs) +- **Signing Keys** – [Example](https://github.com/mertakman/qstash-rs/blob/main/examples/signing_keys/main.rs) +- **URL Groups** – [Example](https://github.com/mertakman/qstash-rs/blob/main/examples/url_groups/main.rs) + +## Supported Environments + +QStash is ideal for use with serverless architectures and edge deployments, supporting scenarios where HTTP-based communication provides flexibility and compatibility with modern applications. diff --git a/examples/cloudflare-worker/mod.rs b/examples/cloudflare-workers/mod.rs similarity index 100% rename from examples/cloudflare-worker/mod.rs rename to examples/cloudflare-workers/mod.rs diff --git a/examples/dead_letter_queue/main.rs b/examples/dead_letter_queue/main.rs index b083271..2943029 100644 --- a/examples/dead_letter_queue/main.rs +++ b/examples/dead_letter_queue/main.rs @@ -1,12 +1,22 @@ use std::env; -use qstash_rs::client::QstashClient; +use qstash_rs::{client::QstashClient, dead_letter_queue::DlqQueryParams}; #[tokio::main] async fn main() -> Result<(), Box> { let api_key = env::var("QSTASH_API_KEY").expect("QSTASH_API_KEY not set"); - let client = QstashClient::builder().api_key(&api_key).build().unwrap(); + let client = QstashClient::builder().api_key(&api_key).build()?; + + let dlq_messages_list = client.dlq_list_messages(DlqQueryParams::default()).await?; + println!("{:#?}", dlq_messages_list); + + let message_list = vec![]; + let deleted_messages_list = client.dlq_delete_messages(message_list).await?; + println!("{:#?}", deleted_messages_list); + + let dlq_message_id = ""; + client.dlq_delete_message(dlq_message_id).await?; Ok(()) } diff --git a/examples/events/main.rs b/examples/events/main.rs index 69f0a2b..7b0442e 100644 --- a/examples/events/main.rs +++ b/examples/events/main.rs @@ -5,10 +5,12 @@ use qstash_rs::{client::QstashClient, events_types::EventsRequest}; #[tokio::main] async fn main() -> Result<(), Box> { let api_key = env::var("QSTASH_API_KEY").expect("QSTASH_API_KEY not set"); - let client = QstashClient::builder().api_key(&api_key).build().unwrap(); + let client = QstashClient::builder().api_key(&api_key).build()?; + println!("Starting the process to retrieve the event list."); let resp = client.list_events(EventsRequest::default()).await?; + println!("Successfully retrieved the events list."); + println!("Retrieved Events: {:#?}", resp); - println!("{:#?}", resp); Ok(()) } diff --git a/examples/llm/main.rs b/examples/llm/main.rs index 2559a8c..f759bd7 100644 --- a/examples/llm/main.rs +++ b/examples/llm/main.rs @@ -1,12 +1,28 @@ use std::env; -use qstash_rs::client::QstashClient; +use qstash_rs::{ + client::QstashClient, + llm_types::{ChatCompletionRequest, Message}, +}; #[tokio::main] async fn main() -> Result<(), Box> { let api_key = env::var("QSTASH_API_KEY").expect("QSTASH_API_KEY not set"); + let client = QstashClient::builder().api_key(&api_key).build()?; - let client = QstashClient::builder().api_key(&api_key).build().unwrap(); - + let mut chat_completion_request = ChatCompletionRequest::default(); + chat_completion_request.model = "meta-llama/Meta-Llama-3-8B-Instruct".to_string(); + chat_completion_request.messages = vec![Message { + role: "user".to_string(), + content: "What is the capital of Türkiye?".to_string(), + name: None, + }]; + + println!("Starting the process to create a chat completion."); + let resp = client + .create_chat_completion(chat_completion_request) + .await?; + println!("Retrieved response succesfully"); + println!("{:#?}", resp); Ok(()) } diff --git a/examples/messages/main.rs b/examples/messages/main.rs index c7360ec..89031d8 100644 --- a/examples/messages/main.rs +++ b/examples/messages/main.rs @@ -1,27 +1,62 @@ -use qstash_rs::client::QstashClient; +use http::{ + header::{AUTHORIZATION, CONTENT_TYPE}, + HeaderValue, +}; +use qstash_rs::{client::QstashClient, message_types::MessageResponseResult}; +use reqwest::header::HeaderMap; use std::env; #[tokio::main] async fn main() -> Result<(), Box> { let api_key = env::var("QSTASH_API_KEY").expect("QSTASH_API_KEY not set"); - let client = QstashClient::builder().api_key(&api_key).build().unwrap(); + let destination = "https://www.example.com".to_string(); + let body = "{\"message\": \"Hello, world!\"}".to_string(); - let message_ids = [ - "msg_id_0".to_string(), - "msg_id_1".to_string(), - "msg_id_2".to_string(), - "msg_id_3".to_string(), - ]; + let mut headers = HeaderMap::new(); - let client = QstashClient::builder().api_key(" ").build().unwrap(); + headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); + headers.insert("Upstash-Method", HeaderValue::from_static("POST")); + headers.insert("Upstash-Delay", HeaderValue::from_static("0s")); + headers.insert("Upstash-Retries", HeaderValue::from_static("3")); + headers.insert( + "Upstash-Forward-Custom-Header", + HeaderValue::from_static("custom-value"), + ); + + let client: QstashClient = QstashClient::builder().api_key(&api_key).build()?; + println!( + "Starting to publish message to the destination: {}", + destination + ); + + let publish_message_resp = client + .publish_message(&destination, headers, body.clone().into()) + .await?; + println!("Message published successfully to {}!", destination); + println!( + "Response from publishing message: {:#?}", + publish_message_resp + ); + + let message_id = match publish_message_resp { + MessageResponseResult::URLResponse(url_response) => url_response.message_id, + MessageResponseResult::URLGroupResponse(_) => { + panic!("Response is not of type URLResponse"); + } + }; + + println!("Retrieving message with id: {}", message_id); + let get_message_resp = client.get_message(&message_id).await?; + println!("Successfully retrieved message with id: {}.", message_id); + println!("Retrieved message details: {:#?}", get_message_resp); + + println!("Initiating cancellation of message with id: {}", message_id); + client.cancel_message(&message_id).await?; + println!( + "Message with id: {} has been cancelled successfully.", + message_id + ); - // client.publish_message(destination, headers, body).await?; - // client.enqueue_message(destination, queue_name, headers, body) - // client.batch_messages(destination, queue_name, headers, body) - // client.get_message(destination, queue_name, headers, body) - // client.cancel_message(&"msg_id_0".to_string()).await?; - // client.bulk_cancel_messages(message_ids[1..].to_vec()).await?; - // Ok(()) } diff --git a/examples/queues/main.rs b/examples/queues/main.rs index b083271..345681d 100644 --- a/examples/queues/main.rs +++ b/examples/queues/main.rs @@ -7,6 +7,5 @@ async fn main() -> Result<(), Box> { let api_key = env::var("QSTASH_API_KEY").expect("QSTASH_API_KEY not set"); let client = QstashClient::builder().api_key(&api_key).build().unwrap(); - Ok(()) } diff --git a/examples/schedulers/main.rs b/examples/schedulers/main.rs index 8ffaf1f..72905ba 100644 --- a/examples/schedulers/main.rs +++ b/examples/schedulers/main.rs @@ -5,7 +5,7 @@ use std::env; async fn main() -> Result<(), Box> { let api_key = env::var("QSTASH_API_KEY").expect("QSTASH_API_KEY not set"); - let client = QstashClient::builder().api_key(&api_key).build().unwrap(); + let client = QstashClient::builder().api_key(&api_key).build()?; Ok(()) } diff --git a/examples/signing_keys/main.rs b/examples/signing_keys/main.rs index b083271..86a03ff 100644 --- a/examples/signing_keys/main.rs +++ b/examples/signing_keys/main.rs @@ -8,5 +8,14 @@ async fn main() -> Result<(), Box> { let client = QstashClient::builder().api_key(&api_key).build().unwrap(); + let signing_keys = client.get_signing_keys().await?; + println!("Signing keys retrieved successfully"); + println!("{:#?}", signing_keys); + + println!("Rotating signing keys"); + let new_signing_keys = client.rotate_signing_keys().await?; + println!("Signing keys rotated successfully"); + println!("{:#?}", new_signing_keys); + Ok(()) } diff --git a/examples/url_groups/main.rs b/examples/url_groups/main.rs index 0c009c2..f9c1d6a 100644 --- a/examples/url_groups/main.rs +++ b/examples/url_groups/main.rs @@ -4,8 +4,7 @@ use qstash_rs::client::QstashClient; #[tokio::main] async fn main() -> Result<(), Box> { - // let api_key = env::var("QSTASH_API_KEY").expect("QSTASH_API_KEY not set"); - let api_key = "eyJVc2VySUQiOiJiMmIxZWIyYS1iNDUzLTQ3NzQtYTllMC0xOTYzMmJhMmE2YmQiLCJQYXNzd29yZCI6ImJhYTNmZmEyODNlYjQzMTdhOGJjY2ViYmIzYTliMGI2In0=".to_string(); + let api_key = env::var("QSTASH_API_KEY").expect("QSTASH_API_KEY not set"); let client = QstashClient::builder().api_key(&api_key).build().unwrap(); Ok(()) diff --git a/src/dead_letter_queue.rs b/src/dead_letter_queue.rs index 65a843a..ded46b7 100644 --- a/src/dead_letter_queue.rs +++ b/src/dead_letter_queue.rs @@ -6,14 +6,19 @@ use serde::{Deserialize, Serialize}; use crate::{client::QstashClient, errors::QstashError}; impl QstashClient { - pub async fn dlq_list_messages(&self,query_params: DlqQueryParams) -> Result { - let request = self.client.get_request_builder( - Method::GET, - self.base_url - .join("/v2/dlq/") - .map_err(|e| QstashError::InvalidRequestUrl(e.to_string()))?, - ) - .query(&query_params.to_query_params()); + pub async fn dlq_list_messages( + &self, + query_params: DlqQueryParams, + ) -> Result { + let request = self + .client + .get_request_builder( + Method::GET, + self.base_url + .join("/v2/dlq/") + .map_err(|e| QstashError::InvalidRequestUrl(e.to_string()))?, + ) + .query(&query_params.to_query_params()); let response = self .client @@ -90,51 +95,51 @@ pub struct DlqQueryParams { // By providing a cursor you can paginate through all of the messages in the DLQ #[serde(skip_serializing_if = "Option::is_none")] pub cursor: Option, - + // Filter DLQ messages by message id #[serde(skip_serializing_if = "Option::is_none")] pub message_id: Option, - + // Filter DLQ messages by url #[serde(skip_serializing_if = "Option::is_none")] pub url: Option, - + // Filter DLQ messages by url group #[serde(skip_serializing_if = "Option::is_none")] pub topic_name: Option, - + // Filter DLQ messages by schedule id #[serde(skip_serializing_if = "Option::is_none")] pub schedule_id: Option, - + // Filter DLQ messages by queue name #[serde(skip_serializing_if = "Option::is_none")] pub queue_name: Option, - + // Filter DLQ messages by API name #[serde(skip_serializing_if = "Option::is_none")] pub api: Option, - + // Filter DLQ messages by starting date, in milliseconds (Unix timestamp). This is inclusive #[serde(skip_serializing_if = "Option::is_none")] pub from_date: Option, - + // Filter DLQ messages by ending date, in milliseconds (Unix timestamp). This is inclusive #[serde(skip_serializing_if = "Option::is_none")] pub to_date: Option, - + // Filter DLQ messages by HTTP response status code #[serde(skip_serializing_if = "Option::is_none")] pub response_status: Option, - + // Filter DLQ messages by IP address of the publisher #[serde(skip_serializing_if = "Option::is_none")] pub caller_ip: Option, - + // The number of messages to return. Default and maximum is 100 #[serde(skip_serializing_if = "Option::is_none")] pub count: Option, - + // The sorting order of DLQ messages by timestamp. Valid values are "earliestFirst" and "latestFirst" #[serde(skip_serializing_if = "Option::is_none")] pub order: Option, @@ -143,65 +148,66 @@ pub struct DlqQueryParams { impl DlqQueryParams { pub fn to_query_params(&self) -> Vec<(String, String)> { let mut params = Vec::new(); - + if let Some(cursor) = &self.cursor { params.push(("cursor".to_string(), cursor.clone())); } - + if let Some(message_id) = &self.message_id { params.push(("messageId".to_string(), message_id.clone())); } - + if let Some(url) = &self.url { params.push(("url".to_string(), url.clone())); } - + if let Some(topic_name) = &self.topic_name { params.push(("topicName".to_string(), topic_name.clone())); } - + if let Some(schedule_id) = &self.schedule_id { params.push(("scheduleId".to_string(), schedule_id.clone())); } - + if let Some(queue_name) = &self.queue_name { params.push(("queueName".to_string(), queue_name.clone())); } - + if let Some(api) = &self.api { params.push(("api".to_string(), api.clone())); } - + if let Some(from_date) = &self.from_date { params.push(("fromDate".to_string(), from_date.to_string())); } - + if let Some(to_date) = &self.to_date { params.push(("toDate".to_string(), to_date.to_string())); } - + if let Some(response_status) = &self.response_status { params.push(("responseStatus".to_string(), response_status.to_string())); } - + if let Some(caller_ip) = &self.caller_ip { params.push(("callerIp".to_string(), caller_ip.clone())); } - + if let Some(count) = &self.count { params.push(("count".to_string(), count.to_string())); } - + if let Some(order) = &self.order { params.push(("order".to_string(), order.clone())); } - + params } } /// Represents a paginated response containing a list of messages. -#[derive(Serialize, Deserialize, Debug)] +#[derive(Default, Serialize, Deserialize, Debug)] +#[serde(default)] pub struct DLQMessagesList { /// A cursor which you can use in subsequent requests to paginate through all events. /// If no cursor is returned, you have reached the end of the events. @@ -289,7 +295,7 @@ pub struct DLQMessage { /// The response header of the last failed delivery attempt. #[serde(rename = "responseHeader")] - pub response_header: Option, + pub response_header: Option>>, /// The response body of the last failed delivery attempt if it is composed of UTF-8 characters only, empty otherwise. #[serde(rename = "responseBody")] @@ -300,7 +306,7 @@ pub struct DLQMessage { pub response_body_base64: Option, } -#[derive(Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize)] pub struct DLQDeleteMessagesResponse { pub deleted: u32, } @@ -309,7 +315,9 @@ pub struct DLQDeleteMessagesResponse { mod tests { use crate::client::QstashClient; - use crate::dead_letter_queue::{DLQDeleteMessagesResponse, DLQMessage, DLQMessagesList, DlqQueryParams}; + use crate::dead_letter_queue::{ + DLQDeleteMessagesResponse, DLQMessage, DLQMessagesList, DlqQueryParams, + }; use crate::errors::QstashError; use httpmock::Method::{DELETE, GET}; use httpmock::MockServer; @@ -345,7 +353,10 @@ mod tests { schedule_id: Some("sched123".to_string()), queue_name: Some("queue1".to_string()), response_status: Some(500), - response_header: Some("Header".to_string()), + response_header: Some(HashMap::from([( + "responseheader".to_string(), + vec!["exampleresponseheader".to_string()], + )])), response_body: Some("Internal Server Error".to_string()), response_body_base64: None, }], @@ -452,7 +463,10 @@ mod tests { schedule_id: Some("sched123".to_string()), queue_name: Some("queue1".to_string()), response_status: Some(500), - response_header: Some("Header".to_string()), + response_header: Some(HashMap::from([( + "responseheader".to_string(), + vec!["exampleresponseheader".to_string()], + )])), response_body: Some("Internal Server Error".to_string()), response_body_base64: None, }; diff --git a/src/events_types.rs b/src/events_types.rs index 94e937a..9f31692 100644 --- a/src/events_types.rs +++ b/src/events_types.rs @@ -74,16 +74,16 @@ impl EventsRequest { } } -#[derive(Debug, Deserialize, Serialize, PartialEq)] -#[serde(rename_all = "camelCase")] +#[derive(Debug, Default, Deserialize, Serialize, PartialEq)] +#[serde(rename_all = "camelCase", default)] pub struct EventsResponse { /// A cursor which you can use in subsequent requests to paginate through all events. If no cursor is returned, you have reached the end of the events. pub cursor: Option, pub events: Vec, } -#[derive(Debug, Deserialize, Serialize, PartialEq)] -#[serde(rename_all = "camelCase")] +#[derive(Debug, Default, Deserialize, Serialize, PartialEq)] +#[serde(rename_all = "camelCase", default)] pub struct Event { /// Timestamp of this log entry, in milliseconds pub time: i64, @@ -117,9 +117,11 @@ pub struct Event { pub queue_name: Option, } -#[derive(Debug, Deserialize, Serialize, PartialEq)] +#[derive(Debug, Default, Deserialize, Serialize, PartialEq)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum EventState { + #[default] + None, /// The message has been accepted and stored in QStash Created, /// The task is currently being processed by a worker. diff --git a/src/llm.rs b/src/llm.rs index 9532432..838c0cb 100644 --- a/src/llm.rs +++ b/src/llm.rs @@ -14,7 +14,7 @@ impl QstashClient { let request = self .client .get_request_builder( - Method::GET, + Method::POST, self.base_url .join("/llm/v1/chat/completions") .map_err(|e| QstashError::InvalidRequestUrl(e.to_string()))?, @@ -45,7 +45,7 @@ mod tests { use crate::client::QstashClient; use crate::errors::QstashError; use crate::llm_types::*; - use httpmock::Method::GET; + use httpmock::Method::POST; use httpmock::MockServer; use reqwest::StatusCode; use reqwest::Url; @@ -84,7 +84,7 @@ mod tests { content: "Hello! How can I assist you today?".to_string(), name: None, }, - finish_reason: "stop".to_string(), + finish_reason: Some("stop".to_string()), stop_reason: Some("\n".to_string()), index: 0, logprobs: Some(LogProbs { @@ -111,7 +111,7 @@ mod tests { }, }; let direct_mock = server.mock(|when, then| { - when.method(GET) + when.method(POST) .path("/llm/v1/chat/completions") .header("Authorization", "Bearer test_api_key") .json_body_obj(&chat_request); @@ -164,7 +164,7 @@ mod tests { top_p: Some(0.9), }; let direct_mock = server.mock(|when, then| { - when.method(GET) + when.method(POST) .path("/llm/v1/chat/completions") .header("Authorization", "Bearer test_api_key") .json_body_obj(&chat_request); @@ -214,7 +214,7 @@ mod tests { top_p: Some(0.9), }; let direct_mock = server.mock(|when, then| { - when.method(GET) + when.method(POST) .path("/llm/v1/chat/completions") .header("Authorization", "Bearer test_api_key") .json_body_obj(&chat_request); @@ -267,7 +267,7 @@ mod tests { {\"id\":\"chatcmpl-123\",\"object\":\"chat.completion.chunk\",\"created\":1625097600,\"model\":\"gpt-4\",\"choices\":[{\"delta\":{\"content\":\" World\"},\"finish_reason\":null,\"index\":0,\"logprobs\":null}]}\n\n\ [DONE]"; let stream_mock = server.mock(|when, then| { - when.method(GET) + when.method(POST) .path("/llm/v1/chat/completions") .header("Authorization", "Bearer test_api_key") .json_body_obj(&chat_request); @@ -330,7 +330,7 @@ mod tests { top_p: Some(0.9), }; let stream_mock = server.mock(|when, then| { - when.method(GET) + when.method(POST) .path("/llm/v1/chat/completions") .header("Authorization", "Bearer test_api_key") .json_body_obj(&chat_request); @@ -380,7 +380,7 @@ mod tests { top_p: Some(0.9), }; let stream_mock = server.mock(|when, then| { - when.method(GET) + when.method(POST) .path("/llm/v1/chat/completions") .header("Authorization", "Bearer test_api_key") .json_body_obj(&chat_request); @@ -437,7 +437,7 @@ mod tests { {\"id\":\"chatcmpl-123\",\"object\":\"chat.completion.chunk\",\"created\":1625097600,\"model\":\"gpt-4\",\"choices\":[{\"delta\":{\"content\":\" World\"},\"finish_reason\":null,\"index\":0,\"logprobs\":null}]}\n\n\ [DONE]"; let stream_mock = server.mock(|when, then| { - when.method(GET) + when.method(POST) .path("/llm/v1/chat/completions") .header("Authorization", "Bearer test_api_key") .json_body_obj(&chat_request); diff --git a/src/llm_types.rs b/src/llm_types.rs index 9a47983..ef54ed5 100644 --- a/src/llm_types.rs +++ b/src/llm_types.rs @@ -1,7 +1,8 @@ use crate::errors::QstashError; use serde::{Deserialize, Serialize}; -#[derive(Serialize, Deserialize, Debug)] +#[derive(Debug, Default, Deserialize, Serialize)] +#[serde(default)] pub struct ChatCompletionRequest { /// Name of the model. pub model: String, @@ -50,7 +51,8 @@ pub struct ChatCompletionRequest { pub top_p: Option, } -#[derive(Serialize, Deserialize, Debug, PartialEq)] +#[derive(Debug, Default, Deserialize, Serialize, PartialEq)] +#[serde(default)] pub struct Message { /// The role of the message author. One of `system`, `assistant`, or `user`. pub role: String, @@ -82,7 +84,8 @@ pub enum ChatCompletionResponse { Direct(DirectResponse), } -#[derive(Debug, Serialize, Deserialize, PartialEq)] +#[derive(Debug, Default, Serialize, Deserialize, PartialEq)] +#[serde(default)] pub struct DirectResponse { // A unique identifier for the chat completion pub id: String, @@ -100,13 +103,13 @@ pub struct DirectResponse { pub usage: Usage, } -#[derive(Debug, Serialize, Deserialize, PartialEq)] +#[derive(Debug, Default, Serialize, Deserialize, PartialEq)] pub struct Choice { // A chat completion message generated by the model pub message: Message, // The reason the model stopped generating tokens #[serde(rename = "finishReason")] - pub finish_reason: String, + pub finish_reason: Option, // The stop string or token id that caused the completion to stop #[serde(rename = "stopReason")] pub stop_reason: Option, @@ -144,7 +147,8 @@ pub struct TopLogProb { pub bytes: Option>, } -#[derive(Debug, Serialize, Deserialize, PartialEq)] +#[derive(Debug, Default, Serialize, Deserialize, PartialEq)] +#[serde(default)] pub struct Usage { // Number of tokens in the generated completion pub completion_tokens: i32, diff --git a/src/message_types.rs b/src/message_types.rs index 8b3c276..8ac5d94 100644 --- a/src/message_types.rs +++ b/src/message_types.rs @@ -3,8 +3,8 @@ use serde::de::{self}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use std::collections::HashMap; -#[derive(Debug, Deserialize, Serialize, PartialEq)] -#[serde(rename_all = "camelCase")] +#[derive(Debug, Default, Deserialize, Serialize, PartialEq)] +#[serde(rename_all = "camelCase", default)] pub struct Message { pub message_id: String, pub topic_name: String, @@ -28,8 +28,8 @@ pub struct MessageResponse { #[derive(Debug, Serialize, Deserialize, PartialEq)] #[serde(untagged)] pub enum MessageResponseResult { - SingleResponse(MessageResponse), - MultipleResponses(Vec), + URLResponse(MessageResponse), + URLGroupResponse(Vec), } #[derive(Debug, Serialize, Deserialize)] @@ -90,7 +90,7 @@ mod tests { let messages: MessageResponseResult = serde_json::from_str(single_json).unwrap(); match messages { - MessageResponseResult::SingleResponse(message) => { + MessageResponseResult::URLResponse(message) => { assert_eq!(message.message_id, "msd_1234"); assert_eq!(message.url, Some("https://www.example.com".into())); } @@ -115,7 +115,7 @@ mod tests { let messages: MessageResponseResult = serde_json::from_str(multiple_json).unwrap(); match messages { - MessageResponseResult::MultipleResponses(messages) => { + MessageResponseResult::URLGroupResponse(messages) => { assert_eq!(messages.len(), 2); assert_eq!(messages[0].message_id, "msd_1234"); assert_eq!(messages[0].url, Some("https://www.example.com".into())); diff --git a/src/messages.rs b/src/messages.rs index 3221ffa..60d604f 100644 --- a/src/messages.rs +++ b/src/messages.rs @@ -166,7 +166,7 @@ mod tests { let mut headers = HeaderMap::new(); headers.insert("content-type", HeaderValue::from_static("application/json")); let body = b"{\"key\":\"value\"}".to_vec(); - let expected_response = MessageResponseResult::SingleResponse(MessageResponse { + let expected_response = MessageResponseResult::URLResponse(MessageResponse { message_id: "msg123".to_string(), url: Some("https://example.com/publish".to_string()), deduplicated: Some(false), @@ -201,7 +201,7 @@ mod tests { let mut headers = HeaderMap::new(); headers.insert("content-type", HeaderValue::from_static("application/json")); let body = b"{\"key\":\"value\"}".to_vec(); - let expected_response = MessageResponseResult::MultipleResponses(vec![ + let expected_response = MessageResponseResult::URLGroupResponse(vec![ MessageResponse { message_id: "msg123".to_string(), url: Some("https://example.com/publish".to_string()), @@ -303,7 +303,7 @@ mod tests { let mut headers = HeaderMap::new(); headers.insert("content-type", HeaderValue::from_static("application/json")); let body = b"{\"key\":\"value\"}".to_vec(); - let expected_response = MessageResponseResult::SingleResponse(MessageResponse { + let expected_response = MessageResponseResult::URLResponse(MessageResponse { message_id: "msg125".to_string(), url: Some("https://example.com/enqueue".to_string()), deduplicated: Some(false), @@ -433,12 +433,12 @@ mod tests { }, ]; let expected_response = vec![ - MessageResponseResult::SingleResponse(MessageResponse { + MessageResponseResult::URLResponse(MessageResponse { message_id: "msg126".to_string(), url: Some("https://example.com/publish1".to_string()), deduplicated: Some(false), }), - MessageResponseResult::MultipleResponses(vec![ + MessageResponseResult::URLGroupResponse(vec![ MessageResponse { message_id: "msg127".to_string(), url: Some("https://example.com/publish2".to_string()), @@ -787,7 +787,7 @@ mod tests { headers.insert("content-type", HeaderValue::from_static("application/json")); headers.insert("X-Custom-Header", HeaderValue::from_static("CustomValue")); let body = b"{\"key\":\"value\"}".to_vec(); - let expected_response = MessageResponseResult::SingleResponse(MessageResponse { + let expected_response = MessageResponseResult::URLResponse(MessageResponse { message_id: "msg129".to_string(), url: Some("https://example.com/publish".to_string()), deduplicated: Some(false), @@ -825,7 +825,7 @@ mod tests { headers.insert("content-type", HeaderValue::from_static("text/plain")); headers.insert("X-Another-Header", HeaderValue::from_static("AnotherValue")); let body = b"Enqueue message".to_vec(); - let expected_response = MessageResponseResult::SingleResponse(MessageResponse { + let expected_response = MessageResponseResult::URLResponse(MessageResponse { message_id: "msg130".to_string(), url: Some("https://example.com/enqueue".to_string()), deduplicated: Some(false), diff --git a/src/schedules.rs b/src/schedules.rs index 89a5b8e..e207af1 100644 --- a/src/schedules.rs +++ b/src/schedules.rs @@ -120,8 +120,8 @@ pub struct CreateScheduleResponse { pub schedule_id: String, } /// Represents a single schedule object within the Response array. -#[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] +#[derive(Debug, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", default)] pub struct Schedule { /// The creation time of the object. Unix timestamp in milliseconds. pub created_at: i64, diff --git a/src/url_groups.rs b/src/url_groups.rs index 5a1aa9b..c425eb8 100644 --- a/src/url_groups.rs +++ b/src/url_groups.rs @@ -98,7 +98,8 @@ impl QstashClient { } } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Default, Serialize, Deserialize, Debug)] +#[serde(default)] pub struct UrlGroup { created_at: u64, updated_at: u64, @@ -106,11 +107,12 @@ pub struct UrlGroup { endpoints: Vec, } -#[derive(Serialize, Clone, Deserialize, Debug)] +#[derive(Default, Serialize, Clone, Deserialize, Debug)] +#[serde(default)] pub struct Endpoint { - #[serde(skip_serializing_if = "String::is_empty", default)] + #[serde(skip_serializing_if = "String::is_empty")] name: String, - #[serde(skip_serializing_if = "String::is_empty", default)] + #[serde(skip_serializing_if = "String::is_empty")] url: String, } @@ -123,10 +125,6 @@ mod tests { use reqwest::Url; use serde_json::json; - fn encode(input: &str) -> String { - urlencoding::encode(input).into_owned() - } - #[tokio::test] async fn test_upsert_url_group_endpoint_success() { let server = MockServer::start();