From 793c191619d22829aefff1c77a2e6c14d4ebc8a0 Mon Sep 17 00:00:00 2001 From: Danik Vitek Date: Sat, 26 Aug 2023 18:43:51 +0300 Subject: [PATCH] feat: `Oco` (Owned Clones Once) smart pointer (#1480) --- .gitignore | 2 + leptos/src/additional_attributes.rs | 11 +- leptos/src/text_prop.rs | 32 +- leptos_dom/src/components.rs | 20 +- leptos_dom/src/components/dyn_child.rs | 6 +- leptos_dom/src/components/each.rs | 10 +- leptos_dom/src/events.rs | 11 +- leptos_dom/src/events/typed.rs | 23 +- leptos_dom/src/html.rs | 55 +- leptos_dom/src/lib.rs | 20 +- .../src/macro_helpers/into_attribute.rs | 50 +- leptos_dom/src/macro_helpers/into_class.rs | 4 +- leptos_dom/src/macro_helpers/into_property.rs | 4 +- leptos_dom/src/macro_helpers/into_style.rs | 68 +- leptos_dom/src/math.rs | 4 +- leptos_dom/src/ssr.rs | 42 +- leptos_dom/src/ssr_in_order.rs | 21 +- leptos_dom/src/svg.rs | 4 +- leptos_reactive/src/lib.rs | 2 + leptos_reactive/src/oco.rs | 681 ++++++++++++++++++ leptos_reactive/src/suspense.rs | 12 +- meta/src/lib.rs | 5 +- meta/src/link.rs | 37 +- meta/src/script.rs | 27 +- meta/src/style.rs | 13 +- meta/src/title.rs | 6 +- router/src/components/link.rs | 60 +- router/src/components/route.rs | 3 +- router/src/components/routes.rs | 3 +- router/src/hooks.rs | 8 +- router/src/matching/expand_optionals.rs | 4 +- 31 files changed, 1020 insertions(+), 228 deletions(-) create mode 100644 leptos_reactive/src/oco.rs diff --git a/.gitignore b/.gitignore index a85086056..fe4ef4e5f 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,5 @@ Cargo.lock .idea .direnv .envrc + +.vscode \ No newline at end of file diff --git a/leptos/src/additional_attributes.rs b/leptos/src/additional_attributes.rs index 4daf27bb0..4be7abe63 100644 --- a/leptos/src/additional_attributes.rs +++ b/leptos/src/additional_attributes.rs @@ -1,10 +1,11 @@ use crate::TextProp; +use std::rc::Rc; /// A collection of additional HTML attributes to be applied to an element, /// each of which may or may not be reactive. -#[derive(Default, Clone)] +#[derive(Clone)] #[repr(transparent)] -pub struct AdditionalAttributes(pub(crate) Vec<(String, TextProp)>); +pub struct AdditionalAttributes(pub(crate) Rc<[(String, TextProp)]>); impl From for AdditionalAttributes where @@ -22,6 +23,12 @@ where } } +impl Default for AdditionalAttributes { + fn default() -> Self { + Self([].into_iter().collect()) + } +} + /// Iterator over additional HTML attributes. #[repr(transparent)] pub struct AdditionalAttributesIter<'a>( diff --git a/leptos/src/text_prop.rs b/leptos/src/text_prop.rs index 8f67b7220..d71531dc1 100644 --- a/leptos/src/text_prop.rs +++ b/leptos/src/text_prop.rs @@ -1,14 +1,15 @@ +use leptos_reactive::Oco; use std::{fmt::Debug, rc::Rc}; /// Describes a value that is either a static or a reactive string, i.e., /// a [`String`], a [`&str`], or a reactive `Fn() -> String`. #[derive(Clone)] -pub struct TextProp(Rc String>); +pub struct TextProp(Rc Oco<'static, str>>); impl TextProp { /// Accesses the current value of the property. #[inline(always)] - pub fn get(&self) -> String { + pub fn get(&self) -> Oco<'static, str> { (self.0)() } } @@ -21,23 +22,38 @@ impl Debug for TextProp { impl From for TextProp { fn from(s: String) -> Self { + let s: Oco<'_, str> = Oco::Counted(Rc::from(s)); TextProp(Rc::new(move || s.clone())) } } -impl From<&str> for TextProp { - fn from(s: &str) -> Self { - let s = s.to_string(); +impl From<&'static str> for TextProp { + fn from(s: &'static str) -> Self { + let s: Oco<'_, str> = s.into(); TextProp(Rc::new(move || s.clone())) } } -impl From for TextProp +impl From> for TextProp { + fn from(s: Rc) -> Self { + let s: Oco<'_, str> = s.into(); + TextProp(Rc::new(move || s.clone())) + } +} + +impl From> for TextProp { + fn from(s: Oco<'static, str>) -> Self { + TextProp(Rc::new(move || s.clone())) + } +} + +impl From for TextProp where - F: Fn() -> String + 'static, + F: Fn() -> S + 'static, + S: Into>, { #[inline(always)] fn from(s: F) -> Self { - TextProp(Rc::new(s)) + TextProp(Rc::new(move || s().into())) } } diff --git a/leptos_dom/src/components.rs b/leptos_dom/src/components.rs index 9eebd0748..2ef9dd0e4 100644 --- a/leptos_dom/src/components.rs +++ b/leptos_dom/src/components.rs @@ -14,12 +14,12 @@ pub use dyn_child::*; pub use each::*; pub use errors::*; pub use fragment::*; -use leptos_reactive::untrack_with_diagnostics; +use leptos_reactive::{untrack_with_diagnostics, Oco}; #[cfg(all(target_arch = "wasm32", feature = "web"))] use once_cell::unsync::OnceCell; +use std::fmt; #[cfg(all(target_arch = "wasm32", feature = "web"))] use std::rc::Rc; -use std::{borrow::Cow, fmt}; pub use unit::*; #[cfg(all(target_arch = "wasm32", feature = "web"))] use wasm_bindgen::JsCast; @@ -55,7 +55,7 @@ pub struct ComponentRepr { #[cfg(all(target_arch = "wasm32", feature = "web"))] mounted: Rc>, #[cfg(any(debug_assertions, feature = "ssr"))] - pub(crate) name: Cow<'static, str>, + pub(crate) name: Oco<'static, str>, #[cfg(debug_assertions)] _opening: Comment, /// The children of the component. @@ -163,24 +163,24 @@ impl IntoView for ComponentRepr { impl ComponentRepr { /// Creates a new [`Component`]. #[inline(always)] - pub fn new(name: impl Into>) -> Self { + pub fn new(name: impl Into>) -> Self { Self::new_with_id_concrete(name.into(), HydrationCtx::id()) } /// Creates a new [`Component`] with the given hydration ID. #[inline(always)] pub fn new_with_id( - name: impl Into>, + name: impl Into>, id: HydrationKey, ) -> Self { Self::new_with_id_concrete(name.into(), id) } - fn new_with_id_concrete(name: Cow<'static, str>, id: HydrationKey) -> Self { + fn new_with_id_concrete(name: Oco<'static, str>, id: HydrationKey) -> Self { let markers = ( - Comment::new(Cow::Owned(format!("")), &id, true), + Comment::new(format!(""), &id, true), #[cfg(debug_assertions)] - Comment::new(Cow::Owned(format!("<{name}>")), &id, false), + Comment::new(format!("<{name}>"), &id, false), ); #[cfg(all(target_arch = "wasm32", feature = "web"))] @@ -236,7 +236,7 @@ where V: IntoView, { id: HydrationKey, - name: Cow<'static, str>, + name: Oco<'static, str>, children_fn: F, } @@ -246,7 +246,7 @@ where V: IntoView, { /// Creates a new component. - pub fn new(name: impl Into>, f: F) -> Self { + pub fn new(name: impl Into>, f: F) -> Self { Self { id: HydrationCtx::id(), name: name.into(), diff --git a/leptos_dom/src/components/dyn_child.rs b/leptos_dom/src/components/dyn_child.rs index ac291f105..8e69822fc 100644 --- a/leptos_dom/src/components/dyn_child.rs +++ b/leptos_dom/src/components/dyn_child.rs @@ -3,7 +3,7 @@ use crate::{ Comment, IntoView, View, }; use cfg_if::cfg_if; -use std::{borrow::Cow, cell::RefCell, fmt, ops::Deref, rc::Rc}; +use std::{cell::RefCell, fmt, ops::Deref, rc::Rc}; cfg_if! { if #[cfg(all(target_arch = "wasm32", feature = "web"))] { use crate::{mount_child, prepare_to_move, unmount_child, MountKind, Mountable, Text}; @@ -83,9 +83,9 @@ impl Mountable for DynChildRepr { impl DynChildRepr { fn new_with_id(id: HydrationKey) -> Self { let markers = ( - Comment::new(Cow::Borrowed(""), &id, true), + Comment::new("", &id, true), #[cfg(debug_assertions)] - Comment::new(Cow::Borrowed(""), &id, false), + Comment::new("", &id, false), ); #[cfg(all(target_arch = "wasm32", feature = "web"))] diff --git a/leptos_dom/src/components/each.rs b/leptos_dom/src/components/each.rs index e621596d5..80cec64ac 100644 --- a/leptos_dom/src/components/each.rs +++ b/leptos_dom/src/components/each.rs @@ -2,7 +2,7 @@ use crate::hydration::HydrationKey; use crate::{hydration::HydrationCtx, Comment, CoreComponent, IntoView, View}; use leptos_reactive::{as_child_of_current_owner, Disposer}; -use std::{borrow::Cow, cell::RefCell, fmt, hash::Hash, ops::Deref, rc::Rc}; +use std::{cell::RefCell, fmt, hash::Hash, ops::Deref, rc::Rc}; #[cfg(all(target_arch = "wasm32", feature = "web"))] use web::*; @@ -79,9 +79,9 @@ impl Default for EachRepr { let id = HydrationCtx::id(); let markers = ( - Comment::new(Cow::Borrowed(""), &id, true), + Comment::new("", &id, true), #[cfg(debug_assertions)] - Comment::new(Cow::Borrowed(""), &id, false), + Comment::new("", &id, false), ); #[cfg(all(target_arch = "wasm32", feature = "web"))] @@ -224,13 +224,13 @@ impl EachItem { let markers = ( if needs_closing { - Some(Comment::new(Cow::Borrowed(""), &id, true)) + Some(Comment::new("", &id, true)) } else { None }, #[cfg(debug_assertions)] if needs_closing { - Some(Comment::new(Cow::Borrowed(""), &id, false)) + Some(Comment::new("", &id, false)) } else { None }, diff --git a/leptos_dom/src/events.rs b/leptos_dom/src/events.rs index e951ebcb6..8d50a3cbf 100644 --- a/leptos_dom/src/events.rs +++ b/leptos_dom/src/events.rs @@ -1,6 +1,7 @@ pub mod typed; -use std::{borrow::Cow, cell::RefCell, collections::HashSet}; +use leptos_reactive::Oco; +use std::{cell::RefCell, collections::HashSet}; #[cfg(all(target_arch = "wasm32", feature = "web"))] use wasm_bindgen::{ convert::FromWasmAbi, intern, prelude::Closure, JsCast, JsValue, @@ -8,7 +9,7 @@ use wasm_bindgen::{ }; thread_local! { - pub(crate) static GLOBAL_EVENTS: RefCell>> = RefCell::new(HashSet::new()); + pub(crate) static GLOBAL_EVENTS: RefCell>> = RefCell::new(HashSet::new()); } // Used in template macro @@ -47,8 +48,8 @@ pub fn add_event_helper( #[cfg(all(target_arch = "wasm32", feature = "web"))] pub fn add_event_listener( target: &web_sys::Element, - key: Cow<'static, str>, - event_name: Cow<'static, str>, + key: Oco<'static, str>, + event_name: Oco<'static, str>, #[cfg(debug_assertions)] mut cb: Box, #[cfg(not(debug_assertions))] cb: Box, options: &Option, @@ -115,7 +116,7 @@ pub(crate) fn add_event_listener_undelegated( #[cfg(all(target_arch = "wasm32", feature = "web"))] pub(crate) fn add_delegated_event_listener( key: &str, - event_name: Cow<'static, str>, + event_name: Oco<'static, str>, options: &Option, ) { GLOBAL_EVENTS.with(|global_events| { diff --git a/leptos_dom/src/events/typed.rs b/leptos_dom/src/events/typed.rs index 71feef646..0406f8a56 100644 --- a/leptos_dom/src/events/typed.rs +++ b/leptos_dom/src/events/typed.rs @@ -1,6 +1,7 @@ //! Types for all DOM events. -use std::{borrow::Cow, marker::PhantomData}; +use leptos_reactive::Oco; +use std::marker::PhantomData; use wasm_bindgen::convert::FromWasmAbi; /// A trait for converting types into [web_sys events](web_sys). @@ -16,10 +17,10 @@ pub trait EventDescriptor: Clone { const BUBBLES: bool; /// The name of the event, such as `click` or `mouseover`. - fn name(&self) -> Cow<'static, str>; + fn name(&self) -> Oco<'static, str>; /// The key used for event delegation. - fn event_delegation_key(&self) -> Cow<'static, str>; + fn event_delegation_key(&self) -> Oco<'static, str>; /// Return the options for this type. This is only used when you create a [`Custom`] event /// handler. @@ -39,12 +40,12 @@ impl EventDescriptor for undelegated { type EventType = Ev::EventType; #[inline(always)] - fn name(&self) -> Cow<'static, str> { + fn name(&self) -> Oco<'static, str> { self.0.name() } #[inline(always)] - fn event_delegation_key(&self) -> Cow<'static, str> { + fn event_delegation_key(&self) -> Oco<'static, str> { self.0.event_delegation_key() } @@ -54,7 +55,7 @@ impl EventDescriptor for undelegated { /// A custom event. #[derive(Debug)] pub struct Custom { - name: Cow<'static, str>, + name: Oco<'static, str>, options: Option, _event_type: PhantomData, } @@ -72,11 +73,11 @@ impl Clone for Custom { impl EventDescriptor for Custom { type EventType = E; - fn name(&self) -> Cow<'static, str> { + fn name(&self) -> Oco<'static, str> { self.name.clone() } - fn event_delegation_key(&self) -> Cow<'static, str> { + fn event_delegation_key(&self) -> Oco<'static, str> { format!("$$${}", self.name).into() } @@ -92,7 +93,7 @@ impl Custom { /// Creates a custom event type that can be used within /// [`HtmlElement::on`](crate::HtmlElement::on), for events /// which are not covered in the [`ev`](crate::ev) module. - pub fn new(name: impl Into>) -> Self { + pub fn new(name: impl Into>) -> Self { Self { name: name.into(), options: None, @@ -299,12 +300,12 @@ macro_rules! generate_event_types { type EventType = web_sys::$web_event; #[inline(always)] - fn name(&self) -> Cow<'static, str> { + fn name(&self) -> Oco<'static, str> { stringify!([< $($event)+ >]).into() } #[inline(always)] - fn event_delegation_key(&self) -> Cow<'static, str> { + fn event_delegation_key(&self) -> Oco<'static, str> { concat!("$$$", stringify!([< $($event)+ >])).into() } diff --git a/leptos_dom/src/html.rs b/leptos_dom/src/html.rs index 00d3c519f..a3105c8e7 100644 --- a/leptos_dom/src/html.rs +++ b/leptos_dom/src/html.rs @@ -66,12 +66,13 @@ use crate::{ macro_helpers::{IntoAttribute, IntoClass, IntoProperty, IntoStyle}, Element, Fragment, IntoView, NodeRef, Text, View, }; -use std::{borrow::Cow, fmt}; +use leptos_reactive::Oco; +use std::fmt; /// Trait which allows creating an element tag. pub trait ElementDescriptor: ElementDescriptorBounds { /// The name of the element, i.e., `div`, `p`, `custom-element`. - fn name(&self) -> Cow<'static, str>; + fn name(&self) -> Oco<'static, str>; /// Determines if the tag is void, i.e., `` and `
`. #[inline(always)] @@ -126,7 +127,7 @@ where /// Represents potentially any element. #[derive(Clone, Debug)] pub struct AnyElement { - pub(crate) name: Cow<'static, str>, + pub(crate) name: Oco<'static, str>, #[cfg(all(target_arch = "wasm32", feature = "web"))] pub(crate) element: web_sys::HtmlElement, pub(crate) is_void: bool, @@ -159,7 +160,7 @@ impl std::convert::AsRef for AnyElement { } impl ElementDescriptor for AnyElement { - fn name(&self) -> Cow<'static, str> { + fn name(&self) -> Oco<'static, str> { self.name.clone() } @@ -178,7 +179,7 @@ impl ElementDescriptor for AnyElement { /// Represents a custom HTML element, such as ``. #[derive(Clone, Debug)] pub struct Custom { - name: Cow<'static, str>, + name: Oco<'static, str>, #[cfg(all(target_arch = "wasm32", feature = "web"))] element: web_sys::HtmlElement, #[cfg(not(all(target_arch = "wasm32", feature = "web")))] @@ -187,7 +188,7 @@ pub struct Custom { impl Custom { /// Creates a new custom element with the given tag name. - pub fn new(name: impl Into>) -> Self { + pub fn new(name: impl Into>) -> Self { let name = name.into(); let id = HydrationCtx::id(); @@ -266,7 +267,7 @@ impl std::convert::AsRef for Custom { } impl ElementDescriptor for Custom { - fn name(&self) -> Cow<'static, str> { + fn name(&self) -> Oco<'static, str> { self.name.clone() } @@ -294,12 +295,12 @@ cfg_if! { #[derive(educe::Educe, Clone)] #[educe(Debug)] pub struct HtmlElement { - pub(crate) element: El, - pub(crate) attrs: SmallVec<[(Cow<'static, str>, Cow<'static, str>); 4]>, - #[educe(Debug(ignore))] - pub(crate) children: ElementChildren, - #[cfg(debug_assertions)] - pub(crate) view_marker: Option + pub(crate) element: El, + pub(crate) attrs: SmallVec<[(Oco<'static, str>, Oco<'static, str>); 4]>, + #[educe(Debug(ignore))] + pub(crate) children: ElementChildren, + #[cfg(debug_assertions)] + pub(crate) view_marker: Option } #[derive(Clone, educe::Educe, PartialEq, Eq)] @@ -308,14 +309,14 @@ cfg_if! { #[educe(Default)] Empty, Children(Vec), - InnerHtml(Cow<'static, str>), + InnerHtml(Oco<'static, str>), Chunks(Vec) } #[doc(hidden)] #[derive(Clone)] pub enum StringOrView { - String(Cow<'static, str>), + String(Oco<'static, str>), View(std::rc::Rc View>) } @@ -445,7 +446,7 @@ impl HtmlElement { /// Adds an `id` to the element. #[track_caller] #[inline(always)] - pub fn id(self, id: impl Into>) -> Self { + pub fn id(self, id: impl Into>) -> Self { let id = id.into(); #[cfg(all(target_arch = "wasm32", feature = "web"))] @@ -575,7 +576,7 @@ impl HtmlElement { #[cfg_attr(all(target_arch = "wasm32", feature = "web"), inline(always))] pub fn attr( self, - name: impl Into>, + name: impl Into>, attr: impl IntoAttribute, ) -> Self { let name = name.into(); @@ -634,7 +635,7 @@ impl HtmlElement { #[track_caller] pub fn class( self, - name: impl Into>, + name: impl Into>, class: impl IntoClass, ) -> Self { let name = name.into(); @@ -686,7 +687,7 @@ impl HtmlElement { /// Adds a list of classes separated by ASCII whitespace to an element. #[track_caller] #[inline(always)] - pub fn classes(self, classes: impl Into>) -> Self { + pub fn classes(self, classes: impl Into>) -> Self { self.classes_inner(&classes.into()) } @@ -698,7 +699,7 @@ impl HtmlElement { ) -> Self where I: IntoIterator, - C: Into>, + C: Into>, { #[cfg(all(target_arch = "wasm32", feature = "web"))] { @@ -708,12 +709,12 @@ impl HtmlElement { leptos_reactive::create_effect( move |prev_classes: Option< - SmallVec<[Cow<'static, str>; 4]>, + SmallVec<[Oco<'static, str>; 4]>, >| { let classes = classes_signal() .into_iter() .map(Into::into) - .collect::; 4]>>( + .collect::; 4]>>( ); let new_classes = classes @@ -797,7 +798,7 @@ impl HtmlElement { #[track_caller] pub fn style( self, - name: impl Into>, + name: impl Into>, style: impl IntoStyle, ) -> Self { let name = name.into(); @@ -856,7 +857,7 @@ impl HtmlElement { #[track_caller] pub fn prop( self, - name: impl Into>, + name: impl Into>, value: impl IntoProperty, ) -> Self { #[cfg(all(target_arch = "wasm32", feature = "web"))] @@ -1016,7 +1017,7 @@ impl HtmlElement { /// sanitize the input to avoid a cross-site scripting (XSS) /// vulnerability. #[inline(always)] - pub fn inner_html(self, html: impl Into>) -> Self { + pub fn inner_html(self, html: impl Into>) -> Self { let html = html.into(); #[cfg(all(target_arch = "wasm32", feature = "web"))] @@ -1103,7 +1104,7 @@ pub fn custom(el: El) -> HtmlElement { /// Creates a text node. #[inline(always)] -pub fn text(text: impl Into>) -> Text { +pub fn text(text: impl Into>) -> Text { Text::new(text.into()) } @@ -1190,7 +1191,7 @@ macro_rules! generate_html_tags { impl ElementDescriptor for [<$tag:camel $($trailing_)?>] { #[inline(always)] - fn name(&self) -> Cow<'static, str> { + fn name(&self) -> Oco<'static, str> { stringify!($tag).into() } diff --git a/leptos_dom/src/lib.rs b/leptos_dom/src/lib.rs index c3732582e..7f3c20ead 100644 --- a/leptos_dom/src/lib.rs +++ b/leptos_dom/src/lib.rs @@ -34,6 +34,7 @@ pub use events::{typed as ev, typed::EventHandler}; pub use html::HtmlElement; use html::{AnyElement, ElementDescriptor}; pub use hydration::{HydrationCtx, HydrationKey}; +use leptos_reactive::Oco; #[cfg(not(feature = "nightly"))] use leptos_reactive::{ MaybeProp, MaybeSignal, Memo, ReadSignal, RwSignal, Signal, SignalGet, @@ -240,7 +241,7 @@ cfg_if! { pub struct Element { #[doc(hidden)] #[cfg(debug_assertions)] - pub name: Cow<'static, str>, + pub name: Oco<'static, str>, #[doc(hidden)] pub element: web_sys::HtmlElement, #[cfg(debug_assertions)] @@ -261,9 +262,9 @@ cfg_if! { /// HTML element. #[derive(Clone, PartialEq, Eq)] pub struct Element { - name: Cow<'static, str>, + name: Oco<'static, str>, is_void: bool, - attrs: SmallVec<[(Cow<'static, str>, Cow<'static, str>); 4]>, + attrs: SmallVec<[(Oco<'static, str>, Oco<'static, str>); 4]>, children: ElementChildren, id: HydrationKey, #[cfg(debug_assertions)] @@ -396,13 +397,13 @@ impl Element { struct Comment { #[cfg(all(target_arch = "wasm32", feature = "web"))] node: web_sys::Node, - content: Cow<'static, str>, + content: Oco<'static, str>, } impl Comment { #[inline] fn new( - content: impl Into>, + content: impl Into>, id: &HydrationKey, closing: bool, ) -> Self { @@ -410,7 +411,7 @@ impl Comment { } fn new_inner( - content: Cow<'static, str>, + content: Oco<'static, str>, id: &HydrationKey, closing: bool, ) -> Self { @@ -466,12 +467,13 @@ pub struct Text { #[cfg(all(target_arch = "wasm32", feature = "web"))] node: web_sys::Node, /// The current contents of the text node. - pub content: Cow<'static, str>, + pub content: Oco<'static, str>, } impl fmt::Debug for Text { + #[inline] fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "\"{}\"", self.content) + fmt::Debug::fmt(&self.content, f) } } @@ -484,7 +486,7 @@ impl IntoView for Text { impl Text { /// Creates a new [`Text`]. - pub fn new(content: Cow<'static, str>) -> Self { + pub fn new(content: Oco<'static, str>) -> Self { Self { #[cfg(all(target_arch = "wasm32", feature = "web"))] node: crate::document() diff --git a/leptos_dom/src/macro_helpers/into_attribute.rs b/leptos_dom/src/macro_helpers/into_attribute.rs index 7a4e0ab9a..25bf36e5a 100644 --- a/leptos_dom/src/macro_helpers/into_attribute.rs +++ b/leptos_dom/src/macro_helpers/into_attribute.rs @@ -1,3 +1,4 @@ +use leptos_reactive::Oco; #[cfg(not(feature = "nightly"))] use leptos_reactive::{ MaybeProp, MaybeSignal, Memo, ReadSignal, RwSignal, Signal, SignalGet, @@ -14,11 +15,11 @@ use wasm_bindgen::UnwrapThrowExt; #[derive(Clone)] pub enum Attribute { /// A plain string value. - String(Cow<'static, str>), + String(Oco<'static, str>), /// A (presumably reactive) function, which will be run inside an effect to do targeted updates to the attribute. Fn(Rc Attribute>), /// An optional string value, which sets the attribute to the value if `Some` and removes the attribute if `None`. - Option(Option>), + Option(Option>), /// A boolean attribute, which sets the attribute if `true` and removes the attribute if `false`. Bool(bool), } @@ -29,7 +30,7 @@ impl Attribute { pub fn as_value_string( &self, attr_name: &'static str, - ) -> Cow<'static, str> { + ) -> Oco<'static, str> { match self { Attribute::String(value) => { format!("{attr_name}=\"{value}\"").into() @@ -46,14 +47,14 @@ impl Attribute { .map(|value| format!("{attr_name}=\"{value}\"").into()) .unwrap_or_default(), Attribute::Bool(include) => { - Cow::Borrowed(if *include { attr_name } else { "" }) + Oco::Borrowed(if *include { attr_name } else { "" }) } } } /// Converts the attribute to its HTML value at that moment, not including /// the attribute name, so it can be rendered on the server. - pub fn as_nameless_value_string(&self) -> Option> { + pub fn as_nameless_value_string(&self) -> Option> { match self { Attribute::String(value) => Some(value.clone()), Attribute::Fn(f) => { @@ -148,7 +149,7 @@ impl IntoAttribute for Option { impl IntoAttribute for String { #[inline(always)] fn into_attribute(self) -> Attribute { - Attribute::String(Cow::Owned(self)) + Attribute::String(Oco::Owned(self)) } impl_into_attr_boxed! {} @@ -157,13 +158,22 @@ impl IntoAttribute for String { impl IntoAttribute for &'static str { #[inline(always)] fn into_attribute(self) -> Attribute { - Attribute::String(Cow::Borrowed(self)) + Attribute::String(Oco::Borrowed(self)) } impl_into_attr_boxed! {} } -impl IntoAttribute for Cow<'static, str> { +impl IntoAttribute for Rc { + #[inline(always)] + fn into_attribute(self) -> Attribute { + Attribute::String(Oco::Counted(self)) + } + + impl_into_attr_boxed! {} +} + +impl IntoAttribute for Oco<'static, str> { #[inline(always)] fn into_attribute(self) -> Attribute { Attribute::String(self) @@ -184,7 +194,7 @@ impl IntoAttribute for bool { impl IntoAttribute for Option { #[inline(always)] fn into_attribute(self) -> Attribute { - Attribute::Option(self.map(Cow::Owned)) + Attribute::Option(self.map(Oco::Owned)) } impl_into_attr_boxed! {} @@ -193,13 +203,31 @@ impl IntoAttribute for Option { impl IntoAttribute for Option<&'static str> { #[inline(always)] fn into_attribute(self) -> Attribute { - Attribute::Option(self.map(Cow::Borrowed)) + Attribute::Option(self.map(Oco::Borrowed)) + } + + impl_into_attr_boxed! {} +} + +impl IntoAttribute for Option> { + #[inline(always)] + fn into_attribute(self) -> Attribute { + Attribute::Option(self.map(Oco::Counted)) } impl_into_attr_boxed! {} } impl IntoAttribute for Option> { + #[inline(always)] + fn into_attribute(self) -> Attribute { + Attribute::Option(self.map(Oco::from)) + } + + impl_into_attr_boxed! {} +} + +impl IntoAttribute for Option> { #[inline(always)] fn into_attribute(self) -> Attribute { Attribute::Option(self) @@ -331,7 +359,7 @@ attr_signal_type_optional!(MaybeProp); #[inline(never)] pub fn attribute_helper( el: &web_sys::Element, - name: Cow<'static, str>, + name: Oco<'static, str>, value: Attribute, ) { use leptos_reactive::create_render_effect; diff --git a/leptos_dom/src/macro_helpers/into_class.rs b/leptos_dom/src/macro_helpers/into_class.rs index 93a69c613..7d246227c 100644 --- a/leptos_dom/src/macro_helpers/into_class.rs +++ b/leptos_dom/src/macro_helpers/into_class.rs @@ -65,14 +65,14 @@ impl Class { } #[cfg(all(target_arch = "wasm32", feature = "web"))] -use std::borrow::Cow; +use leptos_reactive::Oco; #[cfg(all(target_arch = "wasm32", feature = "web"))] #[doc(hidden)] #[inline(never)] pub fn class_helper( el: &web_sys::Element, - name: Cow<'static, str>, + name: Oco<'static, str>, value: Class, ) { use leptos_reactive::create_render_effect; diff --git a/leptos_dom/src/macro_helpers/into_property.rs b/leptos_dom/src/macro_helpers/into_property.rs index c0dce90cb..df6895441 100644 --- a/leptos_dom/src/macro_helpers/into_property.rs +++ b/leptos_dom/src/macro_helpers/into_property.rs @@ -115,13 +115,13 @@ prop_signal_type!(MaybeSignal); prop_signal_type_optional!(MaybeProp); #[cfg(all(target_arch = "wasm32", feature = "web"))] -use std::borrow::Cow; +use leptos_reactive::Oco; #[cfg(all(target_arch = "wasm32", feature = "web"))] #[inline(never)] pub(crate) fn property_helper( el: &web_sys::Element, - name: Cow<'static, str>, + name: Oco<'static, str>, value: Property, ) { use leptos_reactive::create_render_effect; diff --git a/leptos_dom/src/macro_helpers/into_style.rs b/leptos_dom/src/macro_helpers/into_style.rs index 8e572cf09..0d0b92b55 100644 --- a/leptos_dom/src/macro_helpers/into_style.rs +++ b/leptos_dom/src/macro_helpers/into_style.rs @@ -1,3 +1,4 @@ +use leptos_reactive::Oco; #[cfg(not(feature = "nightly"))] use leptos_reactive::{ MaybeProp, MaybeSignal, Memo, ReadSignal, RwSignal, Signal, SignalGet, @@ -8,9 +9,9 @@ use std::{borrow::Cow, rc::Rc}; #[derive(Clone)] pub enum Style { /// A plain string value. - Value(Cow<'static, str>), + Value(Oco<'static, str>), /// An optional string value, which sets the property to the value if `Some` and removes the property if `None`. - Option(Option>), + Option(Option>), /// A (presumably reactive) function, which will be run inside an effect to update the style. Fn(Rc Style>), } @@ -45,28 +46,70 @@ pub trait IntoStyle { impl IntoStyle for &'static str { #[inline(always)] fn into_style(self) -> Style { - Style::Value(self.into()) + Style::Value(Oco::Borrowed(self)) } } impl IntoStyle for String { + #[inline(always)] + fn into_style(self) -> Style { + Style::Value(Oco::Owned(self)) + } +} + +impl IntoStyle for Rc { + #[inline(always)] + fn into_style(self) -> Style { + Style::Value(Oco::Counted(self)) + } +} + +impl IntoStyle for Cow<'static, str> { #[inline(always)] fn into_style(self) -> Style { Style::Value(self.into()) } } +impl IntoStyle for Oco<'static, str> { + #[inline(always)] + fn into_style(self) -> Style { + Style::Value(self) + } +} + impl IntoStyle for Option<&'static str> { #[inline(always)] fn into_style(self) -> Style { - Style::Option(self.map(Cow::Borrowed)) + Style::Option(self.map(Oco::Borrowed)) } } impl IntoStyle for Option { #[inline(always)] fn into_style(self) -> Style { - Style::Option(self.map(Cow::Owned)) + Style::Option(self.map(Oco::Owned)) + } +} + +impl IntoStyle for Option> { + #[inline(always)] + fn into_style(self) -> Style { + Style::Option(self.map(Oco::Counted)) + } +} + +impl IntoStyle for Option> { + #[inline(always)] + fn into_style(self) -> Style { + Style::Option(self.map(Oco::from)) + } +} + +impl IntoStyle for Option> { + #[inline(always)] + fn into_style(self) -> Style { + Style::Option(self) } } @@ -87,7 +130,7 @@ impl Style { pub fn as_value_string( &self, style_name: &'static str, - ) -> Option> { + ) -> Option> { match self { Style::Value(value) => { Some(format!("{style_name}: {value};").into()) @@ -111,10 +154,11 @@ impl Style { #[inline(never)] pub fn style_helper( el: &web_sys::Element, - name: Cow<'static, str>, + name: Oco<'static, str>, value: Style, ) { use leptos_reactive::create_render_effect; + use std::ops::Deref; use wasm_bindgen::JsCast; let el = el.unchecked_ref::(); @@ -132,16 +176,16 @@ pub fn style_helper( _ => unreachable!(), }; if old.as_ref() != Some(&new) { - style_expression(&style_list, &name, new.as_ref(), true) + style_expression(&style_list, &name, new.as_deref(), true) } new }); } Style::Value(value) => { - style_expression(&style_list, &name, Some(&value), false) + style_expression(&style_list, &name, Some(value.deref()), false) } Style::Option(value) => { - style_expression(&style_list, &name, value.as_ref(), false) + style_expression(&style_list, &name, value.as_deref(), false) } }; } @@ -151,7 +195,7 @@ pub fn style_helper( pub(crate) fn style_expression( style_list: &web_sys::CssStyleDeclaration, style_name: &str, - value: Option<&Cow<'static, str>>, + value: Option<&str>, force: bool, ) { use crate::HydrationCtx; @@ -160,7 +204,7 @@ pub(crate) fn style_expression( let style_name = wasm_bindgen::intern(style_name); if let Some(value) = value { - if let Err(e) = style_list.set_property(style_name, &value) { + if let Err(e) = style_list.set_property(style_name, value) { crate::error!("[HtmlElement::style()] {e:?}"); } } else { diff --git a/leptos_dom/src/math.rs b/leptos_dom/src/math.rs index 8a83d6a47..bdd96478d 100644 --- a/leptos_dom/src/math.rs +++ b/leptos_dom/src/math.rs @@ -3,7 +3,7 @@ use super::{ElementDescriptor, HtmlElement}; use crate::HydrationCtx; use cfg_if::cfg_if; -use std::borrow::Cow; +use leptos_reactive::Oco; cfg_if! { if #[cfg(all(target_arch = "wasm32", feature = "web"))] { use once_cell::unsync::Lazy as LazyCell; @@ -145,7 +145,7 @@ macro_rules! generate_math_tags { } impl ElementDescriptor for [<$tag:camel $($second:camel $($third:camel)?)?>] { - fn name(&self) -> Cow<'static, str> { + fn name(&self) -> Oco<'static, str> { stringify!($tag).into() } diff --git a/leptos_dom/src/ssr.rs b/leptos_dom/src/ssr.rs index 88b788ccd..5a48b22ae 100644 --- a/leptos_dom/src/ssr.rs +++ b/leptos_dom/src/ssr.rs @@ -9,8 +9,8 @@ use crate::{ use cfg_if::cfg_if; use futures::{stream::FuturesUnordered, Future, Stream, StreamExt}; use itertools::Itertools; -use leptos_reactive::*; -use std::{borrow::Cow, pin::Pin}; +use leptos_reactive::{Oco, *}; +use std::pin::Pin; type PinnedFuture = Pin>>; @@ -30,7 +30,7 @@ type PinnedFuture = Pin>>; any(debug_assertions, feature = "ssr"), instrument(level = "info", skip_all,) )] -pub fn render_to_string(f: F) -> String +pub fn render_to_string(f: F) -> Oco<'static, str> where F: FnOnce() -> N + 'static, N: IntoView, @@ -42,7 +42,7 @@ where runtime.dispose(); - html.into() + html } /// Renders a function to a stream of HTML strings. @@ -87,7 +87,7 @@ pub fn render_to_stream( )] pub fn render_to_stream_with_prefix( view: impl FnOnce() -> View + 'static, - prefix: impl FnOnce() -> Cow<'static, str> + 'static, + prefix: impl FnOnce() -> Oco<'static, str> + 'static, ) -> impl Stream { let (stream, runtime) = render_to_stream_with_prefix_undisposed(view, prefix); @@ -116,7 +116,7 @@ pub fn render_to_stream_with_prefix( )] pub fn render_to_stream_with_prefix_undisposed( view: impl FnOnce() -> View + 'static, - prefix: impl FnOnce() -> Cow<'static, str> + 'static, + prefix: impl FnOnce() -> Oco<'static, str> + 'static, ) -> (impl Stream, RuntimeId) { render_to_stream_with_prefix_undisposed_with_context(view, prefix, || {}) } @@ -142,7 +142,7 @@ pub fn render_to_stream_with_prefix_undisposed( )] pub fn render_to_stream_with_prefix_undisposed_with_context( view: impl FnOnce() -> View + 'static, - prefix: impl FnOnce() -> Cow<'static, str> + 'static, + prefix: impl FnOnce() -> Oco<'static, str> + 'static, additional_context: impl FnOnce() + 'static, ) -> (impl Stream, RuntimeId) { render_to_stream_with_prefix_undisposed_with_context_and_block_replacement( @@ -179,7 +179,7 @@ pub fn render_to_stream_with_prefix_undisposed_with_context( )] pub fn render_to_stream_with_prefix_undisposed_with_context_and_block_replacement( view: impl FnOnce() -> View + 'static, - prefix: impl FnOnce() -> Cow<'static, str> + 'static, + prefix: impl FnOnce() -> Oco<'static, str> + 'static, additional_context: impl FnOnce() + 'static, replace_blocks: bool, ) -> (impl Stream, RuntimeId) { @@ -363,7 +363,7 @@ impl View { any(debug_assertions, feature = "ssr"), instrument(level = "info", skip_all,) )] - pub fn render_to_string(self) -> Cow<'static, str> { + pub fn render_to_string(self) -> Oco<'static, str> { #[cfg(all(feature = "web", feature = "ssr"))] crate::console_error( "\n[DANGER] You have both `csr` and `ssr` or `hydrate` and `ssr` \ @@ -381,7 +381,7 @@ impl View { pub(crate) fn render_to_string_helper( self, dont_escape_text: bool, - ) -> Cow<'static, str> { + ) -> Oco<'static, str> { match self { View::Text(node) => { if dont_escape_text { @@ -450,7 +450,7 @@ impl View { ) .into() }) - as Box Cow<'static, str>>, + as Box Oco<'static, str>>, ), CoreComponent::DynChild(node) => { let child = node.child.take(); @@ -500,7 +500,7 @@ impl View { "".into() } }) - as Box Cow<'static, str>>, + as Box Oco<'static, str>>, ) } CoreComponent::Each(node) => { @@ -554,7 +554,7 @@ impl View { .join("") .into() }) - as Box Cow<'static, str>>, + as Box Oco<'static, str>>, ) } }; @@ -598,15 +598,15 @@ impl View { .join("") .into() } else { - let tag_name = el.name; + let tag_name: Oco<'_, str> = el.name; - let mut inner_html = None; + let mut inner_html: Option> = None; let attrs = el .attrs .into_iter() .filter_map( - |(name, value)| -> Option> { + |(name, value)| -> Option> { if value.is_empty() { Some(format!(" {name}").into()) } else if name == "inner_html" { @@ -615,9 +615,9 @@ impl View { } else { Some( format!( - " {name}=\"{}\"", - html_escape::encode_double_quoted_attribute(&value) - ) + " {name}=\"{}\"", + html_escape::encode_double_quoted_attribute(&value) + ) .into(), ) } @@ -729,9 +729,9 @@ pub(crate) fn render_serializers( } #[doc(hidden)] -pub fn escape_attr(value: &T) -> Cow<'_, str> +pub fn escape_attr(value: &T) -> Oco<'_, str> where T: AsRef, { - html_escape::encode_double_quoted_attribute(value) + html_escape::encode_double_quoted_attribute(value).into() } diff --git a/leptos_dom/src/ssr_in_order.rs b/leptos_dom/src/ssr_in_order.rs index 2c8012c81..d540a2e83 100644 --- a/leptos_dom/src/ssr_in_order.rs +++ b/leptos_dom/src/ssr_in_order.rs @@ -12,7 +12,7 @@ use cfg_if::cfg_if; use futures::{channel::mpsc::UnboundedSender, Stream, StreamExt}; use itertools::Itertools; use leptos_reactive::{ - create_runtime, suspense::StreamChunk, RuntimeId, SharedContext, + create_runtime, suspense::StreamChunk, Oco, RuntimeId, SharedContext, }; use std::{borrow::Cow, collections::VecDeque}; @@ -59,7 +59,7 @@ pub fn render_to_stream_in_order( #[tracing::instrument(level = "trace", skip_all)] pub fn render_to_stream_in_order_with_prefix( view: impl FnOnce() -> View + 'static, - prefix: impl FnOnce() -> Cow<'static, str> + 'static, + prefix: impl FnOnce() -> Oco<'static, str> + 'static, ) -> impl Stream { #[cfg(all(feature = "web", feature = "ssr"))] crate::console_error( @@ -89,7 +89,7 @@ pub fn render_to_stream_in_order_with_prefix( #[tracing::instrument(level = "trace", skip_all)] pub fn render_to_stream_in_order_with_prefix_undisposed_with_context( view: impl FnOnce() -> View + 'static, - prefix: impl FnOnce() -> Cow<'static, str> + 'static, + prefix: impl FnOnce() -> Oco<'static, str> + 'static, additional_context: impl FnOnce() + 'static, ) -> (impl Stream, RuntimeId) { HydrationCtx::reset_id(); @@ -287,12 +287,11 @@ impl View { StringOrView::String(string) => { chunks.push_back(StreamChunk::Sync(string)) } - StringOrView::View(view) => { - view().into_stream_chunks_helper( + StringOrView::View(view) => view() + .into_stream_chunks_helper( chunks, is_script_or_style, - ); - } + ), } } } else { @@ -313,9 +312,9 @@ impl View { } else { Some( format!( - " {name}=\"{}\"", - html_escape::encode_double_quoted_attribute(&value) - ) + " {name}=\"{}\"", + html_escape::encode_double_quoted_attribute(&value) + ) .into(), ) } @@ -350,7 +349,7 @@ impl View { } } ElementChildren::InnerHtml(inner_html) => { - chunks.push_back(StreamChunk::Sync(inner_html)); + chunks.push_back(StreamChunk::Sync(inner_html)) } // handled above ElementChildren::Chunks(_) => unreachable!(), diff --git a/leptos_dom/src/svg.rs b/leptos_dom/src/svg.rs index f816c9d68..f6155f6ef 100644 --- a/leptos_dom/src/svg.rs +++ b/leptos_dom/src/svg.rs @@ -4,9 +4,9 @@ use super::{html::HTML_ELEMENT_DEREF_UNIMPLEMENTED_MSG, HydrationKey}; use super::{ElementDescriptor, HtmlElement}; use crate::HydrationCtx; +use leptos_reactive::Oco; #[cfg(all(target_arch = "wasm32", feature = "web"))] use once_cell::unsync::Lazy as LazyCell; -use std::borrow::Cow; #[cfg(all(target_arch = "wasm32", feature = "web"))] use wasm_bindgen::JsCast; @@ -142,7 +142,7 @@ macro_rules! generate_svg_tags { } impl ElementDescriptor for [<$tag:camel $($second:camel $($third:camel)?)?>] { - fn name(&self) -> Cow<'static, str> { + fn name(&self) -> Oco<'static, str> { stringify!($tag).into() } diff --git a/leptos_reactive/src/lib.rs b/leptos_reactive/src/lib.rs index c06e87124..57df3f089 100644 --- a/leptos_reactive/src/lib.rs +++ b/leptos_reactive/src/lib.rs @@ -85,6 +85,7 @@ mod effect; mod hydration; mod memo; mod node; +pub mod oco; mod resource; mod runtime; mod selector; @@ -107,6 +108,7 @@ pub use effect::*; pub use hydration::{FragmentData, SharedContext}; pub use memo::*; pub use node::Disposer; +pub use oco::*; pub use resource::*; use runtime::*; pub use runtime::{ diff --git a/leptos_reactive/src/oco.rs b/leptos_reactive/src/oco.rs new file mode 100644 index 000000000..fe9cd3aa4 --- /dev/null +++ b/leptos_reactive/src/oco.rs @@ -0,0 +1,681 @@ +//! This module contains the `Oco` (Owned Clones Once) smart pointer, +//! which is used to store immutable references to values. +//! This is useful for storing, for example, strings. + +use std::{ + borrow::{Borrow, Cow}, + ffi::{CStr, OsStr}, + fmt, + hash::Hash, + ops::{Add, Deref}, + path::Path, + rc::Rc, +}; + +/// "Owned Clones Once" - a smart pointer that can be either a reference, +/// an owned value, or a reference counted pointer. This is useful for +/// storing immutable values, such as strings, in a way that is cheap to +/// clone and pass around. +/// +/// The `Clone` implementation is amortized `O(1)`. Cloning the [`Oco::Borrowed`] +/// variant simply copies the references (`O(1)`). Cloning the [`Oco::Counted`] +/// variant increments a reference count (`O(1)`). Cloning the [`Oco::Owned`] +/// variant upgrades it to [`Oco::Counted`], which requires an `O(n)` clone of the +/// data, but all subsequent clones will be `O(1)`. +pub enum Oco<'a, T: ?Sized + ToOwned + 'a> { + /// A static reference to a value. + Borrowed(&'a T), + /// A reference counted pointer to a value. + Counted(Rc), + /// An owned value. + Owned(::Owned), +} + +impl Oco<'_, T> { + /// Converts the value into an owned value. + pub fn into_owned(self) -> ::Owned { + match self { + Oco::Borrowed(v) => v.to_owned(), + Oco::Counted(v) => v.as_ref().to_owned(), + Oco::Owned(v) => v, + } + } + + /// Checks if the value is [`Oco::Borrowed`]. + /// # Examples + /// ``` + /// # use std::rc::Rc; + /// # use leptos_reactive::oco::Oco; + /// assert!(Oco::::Borrowed("Hello").is_borrowed()); + /// assert!(!Oco::::Counted(Rc::from("Hello")).is_borrowed()); + /// assert!(!Oco::::Owned("Hello".to_string()).is_borrowed()); + /// ``` + pub const fn is_borrowed(&self) -> bool { + matches!(self, Oco::Borrowed(_)) + } + + /// Checks if the value is [`Oco::Counted`]. + /// # Examples + /// ``` + /// # use std::rc::Rc; + /// # use leptos_reactive::oco::Oco; + /// assert!(Oco::::Counted(Rc::from("Hello")).is_counted()); + /// assert!(!Oco::::Borrowed("Hello").is_counted()); + /// assert!(!Oco::::Owned("Hello".to_string()).is_counted()); + /// ``` + pub const fn is_counted(&self) -> bool { + matches!(self, Oco::Counted(_)) + } + + /// Checks if the value is [`Oco::Owned`]. + /// # Examples + /// ``` + /// # use std::rc::Rc; + /// # use leptos_reactive::oco::Oco; + /// assert!(Oco::::Owned("Hello".to_string()).is_owned()); + /// assert!(!Oco::::Borrowed("Hello").is_owned()); + /// assert!(!Oco::::Counted(Rc::from("Hello")).is_owned()); + /// ``` + pub const fn is_owned(&self) -> bool { + matches!(self, Oco::Owned(_)) + } +} + +impl Deref for Oco<'_, T> { + type Target = T; + + fn deref(&self) -> &T { + match self { + Oco::Borrowed(v) => v, + Oco::Owned(v) => v.borrow(), + Oco::Counted(v) => v, + } + } +} + +impl Borrow for Oco<'_, T> { + #[inline(always)] + fn borrow(&self) -> &T { + self.deref() + } +} + +impl AsRef for Oco<'_, T> { + #[inline(always)] + fn as_ref(&self) -> &T { + self.deref() + } +} + +impl AsRef for Oco<'_, str> { + #[inline(always)] + fn as_ref(&self) -> &Path { + self.as_str().as_ref() + } +} + +impl AsRef for Oco<'_, OsStr> { + #[inline(always)] + fn as_ref(&self) -> &Path { + self.as_os_str().as_ref() + } +} + +// -------------------------------------- +// pub fn as_{slice}(&self) -> &{slice} +// -------------------------------------- + +impl Oco<'_, str> { + /// Returns a `&str` slice of this [`Oco`]. + /// # Examples + /// ``` + /// # use leptos_reactive::oco::Oco; + /// let oco = Oco::::Borrowed("Hello"); + /// let s: &str = oco.as_str(); + /// assert_eq!(s, "Hello"); + /// ``` + #[inline(always)] + pub fn as_str(&self) -> &str { + self + } +} + +impl Oco<'_, CStr> { + /// Returns a `&CStr` slice of this [`Oco`]. + /// # Examples + /// ``` + /// # use leptos_reactive::oco::Oco; + /// use std::ffi::CStr; + /// + /// let oco = + /// Oco::::Borrowed(CStr::from_bytes_with_nul(b"Hello\0").unwrap()); + /// let s: &CStr = oco.as_c_str(); + /// assert_eq!(s, CStr::from_bytes_with_nul(b"Hello\0").unwrap()); + /// ``` + #[inline(always)] + pub fn as_c_str(&self) -> &CStr { + self + } +} + +impl Oco<'_, OsStr> { + /// Returns a `&OsStr` slice of this [`Oco`]. + /// # Examples + /// ``` + /// # use leptos_reactive::oco::Oco; + /// use std::ffi::OsStr; + /// + /// let oco = Oco::::Borrowed(OsStr::new("Hello")); + /// let s: &OsStr = oco.as_os_str(); + /// assert_eq!(s, OsStr::new("Hello")); + /// ``` + #[inline(always)] + pub fn as_os_str(&self) -> &OsStr { + self + } +} + +impl Oco<'_, Path> { + /// Returns a `&Path` slice of this [`Oco`]. + /// # Examples + /// ``` + /// # use leptos_reactive::oco::Oco; + /// use std::path::Path; + /// + /// let oco = Oco::::Borrowed(Path::new("Hello")); + /// let s: &Path = oco.as_path(); + /// assert_eq!(s, Path::new("Hello")); + /// ``` + #[inline(always)] + pub fn as_path(&self) -> &Path { + self + } +} + +impl Oco<'_, [T]> +where + [T]: ToOwned, +{ + /// Returns a `&[T]` slice of this [`Oco`]. + /// # Examples + /// ``` + /// # use leptos_reactive::oco::Oco; + /// let oco = Oco::<[u8]>::Borrowed(b"Hello"); + /// let s: &[u8] = oco.as_slice(); + /// assert_eq!(s, b"Hello"); + /// ``` + #[inline(always)] + pub fn as_slice(&self) -> &[T] { + self + } +} + +// ------------------------------------------------------------------------------------------------------ +// Cloning (has to be implemented manually because of the `Rc: From<&::Owned>` bound) +// ------------------------------------------------------------------------------------------------------ + +impl Clone for Oco<'_, str> { + /// Returns a new [`Oco`] with the same value as this one. + /// If the value is [`Oco::Owned`], this will convert it into + /// [`Oco::Counted`], so that the next clone will be O(1). + /// # Examples + /// ``` + /// # use leptos_reactive::oco::Oco; + /// let oco = Oco::::Owned("Hello".to_string()); + /// let oco2 = oco.clone(); + /// assert_eq!(oco, oco2); + /// assert!(oco2.is_counted()); + /// ``` + fn clone(&self) -> Self { + match self { + Oco::Borrowed(v) => Oco::Borrowed(v), + Oco::Counted(v) => Oco::Counted(v.clone()), + Oco::Owned(v) => Oco::Counted(Rc::::from(v.as_str())), + } + } +} + +impl Clone for Oco<'_, CStr> { + /// Returns a new [`Oco`] with the same value as this one. + /// If the value is [`Oco::Owned`], this will convert it into + /// [`Oco::Counted`], so that the next clone will be O(1). + /// # Examples + /// ``` + /// # use leptos_reactive::oco::Oco; + /// use std::ffi::CStr; + /// + /// let oco = Oco::::Owned( + /// CStr::from_bytes_with_nul(b"Hello\0").unwrap().to_owned(), + /// ); + /// let oco2 = oco.clone(); + /// assert_eq!(oco, oco2); + /// assert!(oco2.is_counted()); + /// ``` + fn clone(&self) -> Self { + match self { + Oco::Borrowed(v) => Oco::Borrowed(v), + Oco::Counted(v) => Oco::Counted(v.clone()), + Oco::Owned(v) => Oco::Counted(Rc::::from(v.as_c_str())), + } + } +} + +impl Clone for Oco<'_, OsStr> { + /// Returns a new [`Oco`] with the same value as this one. + /// If the value is [`Oco::Owned`], this will convert it into + /// [`Oco::Counted`], so that the next clone will be O(1). + /// # Examples + /// ``` + /// # use leptos_reactive::oco::Oco; + /// use std::ffi::OsStr; + /// + /// let oco = Oco::::Owned(OsStr::new("Hello").to_owned()); + /// let oco2 = oco.clone(); + /// assert_eq!(oco, oco2); + /// assert!(oco2.is_counted()); + /// ``` + fn clone(&self) -> Self { + match self { + Oco::Borrowed(v) => Oco::Borrowed(v), + Oco::Counted(v) => Oco::Counted(v.clone()), + Oco::Owned(v) => Oco::Counted(Rc::::from(v.as_os_str())), + } + } +} + +impl Clone for Oco<'_, Path> { + /// Returns a new [`Oco`] with the same value as this one. + /// If the value is [`Oco::Owned`], this will convert it into + /// [`Oco::Counted`], so that the next clone will be O(1). + /// # Examples + /// ``` + /// # use leptos_reactive::oco::Oco; + /// use std::path::Path; + /// + /// let oco = Oco::::Owned(Path::new("Hello").to_owned()); + /// let oco2 = oco.clone(); + /// assert_eq!(oco, oco2); + /// assert!(oco2.is_counted()); + /// ``` + fn clone(&self) -> Self { + match self { + Oco::Borrowed(v) => Oco::Borrowed(v), + Oco::Counted(v) => Oco::Counted(v.clone()), + Oco::Owned(v) => Oco::Counted(Rc::::from(v.as_path())), + } + } +} + +impl Clone for Oco<'_, [T]> +where + [T]: ToOwned>, +{ + /// Returns a new [`Oco`] with the same value as this one. + /// If the value is [`Oco::Owned`], this will convert it into + /// [`Oco::Counted`], so that the next clone will be O(1). + /// # Examples + /// ``` + /// # use leptos_reactive::oco::Oco; + /// let oco = Oco::<[i32]>::Owned(vec![1, 2, 3]); + /// let oco2 = oco.clone(); + /// assert_eq!(oco, oco2); + /// assert!(oco2.is_counted()); + /// ``` + fn clone(&self) -> Self { + match self { + Oco::Borrowed(v) => Oco::Borrowed(v), + Oco::Counted(v) => Oco::Counted(v.clone()), + Oco::Owned(v) => Oco::Counted(Rc::<[T]>::from(v.as_slice())), + } + } +} + +impl Default for Oco<'_, T> +where + T: ToOwned, + T::Owned: Default, +{ + fn default() -> Self { + Oco::Owned(T::Owned::default()) + } +} + +impl<'a, 'b, A: ?Sized, B: ?Sized> PartialEq> for Oco<'a, A> +where + A: PartialEq, + A: ToOwned, + B: ToOwned, +{ + fn eq(&self, other: &Oco<'b, B>) -> bool { + **self == **other + } +} + +impl Eq for Oco<'_, T> {} + +impl<'a, 'b, A: ?Sized, B: ?Sized> PartialOrd> for Oco<'a, A> +where + A: PartialOrd, + A: ToOwned, + B: ToOwned, +{ + fn partial_cmp(&self, other: &Oco<'b, B>) -> Option { + (**self).partial_cmp(&**other) + } +} + +impl Ord for Oco<'_, T> +where + T: ToOwned, +{ + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + (**self).cmp(&**other) + } +} + +impl Hash for Oco<'_, T> +where + T: ToOwned, +{ + fn hash(&self, state: &mut H) { + (**self).hash(state) + } +} + +impl fmt::Debug for Oco<'_, T> +where + T: ToOwned, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + (**self).fmt(f) + } +} + +impl fmt::Display for Oco<'_, T> +where + T: ToOwned, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + (**self).fmt(f) + } +} + +impl<'a, T: ?Sized> From<&'a T> for Oco<'a, T> +where + T: ToOwned, +{ + fn from(v: &'a T) -> Self { + Oco::Borrowed(v) + } +} + +impl<'a, T: ?Sized> From> for Oco<'a, T> +where + T: ToOwned, +{ + fn from(v: Cow<'a, T>) -> Self { + match v { + Cow::Borrowed(v) => Oco::Borrowed(v), + Cow::Owned(v) => Oco::Owned(v), + } + } +} + +impl<'a, T: ?Sized> From> for Cow<'a, T> +where + T: ToOwned, +{ + fn from(value: Oco<'a, T>) -> Self { + match value { + Oco::Borrowed(v) => Cow::Borrowed(v), + Oco::Owned(v) => Cow::Owned(v), + Oco::Counted(v) => Cow::Owned(v.as_ref().to_owned()), + } + } +} + +impl From> for Oco<'_, T> +where + T: ToOwned, +{ + fn from(v: Rc) -> Self { + Oco::Counted(v) + } +} + +impl From> for Oco<'_, T> +where + T: ToOwned, +{ + fn from(v: Box) -> Self { + Oco::Counted(v.into()) + } +} + +impl From for Oco<'_, str> { + fn from(v: String) -> Self { + Oco::Owned(v) + } +} + +impl From> for String { + fn from(v: Oco<'_, str>) -> Self { + match v { + Oco::Borrowed(v) => v.to_owned(), + Oco::Counted(v) => v.as_ref().to_owned(), + Oco::Owned(v) => v, + } + } +} + +impl From> for Oco<'_, [T]> +where + [T]: ToOwned>, +{ + fn from(v: Vec) -> Self { + Oco::Owned(v) + } +} + +impl<'a, T, const N: usize> From<&'a [T; N]> for Oco<'a, [T]> +where + [T]: ToOwned, +{ + fn from(v: &'a [T; N]) -> Self { + Oco::Borrowed(v) + } +} + +impl<'a> From> for Oco<'a, [u8]> { + fn from(v: Oco<'a, str>) -> Self { + match v { + Oco::Borrowed(v) => Oco::Borrowed(v.as_bytes()), + Oco::Owned(v) => Oco::Owned(v.into_bytes()), + Oco::Counted(v) => Oco::Counted(v.into()), + } + } +} + +/// Error returned from [`Oco::try_from`] for unsuccessful +/// conversion from `Oco<'_, [u8]>` to `Oco<'_, str>`. +#[derive(Debug, Clone, thiserror::Error)] +#[error("invalid utf-8 sequence: {_0}")] +pub enum FromUtf8Error { + /// Error for conversion of [`Oco::Borrowed`] and [`Oco::Counted`] variants + /// (`&[u8]` to `&str`). + #[error("{_0}")] + StrFromBytes( + #[source] + #[from] + std::str::Utf8Error, + ), + /// Error for conversion of [`Oco::Owned`] variant (`Vec` to `String`). + #[error("{_0}")] + StringFromBytes( + #[source] + #[from] + std::string::FromUtf8Error, + ), +} + +macro_rules! impl_slice_eq { + ([$($g:tt)*] $((where $($w:tt)+))?, $lhs:ty, $rhs: ty) => { + impl<$($g)*> PartialEq<$rhs> for $lhs + $(where + $($w)*)? + { + #[inline] + fn eq(&self, other: &$rhs) -> bool { + PartialEq::eq(&self[..], &other[..]) + } + } + + impl<$($g)*> PartialEq<$lhs> for $rhs + $(where + $($w)*)? + { + #[inline] + fn eq(&self, other: &$lhs) -> bool { + PartialEq::eq(&self[..], &other[..]) + } + } + }; +} + +impl_slice_eq!([], Oco<'_, str>, str); +impl_slice_eq!(['a, 'b], Oco<'a, str>, &'b str); +impl_slice_eq!([], Oco<'_, str>, String); +impl_slice_eq!(['a, 'b], Oco<'a, str>, Cow<'b, str>); + +impl_slice_eq!([T: PartialEq] (where [T]: ToOwned), Oco<'_, [T]>, [T]); +impl_slice_eq!(['a, 'b, T: PartialEq] (where [T]: ToOwned), Oco<'a, [T]>, &'b [T]); +impl_slice_eq!([T: PartialEq] (where [T]: ToOwned), Oco<'_, [T]>, Vec); +impl_slice_eq!(['a, 'b, T: PartialEq] (where [T]: ToOwned), Oco<'a, [T]>, Cow<'b, [T]>); + +impl<'a, 'b> Add<&'b str> for Oco<'a, str> { + type Output = Oco<'static, str>; + + fn add(self, rhs: &'b str) -> Self::Output { + Oco::Owned(String::from(self) + rhs) + } +} + +impl<'a, 'b> Add> for Oco<'a, str> { + type Output = Oco<'static, str>; + + fn add(self, rhs: Cow<'b, str>) -> Self::Output { + Oco::Owned(String::from(self) + rhs.as_ref()) + } +} + +impl<'a, 'b> Add> for Oco<'a, str> { + type Output = Oco<'static, str>; + + fn add(self, rhs: Oco<'b, str>) -> Self::Output { + Oco::Owned(String::from(self) + rhs.as_ref()) + } +} + +impl<'a> FromIterator> for String { + fn from_iter>>(iter: T) -> Self { + iter.into_iter().fold(String::new(), |mut acc, item| { + acc.push_str(item.as_ref()); + acc + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn debug_fmt_should_display_quotes_for_strings() { + let s: Oco = Oco::Borrowed("hello"); + assert_eq!(format!("{:?}", s), "\"hello\""); + let s: Oco = Oco::Counted(Rc::from("hello")); + assert_eq!(format!("{:?}", s), "\"hello\""); + } + + #[test] + fn partial_eq_should_compare_str_to_str() { + let s: Oco = Oco::Borrowed("hello"); + assert_eq!(s, "hello"); + assert_eq!("hello", s); + assert_eq!(s, String::from("hello")); + assert_eq!(String::from("hello"), s); + assert_eq!(s, Cow::from("hello")); + assert_eq!(Cow::from("hello"), s); + } + + #[test] + fn partial_eq_should_compare_slice_to_slice() { + let s: Oco<[i32]> = Oco::Borrowed([1, 2, 3].as_slice()); + assert_eq!(s, [1, 2, 3].as_slice()); + assert_eq!([1, 2, 3].as_slice(), s); + assert_eq!(s, vec![1, 2, 3]); + assert_eq!(vec![1, 2, 3], s); + assert_eq!(s, Cow::<'_, [i32]>::Borrowed(&[1, 2, 3])); + assert_eq!(Cow::<'_, [i32]>::Borrowed(&[1, 2, 3]), s); + } + + #[test] + fn add_should_concatenate_strings() { + let s: Oco = Oco::Borrowed("hello"); + assert_eq!(s.clone() + " world", "hello world"); + assert_eq!(s.clone() + Cow::from(" world"), "hello world"); + assert_eq!(s + Oco::from(" world"), "hello world"); + } + + #[test] + fn as_str_should_return_a_str() { + let s: Oco = Oco::Borrowed("hello"); + assert_eq!(s.as_str(), "hello"); + let s: Oco = Oco::Counted(Rc::from("hello")); + assert_eq!(s.as_str(), "hello"); + } + + #[test] + fn as_slice_should_return_a_slice() { + let s: Oco<[i32]> = Oco::Borrowed([1, 2, 3].as_slice()); + assert_eq!(s.as_slice(), [1, 2, 3].as_slice()); + let s: Oco<[i32]> = Oco::Counted(Rc::from([1, 2, 3])); + assert_eq!(s.as_slice(), [1, 2, 3].as_slice()); + } + + #[test] + fn default_for_str_should_return_an_empty_string() { + let s: Oco = Default::default(); + assert!(s.is_empty()); + } + + #[test] + fn default_for_slice_should_return_an_empty_slice() { + let s: Oco<[i32]> = Default::default(); + assert!(s.is_empty()); + } + + #[test] + fn default_for_any_option_should_return_none() { + let s: Oco> = Default::default(); + assert!(s.is_none()); + } + + #[test] + fn cloned_owned_string_should_become_counted_str() { + let s: Oco = Oco::Owned(String::from("hello")); + assert!(s.clone().is_counted()); + } + + #[test] + fn cloned_borrowed_str_should_remain_borrowed_str() { + let s: Oco = Oco::Borrowed("hello"); + assert!(s.clone().is_borrowed()); + } + + #[test] + fn cloned_counted_str_should_remain_counted_str() { + let s: Oco = Oco::Counted(Rc::from("hello")); + assert!(s.clone().is_counted()); + } +} diff --git a/leptos_reactive/src/suspense.rs b/leptos_reactive/src/suspense.rs index cef43ecf3..4e762c40c 100644 --- a/leptos_reactive/src/suspense.rs +++ b/leptos_reactive/src/suspense.rs @@ -1,14 +1,12 @@ //! Types that handle asynchronous data loading via ``. use crate::{ - create_isomorphic_effect, create_rw_signal, create_signal, queue_microtask, - signal::SignalGet, store_value, ReadSignal, RwSignal, SignalSet, - SignalUpdate, StoredValue, WriteSignal, + create_isomorphic_effect, create_rw_signal, create_signal, oco::Oco, + queue_microtask, signal::SignalGet, store_value, ReadSignal, RwSignal, + SignalSet, SignalUpdate, StoredValue, WriteSignal, }; use futures::Future; -use std::{ - borrow::Cow, cell::RefCell, collections::VecDeque, pin::Pin, rc::Rc, -}; +use std::{cell::RefCell, collections::VecDeque, pin::Pin, rc::Rc}; /// Tracks [`Resource`](crate::Resource)s that are read under a suspense context, /// i.e., within a [`Suspense`](https://docs.rs/leptos_core/latest/leptos_core/fn.Suspense.html) component. @@ -172,7 +170,7 @@ impl Default for SuspenseContext { /// Represents a chunk in a stream of HTML. pub enum StreamChunk { /// A chunk of synchronous HTML. - Sync(Cow<'static, str>), + Sync(Oco<'static, str>), /// A future that resolves to be a list of additional chunks. Async { /// The HTML chunks this contains. diff --git a/meta/src/lib.rs b/meta/src/lib.rs index 0719ef3fc..586ee4f95 100644 --- a/meta/src/lib.rs +++ b/meta/src/lib.rs @@ -51,7 +51,6 @@ use leptos::{ *, }; use std::{ - borrow::Cow, cell::{Cell, RefCell}, fmt::Debug, rc::Rc, @@ -100,7 +99,7 @@ pub struct MetaTagsContext { els: Rc< RefCell< IndexMap< - Cow<'static, str>, + Oco<'static, str>, (HtmlElement, Option), >, >, @@ -130,7 +129,7 @@ impl MetaTagsContext { pub fn register( &self, - id: Cow<'static, str>, + id: Oco<'static, str>, builder_el: HtmlElement, ) { cfg_if! { diff --git a/meta/src/link.rs b/meta/src/link.rs index e968a505a..f8c074a3a 100644 --- a/meta/src/link.rs +++ b/meta/src/link.rs @@ -1,6 +1,5 @@ use crate::use_head; use leptos::{nonce::use_nonce, *}; -use std::borrow::Cow; /// Injects an [HTMLLinkElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLLinkElement) into the document /// head, accepting any of the valid attributes for that tag. @@ -28,62 +27,62 @@ use std::borrow::Cow; pub fn Link( /// The [`id`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-id) attribute. #[prop(optional, into)] - id: Option>, + id: Option>, /// The [`as`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-as) attribute. #[prop(optional, into)] - as_: Option>, + as_: Option>, /// The [`crossorigin`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-crossorigin) attribute. #[prop(optional, into)] - crossorigin: Option>, + crossorigin: Option>, /// The [`disabled`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-disabled) attribute. #[prop(optional, into)] disabled: Option, /// The [`fetchpriority`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-fetchpriority) attribute. #[prop(optional, into)] - fetchpriority: Option>, + fetchpriority: Option>, /// The [`href`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-href) attribute. #[prop(optional, into)] - href: Option>, + href: Option>, /// The [`hreflang`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-hreflang) attribute. #[prop(optional, into)] - hreflang: Option>, + hreflang: Option>, /// The [`imagesizes`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-imagesizes) attribute. #[prop(optional, into)] - imagesizes: Option>, + imagesizes: Option>, /// The [`imagesrcset`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-imagesrcset) attribute. #[prop(optional, into)] - imagesrcset: Option>, + imagesrcset: Option>, /// The [`integrity`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-integrity) attribute. #[prop(optional, into)] - integrity: Option>, + integrity: Option>, /// The [`media`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-media) attribute. #[prop(optional, into)] - media: Option>, + media: Option>, /// The [`prefetch`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-prefetch) attribute. #[prop(optional, into)] - prefetch: Option>, + prefetch: Option>, /// The [`referrerpolicy`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-referrerpolicy) attribute. #[prop(optional, into)] - referrerpolicy: Option>, + referrerpolicy: Option>, /// The [`rel`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-rel) attribute. #[prop(optional, into)] - rel: Option>, + rel: Option>, /// The [`sizes`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-sizes) attribute. #[prop(optional, into)] - sizes: Option>, + sizes: Option>, /// The [`title`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-title) attribute. #[prop(optional, into)] - title: Option>, + title: Option>, /// The [`type`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-type) attribute. #[prop(optional, into)] - type_: Option>, + type_: Option>, /// The [`blocking`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-blocking) attribute. #[prop(optional, into)] - blocking: Option>, + blocking: Option>, ) -> impl IntoView { let meta = use_head(); let next_id = meta.tags.get_next_id(); - let id: Cow<'static, str> = + let id: Oco<'static, str> = id.unwrap_or_else(|| format!("leptos-link-{}", next_id.0).into()); let builder_el = leptos::leptos_dom::html::as_meta_tag({ diff --git a/meta/src/script.rs b/meta/src/script.rs index bbdc09caa..2600c1c53 100644 --- a/meta/src/script.rs +++ b/meta/src/script.rs @@ -1,6 +1,5 @@ use crate::use_head; use leptos::{nonce::use_nonce, *}; -use std::borrow::Cow; /// Injects an [HTMLScriptElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLScriptElement) into the document /// head, accepting any of the valid attributes for that tag. @@ -25,47 +24,47 @@ use std::borrow::Cow; pub fn Script( /// An ID for the `