|
|
@@ -1,26 +1,25 @@
|
|
|
-use std::{collections::HashMap, str::FromStr};
|
|
|
+pub mod ctype;
|
|
|
+pub mod url;
|
|
|
+
|
|
|
+use iced::widget::scrollable;
|
|
|
|
|
|
use iced::{
|
|
|
Element,
|
|
|
- Length::FillPortion,
|
|
|
- widget::{button, column, container, row, rule, text, text_input},
|
|
|
-};
|
|
|
-use nom::{
|
|
|
- Parser,
|
|
|
- bytes::complete::{tag, take_until, take_until1, take_while, take_while1},
|
|
|
- character::complete::char,
|
|
|
- multi::many0,
|
|
|
- sequence::{preceded, separated_pair},
|
|
|
+ Length::{Fill, FillPortion},
|
|
|
+ widget::{
|
|
|
+ button, column, container, responsive, row, rule, space::horizontal, text, text_input,
|
|
|
+ },
|
|
|
};
|
|
|
use reqwest::{
|
|
|
- Body, Method,
|
|
|
+ Body, Method, StatusCode,
|
|
|
header::{self, HeaderMap, HeaderValue},
|
|
|
};
|
|
|
-use sqlx::error::BoxDynError;
|
|
|
+use std::{collections::HashMap, str::FromStr};
|
|
|
|
|
|
use crate::{
|
|
|
- Message,
|
|
|
+ AppResult, Message,
|
|
|
db::{RequestHeader, RequestParams, WorkspaceEntry},
|
|
|
+ request::ctype::ContentType,
|
|
|
};
|
|
|
|
|
|
pub const DEFAULT_HEADERS: &'static [(&'static str, &'static str)] = &[
|
|
|
@@ -29,10 +28,7 @@ pub const DEFAULT_HEADERS: &'static [(&'static str, &'static str)] = &[
|
|
|
("accept-encoding", "gzip, defalte, br"),
|
|
|
];
|
|
|
|
|
|
-pub async fn send(
|
|
|
- client: reqwest::Client,
|
|
|
- req: HttpRequestParameters,
|
|
|
-) -> Result<Option<String>, String> {
|
|
|
+pub async fn send(client: reqwest::Client, req: HttpRequestParameters) -> AppResult<HttpResponse> {
|
|
|
let HttpRequestParameters {
|
|
|
url,
|
|
|
method,
|
|
|
@@ -55,11 +51,7 @@ pub async fn send(
|
|
|
// Handled by reqwest
|
|
|
ContentType::FormData | ContentType::FormUrlEncoded => {}
|
|
|
};
|
|
|
- let body = match Body::try_from(body.content) {
|
|
|
- Ok(b) => b,
|
|
|
- Err(e) => return Err(e.to_string()),
|
|
|
- };
|
|
|
- Some(body)
|
|
|
+ Some(Body::from(body.content))
|
|
|
}
|
|
|
None => None,
|
|
|
};
|
|
|
@@ -70,13 +62,26 @@ pub async fn send(
|
|
|
req = req.body(body)
|
|
|
}
|
|
|
|
|
|
- let res = req.send().await;
|
|
|
-
|
|
|
- let content = res.unwrap().text().await.unwrap();
|
|
|
-
|
|
|
- tracing::debug!("content: {content}");
|
|
|
+ let res = match req.send().await {
|
|
|
+ Ok(res) => {
|
|
|
+ tracing::debug!(
|
|
|
+ "{} {} {:?} {:#?}",
|
|
|
+ res.remote_addr()
|
|
|
+ .map(|addr| addr.to_string())
|
|
|
+ .unwrap_or(String::new()),
|
|
|
+ res.status(),
|
|
|
+ res.content_length(),
|
|
|
+ res.headers()
|
|
|
+ );
|
|
|
+ let status = res.status();
|
|
|
+ let headers = res.headers().clone();
|
|
|
+ let body = ResponseBody::try_from_response(res).await?;
|
|
|
+ HttpResponse::new(status, headers, body)
|
|
|
+ }
|
|
|
+ Err(e) => return Err(e.into()),
|
|
|
+ };
|
|
|
|
|
|
- Ok(None)
|
|
|
+ Ok(res)
|
|
|
}
|
|
|
|
|
|
#[derive(Debug, Clone)]
|
|
|
@@ -116,7 +121,7 @@ impl WorkspaceRequest {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- pub fn view<'a>(&'a self) -> Element<'a, Message> {
|
|
|
+ pub fn view_req<'a>(&'a self) -> Element<'a, Message> {
|
|
|
let url_input = row![
|
|
|
button(text(&self.method)).width(FillPortion(1)),
|
|
|
text_input("", &self.url)
|
|
|
@@ -172,9 +177,78 @@ impl WorkspaceRequest {
|
|
|
body_section = body_section.push(text("TODO: REQUEST BODY"));
|
|
|
}
|
|
|
|
|
|
- let content = column![url_input, param_section, header_section, body_section];
|
|
|
+ column![url_input, param_section, header_section, body_section].into()
|
|
|
+ }
|
|
|
+
|
|
|
+ pub fn view_res<'a>(
|
|
|
+ &'a self,
|
|
|
+ last_response: Option<&'a HttpResponse>,
|
|
|
+ running: bool,
|
|
|
+ ) -> Element<'a, Message> {
|
|
|
+ if running {
|
|
|
+ container("Running").width(Fill).center(Fill).into()
|
|
|
+ } else if let Some(res) = last_response {
|
|
|
+ let icon = if res.status.is_success() { "o" } else { "x" };
|
|
|
+ let ctype = if let Some(ct) = res.headers.get(header::CONTENT_TYPE) {
|
|
|
+ match ct.to_str() {
|
|
|
+ Ok(ct) => ct,
|
|
|
+ Err(e) => {
|
|
|
+ tracing::warn!("unable to parse content-type header: {e}");
|
|
|
+ "unknown"
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ "unknown"
|
|
|
+ };
|
|
|
|
|
|
- container(content).into()
|
|
|
+ let header = row![
|
|
|
+ text(res.status.as_u16()).width(Fill).center(),
|
|
|
+ text(res.status.canonical_reason().unwrap_or(""))
|
|
|
+ .width(Fill)
|
|
|
+ .center(),
|
|
|
+ text(ctype).width(FillPortion(10)).center(),
|
|
|
+ horizontal().width(Fill),
|
|
|
+ text(icon).width(Fill).center(),
|
|
|
+ ]
|
|
|
+ .height(FillPortion(1));
|
|
|
+
|
|
|
+ let body = match &res.body {
|
|
|
+ Some(b) => match b {
|
|
|
+ ResponseBody::Text(value) | ResponseBody::Json(value) => {
|
|
|
+ container(text(value.as_str()).wrapping(text::Wrapping::Word))
|
|
|
+ }
|
|
|
+ },
|
|
|
+ None => container(""),
|
|
|
+ }
|
|
|
+ .padding(10);
|
|
|
+
|
|
|
+ let body = scrollable(body)
|
|
|
+ .height(FillPortion(15))
|
|
|
+ // .direction(Direction::Both {
|
|
|
+ // vertical: Scrollbar::default(),
|
|
|
+ // horizontal: Scrollbar::default(),
|
|
|
+ // });
|
|
|
+ ;
|
|
|
+
|
|
|
+ column![header, body].into()
|
|
|
+ } else {
|
|
|
+ container(text("No response").center())
|
|
|
+ .width(Fill)
|
|
|
+ .height(Fill)
|
|
|
+ .center(Fill)
|
|
|
+ .style(|_| iced::widget::container::Style {
|
|
|
+ text_color: Some(iced::Color::from_rgba(1., 1., 1., 0.7)),
|
|
|
+ background: None,
|
|
|
+ border: iced::Border {
|
|
|
+ color: iced::Color::WHITE,
|
|
|
+ width: 1.,
|
|
|
+ radius: 0.0.into(),
|
|
|
+ },
|
|
|
+ shadow: iced::Shadow::default(),
|
|
|
+ snap: true,
|
|
|
+ })
|
|
|
+ .into()
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
pub fn from_params_and_headers(
|
|
|
@@ -209,70 +283,6 @@ pub struct HttpRequestParameters {
|
|
|
pub body: Option<RequestBody>,
|
|
|
}
|
|
|
|
|
|
-#[derive(Debug, Clone)]
|
|
|
-pub struct RequestBody {
|
|
|
- pub content: String,
|
|
|
- pub ty: ContentType,
|
|
|
-}
|
|
|
-
|
|
|
-#[derive(Debug, Clone)]
|
|
|
-pub enum ContentType {
|
|
|
- Text,
|
|
|
- Json,
|
|
|
- Xml,
|
|
|
- FormData,
|
|
|
- FormUrlEncoded,
|
|
|
- // TODO: files
|
|
|
- // Binary(reqwest::Body::)
|
|
|
-}
|
|
|
-
|
|
|
-impl ContentType {
|
|
|
- pub fn as_str(&self) -> &'static str {
|
|
|
- match self {
|
|
|
- ContentType::Text => "text",
|
|
|
- ContentType::Json => "json",
|
|
|
- ContentType::Xml => "xml",
|
|
|
- ContentType::FormData => "form_data",
|
|
|
- ContentType::FormUrlEncoded => "form_urlencoded",
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- pub fn from_str(s: &str) -> Option<Self> {
|
|
|
- match s {
|
|
|
- "text" => Some(ContentType::Text),
|
|
|
- "json" => Some(ContentType::Json),
|
|
|
- "xml" => Some(ContentType::Xml),
|
|
|
- "form_data" => Some(ContentType::FormData),
|
|
|
- "form_urlencoded" => Some(ContentType::FormUrlEncoded),
|
|
|
- _ => None,
|
|
|
- }
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-// ---- SQLx integration ----
|
|
|
-
|
|
|
-impl sqlx::Type<sqlx::Sqlite> for ContentType {
|
|
|
- fn type_info() -> sqlx::sqlite::SqliteTypeInfo {
|
|
|
- <&str as sqlx::Type<sqlx::Sqlite>>::type_info()
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-impl<'q> sqlx::Encode<'q, sqlx::Sqlite> for ContentType {
|
|
|
- fn encode_by_ref(
|
|
|
- &self,
|
|
|
- buf: &mut Vec<sqlx::sqlite::SqliteArgumentValue<'q>>,
|
|
|
- ) -> Result<sqlx::encode::IsNull, BoxDynError> {
|
|
|
- <&str as sqlx::Encode<sqlx::Sqlite>>::encode(self.as_str(), buf)
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-impl<'r> sqlx::Decode<'r, sqlx::Sqlite> for ContentType {
|
|
|
- fn decode(value: sqlx::sqlite::SqliteValueRef<'r>) -> Result<Self, BoxDynError> {
|
|
|
- let s = <&str as sqlx::Decode<sqlx::Sqlite>>::decode(value)?;
|
|
|
- ContentType::from_str(s).ok_or_else(|| format!("invalid content type: {}", s).into())
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
impl TryFrom<WorkspaceRequest> for HttpRequestParameters {
|
|
|
type Error = String;
|
|
|
|
|
|
@@ -297,247 +307,99 @@ impl TryFrom<WorkspaceRequest> for HttpRequestParameters {
|
|
|
}
|
|
|
|
|
|
#[derive(Debug, Clone)]
|
|
|
-pub enum RequestMessage {
|
|
|
- UrlUpdated(String),
|
|
|
- Run(i64),
|
|
|
- SectionUpdate(RequestSectionUpdate),
|
|
|
-}
|
|
|
-
|
|
|
-#[derive(Debug, Clone, Copy)]
|
|
|
-pub enum RequestSectionUpdate {
|
|
|
- Params,
|
|
|
- Headers,
|
|
|
- Body,
|
|
|
-}
|
|
|
-
|
|
|
-/// A fully deconstructed URL from a workspace request.
|
|
|
-/// Used as an intermediate step for populating the final URL with variables.
|
|
|
-#[derive(Debug)]
|
|
|
-pub struct RequestUrl<'a> {
|
|
|
- /// The URL scheme, e.g. `http`.
|
|
|
- pub scheme: &'a str,
|
|
|
-
|
|
|
- /// The URL host, includes the port if specified.
|
|
|
- pub host: &'a str,
|
|
|
-
|
|
|
- /// The URL path segments.
|
|
|
- ///
|
|
|
- /// All segments will be formatted as `/segment`, meaning empty Static
|
|
|
- /// fields represent a `/`, which is usually trailing.
|
|
|
- pub path: Vec<Segment<'a>>,
|
|
|
-
|
|
|
- /// Query parameters.
|
|
|
- pub query_params: Vec<(&'a str, &'a str)>,
|
|
|
+pub struct HttpResponse {
|
|
|
+ pub status: StatusCode,
|
|
|
+ pub headers: HeaderMap,
|
|
|
+ pub body: Option<ResponseBody>,
|
|
|
}
|
|
|
|
|
|
-impl<'a> RequestUrl<'a> {
|
|
|
- pub fn parse(input: &'a str) -> Result<Self, nom::Err<nom::error::Error<&'a str>>> {
|
|
|
- let (input, scheme) = take_while1(char::is_alphabetic)(input)?;
|
|
|
-
|
|
|
- let (input, _) = tag("://")(input)?;
|
|
|
-
|
|
|
- let mut path_parser = many0(preceded(
|
|
|
- char('/'),
|
|
|
- take_while(|c: char| c.is_ascii_alphanumeric() || c == ':'),
|
|
|
- ));
|
|
|
-
|
|
|
- let result = take_until1::<_, _, nom::error::Error<_>>("?")(input);
|
|
|
- match result {
|
|
|
- // URL has query parameters
|
|
|
- Ok((query, path)) => {
|
|
|
- // Parse query
|
|
|
- // First char will always be a '?' since we parsed succesfully
|
|
|
- let mut query = &query[1..];
|
|
|
- let mut query_params = vec![];
|
|
|
-
|
|
|
- loop {
|
|
|
- if query.is_empty() {
|
|
|
- break;
|
|
|
- }
|
|
|
-
|
|
|
- let (i, params) = separated_pair(
|
|
|
- take_while(|c: char| c != '='),
|
|
|
- char('='),
|
|
|
- take_while(|c: char| c != '&'),
|
|
|
- )
|
|
|
- .parse(query)?;
|
|
|
-
|
|
|
- query = i;
|
|
|
- query_params.push((params.0, params.1));
|
|
|
-
|
|
|
- if let Ok((i, _)) = char::<_, nom::error::Error<_>>('&').parse(query) {
|
|
|
- query = i;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- debug_assert!(query.is_empty());
|
|
|
-
|
|
|
- // Check path segments
|
|
|
-
|
|
|
- match take_until::<_, _, nom::error::Error<_>>("/")(path) {
|
|
|
- // Path exists
|
|
|
- Ok((path, host)) => {
|
|
|
- let (input, segments) = path_parser.parse(path)?;
|
|
|
- debug_assert!(input.is_empty());
|
|
|
- Ok(RequestUrl {
|
|
|
- scheme,
|
|
|
- host,
|
|
|
- path: segments
|
|
|
- .into_iter()
|
|
|
- .map(|segment| {
|
|
|
- segment
|
|
|
- .strip_prefix(':')
|
|
|
- .map_or(Segment::Static(segment), Segment::Dynamic)
|
|
|
- })
|
|
|
- .collect(),
|
|
|
- query_params,
|
|
|
- })
|
|
|
- }
|
|
|
-
|
|
|
- // No path segments
|
|
|
- Err(_) => Ok(RequestUrl {
|
|
|
- scheme,
|
|
|
- host: path,
|
|
|
- path: vec![],
|
|
|
- query_params,
|
|
|
- }),
|
|
|
- }
|
|
|
- }
|
|
|
- // No query params
|
|
|
- Err(_) => {
|
|
|
- match take_until::<_, _, nom::error::Error<_>>("/")(input) {
|
|
|
- // Path exists
|
|
|
- Ok((path, host)) => {
|
|
|
- let (input, segments) = path_parser.parse(path)?;
|
|
|
- debug_assert!(input.is_empty());
|
|
|
- Ok(RequestUrl {
|
|
|
- scheme,
|
|
|
- host,
|
|
|
- path: segments
|
|
|
- .into_iter()
|
|
|
- .map(|segment| {
|
|
|
- segment
|
|
|
- .strip_prefix(':')
|
|
|
- .map_or(Segment::Static(segment), Segment::Dynamic)
|
|
|
- })
|
|
|
- .collect(),
|
|
|
- query_params: vec![],
|
|
|
- })
|
|
|
- }
|
|
|
- // No path segments
|
|
|
- Err(_) => Ok(RequestUrl {
|
|
|
- scheme,
|
|
|
- host: input,
|
|
|
- path: vec![],
|
|
|
- query_params: vec![],
|
|
|
- }),
|
|
|
- }
|
|
|
- }
|
|
|
+impl HttpResponse {
|
|
|
+ pub fn new(status: StatusCode, headers: HeaderMap, body: Option<ResponseBody>) -> Self {
|
|
|
+ Self {
|
|
|
+ status,
|
|
|
+ headers,
|
|
|
+ body,
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-#[derive(Debug, PartialEq, Eq)]
|
|
|
-pub enum Segment<'a> {
|
|
|
- /// Path segments that do not change.
|
|
|
- /// The value is the final path value.
|
|
|
- Static(&'a str),
|
|
|
-
|
|
|
- /// Path segments that depend on request configuration.
|
|
|
- /// The value is the name of the variable in the request configuration
|
|
|
- /// that contains the final path value.
|
|
|
- Dynamic(&'a str),
|
|
|
+#[derive(Debug, Clone)]
|
|
|
+pub enum ResponseBody {
|
|
|
+ Text(String),
|
|
|
+
|
|
|
+ /// A pretty printed JSON string
|
|
|
+ Json(String),
|
|
|
+ // TODO:
|
|
|
+ // Xml(String),
|
|
|
+ // HTML
|
|
|
+ // Binary
|
|
|
}
|
|
|
|
|
|
-#[cfg(test)]
|
|
|
-mod tests {
|
|
|
- use super::{RequestUrl, Segment};
|
|
|
-
|
|
|
- #[test]
|
|
|
- fn parses_path_placeholders() {
|
|
|
- let input = "http://localhost:4000/foo/:bar/bax";
|
|
|
-
|
|
|
- let expected_path = vec![
|
|
|
- Segment::Static("foo"),
|
|
|
- Segment::Dynamic("bar"),
|
|
|
- Segment::Static("bax"),
|
|
|
- ];
|
|
|
-
|
|
|
- let url = RequestUrl::parse(input).unwrap();
|
|
|
-
|
|
|
- assert_eq!("http", url.scheme);
|
|
|
- assert_eq!("localhost:4000", url.host);
|
|
|
- assert_eq!(expected_path, url.path);
|
|
|
- assert!(url.query_params.is_empty());
|
|
|
- }
|
|
|
-
|
|
|
- #[test]
|
|
|
- fn parses_path_placeholders_trailing_slash() {
|
|
|
- let input = "http://localhost:4000/foo/:bar/bax/";
|
|
|
-
|
|
|
- let expected_path = vec![
|
|
|
- Segment::Static("foo"),
|
|
|
- Segment::Dynamic("bar"),
|
|
|
- Segment::Static("bax"),
|
|
|
- Segment::Static(""),
|
|
|
- ];
|
|
|
-
|
|
|
- let url = RequestUrl::parse(input).unwrap();
|
|
|
+impl ResponseBody {
|
|
|
+ pub async fn try_from_response(res: reqwest::Response) -> AppResult<Option<Self>> {
|
|
|
+ if res.content_length().is_none() {
|
|
|
+ tracing::debug!("Response no content");
|
|
|
+ }
|
|
|
|
|
|
- assert_eq!("http", url.scheme);
|
|
|
- assert_eq!("localhost:4000", url.host);
|
|
|
- assert_eq!(expected_path, url.path);
|
|
|
- assert!(url.query_params.is_empty());
|
|
|
- }
|
|
|
+ let Some(ct) = res.headers().get(header::CONTENT_TYPE) else {
|
|
|
+ tracing::warn!(
|
|
|
+ "Response does not contain content-type header, attempting to read as text"
|
|
|
+ );
|
|
|
+ return Ok(Some(Self::Text(res.text().await?)));
|
|
|
+ };
|
|
|
|
|
|
- #[test]
|
|
|
- fn parses_no_path_segments() {
|
|
|
- let input = "http://localhost:4000";
|
|
|
+ let ct = match ct.to_str() {
|
|
|
+ Ok(ct) => ct,
|
|
|
+ Err(e) => {
|
|
|
+ tracing::warn!("Unable to parse content-type header: {e}");
|
|
|
+ return Err(e.into());
|
|
|
+ }
|
|
|
+ };
|
|
|
|
|
|
- let url = RequestUrl::parse(input).unwrap();
|
|
|
+ let ct: mime::Mime = ct.parse()?;
|
|
|
|
|
|
- assert_eq!("http", url.scheme);
|
|
|
- assert_eq!("localhost:4000", url.host);
|
|
|
- assert!(url.path.is_empty());
|
|
|
- assert!(url.query_params.is_empty());
|
|
|
- }
|
|
|
+ if ct.subtype() == mime::JSON || ct.suffix().is_some_and(|s| s == mime::JSON) {
|
|
|
+ tracing::debug!("reading body");
|
|
|
+ let json = serde_json::to_string_pretty(&res.json::<serde_json::Value>().await?)?;
|
|
|
+ tracing::debug!("body read");
|
|
|
+ return Ok(Some(Self::Json(json)));
|
|
|
+ }
|
|
|
|
|
|
- #[test]
|
|
|
- fn parse_no_path_segments_trailing_slash() {
|
|
|
- let input = "http://localhost:4000/";
|
|
|
+ if ct.type_() == mime::TEXT {
|
|
|
+ tracing::debug!("reading body");
|
|
|
+ let text = res.text().await?;
|
|
|
+ tracing::debug!("body read");
|
|
|
+ return Ok(Some(Self::Text(text)));
|
|
|
+ }
|
|
|
|
|
|
- let url = RequestUrl::parse(input).unwrap();
|
|
|
+ tracing::warn!("Body did not match anything!");
|
|
|
|
|
|
- assert_eq!("http", url.scheme);
|
|
|
- assert_eq!("localhost:4000", url.host);
|
|
|
- assert_eq!(vec![Segment::Static("")], url.path);
|
|
|
- assert!(url.query_params.is_empty());
|
|
|
+ Ok(None)
|
|
|
}
|
|
|
+}
|
|
|
|
|
|
- #[test]
|
|
|
- fn parse_query_params_no_path() {
|
|
|
- let input = "http://localhost:4000?foo=bar&baz=bax";
|
|
|
-
|
|
|
- let url = RequestUrl::parse(input).unwrap();
|
|
|
-
|
|
|
- assert_eq!("http", url.scheme);
|
|
|
- assert_eq!("localhost:4000", url.host);
|
|
|
- assert!(url.path.is_empty());
|
|
|
- assert_eq!(vec![("foo", "bar"), ("baz", "bax")], url.query_params);
|
|
|
- }
|
|
|
+#[derive(Debug, Clone)]
|
|
|
+pub struct RequestBody {
|
|
|
+ pub content: String,
|
|
|
+ pub ty: ContentType,
|
|
|
+}
|
|
|
|
|
|
- #[test]
|
|
|
- fn parse_query_params_with_path() {
|
|
|
- let input = "http://localhost:4000/foo/:bar?foo=bar&baz=bax";
|
|
|
+#[derive(Debug, Clone)]
|
|
|
+pub enum RequestMessage {
|
|
|
+ UrlUpdated(String),
|
|
|
+ Run(i64),
|
|
|
+ SectionUpdate(RequestSectionUpdate),
|
|
|
+}
|
|
|
|
|
|
- let url = RequestUrl::parse(input).unwrap();
|
|
|
+#[derive(Debug, Clone, Copy)]
|
|
|
+pub enum RequestSectionUpdate {
|
|
|
+ Params,
|
|
|
+ Headers,
|
|
|
+ Body,
|
|
|
+}
|
|
|
|
|
|
- assert_eq!("http", url.scheme);
|
|
|
- assert_eq!("localhost:4000", url.host);
|
|
|
- assert_eq!(
|
|
|
- vec![Segment::Static("foo"), Segment::Dynamic("bar")],
|
|
|
- url.path
|
|
|
- );
|
|
|
- assert_eq!(vec![("foo", "bar"), ("baz", "bax")], url.query_params);
|
|
|
- }
|
|
|
+#[derive(Debug, Clone)]
|
|
|
+pub enum ResponseMessage {
|
|
|
+ Success(i64, HttpResponse),
|
|
|
+ Error(i64, String),
|
|
|
}
|