diff --git a/rs/Cargo.toml b/rs/Cargo.toml index ad4c64c..82ad7e8 100644 --- a/rs/Cargo.toml +++ b/rs/Cargo.toml @@ -3,8 +3,14 @@ name = "atelier" version = "0.0.0" edition = "2024" +[workspace] +resolver = "2" +members = [".", "newt"] + [dependencies] rusqlite = { version = "*" } async-channel = { version = "*" } urlpattern = { version = "*" } -url = { version = "*" } \ No newline at end of file +url = { version = "*" } + +newt = { path = "./newt" } \ No newline at end of file diff --git a/rs/newt/Cargo.lock b/rs/newt/Cargo.lock new file mode 100644 index 0000000..725dfcb --- /dev/null +++ b/rs/newt/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "newt" +version = "0.0.0" diff --git a/rs/newt/Cargo.toml b/rs/newt/Cargo.toml new file mode 100644 index 0000000..d982884 --- /dev/null +++ b/rs/newt/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "newt" + +[workspace] +resolver = "2" +# members = ["derive", "derive/core"] + +[workspace.dependencies] +proc-macro-error = "1.0" +proc-macro2 = "1" +syn = { version = "2", features = ["full"] } +quote = "1" + +[dependencies] +# newt-derive = { path = "derive", optional = true } + +[features] +# derive = ["dep:newt-derive"] \ No newline at end of file diff --git a/rs/newt/src/builtin_impls.rs b/rs/newt/src/builtin_impls.rs new file mode 100644 index 0000000..f1cfc72 --- /dev/null +++ b/rs/newt/src/builtin_impls.rs @@ -0,0 +1,22 @@ +use crate::prelude_internal::*; + +impl Value for () { +} + +impl Value for &str { + fn render(&self, fmt: &mut dyn Write) -> FmtResult { + write!(fmt, "{self}") + } +} + +impl Value for [(&str, &dyn Value); N] { + fn lookup(&self, name: &str) -> Option<&dyn Value> { + for (k, v) in self.iter() { + if *k == name { + return Some(*v); + } + } + + None + } +} diff --git a/rs/newt/src/lib.rs b/rs/newt/src/lib.rs new file mode 100644 index 0000000..c4293c9 --- /dev/null +++ b/rs/newt/src/lib.rs @@ -0,0 +1,90 @@ +mod builtin_impls; +pub mod parse; +pub mod prelude; +mod prelude_internal; + +use crate::prelude_internal::*; + +pub trait Value { + fn render(&self, w: &mut dyn Write) -> FmtResult { + Ok(()) + } + + fn lookup(&self, name: &str) -> Option<&dyn Value> { + None + } +} + +pub struct Template { + commands: Vec, +} +impl Template { + pub fn render(&self, env: &dyn Value, target: &mut dyn Write) { + for command in self.commands.iter() { + match command { + Command::Print(s) => { + let _ = write!(target, "{s}"); + }, + Command::LookupPrint(s) => { + if let Some(value) = env.lookup(s) { + value.render(target); + } else { + let _ = write!(target, "<<<'{s}' not found>>>"); + } + }, + } + } + } +} + +pub enum Command { + Print(String), + LookupPrint(String), +} + +pub fn parse(input: &str) -> Template { + let mut state = parse::ParseState::new(input); + let mut commands = vec![]; + + state.parse_root(&mut commands); + + Template { commands } +} + +pub fn render(input: &str, env: &dyn Value) -> String { + let template = parse(input); + let mut output = String::new(); + + template.render(env, &mut output); + + output +} + +#[macro_export] +macro_rules! env { + ($($name:literal : $value:expr),* $(,)?) => { + &[$(($name, &$value as &dyn crate::Value),)*] + }; +} + +#[cfg(test)] +mod test { + macro_rules! go { + (($input:expr, $env:expr), $output:expr) => { + assert_eq!(crate::render($input, $env), $output) + }; + } + + #[test] + fn simple() { + go!(("Hello, World!", &()), "Hello, World!"); + } + + #[test] + fn lookup() { + go!( + ("Hello, {name}!", env! { "name": "Steve" }), + "Hello, Steve!" + ); + } +} diff --git a/rs/newt/src/parse.rs b/rs/newt/src/parse.rs new file mode 100644 index 0000000..a32b890 --- /dev/null +++ b/rs/newt/src/parse.rs @@ -0,0 +1,78 @@ +use crate::prelude_internal::*; + +pub struct ParseState<'a> { + input: &'a str, + at: usize, +} +impl<'a> ParseState<'a> { + pub fn new(input: &'a str) -> Self { + Self { input, at: 0 } + } + + pub fn is_empty(&self) -> bool { + self.head().is_empty() + } + + pub fn head(&self) -> &str { + &self.input[self.at..] + } + + pub fn goto(&mut self, at: usize) { + self.at = at; + } + + pub fn skip(&mut self, amt: usize) { + self.at += amt; + } + + pub fn back(&mut self, amt: usize) { + self.at -= amt; + } + + pub fn parse_root(&mut self, out: &mut Vec) { + while !self.is_empty() { + let head = self.head(); + if let Some(bo) = head.find('{') { + if bo != 0 { + out.push(Command::Print(head[0..bo].to_string())); + } + + self.goto(bo); + self.parse_directive(out); + } else { + out.push(Command::Print(head.to_string())); + return; + } + } + } + + pub fn parse_directive(&mut self, out: &mut Vec) { + assert!(self.head().starts_with('{')); + + let mut ok = Err(self.at); + + 'inner: { + self.skip(1); + let head = self.head(); + if let Some(bo) = head.find('}') { + let inner = &head[..bo]; + let bo = bo + 1; + + let mut inner = inner.split(' ').collect::>(); + match inner.as_slice() { + &[name] => { + out.push(Command::LookupPrint(name.to_string())); + ok = Ok(bo); + }, + _ => (), + } + } else { + } + }; + + match ok { + Err(checkpoint) => self.goto(checkpoint), + Ok(skip) => self.skip(skip), + } + } +} diff --git a/rs/newt/src/prelude.rs b/rs/newt/src/prelude.rs new file mode 100644 index 0000000..6f23295 --- /dev/null +++ b/rs/newt/src/prelude.rs @@ -0,0 +1 @@ +pub use crate::{Command, Template, Value}; diff --git a/rs/newt/src/prelude_internal.rs b/rs/newt/src/prelude_internal.rs new file mode 100644 index 0000000..6f6c9c7 --- /dev/null +++ b/rs/newt/src/prelude_internal.rs @@ -0,0 +1,3 @@ +pub use crate::prelude::*; + +pub use std::fmt::{Display, Formatter, Result as FmtResult, Write}; diff --git a/rs/src/lib.rs b/rs/src/lib.rs index 4883262..07398dd 100644 --- a/rs/src/lib.rs +++ b/rs/src/lib.rs @@ -1,4 +1,5 @@ pub mod containers; +pub mod prelude; pub mod router; pub mod rusqlite_thread_pool; diff --git a/rs/src/prelude.rs b/rs/src/prelude.rs new file mode 100644 index 0000000..5aa9b65 --- /dev/null +++ b/rs/src/prelude.rs @@ -0,0 +1 @@ +pub use core::fmt::{Display, Formatter, Result as FmtResult}; diff --git a/rs/src/router.rs b/rs/src/router.rs index 6d6c24b..4addd21 100644 --- a/rs/src/router.rs +++ b/rs/src/router.rs @@ -37,7 +37,10 @@ impl Router { } } - pub fn register(&mut self, method: String, pathname: String, value: T) { + pub fn register(&mut self, method: &str, pathname: &str, value: T) { + let pathname = pathname.to_string(); + let method = method.to_string(); + let upwi = UrlPatternWithInput { pathname: pathname.clone(), pattern: UrlPattern::parse( @@ -90,3 +93,9 @@ impl Router { Err(404) } } + +pub mod methods { + pub const GET: &'static str = "GET"; + pub const POST: &'static str = "POST"; + pub const PUT: &'static str = "PUT"; +} diff --git a/rustfmt.toml b/rustfmt.toml index 7000591..e135cbf 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -3,3 +3,4 @@ hard_tabs = true match_block_trailing_comma = true max_width = 80 empty_item_single_line = false +format_strings = true diff --git a/shelves/Cargo.lock b/shelves/Cargo.lock index b1c5745..1c1d768 100644 --- a/shelves/Cargo.lock +++ b/shelves/Cargo.lock @@ -43,6 +43,7 @@ name = "atelier" version = "0.0.0" dependencies = [ "async-channel", + "newt", "rusqlite", "url", "urlpattern", @@ -586,6 +587,10 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "newt" +version = "0.0.0" + [[package]] name = "object" version = "0.36.7" diff --git a/shelves/backend/main.rs b/shelves/backend/main.rs index a825387..4c4bf3c 100644 --- a/shelves/backend/main.rs +++ b/shelves/backend/main.rs @@ -1,21 +1,15 @@ +mod prelude; +use prelude::*; +mod routes; +mod templates; + use std::net::SocketAddr; -use std::pin::Pin; use std::sync::Arc; use axum::handler::HandlerWithoutStateExt; -use axum::response::IntoResponse; use std::error::Error; use tokio::net::TcpListener; -pub type AnyError = Box; -pub type Result = core::result::Result; - -pub type Dbs = atelier::rusqlite_thread_pool::PoolSender; -pub struct RequestCtx { - dbs: Dbs, - path_params: atelier::router::PathParams, -} - fn migrate(connection: &mut rusqlite::Connection) -> Result<()> { Ok(()) } @@ -24,58 +18,7 @@ fn init(connection: &mut rusqlite::Connection) -> rusqlite::Result<()> { Ok(()) } -type Request = axum::extract::Request; -type Response = axum::response::Response; -type Handler = Box< - dyn Fn( - Request, - RequestCtx, - ) -> Pin + Send + 'static>> - + Send - + Sync - + 'static, ->; -type Router = atelier::router::Router; - -fn make_handler(f: F) -> Handler -where - Fut: Future + Send + 'static, - F: FnMut(Request, RequestCtx) -> Fut + Clone + Send + Sync + 'static, -{ - Box::new(move |req, ctx| { - let mut f = f.clone(); - Box::pin(async move { f(req, ctx).await }) - }) -} - -async fn get_hello(req: Request, ctx: RequestCtx) -> Response { - "hello".into_response() -} - -async fn get_hello_name(req: Request, ctx: RequestCtx) -> Response { - format!("Hello, {}", ctx.path_params.get("name")).into_response() -} - -fn make_router() -> Router { - let mut router = atelier::router::Router::new(); - - macro_rules! r { - ($m:expr, $p:expr, $h:expr) => { - router.register($m.to_string(), $p.to_string(), make_handler($h)) - }; - } - - r!("GET", "/hello", get_hello); - r!("GET", "/hello/:name", get_hello_name); - - router -} - -async fn serve( - dbs: Dbs, - router: Arc, - req: axum::extract::Request, -) -> axum::response::Response { +async fn serve(dbs: DbS, router: Arc, req: Request) -> Response { let method = req.method().as_str(); let uri = format!("{}", req.uri()); @@ -104,7 +47,7 @@ async fn go() -> Result<()> { let addr = "127.0.0.1:8333".parse::()?; let listener = TcpListener::bind(addr).await?; - let router = Arc::new(make_router()); + let router = Arc::new(routes::make_router()); let s = move |req: Request| { let tx = tx.clone(); let router = router.clone(); diff --git a/shelves/backend/prelude.rs b/shelves/backend/prelude.rs new file mode 100644 index 0000000..e73b596 --- /dev/null +++ b/shelves/backend/prelude.rs @@ -0,0 +1,48 @@ +use std::error::Error; +use std::pin::Pin; + +pub use atelier::rusqlite_thread_pool::PoolSender as DbS; +pub type Request = axum::extract::Request; +pub struct RequestCtx { + pub dbs: DbS, + pub path_params: atelier::router::PathParams, +} + +pub type Response = axum::response::Response; +pub type Handler = Box< + dyn Fn( + Request, + RequestCtx, + ) -> Pin + Send + 'static>> + + Send + + Sync + + 'static, +>; +pub type Router = atelier::router::Router; +pub type AnyError = Box; +pub type Result = core::result::Result; + +pub use axum::response::IntoResponse; +pub enum HandlerError { + InternalServerError(AnyError), +} +impl From for HandlerError +where + E: Error + Send + Sync + 'static, +{ + fn from(v: E) -> Self { + Self::InternalServerError(Box::new(v)) + } +} +impl IntoResponse for HandlerError { + fn into_response(self) -> Response { + "".into_response() + } +} + +pub type HandlerResult = core::result::Result; + +pub use crate::templates::{template_fn, Template, TemplateFn}; +pub use core::fmt::{Display, Formatter}; +pub type FmtResult = core::fmt::Result; +pub use axum::response::Html; diff --git a/shelves/backend/routes/hello.rs b/shelves/backend/routes/hello.rs new file mode 100644 index 0000000..7cb8025 --- /dev/null +++ b/shelves/backend/routes/hello.rs @@ -0,0 +1,17 @@ +use crate::prelude::*; + +fn Hello(name: &str) -> impl Template { + template_fn(move |f| write!(f, "Hello, {name}")) +} + +pub async fn view( + request: Request, + ctx: RequestCtx, +) -> HandlerResult { + let name = ctx.path_params.get("name"); + + let template = Hello(name).display(); + let output = format!("{template}"); + + Ok(Html(output)) +} diff --git a/shelves/backend/routes/home.rs b/shelves/backend/routes/home.rs new file mode 100644 index 0000000..04aa74c --- /dev/null +++ b/shelves/backend/routes/home.rs @@ -0,0 +1,81 @@ +use crate::prelude::*; + +enum ActivityType { + CreatedItem, +} +struct Activity { + activity_type: ActivityType, + item_xid: String, + created_timestamp: String, +} + +pub async fn view(req: Request, ctx: RequestCtx) -> HandlerResult { + let activities: Vec<_> = ctx + .dbs + .send(|conn| { + let mut stmt = conn.prepare( + r#" + select activity_json_blob from activity + order by json_extract(activity_json_blob, '$.created_timestamp') + limit 20 + "#, + )?; + + Ok(stmt.query_map([], |r| r.get::<_, String>(0))?.collect()) + }) + .await?; + + todo!() + /* + db.prepare(` + `).values<[string]>().map(([blob]) => JSON.parse(blob)); + + const body = ` +

Recent Activity

+ ${ActivityList({ activities: recentActivities })} + `; + return html(View({ title: 'Home', body })); + */ +} + +/* +import { View } from '@/backend/templates/index.ts'; +import { html } from '@atelier/responses.ts'; +import { arrayIsEmpty } from '@atelier/array.ts'; + +type ActivityType = 'created_item'; +type Activity = { + activityType: ActivityType; + itemXid: string; + createdTimestamp: string; +}; + +function Activity(props: { activity: Activity }) {} +function ActivityList(props: { activities: Activity[] }) { + if (arrayIsEmpty(props.activities)) { + return 'No activities yet!'; + } + + const activities = props.activities.map((activity) => Activity({ activity })); + return ` +
    + +
+ `; +} + +export function viewHome(req: Request, { db }: RequestCtx) { + const recentActivities: Activity[] = db.prepare(` + select activity_json_blob from activity + order by json_extract(activity_json_blob, '$.createdTimestamp') + limit 20 + `).values<[string]>().map(([blob]) => JSON.parse(blob)); + + const body = ` +

Recent Activity

+ ${ActivityList({ activities: recentActivities })} + `; + return html(View({ title: 'Home', body })); +} + +*/ diff --git a/shelves/backend/routes/items.rs b/shelves/backend/routes/items.rs new file mode 100644 index 0000000..939bf8b --- /dev/null +++ b/shelves/backend/routes/items.rs @@ -0,0 +1,23 @@ +use crate::prelude::*; + +pub async fn view(req: Request, ctx: RequestCtx) -> HandlerResult { + todo!() +} + +pub async fn view_one(req: Request, ctx: RequestCtx) -> HandlerResult { + todo!() +} + +pub async fn view_create( + req: Request, + ctx: RequestCtx, +) -> HandlerResult { + todo!() +} + +pub async fn post_create( + req: Request, + ctx: RequestCtx, +) -> HandlerResult { + todo!() +} diff --git a/shelves/backend/routes/mod.rs b/shelves/backend/routes/mod.rs new file mode 100644 index 0000000..dceb268 --- /dev/null +++ b/shelves/backend/routes/mod.rs @@ -0,0 +1,50 @@ +mod hello; +mod home; +mod items; +mod shelves; + +use atelier::router::methods::*; +use std::pin::Pin; + +use crate::prelude::*; + +fn make_handler(f: F) -> Handler +where + Fut: Future + Send + 'static, + F: FnMut(Request, RequestCtx) -> Fut + Clone + Send + Sync + 'static, + R: IntoResponse, +{ + Box::new(move |req, ctx| { + let mut f = f.clone(); + Box::pin(async move { f(req, ctx).await.into_response() }) + }) +} + +pub fn make_router() -> Router { + let mut router = atelier::router::Router::new(); + + macro_rules! r { + ($m:expr, $p:expr, $h:expr) => { + router.register($m, $p, make_handler($h)) + }; + } + + r!(GET, "/", home::view); + r!(GET, "/shelves", shelves::view); + r!(GET, "/shelves/:shelf_xid", shelves::view_one); + r!(GET, "/items", items::view); + r!(GET, "/items/create", items::view_create); + r!(GET, "/items/:item_xid", items::view_one); + r!(POST, "/items/create", items::post_create); + r!(GET, "/hello/:name", hello::view); + + r!(GET, "/static/*", serve_static); + + router +} + +pub async fn serve_static(req: Request, ctx: RequestCtx) -> Response { + let url = req.uri().path(); + + "".into_response() +} diff --git a/shelves/backend/routes/shelves.rs b/shelves/backend/routes/shelves.rs new file mode 100644 index 0000000..331403a --- /dev/null +++ b/shelves/backend/routes/shelves.rs @@ -0,0 +1,16 @@ +use crate::prelude::*; + +pub async fn view(req: Request, ctx: RequestCtx) -> HandlerResult { + todo!() +} + +pub async fn view_one(req: Request, ctx: RequestCtx) -> HandlerResult { + todo!() +} + +pub async fn view_create( + req: Request, + ctx: RequestCtx, +) -> HandlerResult { + todo!() +} diff --git a/shelves/backend/templates/mod.rs b/shelves/backend/templates/mod.rs new file mode 100644 index 0000000..e33dab1 --- /dev/null +++ b/shelves/backend/templates/mod.rs @@ -0,0 +1,35 @@ +use crate::prelude::*; + +pub trait Template { + fn render(&self, fmt: &mut Formatter) -> FmtResult; + + fn display(self) -> impl Display + where + Self: Sized, + { + struct D(T); + impl Display for D { + fn fmt(&self, fmt: &mut Formatter) -> FmtResult { + self.0.render(fmt) + } + } + + D(self) + } +} + +pub struct TemplateFn(F); +pub fn template_fn(f: F) -> TemplateFn +where + F: Fn(&mut Formatter) -> FmtResult, +{ + TemplateFn(f) +} +impl Template for TemplateFn +where + F: Fn(&mut Formatter) -> FmtResult, +{ + fn render(&self, fmt: &mut Formatter) -> FmtResult { + self.0(fmt) + } +}