diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index bd7d7a5..54ff74f 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -15,4 +15,4 @@ jobs: - name: Build run: cargo build --verbose - name: Run tests - run: cargo test --verbose \ No newline at end of file + run: cargo test --verbose --all-features \ No newline at end of file diff --git a/derive/Cargo.lock b/derive/Cargo.lock index 25477dc..dbf0312 100644 --- a/derive/Cargo.lock +++ b/derive/Cargo.lock @@ -30,9 +30,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.66" +version = "2.0.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5" +checksum = "ff8655ed1d86f3af4ee3fd3263786bc14245ad17c4c7e85ba7187fb3ae028c90" dependencies = [ "proc-macro2", "quote", diff --git a/derive/Cargo.toml b/derive/Cargo.toml index f7aed7f..bf9ab2b 100644 --- a/derive/Cargo.toml +++ b/derive/Cargo.toml @@ -2,6 +2,7 @@ name = "derive" version = "1.0.0" edition = "2021" +authors = ["Martin Berg Alstad"] [lib] proc-macro = true diff --git a/derive/src/lib.rs b/derive/src/lib.rs index 3dbc4b4..42d5146 100644 --- a/derive/src/lib.rs +++ b/derive/src/lib.rs @@ -2,7 +2,7 @@ extern crate proc_macro; use proc_macro::TokenStream; use quote::quote; -use syn::{DeriveInput, parse_macro_input}; +use syn::{parse_macro_input, DeriveInput}; #[proc_macro_derive(IntoResponse)] pub fn into_response_derive(input: TokenStream) -> TokenStream { @@ -16,7 +16,9 @@ fn into_response_derive_impl(input: DeriveInput) -> TokenStream { let expanded = quote! { impl IntoResponse for #name { fn into_response(self) -> Response { - BaseResponse::create(self) + let version = env!("CARGO_PKG_VERSION"); + lib::serde::response::BaseResponse::new(version, self) + .into_response() } } }; diff --git a/src/axum/app.rs b/src/axum/app.rs index e69de29..556826c 100644 --- a/src/axum/app.rs +++ b/src/axum/app.rs @@ -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()); + } +} diff --git a/src/axum/load.rs b/src/axum/load.rs index e69de29..e44543d 100644 --- a/src/axum/load.rs +++ b/src/axum/load.rs @@ -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(file_path: Path) -> Result, io::Error> +where + Path: AsRef, +{ + load_file(file_path).await.map(Html) +} + +#[cfg(all(feature = "tokio", feature = "axum"))] +pub async fn load_file(file_path: Path) -> Result +where + Path: AsRef, +{ + 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()); + } + } +} diff --git a/src/axum/mod.rs b/src/axum/mod.rs index e69de29..ace9b3d 100644 --- a/src/axum/mod.rs +++ b/src/axum/mod.rs @@ -0,0 +1,4 @@ +pub mod app; +pub mod load; +pub mod response; +pub mod router; diff --git a/src/axum/response.rs b/src/axum/response.rs index e69de29..4d1a4bc 100644 --- a/src/axum/response.rs +++ b/src/axum/response.rs @@ -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 IntoResponse for BaseResponse { + 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 + ); + } +} diff --git a/src/axum/router.rs b/src/axum/router.rs index e69de29..f557cac 100644 --- a/src/axum/router.rs +++ b/src/axum/router.rs @@ -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) {} +/// 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()]; + } +} diff --git a/src/io/file.rs b/src/io/file.rs index e69de29..d0cace8 100644 --- a/src/io/file.rs +++ b/src/io/file.rs @@ -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(file_path: Path) -> Result, Error> +where + Path: AsRef, +{ + 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()); + } +} diff --git a/src/io/mod.rs b/src/io/mod.rs index e69de29..2e172cd 100644 --- a/src/io/mod.rs +++ b/src/io/mod.rs @@ -0,0 +1 @@ +pub mod file; diff --git a/src/lib.rs b/src/lib.rs index e69de29..8711057 100644 --- a/src/lib.rs +++ b/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; diff --git a/src/nom/combinators.rs b/src/nom/combinators.rs index e69de29..225274a 100644 --- a/src/nom/combinators.rs +++ b/src/nom/combinators.rs @@ -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(n: usize, predicate: F) -> impl Fn(Input) -> IResult +where + Input: InputTake + InputIter + InputLength + Slice>, + F: Fn(::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()); + } +} diff --git a/src/nom/mod.rs b/src/nom/mod.rs index b2819a7..e25f4b1 100644 --- a/src/nom/mod.rs +++ b/src/nom/mod.rs @@ -1 +1,2 @@ -pub mod parser; \ No newline at end of file +pub mod combinators; +pub mod util; diff --git a/src/nom/util.rs b/src/nom/util.rs index e69de29..ea824a2 100644 --- a/src/nom/util.rs +++ b/src/nom/util.rs @@ -0,0 +1,44 @@ +#[cfg(feature = "nom")] +use nom::{error::Error, IResult}; + +#[cfg(feature = "nom")] +pub trait IntoResult { + type Error; + fn into_result(self) -> Result; +} + +#[cfg(feature = "nom")] +impl IntoResult for IResult { + type Error = nom::Err>; + fn into_result(self) -> Result { + 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 + ))) + ); + } +} diff --git a/src/serde/mod.rs b/src/serde/mod.rs index e69de29..4c6f2cd 100644 --- a/src/serde/mod.rs +++ b/src/serde/mod.rs @@ -0,0 +1 @@ +pub mod response; diff --git a/src/serde/response.rs b/src/serde/response.rs index e69de29..8b1a4a9 100644 --- a/src/serde/response.rs +++ b/src/serde/response.rs @@ -0,0 +1,41 @@ +#[cfg(feature = "serde")] +use serde::Serialize; + +#[derive(Serialize)] +#[cfg(feature = "serde")] +pub struct BaseResponse { + pub version: String, + #[serde(flatten)] + pub body: T, // T must be a struct (or enum?) +} + +#[cfg(feature = "serde")] +impl BaseResponse { + pub fn new(version: impl Into, 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()); + } +} diff --git a/src/vector/distinct.rs b/src/vector/distinct.rs index e69de29..284f02e 100644 --- a/src/vector/distinct.rs +++ b/src/vector/distinct.rs @@ -0,0 +1,28 @@ +#[cfg(feature = "vec")] +pub trait Distinct { + fn distinct(&mut self); +} + +#[cfg(feature = "vec")] +impl Distinct for Vec { + 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]); + } +} diff --git a/src/vector/map.rs b/src/vector/map.rs index e69de29..e2d7c08 100644 --- a/src/vector/map.rs +++ b/src/vector/map.rs @@ -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 = 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)); + } +} diff --git a/src/vector/matrix.rs b/src/vector/matrix.rs index e69de29..0d61551 100644 --- a/src/vector/matrix.rs +++ b/src/vector/matrix.rs @@ -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]]); + } +} diff --git a/src/vector/mod.rs b/src/vector/mod.rs index e69de29..0735563 100644 --- a/src/vector/mod.rs +++ b/src/vector/mod.rs @@ -0,0 +1,4 @@ +pub mod distinct; +pub mod map; +pub mod matrix; +pub mod set; diff --git a/src/vector/set.rs b/src/vector/set.rs index e69de29..d585766 100644 --- a/src/vector/set.rs +++ b/src/vector/set.rs @@ -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 = 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)); + } +}