tatara_lisp_script/stdlib/
http_server.rs1use std::sync::Arc;
26
27use tatara_lisp_eval::{Arity, EvalError, Interpreter, Value};
28
29use crate::script_ctx::ScriptCtx;
30
31pub fn install(interp: &mut Interpreter<ScriptCtx>) {
32 interp.register_fn(
33 "http-serve-static",
34 Arity::Exact(2),
35 |args: &[Value], _ctx: &mut ScriptCtx, sp| {
36 let port: u16 = match &args[0] {
37 Value::Int(n) => u16::try_from(*n).map_err(|_| {
38 EvalError::native_fn(
39 "http-serve-static",
40 format!("http-serve-static: port {n} out of range"),
41 sp,
42 )
43 })?,
44 v => {
45 return Err(EvalError::native_fn(
46 "http-serve-static",
47 format!("http-serve-static: port must be int, got {v:?}"),
48 sp,
49 ))
50 }
51 };
52
53 let routes = parse_routes(&args[1], sp)?;
54 let addr = format!("0.0.0.0:{port}");
55 let server = tiny_http::Server::http(&addr).map_err(|e| {
56 EvalError::native_fn(
57 "http-serve-static",
58 format!("http-serve-static bind {addr} failed: {e}"),
59 sp,
60 )
61 })?;
62
63 eprintln!("[http-serve-static] listening on http://{addr}");
64 for path_route in &routes {
65 eprintln!(
66 "[http-serve-static] {} → {}",
67 path_route.path, path_route.status
68 );
69 }
70
71 for request in server.incoming_requests() {
72 let path = request.url();
73 let path_only = path.split('?').next().unwrap_or(path).to_string();
74
75 let chosen: Option<&Route> = routes.iter().find(|r| r.path == path_only);
76
77 let response = match chosen {
78 Some(r) => {
79 eprintln!(
80 "[http-serve-static] {} {} → {}",
81 request.method(),
82 path,
83 r.status
84 );
85 tiny_http::Response::from_string(r.body.clone())
86 .with_status_code(r.status)
87 .with_header(json_header())
88 }
89 None => {
90 eprintln!("[http-serve-static] {} {} → 404", request.method(), path);
91 tiny_http::Response::from_string(r#"{"error":"not_found"}"#.to_string())
92 .with_status_code(404)
93 .with_header(json_header())
94 }
95 };
96 let _ = request.respond(response);
97 }
98 Ok(Value::Nil)
99 },
100 );
101}
102
103fn json_header() -> tiny_http::Header {
104 tiny_http::Header::from_bytes(b"Content-Type".as_ref(), b"application/json".as_ref())
105 .expect("static header")
106}
107
108#[derive(Debug)]
109struct Route {
110 path: String,
111 status: i32,
112 body: String,
113}
114
115fn parse_routes(value: &Value, sp: tatara_lisp::Span) -> Result<Vec<Route>, EvalError> {
116 let outer = match value {
117 Value::List(l) => l.as_ref(),
118 v => {
119 return Err(EvalError::native_fn(
120 "http-serve-static",
121 format!("http-serve-static: routes must be a list, got {v:?}"),
122 sp,
123 ))
124 }
125 };
126
127 let mut routes = Vec::with_capacity(outer.len());
128 for entry in outer {
129 let triple = match entry {
130 Value::List(l) => l.as_ref(),
131 v => {
132 return Err(EvalError::native_fn(
133 "http-serve-static",
134 format!("http-serve-static: each route must be a 3-list, got {v:?}"),
135 sp,
136 ))
137 }
138 };
139 if triple.len() != 3 {
140 return Err(EvalError::native_fn(
141 "http-serve-static",
142 format!(
143 "http-serve-static: route needs (path status body), got {} elements",
144 triple.len()
145 ),
146 sp,
147 ));
148 }
149
150 let path = match &triple[0] {
151 Value::Str(s) => s.to_string(),
152 v => {
153 return Err(EvalError::native_fn(
154 "http-serve-static",
155 format!("http-serve-static: path must be string, got {v:?}"),
156 sp,
157 ))
158 }
159 };
160 let status: i32 = match &triple[1] {
161 Value::Int(n) => i32::try_from(*n).map_err(|_| {
162 EvalError::native_fn(
163 "http-serve-static",
164 format!("http-serve-static: status {n} out of range"),
165 sp,
166 )
167 })?,
168 v => {
169 return Err(EvalError::native_fn(
170 "http-serve-static",
171 format!("http-serve-static: status must be int, got {v:?}"),
172 sp,
173 ))
174 }
175 };
176 let body = match &triple[2] {
177 Value::Str(s) => s.to_string(),
178 v => {
179 return Err(EvalError::native_fn(
180 "http-serve-static",
181 format!("http-serve-static: body must be string, got {v:?}"),
182 sp,
183 ))
184 }
185 };
186 routes.push(Route { path, status, body });
187 }
188 Ok(routes)
189}
190
191#[allow(dead_code)]
193fn _unused() -> Arc<()> {
194 Arc::new(())
195}