1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
use std::{collections::HashMap, path::Path};
use anyhow::Result;
use http::{HeaderMap, Method};
use reqwest::{Client, Response};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use tokio::fs;
use url::Url;
const USER_AGENT: &str = "Requester/0.1.0";
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
pub struct RequestConfig {
#[serde(flatten)]
ctxs: HashMap<String, RequestContext>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
pub struct RequestContext {
#[serde(
with = "http_serde::method",
skip_serializing_if = "is_default",
default
)]
pub method: Method,
pub url: Url,
#[serde(skip_serializing_if = "is_empty_value", default = "default_params")]
pub params: Value,
#[serde(skip_serializing_if = "HeaderMap::is_empty", default)]
#[serde(with = "http_serde::header_map")]
pub headers: HeaderMap,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub body: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub user_agent: Option<String>,
}
fn is_default<T: Default + PartialEq>(t: &T) -> bool {
t == &T::default()
}
fn is_empty_value(v: &Value) -> bool {
v.is_null() || (v.is_object() && v.as_object().unwrap().is_empty())
}
fn default_params() -> Value {
serde_json::json!({})
}
impl RequestConfig {
pub async fn try_load(path: impl AsRef<Path>) -> Result<Self> {
let file = fs::read_to_string(path).await?;
let config: Self = serde_yaml::from_str(&file)?;
for (profile, ctx) in config.ctxs.iter() {
if !ctx.params.is_object() {
return Err(anyhow::anyhow!(
"params must be an object in profile: {}",
profile
));
}
}
Ok(config)
}
pub fn get(&self, profile: &str) -> Result<&RequestContext> {
self.ctxs
.get(profile)
.ok_or_else(|| anyhow::anyhow!("profile {} not found", profile))
}
pub async fn send(&self, profile: &str) -> Result<Response> {
let ctx = self
.ctxs
.get(profile)
.ok_or_else(|| anyhow::anyhow!("profile {} not found", profile))?;
ctx.send().await
}
}
impl RequestContext {
pub async fn send(&self) -> Result<Response> {
let mut url = self.url.clone();
let user_agent = self
.user_agent
.clone()
.unwrap_or_else(|| USER_AGENT.to_string());
match url.scheme() {
"http" | "https" => {
let qs = serde_qs::to_string(&self.params)?;
url.set_query(Some(&qs));
let client = Client::builder().user_agent(user_agent).build()?;
let mut builder = client
.request(self.method.clone(), url)
.headers(self.headers.clone());
if let Some(body) = &self.body {
match self.headers.get(http::header::CONTENT_TYPE) {
Some(content_type) => {
if content_type.to_str().unwrap().contains("application/json") {
builder = builder.json(body);
} else {
return Err(anyhow::anyhow!(
"unsupported content-type: {:?}",
content_type
));
}
}
None => {
builder = builder.json(body)
}
}
builder = builder.body(serde_json::to_string(body)?);
}
let res = builder.send().await?;
Ok(res)
}
_ => Err(anyhow::anyhow!("unsupported scheme")),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn send_request_should_work() {
let config = RequestConfig::try_load("fixtures/req.yml").await.unwrap();
let result = config.send("rust").await.unwrap();
assert_eq!(result.status(), 200);
}
}