This commit is contained in:
soup 2025-02-14 21:28:56 -05:00
parent 8bede90019
commit 142129d6eb
Signed by: soup
SSH key fingerprint: SHA256:GYxje8eQkJ6HZKzVWDdyOUF1TyDiprruGhE0Ym8qYDY
21 changed files with 521 additions and 66 deletions

View file

@ -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 = "*" }
url = { version = "*" }
newt = { path = "./newt" }

7
rs/newt/Cargo.lock generated Normal file
View file

@ -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"

18
rs/newt/Cargo.toml Normal file
View file

@ -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"]

View file

@ -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<const N: usize> 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
}
}

90
rs/newt/src/lib.rs Normal file
View file

@ -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<Command>,
}
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!"
);
}
}

78
rs/newt/src/parse.rs Normal file
View file

@ -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<Command>) {
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<Command>) {
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::<Vec<_>>();
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),
}
}
}

1
rs/newt/src/prelude.rs Normal file
View file

@ -0,0 +1 @@
pub use crate::{Command, Template, Value};

View file

@ -0,0 +1,3 @@
pub use crate::prelude::*;
pub use std::fmt::{Display, Formatter, Result as FmtResult, Write};

View file

@ -1,4 +1,5 @@
pub mod containers;
pub mod prelude;
pub mod router;
pub mod rusqlite_thread_pool;

1
rs/src/prelude.rs Normal file
View file

@ -0,0 +1 @@
pub use core::fmt::{Display, Formatter, Result as FmtResult};

View file

@ -37,7 +37,10 @@ impl<T> Router<T> {
}
}
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<T> Router<T> {
Err(404)
}
}
pub mod methods {
pub const GET: &'static str = "GET";
pub const POST: &'static str = "POST";
pub const PUT: &'static str = "PUT";
}

View file

@ -3,3 +3,4 @@ hard_tabs = true
match_block_trailing_comma = true
max_width = 80
empty_item_single_line = false
format_strings = true

5
shelves/Cargo.lock generated
View file

@ -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"

View file

@ -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<dyn Error + Send + Sync + 'static>;
pub type Result<T> = core::result::Result<T, AnyError>;
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<Box<dyn Future<Output = Response> + Send + 'static>>
+ Send
+ Sync
+ 'static,
>;
type Router = atelier::router::Router<Handler>;
fn make_handler<Fut, F>(f: F) -> Handler
where
Fut: Future<Output = Response> + 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<Router>,
req: axum::extract::Request,
) -> axum::response::Response {
async fn serve(dbs: DbS, router: Arc<Router>, 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::<SocketAddr>()?;
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();

View file

@ -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<Box<dyn Future<Output = Response> + Send + 'static>>
+ Send
+ Sync
+ 'static,
>;
pub type Router = atelier::router::Router<Handler>;
pub type AnyError = Box<dyn Error + Send + Sync + 'static>;
pub type Result<T> = core::result::Result<T, AnyError>;
pub use axum::response::IntoResponse;
pub enum HandlerError {
InternalServerError(AnyError),
}
impl<E> From<E> 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<T: IntoResponse> = core::result::Result<T, HandlerError>;
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;

View file

@ -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<impl IntoResponse> {
let name = ctx.path_params.get("name");
let template = Hello(name).display();
let output = format!("{template}");
Ok(Html(output))
}

View file

@ -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<String> {
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 = `
<h1>Recent Activity</h1>
${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 `
<ul>
</ul>
`;
}
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 = `
<h1>Recent Activity</h1>
${ActivityList({ activities: recentActivities })}
`;
return html(View({ title: 'Home', body }));
}
*/

View file

@ -0,0 +1,23 @@
use crate::prelude::*;
pub async fn view(req: Request, ctx: RequestCtx) -> HandlerResult<String> {
todo!()
}
pub async fn view_one(req: Request, ctx: RequestCtx) -> HandlerResult<String> {
todo!()
}
pub async fn view_create(
req: Request,
ctx: RequestCtx,
) -> HandlerResult<String> {
todo!()
}
pub async fn post_create(
req: Request,
ctx: RequestCtx,
) -> HandlerResult<String> {
todo!()
}

View file

@ -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<Fut, F, R>(f: F) -> Handler
where
Fut: Future<Output = R> + 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()
}

View file

@ -0,0 +1,16 @@
use crate::prelude::*;
pub async fn view(req: Request, ctx: RequestCtx) -> HandlerResult<String> {
todo!()
}
pub async fn view_one(req: Request, ctx: RequestCtx) -> HandlerResult<String> {
todo!()
}
pub async fn view_create(
req: Request,
ctx: RequestCtx,
) -> HandlerResult<String> {
todo!()
}

View file

@ -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>(T);
impl<T: Template> Display for D<T> {
fn fmt(&self, fmt: &mut Formatter) -> FmtResult {
self.0.render(fmt)
}
}
D(self)
}
}
pub struct TemplateFn<F>(F);
pub fn template_fn<F>(f: F) -> TemplateFn<F>
where
F: Fn(&mut Formatter) -> FmtResult,
{
TemplateFn(f)
}
impl<F> Template for TemplateFn<F>
where
F: Fn(&mut Formatter) -> FmtResult,
{
fn render(&self, fmt: &mut Formatter) -> FmtResult {
self.0(fmt)
}
}