Skip to content

Commit f88cc98

Browse files
authored
Merge pull request #2 from infobip-community/init-structure
Init structure
2 parents 78ef398 + 02fd636 commit f88cc98

File tree

17 files changed

+1343
-9
lines changed

17 files changed

+1343
-9
lines changed

.github/workflows/rust.yml

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
1-
name: Rust
1+
name: Build and Checks
22

3-
on:
4-
push:
5-
branches: [ "main" ]
6-
pull_request:
7-
branches: [ "main" ]
3+
on: ["push", "pull_request"]
84

95
env:
106
CARGO_TERM_COLOR: always
@@ -18,7 +14,9 @@ jobs:
1814
- uses: actions/checkout@v3
1915
- name: Build
2016
run: cargo build --verbose
17+
- name: Check format
18+
run: cargo fmt --check
2119
- name: Run tests
2220
run: cargo test --verbose
2321
- name: Run clippy
24-
run: cargo clippy
22+
run: cargo clippy --no-deps

.gitignore

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,13 @@ Cargo.lock
88

99
# These are backup files generated by rustfmt
1010
**/*.rs.bk
11+
12+
13+
# Added by cargo
14+
15+
/target
16+
/Cargo.lock
17+
18+
/.idea/
19+
/.vscode/
20+
.swap

Cargo.toml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
[package]
2+
name = "infobip_sdk"
3+
version = "0.1.0"
4+
edition = "2021"
5+
license = "MIT"
6+
7+
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
8+
9+
[dependencies]
10+
derive_builder = "*"
11+
lazy_static = "*"
12+
regex = "*"
13+
reqwest = { version = "*", features = ["blocking", "json", "multipart"] }
14+
serde = { version = "*", features = ["derive"] }
15+
serde_derive = "*"
16+
serde_json = "*"
17+
thiserror = "*"
18+
tokio = { version = "*", features = ["full"] }
19+
validator = { version = "*", features = ["derive"] }
20+
21+
[dev-dependencies]
22+
httpmock = "*"
23+
24+
[features]
25+
default = ["sms"]
26+
sms = []
File renamed without changes.

README.md

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,55 @@
1-
# infobip-api-rust-sdk
2-
Rust SDK for Infobip's API
1+
# Infobip API Rust SDK
2+
3+
![Workflow](https://github.com/infobip-community/infobip-api-rust-sdk/actions/workflows/rust.yml/badge.svg)
4+
[![Licence](https://img.shields.io/github/license/infobip-community/infobip-api-rust-sdk)](LICENSE-MIT)
5+
6+
Client SDK to use the Infobip API with pure Rust.
7+
8+
This library enables you to use multiple Infobip communication channels, like SMS, MMS,
9+
Whatsapp, Email, etc. It abstracts the needed HTTP calls, and models payloads and error
10+
handling. The modules structure is divided by communication channel, which can be enabled as
11+
library features.
12+
---
13+
14+
## 📡 Supported Channels
15+
- [SMS](https://www.infobip.com/docs/api/channels/sms) (in progress)
16+
17+
More Channels to be added in the future.
18+
19+
## 🔐 Authentication
20+
To use the library, you'll need to setup an Infobip account. Then you can use your API Key and
21+
custom URL to call the endpoints. You can use the `Configuration::from_env_api_key()` method to
22+
load the configuration from the environment. To do that, export the variables `IB_API_KEY` and
23+
`IB_BASE_URL`.
24+
25+
## 📦 Installation
26+
To use the library, add the dependency to your projects `Cargo.toml`
27+
```toml
28+
[dependencies]
29+
infobip-sdk = "0.1"
30+
```
31+
32+
## 🚀 Usage
33+
To use the library, import the client and channel-specific models. For SMS, you can do it
34+
like this:
35+
```rust
36+
```
37+
38+
For more examples on how to use the library, you can check the tests/ directory and the
39+
included CLI examples.
40+
41+
## 👀 Examples
42+
The best way to learn how to use the library is to look at the integration tests under the
43+
[tests](./tests) directory, which work as you would use them in a real scenario.
44+
45+
## Using specific features
46+
You can speed up compile-times a bit by turning only the needed channels as library features.
47+
For example, to only build SMS, add the depedency like this:
48+
```toml
49+
infobip-sdk = { version = "0.1", features = ["sms"] }
50+
```
51+
You can see the complete list of features in the Cargo.toml of the project. Feature names
52+
follow channel names.
53+
54+
## ⚖️ License
55+
This library is distributed under the MIT license, found in the [LICENSE-MIT](LICENSE-MIT) file.

README.tpl

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Infobip API Rust SDK
2+
3+
![Workflow](https://github.com/infobip-community/infobip-api-rust-sdk/actions/workflows/rust.yml/badge.svg)
4+
[![Licence](https://img.shields.io/github/license/infobip-community/infobip-api-rust-sdk)](LICENSE-MIT)
5+
6+
{{readme}}
7+
8+
## ⚖️ License
9+
This library is distributed under the {{license}} license, found in the [LICENSE-MIT](LICENSE-MIT) file.

src/api/mod.rs

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
//! Endpoint functions and base response and error types
2+
use std::collections::HashMap;
3+
use std::fmt;
4+
5+
use reqwest;
6+
use reqwest::{RequestBuilder, Response, StatusCode};
7+
use serde_derive::{Deserialize, Serialize};
8+
use thiserror::Error;
9+
use validator::Validate;
10+
11+
use crate::configuration::{ApiKey, Configuration};
12+
13+
#[cfg(feature = "sms")]
14+
pub mod sms;
15+
16+
/// Holds the possible errors that can happen when calling the Infobip API.
17+
#[derive(Error, Debug)]
18+
pub enum SdkError {
19+
#[error("request body has field errors")]
20+
Validation(#[from] validator::ValidationErrors),
21+
22+
#[error("client error calling endpoint")]
23+
Reqwest(#[from] reqwest::Error),
24+
25+
#[error("serialization error")]
26+
Serde(#[from] serde_json::Error),
27+
28+
#[error("api request error")]
29+
ApiRequestError(#[from] ApiError),
30+
}
31+
32+
/// Holds the status code and error details when a 4xx or 5xx response is received.
33+
#[derive(Error, Clone, Debug)]
34+
pub struct ApiError {
35+
pub details: ApiErrorDetails,
36+
pub status: StatusCode,
37+
}
38+
39+
impl fmt::Display for ApiError {
40+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
41+
write!(
42+
f,
43+
"API request error: status: {} {}",
44+
self.status, self.details
45+
)
46+
}
47+
}
48+
49+
/// Holds information about a server-side error.
50+
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
51+
pub struct ServiceException {
52+
#[serde(rename = "messageId")]
53+
pub message_id: Option<String>,
54+
#[serde(rename = "text")]
55+
pub text: String,
56+
#[serde(rename = "validationErrors", skip_serializing_if = "Option::is_none")]
57+
pub validation_errors: Option<String>,
58+
}
59+
60+
/// Holds the exception produced by a server-side error.
61+
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
62+
pub struct RequestError {
63+
#[serde(rename = "serviceException")]
64+
pub service_exception: ServiceException,
65+
}
66+
67+
/// Holds the details about a 4xx/5xx server-side error.
68+
#[derive(Clone, Debug, Error, PartialEq, Serialize, Deserialize)]
69+
pub struct ApiErrorDetails {
70+
#[serde(rename = "requestError")]
71+
pub request_error: RequestError,
72+
}
73+
74+
impl fmt::Display for ApiErrorDetails {
75+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
76+
write!(
77+
f,
78+
"API request error: {}",
79+
serde_json::to_string(self).expect("error deserializing request error")
80+
)
81+
}
82+
}
83+
84+
/// Holds the status code and the response body of a successful API call.
85+
#[derive(Clone, Debug, PartialEq)]
86+
pub struct SdkResponse<T> {
87+
pub response_body: T,
88+
pub status: StatusCode,
89+
}
90+
91+
fn get_api_key_authorization_value(api_key: &ApiKey) -> String {
92+
let key = api_key.key.to_owned();
93+
let prefix = api_key
94+
.prefix
95+
.to_owned()
96+
.unwrap_or_else(|| "App".to_string());
97+
98+
format!("{} {}", prefix, key)
99+
}
100+
101+
// Async version of add_auth, uses async request builder.
102+
fn add_auth(mut builder: RequestBuilder, configuration: &Configuration) -> RequestBuilder {
103+
if let Some(api_key) = &configuration.api_key {
104+
builder = builder.header("Authorization", get_api_key_authorization_value(api_key));
105+
} else if let Some(basic_auth) = &configuration.basic_auth {
106+
builder = builder.basic_auth(
107+
basic_auth.username.to_owned(),
108+
basic_auth.password.to_owned(),
109+
);
110+
} else if let Some(token) = &configuration.bearer_access_token {
111+
builder = builder.bearer_auth(token);
112+
};
113+
114+
builder
115+
}
116+
117+
// Blocking version of add_auth, uses blocking request builder.
118+
fn add_auth_blocking(
119+
mut builder: reqwest::blocking::RequestBuilder,
120+
configuration: &Configuration,
121+
) -> reqwest::blocking::RequestBuilder {
122+
if let Some(api_key) = &configuration.api_key {
123+
builder = builder.header("Authorization", get_api_key_authorization_value(api_key));
124+
} else if let Some(basic_auth) = &configuration.basic_auth {
125+
builder = builder.basic_auth(
126+
basic_auth.username.to_owned(),
127+
basic_auth.password.to_owned(),
128+
);
129+
} else if let Some(token) = &configuration.bearer_access_token {
130+
builder = builder.bearer_auth(token);
131+
};
132+
133+
builder
134+
}
135+
136+
fn build_api_error(status: StatusCode, text: &str) -> SdkError {
137+
match serde_json::from_str(text) {
138+
Ok(details) => SdkError::ApiRequestError(ApiError { details, status }),
139+
Err(e) => SdkError::Serde(e),
140+
}
141+
}
142+
143+
async fn send_no_body_request(
144+
client: &reqwest::Client,
145+
configuration: &Configuration,
146+
query_parameters: HashMap<String, String>,
147+
method: reqwest::Method,
148+
path: &str,
149+
) -> Result<Response, SdkError> {
150+
let url = format!("{}{}", configuration.base_url, path);
151+
let mut builder = client.request(method, url).query(&query_parameters);
152+
153+
builder = add_auth(builder, configuration);
154+
155+
Ok(builder.send().await?)
156+
}
157+
158+
async fn send_json_request<T: Validate + serde::Serialize>(
159+
client: &reqwest::Client,
160+
configuration: &Configuration,
161+
request_body: T,
162+
query_parameters: HashMap<String, String>,
163+
method: reqwest::Method,
164+
path: &str,
165+
) -> Result<Response, SdkError> {
166+
let url = format!("{}{}", configuration.base_url, path);
167+
let mut builder = client
168+
.request(method, url)
169+
.json(&request_body)
170+
.query(&query_parameters);
171+
172+
builder = add_auth(builder, configuration);
173+
174+
Ok(builder.send().await?)
175+
}
176+
177+
async fn _send_multipart_request(
178+
client: &reqwest::Client,
179+
configuration: &Configuration,
180+
form: reqwest::multipart::Form,
181+
method: reqwest::Method,
182+
path: &str,
183+
) -> Result<Response, SdkError> {
184+
let url = format!("{}{}", configuration.base_url, path);
185+
let mut builder = client.request(method, url);
186+
187+
builder = add_auth(builder, configuration);
188+
189+
Ok(builder.multipart(form).send().await?)
190+
}
191+
192+
fn send_blocking_json_request<T: Validate + serde::Serialize>(
193+
client: &reqwest::blocking::Client,
194+
configuration: &Configuration,
195+
request_body: T,
196+
method: reqwest::Method,
197+
path: &str,
198+
) -> Result<reqwest::blocking::Response, SdkError> {
199+
request_body.validate()?;
200+
201+
let url = format!("{}{}", configuration.base_url, path);
202+
let mut builder = client.request(method, url);
203+
204+
builder = add_auth_blocking(builder, configuration);
205+
206+
Ok(builder.json(&request_body).send()?)
207+
}
208+
209+
mod tests;

0 commit comments

Comments
 (0)