Build v0 example and SSE slices
This commit is contained in:
Generated
+107
@@ -35,6 +35,7 @@ dependencies = [
|
||||
"http",
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
"hyper-util",
|
||||
"itoa",
|
||||
"matchit",
|
||||
"memchr",
|
||||
@@ -45,6 +46,7 @@ dependencies = [
|
||||
"rustversion",
|
||||
"serde",
|
||||
"sync_wrapper",
|
||||
"tokio",
|
||||
"tower",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
@@ -221,6 +223,42 @@ version = "1.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
|
||||
|
||||
[[package]]
|
||||
name = "httpdate"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
||||
|
||||
[[package]]
|
||||
name = "hyper"
|
||||
version = "1.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"http",
|
||||
"http-body",
|
||||
"httpdate",
|
||||
"pin-project-lite",
|
||||
"smallvec",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-util"
|
||||
version = "0.1.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"http",
|
||||
"http-body",
|
||||
"hyper",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.14.0"
|
||||
@@ -237,6 +275,12 @@ version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.186"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
|
||||
|
||||
[[package]]
|
||||
name = "matchit"
|
||||
version = "0.7.3"
|
||||
@@ -255,6 +299,17 @@ version = "0.3.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"wasi",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "multer"
|
||||
version = "3.1.0"
|
||||
@@ -418,6 +473,7 @@ name = "slhx-axum"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"futures-util",
|
||||
"slhx-core",
|
||||
"slhx-js",
|
||||
"tokio",
|
||||
@@ -458,6 +514,31 @@ dependencies = [
|
||||
"slhx-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "slhx-v0-examples"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"slhx",
|
||||
"slhx-build",
|
||||
"slhx-test",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.15.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spin"
|
||||
version = "0.9.8"
|
||||
@@ -513,8 +594,12 @@ version = "1.52.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"mio",
|
||||
"pin-project-lite",
|
||||
"socket2",
|
||||
"tokio-macros",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -538,6 +623,7 @@ dependencies = [
|
||||
"futures-util",
|
||||
"pin-project-lite",
|
||||
"sync_wrapper",
|
||||
"tokio",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
]
|
||||
@@ -594,6 +680,27 @@ version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.1+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.61.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.21"
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = ["slhx", "slhx-core", "slhx-derive", "slhx-js", "slhx-axum", "slhx-build", "slhx-test"]
|
||||
members = ["slhx", "slhx-core", "slhx-derive", "slhx-js", "slhx-axum", "slhx-build", "slhx-test", "examples/v0"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.1.0"
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
[package]
|
||||
name = "slhx-v0-examples"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
slhx = { path = "../../slhx" }
|
||||
|
||||
[dev-dependencies]
|
||||
slhx-test = { path = "../../slhx-test" }
|
||||
|
||||
[build-dependencies]
|
||||
slhx-build = { path = "../../slhx-build" }
|
||||
@@ -0,0 +1 @@
|
||||
fn main() { slhx_build::app().run().unwrap(); }
|
||||
@@ -0,0 +1,108 @@
|
||||
#[slhx::surface]
|
||||
pub mod ui {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::ui;
|
||||
use slhx::{push, Effect, IntoEffect, NavigateMode};
|
||||
use slhx_test::inspect;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct Todo {
|
||||
id: u64,
|
||||
title: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct TodoInput {
|
||||
title: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct WizardInput {
|
||||
step: u8,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct Credentials {
|
||||
email: String,
|
||||
password: String,
|
||||
}
|
||||
|
||||
// req: examples/001 req: ceremony/001 req: build/001 req: build/005 req: codegen/002
|
||||
#[test]
|
||||
fn counter_updates_a_generated_slot() {
|
||||
fn increment(count: u64) -> impl IntoEffect {
|
||||
ui::slots::counter_value.text(count + 1)
|
||||
}
|
||||
|
||||
let effect = inspect(increment(1));
|
||||
|
||||
assert!(effect.has_slot(ui::slots::counter_value));
|
||||
assert!(matches!(effect.ops(), [Effect::Put { .. }]));
|
||||
}
|
||||
|
||||
// req: examples/001 req: progressive_disclosure/001 req: build/001 req: build/005 req: codegen/002
|
||||
#[test]
|
||||
fn todos_append_keyed_rows_from_form_input() {
|
||||
fn add_todo(input: TodoInput) -> impl IntoEffect {
|
||||
let todo = Todo { id: 7, title: input.title };
|
||||
ui::slots::todo_row.append(todo.id.to_string(), format!("<li>{}</li>", todo.title))
|
||||
}
|
||||
|
||||
let effect = inspect(add_todo(TodoInput { title: "Ship v0".into() }));
|
||||
|
||||
assert!(effect.has_resource(ui::slots::todo_row.id()));
|
||||
assert!(matches!(effect.ops(), [Effect::Insert { key, .. }] if key == "7"));
|
||||
}
|
||||
|
||||
// req: examples/001 req: progressive_disclosure/001 req: build/001 req: build/005 req: codegen/004
|
||||
#[test]
|
||||
fn wizard_form_swaps_the_current_step() {
|
||||
fn next_step(input: WizardInput) -> impl IntoEffect {
|
||||
ui::slots::wizard_step.text(format!("Step {}", input.step + 1))
|
||||
}
|
||||
|
||||
let effect = inspect(next_step(WizardInput { step: 1 }));
|
||||
|
||||
assert!(effect.has_slot(ui::slots::wizard_step));
|
||||
assert!(matches!(effect.ops(), [Effect::Put { .. }]));
|
||||
}
|
||||
|
||||
// req: examples/001 req: page_swap/002 req: page_swap/003 req: build/001 req: build/005
|
||||
#[test]
|
||||
fn page_swap_updates_content_and_history() {
|
||||
fn load_docs() -> impl IntoEffect {
|
||||
(
|
||||
ui::slots::content.text("<h1>Docs</h1>"),
|
||||
ui::slots::title.text("Docs"),
|
||||
push("/docs"),
|
||||
)
|
||||
}
|
||||
|
||||
let effect = inspect(load_docs());
|
||||
|
||||
assert!(effect.has_slot(ui::slots::content));
|
||||
assert!(effect.has_slot(ui::slots::title));
|
||||
assert!(matches!(effect.ops().last(), Some(Effect::Navigate { url, mode: NavigateMode::Push, .. }) if url == "/docs"));
|
||||
}
|
||||
|
||||
// req: examples/001 req: progressive_disclosure/001 req: build/001 req: build/005 req: codegen/004
|
||||
#[test]
|
||||
fn auth_flow_reads_typed_input_and_updates_status() {
|
||||
fn login(input: Credentials) -> impl IntoEffect {
|
||||
let status = if input.email == "demo@example.com" && !input.password.is_empty() {
|
||||
"Signed in"
|
||||
} else {
|
||||
"Try again"
|
||||
};
|
||||
|
||||
ui::slots::login_status.text(status)
|
||||
}
|
||||
|
||||
let effect = inspect(login(Credentials { email: "demo@example.com".into(), password: "secret".into() }));
|
||||
|
||||
assert!(effect.has_slot(ui::slots::login_status));
|
||||
assert!(matches!(effect.ops(), [Effect::Put { .. }]));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<section data-slhx-root="auth">
|
||||
<form data-slhx-handle="login" data-slhx-form="credentials">
|
||||
<input name="email" type="email" autocomplete="username">
|
||||
<input name="password" type="password" autocomplete="current-password">
|
||||
<button type="submit">Sign in</button>
|
||||
</form>
|
||||
<p data-slhx-slot="login_status">Signed out</p>
|
||||
</section>
|
||||
@@ -0,0 +1,4 @@
|
||||
<section data-slhx-root="counter">
|
||||
<output data-slhx-slot="counter_value">0</output>
|
||||
<button type="button" data-slhx-handle="increment">+</button>
|
||||
</section>
|
||||
@@ -0,0 +1,7 @@
|
||||
<main data-slhx-root="docs">
|
||||
<nav data-slhx-slot="nav">
|
||||
<a href="/docs" data-slhx-nav="" data-slhx-handle="load_docs">Docs</a>
|
||||
</nav>
|
||||
<article data-slhx-slot="content">Welcome</article>
|
||||
<title data-slhx-slot="title">Welcome</title>
|
||||
</main>
|
||||
@@ -0,0 +1,11 @@
|
||||
<section data-slhx-root="todos">
|
||||
<form data-slhx-handle="add_todo" data-slhx-form="new_todo">
|
||||
<input name="title" required="required">
|
||||
<button type="submit">Add</button>
|
||||
</form>
|
||||
<ul data-slhx-slot="todo_list">
|
||||
<template h-for="todo in &self.todos" h-key="todo.id">
|
||||
<li data-slhx-slot="todo_row">{+ todo.title +}</li>
|
||||
</template>
|
||||
</ul>
|
||||
</section>
|
||||
@@ -0,0 +1,6 @@
|
||||
<section data-slhx-root="wizard">
|
||||
<form data-slhx-handle="next_step" data-slhx-form="wizard_input">
|
||||
<div data-slhx-slot="wizard_step">Step 1</div>
|
||||
<button type="submit">Next</button>
|
||||
</form>
|
||||
</section>
|
||||
@@ -7,7 +7,8 @@ edition.workspace = true
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
axum = { version = "0.7", default-features = false, features = ["multipart"] }
|
||||
axum = { version = "0.7", default-features = false, features = ["multipart", "tokio"] }
|
||||
futures-util = { version = "0.3", default-features = false }
|
||||
slhx-core = { path = "../slhx-core" }
|
||||
slhx-js = { path = "../slhx-js" }
|
||||
|
||||
|
||||
+79
-2
@@ -2,7 +2,9 @@ use axum::async_trait;
|
||||
use axum::body::{to_bytes, Body};
|
||||
use axum::extract::{FromRequest, FromRequestParts, Multipart};
|
||||
use axum::http::{header, request::Parts, HeaderMap, HeaderValue, Request, Response, StatusCode};
|
||||
use axum::response::sse::{Event, Sse};
|
||||
use axum::response::IntoResponse;
|
||||
use futures_util::{Stream, StreamExt};
|
||||
use slhx_core::{BuildFingerprint, EffectBatch, IntoEffect};
|
||||
use std::collections::BTreeMap;
|
||||
use std::convert::Infallible;
|
||||
@@ -13,6 +15,7 @@ pub const SLHX_TITLE_HEADER: &str = "x-slhx-title";
|
||||
pub const SLHX_CONTENT_TYPE: &str = "application/slhx";
|
||||
pub const SLHX_HANDLE_FIELD: &str = "__h";
|
||||
pub const SLHX_RUNTIME_CONTENT_TYPE: &str = "application/javascript; charset=utf-8";
|
||||
pub const SLHX_SSE_EVENT: &str = "slhx";
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||
pub struct RuntimeJs;
|
||||
@@ -426,6 +429,54 @@ pub fn runtime_js_source() -> &'static str {
|
||||
slhx_js::RUNTIME_JS
|
||||
}
|
||||
|
||||
// req: push/001, req: push/003, req: push/004
|
||||
pub fn sse<S, E>(batches: S) -> Sse<impl Stream<Item = Result<Event, E>> + Send>
|
||||
where
|
||||
S: Stream<Item = Result<EffectBatch, E>> + Send + 'static,
|
||||
E: Into<axum::BoxError>,
|
||||
{
|
||||
Sse::new(batches.map(|batch| batch.map(sse_event)))
|
||||
}
|
||||
|
||||
pub fn sse_event(batch: EffectBatch) -> Event {
|
||||
Event::default()
|
||||
.event(SLHX_SSE_EVENT)
|
||||
.data(encode_sse_batch(&batch))
|
||||
}
|
||||
|
||||
pub fn encode_sse_batch(batch: &EffectBatch) -> String {
|
||||
base64_url_no_pad(&batch.to_wire())
|
||||
}
|
||||
|
||||
fn base64_url_no_pad(input: &[u8]) -> String {
|
||||
const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
||||
let mut out = String::with_capacity((input.len() * 4).div_ceil(3));
|
||||
let mut chunks = input.chunks_exact(3);
|
||||
for chunk in &mut chunks {
|
||||
let n = ((chunk[0] as u32) << 16) | ((chunk[1] as u32) << 8) | chunk[2] as u32;
|
||||
out.push(ALPHABET[((n >> 18) & 0x3f) as usize] as char);
|
||||
out.push(ALPHABET[((n >> 12) & 0x3f) as usize] as char);
|
||||
out.push(ALPHABET[((n >> 6) & 0x3f) as usize] as char);
|
||||
out.push(ALPHABET[(n & 0x3f) as usize] as char);
|
||||
}
|
||||
match chunks.remainder() {
|
||||
[a] => {
|
||||
let n = (*a as u32) << 16;
|
||||
out.push(ALPHABET[((n >> 18) & 0x3f) as usize] as char);
|
||||
out.push(ALPHABET[((n >> 12) & 0x3f) as usize] as char);
|
||||
}
|
||||
[a, b] => {
|
||||
let n = ((*a as u32) << 16) | ((*b as u32) << 8);
|
||||
out.push(ALPHABET[((n >> 18) & 0x3f) as usize] as char);
|
||||
out.push(ALPHABET[((n >> 12) & 0x3f) as usize] as char);
|
||||
out.push(ALPHABET[((n >> 6) & 0x3f) as usize] as char);
|
||||
}
|
||||
[] => {}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn parse_urlencoded_pairs(body: &[u8]) -> Result<Vec<(String, String)>, InteractionFormRejection> {
|
||||
if body.is_empty() {
|
||||
return Ok(Vec::new());
|
||||
@@ -479,8 +530,11 @@ fn hex(byte: u8) -> Option<u8> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{html_with_root_fingerprint, BuildFingerprint, InteractionForm};
|
||||
use axum::{body::Body, extract::FromRequest, http::Request};
|
||||
use super::{encode_sse_batch, html_with_root_fingerprint, sse, BuildFingerprint, InteractionForm, SLHX_SSE_EVENT};
|
||||
use axum::{body::{to_bytes, Body}, extract::FromRequest, http::{header, Request}, response::IntoResponse};
|
||||
use futures_util::stream;
|
||||
use slhx_core::{EffectBatch, EFFECT_BATCH_ABI_VERSION};
|
||||
use std::convert::Infallible;
|
||||
|
||||
#[test]
|
||||
fn root_fingerprint_is_added_to_initial_root() {
|
||||
@@ -505,6 +559,29 @@ mod tests {
|
||||
assert_eq!(html, "<main data-slhx-root data-slhx-fp=\"1\">Docs</main>");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn sse_response_streams_base64_url_effect_batches() {
|
||||
let batch = EffectBatch {
|
||||
abi_version: EFFECT_BATCH_ABI_VERSION,
|
||||
fingerprint: BuildFingerprint(11),
|
||||
ops: Vec::new(),
|
||||
};
|
||||
let encoded = encode_sse_batch(&batch);
|
||||
|
||||
let response = sse(stream::iter([Ok::<_, Infallible>(batch)])).into_response();
|
||||
assert_eq!(
|
||||
response.headers().get(header::CONTENT_TYPE).unwrap(),
|
||||
"text/event-stream"
|
||||
);
|
||||
let body = to_bytes(response.into_body(), 1024).await.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
String::from_utf8(body.to_vec()).unwrap(),
|
||||
format!("event: {SLHX_SSE_EVENT}\ndata: {encoded}\n\n")
|
||||
);
|
||||
assert!(!encoded.contains('='));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn interaction_form_extracts_multipart_fields_and_files() {
|
||||
let boundary = "slhx-test-boundary";
|
||||
|
||||
+113
-35
@@ -75,6 +75,7 @@ struct Resources {
|
||||
struct Resource {
|
||||
symbol: String,
|
||||
ident: String,
|
||||
component: String,
|
||||
keyed: bool,
|
||||
id: u32,
|
||||
}
|
||||
@@ -94,6 +95,7 @@ struct GeneratedControl {
|
||||
|
||||
impl Resources {
|
||||
fn add_surface(&mut self, root: &Path, path: &Path, surface: &SurfaceDocument) -> io::Result<()> {
|
||||
let component = component_ident(root, path)?;
|
||||
for node in &surface.nodes {
|
||||
let SurfaceNodeKind::Element { tag } = &node.kind else {
|
||||
continue;
|
||||
@@ -103,19 +105,19 @@ impl Resources {
|
||||
reject_unkeyed_loop(surface, node.scope, path, "slot", &name)?;
|
||||
let keyed = is_inside_keyed_for(surface, node.scope);
|
||||
let canonical = canonical_symbol(root, path, &name);
|
||||
self.insert_slot(canonical, name, keyed)?;
|
||||
self.insert_slot(canonical, name, component.clone(), keyed)?;
|
||||
}
|
||||
|
||||
if let Some(name) = static_attr(&node.attrs, "data-slhx-atom") {
|
||||
reject_unkeyed_loop(surface, node.scope, path, "atom", &name)?;
|
||||
let canonical = canonical_symbol(root, path, &name);
|
||||
self.insert_atom(canonical, name)?;
|
||||
self.insert_atom(canonical, name, component.clone())?;
|
||||
}
|
||||
|
||||
if let Some(name) = static_attr(&node.attrs, "data-slhx-handle") {
|
||||
reject_unkeyed_loop(surface, node.scope, path, "handle", &name)?;
|
||||
let canonical = canonical_symbol(root, path, &name);
|
||||
self.insert_handle(canonical, name.clone())?;
|
||||
self.insert_handle(canonical, name.clone(), component.clone())?;
|
||||
|
||||
if tag == "form" {
|
||||
let controls = surface
|
||||
@@ -133,32 +135,33 @@ impl Resources {
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
self.insert_form(canonical_symbol(root, path, &name), name, controls)?;
|
||||
self.insert_form(canonical_symbol(root, path, &name), name, component.clone(), controls)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn insert_slot(&mut self, symbol: String, name: String, keyed: bool) -> io::Result<()> {
|
||||
insert_resource(&mut self.slots, "slot", symbol, name, keyed)
|
||||
fn insert_slot(&mut self, symbol: String, name: String, component: String, keyed: bool) -> io::Result<()> {
|
||||
insert_resource(&mut self.slots, "slot", symbol, name, component, keyed)
|
||||
}
|
||||
|
||||
fn insert_handle(&mut self, symbol: String, name: String) -> io::Result<()> {
|
||||
insert_resource(&mut self.handles, "handle", symbol, name, false)
|
||||
fn insert_handle(&mut self, symbol: String, name: String, component: String) -> io::Result<()> {
|
||||
insert_resource(&mut self.handles, "handle", symbol, name, component, false)
|
||||
}
|
||||
|
||||
fn insert_atom(&mut self, symbol: String, name: String) -> io::Result<()> {
|
||||
insert_resource(&mut self.atoms, "atom", symbol, name, false)
|
||||
fn insert_atom(&mut self, symbol: String, name: String, component: String) -> io::Result<()> {
|
||||
insert_resource(&mut self.atoms, "atom", symbol, name, component, false)
|
||||
}
|
||||
|
||||
fn insert_form(
|
||||
&mut self,
|
||||
symbol: String,
|
||||
name: String,
|
||||
component: String,
|
||||
controls: Vec<GeneratedControl>,
|
||||
) -> io::Result<()> {
|
||||
let resource = make_resource("form", symbol, name, false)?;
|
||||
let resource = make_resource("form", symbol, name, component, false)?;
|
||||
match self.forms.get(&resource.ident) {
|
||||
Some(existing) if existing.resource.symbol != resource.symbol => Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
@@ -187,67 +190,111 @@ impl Resources {
|
||||
.join(", ")
|
||||
));
|
||||
|
||||
out.push_str("pub mod slots {\n");
|
||||
for res in self.slots.values() {
|
||||
// req: build/001
|
||||
self.push_resource_modules(&mut out, None, 0);
|
||||
|
||||
for component in self.component_names() {
|
||||
out.push_str("\n#[allow(non_upper_case_globals)]\n");
|
||||
out.push_str(&format!("pub mod {component} {{\n"));
|
||||
self.push_resource_modules(&mut out, Some(&component), 1);
|
||||
out.push_str("}\n");
|
||||
}
|
||||
|
||||
out
|
||||
}
|
||||
|
||||
fn push_resource_modules(&self, out: &mut String, component: Option<&str>, indent: usize) {
|
||||
let pad = " ".repeat(indent);
|
||||
let inner = " ".repeat(indent + 1);
|
||||
let mut handle_ids = Vec::new();
|
||||
|
||||
out.push_str(&format!("{pad}#[allow(non_upper_case_globals)]\n{pad}pub mod slots {{\n"));
|
||||
for res in self.slots.values().filter(|res| component_matches(res, component)) {
|
||||
if res.keyed {
|
||||
out.push_str(&format!(
|
||||
" pub const {}: ::slhx::KeyedSlot<::std::string::String, ::std::string::String> = ::slhx::KeyedSlot::new({});\n",
|
||||
"{inner}pub const {}: ::slhx::KeyedSlot<::std::string::String, ::std::string::String> = ::slhx::KeyedSlot::new({});\n",
|
||||
res.ident, res.id
|
||||
));
|
||||
} else {
|
||||
out.push_str(&format!(
|
||||
" pub const {}: ::slhx::Slot<::std::string::String> = ::slhx::Slot::new({});\n",
|
||||
"{inner}pub const {}: ::slhx::Slot<::std::string::String> = ::slhx::Slot::new({});\n",
|
||||
res.ident, res.id
|
||||
));
|
||||
}
|
||||
}
|
||||
out.push_str("}\n\n");
|
||||
out.push_str(&format!("{pad}}}\n\n"));
|
||||
|
||||
out.push_str("pub mod handles {\n");
|
||||
for res in self.handles.values() {
|
||||
out.push_str(&format!("{pad}#[allow(non_upper_case_globals)]\n{pad}pub mod handles {{\n"));
|
||||
for res in self.handles.values().filter(|res| component_matches(res, component)) {
|
||||
handle_ids.push(res.id);
|
||||
out.push_str(&format!(
|
||||
" pub const {}: ::slhx::Handle<()> = ::slhx::Handle::new({});\n",
|
||||
"{inner}pub const {}: ::slhx::Handle<()> = ::slhx::Handle::new({});\n",
|
||||
res.ident, res.id
|
||||
));
|
||||
}
|
||||
out.push_str("}\n\n");
|
||||
out.push_str(&format!(
|
||||
"{inner}pub const ALL_IDS: &[u32] = &[{}];\n",
|
||||
handle_ids
|
||||
.into_iter()
|
||||
.map(|id| id.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
));
|
||||
out.push_str(&format!("{pad}}}\n\n"));
|
||||
|
||||
out.push_str("pub mod atoms {\n");
|
||||
for res in self.atoms.values() {
|
||||
out.push_str(&format!("{pad}#[allow(non_upper_case_globals)]\n{pad}pub mod atoms {{\n"));
|
||||
for res in self.atoms.values().filter(|res| component_matches(res, component)) {
|
||||
out.push_str(&format!(
|
||||
" pub const {}: ::slhx::Atom<::std::string::String> = ::slhx::Atom::new({});\n",
|
||||
"{inner}pub const {}: ::slhx::Atom<::std::string::String> = ::slhx::Atom::new({});\n",
|
||||
res.ident, res.id
|
||||
));
|
||||
}
|
||||
out.push_str("}\n\n");
|
||||
out.push_str(&format!("{pad}}}\n\n"));
|
||||
|
||||
out.push_str("pub mod forms {\n");
|
||||
for form in self.forms.values() {
|
||||
out.push_str(&format!("{pad}#[allow(non_upper_case_globals)]\n{pad}pub mod forms {{\n"));
|
||||
for form in self.forms.values().filter(|form| component_matches(&form.resource, component)) {
|
||||
let res = &form.resource;
|
||||
out.push_str(&format!(
|
||||
" pub const {}: ::slhx::Form<::std::string::String> = ::slhx::Form::new({});\n",
|
||||
"{inner}pub const {}: ::slhx::Form<::std::string::String> = ::slhx::Form::new({});\n",
|
||||
res.ident, res.id
|
||||
));
|
||||
out.push_str(&format!(
|
||||
" pub const {}_CONTRACT: ::slhx::FormContract = ::slhx::FormContract {{ fields: &{}_FIELDS }};\n",
|
||||
"{inner}pub const {}_CONTRACT: ::slhx::FormContract = ::slhx::FormContract {{ fields: &{}_FIELDS }};\n",
|
||||
res.ident.to_ascii_uppercase(), res.ident.to_ascii_uppercase()
|
||||
));
|
||||
out.push_str(&format!(
|
||||
" pub const {}_FIELDS: &[::slhx::FormField] = &[\n",
|
||||
"{inner}pub const {}_FIELDS: &[::slhx::FormField] = &[\n",
|
||||
res.ident.to_ascii_uppercase()
|
||||
));
|
||||
for control in &form.controls {
|
||||
out.push_str(&format!(
|
||||
" ::slhx::FormField {{ name: {}, kind: {}, required: {} }},\n",
|
||||
"{inner} ::slhx::FormField {{ name: {}, kind: {}, required: {} }},\n",
|
||||
rust_str(&control.name),
|
||||
form_control_kind_expr(&control.kind),
|
||||
control.required
|
||||
));
|
||||
}
|
||||
out.push_str(" ];\n");
|
||||
out.push_str(&format!("{inner}];\n"));
|
||||
}
|
||||
out.push_str("}\n");
|
||||
out
|
||||
out.push_str(&format!("{pad}}}\n"));
|
||||
}
|
||||
|
||||
fn component_names(&self) -> Vec<String> {
|
||||
let mut components = Vec::new();
|
||||
for component in self
|
||||
.slots
|
||||
.values()
|
||||
.map(|res| &res.component)
|
||||
.chain(self.handles.values().map(|res| &res.component))
|
||||
.chain(self.atoms.values().map(|res| &res.component))
|
||||
.chain(self.forms.values().map(|form| &form.resource.component))
|
||||
{
|
||||
if !components.contains(component) {
|
||||
components.push(component.clone());
|
||||
}
|
||||
}
|
||||
components.sort();
|
||||
components
|
||||
}
|
||||
|
||||
fn syms(&self) -> String {
|
||||
@@ -306,9 +353,10 @@ fn insert_resource(
|
||||
kind: &str,
|
||||
symbol: String,
|
||||
name: String,
|
||||
component: String,
|
||||
keyed: bool,
|
||||
) -> io::Result<()> {
|
||||
let resource = make_resource(kind, symbol, name, keyed)?;
|
||||
let resource = make_resource(kind, symbol, name, component, keyed)?;
|
||||
match map.get(&resource.ident) {
|
||||
Some(existing) if existing.symbol != resource.symbol => Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
@@ -325,7 +373,13 @@ fn insert_resource(
|
||||
}
|
||||
}
|
||||
|
||||
fn make_resource(kind: &str, symbol: String, name: String, keyed: bool) -> io::Result<Resource> {
|
||||
fn make_resource(
|
||||
kind: &str,
|
||||
symbol: String,
|
||||
name: String,
|
||||
component: String,
|
||||
keyed: bool,
|
||||
) -> io::Result<Resource> {
|
||||
let ident = rust_ident(&name).ok_or_else(|| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
@@ -336,6 +390,7 @@ fn make_resource(kind: &str, symbol: String, name: String, keyed: bool) -> io::R
|
||||
Ok(Resource {
|
||||
symbol,
|
||||
ident,
|
||||
component,
|
||||
keyed,
|
||||
id,
|
||||
})
|
||||
@@ -371,6 +426,13 @@ fn static_attr(attrs: &[SurfaceAttribute], name: &str) -> Option<String> {
|
||||
.and_then(|attr| attr.value.clone())
|
||||
}
|
||||
|
||||
fn component_matches(res: &Resource, component: Option<&str>) -> bool {
|
||||
match component {
|
||||
Some(component) => res.component == component,
|
||||
None => true,
|
||||
}
|
||||
}
|
||||
|
||||
fn is_inside_keyed_for(surface: &SurfaceDocument, mut scope: ScopeId) -> bool {
|
||||
loop {
|
||||
let Some(current) = surface.scopes.get(scope.0 as usize) else {
|
||||
@@ -418,6 +480,20 @@ fn canonical_symbol(root: &Path, path: &Path, name: &str) -> String {
|
||||
format!("{}::{name}", rel.to_string_lossy().replace('\\', "/"))
|
||||
}
|
||||
|
||||
fn component_ident(root: &Path, path: &Path) -> io::Result<String> {
|
||||
let rel = path.strip_prefix(root).unwrap_or(path);
|
||||
let stem = rel
|
||||
.file_stem()
|
||||
.and_then(|stem| stem.to_str())
|
||||
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, format!("invalid template path `{}`", path.display())))?;
|
||||
rust_ident(stem).ok_or_else(|| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!("invalid template name `{stem}`; expected a Rust module identifier"),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn rust_ident(name: &str) -> Option<String> {
|
||||
let mut chars = name.chars();
|
||||
let first = chars.next()?;
|
||||
@@ -510,6 +586,8 @@ mod tests {
|
||||
assert!(generated.contains("pub mod forms"));
|
||||
assert!(generated.contains("pub mod atoms"));
|
||||
assert!(generated.contains("pub const filter"));
|
||||
assert!(generated.contains("pub mod todo"));
|
||||
assert!(generated.contains("pub const ALL_IDS"));
|
||||
|
||||
let syms = std::fs::read_to_string(out.join("slhx.syms")).unwrap();
|
||||
assert!(syms.contains("atom\t"));
|
||||
|
||||
Reference in New Issue
Block a user