initial commit

This commit is contained in:
Borodinov Ilya 2024-12-06 00:05:22 +03:00
commit 6349ff9412
Signed by: noth
GPG key ID: 75503B2EF596D1BD
16 changed files with 2201 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/target

1209
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

26
Cargo.toml Normal file
View file

@ -0,0 +1,26 @@
[workspace]
members = [
".",
"vespid",
"vespid/macros"
]
[workspace.dependencies]
vespid.path = "vespid"
axum = "0.7"
tokio = { version = "1", features = ["full"] }
[package]
name = "crusto"
version = "0.1.0"
edition = "2021"
[dependencies]
vespid.workspace = true
tokio.workspace = true
axum.workspace = true
eyre = "0.6.12"
color-eyre = "0.6.3"
tracing = "0.1.41"
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
tracing-error = "0.2.1"

11
LICENSE Normal file
View file

@ -0,0 +1,11 @@
Anti-GitHub License (AGHL) v1 (based on the MIT license)
Copyright (c) 2024 Ilya Borodinov <borodinov.ilya@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
1. The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
2. The Software shall not be published, distributed, or otherwise made available on GitHub.com or any of its subdomains or affiliated websites. Failure to comply with this condition will immediately revoke the rights granted hereunder.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

24
README.md Normal file
View file

@ -0,0 +1,24 @@
# crusto - the git forge you never knew you needed
[![License: AGHL v1](https://badgers.space/badge/License/AGHL%20v1/blue)](./LICENSE)
[![Please don't upload to GitHub](https://nogithub.codeberg.page/badge.svg)](https://nogithub.codeberg.page)
crusto is a git forge written in Rust, that uses:
- vespid, a custom built in-tree (for now) SSR framework
- htmx, as the HATEOAS client layer
- axum, as the web server
- sleep deprivation, as the motivation
## how to run
```bash
cargo r
```
You can set `RUST_LOG=crusto=trace` to see traces with TRACE and up. By default it logs INFO and up traces.
## roadmap
- [ ] repo shepherding
- [ ] auth and users
- [ ] repo creation

54
src/main.rs Normal file
View file

@ -0,0 +1,54 @@
#[macro_use]
extern crate tracing;
use axum::{response::Html, routing::get};
use tracing::level_filters::LevelFilter;
use tracing_subscriber::layer::SubscriberExt;
use vespid::*;
#[component]
async fn Index() -> String {
info!("Index");
html! {
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Crusto</title>
</head>
<body>
<div>
"Welcome to Crusto!"
</div>
</body>
</html>
}
}
async fn index() -> Html<String> {
vespid::axum_compat::render(async move {
html! {<Index/>}
})
.await
}
#[tokio::main]
async fn main() -> eyre::Result<()> {
color_eyre::install()?;
let registry = tracing_subscriber::registry().with(
tracing_subscriber::EnvFilter::builder()
.with_default_directive(LevelFilter::INFO.into())
.from_env_lossy(),
);
tracing::subscriber::set_global_default(
registry
.with(tracing_error::ErrorLayer::default())
.with(tracing_subscriber::fmt::layer()),
)?;
let app = axum::Router::new().route("/", get(index));
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;
info!("listening on {}", listener.local_addr()?);
axum::serve(listener, app).await?;
Ok(())
}

12
vespid/Cargo.toml Normal file
View file

@ -0,0 +1,12 @@
[package]
name = "vespid"
version = "0.1.0"
edition = "2021"
[dependencies]
html-escape = "0.2.13"
tokio.workspace = true
tokio-util = { version = "0.7.13", features = ["rt"] }
axum.workspace = true
vespid_macros.path = "macros"
typed-builder = "0.20.0"

17
vespid/macros/Cargo.toml Normal file
View file

@ -0,0 +1,17 @@
[package]
name = "vespid_macros"
version = "0.1.0"
edition = "2021"
[dependencies]
syn = { version = "2", features = ["full"] }
rstml = "0.12"
proc-macro2 = "1"
quote = "1"
proc-macro-error2 = "2"
proc-macro2-diagnostics = "0.10.1"
convert_case = "0.6.0"
html-escape = "0.2.13"
[lib]
proc-macro = true

502
vespid/macros/src/lib.rs Normal file
View file

@ -0,0 +1,502 @@
use std::collections::HashSet;
use proc_macro::TokenStream;
use proc_macro2_diagnostics::Diagnostic;
use quote::{quote, quote_spanned, ToTokens};
use rstml::{
node::{KeyedAttribute, Node, NodeAttribute, NodeElement, NodeName}, Infallible, Parser, ParserConfig
};
use syn::punctuated::Punctuated;
use syn::{parse::Parse, parse_quote, spanned::Spanned, Expr, ExprLit, FnArg, ItemStruct, Token};
#[proc_macro]
pub fn html(tokens: TokenStream) -> TokenStream {
html_inner(tokens, false)
}
#[proc_macro]
pub fn html_ide(tokens: TokenStream) -> TokenStream {
html_inner(tokens, true)
}
fn is_empty_element(name: &str) -> bool {
// https://developer.mozilla.org/en-US/docs/Glossary/Empty_element
match name {
"img" | "input" | "meta" | "link" | "hr" | "br" | "source" | "track" | "wbr" | "area"
| "base" | "col" | "embed" | "param" => true,
_ => false,
}
}
fn empty_elements_set() -> HashSet<&'static str> {
[
"area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param",
"source", "track", "wbr",
]
.into_iter()
.collect()
}
fn html_inner(tokens: TokenStream, ide_helper: bool) -> TokenStream {
let config = ParserConfig::new()
.recover_block(true)
.element_close_use_default_wildcard_ident(true)
.always_self_closed_elements(empty_elements_set());
let parser = Parser::new(config);
let (nodes, errors) = parser.parse_recoverable(tokens).split_vec();
process_nodes(ide_helper, &nodes, errors).into()
}
fn process_nodes<'n>(
ide_helper: bool,
nodes: &'n Vec<Node>,
errors: Vec<Diagnostic>,
) -> proc_macro2::TokenStream {
let WalkNodesOutput {
static_format: html_string,
values,
collected_elements: elements,
diagnostics,
} = walk_nodes(&nodes);
let docs = if ide_helper {
generate_tags_docs(elements)
} else {
vec![]
};
let errors = errors
.into_iter()
.map(|e| e.emit_as_expr_tokens())
.chain(diagnostics);
quote! {
{
// Make sure that "compile_error!(..);" can be used in this context.
#(#errors;)*
// Make sure that "enum x{};" and "let _x = crate::element;" can be used in this context
#(#docs;)*
format!(#html_string, #(vespid::FormatRender::new(#values)),*)
}
}
}
fn generate_tags_docs(elements: Vec<&NodeName>) -> Vec<proc_macro2::TokenStream> {
// Mark some of elements as type,
// and other as elements as fn in crate::docs,
// to give an example how to link tag with docs.
let elements_as_type: HashSet<&'static str> =
vec!["html", "head", "meta", "link", "body", "div"]
.into_iter()
.collect();
elements
.into_iter()
.map(|e| {
if elements_as_type.contains(&*e.to_string()) {
let element = quote_spanned!(e.span() => enum);
quote!({#element X{}})
} else {
// let _ = crate::docs::element;
let element = quote_spanned!(e.span() => element);
quote!(let _ = crate::docs::#element)
}
})
.collect()
}
#[derive(Default)]
struct WalkNodesOutput<'a> {
static_format: String,
// Use proc_macro2::TokenStream instead of syn::Expr
// to provide more errors to the end user.
values: Vec<proc_macro2::TokenStream>,
// Additional diagnostic messages.
diagnostics: Vec<proc_macro2::TokenStream>,
// Collect elements to provide semantic highlight based on element tag.
// No differences between open tag and closed tag.
// Also multiple tags with same name can be present,
// because we need to mark each of them.
collected_elements: Vec<&'a NodeName>,
}
impl<'a> WalkNodesOutput<'a> {
fn extend(&mut self, other: WalkNodesOutput<'a>) {
self.static_format.push_str(&other.static_format);
self.values.extend(other.values);
self.diagnostics.extend(other.diagnostics);
self.collected_elements.extend(other.collected_elements);
}
}
fn walk_nodes<'a>(nodes: &'a Vec<Node>) -> WalkNodesOutput<'a> {
let mut out = WalkNodesOutput::default();
for node in nodes {
match node {
Node::Doctype(doctype) => {
let value = &doctype.value.to_token_stream_string();
out.static_format.push_str(&format!("<!DOCTYPE {}>", value));
}
Node::Element(element) => {
let name = element.name().to_string();
if !is_component_tag_name(&name) {
match element.name() {
NodeName::Block(block) => {
out.static_format.push_str("<{}");
out.values.push(block.to_token_stream());
}
_ => {
out.static_format.push_str(&format!("<{}", name));
out.collected_elements.push(&element.open_tag.name);
if let Some(e) = &element.close_tag {
out.collected_elements.push(&e.name)
}
}
}
// attributes
for attribute in element.attributes() {
match attribute {
NodeAttribute::Block(block) => {
// If the nodes parent is an attribute we prefix with whitespace
out.static_format.push(' ');
out.static_format.push_str("{}");
out.values.push(block.to_token_stream());
}
NodeAttribute::Attribute(attribute) => {
let (static_format, value) = walk_attribute(attribute);
out.static_format.push_str(&static_format);
if let Some(value) = value {
out.values.push(value);
}
}
}
}
// Ignore childs of special Empty elements
if is_empty_element(element.open_tag.name.to_string().as_str()) {
out.static_format.push_str(" />");
if !element.children.is_empty() {
let warning = proc_macro2_diagnostics::Diagnostic::spanned(
element.open_tag.name.span(),
proc_macro2_diagnostics::Level::Warning,
"Element is processed as empty, and cannot have any child",
);
out.diagnostics.push(warning.emit_as_expr_tokens())
}
continue;
}
out.static_format.push('>');
// children
let other_output = walk_nodes(&element.children);
out.extend(other_output);
match element.name() {
NodeName::Block(block) => {
out.static_format.push_str("</{}>");
out.values.push(block.to_token_stream());
}
_ => {
out.static_format.push_str(&format!("</{}>", name));
}
}
} else {
// custom elements
out.static_format.push_str("{}");
out.values
.push(CustomElement::new(element).to_token_stream());
}
}
Node::Text(text) => {
out.static_format.push_str(&text.value_string());
}
Node::RawText(text) => {
out.static_format.push_str(&text.to_string_best());
}
Node::Fragment(fragment) => {
let other_output = walk_nodes(&fragment.children);
out.extend(other_output)
}
Node::Comment(comment) => {
out.static_format.push_str("<!-- {} -->");
out.values.push(comment.value.to_token_stream());
}
Node::Block(block) => {
let block = block.try_block().unwrap();
let stmts = &block.stmts;
out.static_format.push_str("{}");
out.values.push(quote!(#(#stmts)*));
}
Node::Custom(_) => {}
}
}
out
}
fn walk_attribute(attribute: &KeyedAttribute) -> (String, Option<proc_macro2::TokenStream>) {
let mut static_format = String::new();
let mut format_value = None;
let key = match attribute.key.to_string().as_str() {
"as_" => "as".to_string(),
_ => attribute.key.to_string(),
};
static_format.push_str(&format!(" {}", key));
match attribute.value() {
Some(Expr::Lit(ExprLit {
lit: syn::Lit::Str(value),
..
})) => {
static_format.push_str(&format!(
r#"="{}""#,
html_escape::encode_unquoted_attribute(&value.value())
));
}
Some(Expr::Lit(ExprLit {
lit: syn::Lit::Bool(value),
..
})) => {
static_format.push_str(&format!(r#"="{}""#, value.value()));
}
Some(Expr::Lit(ExprLit {
lit: syn::Lit::Int(value),
..
})) => {
static_format.push_str(&format!(r#"="{}""#, value.token()));
}
Some(Expr::Lit(ExprLit {
lit: syn::Lit::Float(value),
..
})) => {
static_format.push_str(&format!(r#"="{}""#, value.token()));
}
Some(value) => {
static_format.push_str(r#"="{}""#);
format_value = Some(
quote! {{
// (#value).escape_attribute()
::vespid::EscapeAttribute::escape_attribute(&#value)
}}
.into_token_stream(),
);
}
None => {}
}
(static_format, format_value)
}
fn is_component_tag_name(name: &str) -> bool {
name.starts_with(|c: char| c.is_ascii_uppercase())
}
struct CustomElement<'e> {
e: &'e NodeElement<Infallible>,
}
impl<'e> CustomElement<'e> {
fn new(e: &'e NodeElement<Infallible>) -> Self {
CustomElement { e }
}
}
impl<'e> ToTokens for CustomElement<'_> {
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
let name = self.e.name();
let mut chain = vec![quote! {
::vespid::support::props_builder(&#name)
}];
let children = &self.e.children;
if !children.is_empty() {
let c = process_nodes(false, children, vec![]);
chain.push(quote! { .children(#c) });
}
chain.push({
self.e
.attributes()
.iter()
.map(|a| match a {
NodeAttribute::Block(block) => {
quote! {
.push_attr(
#[allow(unused_braces)]
#block
)
}
}
NodeAttribute::Attribute(attribute) => {
let key = &attribute.key;
let value = attribute.value().unwrap();
quote! { .#key(#value) }
}
})
.collect::<proc_macro2::TokenStream>()
});
chain.push(quote! { .build() });
tokens.extend(quote! {
#name(#(#chain)*).await
});
}
}
fn props(input: proc_macro2::TokenStream) -> proc_macro2::TokenStream {
let props = syn::parse2::<PropsStruct>(input).unwrap();
quote! { #props }.to_token_stream()
}
struct PropsStruct {
name: syn::Ident,
item: ItemStruct,
}
impl Parse for PropsStruct {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let item = input.parse::<ItemStruct>()?;
let name = item.ident.clone();
Ok(PropsStruct { name, item })
}
}
impl ToTokens for PropsStruct {
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
let name = &self.name;
let item = &self.item;
let builder_name =
syn::Ident::new(&format!("{}Builder", name), proc_macro2::Span::call_site());
tokens.extend(quote! {
#[derive(::vespid::typed_builder::TypedBuilder)]
#[builder(doc, crate_module_path=::vespid::typed_builder)]
#item
impl ::vespid::support::Props for #name {
type Builder = #builder_name;
fn builder() -> Self::Builder {
#name::builder()
}
}
});
let has_attributes = item
.fields
.iter()
.any(|field| field.ident.as_ref().unwrap().to_string() == "attributes");
if has_attributes {
tokens.extend(quote! {
impl #builder_name {
pub fn push_attr<A: std::fmt::Display>(mut self, attr: A) -> Self {
self.props.attributes.push_str(&format!("{} ", attr));
self
}
}
});
}
}
}
#[proc_macro_attribute]
pub fn component(_attr: TokenStream, input: TokenStream) -> TokenStream {
let comp = syn::parse_macro_input!(input as ComponentFn);
quote! { #comp }.to_token_stream().into()
}
struct ComponentFn {
item: syn::ItemFn,
}
impl Parse for ComponentFn {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let item = input.parse::<syn::ItemFn>()?;
Ok(ComponentFn { item })
}
}
impl ToTokens for ComponentFn {
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
let item = &self.item;
let name = &item.sig.ident;
let (defs, args) = match item.sig.inputs.len() {
0 => {
// generate empty props
let props_name =
syn::Ident::new(&format!("{}Props", name), proc_macro2::Span::call_site());
(
props(quote! {
pub struct #props_name{}
}),
quote! { _props: #props_name },
)
}
// match if there is a single arg of type #nameProps
1 if matches!(item.sig.inputs.first().unwrap(), syn::FnArg::Typed(arg) if matches!(arg.ty.as_ref(), syn::Type::Path(p) if p.path.segments.last().unwrap().ident.to_string() == format!("{}Props", name))) =>
{
let props = item.sig.inputs.first().unwrap();
(quote! {}, props.to_token_stream())
}
_ => {
let field_defs = &item
.sig
.inputs
.clone()
.into_iter()
.map(|i| match i {
FnArg::Receiver(_) => {
panic!("receiver arguments unsupported");
}
FnArg::Typed(mut t) => {
if t.attrs.is_empty() {
t.attrs.push(parse_quote! { #[builder(setter(into))] });
}
t
}
})
.collect::<Punctuated<_, Token![,]>>();
let field_names = item
.sig
.inputs
.iter()
.map(|i| match i {
FnArg::Receiver(_) => {
panic!("receiver arguments unsupported");
}
FnArg::Typed(t) => &t.pat,
})
.collect::<Punctuated<_, Token![,]>>();
let props_name =
syn::Ident::new(&format!("{}Props", name), proc_macro2::Span::call_site());
(
props(quote! {
pub struct #props_name {
#field_defs
}
}),
quote! { #props_name { #field_names }: #props_name },
)
}
};
let body = &item.block;
let output = &item.sig.output;
let vis = &item.vis;
tokens.extend(quote! {
#defs
#[allow(non_snake_case)]
#vis async fn #name(#args) #output {
#body
}
});
}
}

26
vespid/src/axum_compat.rs Normal file
View file

@ -0,0 +1,26 @@
use axum::response::Html;
use std::{future::Future, sync::OnceLock, thread::available_parallelism};
use tokio_util::task::LocalPoolHandle;
use crate::context;
fn get_rendering_pool() -> LocalPoolHandle {
static LOCAL_POOL: OnceLock<LocalPoolHandle> = OnceLock::new();
LOCAL_POOL
.get_or_init(|| LocalPoolHandle::new(available_parallelism().map(Into::into).unwrap_or(1)))
.clone()
}
pub async fn render<F, O>(f: F) -> Html<O>
where
F: Future<Output = O> + Send + 'static,
O: Send + 'static,
{
get_rendering_pool()
.spawn_pinned(move || async {
let h = context::spawn_local(f).await.unwrap();
Html(h)
})
.await
.unwrap()
}

52
vespid/src/context.rs Normal file
View file

@ -0,0 +1,52 @@
use std::{
any::{Any, TypeId},
cell::RefCell,
collections::HashMap,
future::Future,
};
tokio::task_local! {
pub(crate) static CONTEXT: RefCell<HashMap<TypeId, Box<dyn Any>>>;
}
pub fn spawn_local<F: Future<Output = O> + 'static, O: 'static>(
fut: F,
) -> tokio::task::JoinHandle<O> {
tokio::task::spawn_local(async move { CONTEXT.scope(RefCell::new(HashMap::new()), fut).await })
}
pub fn provide_context<T: 'static>(value: T) {
let _ = CONTEXT.with(|context| {
context
.borrow_mut()
.insert(TypeId::of::<T>(), Box::new(value));
});
}
pub fn use_context<T>() -> Option<T>
where
T: Clone + 'static,
{
CONTEXT.with(|context| {
context
.borrow()
.get(&TypeId::of::<T>())
.map(|any| {
any.downcast_ref::<T>().unwrap_or_else(|| {
panic!(
"Context type mismatch for {T}",
T = std::any::type_name::<T>()
)
})
})
.cloned()
})
}
pub fn expect_context<T>() -> T
where
T: Clone + 'static,
{
use_context::<T>()
.unwrap_or_else(|| panic!("Context not found for {T}", T = std::any::type_name::<T>()))
}

View file

@ -0,0 +1,47 @@
use std::borrow::Cow;
use html_escape::encode_unquoted_attribute;
pub trait EscapeAttribute {
fn escape_attribute(&self) -> Cow<str>;
}
impl EscapeAttribute for &str {
fn escape_attribute(&self) -> Cow<str> {
encode_unquoted_attribute(self)
}
}
impl EscapeAttribute for str {
fn escape_attribute(&self) -> Cow<str> {
encode_unquoted_attribute(self)
}
}
impl EscapeAttribute for String {
fn escape_attribute(&self) -> Cow<str> {
encode_unquoted_attribute(self)
}
}
impl EscapeAttribute for &String {
fn escape_attribute(&self) -> Cow<str> {
encode_unquoted_attribute(self)
}
}
macro_rules! impl_escape_attribute_literal {
($($t:ty),*) => {
$(
impl EscapeAttribute for $t {
fn escape_attribute(&self) -> Cow<str> {
Cow::Owned(self.to_string())
}
}
)*
};
}
impl_escape_attribute_literal!(
u8, u16, u32, u64, u128, usize, i8, i16, i32, i64, i128, isize, f32, f64, bool
);

24
vespid/src/lib.rs Normal file
View file

@ -0,0 +1,24 @@
mod escape_attribute;
pub use escape_attribute::*;
#[doc(hidden)]
pub mod support;
pub use support::*;
mod render;
pub use render::Render;
mod render_adapter;
pub use render_adapter::*;
mod context;
pub use context::*;
pub mod axum_compat;
pub use vespid_macros::*;
#[doc(hidden)]
pub extern crate typed_builder;
#[doc(hidden)]
pub extern crate html_escape;

142
vespid/src/render.rs Normal file
View file

@ -0,0 +1,142 @@
use std::{
borrow::Cow,
convert::Infallible,
env::VarError,
fmt,
io::ErrorKind,
net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6},
num::{
NonZeroI128, NonZeroI16, NonZeroI32, NonZeroI64, NonZeroI8, NonZeroIsize, NonZeroU128,
NonZeroU16, NonZeroU32, NonZeroU64, NonZeroU8, NonZeroUsize,
},
rc::Rc,
sync::{
mpsc::{RecvTimeoutError, TryRecvError},
Arc,
},
};
pub trait Render {
fn render(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result;
}
macro_rules! impl_render_for_basic_types {
($($t:ty)*) => ($(
impl Render for $t {
#[inline(always)]
fn render(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(self, f)
}
}
)*)
}
impl_render_for_basic_types! {
Infallible
VarError
ErrorKind
IpAddr
SocketAddr
RecvTimeoutError
TryRecvError
bool
char
f32
f64
i8
i16
i32
i64
i128
isize
u8
u16
u32
u64
u128
usize
Ipv4Addr
Ipv6Addr
SocketAddrV4
SocketAddrV6
NonZeroI8
NonZeroI16
NonZeroI32
NonZeroI64
NonZeroI128
NonZeroIsize
NonZeroU8
NonZeroU16
NonZeroU32
NonZeroU64
NonZeroU128
NonZeroUsize
String
str
}
impl<B: ?Sized> Render for Cow<'_, B>
where
B: Render + ToOwned,
<B as ToOwned>::Owned: Render,
{
fn render(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
Cow::Borrowed(ref b) => Render::render(b, f),
Cow::Owned(ref o) => Render::render(o, f),
}
}
}
impl<T: Render> Render for Box<T> {
#[inline(always)]
fn render(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
Render::render(&**self, f)
}
}
impl<T: Render + ?Sized> Render for &T {
#[inline(always)]
fn render(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
Render::render(&**self, f)
}
}
impl<T: Render> Render for &mut T {
#[inline(always)]
fn render(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
Render::render(&**self, f)
}
}
impl<T: Render> Render for Rc<T> {
#[inline(always)]
fn render(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
Render::render(&**self, f)
}
}
impl<T: Render> Render for Arc<T> {
#[inline(always)]
fn render(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
Render::render(&**self, f)
}
}
impl<T: Render> Render for Option<T> {
#[inline(always)]
fn render(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(inner) = self {
inner.render(f)
} else {
Ok(())
}
}
}
impl Render for () {
#[inline(always)]
fn render(&self, _: &mut fmt::Formatter<'_>) -> fmt::Result {
Ok(())
}
}

View file

@ -0,0 +1,33 @@
use core::fmt;
use crate::Render;
pub struct RenderDisplay<T>(pub T);
impl<T: core::fmt::Display> Render for RenderDisplay<T> {
fn render(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
pub struct RenderDebug<T>(pub T);
impl<T: core::fmt::Debug> Render for RenderDebug<T> {
fn render(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
pub struct FormatRender<T>(pub T);
impl<T> FormatRender<T> {
pub fn new(value: T) -> Self {
Self(value)
}
}
impl<T: Render> fmt::Display for FormatRender<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.render(f)
}
}

21
vespid/src/support.rs Normal file
View file

@ -0,0 +1,21 @@
pub trait Component<P> {}
impl<P, F, R> Component<P> for F
where
F: FnOnce(P) -> R,
P: Props,
{
}
pub fn props_builder<C, P>(_: C) -> P::Builder
where
C: Component<P>,
P: Props,
{
P::builder()
}
pub trait Props {
type Builder;
fn builder() -> Self::Builder;
}