mikrozen/
lib.rs

1//! # mikrozen
2//!
3//! A minimal, extensible, and WASM-first micro-framework for routing and response handling.
4//!
5//! ## Features
6//! - Simple router and response macros
7//! - WASM and no_std compatible
8//! - Uses [dlmalloc](https://github.com/alexcrichton/dlmalloc-rs) as the global allocator for production-ready WebAssembly applications
9//!
10//! ## Example
11//!
12//! ```rust
13//! #![no_std]
14//! extern crate alloc;
15//!
16//! use mikrozen::prelude::*;
17//!
18//! fn hello(args: Input) -> Output {
19//!     let name = args.get_str("name");
20//!     response! {
21//!         "message" => format!("Hello, {}", name),
22//!         "success" => true,
23//!     }
24//! }
25//!
26//! router! {
27//!     "hello" => hello,
28//! }
29//!
30//! # fn main() {}
31//! ```
32//!
33//! ## Global Allocator
34//!
35//! This crate uses [dlmalloc](https://github.com/alexcrichton/dlmalloc-rs) as the global allocator, which is a production-ready allocator
36//! based on the reliable dlmalloc implementation used by emscripten.
37#![no_std]
38
39extern crate alloc;
40
41#[cfg(not(any(test, feature = "test-utils")))]
42extern crate dlmalloc;
43
44#[cfg(any(test, feature = "test-utils"))]
45extern crate std;
46
47use alloc::collections::BTreeMap;
48use alloc::string::String;
49#[allow(unused_imports)]
50use alloc::string::ToString;
51use alloc::vec::Vec;
52use serde_json::Value;
53
54// dlmalloc handles setting up the global allocator when the "global" feature is enabled
55// For tests, we use the system allocator
56
57// For testing purposes, we need to ensure we have a heap
58#[cfg(any(test, feature = "test-utils"))]
59#[global_allocator]
60static ALLOC: std::alloc::System = std::alloc::System;
61
62pub mod prelude {
63    pub use super::{response, router, Input, Output, RouterInput};
64    pub use alloc::collections::BTreeMap;
65    pub use alloc::format;
66    pub use alloc::string::ToString;
67    pub use serde_json::Value;
68}
69
70pub type Input = RouterInput;
71pub type Output = Value;
72
73pub struct RouterInput(pub BTreeMap<String, Value>);
74
75impl RouterInput {
76    pub fn new(map: BTreeMap<String, Value>) -> Self {
77        Self(map)
78    }
79    #[cfg(feature = "decimal")]
80    pub fn get_decimal(&self, key: &str) -> rust_decimal::Decimal {
81        use rust_decimal::prelude::FromPrimitive;
82        use rust_decimal::prelude::FromStr;
83        match self.0.get(key) {
84            Some(Value::Number(n)) => rust_decimal::Decimal::from_f64(n.as_f64().unwrap_or(0.0))
85                .unwrap_or(rust_decimal::Decimal::ZERO),
86            Some(Value::String(s)) => {
87                rust_decimal::Decimal::from_str(s).unwrap_or(rust_decimal::Decimal::ZERO)
88            }
89            _ => rust_decimal::Decimal::ZERO,
90        }
91    }
92    pub fn get_i64(&self, key: &str) -> i64 {
93        self.0.get(key).and_then(Value::as_i64).unwrap_or(0)
94    }
95    pub fn get_f64(&self, key: &str) -> f64 {
96        self.0.get(key).and_then(Value::as_f64).unwrap_or(0.0)
97    }
98    pub fn get_bool(&self, key: &str) -> bool {
99        self.0.get(key).and_then(Value::as_bool).unwrap_or(false)
100    }
101    pub fn get_str(&self, key: &str) -> &str {
102        self.0.get(key).and_then(Value::as_str).unwrap_or("")
103    }
104    pub fn get_array(&self, key: &str) -> Vec<Value> {
105        self.0
106            .get(key)
107            .and_then(Value::as_array)
108            .cloned()
109            .unwrap_or_default()
110    }
111    pub fn get_object(&self, key: &str) -> BTreeMap<String, Value> {
112        self.0
113            .get(key)
114            .and_then(Value::as_object)
115            .map(|m| m.clone().into_iter().collect())
116            .unwrap_or_default()
117    }
118    pub fn get_value(&self, key: &str) -> Option<&Value> {
119        self.0.get(key)
120    }
121    pub fn has(&self, key: &str) -> bool {
122        self.0.contains_key(key)
123    }
124    pub fn raw(&self) -> &BTreeMap<String, Value> {
125        &self.0
126    }
127}
128
129#[macro_export]
130macro_rules! router {
131    ( $( $route:expr => $handler:ident ),* $(,)? ) => {
132        pub struct Router;
133        impl Router {
134            pub fn dispatch(path: &str, input: $crate::Input) -> $crate::Output {
135                match path {
136                    $( $route => $handler(input), )*
137                    _ => $crate::response! {
138                        "error" => ::alloc::format!("Route not found: {}", path),
139                        "success" => false,
140                    },
141                }
142            }
143        }
144    };
145}
146
147#[macro_export]
148macro_rules! response {
149    ( $( $key:expr => $value:expr ),* $(,)? ) => {{
150        let mut map = ::serde_json::Map::new();
151        $( map.insert($key.to_string(), ::serde_json::json!($value)); )*
152        ::serde_json::Value::Object(map)
153    }};
154}
155
156/// # Example
157///
158/// ```rust
159/// use mikrozen::prelude::*;
160///
161/// fn hello(args: Input) -> Output {
162///     let name = args.get_str("name");
163///     response! {
164///         "message" => alloc::format!("Hello, {}", name),
165///         "success" => true,
166///     }
167/// }
168///
169/// router! {
170///     "hello" => hello,
171/// }
172///
173/// // Usage:
174/// // let input = RouterInput::new(BTreeMap::new());
175/// // let out = Router::dispatch("hello", input);
176/// ```
177
178#[cfg(test)]
179mod tests {
180    use super::*;
181    use alloc::collections::BTreeMap;
182    use alloc::format;
183
184    fn hello(args: Input) -> Output {
185        let name = args.get_str("name");
186
187        #[cfg(feature = "decimal")]
188        {
189            let price = args.get_decimal("price");
190            return response! {
191                "message" => format!("Hello, {}", name),
192                "success" => true,
193                "price" => price,
194            };
195        }
196
197        #[cfg(not(feature = "decimal"))]
198        {
199            return response! {
200                "message" => format!("Hello, {}", name),
201                "success" => true,
202            };
203        }
204    }
205
206    router! {
207        "hello" => hello,
208    }
209
210    #[test]
211    fn test_hello_route() {
212        let mut map = BTreeMap::new();
213        map.insert("name".to_string(), Value::String("World".to_string()));
214        #[cfg(feature = "decimal")]
215        map.insert(
216            "price".to_string(),
217            Value::Number(serde_json::Number::from_f64(100.0).unwrap()),
218        );
219        let input = RouterInput::new(map);
220        let out = Router::dispatch("hello", input);
221        assert_eq!(out["message"], "Hello, World");
222        assert_eq!(out["success"], true);
223        #[cfg(feature = "decimal")]
224        assert_eq!(out["price"].as_str().unwrap(), "100");
225    }
226
227    #[test]
228    fn test_missing_route() {
229        let input = RouterInput::new(BTreeMap::new());
230        let out = Router::dispatch("missing", input);
231        assert_eq!(out["success"], false);
232        assert!(out["error"].as_str().unwrap().contains("Route not found"));
233    }
234}