#![cfg_attr(
not(test),
deny(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::unreachable,
clippy::todo,
clippy::unimplemented,
clippy::indexing_slicing,
clippy::string_slice,
clippy::arithmetic_side_effects,
)
)]
use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
pub enum BinaryOp {
#[serde(rename = "+")]
Add,
#[serde(rename = "-")]
Sub,
#[serde(rename = "*")]
Mul,
#[serde(rename = "/")]
Div,
#[serde(rename = "%")]
Rem,
#[serde(rename = "==")]
Eq,
#[serde(rename = "!=")]
Ne,
#[serde(rename = "<")]
Lt,
#[serde(rename = "<=")]
Le,
#[serde(rename = ">")]
Gt,
#[serde(rename = ">=")]
Ge,
#[serde(rename = "&&")]
And,
#[serde(rename = "||")]
Or,
}
impl BinaryOp {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Add => "+",
Self::Sub => "-",
Self::Mul => "*",
Self::Div => "/",
Self::Rem => "%",
Self::Eq => "==",
Self::Ne => "!=",
Self::Lt => "<",
Self::Le => "<=",
Self::Gt => ">",
Self::Ge => ">=",
Self::And => "&&",
Self::Or => "||",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum RouteSource {
#[default]
Param,
Query,
Path,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[serde(tag = "expr", rename_all = "lowercase")]
pub enum Expr {
Lit {
value: Value,
},
State {
name: String,
#[serde(default)]
path: Option<String>,
},
Var {
name: String,
#[serde(default)]
path: Option<String>,
},
Param {
name: String,
#[serde(default)]
path: Option<String>,
},
Bin {
op: BinaryOp,
left: Box<Self>,
right: Box<Self>,
},
Not {
operand: Box<Self>,
},
Cond {
#[serde(rename = "if")]
condition: Box<Self>,
then: Box<Self>,
#[serde(rename = "else")]
otherwise: Box<Self>,
},
Get {
base: Box<Self>,
path: String,
},
Index {
base: Box<Self>,
key: Box<Self>,
},
Concat {
items: Vec<Self>,
},
Array {
elements: Vec<Self>,
},
Obj {
props: BTreeMap<String, Self>,
},
Route {
name: String,
#[serde(default)]
source: RouteSource,
},
Style {
name: String,
#[serde(default)]
variants: BTreeMap<String, Self>,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Default, Deserialize, Serialize)]
pub struct StylePreset {
#[serde(default)]
pub base: String,
#[serde(default)]
pub variants: BTreeMap<String, BTreeMap<String, String>>,
#[serde(default, rename = "defaultVariants")]
pub default_variants: BTreeMap<String, String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum StateType {
Number,
String,
List,
Boolean,
Object,
}
impl StateType {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Number => "number",
Self::String => "string",
Self::List => "list",
Self::Boolean => "boolean",
Self::Object => "object",
}
}
#[must_use]
pub fn accepts(self, value: &Value) -> bool {
match self {
Self::Number => value.is_number(),
Self::String => value.is_string(),
Self::List => value.is_array(),
Self::Boolean => value.is_boolean(),
Self::Object => value.is_object(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
pub struct StateField {
#[serde(rename = "type")]
pub ty: StateType,
pub initial: Value,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub enum UpdateOperation {
Increment,
Decrement,
Push,
Pop,
Remove,
Toggle,
Merge,
ReplaceAt,
InsertAt,
Splice,
}
impl UpdateOperation {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Increment => "increment",
Self::Decrement => "decrement",
Self::Push => "push",
Self::Pop => "pop",
Self::Remove => "remove",
Self::Toggle => "toggle",
Self::Merge => "merge",
Self::ReplaceAt => "replaceAt",
Self::InsertAt => "insertAt",
Self::Splice => "splice",
}
}
#[must_use]
pub const fn required_target_type(self) -> Option<StateType> {
match self {
Self::Increment | Self::Decrement => Some(StateType::Number),
Self::Push
| Self::Pop
| Self::Remove
| Self::ReplaceAt
| Self::InsertAt
| Self::Splice => Some(StateType::List),
Self::Toggle => Some(StateType::Boolean),
Self::Merge => Some(StateType::Object),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
#[serde(rename_all = "UPPERCASE")]
pub enum HttpMethod {
#[default]
Get,
Post,
Put,
Delete,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum StorageArea {
Local,
Session,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum StorageOperation {
Get,
Set,
Remove,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[serde(tag = "do", rename_all = "camelCase")]
pub enum ActionStep {
Set {
target: String,
value: Expr,
},
Update {
target: String,
operation: UpdateOperation,
#[serde(default)]
value: Option<Expr>,
#[serde(default)]
index: Option<Expr>,
#[serde(default, rename = "deleteCount")]
delete_count: Option<Expr>,
},
SetPath {
target: String,
path: Expr,
value: Expr,
},
If {
condition: Expr,
then: Vec<Self>,
#[serde(default, rename = "else")]
otherwise: Vec<Self>,
},
Fetch {
url: Expr,
#[serde(default)]
method: HttpMethod,
#[serde(default)]
body: Option<Expr>,
#[serde(default)]
result: Option<String>,
#[serde(default, rename = "onSuccess")]
on_success: Vec<Self>,
#[serde(default, rename = "onError")]
on_error: Vec<Self>,
},
Storage {
operation: StorageOperation,
key: Expr,
#[serde(default)]
value: Option<Expr>,
storage: StorageArea,
#[serde(default)]
result: Option<String>,
#[serde(default, rename = "onSuccess")]
on_success: Vec<Self>,
#[serde(default, rename = "onError")]
on_error: Vec<Self>,
},
Navigate {
url: Expr,
#[serde(default)]
replace: bool,
},
Delay {
ms: Expr,
then: Vec<Self>,
},
Interval {
ms: Expr,
action: String,
},
Focus {
target: Expr,
operation: String,
},
}
impl ActionStep {
#[must_use]
pub const fn is_pure(&self) -> bool {
matches!(
self,
Self::Set { .. } | Self::Update { .. } | Self::SetPath { .. } | Self::If { .. }
)
}
#[must_use]
pub const fn kind(&self) -> &'static str {
match self {
Self::Set { .. } => "set",
Self::Update { .. } => "update",
Self::SetPath { .. } => "setPath",
Self::If { .. } => "if",
Self::Fetch { .. } => "fetch",
Self::Storage { .. } => "storage",
Self::Navigate { .. } => "navigate",
Self::Delay { .. } => "delay",
Self::Interval { .. } => "interval",
Self::Focus { .. } => "focus",
}
}
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
pub struct ActionDefinition {
pub name: String,
#[serde(default)]
pub steps: Vec<ActionStep>,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
pub struct EventHandler {
pub event: String,
pub action: String,
#[serde(default)]
pub payload: Option<PayloadSpec>,
#[serde(default)]
pub debounce: Option<u32>,
#[serde(default)]
pub throttle: Option<u32>,
}
#[derive(Debug, Clone, PartialEq, Serialize)]
#[serde(untagged)]
pub enum PayloadSpec {
Single(Expr),
Map(BTreeMap<String, Expr>),
}
impl<'de> Deserialize<'de> for PayloadSpec {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::de::Error as _;
let raw = Value::deserialize(deserializer)?;
if raw.get("expr").is_some() {
Expr::deserialize(raw)
.map(Self::Single)
.map_err(D::Error::custom)
} else {
BTreeMap::<String, Expr>::deserialize(raw)
.map(Self::Map)
.map_err(D::Error::custom)
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize)]
#[serde(untagged)]
pub enum Prop {
Value(Expr),
Handler(Box<EventHandler>),
}
impl<'de> Deserialize<'de> for Prop {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::de::Error as _;
let raw = Value::deserialize(deserializer)?;
if raw.get("event").is_some() {
let handler = EventHandler::deserialize(raw).map_err(D::Error::custom)?;
Ok(Self::Handler(Box::new(handler)))
} else {
let expr = Expr::deserialize(raw).map_err(D::Error::custom)?;
Ok(Self::Value(expr))
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum IslandStrategy {
#[default]
Load,
Idle,
Visible,
Interaction,
Media,
Never,
}
impl IslandStrategy {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Load => "load",
Self::Idle => "idle",
Self::Visible => "visible",
Self::Interaction => "interaction",
Self::Media => "media",
Self::Never => "never",
}
}
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[serde(tag = "kind", rename_all = "camelCase")]
pub enum Node {
Element {
tag: String,
#[serde(default, rename = "ref")]
element_ref: Option<String>,
#[serde(default)]
props: BTreeMap<String, Prop>,
#[serde(default)]
children: Vec<Self>,
},
Text {
value: Expr,
},
If {
condition: Expr,
then: Box<Self>,
#[serde(default, rename = "else")]
otherwise: Option<Box<Self>>,
},
Each {
items: Expr,
#[serde(rename = "as")]
binding: String,
#[serde(default)]
index: Option<String>,
#[serde(default)]
key: Option<Expr>,
body: Box<Self>,
},
Component {
name: String,
#[serde(default)]
props: BTreeMap<String, Expr>,
#[serde(default)]
children: Vec<Self>,
},
Slot {
#[serde(default)]
name: Option<String>,
},
Markdown {
content: Expr,
},
Code {
language: Expr,
content: Expr,
},
Portal {
target: String,
#[serde(default)]
children: Vec<Self>,
},
Island {
id: String,
#[serde(default)]
strategy: IslandStrategy,
content: Box<Self>,
},
Suspense {
#[serde(default)]
id: Option<String>,
fallback: Box<Self>,
content: Box<Self>,
},
ErrorBoundary {
fallback: Box<Self>,
content: Box<Self>,
},
}
impl Node {
#[must_use]
pub const fn kind(&self) -> &'static str {
match self {
Self::Element { .. } => "element",
Self::Text { .. } => "text",
Self::If { .. } => "if",
Self::Each { .. } => "each",
Self::Component { .. } => "component",
Self::Slot { .. } => "slot",
Self::Markdown { .. } => "markdown",
Self::Code { .. } => "code",
Self::Portal { .. } => "portal",
Self::Island { .. } => "island",
Self::Suspense { .. } => "suspense",
Self::ErrorBoundary { .. } => "errorBoundary",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
pub struct ParamDef {
#[serde(rename = "type")]
pub ty: String,
#[serde(default = "default_true")]
pub required: bool,
}
const fn default_true() -> bool {
true
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
pub struct ComponentDef {
#[serde(default)]
pub params: BTreeMap<String, ParamDef>,
pub view: Node,
}
#[derive(Debug, Clone, PartialEq, Eq, Default, Deserialize, Serialize)]
pub struct Lifecycle {
#[serde(default, rename = "onMount")]
pub on_mount: Option<String>,
#[serde(default, rename = "onUnmount")]
pub on_unmount: Option<String>,
#[serde(default, rename = "onRouteEnter")]
pub on_route_enter: Option<String>,
#[serde(default, rename = "onRouteLeave")]
pub on_route_leave: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
pub struct RouteDefinition {
#[serde(default)]
pub path: Option<String>,
#[serde(default)]
pub title: Option<Expr>,
#[serde(default)]
pub meta: BTreeMap<String, Expr>,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
pub struct Program {
pub version: String,
#[serde(default)]
pub route: Option<RouteDefinition>,
#[serde(default)]
pub styles: BTreeMap<String, StylePreset>,
#[serde(default)]
pub lifecycle: Option<Lifecycle>,
#[serde(default)]
pub state: BTreeMap<String, StateField>,
#[serde(default)]
pub actions: Vec<ActionDefinition>,
pub view: Node,
#[serde(default)]
pub components: BTreeMap<String, ComponentDef>,
}