Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 4 additions & 8 deletions crates/rangular-aot/src/lower.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ impl HoistState {
let closure = closure.clone();
self.closure_lets.push(quote! {
let #id = leptos::prelude::StoredValue::new(std::sync::Arc::new({
let host = __rangular_host.get_value().clone();
let host = __rangular_host.get_value();
#closure
}));
});
Expand Down Expand Up @@ -191,7 +191,6 @@ pub fn emit_rust_tokens(template: &Template, fn_name: &str) -> EmitTokens {
let has_default = has_default_projection(&template.nodes);
let tokens = if selects.is_empty() && has_projection(&template.nodes) {
quote! {
#[allow(clippy::needless_pass_by_value, clippy::redundant_clone)]
pub fn #ident<H: rangular_host::Host + 'static>(
host: rangular_aot::HostCell<H>,
children: Children,
Expand All @@ -211,7 +210,6 @@ pub fn emit_rust_tokens(template: &Template, fn_name: &str) -> EmitTokens {
params.push(quote! { children: Children });
}
quote! {
#[allow(clippy::needless_pass_by_value, clippy::redundant_clone)]
pub fn #ident<H: rangular_host::Host + 'static>(
host: rangular_aot::HostCell<H>,
#(#params),*
Expand All @@ -223,7 +221,6 @@ pub fn emit_rust_tokens(template: &Template, fn_name: &str) -> EmitTokens {
}
} else {
quote! {
#[allow(clippy::needless_pass_by_value, clippy::redundant_clone)]
pub fn #ident<H: rangular_host::Host + 'static>(
host: rangular_aot::HostCell<H>,
) -> impl IntoView {
Expand Down Expand Up @@ -618,10 +615,9 @@ fn lower_for(
let:row
>
{
let (__rangular_index, __rangular_count, #item_ident) = row.clone();
let __rangular_loop_store = leptos::prelude::StoredValue::new(std::sync::Arc::new(
#item_ident.clone(),
));
let (__rangular_index, __rangular_count, #item_ident) = row;
let __rangular_loop_store =
leptos::prelude::StoredValue::new(std::sync::Arc::new(#item_ident));
#body_prelude
view! { #body }
}
Expand Down
92 changes: 42 additions & 50 deletions crates/rangular-expr/src/pipe.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,21 +68,6 @@ impl PipeRegistry {
}
}

fn display_plain(value: &Value) -> String {
match value {
Value::Str(s) => s.clone(),
Value::Num(n) => format_num(*n),
Value::Bool(b) => b.to_string(),
Value::List(items) => items
.iter()
.map(display_plain)
.collect::<Vec<_>>()
.join(","),
Value::Event(payload) => format!("{payload:?}"),
Value::Unit => String::new(),
}
}

fn format_num(n: f64) -> String {
if n.fract() == 0.0 && n.abs() < 1e15 {
format!("{n:.0}")
Expand All @@ -102,14 +87,29 @@ fn as_number(value: &Value) -> Result<f64, EvalError> {
}
}

#[allow(clippy::unnecessary_wraps)] // PipeFn signature is always Result
fn pipe_uppercase(value: &Value, _: &[Value]) -> Result<Value, EvalError> {
Ok(Value::Str(display_plain(value).to_uppercase()))
const fn require_str<'a>(value: &'a Value, pipe: &'static str) -> Result<&'a str, EvalError> {
match value {
Value::Str(s) => Ok(s.as_str()),
_ => Err(EvalError::TypeMismatch(pipe)),
}
}

#[allow(clippy::unnecessary_wraps)] // PipeFn signature is always Result
fn pipe_lowercase(value: &Value, _: &[Value]) -> Result<Value, EvalError> {
Ok(Value::Str(display_plain(value).to_lowercase()))
fn pipe_uppercase(value: &Value, args: &[Value]) -> Result<Value, EvalError> {
if !args.is_empty() {
return Err(EvalError::TypeMismatch("uppercase pipe args"));
}
Ok(Value::Str(
require_str(value, "uppercase pipe")?.to_uppercase(),
))
}

fn pipe_lowercase(value: &Value, args: &[Value]) -> Result<Value, EvalError> {
if !args.is_empty() {
return Err(EvalError::TypeMismatch("lowercase pipe args"));
}
Ok(Value::Str(
require_str(value, "lowercase pipe")?.to_lowercase(),
))
}

fn pipe_number(value: &Value, args: &[Value]) -> Result<Value, EvalError> {
Expand All @@ -130,8 +130,10 @@ fn pipe_number(value: &Value, args: &[Value]) -> Result<Value, EvalError> {
Ok(Value::Str(format_num(n)))
}

#[allow(clippy::unnecessary_wraps)] // PipeFn signature is always Result
fn pipe_json(value: &Value, _: &[Value]) -> Result<Value, EvalError> {
fn pipe_json(value: &Value, args: &[Value]) -> Result<Value, EvalError> {
if !args.is_empty() {
return Err(EvalError::TypeMismatch("json pipe args"));
}
Ok(Value::Str(jsonish(value)))
}

Expand Down Expand Up @@ -190,6 +192,22 @@ mod tests {
reg.apply("missing", &Value::Unit, &[]),
Err(EvalError::UnknownPipe(_))
));
assert!(matches!(
reg.apply("uppercase", &Value::Num(1.0), &[]),
Err(EvalError::TypeMismatch(_))
));
assert!(matches!(
reg.apply("lowercase", &Value::Unit, &[]),
Err(EvalError::TypeMismatch(_))
));
assert!(matches!(
reg.apply("uppercase", &Value::Str("x".into()), &[Value::Num(1.0)]),
Err(EvalError::TypeMismatch(_))
));
assert!(matches!(
reg.apply("json", &Value::Unit, &[Value::Str("x".into())]),
Err(EvalError::TypeMismatch(_))
));
}

#[test]
Expand Down Expand Up @@ -223,21 +241,8 @@ mod tests {
}

#[test]
fn display_and_json_cover_value_variants() {
fn json_covers_value_variants() {
let reg = PipeRegistry::with_builtins();
assert_eq!(
reg.apply(
"uppercase",
&Value::List(vec![Value::Str("a".into()), Value::Bool(true)]),
&[]
)
.unwrap(),
Value::Str("A,TRUE".into())
);
assert_eq!(
reg.apply("lowercase", &Value::Unit, &[]).unwrap(),
Value::Str(String::new())
);
assert_eq!(
reg.apply(
"json",
Expand All @@ -256,19 +261,6 @@ mod tests {
.unwrap(),
Value::Str("\"Load\"".into())
);
assert_eq!(
reg.apply("uppercase", &Value::Num(1.5), &[]).unwrap(),
Value::Str("1.5".into())
);
assert_eq!(
reg.apply(
"uppercase",
&Value::from(rangular_host::EventPayload::Error),
&[]
)
.unwrap(),
Value::Str("ERROR".into())
);
assert_eq!(
reg.apply("json", &Value::Str("a\nb\rc\td\\e\"f".into()), &[])
.unwrap(),
Expand Down
22 changes: 19 additions & 3 deletions crates/rangular-runtime/tests/parity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -527,11 +527,27 @@ fn pipes_runtime_snapshot() {
use rangular_expr::{EvalError, PipeRegistry};
use rangular_host::Value as HostValue;

#[allow(clippy::unnecessary_wraps)]
fn pipe_crab(value: &HostValue, _: &[HostValue]) -> Result<HostValue, EvalError> {
fn pipe_crab(value: &HostValue, args: &[HostValue]) -> Result<HostValue, EvalError> {
if !args.is_empty() {
return Err(EvalError::TypeMismatch("crab pipe args"));
}
let text = match value {
HostValue::Str(s) => s.clone(),
_ => String::new(),
HostValue::Num(n) => n.to_string(),
HostValue::Bool(b) => b.to_string(),
HostValue::List(items) => {
let mut parts = Vec::with_capacity(items.len());
for item in items {
match item {
HostValue::Str(s) => parts.push(s.as_str()),
_ => return Err(EvalError::TypeMismatch("crab pipe list")),
}
}
parts.join(", ")
}
HostValue::Event(_) | HostValue::Unit => {
return Err(EvalError::TypeMismatch("crab pipe"));
}
};
Ok(HostValue::Str(format!("{text} 🦀")))
}
Expand Down
38 changes: 26 additions & 12 deletions demo-leptos/src/demo_pipes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,27 @@ pub fn demo_pipes() -> Arc<PipeRegistry> {
}))
}

#[allow(clippy::unnecessary_wraps)]
fn pipe_crab(value: &Value, _: &[Value]) -> Result<Value, EvalError> {
fn pipe_crab(value: &Value, args: &[Value]) -> Result<Value, EvalError> {
if !args.is_empty() {
return Err(EvalError::TypeMismatch("crab pipe args"));
}
let text = match value {
Value::Str(s) => s.clone(),
Value::Num(n) => n.to_string(),
Value::Bool(b) => b.to_string(),
Value::List(items) => items
.iter()
.filter_map(|item| match item {
Value::Str(s) => Some(s.as_str()),
_ => None,
})
.collect::<Vec<_>>()
.join(", "),
Value::Event(payload) => format!("{payload:?}"),
Value::Unit => String::new(),
Value::List(items) => {
let mut parts = Vec::with_capacity(items.len());
for item in items {
match item {
Value::Str(s) => parts.push(s.as_str()),
_ => return Err(EvalError::TypeMismatch("crab pipe list")),
}
}
parts.join(", ")
}
Value::Event(_) | Value::Unit => {
return Err(EvalError::TypeMismatch("crab pipe"));
}
};
Ok(Value::Str(format!("{text} 🦀")))
}
Expand Down Expand Up @@ -57,4 +62,13 @@ mod tests {
let out = eval_with_pipes(&expr, &mut host, &demo_pipes()).unwrap();
assert_eq!(out, Value::Str("Hello 🦀".into()));
}

#[test]
fn crab_pipe_rejects_unit() {
let pipes = demo_pipes();
let err = pipes
.apply("crab", &Value::Unit, &[])
.expect_err("unit");
assert!(matches!(err, rangular_expr::EvalError::TypeMismatch(_)));
}
}
11 changes: 11 additions & 0 deletions docs/SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,17 @@ and property / attribute bindings.
| `{{ a \| b \| c }}` | Left-associative chain |

Builtins (pure transforms on eval): `uppercase`, `lowercase`, `number`, `json`.

| Builtin | Input | Notes |
| --- | --- | --- |
| `uppercase` / `lowercase` | `Str` only | reject non-strings and colon args |
| `number` | `Num`, parseable `Str`, or `Bool` | optional integer digits arg `0..=20` |
| `json` | any value | reject colon args |

Failed pipe eval becomes empty display in runtime / AOT (`Value::Unit`); callers that use
[`PipeRegistry::apply`](../crates/rangular-expr/src/pipe.rs) or `eval_with_pipes` still see
`EvalError`.

Apps register custom pipes on [`Registry::register_pipe`](../crates/rangular/src/registry.rs)
(same map for AOT `HostCell::with_pipes` and runtime `interpret_with_pipes`).

Expand Down