.
This commit is contained in:
parent
8bede90019
commit
142129d6eb
|
|
@ -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
7
rs/newt/Cargo.lock
generated
Normal 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
18
rs/newt/Cargo.toml
Normal 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"]
|
||||
22
rs/newt/src/builtin_impls.rs
Normal file
22
rs/newt/src/builtin_impls.rs
Normal 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
90
rs/newt/src/lib.rs
Normal 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
78
rs/newt/src/parse.rs
Normal 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
1
rs/newt/src/prelude.rs
Normal file
|
|
@ -0,0 +1 @@
|
|||
pub use crate::{Command, Template, Value};
|
||||
3
rs/newt/src/prelude_internal.rs
Normal file
3
rs/newt/src/prelude_internal.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
pub use crate::prelude::*;
|
||||
|
||||
pub use std::fmt::{Display, Formatter, Result as FmtResult, Write};
|
||||
|
|
@ -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
1
rs/src/prelude.rs
Normal file
|
|
@ -0,0 +1 @@
|
|||
pub use core::fmt::{Display, Formatter, Result as FmtResult};
|
||||
|
|
@ -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";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
5
shelves/Cargo.lock
generated
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
48
shelves/backend/prelude.rs
Normal file
48
shelves/backend/prelude.rs
Normal 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;
|
||||
17
shelves/backend/routes/hello.rs
Normal file
17
shelves/backend/routes/hello.rs
Normal 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))
|
||||
}
|
||||
81
shelves/backend/routes/home.rs
Normal file
81
shelves/backend/routes/home.rs
Normal 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 }));
|
||||
}
|
||||
|
||||
*/
|
||||
23
shelves/backend/routes/items.rs
Normal file
23
shelves/backend/routes/items.rs
Normal 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!()
|
||||
}
|
||||
50
shelves/backend/routes/mod.rs
Normal file
50
shelves/backend/routes/mod.rs
Normal 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()
|
||||
}
|
||||
16
shelves/backend/routes/shelves.rs
Normal file
16
shelves/backend/routes/shelves.rs
Normal 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!()
|
||||
}
|
||||
35
shelves/backend/templates/mod.rs
Normal file
35
shelves/backend/templates/mod.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue