automaat_processor_http_request/
lib.rs1#![deny(
80 clippy::all,
81 clippy::cargo,
82 clippy::nursery,
83 clippy::pedantic,
84 deprecated_in_future,
85 future_incompatible,
86 missing_docs,
87 nonstandard_style,
88 rust_2018_idioms,
89 rustdoc,
90 warnings,
91 unused_results,
92 unused_qualifications,
93 unused_lifetimes,
94 unused_import_braces,
95 unsafe_code,
96 unreachable_pub,
97 trivial_casts,
98 trivial_numeric_casts,
99 missing_debug_implementations,
100 missing_copy_implementations
101)]
102#![warn(variant_size_differences)]
103#![allow(clippy::multiple_crate_versions, missing_doc_code_examples)]
104#![doc(html_root_url = "https://docs.rs/automaat-processor-http-request/0.1.0")]
105
106use automaat_core::{Context, Processor};
107use reqwest::{header, Client};
108use serde::{Deserialize, Serialize};
109use std::{error, fmt, str::FromStr};
110use url::Url;
111
112#[cfg_attr(feature = "juniper", derive(juniper::GraphQLObject))]
114#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
115pub struct HttpRequest {
116 #[serde(with = "url_serde")]
118 pub url: Url,
119
120 pub method: Method,
122
123 pub headers: Vec<Header>,
125
126 pub body: Option<String>,
128
129 pub assert_status: Vec<i32>,
132}
133
134#[cfg_attr(feature = "juniper", derive(juniper::GraphQLEnum))]
136#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
137pub enum Method {
138 CONNECT,
140
141 DELETE,
143
144 GET,
146
147 HEAD,
149
150 OPTIONS,
152
153 PATCH,
155
156 POST,
158
159 PUT,
161
162 TRACE,
164}
165
166impl From<Method> for reqwest::Method {
167 fn from(method: Method) -> Self {
168 match method {
169 Method::CONNECT => Self::CONNECT,
170 Method::DELETE => Self::DELETE,
171 Method::GET => Self::GET,
172 Method::HEAD => Self::HEAD,
173 Method::OPTIONS => Self::OPTIONS,
174 Method::PATCH => Self::PATCH,
175 Method::POST => Self::POST,
176 Method::PUT => Self::PUT,
177 Method::TRACE => Self::TRACE,
178 }
179 }
180}
181
182#[cfg_attr(feature = "juniper", derive(juniper::GraphQLInputObject))]
184#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
185pub struct Header {
186 pub name: String,
188
189 pub value: String,
191}
192
193impl Header {
194 pub fn new(name: &str, value: &str) -> Self {
196 Self {
197 name: name.to_owned(),
198 value: value.to_owned(),
199 }
200 }
201}
202
203#[cfg(feature = "juniper")]
212#[graphql(name = "HttpRequestInput")]
213#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, juniper::GraphQLInputObject)]
214pub struct Input {
215 #[serde(with = "url_serde")]
216 url: Url,
217 method: Method,
218 headers: Option<Vec<Header>>,
219 body: Option<String>,
220 assert_status: Option<Vec<i32>>,
221}
222
223#[cfg(feature = "juniper")]
224impl From<Input> for HttpRequest {
225 fn from(input: Input) -> Self {
226 Self {
227 url: input.url,
228 method: input.method,
229 headers: input.headers.unwrap_or_else(Default::default),
230 body: input.body,
231 assert_status: input.assert_status.unwrap_or_else(Default::default),
232 }
233 }
234}
235
236impl<'a> Processor<'a> for HttpRequest {
237 const NAME: &'static str = "HTTP Request";
238
239 type Error = Error;
240 type Output = String;
241
242 fn validate(&self) -> Result<(), Self::Error> {
249 for header in &self.headers {
250 let _ = header::HeaderName::from_str(header.name.as_str())?;
251 let _ = header::HeaderValue::from_str(header.value.as_str())?;
252 }
253
254 Ok(())
255 }
256
257 fn run(&self, _context: &Context) -> Result<Option<Self::Output>, Self::Error> {
277 let mut request = Client::new().request(self.method.into(), self.url.as_str());
279
280 let mut map = header::HeaderMap::new();
282 for header in &self.headers {
283 let _ = map.insert(
284 header.name.as_str().parse::<header::HeaderName>()?,
285 header.value.as_str().parse()?,
286 );
287 }
288
289 if let Some(body) = self.body.to_owned() {
291 request = request.body(body);
292 }
293
294 let mut response = request.headers(map).send()?;
296
297 let status = i32::from(response.status().as_u16());
299 if !self.assert_status.is_empty() && !self.assert_status.contains(&status) {
300 return Err(Error::Status(status));
301 }
302
303 let body = response.text()?;
305 if body.is_empty() {
306 Ok(None)
307 } else {
308 Ok(Some(body))
309 }
310 }
311}
312
313#[derive(Debug)]
318pub enum Error {
319 Response(reqwest::Error),
321
322 Header(String),
324
325 Status(i32),
327
328 #[doc(hidden)]
329 __Unknown, }
331
332impl fmt::Display for Error {
333 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
334 match *self {
335 Error::Response(ref err) => write!(f, "Response error: {}", err),
336 Error::Header(ref err) => write!(f, "Invalid header: {}", err),
337 Error::Status(status) => write!(f, "Invalid status code: {}", status),
338 Error::__Unknown => unreachable!(),
339 }
340 }
341}
342
343impl error::Error for Error {
344 fn source(&self) -> Option<&(dyn error::Error + 'static)> {
345 match *self {
346 Error::Response(ref err) => Some(err),
347 Error::Header(_) | Error::Status(_) => None,
348 Error::__Unknown => unreachable!(),
349 }
350 }
351}
352
353impl From<reqwest::Error> for Error {
354 fn from(err: reqwest::Error) -> Self {
355 Error::Response(err)
356 }
357}
358
359impl From<header::InvalidHeaderName> for Error {
360 fn from(err: header::InvalidHeaderName) -> Self {
361 Error::Header(err.to_string())
362 }
363}
364
365impl From<header::InvalidHeaderValue> for Error {
366 fn from(err: header::InvalidHeaderValue) -> Self {
367 Error::Header(err.to_string())
368 }
369}
370
371#[cfg(test)]
372mod tests {
373 use super::*;
374
375 fn processor_stub() -> HttpRequest {
376 HttpRequest {
377 url: Url::parse("https://httpbin.org/status/200").unwrap(),
378 method: Method::GET,
379 headers: vec![],
380 body: None,
381 assert_status: vec![],
382 }
383 }
384
385 mod run {
386 use super::*;
387
388 #[test]
389 fn test_empty_response() {
390 let processor = processor_stub();
391
392 let context = Context::new().unwrap();
393 let output = processor.run(&context).unwrap();
394
395 assert!(output.is_none())
396 }
397
398 #[test]
399 fn test_response_body() {
400 let mut processor = processor_stub();
401 processor.url = Url::parse("https://httpbin.org/range/5").unwrap();
402
403 let context = Context::new().unwrap();
404 let output = processor.run(&context).unwrap();
405
406 assert_eq!(output, Some("abcde".to_owned()))
407 }
408
409 #[test]
410 fn test_request_body() {
411 let mut processor = processor_stub();
412 processor.url = Url::parse("https://httpbin.org/anything").unwrap();
413 processor.body = Some("hello world".to_owned());
414
415 let context = Context::new().unwrap();
416 let output = processor.run(&context).unwrap().expect("Some");
417
418 assert!(output.contains("hello world"));
419 }
420
421 #[test]
422 fn test_valid_status() {
423 let mut processor = processor_stub();
424 processor.url = Url::parse("https://httpbin.org/status/200").unwrap();
425 processor.assert_status = vec![200, 204];
426
427 let context = Context::new().unwrap();
428 let output = processor.run(&context).unwrap();
429
430 assert_eq!(output, None)
431 }
432
433 #[test]
434 fn test_invalid_status() {
435 let mut processor = processor_stub();
436 processor.url = Url::parse("https://httpbin.org/status/404").unwrap();
437 processor.assert_status = vec![200, 201];
438
439 let context = Context::new().unwrap();
440 let error = processor.run(&context).unwrap_err();
441
442 assert_eq!(error.to_string(), "Invalid status code: 404".to_owned());
443 }
444 }
445
446 #[test]
447 fn test_readme_deps() {
448 version_sync::assert_markdown_deps_updated!("README.md");
449 }
450
451 #[test]
452 fn test_html_root_url() {
453 version_sync::assert_html_root_url_updated!("src/lib.rs");
454 }
455}