#![deny(
clippy::all,
clippy::cargo,
clippy::nursery,
clippy::pedantic,
deprecated_in_future,
future_incompatible,
missing_docs,
nonstandard_style,
rust_2018_idioms,
rustdoc,
warnings,
unused_results,
unused_qualifications,
unused_lifetimes,
unused_import_braces,
unsafe_code,
unreachable_pub,
trivial_casts,
trivial_numeric_casts,
missing_debug_implementations,
missing_copy_implementations
)]
#![warn(variant_size_differences)]
#![allow(clippy::multiple_crate_versions, missing_doc_code_examples)]
#![doc(html_root_url = "https://docs.rs/automaat-processor-http-request/0.1.0")]
use automaat_core::{Context, Processor};
use reqwest::{header, Client};
use serde::{Deserialize, Serialize};
use std::{error, fmt, str::FromStr};
use url::Url;
#[cfg_attr(feature = "juniper", derive(juniper::GraphQLObject))]
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct HttpRequest {
#[serde(with = "url_serde")]
pub url: Url,
pub method: Method,
pub headers: Vec<Header>,
pub body: Option<String>,
pub assert_status: Vec<i32>,
}
#[cfg_attr(feature = "juniper", derive(juniper::GraphQLEnum))]
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub enum Method {
CONNECT,
DELETE,
GET,
HEAD,
OPTIONS,
PATCH,
POST,
PUT,
TRACE,
}
impl From<Method> for reqwest::Method {
fn from(method: Method) -> Self {
match method {
Method::CONNECT => Self::CONNECT,
Method::DELETE => Self::DELETE,
Method::GET => Self::GET,
Method::HEAD => Self::HEAD,
Method::OPTIONS => Self::OPTIONS,
Method::PATCH => Self::PATCH,
Method::POST => Self::POST,
Method::PUT => Self::PUT,
Method::TRACE => Self::TRACE,
}
}
}
#[cfg_attr(feature = "juniper", derive(juniper::GraphQLInputObject))]
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct Header {
pub name: String,
pub value: String,
}
impl Header {
pub fn new(name: &str, value: &str) -> Self {
Self {
name: name.to_owned(),
value: value.to_owned(),
}
}
}
#[cfg(feature = "juniper")]
#[graphql(name = "HttpRequestInput")]
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, juniper::GraphQLInputObject)]
pub struct Input {
#[serde(with = "url_serde")]
url: Url,
method: Method,
headers: Option<Vec<Header>>,
body: Option<String>,
assert_status: Option<Vec<i32>>,
}
#[cfg(feature = "juniper")]
impl From<Input> for HttpRequest {
fn from(input: Input) -> Self {
Self {
url: input.url,
method: input.method,
headers: input.headers.unwrap_or_else(Default::default),
body: input.body,
assert_status: input.assert_status.unwrap_or_else(Default::default),
}
}
}
impl<'a> Processor<'a> for HttpRequest {
const NAME: &'static str = "HTTP Request";
type Error = Error;
type Output = String;
fn validate(&self) -> Result<(), Self::Error> {
for header in &self.headers {
let _ = header::HeaderName::from_str(header.name.as_str())?;
let _ = header::HeaderValue::from_str(header.value.as_str())?;
}
Ok(())
}
fn run(&self, _context: &Context) -> Result<Option<Self::Output>, Self::Error> {
let mut request = Client::new().request(self.method.into(), self.url.as_str());
let mut map = header::HeaderMap::new();
for header in &self.headers {
let _ = map.insert(
header.name.as_str().parse::<header::HeaderName>()?,
header.value.as_str().parse()?,
);
}
if let Some(body) = self.body.to_owned() {
request = request.body(body);
}
let mut response = request.headers(map).send()?;
let status = i32::from(response.status().as_u16());
if !self.assert_status.is_empty() && !self.assert_status.contains(&status) {
return Err(Error::Status(status));
}
let body = response.text()?;
if body.is_empty() {
Ok(None)
} else {
Ok(Some(body))
}
}
}
#[derive(Debug)]
pub enum Error {
Response(reqwest::Error),
Header(String),
Status(i32),
#[doc(hidden)]
__Unknown,
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
Error::Response(ref err) => write!(f, "Response error: {}", err),
Error::Header(ref err) => write!(f, "Invalid header: {}", err),
Error::Status(status) => write!(f, "Invalid status code: {}", status),
Error::__Unknown => unreachable!(),
}
}
}
impl error::Error for Error {
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
match *self {
Error::Response(ref err) => Some(err),
Error::Header(_) | Error::Status(_) => None,
Error::__Unknown => unreachable!(),
}
}
}
impl From<reqwest::Error> for Error {
fn from(err: reqwest::Error) -> Self {
Error::Response(err)
}
}
impl From<header::InvalidHeaderName> for Error {
fn from(err: header::InvalidHeaderName) -> Self {
Error::Header(err.to_string())
}
}
impl From<header::InvalidHeaderValue> for Error {
fn from(err: header::InvalidHeaderValue) -> Self {
Error::Header(err.to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn processor_stub() -> HttpRequest {
HttpRequest {
url: Url::parse("https://httpbin.org/status/200").unwrap(),
method: Method::GET,
headers: vec![],
body: None,
assert_status: vec![],
}
}
mod run {
use super::*;
#[test]
fn test_empty_response() {
let processor = processor_stub();
let context = Context::new().unwrap();
let output = processor.run(&context).unwrap();
assert!(output.is_none())
}
#[test]
fn test_response_body() {
let mut processor = processor_stub();
processor.url = Url::parse("https://httpbin.org/range/5").unwrap();
let context = Context::new().unwrap();
let output = processor.run(&context).unwrap();
assert_eq!(output, Some("abcde".to_owned()))
}
#[test]
fn test_request_body() {
let mut processor = processor_stub();
processor.url = Url::parse("https://httpbin.org/anything").unwrap();
processor.body = Some("hello world".to_owned());
let context = Context::new().unwrap();
let output = processor.run(&context).unwrap().expect("Some");
assert!(output.contains("hello world"));
}
#[test]
fn test_valid_status() {
let mut processor = processor_stub();
processor.url = Url::parse("https://httpbin.org/status/200").unwrap();
processor.assert_status = vec![200, 204];
let context = Context::new().unwrap();
let output = processor.run(&context).unwrap();
assert_eq!(output, None)
}
#[test]
fn test_invalid_status() {
let mut processor = processor_stub();
processor.url = Url::parse("https://httpbin.org/status/404").unwrap();
processor.assert_status = vec![200, 201];
let context = Context::new().unwrap();
let error = processor.run(&context).unwrap_err();
assert_eq!(error.to_string(), "Invalid status code: 404".to_owned());
}
}
#[test]
fn test_readme_deps() {
version_sync::assert_markdown_deps_updated!("README.md");
}
#[test]
fn test_html_root_url() {
version_sync::assert_html_root_url_updated!("src/lib.rs");
}
}