Initial commit
This commit is contained in:
parent
9eb7c2883e
commit
d5974dda20
2
.github/workflows/build.yaml
vendored
2
.github/workflows/build.yaml
vendored
@ -15,4 +15,4 @@ jobs:
|
|||||||
- name: Build
|
- name: Build
|
||||||
run: cargo build --verbose
|
run: cargo build --verbose
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: cargo test --verbose
|
run: cargo test --verbose --all-features
|
4
derive/Cargo.lock
generated
4
derive/Cargo.lock
generated
@ -30,9 +30,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "2.0.66"
|
version = "2.0.67"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5"
|
checksum = "ff8655ed1d86f3af4ee3fd3263786bc14245ad17c4c7e85ba7187fb3ae028c90"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
name = "derive"
|
name = "derive"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
authors = ["Martin Berg Alstad"]
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
proc-macro = true
|
proc-macro = true
|
||||||
|
@ -2,7 +2,7 @@ extern crate proc_macro;
|
|||||||
|
|
||||||
use proc_macro::TokenStream;
|
use proc_macro::TokenStream;
|
||||||
use quote::quote;
|
use quote::quote;
|
||||||
use syn::{DeriveInput, parse_macro_input};
|
use syn::{parse_macro_input, DeriveInput};
|
||||||
|
|
||||||
#[proc_macro_derive(IntoResponse)]
|
#[proc_macro_derive(IntoResponse)]
|
||||||
pub fn into_response_derive(input: TokenStream) -> TokenStream {
|
pub fn into_response_derive(input: TokenStream) -> TokenStream {
|
||||||
@ -16,7 +16,9 @@ fn into_response_derive_impl(input: DeriveInput) -> TokenStream {
|
|||||||
let expanded = quote! {
|
let expanded = quote! {
|
||||||
impl IntoResponse for #name {
|
impl IntoResponse for #name {
|
||||||
fn into_response(self) -> Response {
|
fn into_response(self) -> Response {
|
||||||
BaseResponse::create(self)
|
let version = env!("CARGO_PKG_VERSION");
|
||||||
|
lib::serde::response::BaseResponse::new(version, self)
|
||||||
|
.into_response()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -0,0 +1,20 @@
|
|||||||
|
#[macro_export]
|
||||||
|
#[cfg(feature = "axum")]
|
||||||
|
macro_rules! create_app {
|
||||||
|
($router:expr) => {
|
||||||
|
$router
|
||||||
|
};
|
||||||
|
($router:expr, $($layer:expr),* $(,)?) => {
|
||||||
|
$router$(.layer($layer))*
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(all(test, feature = "axum"))]
|
||||||
|
mod tests {
|
||||||
|
use axum::Router;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_create_app_router_only() {
|
||||||
|
let _app: Router<()> = create_app!(Router::new());
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,93 @@
|
|||||||
|
#[cfg(all(feature = "tokio", feature = "axum"))]
|
||||||
|
use {crate::io::file, axum::body::Body, axum::response::Html, std::io};
|
||||||
|
|
||||||
|
/// Load an HTML file from the given file path, relative to the current directory.
|
||||||
|
/// # Arguments
|
||||||
|
/// * `file_path` - The path to the HTML file.
|
||||||
|
/// # Returns
|
||||||
|
/// The HTML file as a `Html` object containing the content-type 'text/html' or an error message if the file is not found or cannot be read.
|
||||||
|
/// # Examples
|
||||||
|
/// ```
|
||||||
|
/// let html = async { lib::axum::load::load_html("openapi.html").await.unwrap() };
|
||||||
|
/// ```
|
||||||
|
#[cfg(all(feature = "tokio", feature = "axum"))]
|
||||||
|
pub async fn load_html<Path>(file_path: Path) -> Result<Html<Body>, io::Error>
|
||||||
|
where
|
||||||
|
Path: AsRef<std::path::Path>,
|
||||||
|
{
|
||||||
|
load_file(file_path).await.map(Html)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(all(feature = "tokio", feature = "axum"))]
|
||||||
|
pub async fn load_file<Path>(file_path: Path) -> Result<Body, io::Error>
|
||||||
|
where
|
||||||
|
Path: AsRef<std::path::Path>,
|
||||||
|
{
|
||||||
|
file::load_file(file_path).await.map(Body::from_stream)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load an HTML file from the given file path, relative to the resource directory.
|
||||||
|
/// The file is loading on compile time as a string literal.
|
||||||
|
/// # Arguments
|
||||||
|
/// * `filename` - The path to the HTML file.
|
||||||
|
/// # Returns
|
||||||
|
/// The HTML file as a `Html` object containing the content-type 'text/html'.
|
||||||
|
/// # Examples
|
||||||
|
/// ```
|
||||||
|
/// let _html = lib::load_html!("load.rs");
|
||||||
|
/// ```
|
||||||
|
// TODO check platform and use correct path separator
|
||||||
|
#[macro_export]
|
||||||
|
#[cfg(feature = "axum")]
|
||||||
|
macro_rules! load_html {
|
||||||
|
($filepath:expr) => {
|
||||||
|
axum::response::Html(
|
||||||
|
axum::body::Body::new(
|
||||||
|
include_str!($filepath).to_string()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
};
|
||||||
|
($filepath:expr, $($key:expr => $value:expr),*) => {
|
||||||
|
axum::response::Html(
|
||||||
|
axum::body::Body::new(
|
||||||
|
include_str!($filepath)$(
|
||||||
|
.replace($key, $value)
|
||||||
|
)*
|
||||||
|
)
|
||||||
|
)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(all(test, feature = "axum"))]
|
||||||
|
mod tests {
|
||||||
|
#[test]
|
||||||
|
fn test_load_html() {
|
||||||
|
let _html = load_html!("load.rs");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_load_html_with_replacements() {
|
||||||
|
let _html =
|
||||||
|
load_html!("load.rs", "{{replace_me}}" => "hello", "{{replace_me_too}}" => "world");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "tokio")]
|
||||||
|
mod tokio {
|
||||||
|
use super::super::*;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_load_html() {
|
||||||
|
assert!(load_html("Cargo.toml").await.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_load_file() {
|
||||||
|
assert!(load_file("Cargo.toml").await.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_load_file_not_found() {
|
||||||
|
assert!(load_file("not_found.rs").await.is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,4 @@
|
|||||||
|
pub mod app;
|
||||||
|
pub mod load;
|
||||||
|
pub mod response;
|
||||||
|
pub mod router;
|
@ -0,0 +1,56 @@
|
|||||||
|
#[cfg(all(feature = "axum", feature = "serde"))]
|
||||||
|
use {
|
||||||
|
crate::serde::response::BaseResponse,
|
||||||
|
axum::{
|
||||||
|
response::{IntoResponse, Response},
|
||||||
|
Json,
|
||||||
|
},
|
||||||
|
serde::Serialize,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(all(feature = "axum", feature = "serde"))]
|
||||||
|
impl<T: Serialize> IntoResponse for BaseResponse<T> {
|
||||||
|
fn into_response(self) -> Response {
|
||||||
|
Json(self).into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(all(test, feature = "axum", feature = "serde"))]
|
||||||
|
mod tests {
|
||||||
|
use axum::http::header::CONTENT_TYPE;
|
||||||
|
use axum::http::{HeaderValue, StatusCode};
|
||||||
|
use axum::response::IntoResponse;
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
use crate::serde::response::BaseResponse;
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct Response {
|
||||||
|
message: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_into_response() {
|
||||||
|
let response = BaseResponse::new(
|
||||||
|
"",
|
||||||
|
Response {
|
||||||
|
message: "Hi".to_string(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
let json_response = response.into_response();
|
||||||
|
assert_eq!(json_response.status(), StatusCode::OK);
|
||||||
|
assert_eq!(
|
||||||
|
json_response.headers().get(CONTENT_TYPE),
|
||||||
|
Some(&HeaderValue::from_static("application/json"))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_into_response_with_primitive() {
|
||||||
|
let response = BaseResponse::new("", 42);
|
||||||
|
assert_eq!(
|
||||||
|
response.into_response().status(),
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,109 @@
|
|||||||
|
/// Create an axum router function with the given body or routes.
|
||||||
|
/// # Examples
|
||||||
|
/// ```
|
||||||
|
/// use lib::router;
|
||||||
|
/// async fn index() {}
|
||||||
|
///
|
||||||
|
/// router!(
|
||||||
|
/// get "/" => index,
|
||||||
|
/// get "/openapi" => || async {}
|
||||||
|
/// );
|
||||||
|
/// ```
|
||||||
|
/// ```
|
||||||
|
/// use lib::router;
|
||||||
|
/// async fn simplify(path: axum::extract::path::Path<String>) {}
|
||||||
|
/// router!("/simplify", lib::routes!(
|
||||||
|
/// get "/:exp" => simplify,
|
||||||
|
/// get "/table/:exp" => || async {}
|
||||||
|
/// ));
|
||||||
|
/// ```
|
||||||
|
#[macro_export]
|
||||||
|
#[cfg(feature = "axum")]
|
||||||
|
macro_rules! router {
|
||||||
|
($body:expr) => {
|
||||||
|
pub(crate) fn router() -> axum::Router<()> {
|
||||||
|
$body
|
||||||
|
}
|
||||||
|
};
|
||||||
|
($route:expr, $router:expr) => {
|
||||||
|
router!(axum::Router::new().nest($route, $router));
|
||||||
|
};
|
||||||
|
($($method:ident $route:expr => $func:expr),* $(,)?) => {
|
||||||
|
router!($crate::routes!($($method $route => $func),*));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a router with the given routes.
|
||||||
|
/// # Examples
|
||||||
|
/// ```
|
||||||
|
/// async fn index() {}
|
||||||
|
///
|
||||||
|
/// let _: axum::Router<()> = lib::routes!(
|
||||||
|
/// get "/" => index,
|
||||||
|
/// post "/" => || async {}
|
||||||
|
/// );
|
||||||
|
/// ```
|
||||||
|
#[macro_export]
|
||||||
|
#[cfg(feature = "axum")]
|
||||||
|
macro_rules! routes {
|
||||||
|
($($method:ident $route:expr => $func:expr),* $(,)?) => {
|
||||||
|
axum::Router::new()
|
||||||
|
$(
|
||||||
|
.route($route, axum::routing::$method($func))
|
||||||
|
)*
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
#[cfg(feature = "axum")]
|
||||||
|
macro_rules! join_routes {
|
||||||
|
($($route:expr),* $(,)?) => {
|
||||||
|
axum::Router::new()$(
|
||||||
|
.merge($route)
|
||||||
|
)*
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(all(test, feature = "axum"))]
|
||||||
|
mod tests {
|
||||||
|
use axum::Router;
|
||||||
|
|
||||||
|
async fn index() {}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_router() {
|
||||||
|
router!(
|
||||||
|
get "/" => index,
|
||||||
|
post "/" => || async {}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_router_with_expression() {
|
||||||
|
router!(Router::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_nested_router() {
|
||||||
|
router!(
|
||||||
|
"/simplify",
|
||||||
|
routes!(
|
||||||
|
get "/:exp" => || async {},
|
||||||
|
get "/table/:exp" => || async {}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_routes() {
|
||||||
|
let _router: Router<()> = routes!(
|
||||||
|
get "/" => index,
|
||||||
|
post "/" => || async {}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_join_routes() {
|
||||||
|
let _router: Router<()> = join_routes![Router::new(), Router::new()];
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,27 @@
|
|||||||
|
#[cfg(feature = "tokio")]
|
||||||
|
use {std::io::Error, tokio::fs::File, tokio_util::io::ReaderStream};
|
||||||
|
|
||||||
|
#[cfg(feature = "tokio")]
|
||||||
|
pub async fn load_file<Path>(file_path: Path) -> Result<ReaderStream<File>, Error>
|
||||||
|
where
|
||||||
|
Path: AsRef<std::path::Path>,
|
||||||
|
{
|
||||||
|
File::open(file_path).await.map(ReaderStream::new)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(all(test, feature = "tokio"))]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_load_file() {
|
||||||
|
let file = load_file("Cargo.toml").await;
|
||||||
|
assert!(file.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_load_file_error() {
|
||||||
|
let file = load_file("Cargo.tom").await;
|
||||||
|
assert!(file.is_err());
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
pub mod file;
|
10
src/lib.rs
10
src/lib.rs
@ -0,0 +1,10 @@
|
|||||||
|
#![allow(dead_code)]
|
||||||
|
|
||||||
|
pub mod axum;
|
||||||
|
pub mod io;
|
||||||
|
pub mod nom;
|
||||||
|
pub mod serde;
|
||||||
|
pub mod vector;
|
||||||
|
|
||||||
|
#[cfg(feature = "derive")]
|
||||||
|
pub extern crate derive;
|
@ -0,0 +1,162 @@
|
|||||||
|
#[cfg(feature = "nom")]
|
||||||
|
use {
|
||||||
|
nom::{
|
||||||
|
bytes::complete::take_while_m_n,
|
||||||
|
character::complete::{char, multispace0},
|
||||||
|
combinator::eof,
|
||||||
|
sequence::{delimited, terminated},
|
||||||
|
IResult, InputIter, InputLength, InputTake, Slice,
|
||||||
|
},
|
||||||
|
std::ops::RangeFrom,
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO generic input
|
||||||
|
|
||||||
|
/// Trim leading and trailing whitespace from the input Parser
|
||||||
|
/// - Parameters
|
||||||
|
/// - `inner`: The parser to trim
|
||||||
|
/// - Returns: A parser that trims leading and trailing whitespace from the input and then runs the value from the inner parser
|
||||||
|
#[cfg(feature = "nom")]
|
||||||
|
pub fn trim<'a, Parser, R>(inner: Parser) -> impl FnMut(&'a str) -> IResult<&'a str, R>
|
||||||
|
where
|
||||||
|
Parser: Fn(&'a str) -> IResult<&'a str, R>,
|
||||||
|
{
|
||||||
|
delimited(multispace0, inner, multispace0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse a parenthesized expression. This parser will parse an expression that is surrounded by parentheses
|
||||||
|
/// and will trim the whitespace surrounding the expression.
|
||||||
|
/// - Parameters
|
||||||
|
/// - `inner`: The parser to run inside the parentheses
|
||||||
|
/// - Returns: A parser that parses a parenthesized expression
|
||||||
|
#[cfg(feature = "nom")]
|
||||||
|
pub fn parenthesized<'a, Parser, R>(inner: Parser) -> impl FnMut(&'a str) -> IResult<&'a str, R>
|
||||||
|
where
|
||||||
|
Parser: Fn(&'a str) -> IResult<&'a str, R>,
|
||||||
|
{
|
||||||
|
delimited(char('('), trim(inner), char(')'))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Take where the predicate is true and the length is exactly `n`
|
||||||
|
/// - Parameters
|
||||||
|
/// - `n`: The length of the string to take
|
||||||
|
/// - `predicate`: The predicate to call to validate the input
|
||||||
|
/// - Returns: A parser that takes `n` characters from the input
|
||||||
|
#[cfg(feature = "nom")]
|
||||||
|
pub fn take_where<F, Input>(n: usize, predicate: F) -> impl Fn(Input) -> IResult<Input, Input>
|
||||||
|
where
|
||||||
|
Input: InputTake + InputIter + InputLength + Slice<RangeFrom<usize>>,
|
||||||
|
F: Fn(<Input as InputIter>::Item) -> bool + Copy,
|
||||||
|
{
|
||||||
|
take_while_m_n(n, n, predicate)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "nom")]
|
||||||
|
pub fn exhausted<'a, Parser, R>(inner: Parser) -> impl FnMut(&'a str) -> IResult<&'a str, R>
|
||||||
|
where
|
||||||
|
Parser: Fn(&'a str) -> IResult<&'a str, R>,
|
||||||
|
{
|
||||||
|
terminated(inner, eof)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(all(test, feature = "nom"))]
|
||||||
|
mod tests {
|
||||||
|
use nom::bytes::streaming::take_while;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_trim_both_sides() {
|
||||||
|
let input = " test ";
|
||||||
|
let (remaining, result) =
|
||||||
|
trim(take_where(4, |c: char| c.is_ascii_alphabetic()))(input).unwrap();
|
||||||
|
assert_eq!(remaining, "");
|
||||||
|
assert_eq!(result, "test");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_trim_leading() {
|
||||||
|
let input = " test";
|
||||||
|
let (remaining, result) =
|
||||||
|
trim(take_where(4, |c: char| c.is_ascii_alphabetic()))(input).unwrap();
|
||||||
|
assert_eq!(remaining, "");
|
||||||
|
assert_eq!(result, "test");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_trim_trailing() {
|
||||||
|
let input = "test ";
|
||||||
|
let (remaining, result) =
|
||||||
|
trim(take_where(4, |c: char| c.is_ascii_alphabetic()))(input).unwrap();
|
||||||
|
assert_eq!(remaining, "");
|
||||||
|
assert_eq!(result, "test");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_trim_no_trim() {
|
||||||
|
let input = "test";
|
||||||
|
let (remaining, result) =
|
||||||
|
trim(take_where(4, |c: char| c.is_ascii_alphabetic()))(input).unwrap();
|
||||||
|
assert_eq!(remaining, "");
|
||||||
|
assert_eq!(result, "test");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parenthesized() {
|
||||||
|
let input = "(test)";
|
||||||
|
let (remaining, result) =
|
||||||
|
parenthesized(take_where(4, |c: char| c.is_ascii_alphabetic()))(input).unwrap();
|
||||||
|
assert_eq!(remaining, "");
|
||||||
|
assert_eq!(result, "test");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parenthesized_parse_until_end() {
|
||||||
|
let input = "(test)";
|
||||||
|
assert!(parenthesized(take_while(|_| true))(input).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_take_where() {
|
||||||
|
let input = "test";
|
||||||
|
let (remaining, result) = take_where(4, |c: char| c.is_ascii_alphabetic())(input).unwrap();
|
||||||
|
assert_eq!(remaining, "");
|
||||||
|
assert_eq!(result, "test");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_take_where_not_enough() {
|
||||||
|
let input = "tes";
|
||||||
|
assert!(take_where(4, |c: char| c.is_ascii_alphabetic())(input).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_take_where_too_much() {
|
||||||
|
let input = "testing";
|
||||||
|
assert_eq!(
|
||||||
|
take_where(4, |c: char| c.is_ascii_alphabetic())(input),
|
||||||
|
Ok(("ing", "test"))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_take_where_predicate_false() {
|
||||||
|
let input = "test";
|
||||||
|
assert!(take_where(4, |c: char| c.is_ascii_digit())(input).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_exhausted() {
|
||||||
|
let input = "test";
|
||||||
|
let (remaining, result) =
|
||||||
|
exhausted(take_where(4, |c: char| c.is_ascii_alphabetic()))(input).unwrap();
|
||||||
|
assert_eq!(remaining, "");
|
||||||
|
assert_eq!(result, "test");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_exhausted_not_exhausted() {
|
||||||
|
let input = "test ";
|
||||||
|
assert!(exhausted(take_where(4, |c: char| c.is_ascii_alphabetic()))(input).is_err());
|
||||||
|
}
|
||||||
|
}
|
@ -1 +1,2 @@
|
|||||||
pub mod parser;
|
pub mod combinators;
|
||||||
|
pub mod util;
|
||||||
|
@ -0,0 +1,44 @@
|
|||||||
|
#[cfg(feature = "nom")]
|
||||||
|
use nom::{error::Error, IResult};
|
||||||
|
|
||||||
|
#[cfg(feature = "nom")]
|
||||||
|
pub trait IntoResult<T> {
|
||||||
|
type Error;
|
||||||
|
fn into_result(self) -> Result<T, Self::Error>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "nom")]
|
||||||
|
impl<T, R> IntoResult<T> for IResult<R, T> {
|
||||||
|
type Error = nom::Err<Error<R>>;
|
||||||
|
fn into_result(self) -> Result<T, Self::Error> {
|
||||||
|
self.map(|(_remaining, value)| value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(all(test, feature = "nom"))]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use nom::character::complete::char as c;
|
||||||
|
|
||||||
|
fn parse_char(input: &str) -> IResult<&str, char> {
|
||||||
|
c('A')(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_into_result() {
|
||||||
|
let i_result = parse_char("ABC");
|
||||||
|
assert_eq!(i_result.into_result(), Ok('A'));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_into_result_error() {
|
||||||
|
let i_result = parse_char("BC");
|
||||||
|
assert_eq!(
|
||||||
|
i_result.into_result(),
|
||||||
|
Err(nom::Err::Error(Error::new(
|
||||||
|
"BC",
|
||||||
|
nom::error::ErrorKind::Char
|
||||||
|
)))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
pub mod response;
|
@ -0,0 +1,41 @@
|
|||||||
|
#[cfg(feature = "serde")]
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
#[cfg(feature = "serde")]
|
||||||
|
pub struct BaseResponse<T: Serialize> {
|
||||||
|
pub version: String,
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub body: T, // T must be a struct (or enum?)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "serde")]
|
||||||
|
impl<T: Serialize> BaseResponse<T> {
|
||||||
|
pub fn new(version: impl Into<String>, body: T) -> Self {
|
||||||
|
Self {
|
||||||
|
version: version.into(),
|
||||||
|
body,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(all(test, feature = "serde"))]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct Response {
|
||||||
|
message: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_base_response_new() {
|
||||||
|
let response = BaseResponse::new(
|
||||||
|
"",
|
||||||
|
Response {
|
||||||
|
message: "Hi".to_string(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
assert_eq!(response.body.message, "Hi".to_string());
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,28 @@
|
|||||||
|
#[cfg(feature = "vec")]
|
||||||
|
pub trait Distinct {
|
||||||
|
fn distinct(&mut self);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "vec")]
|
||||||
|
impl<T: PartialEq + Clone> Distinct for Vec<T> {
|
||||||
|
fn distinct(&mut self) {
|
||||||
|
*self = self.iter().fold(vec![], |mut acc, x| {
|
||||||
|
if !acc.contains(x) {
|
||||||
|
acc.push(x.clone());
|
||||||
|
}
|
||||||
|
acc
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(all(test, feature = "vec"))]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_distinct() {
|
||||||
|
let mut vec = vec![1, 2, 3, 1, 2, 3];
|
||||||
|
vec.distinct();
|
||||||
|
assert_eq!(vec, vec![1, 2, 3]);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,37 @@
|
|||||||
|
#[macro_export]
|
||||||
|
#[cfg(feature = "vec")]
|
||||||
|
macro_rules! map {
|
||||||
|
() => { std::collections::HashMap::new() };
|
||||||
|
($($k:expr => $v:expr),* $(,)?) => {
|
||||||
|
{
|
||||||
|
let mut temp_map = std::collections::HashMap::new();
|
||||||
|
$(
|
||||||
|
temp_map.insert($k, $v);
|
||||||
|
)*
|
||||||
|
temp_map
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(all(test, feature = "vec"))]
|
||||||
|
mod tests {
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_empty_map() {
|
||||||
|
let map: HashMap<usize, usize> = map!();
|
||||||
|
assert_eq!(map.len(), 0);
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn test_map() {
|
||||||
|
let map = map! {
|
||||||
|
"one" => 1,
|
||||||
|
"two" => 2,
|
||||||
|
"three" => 3,
|
||||||
|
};
|
||||||
|
assert_eq!(map.len(), 3);
|
||||||
|
assert_eq!(map.get("one"), Some(&1));
|
||||||
|
assert_eq!(map.get("two"), Some(&2));
|
||||||
|
assert_eq!(map.get("three"), Some(&3));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,33 @@
|
|||||||
|
#[macro_export]
|
||||||
|
#[cfg(feature = "vec")]
|
||||||
|
macro_rules! matrix {
|
||||||
|
($x:expr; $m:expr, $n:expr) => {
|
||||||
|
vec![vec![$x; $n]; $m]
|
||||||
|
};
|
||||||
|
($($($x:expr),*);*) => {
|
||||||
|
{
|
||||||
|
let mut temp_vec = vec![];
|
||||||
|
{} // Needed to avoid clippy warning
|
||||||
|
$(
|
||||||
|
temp_vec.push(vec![$($x),*]);
|
||||||
|
)*
|
||||||
|
temp_vec
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(all(test, feature = "vec"))]
|
||||||
|
mod tests {
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_matrix() {
|
||||||
|
let matrix = matrix![1, 2, 3; 4, 5, 6; 7, 8, 9];
|
||||||
|
assert_eq!(matrix, vec![vec![1, 2, 3], vec![4, 5, 6], vec![7, 8, 9]]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_matrix_with_single_value() {
|
||||||
|
let matrix = matrix![0; 2, 3];
|
||||||
|
assert_eq!(matrix, vec![vec![0, 0, 0], vec![0, 0, 0]]);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,4 @@
|
|||||||
|
pub mod distinct;
|
||||||
|
pub mod map;
|
||||||
|
pub mod matrix;
|
||||||
|
pub mod set;
|
@ -0,0 +1,33 @@
|
|||||||
|
#[macro_export]
|
||||||
|
#[cfg(feature = "vec")]
|
||||||
|
macro_rules! set {
|
||||||
|
() => { std::collections::HashSet::new() };
|
||||||
|
($($x:expr),* $(,)?) => {
|
||||||
|
{
|
||||||
|
let mut temp_set = std::collections::HashSet::new();
|
||||||
|
$(
|
||||||
|
temp_set.insert($x);
|
||||||
|
)*
|
||||||
|
temp_set
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(all(test, feature = "vec"))]
|
||||||
|
mod tests {
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_empty_set() {
|
||||||
|
let set: HashSet<usize> = set![];
|
||||||
|
assert_eq!(set.len(), 0);
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn test_set() {
|
||||||
|
let set = set![1, 2, 3];
|
||||||
|
assert_eq!(set.len(), 3);
|
||||||
|
assert!(set.contains(&1));
|
||||||
|
assert!(set.contains(&2));
|
||||||
|
assert!(set.contains(&3));
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user