1#![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#[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#[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}