Skip to content

Commit 5cf14eb

Browse files
authored
Merge pull request #5 from infobip-community/add-whatsapp-essentials
Add whatsapp essentials, fix Readme
2 parents 79f7acf + da204f7 commit 5cf14eb

File tree

10 files changed

+264
-10
lines changed

10 files changed

+264
-10
lines changed

Cargo.toml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ homepage = "https://www.infobip.com/"
77
license = "MIT OR Apache-2.0"
88
name = "infobip_sdk"
99
repository = "https://github.com/infobip-community/infobip-api-rust-sdk"
10-
version = "0.1.0"
10+
version = "0.1.1"
1111

1212
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
1313

@@ -26,5 +26,6 @@ validator = { version = "0.16", features = ["derive"] }
2626
httpmock = "0.6"
2727

2828
[features]
29-
default = ["sms"]
30-
sms = []
29+
default = ["sms", "whatsapp"]
30+
sms = []
31+
whatsapp = []

README.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
![Workflow](https://github.com/infobip-community/infobip-api-rust-sdk/actions/workflows/rust.yml/badge.svg)
44
[![Licence](https://img.shields.io/github/license/infobip-community/infobip-api-rust-sdk)](LICENSE-MIT)
5+
![Crates.io](https://img.shields.io/crates/v/infobip_sdk)
6+
![Crate downlads](https://img.shields.io/crates/d/rust_sdk)
57

68
Client SDK to use the Infobip API with pure Rust.
79

@@ -12,7 +14,8 @@ handling. The module structure is divided by communication channel.
1214
---
1315

1416
## 📡 Supported Channels
15-
- [SMS](https://www.infobip.com/docs/api/channels/sms) (in progress)
17+
- [SMS](https://www.infobip.com/docs/api/channels/sms)
18+
- [WhatsApp](https://www.infobip.com/docs/api/channels/whatsapp) (partially, in progress)
1619

1720
More Channels to be added in the near future!
1821

@@ -26,7 +29,7 @@ load the configuration from the environment. To do that, export the variables `I
2629
To use the library, add the dependency to your projects `Cargo.toml`
2730
```toml
2831
[dependencies]
29-
infobip-sdk = "0.1"
32+
infobip_sdk = "0.1"
3033
```
3134

3235
## 🚀 Usage
@@ -131,7 +134,7 @@ model.
131134
You can speed up compile-times a bit by turning only the needed channels as library features.
132135
For example, to only build SMS, add the dependency like this:
133136
```toml
134-
infobip-sdk = { version = "0.1", features = ["sms"] }
137+
infobip_sdk = { version = "0.1", features = ["sms"] }
135138
```
136139
You can see the complete list of features in the Cargo.toml of the project. Feature names
137140
follow channel names.

README.tpl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
![Workflow](https://github.com/infobip-community/infobip-api-rust-sdk/actions/workflows/rust.yml/badge.svg)
44
[![Licence](https://img.shields.io/github/license/infobip-community/infobip-api-rust-sdk)](LICENSE-MIT)
5+
![Crates.io](https://img.shields.io/crates/v/infobip_sdk)
6+
![Crate downlads](https://img.shields.io/crates/d/rust_sdk)
57

68
{{readme}}
79

src/api/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ use crate::configuration::{ApiKey, Configuration};
1414
#[cfg(feature = "sms")]
1515
pub mod sms;
1616

17+
#[cfg(feature = "whatsapp")]
18+
pub mod whatsapp;
19+
1720
/// Holds the possible errors that can happen when calling the Infobip API.
1821
#[derive(Error, Debug)]
1922
pub enum SdkError {

src/api/sms.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ pub struct SmsClient {
4242

4343
impl SmsClient {
4444
/// Builds and returns a new asynchronous `SmsClient` with specified configuration.
45-
pub fn with_configuration(configuration: Configuration) -> SmsClient {
45+
pub fn with_configuration(configuration: Configuration) -> Self {
4646
SmsClient {
4747
configuration,
4848
client: reqwest::Client::new(),

src/api/whatsapp.rs

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
use std::collections::HashMap;
2+
3+
use reqwest::{Method, Response};
4+
use serde::Serialize;
5+
use validator::Validate;
6+
7+
use crate::api::{build_api_error, send_valid_json_request, SdkError, SdkResponse};
8+
use crate::configuration::Configuration;
9+
use crate::model::whatsapp::{SendTextRequestBody, SendTextResponseBody};
10+
11+
pub const PATH_SEND_TEXT: &str = "/whatsapp/1/message/text";
12+
13+
/// Main asynchronous client for the Infobip WhatsApp channel.
14+
#[derive(Clone, Debug)]
15+
pub struct WhatsappClient {
16+
configuration: Configuration,
17+
client: reqwest::Client,
18+
}
19+
20+
impl WhatsappClient {
21+
/// Builds and returns a new asynchronous `WhatsappClient` with specified configuration.
22+
pub fn with_configuration(configuration: Configuration) -> Self {
23+
WhatsappClient {
24+
configuration,
25+
client: reqwest::Client::new(),
26+
}
27+
}
28+
29+
async fn send_request<T: Validate + Serialize>(
30+
&self,
31+
request_body: T,
32+
parameters: HashMap<String, String>,
33+
method: Method,
34+
path: &str,
35+
) -> Result<Response, SdkError> {
36+
send_valid_json_request(
37+
&self.client,
38+
&self.configuration,
39+
request_body,
40+
parameters,
41+
method,
42+
path,
43+
)
44+
.await
45+
}
46+
47+
/// Send a text message to a single recipient. Text messages can only be successfully delivered
48+
/// if the recipient has contacted the business within the last 24 hours, otherwise template
49+
/// message should be used.
50+
pub async fn send_text(
51+
&self,
52+
request_body: SendTextRequestBody,
53+
) -> Result<SdkResponse<SendTextResponseBody>, SdkError> {
54+
let response = self
55+
.send_request(
56+
request_body,
57+
HashMap::new(),
58+
reqwest::Method::POST,
59+
PATH_SEND_TEXT,
60+
)
61+
.await?;
62+
63+
let status = response.status();
64+
let text = response.text().await?;
65+
66+
if status.is_success() {
67+
Ok(SdkResponse {
68+
body: serde_json::from_str(&text)?,
69+
status,
70+
})
71+
} else {
72+
Err(build_api_error(status, &text))
73+
}
74+
}
75+
}

src/lib.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
//! handling. The module structure is divided by communication channel.
77
//!
88
//! ## Supported Channels
9-
//! - [SMS](https://www.infobip.com/docs/api/channels/sms) (in progress)
9+
//! - [SMS](https://www.infobip.com/docs/api/channels/sms)
10+
//! - [WhatsApp](https://www.infobip.com/docs/api/channels/whatsapp) (partially, in progress)
1011
//!
1112
//! More Channels to be added in the near future!
1213
//!
@@ -20,7 +21,7 @@
2021
//! To use the library, add the dependency to your projects `Cargo.toml`
2122
//! ```toml
2223
//! [dependencies]
23-
//! infobip-sdk = "0.1"
24+
//! infobip_sdk = "0.1"
2425
//! ```
2526
//!
2627
//! ## Usage
@@ -128,7 +129,7 @@
128129
//! You can speed up compile-times a bit by turning only the needed channels as library features.
129130
//! For example, to only build SMS, add the dependency like this:
130131
//! ```toml
131-
//! infobip-sdk = { version = "0.1", features = ["sms"] }
132+
//! infobip_sdk = { version = "0.1", features = ["sms"] }
132133
//! ```
133134
//! You can see the complete list of features in the Cargo.toml of the project. Feature names
134135
//! follow channel names.

src/model/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,8 @@
55
#[cfg(feature = "sms")]
66
pub mod sms;
77

8+
#[cfg(feature = "whatsapp")]
9+
pub mod whatsapp;
10+
811
#[cfg(test)]
912
mod tests;

src/model/whatsapp.rs

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
//! Models for calling WhatsApp endpoints.
2+
3+
use serde_derive::{Deserialize, Serialize};
4+
use validator::Validate;
5+
6+
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Validate)]
7+
#[serde(rename_all = "camelCase")]
8+
pub struct TextMessageContent {
9+
/// Content of the message being sent.
10+
#[validate(length(min = 1, max = 4096))]
11+
pub text: String,
12+
13+
/// Allows for URL preview from within the message. If set to true, the message content must
14+
/// contain a URL starting with https:// or http://. Defaults to false.
15+
#[serde(skip_serializing_if = "Option::is_none")]
16+
pub preview_url: Option<bool>,
17+
}
18+
19+
impl TextMessageContent {
20+
pub fn new(text: String) -> Self {
21+
TextMessageContent {
22+
text,
23+
preview_url: None,
24+
}
25+
}
26+
}
27+
28+
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Validate)]
29+
#[serde(rename_all = "camelCase")]
30+
pub struct SendTextRequestBody {
31+
/// Registered WhatsApp sender number. Must be in international format and comply with
32+
/// WhatsApp's requirements.
33+
#[validate(length(min = 1, max = 24))]
34+
pub from: String,
35+
36+
/// Message recipient number. Must be in international format.
37+
#[validate(length(min = 1, max = 24))]
38+
pub to: String,
39+
40+
/// The ID that uniquely identifies the message sent.
41+
#[serde(skip_serializing_if = "Option::is_none")]
42+
#[validate(length(min = 0, max = 50))]
43+
pub message_id: Option<String>,
44+
45+
/// The content object to build a message that will be sent.
46+
#[validate]
47+
pub content: TextMessageContent,
48+
49+
/// Custom client data that will be included in a Delivery Report.
50+
#[serde(skip_serializing_if = "Option::is_none")]
51+
#[validate(length(min = 0, max = 4000))]
52+
pub callback_data: Option<String>,
53+
54+
#[serde(skip_serializing_if = "Option::is_none")]
55+
#[validate(url)]
56+
pub notify_url: Option<String>,
57+
}
58+
59+
impl SendTextRequestBody {
60+
pub fn new(from: String, to: String, content: TextMessageContent) -> Self {
61+
SendTextRequestBody {
62+
from,
63+
to,
64+
message_id: None,
65+
content,
66+
callback_data: None,
67+
notify_url: None,
68+
}
69+
}
70+
}
71+
72+
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
73+
#[serde(rename_all = "camelCase")]
74+
pub struct Status {
75+
/// Status group ID.
76+
#[serde(skip_serializing_if = "Option::is_none")]
77+
pub group_id: Option<i32>,
78+
79+
/// Status group name.
80+
#[serde(skip_serializing_if = "Option::is_none")]
81+
pub group_name: Option<String>,
82+
83+
/// Action that should be taken to eliminate the error.
84+
#[serde(skip_serializing_if = "Option::is_none")]
85+
pub action: Option<String>,
86+
87+
/// Status ID.
88+
#[serde(skip_serializing_if = "Option::is_none")]
89+
pub id: Option<i32>,
90+
91+
/// Status name.
92+
#[serde(skip_serializing_if = "Option::is_none")]
93+
pub name: Option<String>,
94+
95+
/// Human-readable description of the status.
96+
#[serde(skip_serializing_if = "Option::is_none")]
97+
pub description: Option<String>,
98+
}
99+
100+
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
101+
#[serde(rename_all = "camelCase")]
102+
pub struct SendTextResponseBody {
103+
/// The destination address of the message.
104+
#[serde(skip_serializing_if = "Option::is_none")]
105+
pub to: Option<String>,
106+
107+
/// Number of messages required to deliver.
108+
#[serde(skip_serializing_if = "Option::is_none")]
109+
pub message_count: Option<i32>,
110+
111+
/// The ID that uniquely identifies the message sent. If not passed, it will be automatically
112+
/// generated and returned in a response.
113+
#[serde(skip_serializing_if = "Option::is_none")]
114+
pub message_id: Option<String>,
115+
116+
/// Indicates the status of the message and how to recover from an error should there be any.
117+
#[serde(skip_serializing_if = "Option::is_none")]
118+
pub status: Option<Status>,
119+
}

tests/whatsapp.rs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// These tests need to be run manually, due to server state dependencies. The environment variables
2+
// IB_API_KEY, IB_BASE_URL, IB_TEST_SENDER, and IB_TEST_DESTINATION_NUMBER must be set.
3+
4+
#![cfg(feature = "whatsapp")]
5+
#![cfg(test)]
6+
7+
use std::env;
8+
9+
use reqwest::StatusCode;
10+
11+
use infobip_sdk::api::whatsapp::WhatsappClient;
12+
use infobip_sdk::configuration;
13+
use infobip_sdk::model::whatsapp::*;
14+
15+
const DUMMY_TEXT: &str = "Dummy text for tests. Some special chars: áéíø";
16+
17+
fn get_test_wa_client() -> WhatsappClient {
18+
WhatsappClient::with_configuration(
19+
configuration::Configuration::from_env_api_key()
20+
.expect("failed to build default test SMS client"),
21+
)
22+
}
23+
24+
fn get_test_destination_number() -> String {
25+
env::var("IB_TEST_DESTINATION_NUMBER").expect("failed to load test destination number")
26+
}
27+
28+
fn get_test_sender_number() -> String {
29+
env::var("IB_TEST_SENDER").expect("failed to load test sender number")
30+
}
31+
32+
#[ignore]
33+
#[tokio::test]
34+
async fn send_text_whatsapp() {
35+
let content = TextMessageContent::new(DUMMY_TEXT.to_string());
36+
37+
let request_body = SendTextRequestBody::new(
38+
get_test_sender_number(),
39+
get_test_destination_number(),
40+
content,
41+
);
42+
43+
let response = get_test_wa_client().send_text(request_body).await.unwrap();
44+
45+
assert_eq!(response.status, StatusCode::OK);
46+
assert!(!response.body.message_id.unwrap().is_empty());
47+
}

0 commit comments

Comments
 (0)